Bootstrap

mmap 文件映射

🌈 个人主页:Zfox_
🔥 系列专栏:Linux

一:🔥 mmap介绍

NAME
       mmap, munmap - map or unmap files or devices into memory

SYNOPSIS
       #include <sys/mman.h>

       void *mmap(void *addr, size_t length, int prot, int flags,
                      int fd, off_t offset);
                      
       int munmap(void *addr, size_t length);

🦋 基本说明

  • 🧑‍💻 允许用户空间程序将文件或设备的内容直接映射到进程的虚拟地址空间中。通过 mmap ,程序可以高效地访问文件数据,而无需通过传统的 read 或 write 系统调用进行数据的复制
  • 🧑‍💻 mmap 还可以用于实现共享内存,允许不同进程间共享数据
    在这里插入图片描述

🦋 参数介绍

💻

  • 🌾 🦁 void *addr 一个提示地址,表示希望映射区域开始的地址。然而,这个地址可能会被内核忽略,特别是当我们没有足够的权限来请求特定的地址时。如果 addr 是 NULL,则系统会自动选择一个合适的地址。
  • size_t length要映射到进程地址空间中的字节数。这个长度必须是系统页面大小的整数倍(通常是 4KB,但可能因系统而异)。如果指定的 length 不是页面大小的整数倍,系统可能会向上舍入到最近的页面大小边界(系统内存页大小为 4KB(即 4096 字节),而请求的内存大小为 3500 字节,则按照向上舍入的原则,应分配 4096 字节的内存)。
  • int prot指定了映射区域的内存保护属性。可以是以下值的组合(使用按位或运算符 |):
    • PROT_READ映射区域可读。
    • PROT_WRITE映射区域可写。
    • PROT_EXEC映射区域可执行。
  • int flags指定了映射的类型和其他选项:
    • MAP_PRIVATE创建一个私有映射。对映射区域的修改不会反映到底层文件中。
    • MAP_SHARED创建一个共享映射。对映射区域的修改会反映到底层文件中(前提是文件是以写方式打开的,并且文件系统支持这种操作)。
    • 其他选项(如 MAP_ANONYMOUS、MAP_ANONYMOUS_SHARED 等)可能也存在于某些系统上,用于创建不与文件关联的匿名映射。
The flags argument
       The flags argument determines whether updates to the mapping are visible to other processes mapping the same region, and whether updates are carried through to the underlying file.  This behavior is determined by including exactly one of the following values in flags:

       MAP_SHARED
              Share this mapping.  Updates to the mapping are visible to other processes mapping the same region, and (in the case of file-backed mappings) are carried through to the underlying  file.
              (To precisely control when updates are carried through to the underlying file requires the use of msync(2).)

       MAP_SHARED_VALIDATE (since Linux 4.15)
              This  flag  provides  the  same behavior as MAP_SHARED except that MAP_SHARED mappings ignore unknown flags in flags.  By contrast, when creating a mapping using MAP_SHARED_VALIDATE, the
              kernel verifies all passed flags are known and fails the mapping with the error EOPNOTSUPP for unknown flags.  This mapping type is also required to be able to  use  some  mapping  flags
              (e.g., MAP_SYNC).

       MAP_PRIVATE
              Create  a  private copy-on-write mapping.  Updates to the mapping are not visible to other processes mapping the same file, and are not carried through to the underlying file.  It is un‐
              specified whether changes made to the file after the mmap() call are visible in the mapped region.
  • int fd : 一个有效的文件描述符,指向要映射的文件或设备。对于匿名映射,这个参数可以是 -1 (在某些系统上,也可以使用 MAP_ANONYMOUS 或 MAP_ANON 标志来指定匿名映射,此时 fd 参数会被忽略)
  • off_t offset : 文件中的起始偏移量,即映射区域的开始位置。offset 和 length 一起定义了映射区域在文件中的位置和大小。

🦋 返回值

