关于linux内核空间

关于linux内核空间

linux内核对整个系统的物理内存是通过类型为struct page的数组mem_map来管理的。系统中的伙伴系统分配算法最终是通过操作这个数组来记录物理内存的分配、回收等操作。在这里不要被系统的高端内存、低端内存等概念搞混淆了,高、低端内存的分类主要在于区分物理内存地址是否可以直接映射到内核线性地址空间中。

我们知道,linux的内核地址空间大小为1G用户空间0~3G,内核空间3G~4G,这种分法最常见),因此如果把这1G线性地址空间全部拿来直接一一映射物理内存的话,在内核态的所有进程(线程)能使用的物理内存总共最多只有1G,为了能使在内核态的所有进程能使用更多的物理内存,linux采取了一种变通的形式:它将1G内核线性地址空间分为几部分,第一部分为1G的前896M,这部分内核线性空间与物理内存的0~896M一一映射(相差一个为0xc00000003G的常数),后面128M的线性空间拿来动态映射剩下的所有物理内存,由于动态映射的方法不一样,后面的128M又分成了几个部分,有兴趣的可以查看相关资料。在这里,前面896M线性空间对应的物理内存就是所谓的低端物理内存,剩下的物理内存就是高端物理内存。

从上面高、低端物理内存命名的由来我们可以知道,高、低端物理内存与具体的内存分配算法无关,它们都是被mem_map数组控制起来,再由伙伴分配系统实施管理。

 

关于进程及其内存分配

首先要明白一个概念:进程中使用的所有地址都是虚地址linux中进程可运行在用户态和内核态,(典型配置情况下)当进程运行在用户态时,它使用的线性地址只能位于0~3G范围内,当进程运行于内核态时,它使用的线性地址地址范围为3G~4G

为了把线性地址转化为物理地址,每个进程都有自己私有的页目录和页表。linux在建立进程页目录时,把用户地址空间的页目录项(0~767项)清空而将内核页目录表(swapper_pg_dir)的第768项到1023项拷贝到进程的页目录表的第768项到1023项中。由于内核在初始化时也只映射了物理内存的前896M,我们可以知道内核也目录表只能保证第768项开始的224项中有有效映射。从这里我们可以知道,所有的进程都共享了其内核线性地址空间。

当一个进程在内核空间发生缺页故障的时候,这主要发生在访问内核空间动态映射区线性地址,在其处理程序中,就要通过0号进程的页目录(swapper_pg_dir)来同步本进程的内核页目录,实际上就是拷贝0号进程的内核页目录到本进程中(内核页表与进程0共享,故不需要复制)。如果进程0的该地址处的内核页目录也不存在,则出错,具体代码可以参考vmalloc的实现源码。

当进程运行于用户态时,若其需要申请内存空间,内核首先会在其用户线性空间中分配需要的线性地址空间,再通过伙伴分配系统分配物理内存并把分配的物理内存跟用户空间线性地址映射起来,最后再修改进程的页目录项及页表项写入这些映射关系。

 

注;

逻辑地址(Logical Address  指由程序产生的与段相关的偏移地址部分。例如,你在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相 对于你当前进程数据段的地址,不和绝对物理地址相干。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,Cpu不进行 自动地址转换);逻辑也就是在Intel 保护模式下程序执行代码段限长内的偏移地址(假定代码段、数据段如果完全一样)。应用程序员仅需与逻辑地址打交道,而分段和分页机制对您来说是完全透明 的,仅由系统编程人员涉及。应用程序员虽然自己可以直接操作内存,那也只能在操作系统给你分配的内存段操作。

线性地址(Linear Address 是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机 制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386的线性地址空间容量为4G232次方即32根地址总线寻址)。

