linux内存地址管理(一)
2012-09-07 08:29
155 查看
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 让所有的进程(或叫任务)都使用相
ac28
同的逻辑地址空间,因此就没有必要使用局部描述符表 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)特征描述符,对此不进行详细讨论。
相关文章推荐
- linux 内存地址空间管理 mm_struct
- Linux内存地址管理概述
- linux 内存地址空间管理 mm_struct
- Linux用户、群组管理
- linux系统之软件包管理
- Linux(七)编写shell管理脚本
- Linux权限管理及用户与用户组
- 使用 udev 高效、动态地管理 Linux 设备文件(转载)--1
- Linux之磁盘管理
- Linux帐号管理[中]--用户管理
- Linux命令关于用户权限管理
- Linux 用户(user)和用户组(group)管理概述
- Linux下的ssh实验环境搭建与管理
- Linux用户管理-中
- linux中的进程管理命令
- 导航 新随笔 管理 linux 多线程编程 --比较老 是 LinuxThread
- ELF文件的加载过程(load_elf_binary函数详解)--Linux进程的管理与调度(十三)
- shell学习五十七天----linux任务管理,针对上一讲的总结和扩展
- linux用户和组帐户管理
- 第六章 Linux文件与目录管理