[内核内存] 用户态进程虚拟内存管理

1 linux 用户态进程虚拟地址空间

在linux多任务操作系统中,每个用户态进程都有自己的虚拟地址空间,用户态进程虚拟地址空间主要分内核虚拟地址空间和用户虚拟地址空间:

  • 内核虚拟地址空间:内核总是驻留在内存中,是操作系统的一部分。内核虚拟地址空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数

  • 用户虚拟地址空间

    NAMEDESCRIPTION
    栈(stack)局部变量、函数参数、返回地址等,由系统分配和统一回收
    内存映射段(mmap)内核将硬盘文件的内容直接映射到内存,是一种方便高效的文件I/O方式(mmap或用malloc分配大于128K的内存块)
    堆(heap)堆用于存放进程运行时动态分配的内存段,可动态扩张或缩减。堆中内容是匿名的(malloc分配小于128k内存块)
    BSS段未初始化数据
    data段已初始化的数据
    代码段(text)代码段也称正文段或文本段,通常用于存放程序执行代码(即CPU执行的机器指令),通常代码段是可共享的,因此频繁执行的程序只需要在内存中拥有一份拷贝即可

用户态进程地址空间中的虚拟地址需要通过页表(Page Table)的映射才能获取到其对应的物理地址,页表由操作系统维护并被处理器引用。其中进程内核地址空间中的虚拟地址需要通过内核态的页表才能定位到其映射的物理地址,而进程用户地址空间中的虚拟地址则需要通过进程自己私有的页表才能定位到其映射的物理地址.

在Linux中,内核地址空间的页表是持续存在的,并且所有进程切换到内核模式所处的内核地址空间都共用一套内核页表,因此内核代码和数据总是可寻址,随时准备处理中断和系统调用。与此相反,进程处于用户模式时所处的用户地址空间的映射关系随进程切换的发生而不断变化,因此每个进程的用户虚拟地址空间对应的页表是私有的。

新版本arm架构的linux系统用两个寄存器来储存一级页表(pgd页表)的虚拟地址:ttbr0和ttbr1.

  1. 对于arm32架构linux会将进程私有pgd页表的虚拟地址保存在ttbr0寄存器中,而ttbr1寄存器不会使用,因为ttb0和ttb1两个寄存器搭配使用的情况下linux只支持2G,1G和512M等,但是ARM32虚拟地址空间的划分比例为1:3,用户空间是3G,内核空间是1G,所以上述寄存器硬件限制无法满足这种通用配置,所以ARM32未使用TTBR1寄存器。那么当进程从用户态切换到内核态或内核态切换到用户态时,为了避免切换页表带来的性能损耗,arm32系统的用户态进程的用户虚拟地址空间和内核虚拟地址空间使用了相同的页表基地址,存储在ttbr0寄存器中。具体实现方式是: 其中内核空间的pgd页表内容会在进程被fork时复制到进程的私有pgd页表中,也就是说arm32架构的linux系统下用户态进程将linux内核态页表中的一级页表(init_mm->swapper_pg_dir)中的内容拷贝到了该进程的私有一级页表(task_struct->mm_struct->pgd)中,最终mmu只需通过一个ttbr0寄存器中存储的pgd页表就能完成内核态和用态两个虚拟地址空间中的虚实地址转换操作。
  2. 对于arm64架构linux会将内核虚拟地址空间pgd页表(init_mm->swapper_pg_dir)的虚拟地址放在ttbr1寄存器中,而用户进程私有pgd页表(task_struct->mm_struct->pgd)的虚拟地址存放在ttbr0中。当内核需要将获取该进程的一个虚拟地址vaddr对应的物理地址时,先会对该64位虚拟地址vaddr的最高位进行判断:
    1. 若vaddr对应的最高位为1,则可以确定该虚拟地址位于进程的内核地址空间,因此从ttbr1寄存器中获取到内核态pgd页表的地址数据,用于mmu完成对vaddr的地址映射操作.
    2. 若vaddr对应的最高位为0,则可以确定该虚拟地址位于进程的用户地址空间,因此从ttbr0寄存器中获取到该进程私有页表的对应的地址数据,用于mmu完成对vaddr的地址映射操作.

