代码之家  ›  专栏  ›  技术社区  ›  Ismail Badawi

mmap内存由其他内存支持?

  •  0
  • Ismail Badawi  · 技术社区  · 3 年前

    我不确定这个问题是否有意义,但假设我有一个指向记忆的指针:

    char *mem;
    size_t len;
    

    是否有可能以某种方式映射以下内容 mem 作为只读映射到另一个地址?即,我想获得一个指针 mem2 这样 mem2 != mem 以及访问 mem2[i] 实际阅读 mem[i] (无需复制)。

    我的最终目标是获取非连续的内存块,并通过将它们映射到彼此旁边来使它们看起来是连续的。

    我考虑的一种方法是使用 fmemopen 然后 mmap ,但没有与结果关联的文件描述符 fmemopen .

    0 回复  |  直到 3 年前
        1
  •  4
  •   Marco Bonelli    3 年前

    一般情况-无法控制第一次映射

    /proc/[PID]/pagemap + /dev/mem

    我能想到的唯一办法 不进行任何复制 是通过手动打开和检查 /proc/[PID]/pagemap 获取要“别名”的页面对应的物理页面的页面帧号,然后打开并映射 /dev/mem 在相应的偏移处。虽然这在理论上可行,但它需要root权限,而且最有可能 在任何合理的Linux发行版上都是可能的,因为内核通常配置为 CONFIG_STRICT_DEVMEM=y 这对使用施加了严格的限制 /dev/mem 例如,在x86上,它不允许从 /dev/mem (仅允许读取内存映射的PCI区域)。请注意,为了使此功能正常工作,需要锁定您想要“别名”的页面以将其保存在RAM中。

    无论如何,这里有一个例子,说明这将如何工作 如果 你能够/愿意这样做(我在这里假设x86 64位):

    #include <stdio.h>
    #include <errno.h>
    #include <limits.h>
    #include <sys/mman.h>
    #include <unistd.h>
    #include <fcntl.h>
    
    /* Get the physical address of an existing virtual memory page and map it. */
    
    int main(void) {
        FILE *fp;
        char *endp;
        unsigned long addr, info, physaddr, val;
        long off;
        int fd;
        void *mem;
        void *orig_mem;
    
        // Suppose that this is the existing page you want to "alias"
        orig_mem = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);
        if (orig_mem == MAP_FAILED) {
            perror("mmap orig_mem failed");
            return 1;
        }
    
        // Write a dummy value just for testing
        *(unsigned long *)orig_mem = 0x1122334455667788UL;
    
        // Lock the page to prevent it from being swapped out
        if (mlock(orig_mem, 0x1000)) {
            perror("mlock orig_mem failed");
            return 1;
        }
    
        fp = fopen("/proc/self/pagemap", "rb");
        if (!fp) {
            perror("Failed to open \"/proc/self/pagemap\"");
            return 1;
        }
    
        addr = (unsigned long)orig_mem;
        off  = addr / 0x1000 * 8;
    
        if (fseek(fp, off, SEEK_SET)) {
            perror("fseek failed");
            return 1;
        }
    
        // Get its information from /proc/self/pagemap
        if (fread(&info, sizeof(info), 1, fp) != 1) {
            perror("fread failed");
            return 1;
        }
    
        physaddr = (info & ((1UL << 55) - 1)) << 12;
    
        printf("Value: %016lx\n", info);
        printf("Physical address: 0x%016lx\n", physaddr);
    
        // Ensure page is in RAM, should be true since it was mlock'd
        if (!(info & (1UL << 63))) {
            fputs("Page is not in RAM? Strange! Aborting.\n", stderr);
            return 1;
        }
    
        fd = open("/dev/mem", O_RDONLY);
        if (fd == -1) {
            perror("open(\"/dev/mem\") failed");
            return 1;
        }
    
        mem = mmap(NULL, 0x1000, PROT_READ, MAP_PRIVATE|MAP_ANONYMOUS, fd, physaddr);
        if (mem == MAP_FAILED) {
            perror("Failed to mmap \"/dev/mem\"");
            return 1;
        }
    
        // Now `mem` is effecively referring to the same physical page that
        // `orig_mem` refers to.
    
        // Try reading 8 bytes (note: this will just return 0 if
        // CONFIG_STRICT_DEVMEM=y).
        val = *(unsigned long *)mem;
    
        printf("Read 8 bytes at physaddr 0x%016lx: %016lx\n", physaddr, val);
    
        return 0;
    }
    

    userfaultfd(2)

    除了我上面描述的,AFAIK没有一种方法可以在不复制的情况下从用户空间中做你想做的事情。也就是说,没有一种方法可以简单地告诉内核“将第二个虚拟地址映射到现有地址的同一内存中”。但是,您可以通过注册用户空间处理程序来处理页面错误 userfaultfd(2) 系统调用和 ioctl_userfaultfd(2) ,我认为这是你最好的投篮。

    整个机制类似于内核对真实内存页的处理,只是故障由用户定义的用户空间处理程序线程处理。这仍然是一个实际的副本,但对故障线程来说是原子性的,并为您提供了更多的控制。一般来说,它也可能表现得更好,因为复制是由您控制的,因此只能完成 如果/何时 需要(即在第一次读取故障时),而在正常情况下 mmap +复制你 总是 无论页面以后是否会被访问,都要进行复制。

    手册页中有一个很好的示例程序 用户错误fd(2) 我在上面链接了它,所以我不打算复制粘贴到这里。它处理一个或多个页面,应该让您了解整个API。

    更简单的案例-对第一个映射的控制

    如果你 控制您想要“别名”的第一个映射,然后您可以简单地创建一个共享映射。你要找的是 memfd_create(2) 。您可以使用它创建一个匿名文件,然后可以 mmap 使用不同权限多次执行。

    这里有一个简单的例子:

    #define _GNU_SOURCE
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/mman.h>
    #include <sys/types.h>
    
    int main(void) {
            int memfd;
            void *mem_ro, *mem_rw;
    
            // Create a memfd
            memfd = memfd_create("something", 0);
            if (memfd == -1) {
                    perror("memfd_create failed");
                    return 1;
            }
    
            // Give the file a size, otherwise reading/writing will fail
            if (ftruncate(memfd, 0x1000) == -1) {
                    perror("ftruncate failed");
                    return 1;
            }
    
            // Map the fd as read only and private
            mem_ro = mmap(NULL, 0x1000, PROT_READ, MAP_PRIVATE, memfd, 0);
            if (mem_ro == MAP_FAILED) {
                    perror("mmap failed");
                    return 1;
            }
    
            // Map the fd as read/write and shared (shared is needed if we want
            // write operations to be propagated to the other mappings)
            mem_rw = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED, memfd, 0);
            if (mem_rw == MAP_FAILED) {
                    perror("mmap failed");
                    return 1;
            }
    
            printf("ro mapping @ %p\n", mem_ro);
            printf("rw mapping @ %p\n", mem_rw);
    
            // This write can now be read from both mem_ro and mem_rw
            *(char *)mem_rw = 123;
    
            // Test reading
            printf("read from ro mapping: %d\n", *(char *)mem_ro);
            printf("read from rw mapping: %d\n", *(char *)mem_rw);
    
            return 0;
    }