从上一篇 [linux内存管理] 第039篇 用户态内存映射malloc和mmap详解 到现在我们基本已经了解到了内存映射是按照物理内存页为单位进行的,而在内存管理中,内存页主要分为两种:一种是匿名页,另一种是文件页。那何为匿名页?何为文件页?
匿名页与文件页
匿名页(Anonymous Page)
定义:不与磁盘上任何文件对应的内存页,存储的是临时数据。
特点:
没有磁盘文件后备:内容来自运行时程序动态分配
生命周期:通常随进程结束而释放
交换机制:当内存不足时,可能会被换出到交换分区/文件(swap)
这里典型的场景比如说:
进程的栈
动态分配的堆
使用mmap(MAP_ANONYMOUS)分配的匿名映射
大多数用户空间的数据结构
文件页(File-backed Page / Page Cache)
定义:与磁盘文件对应的内存页,是文件缓存。
特点:
有磁盘文件后备:数据源是磁盘上的文件
生命周期:即使进程结束,页面可能仍保留(作为缓存)
脏页处理:修改过的页需要写回磁盘(同步/定期)
文件页的典型场景比如说:
通过
mmap()映射的普通文件使用
read()/write()读写文件时的缓冲区(Page Cache)程序的可执行代码段(.text)
共享库的映射
匿名页与文件页的内存查询方式
查看系统内存使用(包含匿名页和文件页)
cat /proc/meminfo
MemTotal: 8175324 kB
MemFree: 142324 kB
Buffers: 125688 kB # 缓冲区(文件页)
Cached: 4123456 kB # 页面缓存(文件页)
SwapCached: 12456 kB # 交换缓存(匿名页相关)
AnonPages: 2123456 kB # 匿名页总量
Mapped: 856744 kB # 文件映射页
查看进程内存映射
[root@virt-machine mnt]#cat /proc/1/maps
00400000-00601000 r-xp 00000000 fe:00 15 /bin/busybox
00611000-00618000 r--p 00201000 fe:00 15 /bin/busybox
00618000-0061b000 rw-p 00208000 fe:00 15 /bin/busybox
0061b000-00622000 rw-p 00000000 00:00 0
26797000-267b9000 rw-p 00000000 00:00 0 [heap] # 匿名页
7f86efa000-7f86efc000 r--p 00000000 00:00 0 [vvar]
7f86efc000-7f86efd000 r-xp 00000000 00:00 0 [vdso]
7fff210000-7fff231000 rw-p 00000000 00:00 0 [stack] # 匿名页
匿名页与文件页的区别
mmap内存映射分类
mmap用于内存映射,也就是将一段区域映射到自己的进程地址空间中,分为两种:
文件映射: 将文件区域映射到进程空间,文件存放在存储设备上;
匿名映射:没有文件对应的区域映射,内容存放在物理内存上;
同时,针对其他进程是否可见,又分为两种:
私有映射:将数据源拷贝副本,不影响其他进程;
共享映射:共享的进程都能看到;
根据排列组合,就存在以下几种情况了:
私有匿名映射: 通常分配大块内存时使用,堆,栈,bss段等;
共享匿名映射:常用于父子进程间通信,在内存文件系统中创建
/dev/zero设备;私有文件映射:常用的比如动态库加载,代码段,数据段等;
共享文件映射:常用于进程间通信,文件读写等;
下面介绍这四种映射的一些使用场景,以及一些简单的特性,主要是让大家能够区分这些映射有哪些区别?底层逻辑会在下一章节叙述
私有匿名映射(MAP_PRIVATE | MAP_ANONYMOUS)
void *addr = mmap(NULL, size, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
特性:
写时复制(Copy-on-Write):初始共享零页,写操作时创建私有副本
不与文件关联:完全在内存中
进程私有:修改对其他进程不可见
典型场景:
// 1. malloc大内存分配
ptr = malloc(1*1024*1024); // 1MB以上可能用私有匿名映射
// 2. 线程栈(pthread_create内部使用)
pthread_t thread;
pthread_create(&thread, NULL, func, NULL);
// 3. .bss段扩展
static char large_buffer[10*1024*1024]; // 大未初始化全局变量

共享匿名映射(MAP_SHARED | MAP_ANONYMOUS)
void *addr = mmap(NULL, size, PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_ANONYMOUS, -1, 0);
特性:
进程间共享:相同物理页映射到多个进程地址空间
无文件后备:生命周期随最后一个进程结束
内核特殊处理:通过特殊文件
/dev/zero或memfd创建
典型应用场景:
// 1. 父子进程间通信(fork后)
pid_t pid = fork();
if (pid == 0) {
// 子进程能看到父进程在共享映射中的修改
} else {
// 父进程
}
私有文件映射(MAP_PRIVATE)
int fd = open("file.txt", O_RDONLY);
void *addr = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
特性:
写时复制:修改不影响原文件,创建内存副本
高效共享:多个进程可映射同一文件,共享只读的物理页
延迟加载:缺页时才实际读文件
典型场景:
// 1. 动态库加载(最重要的应用!)
// ld.so加载libc.so时使用私有文件映射
$ ldd /bin/ls
linux-vdso.so.1 (0x00007ffd45df0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8c1a200000)
// 2. 可执行文件代码段
// 所有.text段都是私有文件映射(可写时变成私有匿名)
// 3. 数据库的内存映射文件(只读查询)
void *db_data = mmap(NULL, db_size, PROT_READ,
MAP_PRIVATE, db_fd, 0);
共享文件映射(MAP_SHARED)
int fd = open("shared.data", O_RDWR|O_CREAT, 0644);
ftruncate(fd, size);
void *addr = mmap(NULL, size, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
特性:
修改同步到文件:写操作最终会更新磁盘文件
进程间通信:多个进程看到一致的数据
内存一致性:内核保证缓存一致性
典型场景:
// 1. 进程间通信(IPC)
// 进程A:
void *shm = mmap(NULL, SIZE, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
*(int*)shm = 42; // 进程B能看到
// 2. 内存数据库(Redis的RDB/AOF)
// 将数据库文件映射到内存,修改自动同步
// 3. 零拷贝文件I/O
// 避免read/write的系统调用和缓冲区拷贝
demo演示
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
int global_var; // .bss段 → 私有匿名映射(初始时)
const int const_global = 42; // .rodata → 私有文件映射
int main() {
int stack_var; // 栈 → 私有匿名映射
static int static_var; // .bss → 私有匿名映射
// 1. 堆内存(glibc管理)
char *heap_mem = malloc(2*1024*1024); // 可能使用私有匿名映射
// 2. 私有文件映射
int fd1 = open("/bin/ls", O_RDONLY);
void *code_map = mmap(NULL, 4096, PROT_READ,
MAP_PRIVATE, fd1, 0);
// 3. 共享文件映射
int fd2 = open("/tmp/shared", O_RDWR|O_CREAT, 0644);
ftruncate(fd2, 4096);
void *shared_map = mmap(NULL, 4096, PROT_READ|PROT_WRITE,
MAP_SHARED, fd2, 0);
// 4. 共享匿名映射
int memfd = memfd_create("anon_shm", 0);
ftruncate(memfd, 4096);
void *anon_shared = mmap(NULL, 4096, PROT_READ|PROT_WRITE,
MAP_SHARED, memfd, 0);
// 访问映射区域,触发实际分配
if (shared_map) {
memset(shared_map, 'A', 4096); // 写入共享文件映射
}
if (anon_shared) {
memset(anon_shared, 'B', 4096); // 写入共享匿名映射
}
if (code_map) {
char first_byte = *((char*)code_map); // 读取ls的代码
printf("First byte of /bin/ls: 0x%02x\n", first_byte);
}
// 分配大量堆内存
char *big_heap = malloc(2*1024*1024); // 2MB
if (big_heap) {
memset(big_heap, 'C', 2*1024*1024);
}
while(1){
};
return 0;
}
查询内存映射:
ubuntu@sh-liuqiN:~$ pmap -x 45523
45523: ./a.out
Address Kbytes RSS Dirty Mode Mapping
0000000000400000 4 4 0 r---- a.out
0000000000401000 604 604 0 r-x-- a.out
0000000000498000 164 160 0 r---- a.out
00000000004c1000 16 16 8 r---- a.out
00000000004c5000 12 12 12 rw--- a.out
00000000004c8000 20 8 8 rw--- [ anon ]
0000000008b58000 136 8 8 rw--- [ anon ]
0000741bbbc6f000 2052 2052 2052 rw--- [ anon ]
0000741bbbe70000 4 4 4 rw-s- memfd:anon_shm (deleted)
0000741bbbe71000 4 4 4 rw-s- shared
0000741bbbe72000 4 4 0 r---- ls
00007ffe5a28c000 132 12 12 rw--- [ stack ]
00007ffe5a374000 16 0 0 r---- [ anon ]
00007ffe5a378000 8 4 0 r-x-- [ anon ]
---------------- ------- ------- -------
total kB 3176 2892 2108
这里简单的说明一下怎么看上面这段,怎么判断内存页的类型和状态?
如何判断是匿名页还是文件页?
答:通过 Mapping 列判断有具体路径/名字 ⇒ 文件映射
例:a.out、ls、/lib/...等[ anon ]、[ heap ]、[ stack ]、[ vdso ]、[ vvar ]⇒ 匿名映射
例:[ anon ]、[ stack ]memfd:*⇒ 也是“文件型对象”(匿名文件),由memfd_create()创建的、在内存里但“像文件一样”的映射
例:memfd:anon_shm (deleted):说明这个 memfd 已经 unlink/删除了目录项,但 fd 还在,所以映射仍存在
怎么判断:私有还是共享?
答:通过"Mode"列的权限位判断...p⇒ private (MAP_PRIVATE):写时复制(COW),改了会变成进程私有页...s⇒ shared (MAP_SHARED):多个进程/映射者真正共享同一组物理页(或同一文件页)
Mode列格式:r(读)w(写)x(执行)p/s(私有/共享) rw--- # 私有读写映射(最后一个字符是`p`,但pmap省略了) rw-s- # 共享读写映射(`s`表示shared) r---- # 私有只读映射 r-x-- # 私有可执行映射 r--s- # 共享只读映射于是:
rw-s- memfd:...、rw-s- shared这俩就是 共享映射rw--- a.out这种通常是 private(可写数据段一般是 MAP_PRIVATE 的 file-backed + COW
如何判断脏页还是干净?
答:通过"Dirty"列判断Dirty(脏):这段里有多少 KB 的页被进程写过,导致:
对于 文件映射:页缓存中的该页与磁盘文件内容不一致(需要回写或丢弃策略)
对于 匿名映射:页没有“磁盘对应物”,但也会被计为 dirty(因为它是可写、内容已发生变化/需要交换等意义上的“脏”)
Clean(干净):
RSS - Dirty(粗略理解)a.out的r-x--段 Dirty=0,说明它只是被读取执行,没有被写(也通常不允许写)
于是:
0000741bbbc6f000 2052K RSS=2052 Dirty=2052 rw--- [ anon ]
这段匿名内存几乎全被写过(比如 malloc 后写入)rw-s- memfd:anon_shm ... Dirty=4
共享内存页也被写过r-x-- a.out Dirty=0
代码段没写过(正常)
匿名映射
私有匿名映射
okay,目前我们已经从宏观上了解到了匿名映射和文件映射的区别,以及怎么去mmap对应的映射。而这一章我们就开始看看匿名映射的底层逻辑。让我们把思绪回到 [linux内存管理] 第038篇 深入剖析AArch64架构下的do_page_fault缺页异常处理

缺页异常发生后,在函handle_pte_fault 中根vma_is_anonymous 函数来判断当前的vma是匿名页还是文件页?
if (!vmf->pte) {
if (vma_is_anonymous(vmf->vma))
return do_anonymous_page(vmf); ///处理匿名映射
else
return do_fault(vmf); ///文件映射
}
所以本章节将重点聊do_ananymous_page 函数
对于匿名映射,映射完成之后,只是获得了一块虚拟内存,并没有分配物理内存,当第一次访问的时候:
如果是读访问,会将虚拟页映射到0页,以减少不必要的内存分配
如果是写访问,用alloc_zeroed_user_highpage_movable分配新的物理页,并用0填充,然后映射到虚拟页上去
如果是先读后写访问,则会发生两次缺页异常:第一次是匿名页缺页异常的读的处理(虚拟页到0页的映射),第二次是写时复制缺页异常处理。
从上面的总结我们知道,第一次访问匿名页时有三种情况,其中第一种和第三种情况都会涉及到0页。所以什么是零页呢?
零页是什么?
零页是物理内存中的一个特殊页面(通常是物理页框0),其内容全部为0。当进程请求分配一个匿名页并要求初始化为0时,内核并不是立即分配真正的物理页并清零,而是将虚拟地址映射到这个共享的零页。
my_zero_pfn -> ZERO_PAGE -> empty_zero_page
/*
* Empty_zero_page is a special page that is used for zero-initialized data
* and COW.
*/
unsigned long empty_zero_page[PAGE_SIZE / sizeof(unsigned long)] __page_aligned_bss;
EXPORT_SYMBOL(empty_zero_page);可以看到定义了一个全局变量,大小为一页,页对齐到 bss 段,所有这段数据内核初始化的时候会被清零,所有称之为0页。
第一次读匿名页
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
//...
///处理分配页面只读情况,系统返回零页
/* Use the zero-page for reads */
if (!(vmf->flags & FAULT_FLAG_WRITE) &&
!mm_forbids_zeropage(vma->vm_mm)) {
///my_zero_pfn获取零页的页帧号
entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
vma->vm_page_prot));
///获取pte页表项,同时获取锁保护
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
vmf->address, &vmf->ptl);
if (!pte_none(*vmf->pte)) {
update_mmu_tlb(vma, vmf->address, vmf->pte);
goto unlock;
}
ret = check_stable_address_space(vma->vm_mm);
if (ret)
goto unlock;
/* Deliver the page fault to userland, check inside PT lock */
if (userfaultfd_missing(vma)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
return handle_userfault(vmf, VM_UFFD_MISSING);
}
goto setpte; ///读情况处理完,跳转setpte
}
//...
setpte:
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry); ///填写页表项到硬件页表
/* No need to invalidate - it was non-present before */
update_mmu_cache(vma, vmf->address, vmf->pte);
unlock:
pte_unmap_unlock(vmf->pte, vmf->ptl);
return ret;
//...
}pte_mkspecial 是主要函数,设置页表项的值映射到0页。my_zero_pfn就是内核初始化设置empty_zero_page这个0页得到页帧号。
static inline pte_t pte_mkspecial(pte_t pte)
{
return set_pte_bit(pte, __pgprot(PTE_SPECIAL));
}关键点:第一次读取时,页表项被设置为指向共享的零页,并带有特殊标_PAGE_SPECIAL 。
那第一次读后的页表状态是咋样的呢?我们可以写一个demo来验证一下
// test_zero_page.c
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
int main() {
printf("PID: %d\n", getpid());
// 分配一页匿名内存
void *addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED) {
perror("mmap failed");
return 1;
}
printf("映射地址: %p\n", addr);
// 阶段1:第一次读取(触发零页映射)
printf("\n=== 第一次读取 ===\n");
printf("读取前 - 按回车继续...");
getchar();
char value = *((char*)addr); // 第一次读取
printf("读取的值: %d (应为0)\n", value);
printf("读取后 - 查看页表状态,按回车继续...");
getchar();
// 阶段2:第一次写入(触发COW分配新页)
printf("\n=== 第一次写入 ===\n");
*((char*)addr) = 'A'; // 第一次写入
printf("写入后 - 查看物理页变化,按回车退出...");
getchar();
return 0;
}
这时候我们执行这个测试程test_mmap ,会发现一个很有意思的事儿!
在我们读匿名页之前,等待回车进行下一步的时候,竟然已经发生了四次写匿名页!