此处扩展下,就是对于x86架构linux os只用一个CR3寄存器来存储进程的页表信息,当进程被fork时内核虚拟地址空间对应pgd页表的内容回会被复制到用户态进程私有的pgd页表中去(处理方式类似arm32架构的os).

1.1 arm32 用户进程的虚拟地址空间

在linux多任务操作系统中,每个用户态进程都有自己的虚拟地址空间。在arm32架构的linux操作系统下用户态进程的虚拟地址空间是一个4G的内存地址块,通常用户态进程内核态和用态所占虚拟内存比例是1:3,该比例可以通过配置文件根据实际情况来进行设定.

START					    END               			SIZE			USE						
-----------------------------------------------------------------------------
0x00000000					0xbfffffff					3G			USER(用户虚拟地址空间)
0xC0000000					0xffffffff					1G			KERNEL(内核虚拟地址空间)

ps:用户虚拟地址空间一般从ox0804800开始,前面空白部分为未使用地址空间

在这里插入图片描述

1.linux进程虚拟内存空间(32位)

1.2 arm64架构用户态虚拟地址空间.

对于arm64架构的linux,虽然虚拟地址已经达到64位,但是处理器的物理地址总线实际位宽并没有达到64位,通常为39位或48位,较新的arm架构可以支持到52位.

那么为什么arm64架构的的linux os物理地址总线位宽不支持到64位呢?因为物理地址总线宽度过高会给芯片设计带来较大难度,加之一个48位地址线宽,其寻址能力为256TB(2^48bytes),这对于目前的个人电脑或服务器都是够用的。综上所述,对于一个arm64架构的linux os往往用64位中的低48位虚拟地址来进行寻址(也可以通过内核配置进行调整可选项为38,48或52).

对于一个48位虚拟地址,4级页表,页面大小为4K的arm64架构linux os来说,其用户态进程的虚拟地址空间布局如下:

START					    END               			SIZE			USE						
-----------------------------------------------------------------------------
0x0000000000000000			0x0000ffffffffffff			256T			USER(用户虚拟地址空间)
0X0001000000000000			0Xfffeffffffffffff							非规范区
0xffff000000000000			0xffffffffffffffff			256T			KERNEL(内核虚拟地址空间)

其中用户进程地址空间的用户虚拟地址空间的内部结构划分情况和图1中arm32架构的结构划分基本一致,而对于内核虚拟地址空间的结构划分可参考以前介绍的linux内核内存初始化相关的介绍.

2 linux用户态进程虚拟地址空间管理

2.1 进程描述符task_struct

task_struct是linux内核的一种数据结构,Linux内核通过一个task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息。本文主要介绍进程用户空间虚拟内存管理,在task_struct中与进程虚拟地址空间相关的成员变量如下所示(内核态进程task_struct的mm成员为NULL):

//include/linux/sched.h
struct task_struct {
    ......
    /*
     *(1)struct mm_struct被称为进程描述符抽象并描述了Linux视角下管理进程地址空间的所有信息
     *(2)mm指向进程所拥有的内存描述符,而active_mm指向进程运行时所使用的内存描述符。
     *		a.对于普通进程,这两个指针变量相同
     *		b.对于内核线程,不拥有任何内存描述符,mm成员总是设为NULL;当内核线程运行时,它的active_mm成员被初始化		*		 为前一个运行进程的active_mm值
     */
    struct mm_struct *mm, *active_mm;
    ......
};

2.2 进程用户态虚拟地址空间描述符

进程用户态虚拟地址主要由如下两个数据结构来进行描述(只列出部分成员变量):

