mmap 系统调用原理解析

基于 Linux 5.15 内核源码分析
https://elixir.bootlin.com/linux/v5.15/source

什么是 mmap?

mmap 是 Linux 系统的一种内存映射机制,允许将文件或设备直接映射到进程的虚拟地址空间。进程通过操作内存指针即可读写文件,无需调用 read() / write() 等系统调用

mmap 在 Linux 系统中广泛被使用:内存分配(malloc 大块)、零拷贝网络传输、进程间通信、大文件处理等。

mmap 函数原型

1
2
3
#include <sys/mman.h>

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

参数说明:

参数 类型 描述
addr void * 期望的映射起始地址,通常为 NULL(内核选择)
length size_t 映射长度(字节)
prot int 保护标志:PROT_READ / PROT_WRITE / PROT_EXEC / PROT_NONE
flags int 映射标志:MAP_SHARED / MAP_PRIVATE / MAP_ANONYMOUS / MAP_FIXED
fd int 文件描述符(文件映射);匿名映射传 -1
offset off_t 文件偏移量(需页对齐)

返回值: 成功返回映射的虚拟地址,失败返回 MAP_FAILED(即 (void *)-1)。

背景知识:物理内存管理

要理解 mmap 的内核实现,必须先理解两个基础:物理内存如何组织,以及虚拟地址如何翻译为物理地址

页(Page)

Linux 内核将物理内存划分为 4KB 大小的单元,称为页(Page)。每个物理页由一个 struct page 描述(include/linux/mm_types.h:70):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct page {
    unsigned long flags;            /* 页面的状态标志 */
    atomic_t _count;                /* 页面的引用计数 */
    atomic_t _mapcount;             /* 页面被映射的次数 */
    union {
        struct {
            struct address_space *mapping;  /* 页面所属的地址空间 */
            pgoff_t index;                  /* 页面在文件中的偏移量 */
        };
        struct kmem_cache *slab;    /* 如果页面属于 SLAB 缓存,指向缓存 */
        struct page *next;          /* 链表指针 */
    };
    void *virtual;                  /* 页面在内核空间的虚拟地址 */
    /* ... */
};

arm64 四级页表

ARM64 架构(4K 页、48 位虚拟地址)使用四级页表映射,各层级全称如下:

缩写 英文全称 中文含义
PGD Page Global Directory 页全局目录
PUD Page Upper Directory 页上级目录
PMD Page Middle Directory 页中间目录
PTE Page Table Entry 页表项
P4D Page 4th Directory 页四级目录(某些配置下被折叠)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
虚拟地址 VA[47:0] 分解(arm64, 4K页, 48位VA):

  ┌────────────┬────────────┬────────────┬────────────┬────────────┐
  │  PGD 索引   │  PUD 索引   │  PMD 索引   │  PTE 索引   │  页内偏移   │
  │  bits      │  bits      │  bits      │  bits      │  bits      │
  │  [47:39]   │  [38:30]   │  [29:21]   │  [20:12]   │  [11:0]    │
  ├────────────┼────────────┼────────────┼────────────┼────────────┤
  │  9 bits    │  9 bits    │  9 bits    │  9 bits    │  12 bits   │
  │  512 项    │  512 项    │  512 项    │  512 项    │  4KB 页    │
  │  每项 8B   │  每项 8B   │  每项 8B   │  每项 8B   │            │
  └─────┬──────┴─────┬──────┴─────┬──────┴─────┬──────┴────────────┘
        │            │            │            │
        ▼            ▼            ▼            ▼
   ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐  ┌──────────────┐
   │  PGD   │→ │  PUD   │→ │  PMD   │→ │  PTE   │→ │  物理页      │
   │ 页表   │  │ 页表   │  │ 页表   │  │ 页表   │  │  (4KB)      │
   └────────┘  └────────┘  └────────┘  └────────┘  └──────────────┘
        ↑            ↑            ↑            ↑
   CR3/PGDIR   PGD 指向    PUD 指向    PMD 指向
   物理地址     PUD 表      PMD 表      PTE 表

页表层级常量arch/arm64/include/asm/pgtable-hwdef.h:41 及之后):

