目录
补充点1:进程地址空间堆区管理
补充点2:Linux内核进程上下文切换
补充点3:页表映射
补充点4:两级页表
补充点1:进程地址空间堆区管理
Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息。该结构体存放在叫做任务列表的双向循环列表中!
所学习过的包含:
进程标识符 - 进程优先级 - 进程状态 - 进程地址空间 - 文件描述符表 - 进程信号位图 - CPU寄存器的上下文数据 - 进程相关页表(内核级页表、用户级页表)
在进程地址空间中,栈区,代码区等一部分区域,是被整体所使用的!而堆区具有更细粒度的划分,包括使用者等(参考文章:Glibc——堆利用机制[拓展]_IfYouHave的博客-CSDN博客),因此堆是使用一个vm_area_struct小的结构体进行区分,使用双链表的形式进行管理!
(参考文章:linux内核学习笔记-struct vm_area_struct_struct vm_area_struct source code_带着耳机去梦游的博客-CSDN博客)
在进行堆区申请空间,上层调用malloc函数 - 底层调用brk系统调用,就会申请一个vm_area_struct,内有start表针虚拟地址起始,end表示虚拟地址结束,经过页表映射至内存。
堆区典型特征:申请的空间连续!
OS是可以做到让进程进行资源细粒度划分的
补充点2:Linux内核进程上下文切换
进程上下文是进程执行活动全过程的静态描述。我们把已执行过的进程指令和数据在相关寄存器与堆栈中的内容称为进程上文,把正在执行的指令和数据在寄存器与堆栈中的内容称为进程正文,把待执行的指令和数据在寄存器与堆栈中的内容称为进程下文。
实际上linux内核中,进程上下文包括进程的虚拟地址空间和硬件上下文。
进程硬件上下文包含了当前cpu的一组寄存器的集合,arm64中使用task_struct结构的thread成员的cpu_context成员来描述,包括x19-x28,sp, pc等。
进程上下文切换主要涉及到两部分主要过程:进程地址空间切换和处理器状态切换。地址空间切换主要是针对用户进程而言,而处理器状态切换对应于所有的调度单位。
进程地址空间切换:
进程地址空间内有进程运行的指令和数据,因此到调度器从其他进程重新切换到我的时候,为了保证当前进程访问的虚拟地址是自己的必须切换地址空间。
进程pcb内mm_struct结构体将各个vma组织起来进行管理,其中有一个成员pgd至关重要,地址空间切换中最重要的是pgd的设置。pgd中保存的是进程的页全局目录的虚拟地址,那么pgd的值是何时被设置的呢?
答案是fork的时候,如果是创建进程,需要分配设置mm_struct,其中会分配进程页全局目录所在的页,然后将首地址赋值给pgd,完成了这一步,也就完成了进程的地址空间切换,确切的说是进程的虚拟地址空间切换。
处理器状态(硬件上下文)切换:
处理器状态切换就是将前一个进程的sp,pc等寄存器的值保存到一块内存上,然后将即将执行的进程的sp,pc等寄存器的值从另一块内存中恢复到相应寄存器中,恢复sp完成了进程内核栈的切换,恢复pc完成了指令执行流的切换。
其中保存/恢复所用到的那块内存需要被进程所标识,这块内存这就是cpu_contex这个结构的位置(进程切换都是在内核空间完成)。
线程部分会学习:
内核线程,不需要切换地址空间,只进行硬件上下文切换
所有的进程线程之间进行切换都需要切换处理器状态。
对于普通的用户进程之间进行切换需要切换地址空间
同一个线程组中的线程之间切换不需要切换地址空间,因为他们共享相同的地址空间。
内核线程在上下文切换的时候不需要切换地址空间,仅仅是借用上一个进程mm_struct结构。(参考文章:深入理解Linux内核进程上下文的切换 - 知乎 (zhihu.com))
补充点3:页表映射
MMU(Memory Management Unit),即内存管理单元,是一个硬件,是现代CPU架构中不可或缺的一部分,MMU主要包含以下几个功能:
- 虚实地址翻译
在用户访问内存时,将用户访问的虚拟地址翻译为实际的物理地址,以便CPU对实际的物理地址进行访问。
- 访问权限控制
可以对一些虚拟地址进行访问权限控制,以便于对用户程序的访问权限和范围进行管理,如代码段一般设置为只读,如果有用户程序对代码段进行写操作,系统会触发异常。
- 引申的物理内存管理
对系统的物理内存资源进行管理,为用户程序提供物理内存的申请、释放等操作接口。使用MMU带来的好处或者优势:
- 提升物理内存的利用率
物理内存按需申请,如代码段的内存在执行时进行映射和转换,进程fork后,t通过写时复制(Copy-On-Write)进行真正的物理内存分配。解决内存管理碎片化的问题,即在系统运行一段时间后,频繁的内存申请和释放会导致内存碎片化,无法申请到一块足够大的地址连续的内存。
- 对内存地址的访问进行控制
如上述代码段只读权限控制,多线程的栈内存之间的空洞页隔离可以防止栈溢出后改写其他线程的栈内存,不同进程之间的地址隔离等等。
- 将进程的地址空间隔离
不同进程之间可以使用相同的虚拟内存地址空间,而进程间的物理内存又可以做到隔离,这保证了进程的独立性同时,又简化了地址的访问方式,如在早期32位CPU上,为了支持4G以上的物理内存,一般物理地址有36-bit(如PowerPC-604系列),但是用户的虚地址仍然使用32-bit,做法就是将用户的不同进程的32-bit虚地址在MMU转换时,转换为36-bit的物理地址,这样每个进程仍然能访问0-3G虚地址范围,将多个进程的3G空间映射到36-bit的物理内存空间中去。上述参考文档(MMU原理 - page)
如何从虚拟地址映射到物理内存?
- .exe就是一个文件
- 我们的可执行程序本来就是按照地址空间方式进行编译的(编译形成二进制文件的格式 - ELF格式)
- 可执行程序,其实按照区域也已经以4KB为单位进行了划分
- 物理内存也早就按照4KB为单位划分成一个个page(操作系统进行IO的基本单位就是4KB)
- 因为被划分,操作系统就需要管理划分后每一块物理内存的属性等,先描述,在组织,page。因此4G的物理内存,便会形成100w+个块,假设一个结构体为20字节,100w+个page会使用20MB的内存空间
磁盘内文件以4KB为单位划分的块成为页帧,物理内存划分块称为页框
IO基本单位是4KB,就是将页帧内容 -> 页框
补充点4:两级页表
页表在进行映射时,会通过虚拟地址,访问物理内存,页表中含有其他字段,表征磁盘数据是否被加载到内存,没有,变会进行申请内存page,通过文件系统加载内存,最后填充在页表右侧,这种行为为缺页中断!(用户零感知)
4.1 单级页表存在的问题:
若计算机系统按字节寻址,支持32位逻辑地址,采用分页存储管理,页面大小为4KB,页表项长度为4B。4KB = 2^12B,因此页内地址要用12位表示,剩余20位表示页号。
物理内存 4GB = 2^20 * 2^12 B
因此,该系统中用户进程最多有2^20页。相应的,一个进程的页表中,最多会有2^20个页表项,所以一个页表最大需要2^20 * 4B = 2^22B。一个页框(内存)大小为4KB,所以需要2^22/2^12 = 2^10个页框存储该页表。
而页表的存储是需要连续存储的,因为根据页号查询页表的方法:
K号页对应的页表项的位置 = 页表起始地址 + K * 4B(页表项长度),所以这就要求页表的存储必须是连续的。
回想一下,当初为什么使用页表,就是要将进程划分为一个个页面可以不用连续的存放在内存中,但是此时页表就需要1024个连续的页框,似乎和当时的目标有点背道而驰了....
此外,根据局部性原理可知,很多时候,进程在一段时间内只需要访问某几个页面就可以正常运行了。因此也没有必要让整个页面都常驻内存。
所以,单级页表存在以上两个问题。(参考文章:两级页表 - 简书 (jianshu.com))
4.2 两级页表:
如何解决页表过大需要连续存储的问题呢?这个问题可以参考进程太大需要连续存储的答案。因为页表必须连续存放,所以可以将页表再分页。
解决方案:可以将长长的页表进行分组,使每个页面中刚好可以放下一个分组(如上面的例子中,页面的大小4KB),每个页表项4B,所以每个页面中可以存放1K个(1024)个页表项,因此每1K个连续的页表项为一组,每组刚好占一个页面,再讲各组离散的放在各个内存块中)。这样就需要为离散的页表再建立一张页表,称为页目录表,或外层页表,或顶层页表。32位的逻辑地址空间,页表项大小为4B,页面大小4KB,则页内地址占12位
将页表分为分为1024个表,每个表中包含1024个页表项,形成二级页表。二级页表结构的逻辑地址结构如下图
两级页表如何实现地址转换:
(1) 按照地址结构将逻辑地址拆成三个部分。
(2) 从PCB中读取页目录起始地址,再根据一级页号查页目录表,找到下一级页表在内存中存放位置。
(3) 根据二级页号查表,找到最终想要访问的内存块号。
(4) 结合页内偏移量得到物理地址下面以一个逻辑地址为例。将逻辑地址(0000000000,0000000001,11111111111)转换为物理地址的过程。