分类: LINUX
2008-10-15 09:55:09
80x86 微处理器中的分段鼓励程序员把他们的程序化分成逻辑上相关的实体,例如子程序或者全局与局部数据区。然而,Linux 以非常有限的方式使用分段。实际上,分段和分页在某种程度上有点多余,因为它们都可以划分进程的物理地址空间:分段可以给每一个进程分配不同的线性地址空间,而分页可以把同一线性地址空间映射到不同的物理空间。与分段相比,Linux 更喜欢使用分页方式,因为:
• 当所有进程使用相同的段寄存器值时,内存管理变得更简单,也就是说它们能共享
同样的一组线性地址。
• Linux设计目标之一是可以把它移植到绝大多数流行的处理器平台上。然而, RISC
体系结构对分段的支持很有限。
2.6 版的Linux 只有在80x86 结构下才需要使用分段。
运行在用户态的所有Linux 进程都使用一对相同的段来对指令和数据寻址。这两个段就是所谓的用户代码段和用户数据段。类似地,运行在内核态的所有Linux 进程都使用一对相同的段对指令和数据寻址:它们分别叫做内核代码段和内核数据段。表2-3 显示了这四个重要段的段描述符字段的值。
表2-3:四个主要的Linux 段的段描述符字段的值
段Base G Limit S Type DPL D/B P
用户代码段0x00000000 1 0xfffff 1 10 3 1 1
用户数据段0x00000000 1 0xfffff 1 2 3 1 1
内核代码段0x00000000 1 0xfffff 1 10 0 1 1
内核数据段0x00000000 1 0xfffff 1 2 0 1 1
相应的段选择符由宏__USER_CS,__USER_DS,__KERNEL_CS,和__KERNEL_DS分别义。例如,为了对内核代码段寻址,内核只需要把__KERNEL_CS宏产生的值装进cs段寄存器即可。
注意,与段相关的线性地址从0 开始,达到232-1的寻址限长。这就意味着在用户态或内核态下的所有进程可以使用相同的逻辑地址。
所有段都从0x00000000开始,这可以得出另一个重要结论,那就是在Linux 下逻辑地址与线性地址是一致的,即逻辑地址的偏移量字段的值与相应的线性地址的值总是一致的。
如前所述,CPU 的当前特权级(CPL)反映了进程是在用户态还是内核态,并由存放在cs 寄存器中的段选择符的RPL 字段指定。只要当前特权级被改变,一些段寄存器必须相应地更新。例如,当CPL=3 时(用户态),ds寄存器必须含有用户数据段的段选择符,而当CPL=0 时,ds 寄存器必须含有内核数据段的段选择符。
类似的情况也出现在ss 寄存器中。当CPL 为3 时,它必须指向一个用户数据段中的用户栈,而当CPL 为0 时,它必须指向内核数据段中的一个内核栈。当从用户态切换到内核态时,Linux 总是确保ss 寄存器装有内核数据段的段选择符。
当对指向指令或者数据结构的指针进行保存时,内核根本不需要为其设置逻辑地址的段选择符,因为cs 寄存器就含有当前的段选择符。例如,当内核调用一个函数时,它执行一条call 汇编语言指令,该指令仅指定其逻辑地址的偏移量部分,而段选择符不用设置,它已经隐含在cs 寄存器中了。因为 “在内核态执行” 的段只有一种,叫做代码段,由宏__KERNEL_CS 定义,所以只要当CPU 切换到内核态时将__KERNEL_CS 装载进cs 就足够了。同样的道理也适用于指向内核数据结构的指针(隐含地使用ds 寄存器)以及指向用户数据结构的指针(内核显式地使用es 寄存器)。
除了刚才描述的4 个段以外,Linux 还使用了其他几个专门的段。我们将在下一节讲述Linux GDT 的时候介绍它们。
*Linux GDT
在单处理器系统中只有一个GDT,而在多处理器系统中每个CPU 对应一个GDT。所有的GDT都存放在cpu_gdt_table 数组中,而所有GDT 的地址和它们的大小(当初始化gdtr寄存器时使用)被存放在cpu_gdt_descr 数组中。如果你到源代码索引中查看,可以看到这些符号都在文件arch/i386/kernel/head.S 中被定义。本书中的每一个宏、函数和其他符号都被列在源代码索引中,所以能在源代码中很方便地找到它们。
图2-6 是GDT的布局示意图。每个GDT 包含18 个段描述符和14 个空的,未使用的,或保留的项。插入未使用的项的目的是为了使经常一起访问的描述符能够处于同一个32字节的硬件高速缓存行中(参见本章后面“硬件高速缓存”一节)。
每一个GDT 中包含的18 个段描述符指向下列的段:
• 用户态和内核态下的代码段和数据段共4 个(参见前面一节)。
• 任务状态段(TSS),每个处理器有1 个。每个TSS 相应的线性地址空间都是内核数据段相应线性地址空间的一个小子集。所有的任务状态段都顺序地存放在init_tss 数组中;值得特别说明的是,第n 个CPU 的TSS 描述符的Base 字段指向init_tss数组的第n 个元素。G(粒度)标志被清0,而Limit 字段置为0xeb,因为TSS 段是236 字节长。Type 字段置为9 或11(可用的32 位TSS),且DPL 置为0,因为不允许用户态下的进程访问TSS 段。在第三章“任务状态段”一节你可以找到Linux 是如何使用TSS 的细节。
1 个包括缺省局部描述符表的段,这个段通常是被所有进程共享的段(参见下一节)。
• 3 个局部线程存储(Thread-Local Storage,TLS)段:这种机制允许多线程应用程序使用最多3个局部于线程的数据段。系统调用set_thread_area()和get_thread_area()分别为正在执行的进程创建和撤消一个TLS 段。
• 与高级电源管理(AMP)相关的3 个段:由于BIOS代码使用段,所以当Linux APM驱动程序调用BIOS 函数来获取或者设置APM 设备的状态时,就可以使用自定义的代码段和数据段。
• 与支持即插即用(PnP)功能的BIOS服务程序相关的5 个段:在前一种情况下,就像前述与AMP 相关的3 个段的情况一样,由于BIOS 例程使用段,所以当Linux 的PnP 设备驱动程序调用BIOS 函数来检测PnP 设备使用的资源时,就可以使用自定义的代码段和数据段。
• 被内核用来处理“双重错误”(译注1)异常的特殊TSS 段(参见第四章的“异常”
一节)。
如前所述,系统中每个处理器都有一个GDT 副本。除少数几种情况以外,所有GDT 的副本都存放相同的表项。首先,每个处理器都有它自己的TSS 段,因此其对应的GDT项不同。其次,GDT 中只有少数项可能依赖于CPU 正在执行的进程(LDT 和TLS 段描述符)。最后,在某些情况下,处理器可能临时修改GDT 副本里的某个项;例如,当调用APM 的BIOS 例程时就会发生这种情况。
*Linux LDT
大多数用户态下的Linux 程序不使用局部描述符表,这样内核就定义了一个缺省的LDT供大多数进程共享。缺省的局部描述符表存放在default_ldt数组中。它包含5 个项,但内核仅仅有效地使用了其中的两个项:用于iBCS 执行文件的调用门和Solaris/x86 可执行文件的调用门(参见第二十章的“执行域”一节)。调用门是80x86 微处理器提供的一种机制,用于在调用预定义函数时改变CPU的特权级,由于我们不会再更深入地讨论它们,所以请参考Intel 文档以获取更多详情。
在某些情况下,进程仍然需要创建自己的局部描述符表。这对有些应用程序很有用,像Wine 那样的程序,它们执行面向段的微软Windows 应用程序。modify_ldt()系统调用允许进程创建自己的局部描述符表。
任何被modify_ldt()创建的自定义局部描述符表仍然需要它自己的段。当处理器开始执行拥有自定义局部描述符表的进程时,该 CPU 的GDT 副本中的LDT 表项相应地就被修改了。
用户态下的程序同样也利用modify_ldt()来分配新的段,但内核却从不使用这些段,它也不需要了解相应的段描述符,因为这些段描述符被包含在进程自定义的局部描述符表中了。