层级 英文全称 位移宏 值(4K页, 48位VA) 每条目覆盖范围
PGD Page Global Directory PGDIR_SHIFT 39 512 GB
PUD Page Upper Directory PUD_SHIFT 30 1 GB
PMD Page Middle Directory PMD_SHIFT 21 2 MB
PTE Page Table Entry PAGE_SHIFT 12 4 KB

⚠️ 注意:表中 “512 GB” 是 **PGD 的一个条目(entry)**所覆盖的地址范围。PGD 页表共有 PTRS_PER_PGD = 512 个条目(9 位索引),因此 PGD 整张表可覆盖的总地址空间为 512 × 512 GB = 256 TB。在 arm64 48 位 VA 下,用户空间大小 TASK_SIZE_64 = (1UL << vabits_actual) = 1UL << 48 = 256 TBarch/arm64/include/asm/processor.h:53),即整个 lower 48-bit 范围。一个进程能占用的虚拟地址空间受 TASK_SIZE 和物理内存/swap 总量的双重限制。

页表项数:每级页表 PTRS_PER_PTE = 1 << (PAGE_SHIFT - 3) = 512 项。

页表类型定义arch/arm64/include/asm/pgtable-types.h):

1
2
3
4
typedef struct { pteval_t pte; } pte_t;   /* PTE — Page Table Entry */
typedef struct { pmdval_t pmd; } pmd_t;   /* PMD — Page Middle Directory */
typedef struct { pudval_t pud; } pud_t;   /* PUD — Page Upper Directory */
typedef struct { pgdval_t pgd; } pgd_t;   /* PGD — Page Global Directory */

虚拟地址 → 物理地址映射过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
虚拟地址映射物理地址的过程(四级页表):

  进程 PGD 基地址记录在 task_struct->mm->pgd
  CPU 选中进程后 → 页表基地址寄存器设为该值

  步骤1: PGD[VA[47:39]]  → 得到 PUD 页表物理基地址
  步骤2: PUD[VA[38:30]]  → 得到 PMD 页表物理基地址
  步骤3: PMD[VA[29:21]]  → 得到 PTE 页表物理基地址
  步骤4: PTE[VA[20:12]]  → 得到物理页基地址
  步骤5: 物理页基地址 + VA[11:0] → 最终物理地址

  ┌──────┐     ┌──────┐     ┌──────┐     ┌──────┐     ┌─────────┐
  │ PGD  │ ──→ │ PUD  │ ──→ │ PMD  │ ──→ │ PTE  │ ──→ │ 物理页  │
  │[idx0]│     │[idx1]│     │[idx2]│     │[idx3]│     │ + offset│
  └──────┘     └──────┘     └──────┘     └──────┘     └─────────┘
    9位索引      9位索引      9位索引      9位索引      12位偏移

进程虚拟地址空间布局

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
完整的进程虚拟地址空间 (arm64 48VA, 4K页)
┈┈┈ 从低地址到高地址 ┈┈┈

  ┌──────────────────────────────┐  0x0000_0000_0000_0000
           代码段 (Text)            每个进程独立
  ├──────────────────────────────┤
           数据段 (Data)            已初始化全局变量(进程私有)
  ├──────────────────────────────┤
            BSS                   未初始化全局变量
  ├──────────────────────────────┤
      (Heap)  brk/sbrk          brk 增大(地址增大)
  ├──────────────────────────────┤
                               
     文件映射和匿名映射区            mmap 在此分配
                               
  ├──────────────────────────────┤
       (Stack)  向低地址增长      rsp 减小(地址减小)
  ├──────────────────────────────┤
      [vdso] / [vvar]             内核映射到用户空间的全局代码
  ├══════════════════════════════┤  0x0000_FFFF_FFFF_FFFF  用户空间结束
           内核空间              
    ┌──────────────────────────┐│   全局共享,所有进程相同
      kernel logical map      ││  线性映射,直接访问物理内存
    ├──────────────────────────┤│
      [kasan shadow]  32TB    ││  KASAN 影子内存
    ├──────────────────────────┤│
      bpf jit         128MB   ││  BPF JIT 编译区
    ├──────────────────────────┤│
      modules         128MB   ││  驱动模块
    ├──────────────────────────┤│
      vmalloc         124TB   ││  vmalloc() 分配
    ├──────────────────────────┤│
      [guard] + PCI I/O       ││  固定映射 / I/O 空间
    ├──────────────────────────┤│
      vmemmap          2TB    ││  struct page 数组
    ├──────────────────────────┤│
      [guard region]   2TB    ││
    └──────────────────────────┘│  ffff_FFFF_FFFF_FFFF
  └──────────────────────────────┘

arm64 内核地址空间完整分区表(从低到高排列): linux/Documentation/arm64/memory.rst

Start End Size 用途
0000000000000000 0000ffffffffffff 256 TB user(用户空间)
ffff000000000000 ffff7fffffffffff 128 TB kernel logical memory map(内核线性映射,直接访问所有物理内存)
ffff600000000000 ffff7fffffffffff 32 TB [kasan shadow region](KASAN 影子内存)
ffff800000000000 ffff800007ffffff 128 MB bpf jit region(BPF JIT 编译区)
ffff800008000000 ffff80000fffffff 128 MB modules(模块加载区)
ffff800010000000 fffffbffefffffff 124 TB vmalloc(vmalloc/ioremap 分配区)
fffffbfff0000000 fffffbfffdffffff 224 MB fixed mappings(固定映射,top-down 分配)
fffffbfffe000000 fffffbfffe7fffff 8 MB [guard region]
fffffbfffe800000 fffffbffff7fffff 16 MB PCI I/O space(PCI IO 空间)
fffffbffff800000 fffffbffffffffff 8 MB [guard region]
fffffc0000000000 fffffdffffffffff 2 TB vmemmap(struct page 数组映射)
fffffe0000000000 ffffffffffffffff 2 TB [guard region]

表中 “kernel logical memory map”(内核线性映射区)是内核访问物理内存的窗口:virt = phys + PAGE_OFFSET,128 TB 空间足以映射当前所有物理内存。vmalloc 用于非连续内存分配,vmemmap 用于存储每个物理页对应的 struct page

关键分区说明:

· 用户空间 (0000_0000_0000_0000 ~ 0000_FFFF_FFFF_FFFF)(边界由 TASK_SIZE 定义arch/arm64/include/asm/processor.h) 每个进程独立,通过切换页表基地址寄存器隔离

· 内核空间 (FFFF_0000_0000_0000 ~ FFFF_FFFF_FFFF_FFFF) 所有进程全局共享,上下文切换时内核页表部分不变

· 全局变量 (.data / .bss) 位于用户空间数据段,是每个进程私有的副本(写时复制继承)。
内核线性映射(kernel logical memory map)才是真正全局共享的物理内存访问通道。

· [vdso] / [vvar] 内核映射到每个用户空间的"全局"代码页,加速 gettimeofday(), 所有进程看到的内容相同(映射位置可能因 ASLR 而异)

mmap 内核实现原理

有了背景知识,现在来看 mmap 在内核中是如何一步步完成的。

系统调用入口

arm64 入口代码 (arch/arm64/kernel/sys.c:21-29):

1
2
3
4
5
6
7
8
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
                unsigned long, prot, unsigned long, flags,
                unsigned long, fd, unsigned long, off)
{
    if (offset_in_page(off) != 0)
        return -EINVAL;
    return ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
}

先看 mmap 的完整调用链:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
用户态 mmap()
  ┌──────────────────────────────────────────────────────┐
  │ arch/arm64/kernel/sys.c:21  SYSCALL_DEFINE6(mmap)    │  ← 架构入口
  │   检查 offset 页对齐, off >> PAGE_SHIFT              │
  └──────────────────────────┬───────────────────────────┘
  ┌──────────────────────────────────────────────────────┐
  │ mm/mmap.c:1583  ksys_mmap_pgoff()                   │  ← 内核通用
  │   解析 fd → struct file *,处理 hugepage             │
  └──────────────────────────┬───────────────────────────┘
  ┌──────────────────────────────────────────────────────┐
  │ mm/util.c:506  vm_mmap_pgoff()                      │  ← 加锁审计
  │   security_mmap_file() LSM 安全检查                  │
  │   mmap_write_lock_killable(mm) 获取写锁              │
  └──────────────────────────┬───────────────────────────┘
  ┌──────────────────────────────────────────────────────┐
  │ mm/mmap.c:1404  do_mmap()                           │  ← 核心创建
  │   地址选择、权限检查、vm_flags 计算                   │
  └──────────────────────────┬───────────────────────────┘
  ┌──────────────────────────────────────────────────────┐
  │ mm/mmap.c:1716  mmap_region()                       │  ← VMA 安装
  │   分配 vm_area_struct、设置 vm_ops、插入红黑树        │
  └──────────────────────────────────────────────────────┘
  mm_populate() (若 MAP_POPULATE 或 VM_LOCKED)

