系统调用(mmap和munmap)

mmap

内存映射类型

mmap() 系统调用用于在调用进程的虚拟地址空间中 创建内存映射,主要分为两种类型:

  1. 文件映射:将文件的一部分直接映射到虚拟内存中,允许通过内存访问文件内容,映射的分页会在需要时自动加载

  2. 匿名映射:没有对应文件,分页初始化为0,可以视为一个内容总是为0的虚拟文件映射

映射内存可以被多个进程共享,具体情况包括:

  • 共享映射MAP_SHARED):修改内容对所有共享进程可见,直接影响底层文件
  • 私有映射MAP_PRIVATE):修改内容对其他进程不可见,使用 写时复制 技术确保每个进程的修改独立

映射类型总结

映射类型 变更可见性 主要用途
私有文件映射 不可见 初始化内存区域,如文本和数据段
私有匿名映射 不可见 分配零填充内存
共享文件映射 可见 内存映射 I/O,进程间共享内存 (IPC)
共享匿名映射 可见 进程间共享内存 (IPC),仅限相关进程

其他说明

  • 通过 fork() 创建的子进程会继承映射,但在执行 exec() 时映射会丢失

  • 每次调用 mmap() 创建的新映射是独立的,尤其是在匿名映射的情况下

函数原型

#include <sys/mman.h>

void* mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • 参数说明

    • addr:期望的映射起始地址,通常设置为 NULL 让系统自动选择地址
    • length:要映射的字节数,通常是文件的大小
    • prot:映射区域的保护标志,指定访问权限,可以是以下的按位或组合:
      • PROT_READ:可读
      • PROT_WRITE:可写
      • PROT_EXEC:可执行
      • PROT_NONE:不可访问
    • flags:映射的选项,例如:
      • MAP_SHARED:共享映射,多个进程可以访问相同的映射
      • MAP_PRIVATE:私有映射,写入时复制,写入不会影响原文件
      • MAP_ANONYMOUS:不与任何文件关联的匿名映射
    • fd:要映射的文件描述符,使用 open 系统调用获取
    • offset:文件映射的起始偏移量,必须是页面大小的倍数
  • 返回值

    • 成功时返回映射区域的指针,失败时返回 MAP_FAILED,并设置 errno

munmap

munmap() 系统调用用于从进程的虚拟地址空间中 删除一个映射

函数原型

#include <sys/mman.h>
int munmap(void *addr, size_t length);
  • 参数说明

    • addr:待解除映射的起始地址,必须与分页边界对齐
    • length:指定解除映射区域的大小,必须为非负整数,通常应为系统分页大小的倍数
  • 返回值

    • 如果指定范围内不存在映射,munmap() 将无效并返回 0(表示成功)

补充说明

  • 解除映射

    • 通常解除整个映射,可以将 addr 设置为 mmap() 返回的地址,并使 lengthmmap() 使用的值相同
    • 也可以部分解除映射,可能导致映射收缩或分割
  • 内存锁

    • 解除映射时,内核会删除在指定范围内的所有内存锁(由 mlock()mlockall() 创建)
  • 自动解除

    • 进程终止或执行 exec() 时,所有映射会自动解除
  • 注意事项

    • 在解除共享文件映射之前,应先调用 msync() 确保内容写入底层文件

文件映射

1

这张图表示由参数offsetlength决定哪些文件区域被映射到虚拟内存中

共享/私有文件映射

多个进程共享同一区域的内存映射,共享文件映射 所有的修改都是可见的,同时也会反映到底层文件,私有文件映射 的修改仅调用进程自己可见,并且不会反应到底层文件,这是使用 写时复制 的技术实现,即,当要对该内存映射做修改时,内核会复制一份相同的给进程,从而使其真正的独立出来
2

内存映射I/O

内存映射 I/O 是一种将文件的内容映射到进程的虚拟内存地址空间的技术,使得程序可以通过 直接访问内存来执行文件 I/O 操作,而无需使用传统的 read()write() 系统调用

