首先对某些概念进行解释。
数据总线:是计算机中各个组成部件间进行数据传输时的公共通道;“内数据总线宽度”是指CPU芯片内部数据传送的宽度;“外数据总线宽度”是指CPU与外部交换数据时的数据宽度。显然,数据总线位数越多,数据交换的速度就越快。
地址总线:是載对存储器或I/O端口进行访问时,传送由CPU提供的要访问的存储单元或I/O端口的地址信息的总线,其宽度决定了处理器能直接访问的主存容量的大小。
现在的微型计算机系统采用下图的三级存储器组织结构,即缓冲存储器Cache、主存、和外存。
高速缓冲存储器Cache的使用,大大减少了CPU读取指令和操作数所需的时间,使CPU的执行速度显著提高。
在80X86CPU的发展过程中,存储器的管理机制发生了较大的变化。8086/8088CPU对存储器的管理采用分段的实方式;80286CPU除了可在实方式下工作外,还可以在保护模式下工作;而80386CPU之后的处理器则具有三种工作方式:实方式、保护方式和虚拟8086方式。
三种工作方式的转换如图:
80386三种工作方式之间的转换
8086寄存器结构:
8086/8088CPU内部有14个16位寄存器。按功能分为三大类:通用寄存器(8个)、控制寄存器(2个)、段寄存器(4个)。
通用寄存器包括4个数据寄存器、两个地址寄存器和两个变址寄存器。
数据寄存器:用于存放参与运算的操作数或运算结果。包括AX累加器、BX基址寄存器、CX计数寄存器、DX数据寄存器。
指针寄存器:SP堆栈指针寄存器、BP基址寄存器。
变址寄存器:SI源变址寄存器、DI目的变址寄存器。
段寄存器包括:CS代码段寄存器、DS数据段寄存器、SS堆栈段寄存器、ES附加数据段寄存器。
控制寄存器包括:IP指令指针寄存器、FLAGS状态标志寄存器。
段的引入:8086为了用16位寄存器实现1MB的寻址内存空间,引入了段的概念。
段是虚拟地址空间的基本单位,分段机制把虚拟地址空间的一个地址转换为线性地址空间的一个线性地址。
虚拟地址到线性地址的转换
虚拟地址到线性地址的映射
其中base为段的基地址,是线性地址空间中段的起始地址
limit是段的界限,在虚拟空间中,段内可以使用的最大偏移量
当然虚拟地址到线性地址的映射通过段寄存器和段描述符表共同完成。且載保护模式下段描述符表分为全局描述符表(GDT)、中断描述符表(IDT)、及局部描述符表(LDT)。Linux 中的段控制单元
在 Linux 中,所有的段寄存器都指向相同的段地址范围 —— 换言之,每个段寄存器都使用相同的线性地址。这使 Linux 所用的段描述符数量受限,从而可将所有描述符都保存在 GDT 之中。这种模型有两个优点:
· 当所有的进程都使用相同的段寄存器值时(当它们共享相同的线性地址空间时),内存管理更为简单。
· 在大部分架构上都可以实现可移植性。
Linux 使用以下段描述符:
· 内核代码段
·内核数据段
·用户代码段
·用户数据段
·TSS 段
·默认 LDT 段
GDT 中的内核代码段 (kernel code segment)描述符中的值如下:
· Base = 0x00000000
· Limit = 0xffffffff (2^32 -1) = 4GB
· G(粒度标志)= 1,表示段的大小是以页为单位表示的
· S = 1,表示普通代码或数据段
·Type = 0xa,表示可以读取或执行的代码段
· DPL 值 = 0,表示内核模式
与这个段相关的线性地址是 4 GB,S = 1 和 type = 0xa 表示代码段。选择器在cs寄存器中。Linux 中用来访问这个段选择器的宏是_KERNEL_CS。
内核数据段 (kernel data segment)描述符的值与内核代码段的值类似,惟一不同的就是 Type 字段值为 2。这表示此段为数据段,选择器存储在ds寄存器中。Linux 中用来访问这个段选择器的宏是_KERNEL_DS。
用户代码段 (user code segment)由处于用户模式中的所有进程共享。存储在 GDT 中的对应段描述符的值如下:
· Base = 0x00000000
· Limit = 0xffffffff
· G = 1
· S = 1
·Type = 0xa,表示可以读取和执行的代码段
· DPL = 3,表示用户模式
在 Linux 中,我们可以通过_USER_CS宏来访问此段选择器。
在用户数据段 (user data segment)描述符中,惟一不同的字段就是 Type,它被设置为 2,表示将此数据段定义为可读取和写入。Linux 中用来访问此段选择器的宏是_USER_DS。
除了这些段描述符之外,GDT 还包含了另外两个用于每个创建的进程的段描述符 —— TSS 和 LDT 段。
每个 TSS 段 (TSS segment)描述符都代表一个不同的进程。TSS 中保存了每个 CPU 的硬件上下文信息,它有助于有效地切换上下文。
每个进程都有自己在 GDT 中存储的对应进程的 TSS 描述符。这些描述符的值如下:
· Base = &tss (对应进程描述符的 TSS 字段的地址;例如 &tss_struct)这是在 Linux 内核的 schedule.h 文件中定义的
· Limit = 0xeb (TSS 段的大小是 236 字节)
· Type = 9 或 11
· DPL = 0。用户模式不能访问 TSS。G 标志被清除
所有进程共享默认 LDT 段。默认情况下,其中会包含一个空的段描述符。这个默认 LDT 段描述符存储在 GDT 中。Linux 所生成的 LDT 的大小是 24 个字节。
所有GDT存放在cpu_gdt_table[]数 组中,段的大小和指针存放在cpu_gdt_descr[]数组中。
Linux中的段:现在的大多数硬件平台都不支持分段机制,但是又不能绕过她而直接给出线性地址空间,在这种情况下,linux的设计人员巧妙的让段的基地址为0,使得偏移量=线性地址。从而实现了虚拟地址到线性地址的映射。
分页机制的引入:段机制规定,必须为代码段和数据段创建不同的段,这样一来,不同的特权级都出现了相应的CS和DS,这样就失去了段保护的作用。
分页机制完成了线性地址到物理地址的转换过程。
首先了解分页机制之前,先要分清页和页面的概念:
页:将线性地址空间划分成若干大小相等的片,称为页(page)
页面:对应页的划分将物理地址空间分成与页大小相等的若干存储块,就称为页面或块。
我们知道线性地址空间的大小为4GB,那怎样来划分页的大小合理呢?IA32的标准页大小为4KB,也就是说,线性地址空间被分成1M个4KB大小的页。分大点可以不?可以,但是相应的内存中页面的数目就减少,带来的结果是一个页面的空间过大,那么对于一个小数据来说,给他分配一个块是不是有点浪费?要不分小点,在内存运行时数据被分散到了不同的块,读起来也是很费劲的,因此,就巧妙的把页的大小设为4KB。
分页机制中使用页表这种数据结构实现了线性地址到物理地址的映射。
页表中包含物理页面基地址和页的属性。
对于页表有两级页表和三级页表之分。linux为了保证可移植行,
采用了三级分页机制,当然其在某些情况下可以返回到二级分页。
上图为线性地址到物理地址的映射