do_mmap() 处理流程

do_mmap() (mm/mmap.c:1404-1581) 是 mmap 的核心函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
        do_mmap(file, addr, len, prot, flags, pgoff, &populate, &uf)
            ┌─────────────────────┐
            │  参数合法性检查      │  ← len==0 → EINVAL; 地址对齐; 溢出
            └─────────┬───────────┘
            ┌─────────────────────┐
            │ get_unmapped_area() │  ← 查找或验证可用虚拟地址
            └─────────┬───────────┘
            ┌─────────────────────┐
            │  合成 vm_flags      │  ← calc_vm_prot_bits + calc_vm_flag_bits
            └─────────┬───────────┘
        ┌──────────────┴──────────────┐
        │                             │
   file != NULL                  file == NULL
   (文件映射)                     (匿名映射)
        │                             │
        ▼                             ▼
  ┌──────────────────┐      ┌───────────────────────┐
  │ · 检查文件权限    │      │ · MAP_SHARED: pgoff=0 │
  │ · 验证 f_op->mmap │      │ · MAP_PRIVATE:        │
  │ · MAP_SHARED      │      │   pgoff=addr>>PSHIFT  │
  │   需写权限        │      └──────────┬────────────┘
  └────────┬─────────┘                 │
           │                            │
           └──────────┬─────────────────┘
            ┌─────────────────────┐
            │   mmap_region()     │  ← 真正的 VMA 创建
            └─────────────────────┘

mmap_region() — VMA 安装

mmap_region() (mm/mmap.c:1716-1871) 将 VMA 真正安装到进程地址空间:

步骤 代码位置 说明
地址空间检查 may_expand_vm() 检查进程 RLIMIT_AS 限制
解除旧映射 munmap_vma_range() 如果有旧映射先解除
尝试合并 VMA vma_merge() 与相邻 VMA 合并
分配 VMA 结构体 vm_area_alloc() 分配 struct vm_area_struct
文件映射 call_mmap(file, vma) 调用 f_op->mmap() 设置 vm_ops
匿名共享映射 shmem_zero_setup(vma) 创建 shmem 伪文件
匿名私有映射 vma_set_anonymous(vma) vm_ops = NULL
插入树/链表 vma_link() 插入红黑树和双向链表
统计 vm_stat_account() 更新内存统计

内核页表映射函数调用链

mmap 建立地址映射的最终落脚点是填充页表。ARM64 的页表填充通过 arch/arm64/mm/mmu.c 中的函数链完成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
__create_pgd_mapping(pgdir, phys, virt, size, prot, alloc, flags)  ← 第 363 行
    │  遍历 PGD 条目 (按 pgd_addr_end 分段)
    └─ alloc_init_pud()                                          ← 第 308 行
        │  p4d_offset() 访问 P4D → __p4d_populate() 分配 PUD 表
        │  尝试 1GB 大块映射 (pud_set_huge)
        └─ alloc_init_cont_pmd()                                 ← 第 255 行
            │  __pud_populate() 分配 PMD 表
            └─ init_pmd()                                        ← 第 218 行
                │  pmd_set_fixmap_offset() 定位 PMD 条目
                │  尝试 2MB 大块映射 (pmd_set_huge)
                └─ alloc_init_cont_pte()                         ← 第 179 行
                    │  __pmd_populate() 分配 PTE 表
                    └─ init_pte()                                ← 第 155 行
                        │  pte_set_fixmap_offset() 定位 PTE 条目
                        │  set_pte() 写入页表项
                        │  逐页: pfn_pte(__phys_to_pfn(phys), prot)

文件映射 vs 匿名映射

内核通过 vm_ops 函数指针表实现多态机制,三种映射方式在 mmap_region() 中分支:

特性 文件映射 匿名共享 匿名私有
vm_file 真实文件 struct file * shmem 伪文件 NULL
vm_ops 文件系统设置(如 ext4_file_vm_ops &shmem_vm_ops NULL
设置位置 call_mmap() + f_op->mmap() shmem_zero_setup() vma_set_anonymous()
缺页处理 vma->vm_ops->fault() → 从磁盘读取 shmem_fault() → 共享内存 do_anonymous_page() → 零填充页

两种映射没有本质区别——区别仅在于映射的目的物理页不同

  • 文件映射:映射的物理地址指向文件的页缓存(page cache)。Linux 将磁盘文件预读取至页缓存,访问页缓存等同于访问磁盘文件。
  • 匿名映射:映射的物理地址指向匿名页(由 do_anonymous_page() 分配的零填充页,或 shmem 管理的共享页)。
1
2
3
4
5
6
7
8
文件映射原理:

  进程虚拟地址                  页缓存                  磁盘文件
  ┌──────────┐              ┌──────────┐            ┌──────────┐
  │ 虚拟地址  │  ──页表──→   │ 物理页   │  ──回写→   │ 文件数据  │
  │  (VMA)   │              │ (Page)   │  ←预读取─  │   on     │
  └──────────┘              └──────────┘            │  Disk    │
                                                      └──────────┘

mmap demo

以下示例展示 mmap 的三种典型用法,并结合 readelfobjdump/proc/ 文件系统逐一分析。

示例程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/*
 * mmap_demo.c — 演示 mmap 三种典型用法
 * 编译: gcc -O2 -o mmap_demo mmap_demo.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>

int main(int argc, char *argv[])
{
    /* ─── 1. 匿名私有映射 (分配大块内存) ─── */
    size_t anon_len = 4096 * 10;  /* 10 页 */
    void *anon = mmap(NULL, anon_len,
                      PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS,
                      -1, 0);
    if (anon == MAP_FAILED) {
        perror("匿名映射失败");
        exit(1);
    }
    strcpy((char *)anon, "Hello from anonymous mmap!");
    printf("[匿名映射] 地址: %p, 长度: %zu bytes, 内容: %s\n",
           anon, anon_len, (char *)anon);
    printf("  按 ENTER 继续..."); getchar();

    /* ─── 2. 文件映射 (读/写文件) ─── */
    int fd = open("/tmp/mmap_test.txt",
                  O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd < 0) { perror("open"); exit(1); }

    /* 先准备好文件内容 */
    const char *msg = "mmap file I/O demo\n";
    write(fd, msg, strlen(msg) + 1);

    size_t file_len = 4096;  /* 映射 1 页 */
    void *file_map = mmap(NULL, file_len,
                          PROT_READ | PROT_WRITE,
                          MAP_SHARED, fd, 0);
    if (file_map == MAP_FAILED) {
        perror("文件映射失败");
        exit(1);
    }
    close(fd);  /* 映射后可以关闭 fd */

    printf("[文件映射] 地址: %p, 长度: %zu\n", file_map, file_len);
    printf("  文件内容: %s\n", (char *)file_map);
    /* 通过内存修改文件内容 */
    memcpy(file_map, "MODIFIED via mmap!", 19);
    printf("  修改后, 查看文件: $ cat /tmp/mmap_test.txt\n");
    printf("  按 ENTER 继续..."); getchar();

    /* ─── 3. 匿名共享映射 (IPC 模拟) ─── */
    size_t shm_len = 4096;
    void *shared = mmap(NULL, shm_len,
                        PROT_READ | PROT_WRITE,
                        MAP_SHARED | MAP_ANONYMOUS,
                        -1, 0);
    if (shared == MAP_FAILED) {
        perror("共享映射失败");
        exit(1);
    }
    sprintf((char *)shared, "PID=%d shared data", getpid());
    printf("[共享映射] 地址: %p, 内容: %s\n", shared, (char *)shared);

    /* ─── 暂停, 便于观察 /proc/pid/maps ─── */
    printf("\n查看进程内存映射: $ cat /proc/%d/maps\n", getpid());
    printf("按 ENTER 退出清理..."); getchar();

    munmap(anon, anon_len);
    munmap(file_map, file_len);
    munmap(shared, shm_len);
    return 0;
}

编译与运行

1
2
$ gcc -O0 -o mmap_demo mmap_demo.c
$ ./mmap_demo

使用 readelf 分析 ELF 布局

先看编译后的程序本身有哪些内存区域会被内核加载:

1
$ readelf -S mmap_demo          # 查看节区表 (Section Headers)

关键输出片段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [13] .text             PROGBITS         0000000000001060  00001060
       0000000000000b55  0000000000000000  AX       0     0     16
  [16] .rodata           PROGBITS         0000000000002000  00002000
       0000000000000208  0000000000000000   A       0     0     8
  [24] .data             PROGBITS         0000000000004020  00003020
       0000000000000108  0000000000000000  WA       0     0     8
  [25] .bss              NOBITS           0000000000004140  00003128
       0000000000000028  0000000000000000  WA       0     0     8
节区 地址 文件偏移 说明
.text 0x1060 0x1060 代码段,AX(可分配+可执行)
.rodata 0x2000 0x2000 只读数据(字符串常量),A
.data 0x4020 0x3020 已初始化全局数据,WA(可写)
.bss 0x4140 未初始化全局数据(不占文件空间)

再用 readelf -l 查看程序头(决定内核如何加载):

1
$ readelf -l mmap_demo
1
2
3
4
5
6
7
Program Headers:
  Type           Offset   VirtAddr          PhysAddr          FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000000040 0x0000000000000040 0x000208 0x000208 R   0x8
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x000878 0x000878 R   0x1000
  LOAD           0x001000 0x0000000000001000 0x0000000000001000 0x00bb5  0x00bb5  R E 0x1000
  LOAD           0x002000 0x0000000000002000 0x0000000000002000 0x000208 0x000208 R   0x1000
  LOAD           0x002dd8 0x0000000000003dd8 0x0000000000003dd8 0x000258 0x000390 RW  0x1000

每个 LOAD 段对应内核加载时需要创建的一个 VMA:

文件偏移 虚拟地址 文件大小 内存大小 标志 加载内容
LOAD #1 0x000000 0x000000 0x878 0x878 R ELF header, 只读
LOAD #2 0x001000 0x001000 0xbb5 0xbb5 R E .text 代码段
LOAD #3 0x002000 0x002000 0x208 0x208 R .rodata
LOAD #4 0x002dd8 0x003dd8 0x258 0x390 RW .data + .bss

注意第 4 个段:MemSiz > FileSiz,多出的部分就是 .bss(内核启动时清零)。

使用 objdump 反汇编 mmap 调用

反汇编 mmap 在 PLT 中的跳转和实际调用:

1
$ objdump -d mmap_demo | grep -A 20 '<main>'

关注 call 指令后的函数调用关系:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
0000000000001200 <main>:
    1200:  push   %rbp
    1201:  mov    %rsp,%rbp
    1204:  ...                    # 准备栈帧

    # 匿名映射: mmap(NULL, 40960, PROT_READ|PROT_WRITE,
    #                 MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
    1258:  mov    $0xffffffff,%r9d    # offset = -1
    125e:  mov    $0x22,%r8d          # flags = MAP_PRIVATE|MAP_ANONYMOUS
    1264:  mov    $0x3,%ecx           # prot = PROT_READ|PROT_WRITE
    1269:  mov    $0xa000,%edx        # length = 40960
    126e:  xor    %esi,%esi           # addr = NULL
    1270:  xor    %edi,%edi
    1272:  call   <mmap@plt>          # → PLT → glibc → syscall
    ...

mmap@plt 会跳转到 glibc 的 mmap 包装函数,最终执行 syscall 指令陷入内核。

通过 /proc/ 观察运行时的内存映射

运行程序并在暂停时查看 /proc/<pid>/maps

1
$ cat /proc/`pgrep mmap_demo`/maps

输出示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
5566c1a4d000-5566c1a4e000 r--p 00000000 08:01 2883744  mmap_demo    # ELF 头(LOAD #1)
5566c1a4e000-5566c1a4f000 r-xp 00001000 08:01 2883744  mmap_demo    # .text (LOAD #2)
5566c1a4f000-5566c1a50000 r--p 00002000 08:01 2883744  mmap_demo    # .rodata (LOAD #3)
5566c1a50000-5566c1a51000 r--p 00003000 08:01 2883744  mmap_demo    # .data relro
5566c1a51000-5566c1a52000 rw-p 00003000 08:01 2883744  mmap_demo    # .data + .bss (LOAD #4)
5566c1b7d000-5566c1b7e000 rw-p 00000000 00:00 0        [heap]       # 堆
7f0e8db8c000-7f0e8db9d000 rw-p 00000000 00:00 0        [anon]       # ← 匿名映射 (10页)
7f0e8db9d000-7f0e8db9e000 rw-s 00000000 08:01 2883759  /tmp/mmap_test.txt  # ← 文件映射
7f0e8db9e000-7f0e8db9f000 rw-s 00000000 00:00 0        [anon]       # ← 匿名共享映射
...
7ffe96e50000-7ffe96e71000 rw-p 00000000 00:00 0        [stack]      # 栈
7ffe96eef000-7ffe96ef3000 r--p 00000000 00:00 0        [vvar]
7ffe96ef3000-7ffe96ef5000 r-xp 00000000 00:00 0        [vdso]       # 内核提供的 VDSO

