【问题标题】:Why memory mapping to `STDOUT_FILENO` failed?为什么内存映射到“STDOUT_FILENO”失败?
【发布时间】:2021-12-31 07:20:03
【问题描述】:

我已经编写了一些代码来测试mmap 系统调用。

这里我想将虚拟内存地址空间映射到STDOUT,并通过mmap返回的指针ptr打印一个字符串。

int main()
{
    void *ptr = mmap(NULL, 1024, PROT_WRITE | PROT_READ, MAP_PRIVATE, STDOUT_FILENO, 0);
    memcpy(ptr, "hello", 6);
}

但是这段代码失败了:

$ gcc mmap.c
$ ./a.out
Segmentation fault (core dumped)

我已经在STDIN上测试了mmap,没问题。

int main()
{
    // executed by `./a.out < text.txt`
    void *ptr = mmap(NULL, 1024, PROT_WRITE | PROT_READ, MAP_PRIVATE, STDIN_FILENO, 0);
    write(STDOUT_FILENO, ptr, 1024); 
}

为什么STDOUT 上的mmap 在这里失败了?以及STDIN 上的mmapSTDOUT 之间有什么区别?

【问题讨论】:

    标签: c linux mmap


    【解决方案1】:

    作为一般规则,当您使用服务时,请检查错误代码以缩小问题范围。 重写你的程序如下:

    #include <errno.h>
    #include <stdio.h>
    #include <sys/mman.h>
    #include <string.h>
    #include <unistd.h>
    
    int main(void)
    {
        void *ptr = mmap(NULL, 1024, PROT_WRITE | PROT_READ, MAP_PRIVATE, STDOUT_FILENO, 0);
        if (MAP_FAILED == ptr) {
          fprintf(stderr, "mmap(): error '%m' (%d)\n", errno);
          return 1;
        } else {
          memcpy(ptr, "hello", 6);
        }
    
        return 0;
    }
    

    程序显示如下:

    $ ./a.out
    mmap(): error 'No such device' (19)
    

    也就是说你得到了ENODEV错误码。查看manual,得到如下解释:

    ENODEV 指定文件的底层文件系统不支持内存映射。

    其实文件描述符指向的是终端设备驱动,而后者不允许mmap()操作。

    关于第二个程序映射STDIN_FILENO,当你这样运行它时:

    $ ./a.out < text.txt
    

    前面使 STDIN_FILENO 成为 text.txt 文件的“点”,而不是输入终端。因此,mmap() 在这里工作......

    因此,基于相同的想法,我们希望第一个程序适用于:

    $ ./a.out > foo.txt
    

    因为输出不再是终端而是文件。但是你得到 EACCES 错误:

    $ ./a.out > foo.txt
    mmap(): error 'Permission denied' (13)
    

    post 中解释了答案。必须使用 read 访问权限打开文件才能进行映射,但打开由 shell 启动的程序的标准输出 O_WRONLY。因此,mmap() 失败。在 Linux 下,可以使用一个技巧。文件描述符是 /proc/pid/fd 目录中的符号链接。因此,可以使用 O_RDWR 标志重新打开符号链接 /proc/pid/fd/1 指向的文件,并使用著名的 close()/dup() 技巧来制作文件描述符编号 1 (STDOUT_FILENO) 指向这个新打开的文件。需要调用 ftruncate() 来保留文件中的空间,并且需要 MAP_SHARED 标志才能使对其他进程的修改可见(例如,程序终止时的 shell):

    #include <errno.h>
    #include <stdio.h>
    #include <sys/mman.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    
    int main(void)
    {
        char symlink[256];
        int fd;
    
        snprintf(symlink, sizeof(symlink), "/proc/%d/fd/1", getpid());
        fd = open(symlink, O_RDWR);
        if (fd < 0) {
          fprintf(stderr, "fopen(): error '%m' (%d)\n", errno);
          return 1;
        }
        close(STDOUT_FILENO);
        dup(fd);
        close(fd);
        ftruncate(STDOUT_FILENO, 1024);
        void *ptr = mmap(NULL, 1024, PROT_WRITE | PROT_READ, MAP_SHARED, STDOUT_FILENO, 0);
        if (MAP_FAILED == ptr) {
          fprintf(stderr, "mmap(%d): error '%m' (%d)\n", fd, errno);
          return 1;
        } else {
          memcpy(ptr, "hello", 6);
        }
    
        return 0;
    }
    

    因此,你得到了你所期望的:

    $ ./a.out
    mmap(3`): error 'No such device' (19) # The output is the terminal (mmap() forbidden)
    $ ./a.out > foo.txt   # The output is a file
    $ cat foo.txt
    hello$
    

    【讨论】:

    • 著名的 close()/dup() 技巧 引入了竞争条件。不能保证新的文件描述符值会相同——另一个线程甚至信号处理程序——因为所有open()pipe()openat()pipe() 都是异步信号安全的并且可以使用在信号处理程序中 - 可以打开文件并“窃取”文件描述符值。
    • Use dup2() instead: "dup2() 函数将导致文件描述符 fildes2 引用与文件描述符 fildes 相同的打开文件描述并共享任何锁,并返回fildes2。如果fildes2已经是一个有效的打开文件描述符,那么应该先关闭它”
    【解决方案2】:

    区别不是stdin vs stdout

    它介于普通磁盘文件描述符和一些伪 tty 文件描述符之间。请参阅stat(2)(更准确地说是fstat)和inode(7)

    作为常规文件(S_IFREG 类型)可以是lseek(2)-ed,Linux 内核在使用mmap 获取虚拟内存页面时会做一些等效的操作。

    套接字或伪 tty 不能是 lseek-ed。

    您可以尝试echo foo | a.out,它也会失败,因为pipe(7) 不能是lseek-ed。

    当然,你应该阅读mmap(2)的文档。它可能会失败,您应该在失败时使用errno(3)perror(3)

    所以代码:

    void *ptr = 
       mmap(NULL, 1024, PROT_WRITE | PROT_READ, 
            MAP_PRIVATE, STDIN_FILENO, 0);
    if (ptr == MMAP_FAILED) 
       { perror("mmap"); exit(EXIT_FAILURE); }
    

    在现有的命令行程序上尝试strace(1) 以了解有关syscalls(2) 的更多信息

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-06-24
      • 2020-05-20
      • 1970-01-01
      • 2011-04-22
      • 2015-05-20
      • 2015-02-24
      • 2016-10-04
      相关资源
      最近更新 更多