RETURN VALUE
       On success, mmap() returns a pointer to the mapped area.  On error, the value MAP_FAILED (that is, (void *) -1) is returned, and errno is set to indicate the cause of the error.

       On success, munmap() returns 0.  On failure, it returns -1, and errno is set to indicate the cause of the error (probably to EINVAL).

二:🔥 demo代码

🧑‍💻 引入一个手动调整文件大小的系统调用
在这里插入图片描述

🦋 写入映射

#include <iostream>
#include <string>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

#define PAGE_SIZE 4096

// write_map filename
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cout << "Usage: " << argv[0] << " filename" << std::endl;
        return 1;
    }

    // 1. 打开目标文件,mmap 需要你自己先打开文件
    int fd = ::open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(fd < 0)
    {
        std::cout << "Failed to open file: " << argv[1] << std::endl;
        return 2;
    }

    // 2. 我们要手动调整一下文件的大小,方便进行合法映射,用0填充
    if(ftruncate(fd, PAGE_SIZE) == -1)
    {
        std::cout << "Failed to ftruncate file: " << argv[1] << std::endl;
        return 3;
    }

    // 3. 文件映射
    char *shmaddr = (char*)::mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(shmaddr == MAP_FAILED)
    {
        std::cout << "Failed to mmap file: " << argv[1] << std::endl;
        return 4;
    }

    // 4. 进行文件操作
    for(char c = 'a'; c <= 'z'; c++)
    {
        shmaddr[c - 'a'] = c;
        sleep(1);
    }

    // 5. 关闭映射
    if(::munmap(shmaddr, PAGE_SIZE) == -1)
    {
        std::cout << "Failed to munmap file: " << argv[1] << std::endl;
        return 5;
    }

    ::close(fd);

    return 0;
}

💻 运行:

root@# ./a.out log.txt
^C
root@# ll
total 44
drwxr-xr-x 2 root root  4096 Feb  8 23:35 ./
drwxr-xr-x 7 root root  4096 Feb  8 20:58 ../
-rwxr-xr-x 1 root root 16832 Feb  8 23:35 a.out*
-rw-r--r-- 1 root root  4096 Feb  8 23:35 log.txt
-rw-r--r-- 1 root root  1409 Feb  8 21:38 WriteMmap.cc

🧑‍💻 这里我们可以看到 log.txt 文件的大小正是 4096,剩余的用 null 填充,文件内容也是我们写入的 a - z
在这里插入图片描述

🦋 读取映射

#include <iostream>
#include <string>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

// write_map filename
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cout << "Usage: " << argv[0] << " filename" << std::endl;
        return 1;
    }

    // 1. 打开目标文件,mmap 需要你自己先打开文件
    int fd = ::open(argv[1], O_RDONLY);
    if(fd < 0)
    {
        std::cout << "Failed to open file: " << argv[1] << std::endl;
        return 2;
    }

    // 2. 获取文件大小
    struct stat st;
    ::fstat(fd, &st);


    // 3. 文件映射
    char *shmaddr = (char*)::mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
    if(shmaddr == MAP_FAILED)
    {
        std::cout << "Failed to mmap file: " << argv[1] << std::endl;
        return 4;
    }

    // 4. 进行文件操作
    std::cout << "File content: " << shmaddr << std::endl;

    // 5. 关闭映射
    if(::munmap(shmaddr, st.st_size) == -1)
    {
        std::cout << "Failed to munmap file: " << argv[1] << std::endl;
        return 5;
    }

    ::close(fd);

    return 0;
}

💻 运行:
此时我们读取刚才写入文件的内容 a-z:

root@# ./a.out log.txt
File content: abcdefghijklmnopqrstuvwxyz

三:🔥 极简模拟实现 malloc

