1.Linux内核映射
从上面的页表设置可以看出:
- 内核对内核虚拟地址和物理地址之间的转换,是会有需求的。
很容易可以想到最简单的解决方法:
- 将内核虚拟空间地址,和实际物理空间逐一对应进行线性映射。
在很早期的时候,确实就是这样做的。
但是在32位时代,内核的虚拟空间只有1G,也就是说全部都进行线性映射的话,
内核只能使用1G物理内存,但是我们也知道就是32位时代,其实物理内存很多也不止4G了,
而且CPU后面也相应支持。
但是,虚拟地址空间还是只有4G,内核还是只有1G,如果还是进行直接线性映射的话,
内核根本没办法享受这个福利,原因很简单:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gTHtDY04-1594714031611)(C:\Users\RayInt\Desktop\workplace\RayBlog\Linux学习\页表研究\pic1.png)]
那该怎么办呢?也很简单,抽出一个部分内核虚拟空间,像用户虚拟空间那样进行非线性映射就行了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fbwFYFE1-1594714031614)(C:\Users\RayInt\Desktop\workplace\RayBlog\Linux学习\页表研究\pic2.png)]
可以设想那样的操作:
- 数组p放在了物理空间[paddr1, paddr8],内核虚拟空间为[kvaddr1, kvaddr4];
- 首先讲 [kvadd1, kvaddr4] 映射到 [paddr1, paddr4],进行操作;
- 操作完毕,解除映射;
- 然后将 [kvaddr1, kvaddr4] 映射到 [paddr5, paddr8],进行操作;
- 操作完毕,解除映射;
这样就可以用很小的内核虚拟空间,操作大很多的物理空间;
2. 什么是低端内存和高端内存
有了上面的铺垫,高低端内存是什么就很容易说明了。
没错,高低端内存是内核虚拟空间中的划分:
-
低端内存就是内核虚拟空间中,直接和物理空间进行线性映射的那一部分,
类似上图中的内核虚拟地址1~2;
-
高端内存就是内核虚拟空间中,和用户空间一样进行动态映射的一部分,
类似上图中的内核虚拟地址3;
但请注意,
就如上一节所说那样,这是因为在32位架构下,内核虚拟空间实在太小了,
所以抽出了一部分作为高端内存,以达到操作大于1G物理内存的效果。在64位内核虚拟空间足足有512G,
高端内存这个概念,一般来说在64位内核中应该不存在。但64位内核内核还是存在动态映射空间的。
3. 32位架构下的内核虚拟空间布局
引自Linux内存描述之高端内存–Linux内存管理(五)
在x86架构中,三种类型的区域(从3G开始计算)如下:
区域 | 位置 |
---|---|
ZONE_DMA | 内存开始的16MB |
ZONE_NORMAL | 16MB~896MB |
ZONE_HIGHMEM | 896MB ~ 结束(1G) |
IA32架构将高端内存划分为3部分:
- VMALLOC_START~VMALLOC_END
- KMAP_BASE~FIXADDR_START
- FIXADDR_START~4G
4. 64位架构下的内核虚拟空间布局
-
x86_64架构下的linux内核空间布局(内核版本为2.6.12,新版本中会有差异,但差异不算非常大),
文件路径为./linux-2.6.12/Documentation/x86_64/mm.txt:
<previous description obsolete, deleted>Virtual memory map with 4 level page tables:0000000000000000 - 00007fffffffffff (=47bits) user space, different per mm hole caused by [48:63] sign extension ffff800000000000 - ffff80ffffffffff (=40bits) guard hole ffff810000000000 - ffffc0ffffffffff (=46bits) direct mapping of phys. memory ffffc10000000000 - ffffc1ffffffffff (=40bits) hole ffffc20000000000 - ffffe1ffffffffff (=45bits) vmalloc/ioremap space ... unused hole ... ffffffff80000000 - ffffffff82800000 (=40MB) kernel text mapping, from phys 0 ... unused hole ... ffffffff88000000 - fffffffffff00000 (=1919MB) module mapping spacevmalloc space is lazily synchronized into the different PML4 pages of the processes using the page fault handler, with init_level4_pgt as reference.Current X86-64 implementations only support 40 bit of address space, but we support upto 46bits. This expands into MBZ space in the page tables.-Andi Kleen, Jul 2004
-
PAGE_OFFSET
位于./linux-2.6.12/include/asm-x86_64/page.h,以下讨论均基于x86_64平台。
/*** 可以很容易在上面的内存分布,找到PAGE_OFFSET* ffff810000000000 - ffffc0ffffffffff (=46bits) direct mapping of phys. memory* 如上所示,PAGE_OFFSET是内核虚拟空间的起始地址*/ #define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET) #define __PAGE_OFFSET 0xffff810000000000UL
-
__START_KERNEL
/*** 可以很容易在上面的内存分布,找到__START_KERNEL_map* ffffffff80000000 - ffffffff82800000 (=40MB) kernel text mapping, from phys 0* * __START_KERNEL_map放了一些映射相关的数据结构,* 然后到内核代码kernel text区,也就是 __START_KERNEL*/ #define __START_KERNEL 0xffffffff80100000UL #define __START_KERNEL_map 0xffffffff80000000UL
-
内核虚拟地址转物理地址的宏__pa
/*** 原本不是很能理解这段代码,后来突然明白了。* 因为在x86_64架构中,kernel text等在虚拟空间中布局到了高地址空间,* 而如vma、页表等内核数据结构在虚拟空间布局到了低地址空间,* * 但在物理空间,这却是相反的,* * kernel text等实际处于0开始的低地址物理空间,* 而如vma、页表等内核数据结构处于较高地址物理空间。** 因此需要两个偏移量__START_KERNEL_map和PAGE_OFFSET。*/ #define __pa(x) (((unsigned long)(x)>=__START_KERNEL_map)?(unsigned long)(x) - (unsigned long)__START_KERNEL_map:(unsigned long)(x) - PAGE_OFFSET)
5.内核空间布局思路
我觉得其实本质上,内核区域可以分成两类 (注意,这是本文为了帮助理解自行定义的) :
-
静态区域,直接和物理空间进行永久线性映射。
内核也将会永久占据着这段连续的物理空间,分配到的内存块在物理上连续;
-
动态区域,按需和物理空间进行非线性映射。
内核和用户进程共同使用这段物理空间,和用户进程一样分配到的内存块在物理上不一定连续;
下面尝试分析了一下内核空间布局的演进:
-
在最早期的内核空间只有静态区域。
内核空间和用户空间不但在虚拟空间中相互隔离,在物理空间中也同样相互隔离。
内核空间不但在虚拟空间中是固定大小的,在物理空间也是固定大小的。
内核总共能使用的物理空间就只有1G。
-
内核引入了高端内存概念,内核空间中有了静态区域和动态区域。
静态区域如上所述,跟用户空间是虚拟 + 物理双重隔离,
动态区域也就是高端内存区域,是跟用户进程共同使用的。
内核空间虽然在虚拟空间是固定大小的,但通过虚拟地址复用在物理空间可以更大。
内核总共能使用的物理空间大于1G。
说一说我猜想的复用思路,
这样达成的效果是虽然只有1G内核空间,但通过牺牲一些性能可以模拟出远大于1G内核空间的效果:
1.A程序段申请一段内存其物理空间为 [paddr1, paddr4],并将其映射到高端空间 [khaddr1, khaddr4],然后进行操作,且之后还需要继续操作;2.并不释放这段物理空间,对这段已分配的物理空间进行一些记录,但释放掉这段物理空间与高端空间 [khaddr1, khaddr4] 的映射;3.B程序段申请一段内存其物理空间为 [paddr5, paddr8],并将其映射到高端空间 [khaddr1, khaddr4],然后进行操作,且之后还需要继续操作;4.并不释放这段物理空间,对这段已分配的物理空间进行一些记录,但释放掉这段物理空间与高端空间 [khaddr1, khaddr4] 的映射;5.因为有些原因A程序段再次执行,通过上次的记录找到物理空间 [paddr1, paddr4];6.将物理空间 [paddr1, paddr4] 再次映射到 [khaddr1, khaddr4],然后进行操作;......
-
在64位架构下内核虚拟空间已经达到512GB,能轻易将全部物理空间映射到内核虚拟空间中。
内核空间中还是分静态区域和动态区域。
这与高端内存机制唯一区别就是,不用再通过复用虚拟地址的方式访问大于1G的物理空间。
因为虚拟空间很够用,动态区域映射到物理空间后,在完全使用完毕之前可以不必急于解除映射。
但要注意:
动态区域和静态区域不同,仍然是非线性映射的。具体原因是一般不会只需要一页内存,而是需要N页内存,物理空间中不一定会存在N页空闲内存, 所以就会像用户进程空间那样,分配到的内存在虚拟上是连续的,但物理上是不连续的, 物理上不连续,那就无法进行线性映射。
感觉说的有点乱,可能我的思路也还未足够清晰,先就这样吧。
6. 内核为何偏爱分配连续物理内存
其实我也思考了很久,总感觉内核很多地方是不要求物理内存连续的。
因为运行过程中,并不涉及pa和va的转换,也没有读写时候内存连续的需求。
经过一些调研后,暂时得出如下结论:
- 分配连续物理内存对于内核来说,大部分时候并不是必要的,主要是考虑性能。毕竟连续分配 + 线性映射,除了性能外,最容易想到的优势就是pa和va可以很简单地互相转换;
- 涉及到DMA的话,要求物理内存上也必须连续,可能还有些其他外设也有这要求;
- 内核的页不会被swapped出去,而是常驻内存的;
关于kmalloc()和vmalloc():
-
vmalloc()是像用户态那样,分配的内存虚拟上连续而物理上不连续,
但应该会分配后就立即映射;
-
kmalloc()不同于用户态,分配的内存虚拟上连续而物理上也连续,
当然分配后也会立即映射;
-
内核偏爱kmalloc(),推测有以下原因:
- kmalloc()分配的物理内存由于是连续的,所以映射速度非常快;
- vmalloc()分配的物理内存是非连续的,所以需要逐项映射,很慢;
- 物理上连续的内存,在cache方面应该可以减少冲突;
参考
linux-2.6.12源码
233333 Linux内存描述之高端内存–Linux内存管理(五)
What is the difference between vmalloc and kmalloc?