Linux内存地址的分段、分页机制(上)

时间:2022-01-14 15:47:53
在深入学习Linux内核源代码之前,需要先对Linux运行的硬件基础有个大概的认识,主要包括CPU中的寄存器和磁盘。
1.i386寄存器和系统指令

在Linux系统中使用的主要包括i386寄存器中的16位标志寄存器,4个内存管理寄存器和4个控制寄存器及调试寄存器。
(1) 标志寄存器
8086CPU中一种特殊的寄存器,用来存储CPU的状态或者指令执行后的结果,控制CPU的工作方式。
(2) 内存管理寄存器
处理器提供了4个内存管理寄存器(GDTR, IDTR, LDTR and TR),用于指定内存分段管理所用系统表的基地址,处理器为致谢寄存器的加载和保护提供了特定指令。
(3) 控制寄存器 
控制寄存器(CR0~CR3)用于控制和确定处理器的操作模式以及党庆执行任务的特性。
(4) 调试寄存器
Intel 80386以上的CPU提供了调试寄存器,用来调试软件。386和486包括留个32位寄存器:Dr0~Dr3, Dr6, Dr7 .

2.总线 Bus

(1) 数据总线:是计算机中各个组成部件间进行数据传输时的公共通道;“内数据总线宽度”是指CPU芯片内部数据传送的宽度;“外数据总线宽度”是指CPU与外部交换数据时的数据宽度。显然,数据总线位数越多,数据交换的速度就越快。
(2) 地址总线:是載对存储器或I/O端口进行访问时,传送由CPU提供的要访问的存储单元或I/O端口的地址信息的总线,其宽度决定了处理器能直接访问的主存容量的大小。

3.三级存储器组织结构

现在的微型计算机系统采用下图的三级存储器组织结构,即缓冲存储器Cache、主存、和外存。高速缓冲存储器Cache的使用,大大减少了CPU读取指令和操作数所需的时间,使CPU的执行速度显著提高。

在80X86CPU的发展过程中,存储器的管理机制发生了较大的变化。8086/8088CPU对存储器的管理采用分段的实方式;80286CPU除了可在实方式下工作,还可以在保护模式下工作;而80386CPU之后的处理器则具有三种工作方式:实方式、保护方式和虚拟8086方式。

三种工作方式的转换如图:
Linux内存地址的分段、分页机制(上)

Linux内存地址的分段、分页机制(上)

OK,上面简单的介绍了Linux运行的硬件基础,这些不是重点,重点是后面要说的分段、分页机制。我也是边学边记,不足之处请不吝赐教~

1.问题来了,分段到时是怎么回事?有何用?