🧑‍💻 在内存映射中,MAP_PRIVATE 和 MAP_SHARED 是两种不同的映射类型,它们定义了对映射区域的修改是如何反映到底层文件的。

  • MAP_PRIVATE(私有映射)

    • ⚠️ 当使用 MAP_PRIVATE 标志创建映射时,会生成一个写时拷贝(copy-on-write,COW)的私有映射,这些修改实际上是在内存中进行的,并且是私有的,不会影响其他进程的映射或文件在磁盘上的实际内容。
    • ⚠️ 对这个映射区域的任何修改都不会影响底层文件,也不会影响其他进程的映射。
  • MAP_SHARED(共享映射)

    • ⚠️ 使用 MAP_SHARED 标志创建的映射是可写的,并且对映射区域的修改会反映到底层文件中。
    • ⚠️ 这种映射类型允许多个进程共享对同一文件的访问,并且所有进程看到的是文件的最新状态。
    • ⚠️ 如果一个进程修改了映射区域,其他进程也会看到这些更改。
    • ⚠️ 共享映射适用于多个进程需要协作修改文件内容的情况。
  • MAP_ANONYMOUS (匿名映射)

    • 是 mmap 系统调用中的一个标志,用于创建匿名映射。匿名映射区是指创建的映射区域不与任何文件关联,而是由操作系统分配的匿名内存。在调用 mmap 进行匿名映射的时候,是将进程虚拟内存空间中的某一段虚拟内存区域与物理内存中的匿名内存页进行映射。
#include <iostream>
#include <string>
#include <cstring>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

void *my_malloc(size_t size)
{
    if (size > 0)
    {
        void *ptr = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        if (ptr == MAP_FAILED)
        {
            return nullptr;
        }
        return ptr;
    }
    return nullptr;
}

void my_free(void *start, size_t size)
{
    if (start != nullptr && size > 0)
    {
        int ret = ::munmap(start, size);
        if (ret != 0)
        {
            perror("munmap");
        }
    }
}

int main()
{
    char *ptr = (char*)my_malloc(1024);
    if (ptr == nullptr)
    {
        std::cerr << "malloc failed" << std::endl;
        return 1;
    }

    // 使用分配的内存 (这里只是简单地打印指针值)
    printf("Allocated memory at address: %p\n", ptr);

    //...在这里可以使用ptr指向的内存...
    memset(ptr, 'A', 1024);

    for (int i = 0; i < 1024; i++)
    {
        printf("%c ", ptr[i]);
        fflush(stdout);
        sleep(1);
    }

    my_free(ptr, 1024);

    return 0;
}

💻 运行:

root@# ./a.out 
Allocated memory at address: 0x7f1810db7000
A A A A A A A A A A A

👀 为了更好的观察我们所创建的内存空间 我们使用 gdb 调试工具

root@# g++ MallocMmap.cc -g -std=c++11
root@# gdb ./a.out 

在这里插入图片描述

我们在第36行打一个断点,然后运行程序

在这里插入图片描述

可以看到此时 ptr 的地址是 0

在这里插入图片描述

此时我们输入:

(gdb) info proc mapping 
  • 在 GDB(GNU调试器)中,info proc mapping 命令用于显示当前进程的内存映射信息,包括可执行文件的代码段、数据段、堆和栈等信息,以及共享库的地址空间等信息。通过这些信息,我们可以更好地了解程序的内存使用情况,方便我们进行调试和优化。

  • 具体来说,该命令会列出每个内存区域的起始地址、结束地址、大小、偏移量和访问权限等详细信息。
    在这里插入图片描述

  • 可以看到 ptr 对应的地址是进行了内存映射的(匿名映射)MAP_ANONYMOUS

📚 我们知道实际上虚拟地址空间都会有一个 struct vm_area_struct 然后链入自己的 pcb(task_struct)

在这里插入图片描述

当前我们做文件映射就会有文件描述符,而文件描述符是作为当前进程文件描述符表的索引去找对应的文件的,但是真正在 linux 内核里找到一个文件用的是 struct_file 结构体

那么进程地址空间是如何跟文件关联起来的呢?

在这里插入图片描述

🎯 我们发现在 linux 内核源代码 struct vm_area_struct 中包含了一个 struct_file * vm_file 直接指向打开的文件,文件都找到了,文件里又有什么内容找不到呢

四:🔥 共勉

😋 以上就是我对 mmap 文件映射 的理解, 觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~ 😉
在这里插入图片描述

;