Linux 内存管理导读
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
Linux 内存管理导读
1. 存储层次结构和x86存储管理硬件(MMU)
1.1存储层次
高速缓存(cache) à主存(main memory) à磁盘(disk) 理解存储层次结构的根源:CPU速度和存储器速度的差距。
层次结构可行的原因:局部性原理。
LINUX的任务:
l减小footprint,提高cache命中率,充分利用局部性。
l实现虚拟存储以满足进程的需求,有效地管理内存分配,力求最合理地利用有限的资源。
1.2 MMU的作用
辅助操作系统进行内存管理,提供虚实地址转换等硬件支持。
1.3 x86的地址
逻辑地址:出现在机器指令中,用来制定操作数的地址。段:偏移
线性地址:逻辑地址经过分段单元处理后(如将段地址左移四位与偏移量)得到线性地址,这是一个32位的无符号整数,可用于定位4G个存储单元。
物理地址:线性地址经过页表查找后得出物理地址,这个地址将被送到地址总线上指示所要访问的物理内存单元。
LINUX: 尽量避免使用段功能以提高可移植性。如通过使用基址为0的段,
使逻辑地址==线性地址。
1.4 x86的段与Linux下的段比较
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有一定支持,所有有这个特殊的段和相应的专用寄存器。)
__USER_CS和__USER_DS段都是被所有在用户态下的进程共享的。但这种段的共享和进程空间的共享是有区别的:虽然各个进程使用同一个(种)段,但通过使
用不同的页表由分页机制保证了进程空间仍然是独立的。
1.5 x86的分页机制
x86硬件支持两级页表,奔腾pro以上的型号还支持Physical Address Extension Mode (PAE)和三级页表。所谓的硬件支持包括一些特殊寄存器(cr0-cr4)、以及CPU能够识别页表项中(页表项的高20位是页面索引,而低12为标志位)的一些标志位并根据访问情况做出反应等等。如读写Present位为0的页或者写Read/Write位为0的页将引起CPU发出page fault异常,访问完页面后自动设置accessed位等。
具体可参见《Paging Extensions for the Pentium Pro Processor (Overview).htm》
Linux采用的是一个体系结构无关的三级页表模型(如图),使用一系列的宏来掩盖各种平台的细节。例如,为了适应x86的二级页面映射机制,Linux内核通过把PMD看作只有一项的表并存储在pgd表项中(通常pgd表项中存放的应该是pmd表的首地址),页表的中间目录(pmd)被巧妙地‘折叠’到页表的全局目录(pgd),从而适应了二级页表硬件。
虚拟地址
PGD
1.6 TLB
TLB全称是Translation Look-aside Buffer,用来加速页表查找。这里关键的一点是:如果操作系统更改了页表内容,它必须相应的刷新TLB以使CPU不误用过时的表项。
以下是从Intel技术文档中摘出的图,是一个TLB下的线性地址映射过程图
1.7 Cache
Cache 基本上是对程序员透明的,但是不同的使用方法可以导致大不相同的性能。linux有许多关键的地方对代码做了精心优化,其中很多就是为了减少对cache不必要的污染。如把只有出错情况下用到的代码放到.fixup section,把频繁同时使用的数据集中到一个cache行(如struct task_struct),减少一些函数的存储器足迹(footprint),在slab分配器里头的slab coloring等。
另外,我们也必须知道什么时候cache要无效:新map/remap一页到某个地址、页面换出、页保护改变、进程切换等,也即当cache对应的那个地址的内容或含义有所变化时。当然,很多情况下不需要无效整个cache,只需要无效某个地址或地址范围即可。实际上,intel在这方面做得非常好用,cache的一致性完全由硬件维护。
l关于x86处理器更多信息,请参照其手册:V olume 3: Architecture and Programming Manual,可以从ftp:///design/pentium/MANUALS/24143004.pdf获得
1.8 Linux 相关实现
这一部分的代码和体系结构紧密相关,因此大多位于arch子目录下,而且大量以宏定义和inline函数形式存在于头文件中。以i386平台为例,主要的文件包括:
1.8.1 page.h
页大小、页掩码定义。主要是定义了一些宏,如PAGE_SIZE,PAGE_SHIFT和
PAGE_MASK。对页的操作,如清除页内容clear_page、拷贝页copy_page、页对齐page_align 还有内核虚地址的起始点:PAGE_OFFSET(0xC0000000)和相关的内核中虚实地址转换的宏__pa和__va.。
宏virt_to_page从一个内核虚地址得到该页的描述结构struct page *。所有物理内存都由一个memmap数组来描述。这个宏就是通过计算给定地址的物理页在这个数组中的位置。另外这个文件也定义了一个简单的宏检查一个页是不是合法:VALID_PAGE(page)。
如果page离memmap数组的开始太远以至于超过了最大物理页面应有的距离则是不合法的。
但页表项的定义也放在这里。有pgd_t,pmd_t,pte_t和存取它们值的宏pte_val等
1.8.2 pgtable.h pgtable-2level.h pgtable-3level.h
这些文件就是处理页表的,它们提供了一系列的宏来操作页表。pgtable-2level.h和pgtable-2level.h则分别对应x86二级、三级页表的需求。在编译内核的时候通过编译条件来选择具体哪个文件。首先当然是表示每级页表有多少项的定义不同了。而且在PAE 模式下,地址超过32位,页表项pte_t用64位来表示(pmd_t,pgd_t不需要变),一些对整个页表项的操作也就不同。
pgtable.h的宏很多,但都比较直观,通常从名字就可以看出宏的意义。如pte_xxx宏的参数是pte_t,而ptep_xxx的参数是pte_t *。
pgtable.h里除了页表操作的宏外,还有cache和tlb刷新操作,因为他们常常是在页表操作时使用。但关于cache的宏实现全是空的,前面也讲过基本都有硬件做了。
l#define flush_cache_all() do { } while (0)
l#define flush_cache_mm(mm) do { } while (0)
关于tlb操作是以__开始的,也就是说,内部使用的,真正对外接口在pgalloc.h中。
1.8.4 pgalloc.h
包括页表项的分配和释放宏/函数,值得注意的是表项高速缓存的使用:
pgd/pmd/pte_quicklist
还有上面提到的tlb刷新的接口
1.8.5 segment.h
定义Linux中使用的四个段的
#define __KERNEL_CS 0x10
#define __KERNEL_DS 0x18
#define __USER_CS 0x23
#define __USER_DS 0x2B