内存寻址
内存地址
当使用80x86微处理器时,我们必须区分三种不同的地址:
逻辑地址(LA):是包含在机器语言指令(就是用0和1来表示数据和代码的机器语言指令)中用来指定一个操作数或一条指令的地址。在80x86著名的分段结构中表现得很具体。每个逻辑地址由段和偏移量按某种方式组成,偏移量是段起始地址(段基址,例如在8086实地址模式中,段基址 = 段寄存器值*16)到实际地址的距离。
线性地址(VA,也称进程虚拟地址): 是一个32位无符号数,可以表示4GB的地址。也就是从0x00000000到0xffffffff。
物理地址(PA):用于内存芯片级寻址。PAE技术将地址扩展到了36位。
段选择符、段寄存器、段描述符
段寄存器的唯一目的是存放段选择符,段选择符存放段描述符的内容,段描述符放在GDT或LDT中。
疑问1: es,ds,fs在书中说是可以指向任意的数据段,很晦涩抽象,该如何理解?
疑问2: LDT究竟是干嘛的呢?中断门、陷阱门?
通常只定义一个GDT,每个进程除了存放在GDT中的段,如果需要,还需要创建附加的段,这些附加的段放在LDT中。
gdtr控制寄存器存放GDT在主存中的地址和大小,也就是说通过gdtr,可以找到GDT在内存中的存放位置和大小范围。ldtr控制寄存器类似。存放段描述符的非编程寄存器和gdtr控制寄存器是不同的。
段描述府格式个字段的含义:
BASE: 段的首地址(线性地址),在linux中用户数据段、用户代码段、内核数据段、内核代码段的BASE为0x00000000。
G:粒度标志,如果为0,则段大小以字节为单位,否则以页为单位(在linux下,现在是1页=4096字节)。
LIMIT:存放在段中最后一个内存单元的偏移量,从而决定段的长度。如果G=0,则段的大小在1~1MB字节之间变化;否则在4KB~4GB之间变化。
S:清0则为系统段。系统段有TSS段,LDT段,但GDT不被称为系统段,因为它不是通过段选择符和段描述符访问的。
TYPE:描述段的类型和它的存取权限。
DPL:描述符特权级。DPL设为0,只能在CPL为0时才可访问该段,DPL设为3,对任何CPL值都可访问。
P: 0表示段目前不在内存,全部被交换到磁盘。Linux总是把这位设为1,因为它从来不把整个段交换出去。
D/B: 代码段还是数据段。
AVL: 可以被os使用,但被linux忽略。
常用的段描述符,主要有:
代码段描述符:段描述符代表一个代码段(包括用户态代码段或内核态代码段),整个段描述符可以放在GDT或LDT中。该描述符置S为1。
数据段描述符:与代码段类似。需要说明的是栈段是通过一般的数据段实现的。
任务状态描述符:代表一个任务状态段,这个段描述符只出现在GDT中。S=0。
局部描述符表描述符:代表包含一个LDT的段。 它只出现在GDT中。阅读完本书第四章的中断或异常的硬件处理这一节中可以理解这句话的含义。
疑问3:GDT或LDT放在哪?内核堆栈么?
快速访问段描述符
段选择符字段的含义:
INDEX:放着GDT或LDT中相应段描述符的入口。
TI:指明段描述符放在GDT(TI=0)还是LDT(TI=1)。
RPL:当相应的段选择符装入到cs寄存器中时指示出cpu当前的特权级。它还可以用于在访问数据段时有选择地削弱处理器的特权级。
注: GDT的第一项的内容总是设为0。这是为了区别空的段选择符的逻辑地址是无效的。我们举一个例子来看看不这样设置的结果。如果GDT在0x00020000,那么空的段选择符对应的段描述符地址为0x00020000,第一个段描述符地址为0x00020000+(0*8),也是0x00020000,也就是说空的段选择符的逻辑地址和第一个段描述符计算出的逻辑地址是一样的,显然不合理。
分段单元与linux中的分段
有了与段寄存器相关的不可编程寄存器,只有当段寄存器的内容被改变时(也就是说重新装入新的段描述符到不可编程寄存器),才需要执行前两个操作(见书)。
Linux采用页式存储管理。2.6版的linux只有在80x86结构下才需要分段。
四个主要的linux段的段描述符的limit值都为0xfffff,因为G=1,故寻址范围为0到pow(2,20)*pow(2,12)-1= pow(2,32)-1;
linux下与段相关的线性地址从0开始(也就是说所有段都从0x00000000开始),达到pow(2,32)-1 ;那就是逻辑地址和线性地址一样。
cpu的CPL反映了进程是在用户态还是在内核态,并由存放在cs寄存器的段选择符的RPL(请求者特权级)字段指定。只要当前特权级被改变(也就是说用户态与内核态的转变),一些段寄存器必须相应地更新。当CPL=3时,ds寄存器必须含有相应用户数据段的段选择符。ss寄存器必须指向一个用户数据段中的用户栈(在linux下,前面说过堆栈段通过数据段实现,因此ss寄存器的内容就是ds寄存器此时的内容)。
linux GDT 与 linux LDT
多处理器系统中每个cpu对应一个GDT,所有的GDT都存放在put_gdt_table数组中,而每个GDT的地址和大小放在cpu_gdt_descr了;数组中。
任何被modify_ldt()创建的自定义的LDT仍然需要它自己的 ,GDT中的LDTD的值代表正在cpu上执行的拥有自定义LDT的进程的LDT。
硬件中的分页
页(大小等于页框,4kByte=4096Byte)依据页表而放入页框。页表放在主存的内核空间,在启用分页单元之前必须由内核对页表进行适当的初始化。
32位线性地址分为3个域:页目录、页表、偏移量。所以线性地址的转换分两步。
对于没有启动PAE的32位系统,一般采用二级页表。
使用这种二级模式的目的在于减少每个进程页表所需内存。使用一级页表,需要高达 pow(2,32)/pow(2,12)= pow(2,20)个表项来表示每个进程的页表。而二级页表需要2*pow(2,10)个表项,远远少于pow(2,20)。
正在使用的页目录的物理地址的前20位(也就是页目录的基地址)存放在cr3中。
页目录项和页表项有同样的结构。都包含下面的字段:
Present:如果被置为1,所指的页或页表(页表的大小和页一样,因为pow(2,10)*4字节/项)就在主存中,如果为0,则这一页不在主存,此时这个表项剩余的位可由操作系统用于自己的目的。(如记录该页在辅存中的物理位置)。
包含页框物理地址最高20位的字段: 如果这个字段是页目录项的相应字段,则相应的页框就是一个页表;如果这个字段是页表项的相应字段,则相应的页框就是一个页(包含指令或数据)。
Accessed:每当分页单元对相应页框进行寻址时就设置这个标志。当选中的页被交换出去时,这一标志可以被操作系统使用。分页单元(在处理器中)不重置(reset)这个标志,而必须由操作系统去做。
Dirty:只应用于页表项。该位在操作系统重新分配页框时有用,如果一个页面已经被修改过(即为为“dirty”),则必须把它写回磁盘。否则只要简单地被交换出去(即丢弃)就可以了。因为在磁盘中有副本。分页单元(在处理器中)不重置这个标志,而必须由操作系统去做。
Read/Write:含有页或页表的存取权限。若为0,表示只读,否则是可读写的。
User/Supervisor:如为0,则只有cpl< 3(对于linux系统,cpl为0)时才能对页寻址,若标志为1,则总能对页寻址。
PCD和PWT:控制硬件高速缓存处理页或页表的方式。
Page size:只应用于页目录项。如果设置为1,则页目录项指的是2MB或4MB的页框。在这种情况下,分页单元把32位线性地址分为两个字段10:22。扩展分页和正常分页的页目录项基本相同,除了设置Page size标志位,以及包含页框物理地址最高20位的字段中只有最高10位是有意义的。通过设置cr4处理器寄存器的PSE标志能使扩展分页和常规分页共存。
Global:
物理地址扩展(PAE )分页机制
从Pentium模型开始,80x86微处理器引入了扩展分页,它允许页框大小为2MB/4MB而不是4KB。PSE允许页框大小为4MB,而PAE允许页框大小为2MB。
Intel通过在它的处理器上把管脚数从32增加到36可以满足大型服务器对大于4GB内存的要求。那么如何把32位线性地址转换为36位物理地址呢,从Pentium Pro处理器开始,Intel引入PAE机制。另外一个叫PSE-36的页大小扩展机制在Pentium III中引入,但linux并没有采用。下面是一篇分析PAE的好文章,http://blog.csdn.net/zero2011/archive/2010/05/18/5602558.aspx
linux中的分页
linux采用了一种同时适用于32位和64位系统的普通分页模型。两级页表对32位系统来说已经足够,但64位需要更多数量的分页级别。linux在2.6.10版本采用三级分页,从2.6.11开始,采用四级分页模型。
那么,对于使用二级管理架构32位的硬件,现在又是四级转换了,它们怎么能够协调地工作起来呢?从硬件的角度,32位地址被分成了三部份——也就是说,不管理软件怎么做,最终落实到硬件,也只认识这三位老大。从软件的角度,由于多引入了两部份,也就是说,共有五部份。要让二层架构的硬件认识五部份也很容易,在地址划分的时候,将PUD和PMD长度设置为0就可以了。从软件的角度上来讲,因为它的项只有一个,32位,刚好可以存放与PGD中长度一样的地址指针。那么所谓先到PUD,到到PMD中做映射转换,就变成了保持原值不变,一一转手就可以了。(意思就是把PGD的一个表项逻辑上既看成只含有一个表项的PUD的一个表项,同时也看成只有一个表项的PMD的一个表项,在《linux内核情景分析》内存管理中有相关说明:return (pud_t*) pgd,pgd是指向PGD表项的指针,现在转手让它成为指向PUD表项的指针。)这样,就实现了“逻辑上指向一个PUD,再指向一个PDM,但在物理上是直接指向相应的PT的这个抽像,因为硬件根本不知道有PUD、PMD这个东西”。
进程页表
进程的线性地址空间分为用户空间和内核空间。分别是从0x00000000到0xbfffffff的用户空间(3G),从0xc0000000到0xffffffff的内核空间。当进程通过系统调用进入内核态而运行内核代码的时候,不仅可以访问内核空间,还可以访问用户空间。每个进程的页全局目录中768项以上的表项都是一样的,都等于主内核页全局目录的相应表项。
内核页表
内核维持着一组自己使用的页表,驻留在所谓的主内核页全局目录(master kernel PGD)中。系统初始化后,这组页表还从未被任何进程或任何内核线程直接使用;更确切地说,主内核页全局目录的最高目录项部分(即从768项开始的部分)最为其他普通进程对应的页全局目录项提供参考模型。
书中关于保护模式和实模式的说明有点晦涩难懂,譬如说内核映像刚刚被装入内存后,cpu仍然运行于实模式。这和《情景分析》最后一章关于系统启动过程的描述有点出入,《情景分析》说进入startup_32(这是定义于arch/i386/kernel/head.S的startup_32)时cpu运行于保护模式的段式寻址方式,最后通过Internet 才知道这里(ulk书中)的实模式的含义是eip里装的是物理地址的状态下叫实模式,只有开启了页式寻址的保护模式(eip里装的是逻辑地址)才叫保护模式。对比一下,我更赞同《情景分析》的说法。 关于内核如何初始化自己的页表。分两个过程:
第一个阶段,内核创建一个有限的地址空间,包括内核的代码段和数据段、初始化页表(如临时内核页表,包括临时pgd,临时pt)和用于存放
动态数据结构的共128kb大小的空间,为简单起见,我们假设他们能容纳于RAM 前8MB空间里。
临时页全局目录是在内核编译过程中静态初始化的,被放在swapper_pg_dir数组变量中,临时页表由定义于arch/i386/kernel/head.S的startup_32汇编语言函数初始化,被放在pg0数组变量中。分页第一个阶段的目标是允许在实模式下和保护模式(见前面的释疑,准确地说是允许在
保护模式的段式寻址方式和保护模式的页式寻址方式)都能很容易地对这8MB寻址。
阅读(2763) | 评论(1) | 转发(0) |