现在的位置: 首页 > 综合 > 正文

内核源码学习:段机制和描述符(三)

2018年01月24日 ⁄ 综合 ⁄ 共 3290字 ⁄ 字号 评论关闭

6 描述符投影寄存器

为了避免在每次存储器访问时,都要访问描述符表,读出描述符并对段进行译码以得到描述符本身的各种信息,每个段寄存器都有与之相联系的描述符投影寄存器。在这些寄存器中,容纳有由段寄存器中的选择符确定的段的描述符信息。段寄存器对编程人员是可见的,而与之相联系的容纳描述符的寄存器,则对编程人员是不可见的,故称之为投影寄存器。图2.19中所示的是六个寄存器及其投影寄存器。用实线画出的寄存器是段寄存器,用以表示这些寄存器对编程人员可见;用虚线画出的寄存器是投影寄存器,表示对编程人员不可见。

2.19 描述符投影寄存器

投影寄存器容纳有相应段寄存器寻址的段的基地址、界限及属性。每当用选择符装入段寄存器时,CPU硬件便自动地把描述符的全部内容装入对应的投影寄存器。因此,在多次访问同一段时,就可以用投影寄存器中的基地址来访问存储器。投影寄存器存储在80386的芯片上,因而可以由段基址硬件进行快速访问。因为多数指令访问的数据是在其选择符已经装入到段寄存器之后进行的,所以使用投影寄存器可以得到很好的执行性能。

7 Linux中的段

Intel微处理器的段机制是从8086开始提出的, 那时引入的段机制解决了从CPU内部16位地址到20位实地址的转换。为了保持这种兼容性,386仍然使用段机制,但比以前复杂得多。因此,Linux内核的设计并没有全部采用Intel所提供的段方案,仅仅有限度地使用了一下分段机制。这不仅简化了Linux内核的设计,而且为把Linux移植到其他平台创造了条件,因为很多RISC处理器并不支持段机制。但是,对段机制相关知识的了解是进入Linux内核的必经之路。

2.2版开始,Linux让所有的进程(或叫任务)都使用相同的逻辑地址空间,因此就没有必要使用局部描述符表LDT。但内核中也用到LDT,那只是在VM86模式中运行Wine,因为就是说在Linux上模拟运行Winodws软件或DOS软件的程序时才使用。

Linux在启动的过程中设置了段寄存器的值和全局描述符表GDT的内容,段的定义在include/asm-i386/segment.h中:

#define __KERNEL_CS     0x10     /*内核代码段, index=2,TI=0,RPL=0*/

#define __KERNEL_DS     0x18      /*内核数据段, index=3,TI=0,RPL=0*/

        #define __USER_CS         0x23      /*用户代码段, index=4,TI=0,RPL=3*/

#define __USER_DS         0x2B      /*用户数据段, index=5,TI=0,RPL=3*/

从定义看出,没有定义堆栈段,实际上,Linux内核不区分数据段和堆栈段,这也体现了Linux内核尽量减少段的使用。因为没有使用LDT,因此,TI=0,并把这4个段都放在GDT, index就是某个段在GDT表中的下标。内核代码段和数据段具有最高特权,因此其RPL0,而用户代码段和数据段具有最低特权,因此其RPL3。可以看出,Linux内核再次简化了特权级的使用,使用了两个特权级而不是4个。


全局描述符表的定义在arch/i386/kernel/head.S中:

ENTRY(gdt_table)

        .quad 0x0000000000000000        /* NULL descriptor */

        .quad 0x0000000000000000        /* not used */

               .quad 0x00cf9a000000ffff       /* 0x10 kernel 4GB code at 0x00000000 */

        .quad 0x00cf92000000ffff      /* 0x18 kernel 4GB data at 0x00000000 */

        .quad 0x00cffa000000ffff                /* 0x23 user   4GB code at 0x00000000 */

        .quad 0x00cff2000000ffff       /* 0x2b user   4GB data at 0x00000000 */

        .quad 0x0000000000000000        /* not used */

      .quad 0x0000000000000000        /* not used */

        /*

         * The APM segments have byte granularity and their bases

         and limits are set at run time.

          */

        .quad 0x0040920000000000        /* 0x40 APM set up for bad BIOS's */

        .quad 0x00409a0000000000        /* 0x48 APM CS    code */

        .quad 0x00009a0000000000        /* 0x50 APM CS 16 code (16 bit) */

        .quad 0x0040920000000000        /* 0x58 APM DS    data */

        .fill NR_CPUS*4,8,0             /* space for TSS's and LDT's */

从代码可以看出,GDT放在数组变量gdt_table中。按Intel规定,GDT中的第一项为空,这是为了防止加电后段寄存器未经初始化就进入保护模式而使用GDT的。第二项也没用。从下标254项对应于前面的4种段描述符值。对照图2.10,从描述符的数值可以得出:

·      段的基地址全部为0x00000000

·      段的上限全部为0xffff

·      段的粒度G1,即段长单位为4KB

·      段的D位为1,即对这四个段的访问都为32位指令

·      段的P位为1,即四个段都在内存。

由此可以得出,每个段的逻辑地址空间范围为04GB。读者可能对此不太理解,但只要对照图2.9就可以发现,这种设置既简单又巧妙。因为每个段的基地址为0,因此,逻辑地址到线性地址映射保持不变,也就是说,偏移量就是线性地址,我们以后所提到的逻辑地址(或虚拟地址)和线性地址指的也就是同一地址。看来,Linux巧妙地把段机制给绕过去了,而完全利用了分页机制。

从逻辑上说,Linux巧妙地绕过了逻辑地址到线性地址的映射,但实质上还得应付Intel所提供的段机制。只不过,Linux把段机制变得相当简单,它只把段分为两种:用户态(RPL3)的段和内核态(RPL=0)的段,因此,描述符投影寄存器的内容很少发生变化,只在进程从用户态切换到内核态或者反之时才发生变化。另外,用户段和内核段的区别也仅仅在其RPL不同,因此内核根本无需访问描述符投影寄存器,当然也无需访问GDT,而仅从段寄存器的最低两位就可以获取RPL的信息。Linux这样设计所带来的好处是显而易见的,Intel的分段部件对Linux性能造成的影响可以忽略不计。

在上面描述的GDT表中,紧接着那四个段描述的两个描述符被保留,然后是四个高级电源管理(APM)特征描述符,对此不进行详细讨论。

Intel的规定,每个进程有一个任务状态段(TSS)和局部描述符表LDT,但Linux也没有完全遵循Intel的设计思路。如前所述,Linux的进程没有使用LDT,而对TSS的使用也非常有限,每个CPU仅使用一个TSS

通过上面的介绍可以看出,Intel的设计可谓周全细致,但Linux的设计者并没有完全陷入这种沼泽,而是选择了简洁而有效的途径,以完成所需功能并达到较好的性能为目

抱歉!评论已关闭.