《深入理解Linux内核》-2.3. linux分段

译者注:

本节讲的是“linux分段”,上一节是“硬件分段”,硬件分段主要讲80x86如何从硬件层提供分段支持,而本节讲的linux如何使用分段来实现操作系统,当然基于硬件分段也可以实现windows操作系统,但是linux和windows使用相同的硬件基础设施来实现自己的系统的方式肯定是不一样的。所以有本节单独介绍linux分段的必要,读者有必要搞清楚二者的关系,否则在阅读本节的时候容易和上一节产生矛盾和迷惑。

Linux分段

早期的操作系统(包括硬件)不支持分段,内存的换进换出(swap)都是以整个进程的内存空间为单位,一方便非常耗时,另一方面内存利用率低下,尤其是当内存不足或者碎片较多的情况下,很容易导致内存交换失败。

后来有了分段技术,把内存空间分成多个模块:代码段、数据段,或者是一个大的数据块,段成了内存交换的单位,在一定程度上增加了内存利用率。那时候还没有分页技术,虚拟地址(线性地址)是直接映射到物理空间的。80x86系统上可以分页可以不分页,也就是说我们可以在其上设计一个不分页的操作系统,也就是基于分段的操作系统。

但是linux很少使用分段,分段和分页在某些方面是冗余的,因为他们都可以把物理地址空间分割成不同部分:分段给每个进程分配不同的逻辑地址空间,而分页可以把相同的逻辑地址空间映射到不同的物理地址上。因此,Linux优先采用了分页(分页操作系统),基于以下原因:

  • 内存管理更简单:所有进程使用相同段寄存器值,也就是相同的线性地址集。
  • 出于兼容大部分硬件架构的考虑,RISC架构对分段支持的不是很好。

(译者注:这里读者可能会疑惑为什么拿分页和分段做对比,因为分页是在分段的基础上的,没有分段就没有分页,从这上面看是没有可比性的。这里实际上是比较分段操作系统和分页操作系统的。)

Linux2.6仅仅在80x86架构必需的时候才使用分段。

所有运行在用户态的Linux进程使用一套段来定位指令和数据。这些段分别叫做用户代码段和用户数据段。类似的,所有内核态进程也使用另一套相同的段来定位指令和数据:内核代码段和内核数据段。表2-3展示了这几个段的描述符字段值。

表2-3. 四个主要linux段的描述符字段及其值

这里写图片描述

这些段相应的选择器分别由以下宏定义:_USER_CS, __USER_DS, __KERNEL_CS, 和__KERNEL_DS。举例来说,如果要定位内核代码段,内核只需要加载__KERNEK_CS宏的值到cs寄存器中。

扫描二维码关注公众号,回复: 1581818 查看本文章

这些段中的线性地址都以0位起始,到2^32-1。这意味着所有用户态和内核态进程,可以使用相同的逻辑地址。

所有段从0x00000000起始带来另外一个很重要的结果就是,逻辑地址等于线性地址(虚拟地址),也就是说,逻辑地址的的偏移量部分等于对应的线性地址。

之前提到过,CPU的“当前特权级别”指示了处理器处于用户态还是内核态,它对应cs段描述符中的RPL字段。每当RPL变化时,相应的段寄存器必须被同步更新。比如,CPL等于3(用户态)时,ds寄存器必须包含用户数据段的段选择器,而当CPL等于0时,ds寄存器必须包含内核数据段的段选择器。

对于ss寄存器,也是同样的情况。当CPL等于3时,它必须指向用户数据段中的用户态栈,当CPL等于0时,它必须指向内核数据段的内核态栈。当用户态切换到内核态时,Linux总是确保ss寄存器包含内核数据段的段选择器。

当需要保存一条指令或者一个数据结构的指针时,内核不需要保存逻辑地址的段选择器部分,因为ss寄存器包含当前的段选择器。举例来说,当内核调用一个函数,它执行call指令,并仅以该函数逻辑地址的偏移量部分作为参数;cs寄存器中值被默认的选作当前段的选择器。因为只有一个“运行在内核态”类型的段,即由__KERNEL_CS定义的代码段,所以当CPU切换到内核态时,只用把__KERNEL_CS加载到cs中即可。对于指向内核数据的指针(默认使用ds寄存器)和指向用户数据的指针(内核显式使用es寄存器),也是类似的情况。

