Linux的mmap 源码分析

本文详细介绍了Linux的mmap内存映射功能,包括mmap的基础概念,如文件映射和匿名映射,以及内存可见性。讨论了Linux关键结构体task_struct、mm_struct和vm_area_struct,并分析了mmap内存映射的原理和源码流程,涉及arch_get_unmapped_area()、mmap_region()等函数。最后探讨了mmap如何申请虚拟内存以及物理内存的映射时机,并解释了为何在缺页中断时分配物理内存的原因。
摘要由CSDN通过智能技术生成

1、mmap基础概念

mmap() 将文件或设备映射到内存中。这是一种内存映射文件I / O的方法,实现用户进程和内核空间的映射。

1.1 功能特点:

mmap分为文件映射和匿名映射

  1. 文件映射:将进程的虚拟内存区域映射到文件。即读取那些内存区域将导致文件被读取,这是默认的映射类型。
  2. 匿名映射:映射该进程的虚拟内存中没有任何文件支持的区域,内容初始化为零。系统都通过MAP_ANONYMOUSMAP_ANON标志实现了匿名映射,但它并不是POSIX标准的一部分。

1.2 内存可见性:

如果设置MAP_SHARED标志,则将在fork()系统调用中保留该映射。即所有相关(及其子进程)进程中,一个进程对映射区域的写操作,对相关其他进程立即可见。说成人话:不同进程映射了同一块内存,所有进程对这块区域具有可见性。

1.3 Linux关键结构体

1.3.1 task_struct

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内存结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Rw4KODH-1608715193026)(/Users/cvter/Library/Application Support/typora-user-images/image-20201202102103290.png)]

由上图可以看出,进程的虚拟地址空间,由多个虚拟内存区域构成。虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具有同样特性的连续地址范围。上图中所示的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内存映射的实现过程,总的来说可以分为三个阶段:

  1. 进程在用户空间调用库函数mmap()
  2. 在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址
  3. 为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化
  4. 将新建的虚拟区结构(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
  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值