这是不是很奇怪!但实际上完全合理,因为 printf 和 getchar 中已经发生了内存访问!这个有兴趣的同学可以单独去debug一下,这里继续回到主线了
此时我们来看一下,当我们映射完成后,读之前我们的页表是什么样的?

此时的页表
虚拟地址: 0xf7cbd000
四级页表状态:
1. PGD (Page Global Directory): 已分配 ✓
2. PUD (Page Upper Directory): 已分配 ✓ (0xffffff800350c018)
3. PMD (Page Middle Directory): 已分配 ✓ (0xffffff80034abdf0)
4. PTE (Page Table Entry): 未分配 ✗ (0x0)
内存映射状态:
虚拟地址空间: 已预留 ✓ (VMA存在)
页表映射: 未建立 ✗ (PTE为空)
物理内存: 未分配 ✗ (page=0x0)单步调试走pte_offset_map_lock ,pte分配,并指向了0页

走set_pte_at 后,页表项被填充,此时就完成了物理/虚拟地址之间的映射

第一次读匿名页后,页表填充完pte,此时物理地址与虚拟地址真正的映射完成,此时从地址中读数据即为0。
[root@virt-machine mnt]#./test_mmap
[ 126.395667] [LIUQI][PageFault][do_anonymous_page] WRITE!
[ 126.420632] [LIUQI][PageFault][do_wp_page] triger do_swap_page.
[ 126.461501] [LIUQI][PageFault][do_anonymous_page] WRITE!
[ 126.504871] [LIUQI][PageFault][do_anonymous_page] WRITE!
PID: 127
映射地址: 0xf7db6000
=== 第一次读取 ===
[ 126.584772] [LIUQI][PageFault][do_anonymous_page] WRITE!
读取前 - 按回车继续...
[ 126.633565] [LIUQI][PageFault][do_anonymous_page] WRITE!
读取的值: 0 (应为0)
读取后 - 查看页表状态,按回车继续...okay,demo演示暂停,我们继续理论分析写匿名页。
第一次写匿名页
///处理vma可写情况
/* Allocate our own private page. */
///为建立rmap做准备
if (unlikely(anon_vma_prepare(vma)))
goto oom;
///分配一个可移动的匿名物理页面,优先使用高端内存(arm64不存在高端内存)
page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
if (!page)
goto oom;
if (mem_cgroup_charge(page, vma->vm_mm, GFP_KERNEL))
goto oom_free_page;
cgroup_throttle_swaprate(page, GFP_KERNEL);
/*
* The memory barrier inside __SetPageUptodate makes sure that
* preceding stores to the page contents become visible before
* the set_pte_at() write.
*/
__SetPageUptodate(page); ///添加内存屏障
entry = mk_pte(page, vma->vm_page_prot); ///创建一个pte页表项
entry = pte_sw_mkyoung(entry);
if (vma->vm_flags & VM_WRITE)
entry = pte_mkwrite(pte_mkdirty(entry)); ///设置可写标记
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address, ///获取pte页表项,并获得自旋锁,保证不被锁和打断
&vmf->ptl);
if (!pte_none(*vmf->pte)) {
update_mmu_cache(vma, vmf->address, vmf->pte);
goto release;
}
ret = check_stable_address_space(vma->vm_mm);
if (ret)
goto release;
/* Deliver the page fault to userland, check inside PT lock */
if (userfaultfd_missing(vma)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
put_page(page);
return handle_userfault(vmf, VM_UFFD_MISSING);
}
inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES); ///增加进程匿名页计数
page_add_new_anon_rmap(page, vma, vmf->address, false); ///匿名页面添加到rmap系统
lru_cache_add_inactive_or_unevictable(page, vma); ///匿名页面添加到lru
setpte:
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry); ///填写页表项到硬件页表
/* No need to invalidate - it was non-present before */
update_mmu_cache(vma, vmf->address, vmf->pte);
unlock:
pte_unmap_unlock(vmf->pte, vmf->ptl);
return ret;
release:
put_page(page);
goto unlock;
oom_free_page:
put_page(page);
oom:
return VM_FAULT_OOM;
}alloc_zeroed_user_highpage_movable 分配清零的页面
mk_pte:从物理页面创建基础PTE
pte_sw_mkyoung:标记为年轻(访问过)
pte_mkwrite:设置可写位(因为vma有VM_WRITE)
pte_mkdirty:标记为脏页(因为即将写入)
set_pte_at:填写页表项到硬件页表
其他的函数就不看了,我们用demo代码来测试一下
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
int main() {
printf("PID: %d\n", getpid());
// 分配一页匿名内存
void *addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED) {
perror("mmap failed");
return 1;
}
printf("映射地址: %p\n", addr);
// 第一次写入(触发COW分配新页)
printf("\n=== 第一次写入 ===\n");
*((char*)addr) = 'A'; // 第一次写入
printf("写入后 - 查看物理页变化,按回车退出...");
getchar();
return 0;
}写之前pte不存在