关键特点

  1. 共享文件映射:映射的内存内容源自文件,并且对映射内容的任何更改都会自动反映到文件中。这意味着可以通过简单的内存访问操作来进行文件 I/O

  2. 结构化数据类型:通常,程序会定义一个结构化数据类型,与磁盘文件的内容对应,以便于访问和处理映射的内存内容

    举个例子,假设磁盘文件中存储的是员工信息,那我可以先定义一个员工结构体

    struct Employee {
        int id;          // 员工ID
        char name[50];  // 员工姓名
        float salary;    // 员工薪资
    };
    

    一旦文件被映射到内存,程序可以直接通过访问结构体的字段来读取和修改员工信息,而不需要手动处理字节的偏移和数据格式

    struct Employee *employees = mmap(NULL, fileSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    printf("Employee ID: %d\n", employees[0].id); // 直接访问第一个员工的ID
    

优势

  1. 简化应用逻辑:使用内存访问替代传统的 read()write() 调用,可以使一些应用程序的逻辑更为简洁和直观。

  2. 提高性能

    • 减少数据传输:传统的 read()write() 操作需要两次数据传输:一次在文件和内核缓冲区之间,另一次在内核缓冲区和用户空间之间。而使用 mmap() 可以省去第二次传输,用户进程可以直接访问内存中的数据。
    • 共享内存:当使用 mmap() 时,内核空间和用户空间共享同一个缓冲区,避免了在用户空间和内核空间之间复制数据的开销。如果多个进程在同一文件上进行 I/O,它们可以共享同一内核缓冲区,从而节省内存使用。

边界情况

  1. 文件内容更大,超过映射区域
    3

  2. 文件内容小于映射区域
    4

同步映射区域:msync()

msync() 系统调用用于 显式控制共享内存映射与底层文件之间的同步。虽然内核会自动将 MAP_SHARED 映射内容的更改写入文件,但默认情况下并 不保证同步的时间

作用

  • 数据完整性:在数据库等应用中,调用 msync() 可以强制将数据写入磁盘,以确保数据完整性
  • 可见性:确保在可写映射上进行的更新对执行 read() 的其他进程可见

函数原型

#include <sys/mman.h>

int msync(void *addr, size_t length, int flags);
  • 参数

    • addr:需要同步的内存区域的起始地址,必须分页对齐
    • length:同步区域的大小,向上舍入到系统分页大小的下一个整数倍
    • flags:指定同步行为,可以是以下值:
      • MS_SYNC:执行同步写入,调用会阻塞直到所有修改的数据页写入磁盘
      • MS_ASYNC:执行异步写入,修改的数据页将在将来的某个时间写入磁盘,立即对其他进程可见
      • MS_INVALIDATE:使映射数据的缓存副本失效,确保下一次访问时从文件读取更新的内容

匿名映射

匿名映射

匿名映射是一种没有对应文件的内存映射。可以通过以下两种方式在Linux中创建匿名映射:

  1. 使用 MAP_ANONYMOUS

    • mmap()flags 中指定 MAP_ANONYMOUS,同时将 fd 设置为 -1。这个值会被忽略,但为可移植性,建议遵循这个约定
    • 需要在代码中定义 _BSD_SOURCE_SVID_SOURCE 来使用 MAP_ANONYMOUS
  2. 使用 /dev/zero

    • 打开 /dev/zero 设备文件并将其文件描述符传递给 mmap()。该设备始终返回0,写入的数据会被丢弃

无论使用哪种方法,得到的映射都会被初始化为0,且 offset 参数会被忽略

匿名映射类型

  • MAP_PRIVATE 匿名映射

    • 用于分配进程私有的内存块,并将其初始化为0。
    • glibcmalloc() 函数在分配大于 MMAP_THRESHOLD(默认128 KB,可调整)的内存时使用此映射,以提高内存管理效率并减少内存碎片
  • MAP_SHARED 匿名映射

    • 允许相关进程(如父子进程)共享一块内存区域而无需对应的映射文件
    • 如果在创建共享映射后调用 fork(),子进程会继承该映射,从而实现进程间的内存共享

代码示例

#ifdef USE_MAP_ANON
#define _BSD_SOURCE   // 获取 USE_MAP_ANON 定义
#endif

int *addr;   // 假设为int

#ifdef USE_MAP_ANON
  addr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, 
              MAP_SHARED | MAP_ANONYMOUS, -1, 0);
#else
  int fd = open("/dev/zero", O_RDWR);
  addr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
              MAP_SHARED, fd, 0);
#endif

内存锁

修改内存保护信息

mprotect()用于动态修改虚拟内存区域的权限。这在实现某些安全策略或内存保护机制时非常有用

函数原型

int mprotect(void *addr, size_t len, int prot);

