心渐渐失空

导航

第三次学习内存寻址笔记

最近拿起深入理解linux内核这本书来看,开篇绪论一过,直接就是linux内存寻址。回首大学四年,我已经是第三次学习内存寻址了,但现在又忘记的差不多了。
第一次是看操作系统概论时学习过一遍分段和分页,当时学的比较模糊,只是读懂了流程,却不知道具体有什么用。
第二次是看深入理解计算机系统,里面有一部分讲述虚拟内存,当时读完后感觉以前我理解内存的迷惑已经全部解开了。
而这一次翻看,我又忘记了分段,只记得分页。于是打算边读边记重点,下面是我记录的重点内容:
80x86微处理器进行芯片级的内存寻址:
1、内存地址:
逻辑地址 -》【分段单元】-》线性地址-》【分页单元】-》物理地址
逻辑地址=段选择符(16位)+段内偏移(32位)       线性地址=[0, 0xffffffff]      物理地址=内存芯片级内存单元寻址
多CPU共享同一内存,RAM芯片上的读写必须是串行执行的,由内存仲裁器保证。编程观点看,内存仲裁器是由硬件电路管理,因此是隐藏的。
 
2、硬件中的分段:
Intel微处理器以两种不同的方式执行地址转换:实模式(real mode,的、为了维持处理器与早期模型兼容,并让操作系统自举)、保护模式(protected mode,大多数时候在此模式下执行地址转换)
 
段选择符(16位,也称段标识符):通过段选择符能够找到一个对应的段。=index[13](存段描述符的入口) + TI[1](GDT or LDT) + RPT[2](cs寄存器时指定CPU特权)
段寄存器(用于存放段选择符,加快访问段选择符的速度):cs(代码段寄存器)、ss(栈段寄存器)、ds(数据段寄存器)、es(可以存任意段)、fs(任意段)、gs(任意段)。
 
段描述符(64位,描述段的特征及段的起始线性地址):段描述符放在全局描述符表(GDT)或者局部描述符表(LDT)。通常只定义一个GDT,每个进程除了GDT中的段还要其他段则可以有自己的LDT。GDT在主存中的地址和大小存放在gdtr控制寄存器中,当前正在被使用的LDT的地址和大小放在ldtr控制寄存器中。广泛使用的:代码段描述符、数据段(栈段也是通过数据段实现的)描述符、任务状态段(保存处理器寄存器内容的段)描述符(只出现在GDT中)、局部描述符表描述符(只出现在GDT中,增加LDT时需要使用)。
非编程寄存器(不能被程序员设置的寄存器,只用来缓存段描述符,加快访问段描述符的速度,个数和段寄存器个数对应):每当一个段选择符被装入段寄存器时,相应的段描述符就由内存装到对应的非编程CPU寄存器,从那时起,针对那个段的逻辑地址转换就可以不访问主存中的GDT或LDT,处理器只需要直接引用这个寄存器即可。仅当段寄存器的内容改变时,才有必要访问GDT或LDT。
 
GDT的第一项总是设为0,确保了空的段选择符的逻辑地址会被认为是无效的,引起一个处理器异常。
找到段描述符的线性地址:段选择符中的index有13位,所指定的段描述符的地址 = GDT或LDT首地址(gdtr或ldtr中) +  index * 8 Byte(一个段描述符的大小8Byte)。GDT中段描述符的最大数目是2^13-1(第一个总是0)。
逻辑地址转换线性地址:通过段寄存器中的段选择符和gdtr(或ldtr)的内容,能够找到段描述符,段描述符中的Base字段则是段的起始线性地址,这个地址再加上32位的段内偏移就是该逻辑地址对应的线性地址。
 
个人理解说明:80x86等微处理器本身支持分段,为操作系统提供了分段的基础,而各个操作系统的利用硬件支持的方式不同。第3节说明Linux是如何利用硬件,并实现自己的分段的。
 
3、Linux中的分段:
四个主要的linux段:用户代码段、用户数据段、内核代码段、内核数据段。
相应的段选择符和段描述符的内容是固定的。段选择符由宏__USER_CS、__USER_DS、__KERNEL_CS、__KERNEL_DS分别定义。
与段相关的线性地址从0开始,达到2^32-1的寻址限长,这意味着用户态或内核态下的所有进程可以使用相同的逻辑地址(相当于没有把内存分段)。所有段的Base段都是从0开始,这意味着Linux下逻辑地址和线性地址是相等的,及段内偏移等于线性地址(基本上废弃了分段机制)。
只要当前特权级被改变,一些段寄存器必须相应的更新。
 
