内存
回忆线索:
虚拟内存的地址空间、分段机制(vm_area_struct的红黑树记录)、分页机制(GDT、多级页表)、物理内存(节点、区域、页)、换入换出、缺页中断
虚拟内存
1、进程的地址空间
用户代码访问用户的数据结构,内核代码只能访问内核的数据结构。进程用户空间互不干预,内核的空间开放得多,虽然内核栈是各用各的,但是如果想知道的话还是可以知道别人的内核栈在哪里。
一个进程运行所需的内存结构如下:
用户态:
代码段、全局变量、BSS
堆
内存映射区
函数栈
内核态:
内核代码、全局变量、BSS
内核数据结构,如task_sturt
内核栈
内核中的动态分配的内存
2、虚拟内存的分段机制
linux里用全局描述符表GDT(Global Descriptor Table)里的段描述符表来记录段表。
3、虚拟内存的分页机制
多级页表通过两级或者四级目录项对物理内存进行映射,这样设计的好处是可以自由控制大小。页目录是全都需要的,但是如果只用到了其中一个页目录项的话,其余的页目录项下面所对应的PUD、PMD项都可以不分配,这样按需分配就可以把页表所占空间减小。
4、进程内存的用户态布局
task_struct里用mm_struct记录了用户态结构的信息。各个段的起止点,所占页数等。
5、进程内存的内核态布局
内核态中有一大片区域是与物理内存中的起始区域直接映射的。那部分里,有用于系统启动的代码,加载内核代码段,内核全局变量、BSS等。
系统调用或者其他进程管理的代码都在这部分内存里,相应的页表也会被创建在这。在内核中,内存管理模块可以直接操作物理地址,其他模块仍然要操作虚拟地址,通过内存管理模块才能动物理地址。
物理内存
6、物理内存的组织方式
最简单的一种是一条总线一边接内存,一边接多个cpu,这种叫对称多处理器模型(SMP,Symmetric multiprocessing),这种模式总线会成为瓶颈。另一种方式是一条总线,每个cpu都有各自的内存,这样cpu访问本地内存可以不过地址总线,这种叫非一致内存访问(NUMA,Non-uniform memory access)。每个CPU加它的本地内存的组合叫一个NUMA节点。
在总的内存里有一个node_data数组,将所有的节点串起来了,成为了我们的整块物理内存。每个节点node下面分成一个个区域zone,也是用一个数组将当前节点下的zone信息串起来,用node_zone记录节点的zone信息,每个区域由页page组成。这也是物理内存的基本单位。
节点:节点node有一个结构体pglist_data记录着这个节点的信息,如:node_mem_map记录这个节点的所有的页。node_start_pfn记录这个节点的起始页号(例如这块内存对应的是第300页开始的物理地址,因为在这块内存前面有另一块内存占了前面的页)。node_spanned_pages记录节点中包含不连续的物理内存地址的页面数,node_present_pages记录真正可用的物理页面数(内存之间会有一些空洞间隔的区域)。
区域:一个节点有很多个zone,如:ZONE_DMA是可用作DMA的内存,ZONE_NORMAL是直接映射区,ZONE_HIGHMEM是高端内存区,就是存操作系统那块,直接映射的,32位系统里有这个区,64位里没有。ZONE_MOVABLE是可以移动区。
页:有多种使用模式。
(1)匿名页。需要整页使用,用于与虚拟地址空间建立映射关系或者关联一个文件然后再与虚拟地址空间建立关系(将硬盘的文件读取进到内存页上,然后和虚拟地址映射)。这样的文件称为内存映射文件。
(2)切碎使用的页。将一页开来用的,如分配一小半页来记录一个task_struct。
7、物理页分配(与配置器有关,一级配置二级配置)伙伴系统(Buddy System)
物理内存通过free_area存储了11个页块,记录了不同大小的连续空闲页块。假如想要分配250个页,那么会在第八格的链表里,找一块长度为256的连续空闲页块分配给进程,多出的6块就切割成4和2,分别插入到第1格和第2格的链表里。
小内存的分配,如给task_struct分配内存的时候,会将一大块内存切割成每个小块,成为task_struct的缓存区。缓存区中的信息有一部分用来记录对象,一部分记录下个空闲对象的指针。在每个NUMA节点上都会有两个成员变量kmem_cache_cpu(快速通道,拿出第一个就用)和kmem_cache_node(普通通道,快速通道找不到就在这里找),记录每个缓存区的第一个小空闲块。partial是备用的内存块。
8、页面换出
系统初始化的时候会创建一个内核线程kswapd,线程无限循环,内存紧张了就回去检查是否需要换出内存页。
内存页分为匿名页,与虚拟地址关联,另一种是内存映射,同时与虚拟地址和文件管理关联。每一类都有两个列表,active和inactive。页面换出时会先缩减活跃页面列表,再压缩不活跃页面列表,并且回收不活跃链表中被换出的页面。对于匿名页,要将内存页写入文件系统,对于内存映射,要在内存中对文件的修改写回到文件中。(延后操作)
物理内存与虚拟内存的映射
9、mmap原理
mmap将内存空间映射到物理内存,也可以再映射到文件。
mmap映射流程:
mmap根据传入的文件描述符获得文件的struct file——>调用get_unmapped_area函数找一个没有映射的区域,如果是匿名映射,在vm_area_struct红黑树上找到前一个被用了的位置,如果是映射文件,就通过文件支持的操作绕一圈调用get_unmapped_area函数获得前一个被用了的位置——>调用mmap_region映射区域,具体是根据前面找到的前一个被用了的位置看看能不能直接拓展,可以的话直接改一下前面的vm_area_struct大小就行(大概这个意思,具体肯定不止这个操作),不能的话就要创建一个新的vm_area_struct,因为原来的虚拟内存里没有能够容纳它的vm_area_struct。新建vm_area_struct之后就将它接到mm_struct的红黑树上,这就建立了内存的映射。
在虚拟内存中找好位置之后。下一步虚拟内存与物理内存的链接要靠用户态的缺页异常。
CPU中有个cr3寄存器,指向当前进程的顶级pgd,当cpu要访问虚拟内存时,会通过cr3得到pgd的物理地址,从中解析虚拟地址为物理地址,这个过程发生在用户态,无需进入内核态。当访问虚拟内存发现没有映射到物理内存时,触发缺页异常。然后调用相应的系统调用分配一个页表项,调用伙伴系统分配物理页面,调用mk_pte将页表项指向新分配的物理页,set_pte_at将页表项塞进页表里。文件映射就特殊一点,首先在物理内存里分配一个文件的缓存页,将这页加到lru表(最近最少使用的那个表)里,读入文件内容。读文件进这个物理内存需要通过系统调用,内核又不知道这个缓存页的物理地址。(待补充)
至此,映射完成。
10、TLB快表
页表很大,在内存中,快表比较小,在CPU里,可以认为是页表的缓存。
11、内核态映射
总结
物理内存按节点分,节点按区域分,区域按页分。物理页面的分配是通过伙伴系统来进行的,kswapd可以根据物理页面使用情况对页面进行换入换出。
内核态分配大内存时,使用伙伴系统分配物理内存,然后转换为虚拟地址,访问时通过内核页表进行映射,分配小内存时,将伙伴系统分配的内存切成小块来分配。
用户态内存分配时用malloc,分配小内存用sys_brk系统调用在堆区分配;分配大内存通过sys_mmap系统调用。