内存映射原理(mmap)

内存映射是在进程的虚拟地址空间中创建一个映射,分为以下两种。

(1)文件映射:文件支持的内存映射,把文件的一个区间映射到进程的虚拟地址空间, 数据源是存储设备上的文件。

(2)匿名映射:没有文件支持的内存映射,把物理内存映射到进程的虚拟地址空间, 没有数据源。

根据修改是否对其他进程可见和是否传递到底层文件,内存映射分为共享映射和私有映射。

(1)共享映射:修改数据时映射相同区域的其他进程可以看见,如果是文件支持的映射,修改会传递到底层文件。

(2)私有映射:第一次修改数据时会从数据源复制一个副本,然后修改副本,其他进 程看不见,不影响数据源。

两个进程可以使用共享的文件映射实现共享内存。匿名映射通常是私有映射,共享的匿名映射只可能出现在父进程和子进程之间。


内存映射的原理

(1)创建内存映射的时候,在进程的用户虚拟地址空间中分配一个虚拟内存区域。

(2)Linux 内核采用延迟分配物理内存的策略,在进程第一次访问虚拟页的时候,产生缺页异常。如果是文件映射,那么分配物理页,把文件指定区间的数据读到物理页中,然 后在页表中把虚拟页映射到物理页;如果是匿名映射,那么分配物理页,然后在页表中把 虚拟页映射到物理页。


mmap 是 Linux 提供的一种内存映射机制,用于将文件或设备映射到进程的虚拟地址空间。
它的好处是:

  • 避免额外的内存拷贝:可以直接在用户空间像访问内存一样访问文件内容(零拷贝 I/O)。
    • 进程把文件映射到进程的虚拟地址空间,可以像访问内存一样访问文件,不需要调用系统调用read()和write()访问文件,从而避免用户模式和内核模式之间的切换,提高读写文件的速度。
  • 内存共享:多个进程可映射同一个文件(进程间通信)。
  • 映射设备内存:常用于显卡、硬件寄存器等。
  • 匿名映射:不依赖文件,仅作为申请一块内存的方式(比如 malloc 大块内存背后就是 mmap), 把内存的物理页映射到进程的虚拟地址空间。

mmap 的调用与使用

应用程序通常使用C标准库提供的函数 malloc()申请内存。glibc 库的内存分配器 ptmalloc 使用brkmmap 向内核以页为单位申请虚拟内存,然后把页划分成小内存块分配给应用程序。默认的阈值是128KB,如果应用程序申请的内存长度小于阈值,ptmalloc 分配器使用brk向内核申请虚拟内存,否则ptmalloc分配器使用mmap向内核申请虚拟内存。

应用程序可以直接使用mmap向内核申请虚拟内存。


用户态原型(glibc):

#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_READPROT_WRITEPROT_EXEC)。
  • **flags**
    • MAP_SHARED:多进程共享映射内容(修改写回文件)。
    • MAP_PRIVATE:写时拷贝(修改不会影响文件)。
    • MAP_ANONYMOUS:不映射文件,仅分配内存。
  • **fd**:文件描述符(MAP_ANONYMOUS 时可为 -1)。
  • **offset**:文件偏移量(必须是页大小的整数倍)。

内核实现机制(关键原理)

从系统调用入口到 VMA

  • 用户调用 mmap() → glibc 封装 → 系统调用 sys_mmap()
  • 内核会调用:
    • do_mmap()mm/mmap.c
    • 创建一个 VMA(Virtual Memory Area) 结构,用来描述映射区:
