Linux内核在I386架构下的内存管理

I386Intelx86系列CUP中一个重要的里程碑。Linux最初就是I386 CPU上实现的。本文介绍Linux内核对I386架构系统进行内存管理,以及从逻辑地址到物理地址的转换方式。对于绝大多数现代的操作系统,例如:Windows NT(包括Windows 2000Windows XP)在内存管理的实现方式基本上大同小异,通过对Linux分析,也可以加强对非源码开放式操作系统的理解。在I386架构上的现代操作系统,无一例外都需要在保护模式下工作。所以,先来介绍I386在保护模式有别于实模式的特性。

一、  I386 CPU保护模式下的新特性

1.  特权级别

CPU分为4个特权级别,0-3 0最高,3最低。每条指令有其适用的级别。Linux和各种I386 Unix都 只用到03级别,对应Linux/Unix中的系统态和用户态。

2.  增加段寄存器

增加FSGS段地址寄存器。加上原来的4个段寄存器,I386一共提供6个段寄存器。在保护模式下,以前实模式下的段寄存器还是有用的。不过它不再用来存放段的基址,而是用来存放“段选择子”(段描述项),其实段寄存器的名字也变成了“段选择子寄存器”或“段描述项寄存器”。在访问内存的时候,我们需要给出的是“段选择子”(段描述项),而不是段基址了。在段描述项中包含段基址、权限等信息。

3.  增加段表寄存器

Ø  增加GDTRLDTR,他们只用特权指令才能访问。配合段寄存器使用。

GDTR——全局段描述表寄存器 global descriptor table regisiter

LDTR——局部段描述表寄存器 local descriptor table regisiter

Ø  GDTRLDTR加载GDTLDT中的基地址。

GDT——全局地址段描述数据项表。

LDT——局部地址段描述数据项表。

Ø  Linux中只使用GDTLDT在做虚拟机时候下才使用。

4.  中断向量

“中断向量”中不仅是程序的入口地址,而类似psw  + “入口地址的方式,但更复杂。把各种中断向量归为4种类型的门。例如:cup在穿越中断门的时候自动关闭中断。 ......,很多说道,归根结底就是为了提供对“进程切换”和“中断响应”的更细致的控制。

5.  中断向量的保存与恢复

每种类型门的中断向量都包含段选择码TSS,它是用于保存任务现场的数据结构。

TSS——任务状态段 Task_state segment  

当前任务(进程)被调度后,当前CPU各个寄存器的数据被保存到上一个任务(进程)的TSS中;当前任务的TSS的内容被装入各种寄存器,完成任务切换。

6.  增加的其他寄存器

Ø  增加IDTRTR寄存器,只有特权指令才能访问它们。

IDTR——中断向量表指针寄存器

TR——任务寄存器,用于指向当前任务(进程)的TSS

Ø  增加CR3寄存器,指向页表目录的基地址。页表目录,在有的操作系统教材上也叫做,外层页表。

7.  中断向量表的位置

IDT不再只保存在0开始的位置,可以在内存的任何位置,IDTR指向之。

IDT——中断向量表

IDTR——中断向量表指针寄存器

 

二、  LinuxI386的内存管理

1.  内存的初始化

1)   由于Linux需要在多种CPU上运行,而且大部分RISC CPU对段式内存管理的支持都很弱,所以在2.2内核以后,Linux基本上采取了绕过段式内存管理的策略。主要通过2个手段:

a)   通过所有进程指向共用两个GDT表项(其实一共四个表项,内核用2个,其他所有进程共用2个),一个代表数据段,一个代表代码段。

b)   并把这两个段基地址都设置为0,长度设置为最大,这就是所谓的平面内存。

2)   启用页式内存管理。初始化页表。

页表项:

 

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

P

R/W

U/S

PWT

PCD

A

D

0

0

OS专用

物理内存块号

 

页表项占32位,4个字节。其中高20位,为物理内存块号,220次方 = 1M,就是说共可以匹配1M个的物理块,其中一个物理块的大小规定为4K,这样共可以支持1M×4K=4G的物理内存。页表的低12位存放页面的控制信息。

下面看到就是页面表:

控制信息

物理块号

每项4个字节
1024
4K

页表0

每个页表4K
1024
4M

 

 

 

 

 

 

 

 

 

 

 

 

控制信息

物理块号

每项4个字节
1024
4K

页表1023s

 

为了匹配4G的物理内存,需要建立1M个这样的页表项,每个页表项4个字节,1M×4=4M字节。这就是说,存储页表项的物理内存就需要4M字节,而物理内存是分块管理的,每块4K,这样就需要4M字节除以4K字节 = 1024个物理内存块存储页面表项。为了管理这1024个物理内存块,需要建立外层页表项。

外层页表项格式与内层页表项一致。占用空间:1024×4字节=4K,这样所有的外层表项就正好占用一个物理块。

外层页表在物理内存中的存放示意图:

 

控制信息

物理块号

外层页表
每项4个字节
1024
4K

 

在实际的内存中,单个的页表一定占用一个物理页面(按4K边界对齐),页表的基地址的形式一定是 0xXXXXX000

转换成2进制形式:

xxxxxxxxxxxxxxxxxxxx000000000000

12位的低地址都是0,这说明20位的物理块号,一定能找到页表的基地址。并且表项是按线性存储的。如果已知表项和页表的基地址,就可以通过指针的偏移找到相应的表项。

然而,各个内层页表在物理内存页面中则可能不是连续存放的,不过这没关系,外层页表的基地址是操作系统已知的,通过外层页表项的物理块号就可以找到它们。

3)   Linux对页表的管理

Linux是怎么知道哪些页表已经分配出去了,哪些是空闲的呢?解决的办法是,在内核中设有专门的数据结构,记录没有分配出去的页表。内核为新创建的进程分配页面,已经存在的进程申请页面都是以此为依据。

4)   Linux对物理内存的管理

在大多数情况下,系统实际拥有的物理内存可能没有达到4G。那么Linux有是怎么知道实际的物理内存的情况呢?解决办法依然是在内核中设有专门的数据结构,记录物理内存的情况。

2.  分配内存与寻址的过程

1)   程序的编译和链接

编译器把源代码文件编译成模块,链接器把各个模块链接起来。大体上把程序分为代码段、数据段、BSS段、堆(heap)段和栈段;并完成从符号到逻辑地址的转换。

2)   程序的装入和进程的创建

程序装入内存运行的功能是由execve()这一系统调用实现的。简单来讲,程序的装入主要包含以下几个步骤:

a)   读入可执行文件的头部信息以确定其文件格式及地址空间的大小;

b)   以段的形式划分地址空间(分配页面);

c)   将可执行程序读入地址空间中的各个段,建立虚实地址间的映射关系;

d)   bbs段清零;

e)   创建堆栈段;

f)   建立程序参数、环境变量等程序运行过程中所需的信息;

g)   启动运行。

前面提到了,Linux内核已经不再为每个进程维护各自的段表了,相反为所有进程维护同一张段表,就是所谓的GDT,而且所有的用户进程共同使用其中的2条记录,而且由于使用Intel所谓平面地址空间,在理论上段内使用的逻辑地址可以是从0x000000000xFFFFFFFF之间的任意值。

现在有两个问题,一是如何为一个进程的各段分配逻辑地址,保证各段的逻辑地址不互相冲突呢?二是由于每个进程的寻址空间都是4G,在每个进程中都有可能使用像0x8048394这样逻辑地址,系统是怎么把他们区分开的呢,是怎么分别映射到不同的物理内存上的中的呢?

第一个问题,链接器来帮我们解决。在链接的过程中完成符号到逻辑地址的转换,并保持在同一个程序中不会为两个符号分配相同的逻辑地址。

第二个问题解决办法则十分巧妙,Linux内核为每个进程都建立一个页表目录,这个页表目录就是上面提到的外层页表,1024项,每项4字节。内核把进程已经使用的页面记录到这个页表目录中。由于在运行期间,内核负责空闲页表的维护,它不会把相同的页面分配给不同的进程,而页面是和物理内存地址直接映射的,所有尽管不同进程使用的逻辑地址可以相同,但不会映射到现同的物理内存地址上去的。下面就会提到逻辑地址到物理地址的转换的过程。

顺便提一下,由于绝大多数进程使用的内存都远小于4G,所有大多数页表目录项都是空的。

3.  从逻辑地址->线性地址->物理地址的转换

用户程序经过汇编后,所有的指令基本使用的都是逻辑地址。如果使用I386的段式内存管理,需要先把逻辑地址转换成线性地址,但Linux绕过了I386的段式内存管理(这需要gcc编译链接器的帮助),用户程序中的逻辑地址就已经是线性地址了,所以从逻辑地址到线性地址无需转换。

CPU使用页式内存管理前,线性地址就是物理地址,不经转换可以直接使用。但是一旦启用了页式内存管理,就先把线性地址转换成为物理地址后才能使用。这工作由CPU内部的MMU硬件单元来完成。

下面给出的是逻辑内存的格式:

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

页内地址偏移

页表项 10

目录号 10

 

下面给出的是一个逻辑内存的值:0x480492A0

按照2进制展开三个字段分别为:

0100100000  0001001001 001010100000

目录号       = 0100100000(二进制)    = 0x120

页表项       = 0001001001(二进制)    = 0x49

页内地址偏移 = 001010100000 (二进制) = 0x2A0

 

CPUCR3(目录表寄存器)的支持下,找到当前进程的目录表基地址,加0x120个字(每个字等于4个字节)后,得到目录表项;从表项中取出页表的基地址,加0x49个字,得到页表项;从页表项中取出物理块号,加0x2A0,得到实际的物理地址。

在这个过程中,逻辑地址在程序的编译链接的时候就确定了。页表项中的物理块号在操作系统初始化的时候也确定了(因为使用虚拟存储器,会发生换入换出,在这里先不管它)。唯独页表的基地址是在运行时候才分配的,就是说,内核具体上给进程分配哪些页表,只用在进程创建或进程申请内存的时候才能定下来。

在这里假设上面的例子的被分配的页表的页表项中的物理块号是0x02000000,那么实际的物理地址就是 0x02000000 + 0x2A0 = 0x020002A0

4. 关于PAE

Pentium pro的以后的IntelX86系列CPU有了36根地址线,支持3级页式内存管理,为了保持与原有软件兼容,还是使用32位的线性地址。这就是Intel所谓的PAEPhysical Address Extensions)。 它的好处是,虽然每个进程的寻址空间还是4G,但是整个系统的可用内存已经达到64G。它的实现的基本思路是同时支持4K2M的内存页面,而且使用3层页表进行内存管理。它可以看作对I386的扩展,但是具体实现十分繁琐,在这里就不详细介绍了。

 

 

你可能感兴趣的:(Linux内核在I386架构下的内存管理)