//include/linux/mm_types.h
struct mm_struct{
    //地址空间中所有VMA的链表首部
    struct vm_area_struct * mmap;
    rb_root_t mm_rb;
    //最后一次通过find_vma()找到的VMA存放处
    struct vm_area_struct * mmap_cache;
    //全局目录表的起始地址
    pgd_t * pgd;
    //访问用户空间部分的用户计数值
    atomic_t mm_users;
    //匿名用户计数值
    atomic_t mm_count;
    //正在被使用中的vma数量
    int map_count;
    //读写保护锁,长期有效
    struct semaphore mmap_sem;
    //用于保护mm_struct中大部分字段
    spinlock_t page_table_lock;
    //所有的mm_struct结构通过它链接在一起
    struct list_head mmlist;
    //代码段和数据段的起始地址和中止地址。
    unsigned long start_code, end_code, start_data, end_data
    //堆的起始地址和结束地址,栈的起始地址
    unsigned long start_brk, brk, start_stack;
    //命令行参数的起始地址和结束地址,环境变量区域的起始地址和结束地址。
    unsigned long arg_start, arg_end, env_start, env_end;
    /*
     *rss:某一时刻,一般一个进程虚存空间不会完全在内存中,一般驻留在内存中的为其虚存空间的子集,rss描述有多少页驻		*留内存中)驻留集的大小是该进程常驻内存的页面数,不包括全局零页面,total_vm:进程中所有vma区域的内存空间总和,	  *locked_vm:内存中被锁住的常驻页面数
     */
    unsigned long rss, total_vm,locked_vm;
	//VM_LOCKED用于指定在默认情况下将来所有的映射是上锁还是未锁。
    unsigned long def_flags;
    unsigned long cpu_vm_mask;
    unsigned long swap_cnt;
    //当换出整个进程时,页换出进程记录最后一次被换出的地址
    unsigned long swap_address;
    mm_context_t context;
}
//include/linux/mm_types.h
struct vm_area_struct {
	/* The first cache line has the info for VMA tree walking. */
	//指定VMA在进程虚拟地址空间的起始地址和结束地址
	unsigned long vm_start;		/* Our start address within vm_mm. */
	unsigned long vm_end;		/* The first byte after our end address
					   within vm_mm. */

	/* linked list of VM areas per task, sorted by address */
    //进程中所有的VMA都链接成一个链表
	struct vm_area_struct *vm_next, *vm_prev;
    //指定的VMA作为一个节点加入到红黑树中
	struct rb_node vm_rb;

	/*
	 * Largest free memory gap in bytes to the left of this VMA.
	 * Either between this VMA and vma->vm_prev, or between one of the
	 * VMAs below us in the VMA rbtree and its ->vm_prev. This helps
	 * get_unmapped_area find a free area of the right size.
	 */
	unsigned long rb_subtree_gap;

	/* Second cache line starts here. */
    //指向该VMA所属进程的mm_struct结构体
	struct mm_struct *vm_mm;	/* The address space we belong to. */
    //VMA的访问权限
	pgprot_t vm_page_prot;		/* Access permissions of this VMA. */
    //指向该VMA的一组标志
	unsigned long vm_flags;		/* Flags, see mm.h. */

	/*
	 * For areas with an address space and backing store,
	 * linkage into the address_space->i_mmap interval tree.
	 */
	struct {
		struct rb_node rb;
		unsigned long rb_subtree_last;
	} shared;
	
    //实现反向映射(anon_vma_chain,anon_vma)
	struct list_head anon_vma_chain; /* Serialized by mmap_sem &
					  * page_table_lock */
	struct anon_vma *anon_vma;	/* Serialized by page_table_lock */

	/* Function pointers to deal with this struct. */
	const struct vm_operations_struct *vm_ops;

	/* Information about our backing store: */
    //指定文件映射的偏移量,单位是页面大小,对于匿名映射它的值是0或者vm_addr/PAGE_SIZE
	unsigned long vm_pgoff;		/* Offset (within vm_file) in PAGE_SIZE units */
    //描述一个被映射的文件,执行一个file实例
	struct file * vm_file;		/* File we map to (can be NULL). */
	void * vm_private_data;		/* was vm_pte (shared mem) */

#ifndef CONFIG_MMU
	struct vm_region *vm_region;	/* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
	struct mempolicy *vm_policy;	/* NUMA policy for the VMA */
#endif
	struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
};

linux系统用户态一个进程虚拟地址空间由两个数据结构来描述mm_struct和vm_area_struct。mm_struct对进程整个用户空间虚拟内存进行了描述。同时一个进程的内存变化是动态扩充的,比如栈的扩充,堆的扩充,新文件的映射等,这些都需要一个粒度更小的结构体来表示内存的动态增加或减少,因此linux os又引入了vm_area_struct结构体来对进程的一段具有相同属性的虚拟地址空间进行描述。其中一个进程中的每个vm_area_struct实例都要连接到该进程mm_struct实例的mmap链表和mm_rb红黑树中,以方便查找。

