今天学习一下 Linux 的 9 号系统调用 mmap,它在虚拟内存的管理中拥有着至高无上的地位。

虚拟内存

众所周知,虚拟内存是一种非常成功的计算机内存管理技术。它使得多个进程可以拥有相同的地址,并且应用层开发者不必考虑不同进程地址冲突的问题,所有的进程都可以使用同样地址的虚拟内存。

每个进程的虚拟内存都是独立的,进程看似拥有一大片内存,实际上能用的仅有一小部分,而正是虚拟内存技术拯救了这一切,使得物理内存不会被过度碎片化而导致利用率低下。

mmap函数介绍

它用于创建一片虚拟内存,这个虚拟内存可以是映射新分配的物理内存,也可以是映射已有的物理内存,甚至是映射磁盘文件。

同样先看它的函数原型:

1
2
3
4
5
6
#include <unistd.h>
#include <sys/mman.h>

void *mmap(void *start, size_t length, int prot, int flags,
int fd, off_t offset);
// 返回:若成功时则为指向映射区域的指针,若出错则为 MAP_FAILED(-1)。

参数解释:

  • start:按时 mmap 尽量以该地址为起点分配连续内存,如果给 NULL 值,则会随机分配。
  • length:指示虚拟内存的长度,一般会以页为单位。
  • prot:指示虚拟内存的保护属性,一般为下面四个宏或者其组合:
    • PROT_EXEC:这个区域内的页面由可以被 CPU 执行的指令组成。
    • PROT_READ:这个区域内的页面可读。
    • PROT_WRITE:这个区域内的页面可写。
    • PROT_NONE:这个区域内的页面不能被访问。
  • flags:指示该虚拟内存的映射方式,通常需要指定 MAP_SHARED 和 MAP_PRIVATE 中的其中一个,具体标志位如下所示:
    • MAP_FIXED:如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。
    • MAP_SHARED:对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
    • MAP_PRIVATE:对映射区域的写入操作会产生一个映射文件的复制,该内存区域所对的实际内存是写拷贝的,任何修改不会对原文件产生操作。
    • MAP_ANONYMOUS:建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
    • MAP_DENYWRITE:只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
    • MAP_LOCKED:将映射区域锁定住,该内存区域在程序运行时不会被交换出去。
  • fd:指示映射到内存的文件描述符。
  • offset:文件的偏移量。

mmap的具体用途

映射文件

为了避免大量 IO,可以选择以共享方式将文件映射到内存中,如果想操作文件可以仅仅修改映射的内存便可以达到修改文件的效果,下面是示例:

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
#include<stdio.h>
#include<unistd.h>
#include<sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
const char *filename="./tmpfile";

void prepare_file(){
unlink(filename);
int fd=open(filename,O_RDWR|O_CREAT,0777);
for(int i=0;i<0x1000;i++){
write(fd,"a",1);
}
close(fd);
}

int main(){
prepare_file();
int fd=open(filename,O_RDWR);
if(fd<0){
perror("open");
return 0;
}
MAP_SHARED;

void *area=mmap(NULL,0x1000,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(area==-1){
perror("mmap");
return 0;
}
close(fd);
for(int i=0;i<0x1000;i++){
((char *)area)[i]='1';
sleep(1);
}
}

在运行之后,创建一个带有 4096 个 a 的文件,并且每秒将内存中的 a 修改为 1,同时外部可以不停地 cat 文件,会发现文件会被同步修改。

匿名内存

在 flags 中加入标志 MAP_ANONYMOUS 可以选择不映射文件而是选择让内核自己找一片空的内存映射出来。这个如果还设置了共享标志位 MAP_SHARED,那么这段内存将只有一份,由其它所有映射了这片内存的进程共享。

因为是匿名内存,不关联任何对象,因此另外一个与此毫无关联的进程无法直接映射这片内存,所以如果要做多进程共享内存,则需要映射完内存之后 fork 进程,这样多个进程就会共享这片内存了。

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
#include<stdio.h>
#include<unistd.h>
#include<sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#ifndef MAP_ANONYMOUS
#define MAP_ANONYMOUS 0x20
#endif

int main(){

char *ptr=mmap(NULL,0x1000,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,0,0);
int pid=fork();
if(pid==0){
while(1){
for(int i=0;i<0x1000;i+=2){
ptr[i]='a';
sleep(1);
}


}
return 0;
}
pid=fork();
if(pid==0){
while(1){
for(int i=1;i<0x1000;i+=2){
ptr[i]='b';
sleep(1);
}
}
}
while(1){
puts(ptr);
sleep(1);
}
}

运行结果也可以发现两个子进程的修改都可以被父进程读取到。