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必须为1MAP_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_mmap与sys_munmap将一个文件映射到进程的内存空间。程序最开始通过调用
open和write,先是创建了目标文件,然后向该文件中写入了一段较长的字符串,内容为十四个空位\0,目的是为了先填充文件大小至14个字节,以防止爆出总线错误SIGBUS。程序在调用
mmap时将系统调用号9(sys_mmap)存入%rax。然后,它依次将各个参数加载到相应的寄存器:addr(%rdi)设为0(NULL),让内核自动选择地址;length(%rsi)设为msg_len;prot(%rdx)设为可读写权限;flags(%r10)设为MAP_SHARED;fd(%r8)设置为先前打开的文件描述符;offset(%r9)设为0。等效为:1
void * mapped_addr = mmap(NULL, msg_len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
执行
syscall后,如果成功,%rax中将包含映射区域的起始地址。程序将这个地址保存起来,然后可以使用它来读写文件,就像读写普通内存一样。在执行完上述操作之后,程序又调用了
munmap关闭了前面建立的映射,并关闭了整个程序。