在保护模式下,IA32采用段页式虚拟内存管理,即先分段再分页。地址的类型有以下三种:
- 逻辑地址(48bit)
- 线性地址(32bit)
- 物理地址
由逻辑地址转化为线性地址由分段过程完成,由线性地址转化为物理地址由分页过程完成。
逻辑地址向线性地址的转化
48bit的逻辑地址可分为16位的段选择符和32位的段内偏移量(也即有效地址EA,汇编指令中给出的地址),比如对于这样一条采用基址+比例变址+偏移量寻址的汇编指令:movw 8(%ebp, %ebx, 4), %ax
,其给出的就是32位的段内偏移量=8+[%ebp]+[%ebx]*4
首先总的看一下从逻辑地址到线性地址的转换过程:
由段寄存器中存储的段选择符找到段表(段描述符表)中的段表项(段描述符),该段表项给出了该段的段基址,再加上指令中的有效地址EA就得到了最终的线性地址
回顾一下IA32的寄存器组织:
6个16位的段寄存器存储了一个段选择符:
- TI=0,使用全局描述符表GDT
- TI=1,使用局部描述符表LDT
- 高13位的索引指向段描述符表(段表)中的一个段描述符(段表项)
代码段寄存器CS的最低两位RPL就指明了当前执行代码的特权级,IA32采用环保护的方式:0表示内核,3表示用户
段描述符和段描述符表的分类:
段描述符的结构:
- B31~B0为基地址
- L19~L0为限界
- G(granularity)为粒度,G=1表示段以页为基本单位,此时最大段长4GB;G=0表示以字节为基本单位,此时最大段长1MB
- D=1表示32位宽数据,D=0表示16位宽数据
- P=1表示段是否已经在主存中,Linux总是把P置1,因为它从不把一个段置换到磁盘,而是以页为单位交换的
- DPL表示访问该段的最低特权级要求
- S=0表示系统控制描述符,S=1表示普通代码段或数据段描述符
- A=1表示该段已经被访问过,A=0表示未被访问过
逻辑地址向线性地址的转换:
- 首先根据段选择符中的TI位确定使用GDT(0)还是LDT(1)
- 然后使用段选择符中的13位的索引,找到段描述符,GDT的基址由GDTR获得,LDT的基址由LDTR获得
- 从段描述符中取出32位的基址B31~B0,与逻辑地址中的EA相加,得到32位的线性地址
考虑到对RISC机器的支持(RISC对分段的支持有限),Linux对IA32的分段机制做了简化,它将所有段描述符的基地址全部设为0,每个段的段内地址空间大小都是4GB,所以,所有逻辑地址中的段内偏移量(EA)就是其线性地址。相当于仅启用了分页机制,而对分段机制进行了简化(所以前面一堆白学)
线性地址向物理地址的转化
这个过程就比较简单了,首先根据控制寄存器CR3找到当前进程的页目录表基址,然后根据线性地址的最高10位作为页目录索引在页目录表中找到对应的页目录项,该页目录项指向了一个页表基址,然后根据线性地址的中间10位作为页表索引在页表中找到页表项,该页表项指向了一个物理页框,将该物理页框拼接上线性地址最低12位的页内偏移就得到了最终的物理地址。
页目录项和页表项的结构:
- P=1表示该页表或页在内存中;否则发生了缺页故障,需要将页故障线性地址记录在CR2中,由OS处理页故障
- R/W=0表示该页只读,否则表示读写
- U/S=0表示用户进程不可访问该页,可以保护操作系统的页不会被用户进程破坏
- PWT指明该页的cache写策略,write throuhg OR write back
- PCD表示cache允许位,控制该页能否被缓存到cache中
- A=1表示该页被访问过,供页面替换算法参考
- D脏位,表示该页是否被修改过
- 高20位就是页表或页在主存中首地址对应的页框号
所以,一条取数指令的大致流程如下:
进程切换时,OS是如何找到当前进程的页目录表基址并将其装入CR3中的呢?一个很自然的想法就是将页目录表基址保存到task_struct
中: