您的位置:首页 > 运维架构 > Linux

linux内核设计与实现--内存寻址

2015-09-26 22:24 495 查看
如果这个世界上只有RISC架构的处理器的话, 内存寻址就非常简单了, 无非是虚拟地址转物理地址什么的. 但是由于有X86的存在, 内存管理复杂了许多. 由于历史影响, X86不得不一直保留着传统的段式寻址方式.

 

内存地址

在X86下有逻辑地址(段+偏移量), 线性地址(虚拟地址), 和物理地址.

对于X86架构来说, 分段处理单元始终都是在工作的, 所有对于X86架构, C语言操作指针的时候, 指针的地址不能称之为虚拟地址, 而应该是逻辑地址. 逻辑地址加上段描述符里面的偏移才是虚拟地址(线性地址).

逻辑地址经过分段单元(segmentation unit)的硬件电路, 转换成为线性地址, 然后分页单元(paging unit)的硬件电路把线性地址转换成一个物理地址. X86下通过CR0寄存器可以设置打开或关闭分页单元, 只有在保护模式下才能开启分页单元.(当PE位清0时(实模式), 设置PG位将导致处理器产生一个异常中断)

在RISC架构的处理器中, 基本都取消了段式管理模式, 只使用页式内存管理. 这样就没有了逻辑地址的概念, 只保留虚拟地址和物理地址, 从而精简了指令集和设计.

 

内存的访问

在SMP系统中, 或者在支持DMA的单CPU系统中, 由于内存有可能被并发访问, 所以便需要一种所谓的内在仲裁器(memory arbiter). 该设计完全由硬件来实现, 因此对软件来说是隐藏的.

 

X86的段式寻址

X86提供了6个段寄存器, cs dsss es fs gs. 每个段寄存器有16个bit, 里面存放的是段描述符, 简单来说, X86是通过段寄存器找到段描述符, 通过段描述符再找到段的基址来完成逻辑地址到线性地址的转换的.

段寄存器是有隐藏属性的, 当段选择符被设置到段寄存器中的时候, 相应的段描述符就由内在装入到对应的非编译CPU寄存器. 这样当逻辑地址转换到虚拟地址的时候, 就不需要再去内存中读取段描述符了.



 RPL位说明的是进程对段访问的请求权限(Request Privilege Level),是对于段选择子而言的,每个段选择子有自己的RPL,它说明的是进程对段访问的请求权限。而且RPL对每个段来说不是固定的,两次访问同一段时的RPL可以不同。当进程访问一个段时,需要进程特权级检查,一般要求DPL >= max {CPL, RPL}, 因此RPL有可能会削弱CPL的作用.

 

TI位标识了是要去GDT还是LDT中查找索引.

 

CS寄存器没有RPL段, 只有CPL段, 长度和RPL段一样. 它表示CPU当前的特权级(Ring0~Ring3),Windows和Linux都只选用了0级和3级.

 

注:

CPL: Current Privilege Level, 存放在CS寄存器中.

RPL: Request Privilege Level, 存放在其它几个段寄存器中,表示请求的权限.

DPL: Descriptor Privilege Level, 保存在段描述符中, 表明该段需要的访问权限.

 

段描述符

每个段由一个8字节的段描述符(Segment Descriptor)表示, 段描述符放在全局描述符表(Global Descriptor Table, GDT)或局部描述符表(LocalDescriptor Table, LDT)中.

GDT在内存中的基地址和大小存放在gdtr控制寄存器中, 当前正在被使用的LDT地址和大小放在ldtr控制寄存器中. 在linux下, 单处理器系统中只有一个GDT,而在多处理器系统中每个CPU对应一个GDT, 所有的GDT都存放在cpu_gdt_table数组中.

LDT不是全局可见的,它们只对引用它们的任务可见,每个任务最多可以拥有一个LDT。另外,每一个LDT自身作为一个段存在,它们的段描述符被放在GDT中。

IA-32为LDT的入口地址也提供了一个寄存器LDTR,因为在任何时刻只能有一个任务在运行,所以LDT寄存器全局也只需要有一个。如果一个任务拥有自身的LDT,那么当它需要引用自身的LDT时,它需要通过LLDT将其LDT的段描述符装入此寄存器。LLDT指令与LGDT指令不同的时,LGDT指令的操作数是一个32-bit的内存地址,这个内存地址处存放的是一个32-bit GDT的入口地址,以及16-bit的GDT Limit。而LLDT指令的操作数是一个16-bit的选择子,这个选择子主要内容是:被装入的LDT的段描述符在GDT中的索引值.

一个段描述符是8字节长, 因此它在GDT或LDT内的相对地址是由段选择符的最高13位的值乘以8得到的. 例如: 如果GDT在0x00020000(gdtr寄存器的值), 段选择符是2, 则段描述符的地址是0x00020000 + (2 * 8) = 0x00020010

GDT的第一项总是为0, 这就确保空段选择符的逻辑地址会被认为是无效的, 因此引起一个处理器异常. 能够保存在GDT中的段描述符的最大数目是8191, 即213 -1.

从逻辑地址转化为虚拟地址的过程如下图:



 

分页内存管理机制

与分段内存管理机制不同, 支持MMU的处理器基本都实现了内存分页管理机制, 以支持虚拟地址操作. 分页管理机制也可以像分段机制一样, 保护内存, 控制访问权限等. 分页管理机制在大部分处理器上的设计都是类似的.

 

页(page)

内核把物理页作为内存管理的基本单位. MMU以页大小为单位来管理系统中的页表.

32位系统的页大小一般为4KB, 64位系统的页大小一般为8KB.32位Linux系统中一般只需要两级页表, 而64位系统下则用到了4级.

内核用strcut page结构来表示系统中的每个物理页. 每个物理页有自己的flag(状态标识), count(引用计数), virtual(虚拟地址)域.

 

区(zone)

由于硬件的限制, 只有某些页能做特定的任务, 所以内核把页划分为不同的区.

Linux主要使用的4种区:

ZONE_DMA: 可以用来执行DMA操作的页.

ZONE_DMA32: 可以被32位设备访问的用于执行DMA的页.

ZONE_NORNAL: 能正常映射的页.

ZONE_HIGHMEM: 高端内存, 不能永久映射到内核地址空间.

 

TLB

TLB的作用是缓存虚拟地址到物理地址的映射关系.

TLB命中成功的时候, MMU不需要读取内存中的页表, 减少了对内存的读取.

 

X86下线性地址到物理地址的映射

X86架构下, CR3指向了PMD的基地址.
 


具体转换算法如下:
1.      cr3 + Page Directory (10MSB) = 指向 page_table_base
2.      page_table_base + PageTable (10 中间位) = 指向 page_frame_base
3.      page_frame_base + Offset =物理地址 (获得页框)
 

总结:

本来只是想讲一讲内存的寻址方式的, 结果一不小心变成了X86专题了, 这里其实也从侧面说明了X86的复杂(窝心的设计, 看了好久才看懂). 其实抛开分段内存管理只讨论分页的话, X86和其它RISC架构的处理器并没有太大的不同.

Linux不像Windows那样利用了很多X86的分段特性, 而是把几个重要的段设置成统一的基地址(0x00000000)的方式, 来有限的使用分段机制. 这样内核代码在不同处理器架构上的兼容性就比较简单了.

 

参考资料

深入理解Linux内核(第三版)

Linux内核设计与实现(第三版)

Linux 的 NUMA 技术  http://www.ibm.com/developerworks/cn/linux/l-numa/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: