sys_mmap(调用号为9)和sys_munmap (调用号为11)是 Linux 中功能最强大的内存管理接口之一。二者分别可用于将文件或匿名内存映射到进程的虚拟地址空间以及取消前者所建立的映射,从而实现文件 I/O 的内存语义、堆外大块内存分配、共享内存、线程栈空间分配等功能。

  • sys_mmap

    • 函数原型

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

      • addr:建议的映射起始地址(通常传 NULL,即让内核自动选择合适的地址)。
        如果与 MAP_FIXED 标志一起使用,则必须是页对齐地址,且会覆盖该处原有映射。
      • length:映射的字节数。内核会向上取整到页大小的倍数。
      • prot:映射区域的访问权限(可按位或组合):
        • PROT_NONE:页面不可访问
        • PROT_READ:页面可读
        • PROT_WRITE:页面可写
        • PROT_EXEC:页面可执行
        • PROT_SEM:页面可用于原子操作同步(自 Linux 2.5.7 起支持)
      • flags:映射的类型和属性(可按位或组合):
        • 基本属性
          • MAP_SHARED:修改共享到文件或其它进程
          • MAP_PRIVATE:写时复制(COW),修改不会影响底层文件
        • 地址控制
          • MAP_FIXED:强制在 addr 指定位置映射,若冲突则覆盖(危险)
          • MAP_FIXED_NOREPLACE:强制映射但不覆盖已有区域(Linux 4.17+)
          • MAP_32BIT:将映射放在低 2GB 地址空间
          • MAP_STACK:提示内核将区域当作栈(自动增长)
        • 匿名/特殊
          • MAP_ANONYMOUS:匿名映射(不依赖文件),fd 必须为 1
          • MAP_GROWSDOWN:向下增长(栈使用)
          • MAP_HUGETLB:使用大页内存(HugeTLB
        • 性能/控制
          • MAP_POPULATE:立即预取(触发缺页加载)
          • MAP_NONBLOCK:结合 MAP_POPULATE,后台加载
          • MAP_NORESERVE:不为交换区预留空间
          • MAP_LOCKED:将映射锁定在物理内存中
          • MAP_SYNC:与持久化存储同步(需 DAX 支持)
          • MAP_UNINITIALIZED:允许返回未清零内存(仅某些内核配置)
      • fd:文件描述符。如果是匿名映射,则必须为 1
      • offset:文件中映射的起始偏移,必须是页大小的整数倍。
    • 返回值

      • 成功时返回映射区的起始地址(void * 指针)。
      • 失败时返回-1 并设置errno
  • sys_munmap

    • 函数原型

      1
      int munmap(void *addr, size_t length);
    • 参数

      • addr:要取消的映射区的起始地址。
      • length:映射的字节数。
    • 返回值

      • 成功时返回0
      • 失败时返回-1 并设置errno
  • 用例

    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
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    	.equ MAP_SHARED, 0x01
    .equ PROT_READ, 0x01
    .equ PROT_WRITE, 0x02
    .equ PROT_READ_WRITE, 0x03

    .section .data
    filename:
    .ascii "/tmp/test.txt\0"
    mmap_msg:
    .ascii "Data from mmap\n\0"
    mmap_msg_len = . - mmap_msg
    write_msg:
    .ascii "\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
    write_msg_len = . - write_msg
    errmsg:
    .ascii "Error\n\0"
    errmsg_len = . - errmsg

    .section .bss
    .lcomm mapped_addr, 8 # 存储映射的地址
    .lcomm fd, 4

    .section .text
    .globl _start

    _start:
    # 开启一个文件描述符 %r12 读取 /tmp/test.txt
    mov $2, %rax
    lea filename(%rip), %rdi
    mov $0x242, %rsi
    mov $0644, %rdx
    syscall
    mov %rax, fd(%rip)

    # 调用 write 往文件中写入数据
    mov $1, %rax
    mov fd(%rip), %rdi
    lea write_msg(%rip), %rsi
    mov $write_msg_len, %rdx
    syscall

    # 调用 mmap 将文件映射到内存
    mov $9, %rax
    xor %rdi, %rdi # addr = NULL
    mov $mmap_msg_len, %rsi # length = msg_len
    mov $PROT_READ_WRITE, %rdx # prot = PROT_READ | PROT_WRITE
    mov $MAP_SHARED, %r10 # flags = MAP_SHARED
    mov fd(%rip), %r8 # fd
    xor %r9, %r9 # offset = 0
    syscall

    # 检查 mmap 是否出错
    cmp $-1, %rax
    jle exit_error

    # 将 mmap 返回的地址保存到 mapped_addr
    mov %rax, mapped_addr(%rip)

    # 将数据从 msg 复制到映射的内存区域
    mov mapped_addr(%rip), %rdi # 目标地址 (dst) = mapped_addr
    lea mmap_msg(%rip), %rsi # 源地址 (src) = message
    mov $mmap_msg_len, %rcx # 复制长度 (count) = msg_len
    cld # 清除方向标志
    rep movsb # 循环复制字节

    # 关闭映射
    mov $11, %rax
    mov mapped_addr(%rip), %rdi
    mov $mmap_msg_len, %rsi
    syscall

    # 关闭文件描述符
    mov $3, %rax
    mov fd(%rip), %rdi
    syscall

    # 退出程序
    mov $60, %rax
    xor %rdi, %rdi
    syscall

    exit_error:
    mov %rax, %r12
    mov $1, %rax
    mov $2, %rdi
    lea errmsg(%rip), %rsi
    mov $errmsg_len, %rdx
    syscall

    mov $60, %rax
    mov %r12, %rdi
    syscall

    分析

    上述代码演示了如何使用sys_mmapsys_munmap 将一个文件映射到进程的内存空间。

    程序最开始通过调用openwrite ,先是创建了目标文件,然后向该文件中写入了一段较长的字符串,内容为十四个空位\0 ,目的是为了先填充文件大小至14个字节,以防止爆出总线错误SIGBUS

    程序在调用mmap 时将系统调用号9sys_mmap)存入%rax。然后,它依次将各个参数加载到相应的寄存器:addr (%rdi)设为0NULL),让内核自动选择地址;length (%rsi)设为msg_lenprot (%rdx)设为可读写权限;flags (%r10)设为MAP_SHAREDfd (%r8)设置为先前打开的文件描述符;offset (%r9)设为0。等效为:

    1
    void * mapped_addr = mmap(NULL, msg_len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

    执行syscall后,如果成功,%rax中将包含映射区域的起始地址。程序将这个地址保存起来,然后可以使用它来读写文件,就像读写普通内存一样。

    在执行完上述操作之后,程序又调用了munmap 关闭了前面建立的映射,并关闭了整个程序。