x86
CPU采用了段页式地址映射模型。进程代码中的地址为逻辑地址,经过段页式地址映射后,才真正访问物理内存。
段页式机制如下图。
linux内核地址空间划分
通常32位linux内核地址空间划分0~3G为用户空间,3~4G为内核空间。注意这里是32位内核地址空间划分,64位内核地址空间划分是不同的。
linux内核高端内存的由来
当内核模块代码或线程访问内存时,代码中的内存地址都为逻辑地址,而对应到真正的物理内存地址,需要地址一对一的映射,如逻辑地址0xc0000003对应的物理地址为0×3,0xc0000004对应的物理地址为0×4,…
…,逻辑地址与物理地址对应的关系为
物理地址
=
逻辑地址
0xC0000000
逻辑地址物理内存地址0xc00000000×00xc00000010×10xc00000020×20xc00000030×3…
…
0xe00000000×20000000……0xffffffff0×40000000
??
显然不能将内核地址空间0xc0000000
~
0xfffffff全部用来简单的地址映射。因此x86架构中将内核地址空间划分三部分:ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM。ZONE_HIGHMEM即为高端内存,这就是内存高端内存概念的由来。
在x86结构中,三种类型的区域如下:
ZONE_DMA
内存开始的16MB
ZONE_NORMAL
16MB~896MB
ZONE_HIGHMEM
896MB
~
结束
linux内核高端内存的理解
前面我们解释了高端内存的由来。
linux将内核地址空间划分为三部分ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM,高端内存HIGH_MEM地址空间范围为0xF8000000
~
0xFFFFFFFF(896MB~1024MB)。那么如内核是如何借助128MB高端内存地址空间是如何实现访问可以所有物理内存?
当内核想访问高于896MB物理地址内存时,从0xF8000000
~
0xFFFFFFFF地址空间范围内找一段相应大小空闲的逻辑地址空间,借用一会。借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核PTE页面表),临时用一会,用完后归还。这样别人也可以借用这段地址空间访问其他物理内存,实现了使用有限的地址空间,访问所有所有物理内存。如下图。
例如内核想访问2G开始的一段大小为1MB的物理内存,即物理地址范围为0×80000000
~
0x800FFFFF。访问之前先找到一段1MB大小的空闲地址空间,假设找到的空闲地址空间为0xF8700000
~
0xF87FFFFF,用这1MB的逻辑地址空间映射到物理地址空间0×80000000
~
0x800FFFFF的内存。映射关系如下:
逻辑地址物理内存地址0xF87000000×800000000xF87000010×800000010xF87000020×80000002…
…0xF87FFFFF0x800FFFFF
当内核访问完0×80000000
~
0x800FFFFF物理内存后,就将0xF8700000
~
0xF87FFFFF内核线性空间释放。这样其他进程或代码也可以使用0xF8700000
~
0xF87FFFFF这段地址访问其他物理内存。
从上面的描述,我们可以知道高端内存的最基本思想:借一段地址空间,建立临时地址映射,用完后释放,达到这段地址空间可以循环使用,访问所有物理内存。
看到这里,不禁有人会问:万一有内核进程或模块一直占用某段逻辑地址空间不释放,怎么办?若真的出现的这种情况,则内核的高端内存地址空间越来越紧张,若都被占用不释放,则没有建立映射到物理内存都无法访问了。
Linux内核采用页式存储管理。虚拟地址空间划分成固定大小的“页面”,由MMU在运行时将虚拟地址“映射”成(或者说变换成)某个物理内存页面中的地址。与段式存储管理相比,页式存储管理有很多好处。首先,页面都是固定大小的,便于管理。更重要的是,当要将一部分物理空间中的内容换出到磁盘上的时候,在段式存储管理中要将整个段(通常都很大)都换出,而在页式存储管理中则是按页进行,效率显然要高得多。页式存储管理与段式存储管理所要求的硬件支持不同,一种CPU既然支持页式存储管理,就无需再支持段式存储管理。但是,i386的情况是特殊的。由于i386系列的历史演变过程,它对页式存储管理的支持是在其段式存储管理已经存在了相当长的时间以后才发展起来的。所以,不管程序是怎样写的,i386 CPU一律对程序中使用的地址先进行段式映射,然后才能进行页式映射。既然CPU的硬件结构是这样,Linux内核也只好服从Intel的选择。这样的双重映射其实是毫无必要的,也使映射的过程变得不容易理解,以至有人还得出了Linux采用“段页式”存储管理技术这样一种似是而非的结论。Linux内核所采取的办法是使段式映射的过程实际上不起作用(除特殊的VM86模式外,那是用来模拟80286的)。Linux内核中只使用四种不同的段寄存器数值,两种用于内核本身,两种用于所有的进程:
-----------------------------------------------------------
__KERNEL_CS 0x100 0 0 0 0 0 0 0 0 0 0 1 0 | 0 | 0 0
__KERNEL_DS 0x180 0 0 0 0 0 0 0 0 0 0 1 1 | 0 | 0 0
__USER_CS 0x230 0 0 0 0 0 0 0 0 0 1 0 0 | 0 | 1 1
__USER_DS 0x280 0 0 0 0 0 0 0 0 0 1 0 1 | 0 | 1 1
-----------------------------------------------------------
按照Intel对段寄存器的定义我们可以看出这四个段地址描述符都使用GDT表,再结合linux初始化时设置的GDT表的内容(全局段地址描述符表)我们可以知道他们所描述的段都是从0地址到4GB的整个32位地址空间,也就是说经过这些段地址描述符映射后的地址与映射前保持原值不变。所以说实际上Linux根本没有使用80386以上CPU所提供的段式存储管理机制。
它们之间的唯一区别就是内核态的优先级是0,而用户态的优先级是3。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)