PID: 130
映射地址: 0xf7899000
=== 第一次写入 ===
[ 1127.043013] [LIUQI][PageFault][do_anonymous_page] WRITE!
[ 1127.125955] [LIUQI][PageFault][do_anonymous_page] WRITE!
写入后 - 查看物理页变化,按回车退出...
读取的值: A (应为A)
读取后 - 查看页表状态,按回车继续...读之后写匿名页
okay,我们现在到了先读后写的这种情况。这里我们只做演示,因为会走do_wp_page ,这是我们后面的篇章要讲的!这里就暂时不进行叙述。
共享匿名映射
从上一节,我们可以得知私有匿名映射走到的do_anonymous_page 函数,但是do_anonymous_page 函数的开头,就有这么一段
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
/* File mapping without ->vm_ops ? */
if (vma->vm_flags & VM_SHARED)
return VM_FAULT_SIGBUS;
//...
}首先就判断vma的vm_flags是不VM_SHARED 类型,那就说明这个函数只来出来私有匿名映射,那么共享匿名映射的流程应该是如何的呢?这里也是我们之前mmap没有提到的细节部分。
首先我们在上层通过mmap,传入参数MAP_ANONYMOUS|MAP_SHARED,会走到底层do_mmap 函数
unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, unsigned long pgoff,
unsigned long *populate, struct list_head *uf)
{
//...
switch (flags & MAP_TYPE) {
case MAP_SHARED:
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
return -EINVAL;
/*
* Ignore pgoff.
*/
pgoff = 0;
// 通过上层设置的MAP_SHARED标志,设置vm_flags为 VM_SHARED
vm_flags |= VM_SHARED | VM_MAYSHARE;
break;
//...
addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
}vm_flags会传入 mmap_region 处理
unsigned long mmap_region(struct file *file, unsigned long addr,
unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
struct list_head *uf)
{
//...
} else if (vm_flags & VM_SHARED) {
error = shmem_zero_setup(vma);
if (error)
goto free_vma;
//...
}mmap_region 中会在vm_flagsVM_SHARED 时调shmem_zero_setup
int shmem_zero_setup(struct vm_area_struct *vma)
{
struct file *file;
loff_t size = vma->vm_end - vma->vm_start;
/*
* Cloning a new file under mmap_lock leads to a lock ordering conflict
* between XFS directory reading and selinux: since this file is only
* accessible to the user through its mapping, use S_PRIVATE flag to
* bypass file security, in the same way as shmem_kernel_file_setup().
*/
// tmpfs(shmem)文件系统中创建一个匿名的、内存中的文件对象
file = shmem_kernel_file_setup("dev/zero", size, vma->vm_flags);
if (IS_ERR(file))
return PTR_ERR(file);
if (vma->vm_file)
fput(vma->vm_file);
// 将新创建的 shmem 文件与这个 VMA 进行强绑定
vma->vm_file = file;
vma->vm_ops = &shmem_vm_ops;
return 0;
}
static const struct vm_operations_struct shmem_vm_ops = {
.fault = shmem_fault,
.map_pages = filemap_map_pages,
#ifdef CONFIG_NUMA
.set_policy = shmem_set_policy,
.get_policy = shmem_get_policy,
#endif
};共享匿名映射的vma->vm_ops被赋值shmem_vm_ops
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
//...
if (!vmf->pte) {
if (vma_is_anonymous(vmf->vma)) {
return do_anonymous_page(vmf);
}
else {
return do_fault(vmf);
}
}
//...
}
static inline bool vma_is_anonymous(struct vm_area_struct *vma)
{
return !vma->vm_ops;
}到这里我们就知道了实际上私有匿名映射其实是走的文件映射的流程,内核通过 shmem 文件系统,将一个“匿名”的内存区域,伪装成一个对“特殊文件”的映射,而这正是共享匿名映射区别于私有匿名映射的不同之处!
PS: 共享匿名映射的流程在下面的文件映射中描述。
文件映射
文件映射是需要对文件进行操作的,所以我们在do_mmap 函数时struct file 是不为空的,这个file指针是从上层传下来的,对应的就是具体的文件。
file_operation与vma的联系
unsigned long mmap_region(struct file *file, unsigned long addr,
unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
struct list_head *uf)
{
//...
vma = vm_area_alloc(mm);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}
vma->vm_start = addr;
vma->vm_end = end;
vma->vm_flags = vm_flags;
vma->vm_page_prot = vm_get_page_prot(vm_flags);
vma->vm_pgoff = pgoff;
// 只有当传入的 file 指针非空时(即文件映射)
if (file) {
if (vm_flags & VM_SHARED) {
error = mapping_map_writable(file->f_mapping);
if (error)
goto free_vma;
}
// 绑定文件对象
vma->vm_file = get_file(file);
//调用文件系统驱动
error = call_mmap(file, vma);
//...
}
//...
}
static inline int call_mmap(struct file *file, struct vm_area_struct *vma)
{
return file->f_op->mmap(file, vma);
}call_mmap 函数主要就是执行对应文件系统的file_operation的mmap函数~,而这个在对应的文件系统的代码中已经初始化了,
比如ext4文件系统 (本文后续的篇幅也均以ext4文件系统为例子)
// fs/ext4/file.c
const struct file_operations ext4_file_operations = {
.llseek = ext4_llseek,
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
.iopoll = iocb_bio_iopoll,
.unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ext4_compat_ioctl,
#endif
.mmap = ext4_file_mmap,
.mmap_supported_flags = MAP_SYNC,
.open = ext4_file_open,
.release = ext4_release_file,
.fsync = ext4_sync_file,
.get_unmapped_area = thp_get_unmapped_area,
.splice_read = generic_file_splice_read,
.splice_write = iter_file_splice_write,
.fallocate = ext4_fallocate,
};
static int ext4_file_mmap(struct file *file, struct vm_area_struct *vma)
{
...........
file_accessed(file);
if (IS_DAX(file_inode(file))) {
vma->vm_ops = &ext4_dax_vm_ops;
vm_flags_set(vma, VM_HUGEPAGE);
} else {
vma->vm_ops = &ext4_file_vm_ops;
}
return 0;
}比如erofs文件系统
// fs/erofs/data.c
int generic_file_mmap(struct file *file, struct vm_area_struct *vma)
{
struct address_space *mapping = file->f_mapping;
if (!mapping->a_ops->read_folio)
return -ENOEXEC;
file_accessed(file);
vma->vm_ops = &generic_file_vm_ops;
return 0;
}
const struct file_operations erofs_file_fops = {
.llseek = generic_file_llseek,
.read_iter = erofs_file_read_iter,
.mmap = erofs_file_mmap,
.splice_read = generic_file_splice_read,
};
文件映射缺页处理
这是最核心的阶段,根据访问类型(读/写)和映射标志(共享/私有)分为三个子路径。