每个字段的含义(以文件映射行为例):

1
2
3
4
5
6
7f0e8db9d000-7f0e8db9e000  rw-s  00000000  08:01  2883759  /tmp/mmap_test.txt
├────── VMA 范围 ──────┤   ├┤  ├─offset─┤ ├──┼──┼─├──── inode ────┤
│ 起始地址-结束地址          ││  文件偏移   设备  inode  文件名
                            ││
                            │└─ s = MAP_SHARED / p = MAP_PRIVATE
                           └── 权限: r=读 w=写 x=执行

再看更详细的 /proc/<pid>/smaps

1
$ cat /proc/`pgrep mmap_demo`/smaps | grep -A 10 "7f0e8db9d000"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
7f0e8db9d000-7f0e8db9e000 rw-s 00000000 08:01 2883759  /tmp/mmap_test.txt
Size:                  4 kB     # VMA 大小
KernelPageSize:        4 kB     # 内核页大小
Rss:                   4 kB     # 实际驻留物理内存
Pss:                   4 kB     # 按共享比例分摊的物理内存
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         4 kB     # 私有脏页
Referenced:            4 kB
Anonymous:             0 kB
VmFlags: rd wr sh mr mw me ms  # VMA 标志

综合分析对照表

将 ELF 静态布局、内核加载、运行时 /proc/ 观测三方面对应起来:

ELF 节区 (readelf) VMA 起始 (maps) 权限 文件偏移 说明
.text (LOAD #2) 0x5566c1a4e000 r-xp 0x1000 代码段,readelf 地址+偏移一致
.rodata (LOAD #3) 0x5566c1a4f000 r–p 0x2000 只读数据
.data + .bss (LOAD #4) 0x5566c1a51000 rw-p 0x3000 可读写数据,MemSiz > FileSiz
匿名 mmap 0x7f0e8db8c000 rw-p 0x00 MAP_ANONYMOUS, fd=-1
文件 mmap 0x7f0e8db9d000 rw-s 0x00 MAP_SHARED, fd=/tmp/...
共享匿名 mmap 0x7f0e8db9e000 rw-s 0x00 `MAP_SHARED

关键观察

  1. 匿名映射无后备文件/proc/pid/maps 中显示 [anon][heap],无文件名
  2. 文件映射关联 inode/proc/ 输出中可见设备号和 inode,与 ls -i 一致
  3. MAP_SHARED vs MAP_PRIVATE:权限字段末位 s vs p,决定写时复制行为
  4. 文件偏移来自 ELF LOAD 段:.text 段在文件偏移 0x1000,映射后 VMA 起始也看到偏移 0x1000
  5. 缺页按需填充smapsRss < Size(未访问的页尚未分配物理页)

关键源码文件索引

文件 内容
arch/arm64/kernel/sys.c:21 arm64 mmap 系统调用入口
mm/mmap.c:1404 do_mmap() 核心映射函数
mm/mmap.c:1583 ksys_mmap_pgoff() 内核通用入口
mm/mmap.c:1716 mmap_region() VMA 安装
mm/util.c:506 vm_mmap_pgoff() 加锁审计
arch/arm64/mm/mmu.c:363 __create_pgd_mapping() 页表创建
arch/arm64/include/asm/pgtable-types.h:23 页表类型(pte_t/pmd_t/pud_t/pgd_t)
arch/arm64/include/asm/pgtable-hwdef.h:41 页表位移常量
include/linux/mm_types.h:319 struct vm_area_struct
include/linux/mm_types.h:70 struct page
include/linux/mm.h:588 struct vm_operations_struct

一张图搞懂mmap实现原理

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计