1. 存储层次
高速缓存(cache) --〉 主存(main memory) ---〉 磁盘(disk)
理解存储层次结构的根源:CPU速度和存储器速度的差距。
层次结构可行的原因:局部性原理。
LINUX的任务:
l 减小footprint,提高cache命中率,充分利用局部性。
l 实现虚拟存储以满足进程的需求,有效地管理内存分配,力求最合理地利用有限的资源。
2. MMU的作用
辅助操作系统进行内存管理,提供虚实地址转换等硬件支持。没有它,操作系统不能够真正实现进程空间的隔离,也就不能实现真正的多任务。比如uClinux,去掉了MMU相关的代码,所以在创建进程时只有vfork,而没有fork。
3. x86的地址
逻辑地址: 出现在机器指令中,用来制定操作数的地址。段:偏移
线性地址:逻辑地址经过分段单元处理后得到线性地址,这是一个32位的无符号整数,可用于定位4G个存储单元。
物理地址:线性地址经过页表查找后得出物理地址,这个地址将被送到地址总线上指示所要访问的物理内存单元。
4. x86的段
保护模式下的段:选择子+描述符。不仅仅是一个基地址的原因是为了提供更多的信息:保护、长度限制、类型等。描述符存放在一张表中(GDT或LDT),选择子可以认为是表的索引。段寄存器中存放的是选择子,在段寄存器装入的同时,描述符中的数据被装入一个不可见的寄存器以便cpu快速访问。
专用寄存器:GDTR(包含全局描述附表的首地址),LDTR(当前进程的段描述附表首地址),TSR(指向当前进程的任务状态段)
LINUX使用的段:
__KERNEL_CS: 内核代码段。范围 0-4G。可读、执行。DPL=0。
__KERNEL_DS:内核代码段。范围 0-4G。可读、写。DPL=0。
__USER_CS:内核代码段。范围 0-4G。可读、执行。DPL=3。
__USER_DS:内核代码段。范围 0-4G。可读、写。DPL=3。
TSS(任务状态段):存储进程的硬件上下文,进程切换时使用。(因为x86硬件对TSS有一定支持,所有有这个特殊的段和相应的专用寄存器)。但Linux并不使用这个Intel规定的机制进行进程切换。后面讲进程管理时会看到这点。
__USER_CS和__USER_DS段都是被所有在用户态下的进程共享的。注意不要把这个共享和进程空间的共享混淆:虽然大家使用同一个段,但通过使用不同的页表由分页机制保证了进程空间仍然是独立的。
段功能是Intel定义的,在X86系列处理器上必须使用。为了保持其处理器的前后兼容性,Intel保留了段机制。在386之后,又有段又有页,在保护机制上这是重复了,从而影响了存储器访问的性能。Linux很巧妙地将段功能的影响降到了最低。如通过使用基址为0的段,使逻辑地址==线性地址。也即是所有的进程都使用的是相同的线性地址空间0x00000000-0xC0000000.
5. x86的分页机制
与段式存储管理线笔,页式存储管理有很多好处。首先,页面都是固定大小的,便于管理。更重要的是,但要将一部分物理空间的内容换乘到磁盘上的时候,在段式存储管理中要将整个段都换出,而段通常都很大,效率上页式存储管理要高很多。
x86硬件支持两级页表,奔腾pro以上的型号还支持Physical address Extension Mode和三级页表。所谓的硬件支持包括一些特殊寄存器(cr0-cr4)、以及CPU能够识别页表项中的一些标志位并根据访问情况做出反应等等。如读写Present位为0的页或者写Read/Write位为0的页将引起CPU发出page fault异常,访问完页面后自动设置accessed位等。
linux采用的是一个体系结构无关的三级页表模型(如图),使用一系列的宏来掩盖各种平台的细节。例如,通过把PMD看作只有一项的表并存储在pgd表项中(通常pgd表项中存放的应该是pmd表的首地址),页表的中间目录(pmd)被巧妙地‘折叠’到页表的全局目录(pgd),从而适应了二级页表硬件。
6. Linux下进程虚地址空间
目前,在x86系列处理器上,Linux进程虚地址为32位线性地址。32位地址意味着4G字节的虚地址空间,Linux内核将这4G的地址空间分成了两部分。最高1G字节(0xC0000000-0xFFFFFFFF)用于内核本身,即系统空间。而低3G字节空间(0x00000000-0xBFFFFFFF)用于各进程的用户空间。这样,理论上每个进程可以使用的用户空间都是3G字节。当然,实际的空间大小受到物理存储器大小的限制。系统空间是由各个进程所共享了,每当一个进程因某种原因(系统调用或时钟中断等)进入了内核,该进程就在共享的系统空间中运行,也即是使用的地址可以是(0x00000000-0xFFFFFFFF)。而在用户空间运行的时候,则只能是低3G的地址空间。
7. TLB
TLB全称是Translation Look-aside Buffer,用来加速页表查找。这里关键的一点是:如果操作系统更改了页表内容,它必须相应的刷新TLB以使CPU不误用过时的表项。
8. Cache
Cache 基本上是对程序员透明的,但是不同的使用方法可以导致大不相同的性能。linux有许多关键的地方对代码做了精心优化,其中很多就是为了减少对cache不必要的污染。如把只有出错情况下用到的代码放到.fixup section,把频繁同时使用的数据集中到一个cache行(如struct task_struct),减少一些函数的footprint。
这里要说明一下,.fixup section,一种尽量使程序回到正常运行的机制,是GCC编译器提供的。Linux内核也是一个程序,所以也会有可能崩溃的时候,所以这里采用了一定保护措施,使内核能够回到之前的某个正常的点上。
9. Linux物理页面管理
内核维护着一个page结构数组mem_map,系统在初始化时根据物理内存的大小建立这个数组,作为物理页面的仓库,每一个数组元素都代表一个物理页面。
Linux使用了Buddy关系系统来维护物理页面的使用。
10. slab方法
Linux借鉴了Solaris的经验,设计了一种有效的管理内核数据结构的机制slab。每个slab即是一个对象容器,可以容纳若干同种对象的块。内核需要某种数据结构是就会在这里面找,效率很高。比如进程PCB task_struct结构、文件系统的inode结构。