struct vm_area_struct {
    unsigned long vm_start; // 起始地址
    unsigned long vm_end;   // 结束地址
    pgprot_t vm_page_prot;  // 页保护属性
    struct file *vm_file;   // 关联文件(文件映射)
    unsigned long vm_flags; // 权限、类型
    const struct vm_operations_struct *vm_ops;
    ...
};
- 这个 VMA 会加入到 `mm_struct`(进程内存描述符)的红黑树/链表中。
- 每一个VMA结构,都可以在用户虚拟地址空间查询到(`/proc/pid/maps`)。
-  `vm_area_struct`中的`vm_ops`:对于文件映射,使用的是文件系统/设备驱动的`file_opertions`;匿名映射,则设置为`NULL`。
用户态
┌──────────────────────────────────────────┐
│ mmap(addr, len, prot, flags, fd, offset) │
└──────────────────────────────────────────┘
                 │
                 ▼
内核态
sys_mmap()   [arch/*/kernel/syscall.c]
    │
    ▼
vm_mmap()
    │
    ▼
do_mmap()
    │
    ▼
mmap_region()  ← 建立 vm_area_struct(VMA)
    │
    ├── MAP_ANONYMOUS? ──┐
    │       是           │
    │     ┌──────────────┴────────────────────┐
    │     │ vma->vm_ops = NULL                 │
    │     │ vma->vm_file = NULL                 │
    │     │ 缺页时走 do_anonymous_page()        │
    │     └────────────────────────────────────┘
    │
    └── 否(文件映射)
            │
            ▼
        file->f_op->mmap(file, vma)  ← 文件系统/驱动实现
            │
            ├── 文件系统例子(ext4)
            │       vma->vm_ops = &ext4_file_vm_ops
            │       vm_ops.fault = ext4_filemap_fault
            │
            └── 设备驱动例子
                    vma->vm_ops = &mydev_vm_ops
                    vm_ops.fault = mydev_fault_handler


文件映射与缺页异常

  • 不会立即分配物理页(除非是 MAP_POPULATE),而是延迟分配物理内存
  • 当进程访问该区域时,CPU 发现该虚拟地址无对应物理页 → 触发 缺页异常page fault)。
  • 内核缺页处理流程(handle_mm_fault()):
    1. 查找该地址对应的 VMA。
    2. 如果是文件映射:
      • 调用对应 vm_ops->fault()(比如 ext4 的实现)。
      • 通过页缓存(page cache)读取文件内容到物理页。
    3. 如果是匿名映射:
      • 分配一页物理内存并清零(alloc_zeroed_user_highpage())。
    4. 更新页表,映射虚拟地址到物理页。
用户态访问 VMA 对应的虚拟地址
       │
       ▼
CPU 检测无对应物理页 → Page Fault
       │
       ▼
do_page_fault() [arch/*/mm/fault.c]
       │
       ▼
handle_mm_fault(mm, vma, address, flags)
       │
       ├── vma->vm_ops != NULL ?
       │       │
       │       ├── 是 → 调用 vma->vm_ops->fault()
       │       │           (文件映射或设备映射)
       │       │
       │       └── 否 → 调用 do_anonymous_page()
       │                   (匿名映射)
       │
       ▼
建立物理页与虚拟页映射 → 更新页表 → 恢复用户态


写时拷贝(COW)

  • 如果是 MAP_PRIVATE 且文件映射:
    • 初始多个进程共享同一物理页,标记为只读。
    • 当某个进程写该页时 → 触发写缺页异常 → 分配新物理页并拷贝原数据 → 修改只在该进程可见。

特殊用途

  • 共享内存 IPCshm_open + mmap
  • 设备内存映射:驱动中 remap_pfn_range() 把设备物理地址映射到用户空间。
  • 大内存分配:glibc 的 malloc 在申请大块内存时用 mmap 而不是 brk/sbrk

关键点总结

  1. VMA 是抽象描述,页表才是真正的映射。
  2. 延迟分配物理内存:按需分配,访问才分配(缺页中断)。
  3. 文件映射与页缓存绑定:多个进程可共享缓存。
  4. COW 保证 MAP_PRIVATE 的写时隔离
  5. 设备映射需要特殊处理(页不在页缓存)。

mmap和DMA结合使用(后续补充)

测试

/proc/pid/maps查询VMA

