为什么内存映射到 `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 STDIN
和 STDOUT
之间有什么区别?
作为一般规则,当您使用服务时,检查错误代码以缩小问题范围。
按如下方式重写您的程序:
#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)
我写了一些代码来测试 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 STDIN
和 STDOUT
之间有什么区别?
作为一般规则,当您使用服务时,检查错误代码以缩小问题范围。 按如下方式重写您的程序:
#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)
答案在此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)