为什么我不能 mmap /proc/self/maps?
Why can I not mmap /proc/self/maps?
具体来说:为什么我可以这样做:
FILE *fp = fopen("/proc/self/maps", "r");
char buf[513]; buf[512] = NULL;
while(fgets(buf, 512, fp) > NULL) printf("%s", buf);
但不是这个:
int fd = open("/proc/self/maps", O_RDONLY);
struct stat s;
fstat(fd, &s); // st_size = 0 -> why?
char *file = mmap(0, s.st_size /*or any fixed size*/, PROT_READ, MAP_PRIVATE, fd, 0); // gives EINVAL for st_size (because 0) and ENODEV for any fixed block
write(1, file, st_size);
我知道 /proc 文件不是真正的文件,但它似乎为 FILE* 版本定义了一些大小和内容。它是在运行中秘密生成它以供阅读之类的吗?我在这里错过了什么?
编辑:
正如我可以清楚地从他们那里读取(),有没有办法获得可能的可用字节?还是我一直读到 EOF?
proc
“文件”并不是真正的文件,它们只是可以 read/written 来自的流,但它们不包含您可以映射到的内存中的物理数据。
https://tldp.org/LDP/Linux-Filesystem-Hierarchy/html/proc.html
它们是在您阅读时即时创建的。也许这会有所帮助,这是一个展示如何实现 proc 文件的教程:
https://devarea.com/linux-kernel-development-creating-a-proc-file-and-interfacing-with-user-space/
tl;dr:你给它一个名字并读写处理程序,就是这样。从内核开发人员的角度来看,Proc 文件应该非常容易实现。不过,它们的行为不像功能齐全的文件。
关于bonus question,好像没有办法显示文件大小,只有EOF on reading。
正如其他人已经解释的那样,/proc
和 /sys
是伪文件系统,由内核提供的数据组成,在读取之前并不存在 – 内核生成数据然后那里。由于大小不同,并且在打开文件进行读取之前是未知的,因此根本不会提供给用户空间。
然而,这并不“不幸”。同样的情况经常发生,例如字符设备(在 /dev
下)、管道、FIFO(命名管道)和套接字。
我们可以简单地编写一个辅助函数来完全读取伪文件,使用动态内存管理。例如:
// SPDX-License-Identifier: CC0-1.0
//
#define _POSIX_C_SOURCE 200809L
#define _ATFILE_SOURCE
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
/* For example main() */
#include <stdio.h>
/* Return a directory handle for a specific relative directory.
For absolute paths and paths relative to current directory, use dirfd==AT_FDCWD.
*/
int at_dir(const int dirfd, const char *dirpath)
{
if (dirfd == -1 || !dirpath || !*dirpath) {
errno = EINVAL;
return -1;
}
return openat(dirfd, dirpath, O_DIRECTORY | O_PATH | O_CLOEXEC);
}
/* Read the (pseudofile) contents to a dynamically allocated buffer.
For absolute paths and paths relative to current durectory, use dirfd==AT_FDCWD.
You can safely initialize *dataptr=NULL,*sizeptr=0 for dynamic allocation,
or reuse the buffer from a previous call or e.g. getline().
Returns 0 with errno set if an error occurs. If the file is empty, errno==0.
In all cases, remember to free (*dataptr) after it is no longer needed.
*/
size_t read_pseudofile_at(const int dirfd, const char *path, char **dataptr, size_t *sizeptr)
{
char *data;
size_t size, have = 0;
ssize_t n;
int desc;
if (!path || !*path || !dataptr || !sizeptr) {
errno = EINVAL;
return 0;
}
/* Existing dynamic buffer, or a new buffer? */
size = *sizeptr;
if (!size)
*dataptr = NULL;
data = *dataptr;
/* Open pseudofile. */
desc = openat(dirfd, path, O_RDONLY | O_CLOEXEC | O_NOCTTY);
if (desc == -1) {
/* errno set by openat(). */
return 0;
}
while (1) {
/* Need to resize buffer? */
if (have >= size) {
/* For pseudofiles, linear size growth makes most sense. */
size = (have | 4095) + 4097 - 32;
data = realloc(data, size);
if (!data) {
close(desc);
errno = ENOMEM;
return 0;
}
*dataptr = data;
*sizeptr = size;
}
n = read(desc, data + have, size - have);
if (n > 0) {
have += n;
} else
if (n == 0) {
break;
} else
if (n == -1) {
const int saved_errno = errno;
close(desc);
errno = saved_errno;
return 0;
} else {
close(desc);
errno = EIO;
return 0;
}
}
if (close(desc) == -1) {
/* errno set by close(). */
return 0;
}
/* Append zeroes - we know size > have at this point. */
if (have + 32 > size)
memset(data + have, 0, 32);
else
memset(data + have, 0, size - have);
errno = 0;
return have;
}
int main(void)
{
char *data = NULL;
size_t size = 0;
size_t len;
int selfdir;
selfdir = at_dir(AT_FDCWD, "/proc/self/");
if (selfdir == -1) {
fprintf(stderr, "/proc/self/ is not available: %s.\n", strerror(errno));
exit(EXIT_FAILURE);
}
len = read_pseudofile_at(selfdir, "status", &data, &size);
if (errno) {
fprintf(stderr, "/proc/self/status: %s.\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("/proc/self/status: %zu bytes\n%s\n", len, data);
len = read_pseudofile_at(selfdir, "maps", &data, &size);
if (errno) {
fprintf(stderr, "/proc/self/maps: %s.\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("/proc/self/maps: %zu bytes\n%s\n", len, data);
close(selfdir);
free(data); data = NULL; size = 0;
return EXIT_SUCCESS;
}
上面的示例程序将目录描述符(“atfile handle”)打开到 /proc/self
。 (这样你就不需要连接字符串来构造路径。)
然后读取/proc/self/status的内容。如果成功,它会显示其大小(以字节为单位)及其内容。
接下来,它读取/proc/self/maps的内容,重新使用之前的缓冲区。如果成功,它还会显示其大小和内容。
最后,目录描述符因为不再需要而被关闭,动态分配的缓冲区被释放。
请注意,执行 free(NULL)
和丢弃 read_pseudofile_at()
调用之间的动态缓冲区 (free(data); data=NULL; size=0;
) 是完全安全的。
因为伪文件通常很小,read_pseudofile_at()
使用线性动态缓冲区增长策略。如果没有先前的缓冲区,它从 8160 字节开始,然后增加 4096 字节直到足够大。随意用你喜欢的任何增长策略替换它,这只是一个例子,但在实践中效果很好,不会浪费太多内存。
具体来说:为什么我可以这样做:
FILE *fp = fopen("/proc/self/maps", "r");
char buf[513]; buf[512] = NULL;
while(fgets(buf, 512, fp) > NULL) printf("%s", buf);
但不是这个:
int fd = open("/proc/self/maps", O_RDONLY);
struct stat s;
fstat(fd, &s); // st_size = 0 -> why?
char *file = mmap(0, s.st_size /*or any fixed size*/, PROT_READ, MAP_PRIVATE, fd, 0); // gives EINVAL for st_size (because 0) and ENODEV for any fixed block
write(1, file, st_size);
我知道 /proc 文件不是真正的文件,但它似乎为 FILE* 版本定义了一些大小和内容。它是在运行中秘密生成它以供阅读之类的吗?我在这里错过了什么?
编辑: 正如我可以清楚地从他们那里读取(),有没有办法获得可能的可用字节?还是我一直读到 EOF?
proc
“文件”并不是真正的文件,它们只是可以 read/written 来自的流,但它们不包含您可以映射到的内存中的物理数据。
https://tldp.org/LDP/Linux-Filesystem-Hierarchy/html/proc.html
它们是在您阅读时即时创建的。也许这会有所帮助,这是一个展示如何实现 proc 文件的教程:
https://devarea.com/linux-kernel-development-creating-a-proc-file-and-interfacing-with-user-space/
tl;dr:你给它一个名字并读写处理程序,就是这样。从内核开发人员的角度来看,Proc 文件应该非常容易实现。不过,它们的行为不像功能齐全的文件。
关于bonus question,好像没有办法显示文件大小,只有EOF on reading。
正如其他人已经解释的那样,/proc
和 /sys
是伪文件系统,由内核提供的数据组成,在读取之前并不存在 – 内核生成数据然后那里。由于大小不同,并且在打开文件进行读取之前是未知的,因此根本不会提供给用户空间。
然而,这并不“不幸”。同样的情况经常发生,例如字符设备(在 /dev
下)、管道、FIFO(命名管道)和套接字。
我们可以简单地编写一个辅助函数来完全读取伪文件,使用动态内存管理。例如:
// SPDX-License-Identifier: CC0-1.0
//
#define _POSIX_C_SOURCE 200809L
#define _ATFILE_SOURCE
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
/* For example main() */
#include <stdio.h>
/* Return a directory handle for a specific relative directory.
For absolute paths and paths relative to current directory, use dirfd==AT_FDCWD.
*/
int at_dir(const int dirfd, const char *dirpath)
{
if (dirfd == -1 || !dirpath || !*dirpath) {
errno = EINVAL;
return -1;
}
return openat(dirfd, dirpath, O_DIRECTORY | O_PATH | O_CLOEXEC);
}
/* Read the (pseudofile) contents to a dynamically allocated buffer.
For absolute paths and paths relative to current durectory, use dirfd==AT_FDCWD.
You can safely initialize *dataptr=NULL,*sizeptr=0 for dynamic allocation,
or reuse the buffer from a previous call or e.g. getline().
Returns 0 with errno set if an error occurs. If the file is empty, errno==0.
In all cases, remember to free (*dataptr) after it is no longer needed.
*/
size_t read_pseudofile_at(const int dirfd, const char *path, char **dataptr, size_t *sizeptr)
{
char *data;
size_t size, have = 0;
ssize_t n;
int desc;
if (!path || !*path || !dataptr || !sizeptr) {
errno = EINVAL;
return 0;
}
/* Existing dynamic buffer, or a new buffer? */
size = *sizeptr;
if (!size)
*dataptr = NULL;
data = *dataptr;
/* Open pseudofile. */
desc = openat(dirfd, path, O_RDONLY | O_CLOEXEC | O_NOCTTY);
if (desc == -1) {
/* errno set by openat(). */
return 0;
}
while (1) {
/* Need to resize buffer? */
if (have >= size) {
/* For pseudofiles, linear size growth makes most sense. */
size = (have | 4095) + 4097 - 32;
data = realloc(data, size);
if (!data) {
close(desc);
errno = ENOMEM;
return 0;
}
*dataptr = data;
*sizeptr = size;
}
n = read(desc, data + have, size - have);
if (n > 0) {
have += n;
} else
if (n == 0) {
break;
} else
if (n == -1) {
const int saved_errno = errno;
close(desc);
errno = saved_errno;
return 0;
} else {
close(desc);
errno = EIO;
return 0;
}
}
if (close(desc) == -1) {
/* errno set by close(). */
return 0;
}
/* Append zeroes - we know size > have at this point. */
if (have + 32 > size)
memset(data + have, 0, 32);
else
memset(data + have, 0, size - have);
errno = 0;
return have;
}
int main(void)
{
char *data = NULL;
size_t size = 0;
size_t len;
int selfdir;
selfdir = at_dir(AT_FDCWD, "/proc/self/");
if (selfdir == -1) {
fprintf(stderr, "/proc/self/ is not available: %s.\n", strerror(errno));
exit(EXIT_FAILURE);
}
len = read_pseudofile_at(selfdir, "status", &data, &size);
if (errno) {
fprintf(stderr, "/proc/self/status: %s.\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("/proc/self/status: %zu bytes\n%s\n", len, data);
len = read_pseudofile_at(selfdir, "maps", &data, &size);
if (errno) {
fprintf(stderr, "/proc/self/maps: %s.\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("/proc/self/maps: %zu bytes\n%s\n", len, data);
close(selfdir);
free(data); data = NULL; size = 0;
return EXIT_SUCCESS;
}
上面的示例程序将目录描述符(“atfile handle”)打开到 /proc/self
。 (这样你就不需要连接字符串来构造路径。)
然后读取/proc/self/status的内容。如果成功,它会显示其大小(以字节为单位)及其内容。
接下来,它读取/proc/self/maps的内容,重新使用之前的缓冲区。如果成功,它还会显示其大小和内容。
最后,目录描述符因为不再需要而被关闭,动态分配的缓冲区被释放。
请注意,执行 free(NULL)
和丢弃 read_pseudofile_at()
调用之间的动态缓冲区 (free(data); data=NULL; size=0;
) 是完全安全的。
因为伪文件通常很小,read_pseudofile_at()
使用线性动态缓冲区增长策略。如果没有先前的缓冲区,它从 8160 字节开始,然后增加 4096 字节直到足够大。随意用你喜欢的任何增长策略替换它,这只是一个例子,但在实践中效果很好,不会浪费太多内存。