除了上面提到的四个寄存器外,Linux还使用了一些其他特殊的寄存器。我们在下面的章节中讲述Linux GDT时会介绍。

Linux GDT

单处理器系统只有一个GDT,而在多处理器系统中每个CPU都有一个GDT。所有GDT都保存在cpu_gdt_table数组中,它们的地址和大小保存在cpu_gdt_descr数组中。如果你查看源码索引,可以看到这些符号定义在arch/i386/kernel/head.S中。这本书中的每个宏、函数以及其他符号都列出在源码索引中,所以你可以快速在源码中找到它们。

图2-6展示了GDT的布局。每个GDT包含18个段描述符和14个空的、未使用的或者保留字段。插入未使用的字段的目的是一起访问的段描述符通常保存在缓存相同的32字节行中(参看本章之后的“硬件缓存”一节)。

这18个段描述符指向以下段:

  • 用于用户态和内核态的代码段和数据段(参考前面章节)。
  • 任务状态段(TSS),每个处理器各不相同。TSS的线性地址空间是相应的内核数据段地址空间的一个子集。任务状态段顺序保存在init_tss数组中;特别的,第n个CPU的TSS描述符的Base字段指向init_tss数组的第n个元素。当Limit字段被设置为0xeb时,G(粒度)标志位被清除,因为TSS段的长度为236字节。Type字段设置为9或者11(32位TSS可用),DPL设置为0,因为用户态进程不允许访问TSS段。关于Linux怎样使用TSS更详细的内容,请参考第三章“状态段”一节。

图2-6. 全局描述符表

这里写图片描述

  • 局部描述符表,通常为所有处理器共享(参考下一节)。
  • 三个线程局部存储(TLS)段:它允许一个多线程应用程序使用最多三个包含线程局部数据的段。set_thread_area()get_thread_area()系统调用分别用来创建个释放一个进程的TLS段。
  • 跟高级电源管理(APM)相关的三个段:当Linux APM驱动调用BIOS函数时,BIOS利用这些段来设置APM设备的状态,它也可以使用自定义的代码和数据段。
  • 跟即插即用BIOS设备相关的五个段。跟前面一样,当Linux PnP驱动调用BIOS函数时,BIOS利用这些段来检测PnP设备使用的资源,他也可以使用自定义的代码段和数据段。
  • 一个特殊的TSS段,内核用来处理“双精度浮点错误”异常(参考第4章“异常”)。

之前提到,系统中每个处理器都有一个GDT拷贝。所有GDT拷贝存储大致相同的字段,除了一些极少的情况。第一,每个处理器拥有自己的TSS段,如此相应的GDT字段可能不一样。再者,GDT中的某些字段依赖于cpu正在运行的进程(LDT和TLS段描述符)。最后,某些情况下,处理器会临时修改自己GDT中的字段;比如当调用一个APM BIOS过程时。

2.3.2 Linux LDT

由于大部分Linux用户态程序不使用局部描述符表,所以内核定义一个默认的LDT供大部分进程共用。默认局部描述符表保存在default_ldt数组中。它有五个字段,但是内核只用了其中两个:iBCS可执行程序的调用门(call gate),Solaris/x86可执行程序的调用门(参考第20章“执行域”一节)。调用门是80x86微处理器提供的一种机制,用来在调用一个预定义函数时改变CPU的特权级别;因为我们不打算更深入的探讨它们,感兴趣的同学可以通过Intel文档获得更多的细节。

然而,在某些情况下,进程可能需要设置它们自己的LDT。这在运行面向分段的windows程序的应用(比如wine)里面非常有用。modify_ldt()系统调用可以实现这一功能。

modift_ldt()创建的任何自定义LDT还需要自己的段。当处理器开始执行一个拥有自定义LDT的进程时,CPU特有的GDT的LDT字段被相应的修改。

用户态应用也可以通过modify_ldt()来分配一个新的段;但是内核永远不会使用这些段,而且不需要记录对应的段描述符,因为他们包含在进程自定义的LDT里面。

猜你喜欢

转载自blog.csdn.net/ybxuwei/article/details/80657461
今日推荐