前面花了好多时间讲了内存管理中 node, zone, page frame, buddy system等。这些都是物理地址空间中的概念。然而,对于一个进程来说,它看到的却是完全不同的地址空间。
我们接下来就来看看这些地址空间,以及它们之间的映射。
1.内存地址
在内存管理中,会涉及到三种内存地址。
- 逻辑地址:程序的机器语言指令里,指定一条指令或是一个操作数的地址时,所使用的实际上都是逻辑地址。一个逻辑地址由两部分组成:段选择符和段偏移值。
- 线性地址:逻辑地址经过分段映射后,得到的就是线性地址。
- 物理地址:访问内存条上的内存单元时所使用的地址。CPU会把该地址送往地址总线来读/写某一内存单元。
逻辑地址到物理地址的转换是由MMU (Memory Management Unit)来完成的:分段单元把逻辑地址转换为线性地址,分页单元把线性地址转换为物理地址。
2.分段
系统中每个段都有一个对应的段描述符。段描述符中包含了该段的详细信息,比如:段的起始地址,段的长度,段的类型,段的权限等等。
系统中所有的段描述符都集中放在了两个table里:GDT (Global Descriptor Table) 或是 LDT (Local Descriptor Table)。这两个表的线性地址分别保存在寄存器 GDTR 和 LDTR 中。
一个逻辑地址由段选择符和段内偏移值组成。其中,段选择符格式如下:
TI:Table Indicator,指定了该段存在于GDT中(TI = 0)还是LDT中(TI = 1)。
index:指定了该段在GDT/LDT中的索引。
RPL:Requestor Privilege Level,与该段的权限相关。
系统提供了不同的段寄存器来存放段选择符:CS, SS, DS, ES, FS, GS. 其中,CS专门来放代码段的段选择符,DS专门来放数据段的段选择符,SS专门来放栈的段选择符。
其中,CS段寄存器中的RPL,代表了CPU当前的运行level。Linux只是用了两个level:0(内核态)和 3(用户态)。
逻辑地址经过分段单元转换成线性地址的过程,可以用下面的图来说明:
Linux简化了分段逻辑:所有运行在用户态的进程,都使用同一个代码/数据段,即用户态代码/数据段;同样的,所有运行在内核态的进程,都使用同一个代码/数据段,即内核态代码/数据段。
这四个段的段选择符由四个宏来定义:__USER_CS,__USER_DS,__KERNEL_CS,__KERNEL_DS。
而且,这四个段的起始地址都为0x0。这样带来一个好处:逻辑地址中的段偏移值总是和对应的线性地址相等。所以,逻辑地址空间到线性地址空间的映射,是非常简单的。
3.分页
既然Linux中所有的进程都使用相同的线性地址空间,那么将这些进程分隔开来的重任就要由分页来完成了。
分页机制非常简单,一张图就可以讲清楚了:
不同的进程,虽然使用相同的线性地址空间,但是会使用不同的页表,因此实际上使用的物理地址是不同的。
不过,线性地址空间到物理地址空间的映射,却并没有这么简单,是我们接下来的重点。