参数

  • addr: 要修改保护属性的内存区域的起始地址。该地址必须是页面边界对齐的
  • len: 要保护的字节数,通常是页面大小的倍数
  • prot: 新的保护标志,可以是以下值的组合:
    • PROT_READ: 可读
    • PROT_WRITE: 可写
    • PROT_EXEC: 可执行
    • PROT_NONE: 无权限

使用示例

    void *addr = mmap(NULL, pagesize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    
    // 修改保护属性为只读
    mprotect(addr, pagesize, PROT_READ);

    // 试图写入将导致段错误
    strcpy(addr, "Hello, World!");

内存加锁和解锁

内存加锁就是把指定的页固定到内存中

mlock()mlockall()用于防止特定的内存页被换出到磁盘,这在需要高性能或低延迟的场合非常重要
munlock()munlockall()用于解锁特定区域的内存页

函数原型

  • mlock()
int mlock(const void *addr, size_t len);
  • mlockall()
int mlockall(int flags);
  • munlock()
int munlock(const void *addr, size_t len);
  • munlockall()
int munlockall(void);

参数

  • mlock()munlock():
    • addr: 要锁定/解锁的内存区域的起始地址
    • len: 要锁定/解锁的字节数

锁定/解锁的内存区域是以页为单位,也就是只会解锁/锁定整数倍的页

  • mlockall() 和 munlockall():
    • flags: 锁定的范围,通常是以下之一:
      • MCL_CURRENT: 锁定当前进程的所有已映射内存
      • MCL_FUTURE: 锁定当前进程未来映射的所有内存

使用示例

    size_t pagesize = getpagesize();
    char *buf = mmap(NULL, pagesize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

    mlock(buf, pagesize);    // buf 现在被锁定在物理内存中
    munlock(buf, pagesize);

mincore()

mincore()用于检查某个内存区域的页面是否驻留在物理内存中。返回的vec数组中的每一位代表一个页面的状态,这在内存管理和优化方面非常有用

函数原型

int mincore(void *addr, size_t len, unsigned char *vec);

参数

  • addr: 要检查的内存区域的起始地址
  • len: 要检查的字节数
  • vec: 指向字节数组的指针,每个字节对应一个页面,指示该页面是否驻留在物理内存中

使用示例


    size_t pagesize = getpagesize();
    char *buf = mmap(NULL, pagesize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

    unsigned char vec[1];  // 一个字节表示一个页面的状态
    mincore(buf, pagesize, vec);

    printf("Page is %s in physical memory\n", (vec[0] & 1) ? "resident" : "not resident"); 
    // vec中的字节,仅最低位有效,1表示存在,0表示不存在

    munmap(buf, pagesize);

补充说明

  • 内存锁操作的是虚拟页
    • 内存锁主要是 操作虚拟页,而由于虚拟页和物理页之间的映射关系,内存锁的效果是确保某些 虚拟页对应的物理页 不会被换出
  • 内存锁的删除
    • 显示的调用munlock()munlockall()
    • 在进程终止时
    • 当被锁住的分页通过 munmap() 被解除映射时。
    • 当被锁住的分页被使用 mmap() MAP_FIXED 标记的映射覆盖时
  • 内存锁语义
    • 继承与保留

      • 内存锁不会在通过 fork() 创建的子进程中继承,也不会在 exec() 执行期间保留
    • 共享内存锁

      • 当多个进程共享相同的分页(例如通过 MAP_SHARED 映射)时,只要至少有一个进程保持对这些分页的内存锁,这些分页就会被锁定在内存中
    • 锁的唯一性

      • 内存锁是独立的,不会在单个进程中叠加。如果一个进程在同一虚拟地址区域多次调用 mlock(),只有一个锁会被建立,且只需一个 munlock() 调用即可解除锁
    • 多重映射的影响

      • 如果同一组分页(相同的文件)在一个进程中被多次映射到不同的位置,并且分别对这些映射进行加锁,则这些分页会保持在 RAM 中,直到所有映射都被解锁
    • 锁的逻辑不正确性

      • 由于内存锁的单位是分页且不能叠加,单独对 同一虚拟分页 上的不同数据结构(如指针 p1p2 指向的结构)进行 mlock()munlock() 调用在逻辑上是错误的。尽管所有调用都可能成功,但最终会导致整个分页被解锁,从而影响所有指向该分页的数据结构
 posted on 2024-10-21 22:40  Dylaris  阅读(331)  评论(0)    收藏  举报