今天还有时间,之前一直想抽时间来写写Linux内核原理相关的东西,关注点不在代码,而在于内在的原理和机制,让大家对Linux内核有个总体的感性认识,个人认为这很有必要,把看似复杂、深不可测的内核实现,用大家都能理解的方式,用讲故事的方式,讲给大家听,如果有人听后,有原来不过如此的感觉,那我的目的就达到了。

# 内存管理

从哪里开始呢?还是从最基础的内存管理开始吧。

内存管理是Linux内核中最基础,也是相当重要的部分。理解相关原理,不管是对内存的理解,还是对大家写用户态代码都很有帮助。很多书上、很多文章都写了相关内容,但个人总觉得内容太复杂,不是太容易理解,这里想用我自己理解的简单的方式来描述,希望能有所帮助,按自己的思路,可能有点乱,见谅。

从内存分配开始

大家写代码时,应该都会分配内存,不同语言,层次不同,使用的接口不同,不管使用哪种方式,在Linux系统中,基本上都会调用到C库的malloc接口,那就从malloc分配内存开始。

malloc就是用于分配一段内存,但这里分配到的内存并非物理内存,而是虚拟内存,这里没有严格区分虚拟地址、线性地址之类的概念,只会给大家添负担,也不深入讲述物理内存和虚拟内存的概念,书上通常有大量的篇幅介绍,大家可以简单这样理解:

  • 虚拟内存就是从进程的角度看,逻辑上的概念,并不实际存在;

  • 物理内存就对应物理上内存条上的内存;

  • 虚拟内存和物理内存有对应关系;
  • 虚拟内存分配时,相应的物理内存还没有分配;

既然虚拟内存并不实际存在,那么分配来有何用处呢,如果要向其中写数据,数据如何写入呢?会写到哪里去呢?

当然,内存分配后就是拿来用的,如果不能用(比如写数据),那就没有意义了。前面说的虚拟内存和物理内存有对应关系,当分配虚拟内存时,相应的物理内存还没有分配,这里有几个关键问题:

  1. 虚拟内存和物理内存的对应关系由谁来负责维护,如何对应?
  2. 物理内存何时分配?

虚拟内存到物理内存的映射

先来解答问题1。

页表来建立虚拟内存到物理内存之间的映射关系。 页表是啥?

简单看,页表就是在内存中的一张表(不详细介绍页表的具体格式啥的,看书就可以了),可以简单看做一张hash表,记录的是虚拟地址和物理地址的对应关系,每个虚拟地址对应一个表项,通过这张表,就能将虚拟地址转换为物理地址,也就能建立虚拟内存到物理内存的映射关系了。

谁用页表?

页表有了,那谁来用呢?不可能是应用程序自己用吧,我写代码时好像从来都没见过页表

当然,用户看到的只是虚拟地址(虚拟内存),其他的对用户都是透明的~

CPU中有个硬件单元,叫MMU(内存管理单元),页表就是给MMU硬件用的,MMU使用页表进行虚拟地址到物理地址的映射。也就是说,地址映射是由硬件完成的,软件(包括操作系统内核自身)都不管关心。

都说软件不用关心了,那我们为嘛还需要讲页表?

软件只是不用页表而且,但页表的创建和维护都是由软件(操作系统内核)负责的,也就是说我们(软件)创建虚拟内存和物理内存的映射关系,然后由硬件来自动进行地址映射(转换),我们不需要关心具体的转换过程。

前面说了,页表是在内存中,而页表是由软件创建的,那MMU如果知道页表到底在哪儿呢?

简单说,需要我们(软件)告诉它在哪儿,如何告诉?当然,写寄存器。CPU上有特别的寄存器(CR3),向其中写入页表的地址,MMU就知道了,然后硬件自己使用即可,我们就不管了。

有多少张页表?

看似一张页表就能完成所有的地址映射了?

当然不行,如果是这样,虚拟内存就没有什么必要存在了。

这里又涉及新概念了:进程,这是操作系统中最基础的概念,其实不“新”。

系统中,所有任务都是以进程方式运行的,每个进程都有自己的独立的虚拟地址空间,好像又说复杂了,简单说,就是每个进程都有自己的虚拟内存,独立代表,其他进程看不到自己的虚拟内存,那么就意味着,每个进程都需要独立的虚拟内存到物理内存的映射,就是说,每个进程都需要自己的页表

所以,系统中有多少进程,就有多少张页表。

页表创建

前面说了,页表的创建和维护是由操作系统内核完成的,那何时创建页表呢?

如前面所说,每个进程都需要自己的页表,显然,页表需要在进程创建时建立。具体的创建过程和原理就不讲了。

页表的维护

如果维护页表?

当然,进程自己维护,如果维护?

拿前面的例子说,malloc分配虚拟内存后,并没有分配物理内存,也没有建立映射关系,没有修改页表,那到底什么时候,由谁来做映射?

答案是:“缺页异常”。

缺页异常

缺页异常(page fault)又是另一个专业术语,表面上看好像不好理解。

简单看,就是一个异常。什么是异常?这又是硬件上的概念了,具体看看书

简单说,就是硬件上的一种机制,当硬件检测到某种“不对”时,主动触发,然后会自动跳转到异常处理程序处理。异常跟中断类似,区别在于,中断是异步的,由外设触发;异常是同步的,由CPU自己触发;好像又扯远了。

缺页异常就是一种特定的异常,触发条件是:MMU检测到页表项(页表中的项~)不存在。

何时触发缺页异常?

MMU何时会去检测页表项?

当CPU需要访问某个虚拟地址时,比如向某个虚拟地址写数据(memset),需要将虚拟地址转换成物理地址,此时MMU就会自动去页表中找相应的页表项,如果发现此时相应的页表项(也就是虚拟地址到物理地址的映射关系)还不存在,那么就会自动从硬件层面触发缺页异常。

也就说,缺页异常是硬件自己触发的,条件是当需要访问某个虚拟地址,而该虚拟地址还没有相应的页表项时。

典型的场景就是,当malloc之后,对相应的虚拟地址执行memset操作。

缺页异常中干嘛?

缺页异常后,会跳转到相应的异常处理程序处理,该异常处理程序是内核中提前注册好的,其中要做的主要操作,就是:分配物理内存,然后修改相应进程的页表,建立该物理内存和虚拟内存的映射关系(页表项)。

当然,实际实现要复杂得多,这里不详细描述。

回头看看

再来重头捋一下内存分配过程:

  1. 用户态程序使用malloc接口,分配虚拟地址。
  2. 用户程序访问该虚拟地址,比如memset。
  3. 硬件(MMU)需要将虚拟地址转换为物理地址。
  4. 硬件读取页表。
  5. 硬件发现相应的页表项不存在,硬件自动触发缺页异常。
  6. 硬件自动跳转到page fault的处理程序(内核实现注册好)
  7. 内核中的page fault处理程序执行,在其中分配物理内存,然后修改页表(创建页表项)
  8. 异常处理完毕,返回程序用户态,继续执行memset相应的操作。

至此,虚拟内存和物理内存都分配完成,并完成映射。

另一个角度看,如果malloc分配内存后,一直不使用,那就一直不会分配物理内存,这种内存分配策略叫延迟分配,有其相应的好处,自己理解一下~

接下来?

本章,从内存分配的角度看了Linux内核中内存管理的关键原理,已经以尽量简单的方式描述了,希望没给大家带来负担~

Linux内存管理还涉及其他很多方面,如:

  • 内核自身使用的内存(slab、vmalloc)
  • 伙伴系统
  • 进程地址空间管理

后面抽空慢慢讲,但都希望能尽量简单,希望大家一看就明白。