1、mmap基础概念
mmap() 将文件或设备映射到内存中。这是一种内存映射文件I / O的方法,实现用户进程和内核空间的映射。
1.1 功能特点:
mmap分为文件映射和匿名映射
- 文件映射:将进程的虚拟内存区域映射到文件。即读取那些内存区域将导致文件被读取,这是默认的映射类型。
- 匿名映射:会映射该进程的虚拟内存中没有任何文件支持的区域,内容初始化为零。系统都通过
MAP_ANONYMOUS
和MAP_ANON
标志实现了匿名映射,但它并不是POSIX标准的一部分。
1.2 内存可见性:
如果设置MAP_SHARED
标志,则将在fork()系统调用中保留该映射。即所有相关(及其子进程)进程中,一个进程对映射区域的写操作,对相关其他进程立即可见。说成人话:不同进程映射了同一块内存,所有进程对这块区域具有可见性。
1.3 Linux关键结构体
1.3.1 task_struct
task_struct这个结构体,它被叫做进程描述符,内部成员包含了很多与进程相关的信息,表示一个进程
struct task_struct {
// 进程状态
volatile long state; // -1不可运行,0可运行,> 0已停止
int exit_state;
// 进程地址空间
struct mm_struct *mm; // 进程所拥有的内存空间描述符,对于内核线程的mm为NULL
struct mm_struct *active_mm; // 指进程运行时所使用的进程描述符
// 进程标识符(PID)
pid_t pid;
pid_t tgid;
// 指定调度程序行为
unsigned int flags;
// 表示进程亲属关系的成员
struct task_struct __rcu *real_parent;
// 进程调度
int prio, static_prio, normal_prio;
unsigned int policy; // 表示进程的调度策略
}
1.3.2 mm_struct
内存描述符的结构体——mm_struct,抽象的来描述linux下进程的地址空间的所有的信息,一个task_struct只有一个mm_struct。
struct mm_struct {
//指向线性区对象的链表头
struct vm_area_struct * mmap; /* list of VMAs */
//指向线性区对象的红黑树
struct rb_root mm_rb;
//vma缓存 存放最近找到的vma(数组结构)
struct vm_area_struct * mmap_cache;
//标识第一个分配文件内存映射的线性地址
unsigned long mmap_base; /* base of mmap area */
// 用来在进程地址空间中搜索有效的进程地址空间的函数
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
// 指向页表的目录
pgd_t * pgd;
unsigned long total_vm; //进程地址空间的页数
unsigned long locked_vm; //锁住的页数,不能换出
}
1.3.3 vm_area_struct
进程虚拟内存描述符,linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域。vm_area_struct结构中包含区域起始和终止地址以及其他相关信息,同时也包含一个vm_ops指针,其内部可引出所有针对这个区域可以使用的系统调用函数。这样,进程对某一虚拟内存区域的任何操作需要用要的信息,都可以从vm_area_struct中获得。mmap函数就是要创建一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连。
// 该结构定义了一个内存VMM内存区域。
struct vm_area_struct {
/* 第一高速缓存行具有VMA树遍历的信息 */
struct mm_struct *vm_mm; // 所属的内存描述符。
unsigned long vm_start; //vma的起始地址
unsigned long vm_end; //vma的结束地址
// 该vma的在一个进程的vma链表中的前驱vma和后驱vma指针,链表中的vma都是按地址来排序的
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb; //红黑树rb(red black)中对应的节点(红黑树的引入就是为了解决当查找数量非常多时效率低下的问题,在红黑树中,搜索元素,插入,删除等操作,都会变得非常高效)
pgprot_t vm_page_prot; // vma的访问权限。
unsigned long vm_flags; // 标志,请参见mm.h
unsigned long vm_pgoff; //映射文件的偏移量,以PAGE_SIZE为单位
struct file * vm_file; // 我们映射到的文件(可以为NULL)
1.3.4 task_struct、mm_struct与vm_area_struct关系图
一个进程task_struct的虚拟地址空间主要由两个数据结来描述。一个是最高层次的:mm_struct,一个是较高层次的:vm_area_structs。最高层次的mm_struct结构描述了一个进程的整个虚拟地址空间。较高层次的结构vm_area_truct描述了虚拟地址空间的一个区间(简称虚拟区)。每个进程只有一个mm_struct结构,在每个进程的task_struct结构中,有一个指向该进程的结构。可以说,mm_struct结构是对整个用户空间的描述。
问:Linux内存使用红黑树和链表的作用是什么?
答:红黑树是方便遍历查找符合要求的结点(比如mmap的时候查找vma),链表是顺序结构,当需要顺序遍历时起到作用( 比如mm_take_all_locks())
1.4 mmap内存结构:
由上图可以看出,进程的虚拟地址空间,由多个虚拟内存区域构成。虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具有同样特性的连续地址范围。上图中所示的text数据段(代码段)、初始数据段、BSS数据段、堆、栈和内存映射,都是一个独立的虚拟内存区域。而为内存映射服务的地址空间处在堆栈之间的空余部分。
2. mmap函数
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
参数说明:
start:指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
length:映射区的大小
prot:映射区域的保护方式
- PROT_EXEC 映射区域可被执行
- PROT_READ 映射区域可被读取
- PROT_WRITE 映射区域可被写入
- PROT_NONE 映射区域不能存取
flags:影响映射区域的各种特性
- MAP_SHARED 对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
- MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,对此区域作的任何修改都不会写回原来的文件内容。
- MAP_ANONYMOUS 匿名映射,映射区不与任何文件关联
fd:如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1
offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍
返回值:若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。
问:为什么映射区的大小必须是物理页的整数倍?
物理内存的单位是页,而进程虚拟地址空间和内存的映射也是以页为单位,32位系统中一个物理页大小(page_size)通常是4K = 4096
3. mmap内存映射原理
mmap内存映射的实现过程,总的来说可以分为三个阶段:
- 进程在用户空间调用库函数mmap()
- 在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址
- 为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化
- 将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中
问:为什么mmap比常规文件操作效率高呢?
常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝(常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务)。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。
4. mmap源码分析
我们从一个简单的mmap demo开始入手,我们先open一个mapTest.txt,获得fd,然后在mmap()中传入fb。
问:mmap是怎样申请虚拟内存vm_area_struct,最终跟文件建立关联的?
答:我们从源码去寻找答案。
int main(int argc, const char* argv[]) {
// 打开一个文件
int fd = open("mapTest.txt", O_RDWR);
int len = lseek(fd, 0, SEEK_END);
// 创建内存映射区
void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if