单处理器系统中只有一个GDT,多处理器系统中每个CPU对应一个GDT。所有的GDT都放在cpu_gdt_table数组中,所有GDT的大小和地址都存放在cpu_gdt_descr数组中。
每个GDT包含18个段描述符和14个空的,中间插入14个空的是为了使经常一起访问的描述符能处于同一个32字节的硬件高速缓存行中。
18个段描述符分别是:4个主要段(用户代码段、用户数据段、内核代码段、内核数据段),任务状态段(TTS),包括缺省LDT的段,3个局部线程存储(TLS)段,与高级电源管理(AMP)相关的3个段,与支持即插即用(PnP)功能的BIOS服务程序相关的5个段,被内核用来处理“双重错误”异常的特殊TSS段。
大多数用户态下的linux程序不使用LDT,内核中定义了一个缺省的LDT供大多数进程共享,其描述符存在上述的GDT中。缺省的LDT存放在default_ldt数组中。
 
4、硬件中的分页
分页单元把所有的RAM分成固定长度的页框(page frame,也叫物理页,主存的一个小块),每个页框包含一个页(page,数据块,可以放在任何页框或磁盘中),页和页框长度一致。
页表:把线性地址映射到物理地址的数据结构称为页表(page table)。页表存放在主存中,并在启用分页单元之前必须由内核对页表进行适当的初始化。
从80386开始,所有的80x86处理器都支持分页,它通过设置cr0寄存器的PG标志启用,如果PG=0,则线性地址就被直接解释成物理地址。
从80386开始,Intel处理器的分页单元处理4KB的页。
32位的线性地址=Directory[10] + Table[10] + Offset[12]
先通过Directory偏移在页目录表中找到页表起始地址,再通过Table偏移在页表中找到块起始地址,再通过Offset偏移在块内找到地址。页目录物理地址存放在控制寄存器cr3中。
页目录和页表具有相同的结构,此模式为二级模式,如果使用一级模式,则页表有2^20个表项,每一项4字节,则每个进程都需要4MB的RAM存放页表,二级模式通过只为进程实际使用的那些虚拟内存区请求页表来减少页表所占用的内存大小。
不管是页目录、页表、页,都是以页框为单位存放。页目录、页表的每一项都有相同的结构,其中包含的信息有是否缺页、指向页的地址、指向页的读写权限、等等
 
从Pentium模型开始,80x86微处理器引入了扩展分页(extended paging),它允许页框大小为4MB(PS=1)而不是4kB(PS=0),在这种情况下,cr3寄存器里的页目录地址+ Directory[10]得到页目录中的项,这个项直接跳过页表而指向一个4MB页首地址,线性地址中的Table[10] + Offset[12]都用来在这4MB的页内做偏移。相当于32位的线性地址=Directory[10] + Offset[22],通过设置cr4处理器寄存器的PSE标志能使扩展分页与常规分页共存。
 
注:段有三种存取权限:读、写、执行;页有两种存取权限:读、写;
 
Intel通过在它的处理器上把管脚数从32增加到36,扩展了CPU支持的RAM最大寻址数,物理寻址从2^32(4GB)扩展到2^36(64GB),只有引入新的分页机制才能把32位线性地址转成36位物理地址。从Pentium Pro处理器开始,Intel引入物理地址扩展(PAE)机制,通过设置cr4中的PAE标志激活PAE。页目录项中的页大小标志PS启用大尺寸页(在PAE启用时为2MB)。在Pentium III处理器中有一种叫做页大小扩展(PSE-36)的机制也能扩展物理寻址,Linux没有使用。
 
为了支持PAE改变了分页机制:
    64GB的RAM被分为2^24个页框,页表项从20位扩展到24位。页表项从32位变为64位,一个4KB的页表只包含512个表项而不是原来的1024个。
    引入一个叫做页目录指针表(PDPT)的页表新级别,它由4个64位的表项组成。
    cr3控制寄存器包含一个27位的PDPT基地址字段,PDPT存放在RAM的前4GB中,并在32字节(2^5)的倍数上对齐,因此27位足以找到PDPT。
新的寻址方式为:
    PS=0,页大小为4KB时:cr3中的27位地址找到PDPT,32位线性地址=[2]指向PDPT4个项中的一个,[9]指向页目录512个项中的一个,[9]指向页表512个项中的一个,[12]4KB页中的偏移。
    PS=1,页大小为2MB时:cr3中的27位地址找到PDPT,32位线性地址=[2]指向PDPT4个项中的一个,[9]指向页目录512个项中的一个,[21]2MB页中的偏移。
只有内核能够修改进程的页表,所以在用户态下运行的进程不能使用大于4GB的物理地址空间。
 
