规划虚拟空间的时候,是将空间分成多个段进行保存,而x86 CPU中就有一种分段机制,可以用这个分段机制来完成
分段机制的原理
- 分段机制下的虚拟地址由两部分组成,段选择子和段内偏移量。
- 段选择子就保存在段寄存器中,段选择子中有一个段号,可以用作段表的索引
- 段表里面保存的是这个段的基地址、段的界限、特权等级等
- 虚拟地址中的段内偏移量应该位于0和段界限之间。
- 如果段内偏移量是合理的,就将段基地址加上段内偏移量得到物理内存地址
比如,我们将上面的虚拟空间分为下面4个段,用0~3来编号。每个段在短标中有一个项目,在物理空间中,段的排列如下图右边所示:
如果要访问段 2 中偏移量 600 的虚拟地址,我们可以计算出物理地址为,段 2 基地址2000 + 偏移量 600 = 2600
Linux是如何使用这个分段机制的
- 在Linux中,段表的全称是段描述符表(segment descriptors),放在全局描述符表GDT(Global Descriptor Table)里面,会有下面的宏来初始化段描述符表里面的表项:
#define GDT_ENTRY_INIT(flags, base, limit) { { { \
.a = ((limit) & 0xffff) | (((base) & 0xffff) << 16), \
.b = (((base) & 0xff0000) >> 16) | (((flags) & 0xf0ff) << 8) | \
((limit) & 0xf0000) | ((base) & 0xff000000), \
} } }
- 一个段表项由段基地址base、段界限limit,以及一些标识符组成
DEFINE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page) = { .gdt = {
#ifdef CONFIG_X86_64
[GDT_ENTRY_KERNEL32_CS] = GDT_ENTRY_INIT(0xc09b, 0, 0xfffff),
[GDT_ENTRY_KERNEL_CS] = GDT_ENTRY_INIT(0xa09b, 0, 0xfffff),
[GDT_ENTRY_KERNEL_DS] = GDT_ENTRY_INIT(0xc093, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER32_CS] = GDT_ENTRY_INIT(0xc0fb, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER_DS] = GDT_ENTRY_INIT(0xc0f3, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER_CS] = GDT_ENTRY_INIT(0xa0fb, 0, 0xfffff),
#else
[GDT_ENTRY_KERNEL_CS] = GDT_ENTRY_INIT(0xc09a, 0, 0xfffff),
[GDT_ENTRY_KERNEL_DS] = GDT_ENTRY_INIT(0xc092, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER_CS] = GDT_ENTRY_INIT(0xc0fa, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER_DS] = GDT_ENTRY_INIT(0xc0f2, 0, 0xfffff),
......
#endif
} };
EXPORT_PER_CPU_SYMBOL_GPL(gdt_page);
-
这里面对于64位和32位的,都定义了内核代码段、内核数据段、用户代码段和用户数据段。
-
另外,还会定义下面四个段选择子,指向上面的段描述符表项。在内核初始化的时候,启动第一个用户态的进程,就是将这四个值赋值给段寄存器
#define __KERNEL_CS (GDT_ENTRY_KERNEL_CS*8)
#define __KERNEL_DS (GDT_ENTRY_KERNEL_DS*8)
#define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS*8 + 3)
#define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS*8 + 3)
- 通过分析,我们发现,所有段的起始地址都是一样的,但是0。也就是说,压根就没有分段。这里的分段仅仅用于做权限审核,比如用户态DPL是3,内核态DPL是0。当用户态试图访问内核态时,会因为权限不足而报错
其实linux倾向于另外一种从虚拟地址到物理地址的转换方式,叫做分页(paging)
- 对于物理内存,操作系统把它分成一块一块大小相同的页,这样更方便管理。比如有的内存页长时间不用了,可以暂时写到硬盘上,称为换出;一旦需要的时候,再加载进来,叫做换入。这样可以扩大可用物理内存的大小,提高物理内存的利用率
- 这个换入和换出都是以页为单位的。页面的大小一般为4KB。为了能够定位和访问每个页,需要有个页表,保存每个页的起始地址,在加上在页内的偏移量,组成线性地址,就能对内存中的每个位置进行访问了
- 虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址。这个基地址与页内偏移的组合就形成了物理内存地址
总结
内存管理系统主要做了下面三件事情:
- 第一,虚拟内存空间的管理,将虚拟内存分为大小相等的页
- 第二,物理内存的管理,将物理内存分为大小相等的页
- 第三,内存映射,将虚拟内存和物理内存映射起来,并且在内存紧张的时候可以换出到硬盘中