逻辑地址(logical address)
指定一个操作数或一条指令的地址。每个逻辑地址,都由一个段和偏移量组成,偏移量指明了段开始的地方到实际地址之间的距离。
线性地址(linear addres)也称为虚拟地址(virtual address)
是一个32位无符号整数,表达高达4GB的地址。
物理地址(physical address)
用于内存芯片级内存单元寻址。
逻辑地址------------>(分段单元)-------->线性地址-------------->(分页单元)---------------->物理地址
内存控制单元(MMU)通过分段和分页两个硬件电路,转换地址。
在多处理器系统中,所有CPU都共享同一个内存。RAM芯片在读或写操作必须串行的执行,所以必须有一个
内存仲裁器的硬件电路在总线和每个RAM芯片之间。
即使在单处理器上也使用内存仲裁器,因为单处理器系统包含一个DMA控制器的特殊处理器,DMA控制器与
CPU并发操作。
硬件中的分段
从80286开始,Intel处理器以两种不同的方式执行地址转换,分别是实模式和保护模式。
段选择符和段寄存器
逻辑地址由两部门组成:
1.短标识符(16位长的字段,也叫段选择符) 2.指定段内相对地址的偏移量(32位长的字段)
为了快速方便找到段选择符,处理器提供了段寄存器,段寄存器的唯一目的是存放段选择符。
只有6个段寄存器:
1.cs 代码段寄存器,指向包含程序指令的段。还有一个重要功能,包含一个两位的字段,指明CPU的当前特权级(current privilege Level, CPL),0为最高,3为最低,只有0,3分别是内核与用户态。
2.ss 栈段寄存器,指向包含当前程序栈的段
3.ds 数据段寄存器,指向包含静态数据或全局数据段
4.es
5.fs
6.gs
段描述符
每个段由一个8字节的段描述符表示,段描述符放在全局描述符表(GDT)和局部描述符表(LDT)中。
通常只有一个GDT,每个进程除了存放在GDT中的段之外如果还需要创建附加的段,就可以有自己的LDT。
GDT在主存中的地址和大小存放在gdtr控制寄存器中,当前正被使用的LDT地址和大小存放在ldtr控制寄存器中。
代码段描述符:
表示这个段描述符代表一个代码段,可以放在GDT或LDT中,S标志位1(非系统段)
数据段描述符:
代表一个数据段,S标志为1,栈段通过一般的数据段实现。
任务状态段描述符:
代表一个任务状态段(Task State Segment,TSS),保存处理器寄存器的内容。只出现在GDT中。
局部描述符表描述符(LDTD)
代表一个包含LDT的段,只出现在GDT中。
快速访问段描述符
每个段选择符被装入段寄存器时,相应的段描述符就由内存装入到对应的非编承CPU寄存器。处理器只需要直接引用
存放段描述符的CPU寄存器即可,仅当段寄存器的内容改变时,才有必要访问GDT或LDT。
段选择符字段
index:指定放在GDT或LDT中的相应段描述符的入口
TI:0代表在GDT,1代表在LDT
RPL:请求者特权:
分段单元
逻辑地址怎样转换为相应的线性地址:
1.先检查段选择符的TI字段,决定段描述符保存在GDT还是LDT。
2.index字段的值乘以8,与gdtr或ldtr的内容相加。
3.把逻辑地址的偏移量与段描述符Base字段的值相加得到线性地址。
linux中的分段
分段可以给每一个进程分配不同的线性地址空间,而分页可以把同一个线性地址空间映射到不同的物理空间。
linux更喜欢使用分页方式,因为:
1.当所有进程使用相同的段寄存器值时,内存管理变得简单,它们能共享同样的一组线性地址。
2.RISC体系结构对分段的支持很有限。
2.6版本的linux只有在80x86结构下才需要使用分段。
用户态的所有linux进程,使用一对相同的段来对指令和地址寻址,就是用户代码段和用户数据段。
同样内核也有相应的内核代码段和内核数据段。
相应的段选择符由宏__USER_CS, __USER_DS, __KERNEL_CS, 和__KERNEL_DS定义。
所有段都从0x00000000开始,得出一个重要结论,linux下逻辑地址与线性地址是一致的,即逻辑地址
的偏移量字段的值与相应的线性地址的值总是一致的。
linux GDT
在单处理器系统中只有1个GDT,而在多处理器系统中每个CPU对应一个GDT。所有的GDT都存在cpu_gdt_table数组中,所有GDT的地址和他们的大小存放在cpu_gdt_descr数组,定义在arch/i386/kernel/head.S中。每个GDT包含18个段描述符和14个空的,未使用,或保留的项。
18个段:
1.用户态和内核态的代码段和数据段4个
2.任务状态段(TSS),每个处理器1个,所有的TSS都顺序的存放在init_tss数组中,不允许用户态下的进程访问TSS段。
3.1个包含缺省局部描述符表的段,通常被所有进程共享的段。
4.3个局部线程存储(Thread-Local Storage TLS)段,这样允许多线程应用程序使用最多3个局部于线程的数据段。系统调用set_thread_area()和get_thread_area()分别为正在执行的进程创建和撤销一个TLS段。
5.与高级电源管理(APM)相关的3个段,
6.与支持即插即用(PnP)功能的BIOS服务程序相关的5个段,当PnP设备驱动程序调用BIOS函数来检测PnP设备使用的资源时,可以使用自定义的代码段和数据段。
7.内核用来处理“双重错误”异常的特殊TSS段。(双重错误,就是处理一个异常时,可能引发另一个异常)
Linux LDT
大多数用户态下的linux程序不使用局部描述符表,内核定义一个缺省的LDT供大多数进程共享。存放在default_ldt(arch/i386/kernel/traps.c)数组中。内核只有效地使用了其中的两项:用于iBCS执行文件的调用门和Solaris/x86可执行文件的调用门。modify_ldt()系统调用允许进程创建自己的局部描述符表。
硬件中的分页
分页单元把线性地址转换为物理地址。关键任务是把所请求的访问类型与线性地址的访问权限相比较。如果内存访问是无效的,产生一个缺页异常。
线性地址被分成以固定长度为单位的组,称为页。页内部连续的线性地址被映射到连续的物理地址。内核只需要指定一个页的物理地址和其存取权限。分页单元把所有的RAM分成固定长度的页框(page frame)有时叫做物理页。每个页框包含一个页,一个页框的长度与一个页的长度一致。区分页和页框很重要,前者只是一个数据块,可以存放在任何页框和磁盘中。
把线性地址映射到物理地址的数据结构称为页表(page table)。
从80386开始,所有的80x86处理器都支持分页,通过设置cr0寄存器的PG标志启用。PG=0时,线性地址被解释成物理地址。
常规分页
32位线性地址被分成3个域:
Directory(目录):最高10位
Table(页表):中间10位
offset(偏移量):最低12位
线性地址转换为两步完成,第一步转换表称为页目录表,第二步转换表为页表。使用二级模式的目的在于减少每个进程页表所需的RAM数量,二级模式通过只为进程实际使用的那些虚拟内存区请求页表来减少内存容量。
每个活动进程必须有一个分配给她的页目录,没有必要马上为进程的所有页表都分配RAM。正在使用的页目录的物理地址存放在控制寄存器cr3中。
页目录项和页表项有同样的结构,包含下面的字段:
1.Present标志: 1表示在主存中,0表示不在。如果执行一个地址转换所需的页表项中Present被清0,那么分页单元就把线性地址存放在控制寄存器cr2中,并产生14号异常,缺页异常。
2.包含页框物理地址最高20位的字段:由于页框是4KB的容量,物理地址必须是4096的倍数,所以物理地址的最低12位总是0.
3.Acessed标志:分页单元对相应页框进行寻址时就撒 设置这个标志。
4.Dirty标志:只应用于页表项。每当对一个页框进行写操作时就设置这个标志。
5.Read/Write标志:含有页或页表的存取权限。
6.User/Supervisor标志:含有特权级。
7.PCD和PWT标志:控制硬件高速缓存处理页或页表的方式。
8.Page Size标志:只应用于页目录项如果为1,则页目录项指的是2MB或4MB的页框。
9.Global标志:只应用于页表项。防止常用页从TLB高速缓存中刷新出去。
扩展分页
80x86微处理器引入了扩展分页(extened paging),允许页框大小为4MB而不是4KB。扩展分页 用于把大段连续的线性地址转换成相应的物理地址。这里可以不用中间页表项进行转换。
在这种情况下,32位线性地址分成两个字段:
Directory:最高10位
offset:其余22位。
这时候Page size标志必须被设置。这个时候,物理地址的最低22位为0. 通过设置cr4控制寄存器的PSE标志能使扩展分页与常规分页共存。
硬件保护方案
与段的3中存取权限(读,写,执行)不同,页的存取权限只有两种(读,写)。
常规分页举例
例如内核已给一个正在运行的进程分配的线性地址范围是0x20000000到0x2003ffff。最高10位值为0x080或128,则页目录的129项外其他1023项全部填0,中间10位0x03f范围是0到63,只有这个64个表项有意义,其余960都填0.页表的索引范围就在0到0xfff之间。无论何时,当进程试图访问限定在0x20000000到0x2003ffff之外的线性地址时,都将产生一个缺页异常。
物理地址扩展分页机制(PAE)
处理器所支持的RAM容量受链接到在地址总线上的地址管脚数限制。Intel通过在处理器上把管教数从32增加到36,寻址能力达到64GB。但是,需要引入一种新的分页机制把32位线性地址转换为36位物理地址才能使用所增加的物理地址。
在Pentium Pro处理器开始,Intel引入一种物理地址扩展的机制(Phsical Address Extension, PAE),另外一种页大小扩展(Page Size Extension,PSE) 的机制在Pentium III处理器引入。但是linux没有采用这中机制。
通过设置cr4控制寄存器中的物理地址扩展(PAE)标志激活PAE,页目录项中的页大小标志PS启用大尺寸页(在PAE启用时为2MB)。
Intel为了支持PAE已经改变了分页机制:
1.64GB的RAM被分为16M个页框。
2.引入一个叫做页目录指针表(Page Directory Pointer Table, PDPT)的页表新级别。
3.cr3控制寄存器包含一个27位的页目录指针表基地址字段。
4.当把线性地址映射到4KB的页时,32位线性地址按下列方式解释:
cr3:指向一个PDPT
位31-30:指向PDPT中4个项中的一个
位29-21:指向页目录中512个项中的一个
位20-12:窒息那个页表中512项中的一个
位11-0:4KB页中的偏移量
5.当把线性地址映射到2MB的页时,按如下解释:
cr3:指向一个PDPT
位31-30:指向PDPT中4个项中的一个
位29-21:指向页目录中512个项中的一个
位20-0:2MB页中的偏移量
启用PAE的主要问题是线性地址仍然是32位长。必须用同一个线性地址映射不同的RAM区。
64位系统中的分页
硬件高速缓存
处理器的时钟频率接近几个GHZ,动态RAM芯片的存取时间是时钟周期的数百倍,所以,RAM操作时,CPU可能要等待很长时间。所以,引入了硬件高速缓存内存(hardware cache memory)。它基于著名的局部性原理。80x86体系结构引入了一个叫做行的新单元,以脉冲突发模式在慢速的DRAM和实现高速缓存的SRAM之间传送,实现高速缓存。高速缓存(SRAM)插在分页单元和主内存(DRAM)之间。
高速缓存控制器存放一个表项数组,每个表项对应高速缓存内存中的一个行(line),还有一个标签(tag)和描述高速缓存行状态的几个标志(flag)。这种物理地址通常分为3组:最高几位对应标签,中间几位对应高速 缓存控制器的子集索引,最低几位对应行内的偏移量。
对于写操作,控制器可能采用两个基本策略之一:通写(write-through)和回写(write-back)。通写总是既写RAM也写高速缓存,为了提高写操作的效率关闭高速缓存。回写方式只更新高速缓存,提供了更快的功效。只有当CPU执行一条刷新高速缓存表项的指令时,或者当一个FLUSH硬件信号产生时,才把高速缓存写回到RAM中。
多处理器系统,每一个处理器都有一个单独的硬件高速缓存,需要额外的硬件电路用于保持高速缓存内容的同步。一个CPU修改,必须通知另一个CPU也进行更新。这种叫做高速缓存侦听。
处理器的cr0寄存器的CD标志位用来启用或禁用高速缓存电路。NW标志指明使用通写还是 回写。
Pentium处理器高速缓存还有一个特点,把不同的高速缓存策略与每一个页框关联。每个页目录项和每一个页表项都包含两个标志。PCD,(page cache disablt)指明访问包含在这个页框中的数据时,必须启用或禁用高速缓存。PWT,必须使用通写或是回写策略。linux清除所有的PCD和PWT标志,即所有页框启用高速缓存,对于写操作总是采用回写策略。
转换后援缓冲器(TLB)
80x86还包含另一个成为转换后援缓冲器TLB的高速缓存用于加快线性地址的转换。在多处理器系统中,每个CPU都有自己的TLB,TLB中的对应项不必同步,因为现有CPU上的进程可以使用同一线性地址与不同的物理地址发生联系。CPU的cr3控制寄存器被修改时,自动使本地TLB中的所有项都无效。
linux中的分页
从2.6.11版本开始,采用了四级分页模型: 页全局目录(Page Global Directory),页上级目录(Page Upper Directory),页中间目录(Page Middle Directory), 页表(Page Table)。线性地址因此被划为五个部分,没有位数,因为每一部分的大小与具体的计算机体系结构相关。
linux的进程处理很大程度上依赖于分页。
当发生进程切换时,linux把cr3控制寄存器的内容保存在前一个执行进程的描述符中,然后把下一个进程的描述符的值装入cr3寄存器中。
线性地址字段
PAGE_SHIFT: 指定offset字段的位数,这个宏由PAGE_SIZE使用以返回页的大小,最后,PAGE_MASK产生的值位0xfffff000,用以屏蔽Offset字段的所有位。
PMD_SHIFT: 指定线性地址的offset字段和table字段的总位数。
PUD_SHIFT: 指定页上级目录项能映射的区域大小的对数。在80x86上,PUD_SHIFT总是等价于PMD_SHIFT。
PGDIR_SHIFT: 确定页全局目录项能映射的区域大小的对数。
PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD, PTRS_PER_PGD: 用于计算页表,页中间目录,页上级目录和页全局目录中表项的个数。
页表处理
pte_t, pmd_t, pud_t, pgd_t分别描述页表项、页中间目录项、页上级目录和页全局目录项的格式。
五个类型转换宏(__pte, __pmd, __pud, __pgd, __pgprot)把一个无符号整数转换成所需的类型。另外五个类型转换宏(pte_val, pmd_val, pud_val, pgd_val, pgprot_val)执行相反的转换。
内核还提供了许多宏和函数用于读和修改页表项:
1.pte_none, pmd_none, pud_none, pgd_none,如果为0, 则返回1.
2.pte_clear, pmd_clear, pud_clear, pgd_clear清楚相应表的一个表项。
3.set_pte, set_pmd, set_pud, set_pgd向一个表项写入指定的值。
4.pte_same(a,b)如果a和b两个页表项指向同一页并且指定同样的访问优先级,则返回1.
5.pmd_large(e),如果页中间目录项指向一个大型页,则返回1.
读页标志的函数
pte_user(): 读User/Supervisor标志
pte_read(): 读User/Supervisor标志(表示80x86处理器上的页不受读的保护)。
pte_write(); 读Read/Write标志
pte_exec(): 读 User/Supervisor标志(不受执行的保护)。
pte_dirty(): 读Dirty标志
pte_young(): 读Accessed标志
pte_file(): 读dirty标志,
设置页标志的函数
mk_pte_huge(): 设置页表项的PS和Present标志
pte_wrprotect(): 清除Read/Write标志
pte_rdprotect(): 清除User/Supervisor标志
pte_exprotect(): 清除User/Supervisor标志
物理内存布局
- 不可用的物理地址范围内的页框
- 含有内核代码和已初始化的数据结构的页框
- 页框0由BIOS使用,存放加电自检期间检查到的系统硬件配置。
- 物理地址从0x000a000到0x000fffff的范围通常留给BIOS例程,并且映射到ISA图形卡上的内部内存。
- 第一个MB内的其他页框可能由特定计算机模型保留。
内核执行machine_specific_memory_setup函数,建立物理地址映射。setup_memory()在之后执行,分析物理内存区域表并初始化一些变量来描述内核的物理内存布局。
进程页表
进程的线性地址分成俩部分:
- 从0x00000000 到0xbfffffff的线性地址,内核和用户态都可以寻址的。
- 从0xc0000000 到0xffffffff的线性地址,只有内核态的进程可以寻址。
宏PAGE_OFFSET产生的值是0xc0000000,这就是内核生存的开始之处。
内核页表
内核维持一组自己的页表,驻留在主内核全局目录中。系统初始化后,这组页表未被任何进程或内核线程所使用。
内核初始化自己的页表,分为两个阶段:
- 内核创建一个有限的地址空间,包括内核的代码段和数据段、初始页表和用于存放动态数据结构的共128KB大小的空间。
- 内核充分利用剩余的RAM并适当建立分页表。
临时页全局目录是在内核编译过程中静态初始化的,而临时页表由startup_32()汇编语言函数初始化的。定义于arch/i386/boot/compressed/head.S。
临时页全局目录放在swapper_pg_dir变量中,为了映射RAM前8MB的空间,需要用到两个页表。内核必须创建一个映射,把线性地址0x00000000到0x007fffff的线性地址和从0xc0000000到0xc07ffffff到从0x00000000到0x007fffff的物理地址。
内核通过把swapper_pg_dir所有项都填充为0来创建期望的映射。
当RAM小于896MB时的最终内核页表
宏__pa用于把从PAGE_OFFSET开始的线性地址转换成相应的物理地址,而宏__va做相反的转化。
主内核页全局目录仍然保存在swapper_pg_dir变量中,由paging_init()函数初始化,函数操作如下:
- 调用pagetable_init()适当地建立页表项
- 把swapper_pg_dir的物理地址写入cr3控制寄存器
- 如果CPU支持PAE并且如果内核编译时支持PAE,则将cr4控制寄存器的PAE标志置位。
- 调用__flush_tlb_all()使TLB的所有项无效。
当RAM大小在896MB和4096MB之间时的最终内核页表
这种情况下,并不把RAM全部映射到内核地址空间。
当RAM大于4096MB时的最终内核页表
- CPU模型支持物理地址扩展(PAE)
- RAM容量大于4GB
- 内核以PAE支持来编译