Linux内核中进程的Pagemap文件夹可以被读取(每次读取64位)有限次吗?

Posted

技术标签:

【中文标题】Linux内核中进程的Pagemap文件夹可以被读取(每次读取64位)有限次吗?【英文标题】:Can the Pagemap folder of processes in the Linux kernel be read(64bit per read) a finite number of times? 【发布时间】:2020-11-17 15:26:31 【问题描述】:

我正在尝试跟踪文件“proc/PID/pagemap”中每个物理页面的写入次数。但是该文件是二进制文件,文件属性中显示的大小为0,以及以下函数也读为 0。

    struct stat buf;
    int iRet = fstat(fd, &buf);
    if(iRet == -1)
    
       perror("fstat error");
       exit(-1);
    
    printf("the size of file is : %ld\n", buf.st_size);

我编写了一个监控程序,从进程的“pagemap”中读取一次 64 位数据并记录 55 位(软脏位)以检查是否写入了一页。当然在此之前我清除了所有软脏位在进程的pagemap中。该方法由linux内核提供,我在编码过程中的问题是当我使用文件描述符(也尝试过fstream指针)从pagemap中获取数据时。我对pagemap的读取仅在我正在监视的进程时结束完成了,好像文件是无限的。我知道进程的逻辑地址管理是动态的,但我想知道如何正确计算写入数。我应该在固定的时间间隔内读取这个无限文件的一部分吗?以及如何我应该读多少项目? T_T。

【问题讨论】:

/proc 中的大多数文件显示大小为 0,应忽略它们的大小。 /proc/$PID/pagemap 文件每页有 8 个字节的虚拟内存,但在 64 位机器上虚拟内存非常大。 “Linux 内核用户和管理员指南”的Examining Process Page Tables 部分提供了检查该文件的有用提示,例如:“此接口的高效用户将使用 /proc/pid/maps 来确定哪些内存区域实际被映射,并且 llseek 跳过未映射的区域。” 【参考方案1】:

你需要类似以下的东西:

#define _GNU_SOURCE
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

struct pagemap_region 
    struct pagemap_region *next;
    uintptr_t              addr;    /* First address within region */
    uintptr_t              ends;    /* First address after region */
    size_t                 pages;   /* Number of pages in this region */
    uint64_t               page[];  /* 64-bit pagemap flags per page */
;

static void free_pagemaps(struct pagemap_region *list)

    while (list) 
        struct pagemap_region *curr = list;

        list = curr->next;

        curr->addr = 0;
        curr->ends = 0;
        curr->pages = 0;
        free(curr);
    


struct pagemap_region *get_pagemaps(const pid_t pid)

    struct pagemap_region *list = NULL;
    size_t   page;
    char    *line_ptr = NULL;
    size_t   line_max = 256;
    ssize_t  line_len;
    FILE    *maps;
    int      n, fd;

    page = sysconf(_SC_PAGESIZE);

    /* We reuse this for the input line buffer. */
    line_ptr = malloc(line_max);
    if (!line_ptr) 
        errno = ENOMEM;
        return NULL;
    

    /* First, fill it with the path to the map pseudo-file. */
    if (pid > 0)
        n = snprintf(line_ptr, line_max, "/proc/%d/maps", (int)pid);
    else
        n = snprintf(line_ptr, line_max, "/proc/self/maps");
    if (n < 0 || (size_t)n + 1 >= line_max) 
        free(line_ptr);
        errno = EINVAL;
        return NULL;
    

    /* Read the maps pseudo-file. */
    maps = fopen(line_ptr, "re"); /* Read-only, close-on-exec */
    if (!maps) 
        free(line_ptr);
        errno = ESRCH;
        return NULL;
    
    while (1) 
        struct pagemap_region *curr;
        unsigned long  addr, ends;
        size_t pages;
        char *ptr, *end;

        line_len = getline(&line_ptr, &line_max, maps);
        if (line_len < 0)
            break;
 
        /* Start address of the region. */
        end = ptr = line_ptr;
        errno = 0;
        addr = strtoul(ptr, &end, 16);
        if (errno || end == ptr || *end != '-')
            break;

        /* End address of the region. */
        ptr = ++end;
        errno = 0;
        ends = strtoul(ptr, &end, 16);
        if (errno || end == ptr || *end != ' ')
            break;

        /* Number of pages in the region. */
        pages = (ends - addr) / page;
        if (addr + page * pages != ends || (addr % page) != 0)
            break;

        /* Allocate new region map. */
        curr = malloc(sizeof (struct pagemap_region) + pages * sizeof curr->page[0]);
        if (!curr)
            break;

        curr->addr = addr;
        curr->ends = ends;
        curr->pages = pages;

        /* Prepend to the region list. */
        curr->next = list;
        list = curr;
    

    /* Any issues when reading the maps pseudo-file? */
    if (!feof(maps) || ferror(maps)) 
        fclose(maps);
        free(line_ptr);
        free_pagemaps(list);
        errno = EIO;
        return NULL;
     else
    if (fclose(maps)) 
        free(line_ptr);
        free_pagemaps(list);
        errno = EIO;
        return NULL;
    

    /* Reuse the line buffer for the pagemap pseudo-file path */
    if (pid > 0) 
        n = snprintf(line_ptr, line_max, "/proc/%d/pagemap", (int)pid);
    else
        n = snprintf(line_ptr, line_max, "/proc/self/pagemap");
    if (n < 0 || (size_t)n + 1 >= line_max) 
        free(line_ptr);
        free_pagemaps(list);
        errno = ENOMEM;
        return NULL;
    
    do 
        fd = open(line_ptr, O_RDONLY | O_NOCTTY | O_CLOEXEC);
     while (fd == -1 && errno == EINTR);
    if (fd == -1) 
        n = errno;
        free(line_ptr);
        free_pagemaps(list);
        errno = n;
        return NULL;
    

    /* Path no longer needed. */
    free(line_ptr);
    line_ptr = NULL;
    line_max = 0;

    /* Read each pagemap section. */
    for (struct pagemap_region *curr = list; curr != NULL; curr = curr->next) 
        off_t          offset = (size_t)(curr->addr / page) * (sizeof curr->page[0]);
        unsigned char *ptr = (unsigned char *)&(curr->page[0]);
        size_t         need = curr->pages * sizeof curr->page[0];
        ssize_t        bytes;

        while (need > 0) 
            bytes = pread(fd, ptr, need, offset);
            if (bytes >= need)
                break;
            else
            if (bytes > 0) 
                ptr += bytes;
                offset += bytes;
                need -= bytes;
             else
            if (bytes == 0) 
                /* Assume this is a region we can't access, like [VSYSCALL]; clear the rest of the bits. */
                memset(ptr, 0, need);
                break;
             else
            if (bytes != -1 || errno != EINTR) 
                close(fd);
                free_pagemaps(list);
                errno = EIO;
                return NULL;
            
        
    
    if (close(fd) == -1) 
        free_pagemaps(list);
        errno = EIO;
        return NULL;
    

    return list;