最终linux进程的整个用户态虚拟地址空间都是通过mm_struct来进行描述,而虚拟地址空间上的每个段空间都是通过vm_area_struct来表示,如图2所示:

在这里插入图片描述

2.进程用户态虚拟内存管理

下面对task_struct,mm_struct和vm_area_struct这3者关系做一个总结:

linux内核为每一个进程维护一个task_struct结构体,内核用这个结构体来描述一个进程,这个结构包含了进程的一些信息,包括进程id,可执行文件名,指向用户栈的指针以及上下文的task结构体的指针等等。而task_struct中mm成员变量(该成员是一个mm_struct结构体数据)主要用于进程用户态虚拟地址空间的管理。这个mm_struct结构体中有两个比较重要的成员,一个是pgd,他指向该进程的页表集,便于根据虚拟地址查询页表获得需要的物理地址,另一个是mmap,他指向一个vm_area_struct的链式结构,链式结构中的每个vm_area_struct成员用来描述进程虚拟地址空间中具有相同属性的一个段,vm_area_struct结构体中包括该段的起止虚拟地址,标志访问权限的权限位,标志是否是共享区的标志位。CPU在操作一块数据时,内核会先遍历进程mm中的vm_area_struct链表(为了便于搜索,进程所有的vm_area_struct结构构成一个红黑树,树根保存在mm_struct中的mm_rb字段),若链表中任何一个VMA都与数据块的虚拟地址区间不匹配,就说明操作的地址还未分配,属于非法行为,内核会报段错误,并结束这个进程。当然如果权限不够也会报错。

ps:linux系统可以通过cat /proc/pid/map查看进程的虚拟地址段

3 用户态进程的虚拟地址和物理地址的映射管理

我们知道每个用户态进程都有一个独立的虚拟地址空间,维护着一个独立的页表。因此用户态进程虚拟地址vaddr必须与进程的唯一标识符进程pid相结合才有意义。用户态进程的虚拟地址和物理地址的映射如下所示 :

在这里插入图片描述

3.进程用户态虚拟地址映射示意图
  1. 通过进程的pid,获取到该进程的进程描述符task_struct结构体,进而通过task_struct结构体中的mm成员获得进程对应的虚拟地址空间描述符mm_struct结构体,最后通过mm_struct结构体成员pgd获得该进程的页表集。(ps:vaddr必须在mm_struct结构体的的mmap链表中找到对应的虚拟内存段vm_area_struct与其对应,否则会报段错误。此处默认vaddr在该进程虚拟地址空间范围内)

  2. 在步骤1获取到的进程页表集的基础上,将用户空间的虚拟地址vaddr通过MMU(pgd,pmd,pte)找到对应的页表项X(linux采用了一种与具体体系结构无关代码 的三层页表机制来完成内存管理,即使底层的体系结构并不支持这个概念。每一个进程都有一个指向其自己的PGD指针 (mm_struct->pgd),这就是一个物理页面号,其中包含了一个pgd_t类型的数组,进程页表的载入是通过把这个结构体复制到ttbr0寄存器完成。PGD表中每个有效的项都指向一个页面号,此页面号包含一个pmd_t类型的PMD项数组,每一个pmd_t又指向另外的页面号,这些页面号由 很多个pte_t类型的PTE构成,而pte_t最终指向包含真正用户数组的页面).

  3. 在找到页表项X后,需要对页表项X的数据进行分析来确定进程虚拟地址vaddr映射的物理页面相关情况。为了便于分析假设虚拟地址32位,页大小为4K。则页表项X为一个4bytes,32位的数据,其中X的12-31位是page base描述,而0-11位是属性描述。

    1. 若页表项X的0位为1,则进程的虚拟地址vaddr对应的物理页在物理内存中。则此时将页表项X的0-11位数据用虚拟地址的vaddr的0-11位数据替换,得到的32位地址即为虚拟地址vaddr对应的物理地址。(X[12:31]为虚拟地址vaddr所映射的物理页的页框号记为pfn,则vaddr对应的物理地址phyaddr = ( pfn<<12 ) & ( vaddr & ( 1 << 13 - 1) ) )

    2. 若页表项X的0位为0且X的1-31位都为0,则进程vaddr虚拟地址对应的页不在物理内存地址空间中,需要调页(缺页异常,触发缺页中断,以后专题讲解)。

    3. 若页表项的0位为0且1-31位至少有一位为1,则该页被交换到swap的磁盘分区。则此时页表项X的1-7位是表示的是磁盘交换区的区号,而X的8-31位表示磁盘交换分区的页槽索引(page-slot)类似于物理页的页框号(page frame)。由上我们就可以将该虚拟地址与磁盘的swap分区中的某一页关联起来。

      在这里插入图片描述

      4.pte页表项与swap磁盘分区索引图