64位系统中的分页:32位系统普遍采用两级分页,两级分页不适用于64位系统的计算机,这会使得页表含有的项太多,占用大量内存。以下是一些实际系统对应的物理分页方式:
平台名
页大小
寻址使用位数
分页级别数
线性地址分级
alpha
8k(还支持其他页大小)
43
3
[10]+[10]+[10]+[13]
ia64
4k(还支持其他页大小)
39
3
[9]+[9]+[9]+[12]
ppc64
4k
41
3
[10]+[10]+[9]+[12]
sh64
4k
41
3
[10]+[10]+[9]+[12]
x86_64
4k
48
4
[9]+[9]+[9]+[9]+[12]
为了缩小CPU和RAM之间的速度不匹配,引入了硬件高速缓存内存,它基于局部性原理。主存(DRAM),(高速缓存内存(SRAM)+高速缓存控制器)插入在分页单元与DRAM之间。
CPU进行读写RAM操作时,操作数都带有寄存器地址或者RAM地址,RAM地址经过分页单元转换成物理地址,高速缓存控制器接到物理地址,判断该地址存储的操作数是否在SRAM中,如果在则:(读:直接读SRAM;写:可能使用通写或者回写;)(通写:同时写SRAM和DRAM;回写:只写SRAM,在CPU发出刷新指令时或者发生FLUSH硬件信号时(通常在高速缓存没命中时发生)再写到DRAM);如果没在则:读写DRAM,有必要时将该行加载到SRAM中。
由于单核CPU速度提升所花价格呈指数提升,所以多核CPU得到了大力发展。多处理器系统中每个处理器都有一个单独的硬件高速缓存,因此它们需要额外的硬件电路用于保持高速缓存内容的同步。高速缓存侦听:CPU修改它的SRAM时,它必须检查同样的数据是否包含在其他的SRAM中,如果是则必须通知其他CPU用适当的值对其进行更新。高速缓存侦听在硬件级处理,内核无需关系。但是这仍然不能保证线程间数据安全。
由于DRAM速度真的慢,被CPU与DRAM之间的线带宽限制。所以高速缓存在快速发展。第一代Pentium芯片包含一颗L1-cache,近期的芯片又额外包含容量更大、速度较慢的L2-cache、L3-cache等片上高速缓存,多级高速缓存之间的一致性是由硬件实现的,linux忽略这些细节并假定只有一个单独的高速缓存。
处理器的cr0寄存器的CD标志位用来启动或禁用高速缓存、NW标志用来指明使用通写还是回写。
 Pentium处理器一个有趣的特点:每一个页目录项、页表项都包含PCD和PNT,提供了以页框为单位的启、禁高速缓存和通、回写策略。但是Linux清除了这两个标志,结果是所有的页框都启用高速缓存并总使用回写策略。
 
转换后援缓冲器(TLB):80x86处理器还包含了一个叫做TLB的高速缓存用于加快线性地址的转换。当一个线性地址第一次被使用时,分页单元通过访问DRAM中的页表计算出物理地址,同时此物理地址被存放在TLB表项中,以后同一个线性地址的引用可以快速得到物理地址。多处理器环境下每个CPU都有自己本地的TLB,各TLB之间不需要同步。CPU的cr3寄存器被修改时,新的一组页表被启用,TLB中所有项都无效。
 
4、Linux中的分页
Linux采用了一种同时适用于32位和64位系统的普通分页模型。直到2.6.10版本Linux采用三级分页模型,2.6.11开始使用4级分页模型(cr3+页全局目录->页上级目录->页中间目录->页表->页)
32位系统中,页上级目录和页中级目录位全为0,所以相当于三级分页。(cr3+页全局目录(相当于80x86的PDPT)->页上级目录(0)->页中间目录(相当于80x86的页目录)->页表->页)
64位系统中,使用三级还是四级分页取决于硬件对线性地址的划分(上表)。
Linux的进程处理很大程度的依赖于分页:不同进程的相同线性地址对应不同物理地址,防止寻址错误、虚拟内存机制(swap)。每一个进程都有它自己的页全局目录和页表,发生进程切换时保存cr3的内容并换成下一个进程的cr3内容即可。
 
内容至此,芯片级别和操作系统级别的寻址(分段和分页)已经大概说了一遍下来,包括很多细节原理。本章下面的内容都是细讲linux分页中的源码,以及linux操作系统如何利用分页实现一些底层功能来为进程环境提供支持。由于太过详细,我打算粗读一遍跳过本章后面的内容,等用到时再当成手册来翻阅其中的源码。另外里面还包含一些汇编代码,我对汇编指令已经忘的差不多了,得先复习一下《深入理解计算机体系》里讲汇编指令的部分再来看。
学习操作系统会学多很好有趣的内容,只要细细品读,就会发现这些内容简单而且有趣,最重要的是这些内容还能解答我心中的迷惑,从侧面提升我的编程能力。这些内容包括操作系统的:硬件中断、软件中断、分页机制、进程环境、虚拟地址空间、启动流程、远程启动、网络协议栈、缓存体系、用户管理、进程关系、会话关系、系统调用、外壳程序、终端等。本书的下一章讲述的是进程。期待。

 

posted on 2018-06-25 18:28  心渐渐失空  阅读(236)  评论(0编辑  收藏  举报