物理地址(Physical Address 是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。

虚拟内存(Virtual Memory 是指计算机呈现出要比实际拥有的内存大得多的内存量。因此它允许程序员编制并运行比实际系统拥有的内存大得多的程序。这使得许多大型项目也能够在具有有限 内存资源的系统上实现。一个很恰当的比喻是:你不需要很长的轨道就可以让一列火车从上海开到北京。你只需要足够长的铁轨(比如说3公里)就可以完成这个任 务。采取的方法是把后面的铁轨立刻铺到火车的前面,只要你的操作足够快并能满足要求,列车就能象在一条完整的轨道上运行。这也就是虚拟内存管理需要完成的 任务。在Linux 0.11内核中,给每个程序(进程)都划分了总容量为64MB的虚拟内存空间。因此程序的逻辑地址范围是0x00000000x4000000

与物理地址空间类似,线性地址空间也是平坦的4GB地址空间,地址范围从00xFFFFFFFF,线性空间中含有为系统定义的所有段和系统表。

有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的。

逻辑地址与物理地址的差距0xC0000000,是由于虚拟地址->线性地址->物理地址映射正好差这个值。这个值是由操作系统指定的。

 

虚拟地址到物理地址的转化方法是与体系结构相关的。一般来说有分段、分页两种方式。以现在的x86 cpu为例,分段分页都是支持的。Memory Mangement Unit负责从虚拟地址到物理地址的转化。逻辑地址是段标识+段内偏移量的形式,MMU通过查询段表,可以把逻辑地址转化为线性地址。如果cpu没有开启 分页功能,那么线性地址就是物理地址;如果cpu开启了分页功能,MMU还需要查询页表来将线性地址转化为物理地址:
逻辑地址 —-(段表)—> 线性地址(页表)—> 物理地址
不同的逻辑地址可以映射到同一个线性地址上;不同的线性地址也可以映射到同一个物理地址上;所以是多对一的关系。另外,同一个线性地址,在发生换页以后,也可能被重新装载到另外一个物理地址上。所以这种多对一的映射关系也会随时间发生变化。

运行时环境

运行时环境

堆栈帧

       一个函数分成三个部分:函数序prologue、函数体body和函数跋epilogue

l  函数序prologue:用于执行函数启动需要的一些工作,例如为局部变量保留堆栈中的内存;

l  函数体body:用于执行有用工作的地方;

l  函数跋epilogue:用于在函数即将返回之前清理堆栈;

C和汇编语言的接口

为了从汇编语言程序调用C程序:

l  如果寄存器d0d1a0a1保存了重要的值,它们必须在调用C程序之前进行保存,因为C函数不会保存它们的值;

l  任何函数的参数必须以参数列表相反的顺序压入堆栈中;

l  函数必须由一条跳转子程序类型的指令调用,它会把返回地址压入到堆栈中;

l  C函数返回时,汇编程序必须清楚堆栈中的任何参数;

l  如果汇编程序期望接收一个返回值,它将保存在d0(如果返回值的类型为double,它的另一半将位于d1);

l  任何在调用之前进行过保存的寄存器此时可以恢复;

 

为了编写一个由C程序调用的汇编程序:

Ø  保存任何你希望修改的寄存器(d0d1a0a1除外);

Ø  参数值从堆栈中获得,因为调用它的C函数把参数压入在堆栈中;

Ø  如果函数应该返回一个值,它的值应该保存在d0中;

Ø  在返回之前,函数必须清除任何它压入到堆栈中的内容;

运行时效率

       现代的计算机系统提供了虚拟内存Virtual Memory),虚拟内存是由操作系统实现的,它在需要时把程序的活动部分放入内存并把不活动的部分复制到磁盘中,这样就允许系统运行大型的程序。但程序越大,需要进行复制的就越多,也就会导致执行效率的降低。

如果一个程序太大,可以从最大的函数和数据结构入手;

如果一个程序太慢,可以对程序进行性能评测,找到程序中花费时间最多的那部分进行优化;

总结

       提高程序效率的最好方法是为它选择一种更好的算法。