下面是一个简化的代码
// mm/memory.c
static vm_fault_t do_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
vm_fault_t ret;
// 判断是读异常还是写异常
if ((vmf->flags & FAULT_FLAG_WRITE) && // 是写访问
!(vma->vm_flags & VM_SHARED)) { // 且是私有映射(MAP_PRIVATE)
// 私有写:触发写时复制(COW)
ret = do_cow_fault(vmf);
} else if (!(vmf->flags & FAULT_FLAG_WRITE)) {
// 读访问:分配页框,从文件读取数据
ret = do_read_fault(vmf);
} else {
// 共享写:分配页框,从文件读取数据(后续多进程可写同一页)
ret = do_shared_fault(vmf);
}
return ret;
}三个子路径详解:
1. do_read_fault (读取映射)
这是最简单的情况,进程第一次读一个私有或共享的文件页。
分配一个全新的物理页(称为“页框”)。
调用
vma->vm_file->f_op->readpage()方法,该函数由具体文件系统实现(如ext4_readpage),负责从磁盘读取文件数据到刚分配的页框。将页框映射到触发异常的虚拟地址(填写PTE)。
映射是只读的(即使VMA权限是RW)。这是因为如果是私有映射(
MAP_PRIVATE),第一次写时需要触发COW。
2. do_cow_fault (私有写映射,触发COW)
进程第一次写一个私有文件映射(MAP_PRIVATE)页。
内核会分配两个新的物理页:一个用于读(源页),一个用于写(目标页)。
读操作:调用
readpage将文件数据读到源页。复制操作:将源页的数据拷贝到目标页。
映射操作:将触发缺页的虚拟地址,映射到目标页,并设置PTE为可写。
结果:进程写入的是自己私有的副本,不会影响磁盘上的原文件或其他映射了同一文件的进程。这就是“写时复制”(Copy-on-Write)。
3. do_shared_fault (共享写映射)
进程第一次写一个共享文件映射(MAP_SHARED)页。
流程和
do_read_fault类似:分配页框,readpage读入数据。关键区别:映射时PTE被设置为可写,且该页被标记为脏页。
后续该页被修改后,内核的回写机制(如
pdflush内核线程)会在适当时候,调用文件系统的writepage方法,将修改写回磁盘文件。这样,所有映射该文件的进程都能看到修改。