int main(int argc, char *argv[])

    struct pagemap_region *list, *curr;
    long  pid;
    char *end;

    if (argc != 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) 
        const char *argv0 = (argc > 0 && argv && argv[1]) ? argv[1] : "(this)";
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv0);
        fprintf(stderr, "       %s PID\n", argv0);
        fprintf(stderr, "\n");
        fprintf(stderr, "This program prints the a map of the pages of process PID;\n");
        fprintf(stderr, "R for pages in RAM, S for pages in swap space, and . for others.\n");
        fprintf(stderr, "You can use -1 for the PID of this process itself.\n");
        fprintf(stderr, "\n");
        return EXIT_SUCCESS;
    

    end = argv[1];
    errno = 0;
    pid = strtol(argv[1], &end, 10);
    if (errno || end == argv[1] || *end) 
        fprintf(stderr, "%s: Invalid PID.\n", argv[1]);
        return EXIT_FAILURE;
    
    if (pid != -1 && (pid < 1 || (long)(pid_t)pid != pid)) 
        fprintf(stderr, "%s: Not a valid PID.\n", argv[1]);
        return EXIT_FAILURE;
            
    list = get_pagemaps(pid);
    if (!list) 
        fprintf(stderr, "%s.\n", strerror(errno));
        return EXIT_FAILURE;
    
    for (curr = list; curr != NULL; curr = curr->next)        
        printf("Region %p - %p: %zu pages\n", (void *)(curr->addr), (void *)(curr->ends), curr->pages);
        for (uint64_t *map = curr->page; map < curr->page + curr->pages; map++) 
            if ((*map >> 63) & 1)
                putchar('R');
            else
            if ((*map >> 62) & 1)
                putchar('S');
            else
                putchar('.');
        
        putchar('\n');
    

    return EXIT_SUCCESS;

我们逐行读取/proc/PID/maps,并为每个构造一个struct pagemap_region;这包含起始地址、结束地址和区域中的页数。 (不过,我没有费心去支持大页面;如果你这样做了,请考虑解析/proc/PID/smaps。如果一行以0-9 或小写a-f 开头,它指定一个区域;否则该行以大写字母 A-Z 开头并指定该区域的属性。)

每个struct pagemap_region 还包含用于每页 64 位页面映射值的空间。在找到/选择区域之后——这个尝试所有——,/proc/PID/pagemap文件被打开,并使用pread()从正确的位置读取相应的数据,它的工作方式类似于read(),但也获取文件偏移量作为一个额外的参数。

并非所有区域都可以访问。我确实相信[VSYSCALL] 就是其中之一,但作为一个内核用户空间接口,它的页面映射位无论如何都是无趣的。以上只是将这些位清除为零,而不是从列表中删除这些区域。

这并不是作为“完全按照这个方法做,只需复制并粘贴这个”的答案,而是作为如何开始解决这个问题的建议,也许会进行一些探索,将结果或行为与您的特定需求进行比较;一种粗略的大纲,仅用于初步建议。

另外,因为我是一口气写完的,它很可能有讨厌的错误。 (如果我知道在哪里或确定,我会修复它们;只是错误发生了。)

【讨论】:

非常感谢您的回答,这里真的有很多有趣的知识,我学习了一段时间后完成了我的程序,它已经完成了从一个进程到另一个进程的监控任务监控物理页统计的读写次数,以后有其他问题我们也可以交流!!

以上是关于Linux内核中进程的Pagemap文件夹可以被读取(每次读取64位)有限次吗?的主要内容,如果未能解决你的问题,请参考以下文章

linux管道pipe详解

linux 在 /proc 里实现文件

3.1 进程间通信之管道

在 /proc 里实现文件

linux 使用 /proc 文件系统

利用/proc/pid/pagemap将虚拟地址转换为物理地址