每执行一次mmap/proc/pid/maps下都会新增一条VMA的记录。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>

int main() {
    size_t size;
    void *addr;

    pid_t pid = getpid();
    printf("当前进程 PID: %d\n", pid);
    printf("请输入要申请的内存大小(字节),输入 0 或负数退出:\n");

    while (1) {
        printf("大小: ");
        if (scanf("%zu", &size) != 1) {
            fprintf(stderr, "输入错误,请输入一个正整数。\n");
            while (getchar() != '\n'); // 清除输入缓冲区
            continue;
        }

        if (size <= 0) {
            printf("退出程序。\n");
            break;
        }

        addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
                    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

        if (addr == MAP_FAILED) {
            perror("mmap 失败");
            continue;
        }

        printf("成功分配 %zu 字节,地址: %p\n", size, addr);

        memset(addr, 0xAB, size);
        printf("已将内存填充为 0xAB。\n");

    }

    return 0;
}

测试结果:

通过mmap实现共享内存

通过shm_open+mmap+pthread_cond_wait可以实现共享内存方式的进程间通信。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/stat.h>

#define SHM_NAME "/cond_shm"
#define MAX_LEN 128

typedef struct {
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    char buffer[MAX_LEN];
    int ready;
    int msg_id;
    int init_done;
} SharedData;

int main() {
    int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (fd == -1) {
        perror("shm_open");
        exit(1);
    }

    ftruncate(fd, sizeof(SharedData));

    SharedData* data = mmap(NULL, sizeof(SharedData),
                            PROT_READ | PROT_WRITE,
                            MAP_SHARED, fd, 0);

    pthread_mutexattr_t mattr;
    pthread_condattr_t cattr;
    pthread_mutexattr_init(&mattr);
    pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);
    pthread_mutex_init(&data->mutex, &mattr);

    pthread_condattr_init(&cattr);
    pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_SHARED);
    pthread_cond_init(&data->cond, &cattr);

    data->ready = 0;
    data->msg_id = 0;
    data->init_done = 1;

    printf("[Receiver] PID = %d, initialized shared memory\n", getpid());
    fflush(stdout);

    int last_msg_id = 0;

    while (1) {
        pthread_mutex_lock(&data->mutex);

        while (data->msg_id == last_msg_id) {
            pthread_cond_wait(&data->cond, &data->mutex);
            printf("[Receiver] recv signal from sender\n");
        }

        printf("[Receiver] Read: %s (msg_id=%d)\n", data->buffer, data->msg_id);
        last_msg_id = data->msg_id;

        pthread_mutex_unlock(&data->mutex);
    }

    return 0;
}

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/stat.h>

#define SHM_NAME "/cond_shm"
#define MAX_LEN 128

typedef struct {
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    char buffer[MAX_LEN];
    int ready;
    int msg_id;
    int init_done;
} SharedData;

int main() {
    int fd = shm_open(SHM_NAME, O_RDWR, 0666);
    if (fd == -1) {
        perror("shm_open");
        exit(1);
    }

    SharedData* data = mmap(NULL, sizeof(SharedData),
                            PROT_READ | PROT_WRITE,
                            MAP_SHARED, fd, 0);

    while (data->init_done != 1) {
        usleep(1000);  // 1ms
    }

    for (int i = 0; i < 2; i++) {
        pthread_mutex_lock(&data->mutex);

        snprintf(data->buffer, MAX_LEN, "message %d", i + 1);
        data->msg_id++;
        data->ready = 1;

        printf("[Sender] Wrote: %s (msg_id=%d)\n", data->buffer, data->msg_id);

        pthread_cond_signal(&data->cond);
        pthread_mutex_unlock(&data->mutex);

        sleep(1);
    }

    return 0;
}

测试结果:

参考资料

  1. Professional Linux Kernel Architecture,Wolfgang Mauerer
  2. Linux内核深度解析,余华兵
  3. Linux设备驱动开发详解,宋宝华
  4. linux kernel 4.12
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值