Linux线性地址空间的划分及内核寻址方式

来源:互联网 发布:如何做好淘宝客服 编辑:程序博客网 时间:2024/06/05 18:58

        今天研究Linux1.2内核时,注意到该版本中的PAGE_OFFSET宏被定义为0,考虑到进程的地址空间被划分为3G的用户态地址空间和1G的内核态地址空间,于是深入的研究了一下这个问题。

        一开始我只是疑惑,当前版本的内核的PAGE_OFFSET宏被定义为0xc0000000(3G),而且有一系列的宏用于在内核空间可以进行虚拟地址和物理地址的相互转换。特别是那个经典的例子--当内核要进行进程切换的时候,要加载新进程的页表,将其加载到CR3寄存器中,但CR3寄存器要求加载页表的物理地址,所以内核用x-PAGE_OFFSET计算出页表的的物理地址(x是其虚拟地址,这里还有一个疑惑:这个虚拟地址是怎么来的?内核申请内存,返回的不是其物理地址吗?),然后加载到CR3寄存器中。但是,1.2版本中的PAGE_OFFSET被定义为了0,而且内核中大部分的代码和现在的内核本质上没有什么区别,也就是说,1.2版本的内核其实也是划分了1G的内核空间和3G的用户进程地址空间的,而且和现代内核版本一样,也是将高1G的虚拟地址空间分配给了内核,3G的虚拟地址空间分配给了用户态进程地址空间。但是,为什么PAGE_OFFSET被定义为了0呢?如果这样的话,对于那个经典的加载CR3的例子,该怎么解释呢?1.2版本内核中加载CR3寄存器是直接将地址加载进去的,没有经过虚拟地址到物理地址的转换!那么,问题就来了--内核虚拟地址空间从3G开始,而且进程都是运行在虚拟地址空间中的,但是内核映射物理地址却是从物理地址0开始的,那么1.2版本的内核为什么可以直接不转换虚拟地址就将其当做物理地址使用呢?

        在网上查资料的时候发现这样一个帖子:http://bbs.csdn.net/topics/100049760。讨论的问题是“当进程进入内核态时,如何寻址”,楼主认为当进入内核态时,内核就不再使用MMU单了,而是直接使用物理寻址的方式。这个观点对我思考这个问题曾有过一定的启发,但是这个观点从根本上就是错的,在x86上(别的不敢说),进程、程序都是直接运行在虚拟地址空间中的--无论是内核态还是用户态,本文主要就是讨论这个问题的。

        首先回答为什么Linux1.2内核中的PAGE_OFFSET定义为0,但是其仍然可以将高1G的虚拟地址空间分配给内核。注意,PAGE_OFFSET为0,代表着编译内核时,编译程序会将内核代码的地址从0开始“偏移”,而现代linux的PAGE_OFFSET为3G,所以编译内核时,编译程序将内核代码从3G开始“偏移”,从而导致内核代码中对指令和数据地址的引用都会落入其虚拟地址空间(从3G处开始)。

        1.2内核就会出现这样一个问题--内核代码地址从0开始,但是每个进程都共享内核代码和数据,即进程的页表映射中,低3G的虚拟地址映射的是进程的用户空间,而3G到4G才映射内核地址空间。对于用户态进程不会出现问题,因为其虚拟地址空间从0开始,编译器编译用户程序也将其分配在用户虚拟地址空间,当运行程序时,进程也会运行在其虚拟地址空间,比如用户进程要执行地址0(虚拟地址)处的指令,那么进程将虚拟地址0传递给MMU单元(这里忽略了分段),MMU会根据这个0在也表中查找到真正的物理地址。但是,如果进程进入内核态后呢?由于内核编译也是从0开始偏移的,而且PAGE_OFFSET也为0,内核中也没有对地址的引用做任何的处理,那么比如内核执行其开始的一条指令,那么,问题就来了,这条指令的虚拟地址是0啊,把0传递给MMU????这样地址转换不会访问到3G处的页表项啊!(这里忽略了分段,这是关键)

        其实这个问题涉及到x86的分段问题。我们知道,x86 CPU的寻址方式无论如何都避不开段式寻址,虽然Linux有意避开不用,但是还是从根本上受到其分段的影响的(个人观点)。开门见山的说吧,在linux\arch\i386\kernel/head.S文件中可以看到内核定义了四个重要的全局段选择描述符: 

        还可以在linux/include/asm-i386/segment.h中看到以下定义:


        可以看出内核定义了内核数据段描述符、内核代码段描述符、用户数据段描述符、用户代码段描述符。当进程进入内核态时,将加载内核代码段和内核数据段进入相应的寄存器中,从而可以执行内核代码和访问内核数据。以上的段描述符都是出自1.2内核,从中可以看出,1.2内核将内核代码和数据段都定义为长度为1G,从0xC0000000(3G)开始的段,而将用户代码和数据段都定义为长度为3G,从0x00000000开始的段(内核段和用户段不仅长度和起始位置不一样,而且相应的保护权限也不一样)。看到这里就明白了,原来1.2内核将内核数据段和代码段直接定义在了0xC0000000(3G)处,从而达到在内核中执行代码和访问内核数据时,直接引用从0(PAGE_OFFSET)开始偏移的地址,会将这个偏移加上到内核代码或数据段的起始地址--3G!!从而将奇怪的内核地址转换为了从3G开始的内核虚拟地址空间中的地址,然后再经过分页单元的处理,最终映射到实际的物理地址处。

        而现代linux内核,将内核代码和数据段的长度定义为了4G,起始位置为0,所以可以将PAGE_OFFSET定义为3G,而且编译内核形成的内核代码地址也是从3G处开始的。其实,以上的所有问题,都是因为对x86的寻址方式没有深刻的理解,忽略了分段,如果真正的理解了其寻址方式和分段机制对内核的影响,那么这一切是很容易理解的,所以我说,虽然Linux极力想避开x86的分段机制,但是从根本上还是会受到其影响。

        最后,还要讨论一个问题,那就是,为什么要分内核1G的虚拟地址空间,用户进程3G的虚拟地址空间。Linux为什么会形成这种线性地址空间?为什么不给内核和用户进程各自4G的线性地址空间?在网上看到有人说这只是针对x86体系结构,对于其他的体系结构,有的可以做到内核和用户进程各自4G的地址空间。

        其实,个人认为,这还是分段的问题。研读《深入linux内核》第二章--存储器寻址,深有感悟。系统中的所有进程通过使用相同的段寄存器值(就是内核/用户代码/数据段)而共享同样的一组线性地址空间的(0-4G),而分页的意义在于,可以把同一线性地址空间映射到不同的物理空间。

        那么,为什么内核地址空间要和用户地址空间要“平分”这4G的线性地址空间呢?我觉得有以下几方面的原因:

        1.权限问题。内核态的权限和用户态的权限是不一样的,所以它们不能放在同一个段里,如果它们在同一个段里,那么它们的权限就一样了。

        2.虚拟地址空间针对特定的一个进程也是一种“稀有资源”,如果全部分配给了内核或者用户进程,那么另一方将无法工作。

        3.为什么没有各自给内核和用户进程4G的虚拟地址空间?因为内核代码也是用户进程的一部分,只不过内核由所有进程所共享。另一方面,内核和用户态进程需要进行数据的访问,比如在内核态访问用户态的数据等,而对于一个特定的进程,只有一个页表存在,访问指令代码和数据都要经过这个页表,如果分配给内核和用户态进程各自4G的地址空间,那么,该如何访问这一个页表呢?需要注意的是,内核地址空间中存放的是内核数据和代码,而用户态地址空间中存放的是用户代码和数据。

        4.虽然现代linux内核将内核代码和数据段的长度定义为4G,但编译内核仍然是将其偏移到3G处开始的,所以内核代码和数据应该还是只存在于高1G的虚拟地址空间中。

       5.Linux将内存分为内核程序、高速缓冲、虚拟盘和主内存四个部分。由此可以得出:内核程序是被单独划出的。原因如下:内核代码在操作系统运行时会经常被调用,因此、需要常驻内存。所以,将内核代码与一般进程所使用的空间区分开,为他专门化出一块内存区域;将内核代码全部载入并保护起来。

        想到的就这么多了,如果有兴趣可以看一看《深入理解linux内核》中有关存储器寻址的内容。

        补充:看《Linux内核源代码情景分析》中的相关内容,感觉可能上面所说的一些问题可能不对,Linux确实可能真的避开了intel的分段机制,而且使其分段机制毫无意义。而且现代内核的内核和用户代码和数据段都是长度为4G,从0开始的,所以真正起作用的将是其分页机制。有兴趣的可以参看《Linux内核源代码情景分析》中的相关内容。

0 0
原创粉丝点击