目录
- [1. mmap](#1. mmap)
- 2.malloc的原理
1. mmap
(1)文件映射
我们访问文件都是调用系统调read、write,这本质上是和文件缓冲区进行IO,之后系统自动刷新缓冲区到磁盘中。我们可以认为拿到文件缓冲区的访问权,我们就能访问文件。 除此之外,我们还可以将文件缓冲区映射到进程地址空间中,像动态库那样,这样的话我们就可以通过直接访问进程地址空间来操作文件缓冲区,进而不使用系统调用来操作文件。多个进程都可以这么做,就像一个动态库实例可以被映射到多个进程那样,这就是共享内存的原理 ,因此,我们可以说共享内存本质上还是看到了同一块文件缓冲区。
mmap就是负责把内核中的文件缓冲区,直接映射到进程地址空间中,我们也可以用此函数实现共享内存。
(2)mmap映射后的文件缓冲区
当我们利用mmap将文件缓冲区映射到进程中后,我们就可以像操作内存一样,通过指针直接读写这块地址空间,而这些操作会自动同步到对应的文件缓冲区,最终由内核刷新到磁盘。 这就直接越过了系统调用,因为调用read、write实际上是间接和缓冲区打交道的,而mmap映射后我们可以直接进行访问。
对于多个进程而言,如果多个进程同时映射同一个文件缓冲区,它们看到的就是同一块区域,即共享内存 ,因此可见共享内存根本不是什么特殊的内存区域,就是多个进程共享了同一块文件缓冲区而已。相比命名管道而言,共享内存的优势在于随机访问,mmap后拿到的是一整个内存块。而命名管道虽然是内存级文件,但命名管道不支持随机访问,更适合流式通信。
因此我们可以总结mmap的核心用途:高效读写文件,实现进程间通信(共享内存)
(3)mmap参数含义
下面是mmap的声明
c
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
- 返回值 :成功则返回映射区域的虚拟地址起始地址 ;失败则返回
MAP_FAILED(本质就是把-1强转成void*) - 参数详解 :
addr:期望的映射起始地址。一般传nullptr,让内核自动选择最合适的地址;如果传具体地址,内核会尽量满足,但不保证length:要映射到地址空间的字节数。注意:系统实际分配的大小一定是页面大小的整数倍(通常是4KB) 。比如你传3500字节,内核会给你映射4096字节(1页),因为操作系统管理内存的最小单位是页。但注意我们实际上能访问的还是3500字节,申请多少访问多少,只不过物理层面开辟的空间会对齐。要理解这个,我们须知道对齐操作是为了提高硬件效率设计出来的,操作系统要是处处根据其硬件实际处理来调节访问权限,那就乱套了!也就是说进程地址空间和物理内存都会浪费一定空间。prot:映射区域的访问权限,用宏定义按位或组合:PROT_READ:可读PROT_WRITE:可写PROT_EXEC:可执行
- (重点,后续会讲)
flags:映射类型,最常用的两个:MAP_PRIVATE | MAP_ANONYMOUS:私有匿名映射。对映射区域的修改不会写回底层文件,也不会被其他进程看到(写时复制机制)MAP_SHARED:共享映射。对映射区域的修改会写回底层文件,并且对所有映射该文件的进程可见
fd:要映射的文件描述符。如果是匿名映射(不需要文件),传-1offset:从文件的哪个位置开始映射。比如文件有3000字节,offset=1000就表示从第1000字节开始映射,此时length传2000就够了
(4)munmap
映射的内存使用完后,必须手动释放,否则会造成内存泄漏:
c
int munmap(void* addr, size_t length);
addr:就是mmap返回的起始地址,就是需要回收的地址length:和mmap中传入的length保持一致
(5)mmap的使用
a.写文件
用mmap写文件有一个非常重要的前提:必须先保证文件的大小足够容纳你要映射的区域 。因为mmap只负责映射文件缓冲区,它不会自动帮你扩容文件。
所以写文件的步骤是:
open打开文件(如果不存在就创建)- 用
ftruncate调整文件到目标大小(空文件必须做这一步!) mmap映射文件- 直接通过指针操作内存,完成写入
munmap取消映射close关闭文件
c
// 示例:用mmap写入"Hello mmap"到文件
int fd = open("test.txt", O_RDWR | O_CREAT, 0644);
ftruncate(fd, 10); // 把文件调整到10字节大小,默认填充0
char* buf = (char*)mmap(nullptr, 10, PROT_WRITE, MAP_SHARED, fd, 0); // 共享映射
strcpy(buf, "Hello mmap"); // 直接写内存就等于写文件
munmap(buf, 10);
close(fd);
注意:
ftruncate调整后的文件,未写入的部分默认是0,用文本编辑器打开可能会显示成^@乱码。
b.读文件
读文件相对简单,不需要提前扩容,只需要先获取文件的实际大小即可:
open打开文件- 用
fstat获取文件属性,得到文件大小st_size mmap映射整个文件(或部分)- 直接通过指针读取内存
munmap取消映射close关闭文件
c
// 示例:用mmap读取文件内容
int fd = open("test.txt", O_RDONLY);
struct stat st;
fstat(fd, &st);
char* buf = (char*)mmap(nullptr, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
printf("%s\n", buf); // 直接读内存就等于读文件
munmap(buf, st.st_size);
close(fd);
对于大文件,我们可以通过
length + offset的组合,分段映射文件的不同部分,实现大文件的分片映射。同时注意,不管读文件还是写文件,mmap映射的区域不要超过文件缓冲区的大小,因为这样会出现越界访问。
2.malloc的原理
(1)共享映射和私有映射
这是mmap最重要的参数,理解了这个参数我们可以进一步理解内存开辟的本质,特别是私有映射。
a.共享映射(MAP_SHARED)
对映射区域的修改会实时写回磁盘 ,并且对所有映射该文件的进程可见。这就是实现共享内存的基础。因此我们可以实现进程间通信(IPC),多个进程同时映射同一个磁盘文件,就可以通过这块共享内存直接交换数据。
b.私有映射(MAP_PRIVATE)和匿名映射(MAP_ANONYMOUS)
私有映射,私有两字即对映射区域的修改不会写回磁盘 ,也不会被其他进程看到。内核采用写时拷贝(Copy-On-Write) 机制,当你修改私有映射的内存时,内核会复制一份新的内存给你,后续的修改都在这个副本上进行。也就类似于进程保持数据独立性的方法一样,映射后保持只读,只要修改触发中断,根据访问地址确定是否为越界访问,如果不是则进行写时拷贝。 因此我们可以理解为mmap映射了一个内存级文件,其实就是在进程地址空间中映射出来一块可用的内存,并且这个内存只能该进程使用(相比而言,共享映射强调通信)。
只读文件的读取不需要写回磁盘的场景,用私有映射更安全。
匿名映射是一种特殊的映射,它不需要关联任何磁盘文件,直接由内核分配一块物理内存映射到进程地址空间。
下面是私有匿名映射的使用:
c
void* buf = mmap(nullptr, 1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
问题是这有什么用?这不就是malloc、加载动态库的原理吗?
malloc会直接调用mmap分配私有匿名映射内存。以及进程加载动态库,就是把动态库文件映射到自己的地址空间,用的就是私有映射。
或者父子进程间的共享内存(父进程先创建匿名共享映射,再fork子进程,子进程会继承这个映射)都是使用mmap的这个选项。mmap默认映射的空间是在堆栈之间的,我们也可自己指定。
(2)mmap的底层原理
mmap的底层实现,核心依赖内核中的两个数据结构:
struct mm_struct:每个进程都有一个,描述整个进程的地址空间struct vm_area_struct:描述地址空间中的一个连续区域(比如代码段、数据段、堆、栈、mmap映射区)
vm_area_struct中几个关键字段:
vm_start:映射区域的起始虚拟地址vm_end:映射区域的结束虚拟地址vm_mm:指向所属进程的mm_structvm_file:指向映射的文件对象(如果是文件映射);如果是匿名映射,这个字段为NULL
当我们调用mmap时,内核会做两件事:
- 在进程的地址空间中,找到一块合适的空闲区域,创建一个新的
vm_area_struct - 如果是文件映射,就把这个
vm_area_struct和对应的文件对象关联起来。如果像malloc那样只是为了开辟内存,则为NULL
注意:此时内核并没有立即分配物理内存,只有当进程第一次访问这个虚拟地址时,才会触发缺页异常,内核才会真正分配物理页 ,并建立虚拟地址到物理地址的映射。这就是mmap的延迟分配机制。