天天看点

尽力说透linux内存管理

前言废话:

linux内存管理涉及的原理知识太多了,也是学习linux系统软硬件绕不开的部分,笔者水平有限,只能随心列出一点点理解,希望能帮助到众多学习linux的技术人员。

我们知道处理器core序列化执行指令,第一步是读指令,从哪里读呢,当然是支持随机访问的ram存储器(norflash也可以,这里不说)。

当然啦,要读ram,就需要一个叫做地址的东西去规定从ram的哪一个单元去读,这里拿32位core总线来举例简单说明硬件原理,从硬件上说就是在某几个时钟周期内给这32位地址总线上加上不一样的电平,比如这32位地址线的状态是10000000 00000000 00000000 00000000,就代表要操作第0x80000000的存储单元,然后再给用电平逻辑告诉ram是要读,然后就用数据总线输出这个单元的数据。

处理器core执行指令干嘛呢?当然是处理数据,对于cpu而言,直接的数据来源可能是ram,也可能是某外设(统一编地址独立编址,这里不说),都是需要一个地址来表征要操作哪个单元。

综上所述,地址很重要。

正题:

1.三类地址:逻辑地址 虚拟地址 物理地址

物理地址:当存储器与cpucore总线接好之后,每一个存储单元的地址在特定的配置下是唯一的,不变的,每个存储单元都有一个地址,这个地址就是物理地址。

逻辑、虚拟地址:linux系统中,虚拟地址是理论地址,是理论上能操作的地址,有一个范围,比如32位系统,就是0~0xffffffff,并且在linux系统中,逻辑地址与虚拟地址一致,举例,逻辑地址的0x88888888就是虚拟地址的0x88888888,可这个逻辑、虚拟的0x88888888可不一定对应到物理地址的0x88888888,可能一会是0x02019888一会是0x12019888,linux内存管理的魅力就在这里。那干脆一个名字不行么,不行,逻辑地址只是说和虚拟地址在linux系统中一致,他它两在概念上差别可大了,这里我们不多说。

使用了虚拟地址之后,整个软件上层的设计变得简单多了,代码的编译连接,操作系统内核、应用空间的地址规划,应用程序的执行地址空间等等都统一的使用虚拟地址,与硬件平台地址的耦合性大大降低,使得软件的开发移植变得相当容易,通用性大大增强。

一个现实的问题是,不是所有使用32位core的系统都要配全4G地址的存储和设备,在linux早期很多系统只有几百k 大小的ram,那虚拟地址和物理地址一定要产生某些联系才行,我们接下来就来看看linux下这个逻辑、虚拟地址(以下统称虚拟地址)是怎样与物理地址产生关联的。

2.MMU

MMU是一个与core相连的模块,其功能就是是把虚拟地址转化成物理地址,我们搞个图来看看:

尽力说透linux内存管理

图相当明显了,功能上没什么好说的。但具体到这个模块是怎么工作的,怎么配置的,那就有的说了,再搞张图来看看:

尽力说透linux内存管理

上图是个两级mmu的映射表示,20-31 共12bits一级,对应为4096个项,第二级12-19共8bits共256项,最后0-11共12bits是OFFSET。

举例:给一个虚拟地址1000 0000 0001 0001 0001 0000 0000 0100对应的物理地址是啥呢,我们就要去查表,首先查20-31bit 一级映射1000 0000 0001对应的物理地址的第20-31bit表的第1000 0000 0001项里面的内容是0000 0000 0001 ,物理地址12bits就有了,以此类推,二级0001 0001项的内容是0000 0001则物理地址的中间8bit就有了,然后最后再来个0000 0000 0100的OFFSET,整个物理地址就出来了:0000 0000 0001 0000 0001 0000 0000 0100,虚拟地址0x80111008,经MMU转化之后变成0x00111008并且值小了许多。linux虚拟地址0-4G而实际运行在物理内存只有几百M的的系统上的原因了。

MMU硬件模块作为CORE和MEM的中介,它到底做了哪些具体的事呢?大体说来可以描述如下:

尽力说透linux内存管理

从上面的图中可以看出,core访问实际的物理存储需要经过mmu,mmu要从物理存储中读取页表,计算物理地址,然后core再用物理地址访问真正需要的目的地,这么几个来回的访问,直观上看,访问速度好像慢了好几倍啊,其实并无多大影响,接着看:

这里就不得不介绍cache了,不同的随机存储设备,访问速度是不一样的,core的主频往往比存储设备的主频高的多,几倍甚至几十倍,数据吞吐的关键是外部存储的速度,内存访问的话,就是DDR的速度,DDR时钟周期是N,core是M,则不论M多小,访问一次ddr都需要至少N时间,cache的访问速度可以与cpu相当,在mmu从物理内存中读取页表的时候,可以直接从cache中读,速度极快,一个ddr的访问周期里面,core ,mmu,cache已经把虚拟地址,物理地址的事给做完了,并未增加多少时间,并不会需要n*N的时间,当然啦,cache大小有限,会出现miss的情况,miss了就真的需要花多余的时间去DDR中读页表了。

那么上图中操作步骤就可以描述为:

1—CPU内核(图中的ARM)发出VA请求读数据,TLB(translation lookaside buffer)接收到该地址,那为什么是TLB先接收到该地址呢?因为TLB是MMU中的一块高速缓存(也是一种cache,是CPU内核和物理内存之间的cache),它缓存最近查找过的VA对应的页表项,如果TLB里缓存了当前VA的页表项就不必做translation table walk了,否则就去物理内存中读出页表项保存在TLB中,TLB缓存可以减少访问物理内存的次数。

2—页表项中不仅保存着物理页面的基地址,还保存着权限和是否允许cache的标志。MMU首先检查权限位,如果没有访问权限,就引发一个异常给CPU内核。然后检查是否允许cache,如果允许cache就启动cache和CPU内核互操作。

3— 如果不允许cache,那直接发出PA从物理内存中读取数据到CPU内核。

4— 如果允许cache,则以VA为索引到cache中查找是否缓存了要读取的数据,如果cache中已经缓存了该数据(称为cache hit)则直接返回给CPU内核,如果cache中没有缓存该数据(称为cache miss),则发出PA从物理内存中读取数据并缓存到cache中,同时返回给CPU内核。但是cache并不是只去读取Core所需要的数据,而是把相邻的数据都去上来缓存,这称为一个cache line。

拓展:现在有6G内存,12G内存是如何处理的呢?

----用64bit的core,当然32bit的core也有过一些曲线的方法来访问>4G的内存,这里就不讨论了。

3.linux系统三个数据集:页目录DIRECTORY,页表TABLE,偏移量OFFSET

32位系统中,

页目录:1024项,每项32bits,每项大小范围二进制10bits即0~1023也即1024个项,每项里面的内容是啥呢?是物理内存中一个页表的地址,注意是物理地址,也就是说MMU硬件上去访问内存,从内存中拿页表的地址,使用的是物理地址。另外这里的1024不准确,不同体系的cpu可能不一样,arm会有4096项,甚至将来的8192项等等。

盗个图来看看:

尽力说透linux内存管理

图中只能是示意图,是intel X86 32位的情景下的图,读者可能会发现,与上面的mmu映射有点像哦.directory,table到底是什么样的数据结构,内核软件上如何实现对他们的管理的呢,只能深入代码来看了。

4.linux存储系统初始化

我在刚开始接触kernel的时候,我很崇拜和迷惑,因为2000多万行的代码确实容易让人迷惑,忽视了cpu执行代码的本质逻辑。本质上说,在cpu看来,内核代码和你写的printf(“hello world”)没有本质区别,都是要告诉我指令在哪里,数据往那里存,执行什么运算,就这么简单。从代码的角度看,一个段代码,有数据段,代码断,堆栈段,有全局变量,有局部变量,内核代码量再大功能再天花乱坠,这个本质没有变。所以不要看到kernel就觉得,好复杂啊,不知道从哪入手啊,知道上面的本质之后,是不是突然觉得,也就是代码量大一点,功能多一点而已。

挑出内存管理着一块的内核代码来阅读:

首先解决第一个问题,内核image如何存放,cpu如何从ddr中取得内核的第一条指令,代码如何指定内核的entry地址的,又是怎么安排一个语句为第一条指令的。下

我们知道,bootloader在把内核的image,ramdisk还有设备树文件dtb从flash读到内存之后,把dtb和ramdisk的分区或者地址写在某个固定的地址上或者寄存器中,然后跳转到kernel entry开始执行kernel。

(等待继续)

5.内存分配管理函数(代码)

6.内存碎片的产生以及mem pool技术

未完待续

继续阅读