前言
因为春招的缘故, 经历了一次自我整体全面的知识上的查漏补缺, 接下来应该也会陆陆续续对这些知识点做一个总结吧, 今天就先总结一下Linux
对内存管理方面的知识, 博主水平有限, 如果文章中有错误或遗漏的点, 欢迎指正和补充
面临的问题
假设我们下面介绍的虚拟内存, 分页, 分段
技术都尚未出现, 那么我们运行一个程序, 就需要在内存中划分出一个连续的可用内存, 将程序加载进去, 这样以来, 我们面临以下三个问题 :
- 地址空间不隔离
- 程序运行时地址不确定
- 内存使用率低
虚拟内存
Linux
下的内存管理是虚拟内存结合分页分段机制来做的, 虚拟内存指的是 : Linux 下进程认为自身拥有的是一段完整连续的可用内存, 但实际上其对应的是一些分散开的物理内存块
进程在访问内存时实际上是先使用 MMU(内存管理单元) 将其拥有的虚拟内存地址映射到对应的物理内存 (通过页表)
分段
将程序分成不同的段进行管理, 将虚拟地址空间映射到物理地址空间, 我们程序操作的都是虚拟地址空间
这样一来, 不同进程操作的都是虚拟地址空间, 就算两个进程虚拟地址空间一样, 其对应的物理内存地址也是不同的, 这就做到了隔离
而且, 程序不用再关心物理地址, 只需要关注虚拟地址, 只要你的虚拟地址确定了, 你对应的物理地址就确定了
但是分段机制映射的是一片连续的物理内存, 所以其内存使用率较低, 于是要结合分页机制
分页
虚拟内存空间被分割成等大小的页来管理, 分页仍然是一种虚拟地址到物理地址的映射机制, 他和分段的区别在于 : 分页的粒度更小, 它的单位不再是对整个程序, 而是针对上述的分段机制分出的程序不同的段
这样一来, 不再像分段, 使用分页后, 虽然虚拟地址空间仍然是连续的, 但是每一页映射后的物理地址可以不再连续
Linux 下默认页大小为4K, 当然 Linux 也支持4K整数倍的大小, 改变页大小需要修改内核配置参数, 并重新编译内核
页表
上面提到了, 虚拟内存技术将非连续的空闲物理内存映射成连续的一段连续的内存分配给进程, 进程将这些虚拟内存地址空间分成固定大小的页并使用页表进行管理
进程的逻辑地址空间的每一页, 依次在页表中有一个表项, 记录该页对应的物理内存地址, 进程只需要将当前需要用到的部分加载到内存, 而不需要将所有的虚拟内存全部加载进内存, 这样一来, 就可以让虚拟内存远大于物理内存的大小, 给进程提供了巨大的可用空间, 而且, 以分页模式来管理内存另一个好处是减少了内存碎片, 因为在页内连续的地址, 其对应的物理内存一定也连续
快表 TLB
刚才提到了页表, 进程访问一个虚拟内存时, 要先将该虚拟地址交给 MMU, MMU 去访问页表, 找到对应的物理地址, 再访问其对应的物理内存, 这样就有两次内存的访问, (因为页表一般较大, 存放在内存中)
TLB 就相当于页表的Cache
, 用来改进虚拟地址到物理地址转换的速度, TLB 中存储了当前最可能被访问的页表项(局部性原理), 其内容是页表项的副本, MMU 会先在 TLB 中查找, 只有在 TLB 无法完成对地址的翻译任务时, 才会到内存中查询页表, 这一样就减少了页表查询导致的 CPU 性能下降
发生进程切换时, TLB 刷新, 所以当进程切换后的刚开始的时间, 进程访问内存的速度会下降
多级页表
多级页表是只针对一级页表中用到的页面建立的, 那些没有被用到的页表项, 根本不会建立二级页表
多级页表是为了节约内存而发展出来的
如果我们只使用一级页表, 这个单页表就需要映射所有的虚拟地址空间, 需要一次性分配所有的页表空间
我一开始想不通的是同样是放在内存中, 多级页表怎么就节约内存了, 后来我是这么理解的 :
就像是一本书, 书的内容就是对应的物理内存地址, 书的目录就是一级页表, 假如有十章, 我们当前只有第二章的内容, 那这本书的目录中只有第二章的内容, 其他九章目前都为空(不用创建), 一下子节省了很多, 不然我们要时刻都是具备十个章节的目录
缺页错误
页表项有效位为0 : 当前虚拟内存在物理内存中没有对应的缓存, 即发生缺页
而这时又没有空闲的物理内存, 那么引发缺页中断, 从用户态进入内核态, 操作系统处理缺页错误, 为了能够把当前所缺的页面装入内存, 系统要从内存中选择一页为牺牲页, 调出到磁盘兑换区,
LRU LFU FIFO
进程的内存布局
不配图了相信大家都见过
从低地址到高地址依次是 :
用户空间 :
- 代码段
- 数据段
存放全局变量, 依次为data
段 : 存放已初始化数据,bss
段 : 存放未初始化数据 - 堆段
- 共享库的内存映射区域
- 栈段
内核空间 :
内核代码和数据, 与进程相关的数据结构…
我们一般关注的都是用户空间的内存布局, 这也是我们上面提到的分段处理
内存分配机制(不做详述)
Linux
下进行物理内存页框分配的主要机制是伙伴算法
而对于更细块内存的分配, 使用slab
分配器, 它使用伙伴算法获得内存块, 之后切出 slab (更小的单元)进行分配
mm_struct 和 vm_area_structs
每个进程的用户地址空间都由两个数据结构来描述, 一个是位于task_struct
中的mm_struct
, 他是对进程整个用户空间的记录, 还有一个是位于mm_struc
中的vm_area_structs
类型的一个链表, mm_struct
中存储其链表头指针
mm_struct
中对用户空间的每一个段都由start_*, end_*
两个指针标记其起始位置和终止位置, 其中还有一个pgd_t *
指针指向该进程页表
如, start_code, end_code, start_brk, brk…
进程的虚拟地址空间被分割成连续页面组成的区域
, 每一个区域由一系列连续的具有相同保护属性和分页属性的页面组成, 每一个区
由 vm_area_struct
表示, 其中记录了每一个区的属性 : 保护模式(只读还是可读可写) 是否固定在内存中(不可换出) 朝向哪个方向生长(数据段向上长还是栈段向下长),
也同时记录该部分虚拟内存是私有的还是和其他进程共享的
fork 的
写时复制
就是这么实现的, fork 之后为子进程复制父进程的区链表, 但是父子进程指向相同的页表, 区域被标记为可读可写, 但是页面被标记为只读, 如果任何一个页面试图写页面, 就会产生一个保护故障, 此时内核发现该内存区逻辑上可写, 但是页面却不是, 这时将该页面的一个副本交给当前进程同时将其标记为可读可写
刚才说了mm_struct
结构中有vm_area_struct
的单链表的头指针, 当这个链表长度过长(多于32项), 就会同时再创建一个红黑树来优化对其的查找速度
很多点讲的都是比较粗糙了, 只是一个大概的知识总结, 日后再补充吧…