linux内存地址管理(一)

linux内存地址管理(一)

本文主要介绍的linux中的内存管理原理和相关的管理机制(以80386为例),纯属娱乐,如有错漏,欢迎指正。

1.内存地址

当程序执行到这样一条指令时:

MOVE REG, ADDR

它的作用是将地址为ADDR(假设为10000)的内存地址中的内容复制到寄存器REG中,ADDR传到CPU后,CPU(如果有MMU会家上MMU)经过一系列的运算,得到物理地址,然后从物理地址取出对应的内容。

1.1.8086的实模式寻址

在8086的实模式下,会把某一段寄存器左移4位,然后与地址 ADDR 相加后被直接送到内存总线上,这个相加后的地址就是内存单元的物理地址,而程序中的这个地址就叫逻辑地址(或叫虚地址)。

1.2.8086的保护模式寻址

在 80386 的保护模式下,这个逻辑地址不是被直接送到内存总线,而是被送到内存管理单元(MMU)。MMU 由一个或一组芯片组成,其功能是把逻辑地址映射为物理地址,即进行地址转换,如下图所示。

(图一)

MMU是一种硬件电路,它包含两个部件,一个是分段部件,一个是分页部件。分段机制把一个逻辑地址转换为线性地址,分页地址将线性地址转换为物理地址,如下图所示。

(图二)

2.段机制和描述符


2.1.段机制

在 80386 的段机制中,逻辑地址由两部分组成,即段部分(选择符)及偏移部分。一个段有如下属性:

(1)段的基地址(Base Address):在线性地址空间中段的起始地址。
(2)段的界限(Limit):表示在逻辑地址中,段内可以使用的最大偏移量。
(3)段的属性(Attribute): 表示段的特性。例如,该段是否可被读出或写入,或者该段是否作为一个程序来执行,以及段的特权级等。

段的这些属性储存在段的描述符表中,在逻辑—线性地址转换过程中要对描述符进行访问。

2.2.描述符

所谓描述符(Descriptor),就是描述段的属性的一个 8 字节存储单元。在实模式下,段的属性不外乎是代码段、堆栈段、数据段、段的起始地址、段的长度等,而在保护模式下则复杂一些。80386 将它们结合在一起用一个 8 字节的数表示,称为描述符。如下图所示:

(图三)

一个段描述符指出了段的 32 位基地址和 20 位段界限(即段长,1M)。 
第 6 个字节的 G 位是粒度位,当 G=0 时,段长表示段格式的字节长度,即一个段最长可达 1M 字节。当 G=1 时,段长表示段的以 4K 字节为一页的页的数目,即一个段最长可达1M×4K=4G 字节。D 位表示缺省操作数的大小,如果 D=0,操作数为 16 位,如果 D=1,操作数为 32 位。第 6 个字节的其余两位为 0,这是为了与将来的处理器兼容而必须设置为 0 的位。

第 5 个字节是存取权字节,它的一般格式下图所示:

(图4)

第 7 位 P 位(Present) 是存在位,表示段描述符描述的这个段是否在内存中,如果在内存中。P=1;如果不在内存中,P=0。

DPL(Descriptor Priv ilege Level),就是描述符特权级,它占两位,其值为 0~3,用来确定这个段的特权级即保护等级。

S 位(System)表示这个段是系统段还是用户段。如果 S=0,则为系统段,如果 S=1,则为用户程序的代码段、数据段或堆栈段。系统段与用户段有很大的不同,后面会具体介绍。 类型占 3 位,第 3 位为 E 位,表示段是否可执行。当 E=0 时,为数据段描述符,这时的第 2 位 ED 表示扩展方向。当 ED=0 时,为向地址增大的方向扩展,这时存取数据段中的数据的偏移量必须小于或等于段界限,当 ED=1 时,表示向地址减少的方向扩展,这时偏移量必须大于界限。当表示数据段时,第 1 位(W)是可写位,当 W=0 时,数据段不能写,W=1 时,数据段可写入。在 80386 中,堆栈段也被看成数据段,因为它本质上就是特殊的数据段。当描述堆栈段时,ED=0,W=1,即堆栈段朝地址增大的方向扩展。 也就是说,当段为数据段时,存取权字节的格式如下图 所示:

(图5)

当段为代码段时,第 3 位 E=1,这时第 2 位为一致位(C)。当 C=1 时,如果当前特权级低于描述符特权级,并且当前特权级保持不变,那么代码段只能执行。所谓当前特权级(Current Privilege Level),就是当前正在执行的任务的特权级。第 1 位为可读位 R,当R=0 时,代码段不能读,当 R=1 时可读。也就是说,当段为代码段时,存取权字节的格式下图所示:

(图6)

存取权字节的第 0 位 A 位是访问位,用于请求分段不分页的系统中,每当该段被访问时,将 A 置 1。对于分页系统,则 A 被忽略未用。


系统描述符的第5、第6直接和用户描述符略有不同,系统段描述符的第 5 个字节的第 4 位为 0,说明它是系统段描述符,类型占4 位,没有 A 位。第 6 个字节的第 6 位为 0,说明系统段的长度是字节粒度,所以,一个系统段的最大长度为 1M 字节。如下图所示:

(图7)

2.3.描述符表

各样的描述符,都放在对应的全局描述符表、局部描述符表和中断描述符表中。

描述符表(即段表)定义了 386 系统的所有段的情况。所有的描述符表本身都占据一个字节为 8 的倍数的存储器空间,空间大小在 8 个字节(至少含一个描述符)到 64K 字节(至多含 8K)个描述符之间。


2.4.选择符与描述符表寄存器

在实模式下,段寄存器存储的是真实的段地址,在保护模式下,16 位的段寄存器无法放下 32 位的段地址,因此,它们被称为选择符,即段寄存器的作用是用来选择描述符。选择符的结构如下图所示:

(图8)

可以看出,选择符有 3 个域:第 15~3 位这 13 位是索引域,表示的数据为 0~8129,用于指向全局描述符表中相应的描述符。第 2 位为选择域,如果 TI=1,就从局部描述符表中选择相应的描述符,如果 TI=0,就从全局描述符表中选择描述符。第 1、0 位是特权级,表示选择符的特权级,被称为请求者特权级 RPL(Requestor Privilege Level)。只有请求者特权级 RPL 高于(数字低于)或等于相应的描述符特权级 DPL,描述符才能被存取,这就可以实现一定程度的保护。

我们知道,实模式下是直接在段寄存器中放置段基地址,现在则是通过它来存取相应的描述符来获得段基地址和其他信息,这样以来,存取速度会不会变慢呢?为了解决这个问题,386 的每一个段选择符都有一个程序员不可见(也就是说程序员不能直接操纵)的 88 位宽的段描述符高速缓冲寄存器与之对应。无论什么时候改变了段寄存器的内容,只要特权级合理,描述符表中的相应的 8 字节描述符就会自动从描述符表中取出来,装入高速缓冲寄存器中(还有 24 位其他内容)。一旦装入,以后对那个段的访问就都使用高速缓冲寄存器的描述符信息,而不会再重新从表中去取,这就大大加快了执行的时间,如下图 所示:


下面讲一下在没有分页操作时,寻址一个存储器操作数的步骤。
(1)在段选择符中装入 16 位数,同时给出 32 位地址偏移量(比如在 ESI、EDI 中等)。
(2)根据段选择符中的索引值、TI 及 RPL 值,再根据相应描述符表寄存器中的段地址和段界限,进行一系列合法性检查(如特权级检查、界限检查),该段无问题,就取出相应的描述符放入段描述符高速缓冲寄存器中。

(4)将描述符中的 32 位段基地址和放在 ESI、EDI 等中的 32 位有效地址相加,就形成了 32 位物理地址。

寻址过程如下图所示:


2.5.LINUX中的段

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

从 2.2 版开始,Linux 让所有的进程(或叫任务)都使用相同的逻辑地址空间,因此就没有必要使用局部描述符表 LDT。

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

#define __KERNEL_CS0x10   /*内核代码段,index=2,TI=0,RPL=0*/ 
#define __KERNEL_DS0x18   /*内核数据段, 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 表中的下标。内核代码段和数据段具有最高特权,因此其 RPL为 0,而用户代码段和数据段具有最低特权,因此其 RPL 为 3。可以看出,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 的。第二项也没用。从下标 2~5 共 4 项对应于前面的 4 种段描述符值。对照图 2.10,从描述符的数值可以得出: 
•  段的基地址全部为 0x00000000; 
•  段的上限全部为 0xFFFFFF; 
•  段的粒度 G 为 1,即段长单位为 4KB; 
•  段的 D 位为 1,即对这 4 个段的访问都为 32 位指令; 
•  段的 P 位为 1,即 4 个段都在内存。

由此可以得出,每个段的逻辑地址空间范围为 0~4GB。因为每个段的基地址为 0,因此,逻辑地址到线性地址映射保持不变,也就是说,偏移量就是线性地址,我们以后所提到的逻辑地址(或虚拟地址)和线性地址指的也就是同一地址。

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值