【INT的内核笔记】Linux内核内存空间布局研究

el/2023/12/3 2:11:53

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_NORMAL16MB~896MB
ZONE_HIGHMEM896MB ~ 结束(1G)

IA32架构将高端内存划分为3部分:

  • VMALLOC_START~VMALLOC_END
  • KMAP_BASE~FIXADDR_START
  • FIXADDR_START~4G
pic3

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?


http://www.ngui.cc/el/4104844.html

相关文章

【内核资料】stackoverflow上关于内核为何偏爱kmalloc(),而很少用vmalloc()的讨论

1. 问题 What is the difference between vmalloc and kmalloc? 2. 大致观点 涉及到DMA的话&#xff0c;需要物理上连续的内存&#xff1b;内核之所以偏好分配物理上连续内存&#xff0c;并不是必须的&#xff0c; 而主要是考虑性能&#xff1a; kmalloc()比vmalloc()效率高…

【INT的内核笔记】Linux信号递达过程详解

1. 什么时候执行信号递达 书上是叫递达的&#xff0c;但我个人比较喜欢叫信号处理吧&#xff0c;毕竟明明就是信号的接收进程处理信号的过程╮(╯▽╰)╭&#xff0c;所以文中可能会多处出现信号处理。 一般在从中断或系统调用返回到用户态前&#xff0c;会执行一段逻辑检查T…

【INT的内核笔记】tcp接收端相关实现

1. file_operations 在epoll_ctl(add)中有这样的调用链&#xff1a; 主要涉及了file->f_op和private_data中的socket结构 tfile->f_op->poll(tfile, &epq.pt)|->sock_poll|->sock->ops->poll|->tcp_poll(应该也是类似sk->sk_prot->recvmsg?…

关于char数组的输入与输出

cin一个char数组&#xff1a; 遇到空白符号&#xff08;空格&#xff0c;tab制表符&#xff0c;回车&#xff09;时读取停止&#xff0c;空白符号并不会被读入&#xff0c;但是碰到这些空白符号后&#xff0c;字符串末尾会放一个’\0‘ tip:空白字符的读入可以靠getchar() cout…

平衡三进制——Roj_1016

这道题目其实回顾的时候觉得还是挺容易的&#xff0c;但是做的时候还是忍不住看了题解。 这道题给我的主观层面的反思要远远大于客观上的&#xff0c;不要惧怕题目&#xff01;不要惧怕题目&#xff01;不要惧怕题目&#xff01;&#xff01;&#xff01;重要的事情说三遍 *注意…

平衡三进制Ⅱ——Roj_1018

前提 平衡三进制Ⅱ就是1016的反过来计算&#xff0c;本来可能想转换的方法可能是这道题目的难点&#xff0c;但是题目直接给出了转换的方法&#xff0c;所以这道题的难点就转移到了如何用代码实现它所提供的方法&#xff08;没有数学上的难度的 要点 1.最主要的问题&#xff1a…

字串排序——Roj_1020

这道题光看题目好像没有什么难度&#xff0c;但是操作起来发现了如下问题 1.输入这一串数组的方式&#xff0c;我刚开始采取的是多次get&#xff08;&#xff09;到‘\n’结束&#xff0c;后来发现可能还是一个一个读入比较方便&#xff0c;因为还要考虑数组中是否存在数字(cge…

i-1进制——Roj_1014

思考过程 这道题刚开始搞得时候&#xff0c;毫无头绪&#xff0c;因为一直把abi看成了一个整体&#xff0c;没有想着把a和b分开运算&#xff0c;然后思维上的难度就比较大。然后某大佬给了示范 &#xff08;刚开始我还没有想到用&1的办法搞成二进制&#xff0c;我刚开始是…

-1+i进制——Roj_1017

1017是1014反过来进行计算 但比较麻烦的点在于要考虑读入的各种情况&#xff1a; 1.是否有实部 2.是否有虚部 3.实部的正负 3.虚部的正负 4.虚部是否为/- 1 5.不是个位数的数字要进行进行运算 综合考虑各种情况&#xff0c;然后进行分类讨论 计算过程中可以拿aib/-1i找出统一的…

八进制小数——Roj_1005

难点&#xff1a; 1.这题最主要的问题就是big num&#xff0c;long long 无法存下这么大的数&#xff0c;因此要考虑将这个数放在一个数组里&#xff0c;将整数的运算转换成数组各个位置上的运算。 2.其次就是考虑将其放在结构体中&#xff0c;用operator重新定义各种运算后。 …