4 linux用户态进程文件页的虚拟地址如何对应到磁盘中文件的具体位置?

在本章第3节中,我们根据某一进程的虚拟地址vaddr定位到了其pte页表项X,若该页表项X的0-31位都为0,则内存中没有物理页与vaddr虚拟地址建立映射关系,此时需要调页。假设vaddr虚拟地址映射的页是文件页(在进程mm_struct结构体中的mmap链表中找到vaddr对应的VMA结构体,若VMA中的vm_ops成员不为空,则vaddr映射的物理页为文件页,为空则vaddr映射的物理页为匿名页),对于文件页的调页linux需要进行如下操作,下面为了简化流程便于理解忽略page cache操作

  1. linux内核分配一个新的物理页A
  2. 将新物理页A的物理页框号(pfn)和页对应的权限信息更新到页表项X中并刷新进程的页表缓存
  3. 最后将虚拟地址vaddr文件页对应的磁盘文件内容copy到新的物理页A中,则文件页的调页过程完成(忽略page cache)。

上面这一过程其实就是缺页中断中linux对于文件页的一个处理过程。那么在上述的操作流程中linux内核是如何通过一个文件页的虚拟地址,准确地找到对应磁盘文件中的页数据的呢?

其实在用户进程P将文件映射到内存时,刚开始进程P只给文件分配了虚拟地址空间即分配了一个VMA(该VMA在进程页表集中也分配了对应的页表项,但对应的PTE页表项内容为空),而并没有将分配的VMA虚拟地址空间映射到物理内存中;但是在进程P在给文件分配VMA时,它会将磁盘文件的位置信息记录在VMA的vm_file和vm_pgoff成员中。假设进程P的虚拟地址vaddr在VMA的虚拟地址空间范围内,当对进程P的vaddr进行读或写操作时,会发现该vaddr虚拟地址并未映射物理地址;由此触发缺页异常,进程p因缺页中断而切换到内核态,进入内核态的进程p会先分偶尔一页物理内存并与vaddr建立映射(填写对应的pte页表项包括物理页的pfn和该页的访问权限信息)。接下来需要将vaddr对应的磁盘数据拷贝到新分配的物理页内存中,操作步骤如下:

  1. 在进程P用户态虚拟地址空间的vma链表中找到与vaddr对应的VMA
  2. 通过VMA中的vm_file,vm_pgoff成员和vaddr相对VMA起始虚拟地址的偏移offset这3个数据并利用VFS相关接口来定位到vaddr对应的文件段在磁盘中的具体位置。
  3. 最后通过相关系统调用函数将vaddr对应的文件段在磁盘中的数据拷贝到刚分配的物理页中。

以上就完成了文件页在缺页异常情况下的调页流程,需要注意的是上述流程中忽略了page cache操作。最后进程P就能通过虚地址vaddr获取到对应的文件数据。上诉流程分析结合图5更容易理解。

在这里插入图片描述

5.linux用户态进程文件页虚拟地址空间-物理地址空间-磁盘文件空间的映射关系示意图

ps:内存主要通过虚拟文件系统(VFS)来操作磁盘上的文件数据的,VMA的vm_file是一个struct file *数据结构,VFS系统能根据此数据结构确定该VMA虚拟地址空间对应于哪个磁盘文件。VMA的vm_pgoff记录的是文件的偏移量,VFS文件系统根据此数据确认该VMA虚拟地址空间对应磁盘文件中的哪一段数据。VFS详细介绍可参考https://www.cnblogs.com/huxiao-tee/p/4657851.html

知识来源:

1.https://www.zhihu.com/question/24011983

2.https://www.cnblogs.com/ck1020/p/6678530.html

3.https://blog.csdn.net/a372048518/article/details/103865898

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值