内存映射是在进程的虚拟地址空间中创建一个映射,分为以下两种。
(1)文件映射:文件支持的内存映射,把文件的一个区间映射到进程的虚拟地址空间, 数据源是存储设备上的文件。
(2)匿名映射:没有文件支持的内存映射,把物理内存映射到进程的虚拟地址空间, 没有数据源。
根据修改是否对其他进程可见和是否传递到底层文件,内存映射分为共享映射和私有映射。
(1)共享映射:修改数据时映射相同区域的其他进程可以看见,如果是文件支持的映射,修改会传递到底层文件。
(2)私有映射:第一次修改数据时会从数据源复制一个副本,然后修改副本,其他进 程看不见,不影响数据源。
两个进程可以使用共享的文件映射实现共享内存。匿名映射通常是私有映射,共享的匿名映射只可能出现在父进程和子进程之间。
内存映射的原理
(1)创建内存映射的时候,在进程的用户虚拟地址空间中分配一个虚拟内存区域。
(2)Linux 内核采用延迟分配物理内存的策略,在进程第一次访问虚拟页的时候,产生缺页异常。如果是文件映射,那么分配物理页,把文件指定区间的数据读到物理页中,然 后在页表中把虚拟页映射到物理页;如果是匿名映射,那么分配物理页,然后在页表中把 虚拟页映射到物理页。
mmap 是 Linux 提供的一种内存映射机制,用于将文件或设备映射到进程的虚拟地址空间。
它的好处是:
- 避免额外的内存拷贝:可以直接在用户空间像访问内存一样访问文件内容(零拷贝 I/O)。
- 进程把文件映射到进程的虚拟地址空间,可以像访问内存一样访问文件,不需要调用系统调用read()和write()访问文件,从而避免用户模式和内核模式之间的切换,提高读写文件的速度。
- 内存共享:多个进程可映射同一个文件(进程间通信)。
- 映射设备内存:常用于显卡、硬件寄存器等。
- 匿名映射:不依赖文件,仅作为申请一块内存的方式(比如
malloc大块内存背后就是mmap), 把内存的物理页映射到进程的虚拟地址空间。
mmap 的调用与使用
应用程序通常使用C标准库提供的函数 malloc()申请内存。glibc 库的内存分配器 ptmalloc 使用brk 或mmap 向内核以页为单位申请虚拟内存,然后把页划分成小内存块分配给应用程序。默认的阈值是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_READ、PROT_WRITE、PROT_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()):- 查找该地址对应的 VMA。
- 如果是文件映射:
- 调用对应
vm_ops->fault()(比如 ext4 的实现)。 - 通过页缓存(
page cache)读取文件内容到物理页。
- 调用对应
- 如果是匿名映射:
- 分配一页物理内存并清零(
alloc_zeroed_user_highpage())。
- 分配一页物理内存并清零(
- 更新页表,映射虚拟地址到物理页。
用户态访问 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且文件映射:- 初始多个进程共享同一物理页,标记为只读。
- 当某个进程写该页时 → 触发写缺页异常 → 分配新物理页并拷贝原数据 → 修改只在该进程可见。
特殊用途
- 共享内存 IPC:
shm_open+mmap。 - 设备内存映射:驱动中
remap_pfn_range()把设备物理地址映射到用户空间。 - 大内存分配:glibc 的
malloc在申请大块内存时用mmap而不是brk/sbrk。
关键点总结
- VMA 是抽象描述,页表才是真正的映射。
- 延迟分配物理内存:按需分配,访问才分配(缺页中断)。
- 文件映射与页缓存绑定:多个进程可共享缓存。
- COW 保证 MAP_PRIVATE 的写时隔离。
- 设备映射需要特殊处理(页不在页缓存)。
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;
}
测试结果:

参考资料
- Professional Linux Kernel Architecture,Wolfgang Mauerer
- Linux内核深度解析,余华兵
- Linux设备驱动开发详解,宋宝华
- linux kernel 4.12

8685

被折叠的 条评论
为什么被折叠?



