为什么内存映射到 `STDOUT_FILENO` 失败了?

Why memory mapping to `STDOUT_FILENO` failed?

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

这里我想将虚拟内存地址space映射到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); 
}

为什么 mmap on STDOUT 在这里失败了? mmap on STDINSTDOUT 之间有什么区别?

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

#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 The underlying filesystem of the specified file does not support memory mapping.

实际上,文件描述符指向终端设备驱动程序,后者不允许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)

答案在此中解释。必须使用 read 访问权限打开文件才能映射,但 shell 启动的程序的标准输出打开 O_WRONLY。因此,mmap() 失败了。在Linux下,可以使用技巧。文件描述符是 /proc/pid/fd 目录中的符号 links。因此,可以用 O_RDWR[=57= 重新打开符号 link /proc/pid/fd/1 指向的文件] 标记并使用著名的 close()/dup() 技巧使文件描述符编号 1 (STDOUT_FILENO) 指向这个新打开的文件。需要调用 ftruncate() 以在文件中保留 space 并且需要 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$

区别不是 stdin vs stdout

它介于常规磁盘文件描述符和一些伪tty 文件描述符之间。参见 stat(2) (more precisely fstat) and inode(7)

作为常规文件(S_IFREG 类型)可以被 lseek(2) 编辑并且 Linux 内核在使用 mmap 获取虚拟内存页面时正在做一些等效的事情.

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

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

当然,你应该阅读mmap(2). It can fail, and you should use errno(3) or 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) on existing command-line programs to learn more about syscalls(2)