Linux内存管理的易错点
1.为什么现代处理器访问的地址是虚拟地址而不直接采用物理地址?
处理器直接使用物理地址存在以下问题:
- 进程地址空间保护(isolation)和安全问题(security)。所有的用户进程都可以访问全部的物理内存,如果有恶意的程序,那么它可以修改其他程序的内存数据,这样便破坏了其他进程的内部数据。即使系统里所有的进程都不是恶意进程,但是进程A依然可能不小心修改了进程B的数据,从而导致进程B运行崩溃。
- 内存使用效率低。如果即将要运行的进程所需要的内存空间不足,就需要选择一个进程进行整体换出,这种机制导致有大量的数据需要换出和换入,效率非常低下,通常换出是把数据写入到交换分区,导致大量的磁盘IO。
- 程序运行地址重定位问题。进程在每次换出换入时运行的地址都是不固定的,这给程序的编写带来一定的麻烦,因为访问数据和指令跳转时的目标地址通常是固定的,这就需要重定位技术了。
总之,进程地址空间是对内存的一个重要的抽象,让内存虚拟化得到了实现,它和进程的CPU虚拟化,以及文件对存储地址空间的抽象,这三个是操作系统中三大抽象,组成了操作系统的三个元素。
进程地址空间的概念引入了虚拟内存,而这个思想可以解决刚才提到的三个问题。
- 隔离性和安全性。虚拟内存机制可以提供这样的隔离性,因为每个进程感觉全部拥有了整个地址空间,它可以随意访问整个地址空间,然后由处理器来转换到实际的物理地址,所以,进程A没法访问到进程B的物理内存,也就没办法做到破坏了。
- 效率。后来出现的分页机制可以解决动态分区法出现的碎片化和效率的问题。
- 重定位问题。进程换入换出时访问的地址变成相同的虚拟地址。它不用关心物理地址。这个虚拟地址我们称为链接地址。
2.MMU查询页表的目的是找到虚拟地址对应的物理地址,页表项中有指向下一级页表基地址的指针,那它指向的是下一级页表基地址的物理地址还是虚拟地址呢?
答案是物理地址,因为MMU地址管理单元本身就是用来完成虚拟地址转换为物理地址的工作的,如果页表项中存放的是虚拟地址,为了找到下一级页表的物理地址,MMU必须再次进行地址转换…由此无限循环,因此只能是物理地址。
3.MMU硬件单元可以遍历页表,Linux内核也提供了软件遍历页表的函数如walk_pgd()等,站在软件的视角,Linux内核中的pgd_t、pud_t、pmd_t和pte_t数据结构(其实都是u64类型)并没有存储一个指向下一级页表的指针,它是如何遍历的呢?
这个问题的本质是软件的方式如何找到下一级页表基地址的虚拟地址(因为页表项中存放的是物理地址),此时需要我们明白一个知识点:Linux内核线性区中,虚拟地址和物理地址可以很方便的互相转换!
4.为什么内核在初始化的时候需要把整个物理内存都线性映射到内核空间呢?
本质上来说,软件来填充页表,而MMU硬件单元只是根据页表的页表项内容,来完成虚拟地址到物理地址的映射。所以,程序猿让MMU映射哪里,它就映射哪里,MMU不会自作聪明自动给你建立映射的。多级页表的按需映射,是页表的基本功能,只是用来节省页表占用内存空间,而内核的线性映射和这个多级页表的按需映射,其实是两回事。内核空间是所有进程共享的一个空间。CPU虚拟化也就是进程的抽象,内存虚拟化也就是地址空间的抽象,在这两个概念下,进程感觉它拥有了全部的地址空间,包括用户空间和内核空间,用户空间是它独有的,而内核空间是所有进程共享的。
当进程陷入到内核空间时,它访问的地址同样是虚拟地址,只不过是它访问了内核地址空间。但是,有一点不一样的是,进程在用户空间访问虚拟地址,如果这个虚拟地址没有映射物理内存时,处理器会触发缺页异常,然后陷入到内核态的缺页异常中来修复这个映射。但是如果在内核态访问一个没有映射的内核地址空间,那么内核陷入崩溃状态,这就是我们常常看的oops错误。这是因为运行在内核空间的程序需要稳定性和安全性。假设内核空间的虚拟地址没有预先映射,内核运行在内核空间里也常常需要访问物理内存,那么内核就会常常处于缺页异常中,若缺页异常无法修复错误的话,那么整个系统就挂掉了。所以,为了操作系统的安全性和稳健性,对内核空间虚拟地址是不做缺页异常处理的,在内核空间里访问一个空指针常常会引发系统崩溃。所以,通常做法是:在内核初始化时,把全部物理内存都线性映射到内核地址空间,这样预先映射有不少好处,比如可以减少在内核空间崩溃的几率。第二,也能提高系统性能,因为在内核空间也是常常会分配内存,比如伙伴系统,slab机制分配内存,这些分配的内存,不需要重新来建立虚拟地址到物理地址的映射了,因为系统初始化时,已经预先映射好了。
ARM64 Linux内核空间布局图(基于Linux5.0):