实模式的诞生(16位处理器及寻址

在8086处理器诞生之前,内存寻址方式就是直接访问物理地址。8086处理器为了寻址1M的内存空间,把地址总线扩展到了20位。但是,一个尴尬的问题出现了,ALU(算术逻辑单元)的宽度只有16位,也就是说,ALU不能计算20位的地址。为了解决这个问题,就引入了分段机制。

为了支持分段,8086处理器设置了四个段寄存器:CS, DS, SS, ES. 每个段寄存器都是16位的,同时访问内存的指令中的地址也是16位的。但是,在送入地址总线之前,CPU先把它与某个段寄存器内的值相加。这里要注意:段寄存器的值对应于20位地址总线的中的高16位,所以相加时实际上是内存总线中的高12位与段寄存器中的16位相加,而低4位保留不变,这样就形成一个20位的实际地址,也就实现了从16位内存地址到20位实际地址的转换,或者叫“映射”。

保护模式的诞生(32位处理器及寻址)

* 80286处理器的地址总线为24位,寻址空间达16M,同时引入了保护模式(内存段的访问受到限制)
* 80386处理器是一个32位处理器,ALU和地址总线都是32位的,寻址空间达 4G。也就是说它可以不通过分段机制,直接访问4G的内存空间。虽然它是新时代的小王子,超越它的无数前辈,然而,它需要背负家族的使命–兼容前代的处理器。也就是说,它必须支持实模式和保护模式。所以,80386在段寄存器的基础上构筑保护模式,并且保留16位的段寄存器。
* 从80386之后的处理器,架构基本相似,统称为IA32(32 Bit Intel Architecture)。

                                    从80386之后的处理器统称为IA32

2.IA32的内存寻址机制

寻址硬件

在 8086 的实模式下,把某一段寄存器左移4位,然后与地址ADDR相加后被直接送到内存总线上,这个相加后的地址就是内存单元的物理地址,而程序中的这个地址就叫逻辑地址(或叫虚地址)。在IA32的保护模式下,这个逻辑地址不是被直接送到内存总线而是被送到内存管理单元(MMU)。MMU由一个或一组芯片组成,其功能是把逻辑地址映射为物理地址,即进行地址转换,如图所示

Linux内存地址的分段、分页机制(上)

IA32的三种地址

* 逻辑地址:
机器语言指令仍用这种地址指定一个操作数的地址或一条指令的地址。 这种寻址方式在Intel的分段结构中表现得尤为具体,它使得MS-DOS或Windows程序员把程序分为若干段。每个逻辑地址都由一个段和偏移量组成。
* 线性地址:
线性地址是一个32位的无符号整数,可以表达高达232(4GB)的地址。通常用16进制表示线性地址,其取值范围为0x00000000~0xFFFFFFFF.
* 物理地址:
也就是内存单元的实际地址,用于芯片级内存单元寻址。 物理地址也由32位无符号整数表示。

MMU地址转化过程

MMU是一种硬件电路,它包含两个部件,一个是分段部件,一个是分页部件,在此,我们把它们分别叫做分段机制和分页机制,以利于从逻辑的角度来理解硬件的实现机制。分段机制把一个逻辑地址转换为线性地址;接着,分页机制把一个线性地址转换为物理地址。

Linux内存地址的分段、分页机制(上)

IA32的段寄存器

IA32中有六个16位段寄存器:CS, DS, SS, ES,FS, GS.跟8086的段寄存器不同的是,这些寄存器存放的不再是某个段的基地址,而是某个段的选择符(Selector)

分段机制的实现

段是虚拟地址空间的基本单位,分段机制必须把虚拟地址空间的一个地址转换为线性地址空间的一个线性地址。

为了实现这种映射,仅仅用段寄存器来确定一个基地址是不够的,还得描述段的长度,段的属性等。这就是段描述符:

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

多个段描述符组成的表称为段描述符表。

段描述符表

各种各样的用户描述符和系统描述符,都放在对应的全局描述符表、局部描述符表和中断描述符表中。描述符表(即段表)定义了IA32系统的所有段的情况。所有的描述符表本身都占据一个字节为8的倍数的存储器空间,空间大小在8个字节(至少含一个描述符)到64K字节(至多含8K)个描述符之间。

1.全局描述符表(GDT)
全局描述符表GDT(Global Descriptor Table),除了任务门,中断门和陷阱门描述符外,包含着系统中所有任务都共用的那些段的描述符。 它的第一个8字节位置没有使用。

2.中断描述符表IDT(Interrupt Descriptor Table)
中断描述符表IDT(Interrupt Descriptor Table),包含256个门描述符。IDT中只能包含任务门、中断门和陷阱门描述符,虽然IDT表最长也可以为64K字节,但只能存取2K字节以内的描述符,即256个描述符,这个数字是为了和8086保持兼容。

3.局部描述符表(LDT)
局部描述符表LDT(local Descriptor Table),包含了与一个给定任务有关的描述符,每个任务各自有一个的LDT。 有了LDT,就可以使给定任务的代码、 数据与别的任务相隔离。每一个任务的局部描述符表LDT本身也用一个描述符来表示,称为LDT描述符,它包含了有关局部描述符表的信息,被放在全局描述符表GDT中。

总结

IA32的内存寻址机制完成从逻辑地址–线性地址–物理地址的转换。其中,逻辑地址的段寄存器中的值提供段描述符,然后从段描述符中得到段基址和段界限,然后加上逻辑地址的偏移量,就得到了线性地址,线性地址通过分页机制得到物理地址。
首先,我们要明确,分段机制是IA32提供的寻址方式,这是硬件层面的。就是说,不管你是windows还是linux,只要使用IA32的CPU访问内存,都要经过MMU的转换流程才能得到物理地址,也就是说必须经过逻辑地址–线性地址–物理地址的转换。

Linux中分段的实现

前面说了那么多关于分段机制的实现,其实,对于Linux来说并没有什么卵用。因为Linux基本不使用分段机制,或者说,Linux中的分段机制只是为了兼容IA32的硬件而设计的。

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

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

在 IA32 上任意给出的地址都是一个虚拟地址,即任意一个地址都是通过“选择符:偏移量”的方式给出的,这是段机制存访问模式的基本特点。所以在IA32上设计操作系统时无法回避使用段机制。一个虚拟地址最终会通过“段基地址+偏移量”的方式转化为一个线性地址。 但是,由于绝大多数硬件平台都不支持段机制,只支持分页机制,所以为了让 Linux 具有更好的可移性,我们需要去掉段机制而只使用分页机制。但不幸的是,IA32规定段机制是不可禁止的,因此不可能绕过它直接给出线性地址空间的地址。

怎么办呢?Linux的设计人员干脆让段的基地址为0,而段的界限为4GB,这时任意给出一个偏移量,则等式为“0+偏移量=线性地址”,也就是说“偏移量=线性地址”。另外由于段机制规定“偏移量<4GB”,所以偏移量的范围为0H~FFFFFFFFH,这恰好是线性地址空间范围,也就是说虚拟地址直接映射到了线性地址,我们以后所提到的虚拟地址和线性地址指的也就是同一地址。看来,Linux在没有回避段机制的情况下巧妙地把段机制给绕过去了。

另外,由于IA32段机制还规定,必须为代码段和数据段创建不同的段,所以Linux必须为代码段和数据段分别创建一个基地址为0,段界限为4GB的段描述符。不仅如此,由于Linux内核运行在特权级0,而用户程序运行在特权级别3,根据IA32段保护机制规定,特权级3的程序是无法访问特权级为0的段的,所以Linux必须为内核用户程序分别创建其代码段和数据段。这就意味着Linux必须创建4个段描述符——特权级0的代码段和数据段,特权级3的代码段和数据段。

总结

分段机制是IA32架构CPU的特色,并不是操作系统寻址方式的必然选择。Linux为了跨平台,巧妙的绕开段机制,主要使用分页机制来寻址。
参考资料

<<Linux内核源码注释>>
<<深入分析Linux内核源码>>
以及"benpaobagzb"的博客