内核层面的代码比较难理解,一方面可能是涉及底层相关的一些操作,比如寄存器、Cache、各种锁、屏障、中断、SMP抢占、调度等,这让内核代码运行时考虑的东西比较多;另一方面我觉得就是内存相关的部分。这倒不是说内存本身的使用部分,而是内核本身就处于自己管理的内存之中运行。对于这第二点,是否真的理解起来比较费解,可能要因人而异。对于应用层,我们总能找到一个第三方,帮助我们管理内存,简言之,就是用户空间的程序可以将自己和内存隔离开,而在内核里,这一点就感觉比较绕。
下面,自己再给自己梳理一下。之前对这部分有过一些整理,参见《程序的运行视图》。程序的运行视图_龙赤子的博客-CSDN博客_运行视图
首先,我们知道CPU和内存是两个部件。CPU从内存中取东西(指令和数据),处理后,再写回内存(数据)。所有的过程都可以抽象为这三步。
如何处理取到的“东西”是指令决定的。指令是代码,存在于内存中。所以,内存里既有指令又有数据。指令使用的数据一般分为三个部分,一部分是全局静态的,比如全局变量,静态变量所定义的数据。这些在数据段存放。一部分是局部的,这些在栈上存放。栈也是一块相对固定的内存。最后,就是动态分配的,这些在堆上存放,相对而言,不那么固定,这些就需要一个第三方来帮助管理。也就是说,堆上的内存,不是自己管理的,而是另外的第三方。在上层用户空间代码环境中,内存可以想象为由内核或者特定的库在管理。而在内核中,内存可以想象为内核的某一个模块在管理。无论是哪一种方式,都可以归结为一个管理器。这个管理器本身的构成又是一块代码加全局内存、加栈、加堆构成的。对内核而言,此时管理器就不再有堆之说,否则就无限循环下去了。我们可以理解为,管理器看管了一大块内存,它自己不需要额外分配内存,即使需要,也是提前准备好了。然后,所有其他模块获取内存,就找管理器要好了,用完还给管理器就好了。
基于上面的描述,我们来总结一下。我们编写完的程序,会由编译器和连接器组织成一个特殊结构的文件。参见《程序的构成系列》:程序的构成之一_龙赤子的博客-CSDN博客
对内核而言,也不例外。而这个特殊的文件(二进制程序文件),最主要的构成就是代码和数据。其要运行时,需要占用内存。对内核而言,会给其预先准备一块内存使用。这块内存中的代码没有什么特别的,但是数据就需要根据上面的说明,做些区分。首先是全局的数据,是跟代码随着二进制文件一起加载到内存中,占用了固定的内存空间。我们可以认为,加载进内存的全局数据存放在特定位置(无论是在小地址区域还是大地址区域,都不应该跟其他使用内存的部分产生冲突),内核代码可以访问到它们。后面,我们将其称为内核数据区。其次,是栈。只要执行函数,就需要栈来做辅助,除非是全汇编的代码。所以,内核在头部汇编完成处理,准备跳转到start函数执行时,需要创建一个栈。这个栈是内核自己主动创建的,通过找一块安全的内存。我们将其称为内核栈区。有了这些,内核的创世代码就可以执行了。基于上述所述已构建的环境,内核可以完成许多基础的工作,并为开展复杂的工作做准备(这是很关键的。在之前有关编译器的说明中,也是这个思路)。最后,内核在执行过程中,如果需要动态获取较大块的内存时,该怎么办?最初,堆内存的分配管理环境还没有构建起来(可以理解为相关管理数据结构的复杂关系还没有在管理器所属的内存中填写记录,代码的逻辑是基于这些管理数据,比如管理表来完成具体的功能的)。此时,内核通过一个简单的分配器完成创世阶段的内存分配需求。
上面所述过程中使用的内存,是可以调整的,或者说改变的。比如,内核可以将自身搬移到另一个地方,只要处理好代码和数据的重定位即可。否则,这部分内存就需要永久占用了。例外就是一过性的代码部分(如果可以提取出来,比如通过分成几个部分)和初始代码用到的栈所占用的内存最后是可以释放的。这样,可以节省内存。而对于全局访问的数据和分配的内存,如果正常运行过程中的代码还会用到的话,就属于开始提到的永久占用,其所占用的内存是不能动的。当然,全局数据经过编译链接加载完后,位置和大小基本上也就是确定的。这里主要是说动态分配的部分。如果后续存在新的分配器,那么新分配器管理的内存就要避开前期动态分配的部分,或者无缝衔接,接管之前分配的内存(如果前期的内存管理器分配的内存在管理器退出工作时没有释放完的话。Linux下的保留内存可以做到这一点,平时可以用于别的模块,需要时,再迁移回来,专存专用)。从这里,我们看出,为了管理内存,内核需要对自己使用的内存有清晰的掌握(包括它们对应的物理地址空间)。
现在,当我们看内核代码时,脑海里应该有这样一幅图景:(结合Linux本身内存处理的实际情况)参见《虚拟内存之step by step系列》7.4 Step by step之虚拟内存一_龙赤子的博客-CSDN博客
有两个地址空间,一个是整个物理内存空间,一个是虚拟线性空间。内核对这两个空间都要了如指掌。
内核代码本身占据了物理空间的一部分,内核数据也占据了物理空间的一部分。这两部分是固定的。CPU在执行内核代码过程中,不断的修改内核数据,以此构建一部分管理逻辑。内核在执行过程中,也会按需占用一部分内存,这部分内存是用作特定用途的,比如异常向量表,页表等。虽说是分配,但是基本上也不用动态分配,通过自己掌握的一些信息(比如我(内核)是第一个使用内存的,所以,可以完全判断出那些位置的内存肯定是可用的),来主动构建相关的数据。再就是内核创世时动态分配的内存,虽没有看到这部分代码,但是可以预见也是精心选择分配的。如果后续需要释放,是需要做好相关记录工作的。最后,就是最底层的内存管理器自身占用的内存。这部分数据内存占用(代码占用部分融合进内核代码段里了)是用于让内存管理器构建管理结构,比如记录总共有多少内存,分别在什么地址,那些分配出去了,那些还没有,对应的物理地址和虚拟地址是什么等等。这部分在内核里就是与page直接相关的部分。如果页面大小确定了,总的内存大小确定了,这部分需要的内存量其实也就可以计算出来了。
上述这些准备ok后,内核就可以真正的动态分配内存了,真正的功能扩展也就可以展开了。我们可以想象出此时,内核的内存管理器对可用内存的情况了如指掌(通过相关数据结构),内核里所有需要动态分配内存的地方,都跟该管理器要内存。管理器来者不拒,高效完成使命。
为了更好的理解上面的过程,我在这里以最近学习的slab举个例子。同时,也为了更好的掰开内存和其上的代码,我们可以将内存想象为土地,代码和相关逻辑如土地上的城堡。城堡里有各个机构和相关组织,并通过管理有机串起来。显然,城堡自身也是要占用土地的。但是,城堡往往管理着更大的土地。
下一节,我们就具体介绍slab的内存管理思想。