zz : http://blog.csdn.net/yunsongice/article/details/5220724
1 基于80x86的Linux分段机制
80386的两种工作模式:80386的工作模式包括实地址模式和虚地址模式(保护模式)。Linux主要工作在保护模式下。
在保护模式下,80386虚地址空间可达16K个段,每段大小可变,最大达4GB。逻辑地址到线性地址的转换由80386分段机制管理。段寄存器CS、DS、ES、SS、FS或GS各标识一个段。这些段寄存器作为段选择器,用来选择该段的描述符。
分段逻辑地址到线性地址转换图:
Linux对80386的分段机制使用得很有限,因为Linux的设计目标是支持绝大多数主流的CPU,而很多CPU使用的是RISC体系结构,并没有分段机制,所以2.6版内核只有在80x86结构下才使用分段,而且只是象征性地使用了一下:
所有Linux进程仅仅使用四种段来对指令和数据寻址。运行在用户态的进程使用所谓的用户代码段和用户数据段。类似地,运行在内核态的所有Linux进程都使用一对相同的段对指令和数据寻址:它们分别叫做内核代码段和内核数据段。下表显示了这四个重要段的段描述符字段的值:
段 | Base | G | Limit | S | Type | DPL | D/B | P |
用户代码段 | 0x00000000 | 1 | 0xfffff | 1 | 10 | 3 | 1 | 1 |
用户数据段 | 0x00000000 | 1 | 0xfffff | 1 | 2 | 3 | 1 | 1 |
内核代码段 | 0x00000000 | 1 | 0xfffff | 1 | 10 | 0 | 1 | 1 |
内核数据段 | 0x00000000 | 1 | 0xfffff | 1 | 2 | 0 | 1 | 1 |
相应的段描述符由宏__USER_CS,__USER_DS,__KERNEL_CS,和__KERNEL_DS分别定义。例如,为了对内核代码段寻址,内核只需要把这个宏产生的值装进cs段寄存器即可。 注意,与段相关的线性地址从0开始,达到232 -1的寻址限长。这就意味着在用户态或内核态下的所有进程可以使用相同的逻辑地址。所有段都从0x00000000开始,这可以得出另一个重要结论,那就是在Linux下逻辑地址与线性地址是一致的,即逻辑地址的偏移量字段的值与相应的线性地址的值总是一致的。
如前所述,CPU的当前特权级(CPL)反映了进程是在用户态还是内核态,并由存放在cs寄存器中的段选择符的RPL字段指定。只要当前特权级被改变,一些段寄存器必须相应地更新。例如,当CPL=3时(用户态),ds寄存器必须含有用户数据段的段选择符,而当CPL=0时,ds寄存器必须含有内核数据段的段选择符。
类似的情况也出现在ss寄存器中。当CPL为3时,它必须指向一个用户数据段中的用户栈,而当CPL为0时,它必须指向内核数据段中的一个内核栈。当从用户态切换到内核态时,Linux总是确保ss寄存器装有内核数据段的段选择符。
当对指向指令或者数据结构的指针进行保存时,内核根本不需要为其设置逻辑地址的段选择符,因为cs寄存器就含有当前的段选择符。例如,当内核调用一个函数时,它执行一条call汇编语言指令,该指令仅指定它逻辑地址的偏移量部分,而段选择符不用设置,其隐含在cs寄存器中了。因为“在内核态执行” 的段只有一种,叫做代码段,由宏_KERNEL_CS定义,所以只要当CPU切换入内核态时足可以将__KERNEL_CS装载入cs。同样的道理也适用于指向内核数据结构的指针(隐含地使用ds寄存器)以及指向用户数据结构的指针(内核显式地使用es寄存器)。
2 基于80x86的Linux分页机制
Linux分页机制的作用:分页机制是在段机制之后进行的,它进一步将线性地址转换为物理地址。我们先来看看硬件构造:
80386使用4K字节大小的页,且每页的起始地址都被4K整除。因此,早期80386把4GB字节线性地址空间划分为1M个页面,采用了两级表结构。
两级表的第一级表称为页目录,存储在一个4K字节的页中,页目录表共有1K个表项,每个表项为4个字节,线性地址最高的10位(22-31)用来产生第一级表索引,由该索引得到的表项中的内容定位了二级表中的一个表的地址,即下级页表所在的内存块号。
第二级表称为页表,存储在一个4K字节页中,它包含了1K字节的表项,每个表项包含了一个页的物理地址。二级页表由线性地址的中间10位(12-21)位进行索引,定位页表表项,获得页的物理地址。页物理地址的高20位与线性地址的低12位形成最后的物理地址。
利用两级页表转换地址图:
80x86的分页机制由CR0中的PG位启用。如PG=1,启用分页机制,并使用本节要描述的机制,把线性地址转换为物理地址。如PG=0,禁用分页机制,直接把前面段机制产生的线性地址当作物理地址使用。
80386使用4K字节大小的页。每一页都有4K字节长,并在4K字节的边界上对齐,即每一页的起始地址都能被4K整除(物理地址最低12位为0)。因此,80386把4G字节的线性地址空间,划分为1G个页面,每页有4K字节大小。
分页机制通过把线性地址空间中的页,重新定位到物理地址空间来进行管理,因为每个页面的整个4K字节作为一个单位进行映射,并且每个页面都对齐4K字节的边界,因此,线性地址的低12位经过分页机制直接地作为物理地址的低12位使用。
线性/物理地址的转换,可将其意义扩展为允许将一个线性地址标记为无效,而不是实际地产生一个物理地址。有两种情况可能使页被标记为无效:其一是线性地址是操作系统不支持的地址;其二是在虚拟存储器系统中,线性地址对应的页存储在磁盘上,而不是存储在RAM存储器中。在前一种情况下,程序因产生了无效地址而必须被终止。
对于后一种情况,该无效的地址实际上是请求操作系统的虚拟存储管理系统,把存放在磁盘上的页传送到物理存储器中,使该页能被程序所访问。由于无效页通常是与虚拟存储系统相联系的,这样的无效页通常称为未驻留页,并且用页表属性位中叫做存在位的属性位进行标识。未驻留页是程序可访问的页,但它不在主存储器中。对这样的页进行访问,形式上是发生异常,实际上是通过异常进行缺页处理。
2.1 页全局目录
页全局目录表,最多可包含1024个页目录项,每个页目录项为4个字节,算起来正好一个页面,结构如图所示:
·第0位是存在位,Present标志:如果被置为1,所指的页(或页表)就在主存中;如果该标志为0,则这一页不在主存中,此时这个表项剩余的位可由操作系统用于自己的目的。如果执行一个地址转换所需的页表项或页目录项中Present标志被清0,那么分页单元就把该线性地址存放在控制寄存器cr2中,并产生14号异常:缺页异常。(我们将在后面的一系列博客中重点讨论Linux如何使用这个字段)。
·第1位是读/写位,第2位是用户/管理员位,Read/Write标志:含有页或页表的存取权限(Read/Write或Read);User/Supervisor标志:含有访问页或页表所需的特权级。这两位为页目录项提供硬件保护。当特权级为3的进程要想访问页面时,需要通过页保护检查,而特权级为0的进程就可以绕过页保护,如图所示:
·第3位是PWT(Page Write-Through)位,表示是否采用写透方式,写透方式就是既写内存(RAM)也写高速缓存,该位为1表示采用写透方式
·第4位是PCD(Page Cache Disable)位,表示是否启用高速缓存,该位为1表示启用高速缓存。
·第5位是访问位,Accessed标志:当对页目录项进行访问时,A位=1。每当分页单元对相应页框进行寻址时就设置这个标志。当选中的页被交换出去时,这一标志就可以由操作系统使用。分页单元从不重置这个标志;而是必须由操作系统去做。
·第6位Dirty标志,对于页全局目录项,其始终为1。
·第7位是Page Size标志,只适用于页目录项。如果置为1,页目录项指的是4MB的页面,请看后面的扩展分页。
·第8位是Global 标志:只应用于页表项。这个标志是在Pentium Pro引入的,用来防止常用页从TLB高速缓存中刷新出去。只有在cr4寄存器的页全局启用(Page GlobalEnable ,PGE)标志置位时这个标志才起作用。
·第9~11位由操作系统专用,Linux也没有做特殊之用
2.2 页表
80386的每个页目录项指向一个页表,页表最多含有1024个页面项,每项4个字节,包含页面的起始地址和有关该页面的信息。页面的起始地址也是4K的整数倍,所以页面的低12位也留作它用,如图所示。
第31~12位是20位物理页面地址,除第6位外第0~5位及9~11位的用途和页目录项一样,第6位是页面项独有的,当对涉及的页面进行写操作时,D位被置1。
4GB的存储器只有一个页目录,它最多有1024个页目录项,每个页目录项又含有1024个页面项,因此,存储器一共可以分成1024×1024=1M个页面。由于每个页面为4K个字节,所以,存储器的大小正好最多为4GB。
2.3 线性地址到物理地址的转换
当访问一个操作单元时,如何由分段结构确定的32位线性地址通过分页操作转化成32位物理地址呢?过程如图所示。
第一步,CR3包含着页目录的起始地址,用32位线性地址的最高10位A31~A22作为页目录的页目录项的索引,将它乘以4,与CR3中的页目录的起始地址相加,形成相应页表的地址。
第二步,从指定的地址中取出32位页目录项,它的低12位为0,这32位是页表的起始地址。用32位线性地址中的A21~A12位作为页表中的页面的索引,将它乘以4,与页表的起始地址相加,形成32位页面地址。
第三步,将A11~A0作为相对于页面地址的偏移量,与32位页面地址相加,形成32位物理地址。
下面,我们就通过一个实例来介绍一下常规分页是如何工作的。我们假定内核已给一个正在运行的进程分配的线性地址空间范围是0x20000000 到 0x2003ffff(3GB线性地址空间是一个上限,用户态进程只是引用其中的一个子集)。这个空间正好由64页面组成。其实我们并不必关心包含这些页的页框的物理地址,为什么呢?事实上,其中的一些页甚至可能不在主存中。我们只关注页表项中剩余的字段。
让我们从分配给进程的线性地址的最高10位开始。这两个地址都以2开头后面跟着0,因此高10位有相同的值,即0x080或十进制的128。因此,这两个地址的页目录(Directory字段)都指向进程页目录的第129项。相应的目录项中必须包含分配给该进程的页表的物理地址。如果没有给这个进程分配其它的线性地址,页目录的其余1023项都填为0。
中间10位的值(即Table字段的值)范围从0到0x03f,或十进制的从0到63。因而只有页表的前64个表项是有意义的,其余960表项都填0。
假设进程需要读线性地址0x20021406中的字节。这个地址由分页单元按下面的方法处理:
1. Directory字段的0x80用于选择页目录的第0x80目录项,此目录项指向和该进程的页相关的页表。
2. Table字段0x21用于选择页表的第0x21表项,此表项指向包含所需页的页框。
3. 最后,Offet字段0x406用于在目标页框中读偏移量为0x406中的字节。
如果页表第0x21表项的Present标志为0,则此页就不在主存中;在这种情况下,分页单元在线性地址转换的同时产生一个缺页异常。无论何时,当进程试图访问限定在0x20000000到0x2003ffff范围之外的线性地址时,都将产生一个缺页异常,因为这些页表项都填充了0,尤其是它们的Present标志都被清0。
当今,Linux采用了一种同时适用于32位和64位系统的普通分页模型。前面我们看到,两级页表对32位系统来说已经足够了,但64位系统需要更多数量的分页级别。直到2.6.10版本,Linux采用三级分页的模型。从2.6.11版本开始,采用了四级分页模型:
图中展示的4种页表分别被称作:
• 页全局目录(Page Global Directory)
• 页上级目录(Page Upper Directory)
• 页中间目录(Page Middle Directory)
• 页表(Page Table)
页全局目录包含若干页上级目录的地址,页上级目录又依次包含若干页中间目录的地址,而页中间目录又包含若干页表的地址。每一个页表项指向一个页框。线性地址因此被分成五个部分。图中没有显示位数,因为每一部分的大小与具体的计算机体系结构有关。
对于没有启用物理地址扩展的32位系统,两级页表已经足够了。从本质上说Linux通过使“页上级目录”位和“页中间目录”位全为0,彻底取消了页上级目录和页中间目录字段。不过,页上级目录和页中间目录在指针序列中的位置被保留,以便同样的代码在32位系统和64位系统下都能使用。内核为页上级目录和页中间目录保留了一个位置,这是通过把它们的页目录项数设置为1,并把这两个目录项映射到页全局目录的一个合适的目录项而实现的。
启用了物理地址扩展的32 位系统使用了三级页表。Linux 的页全局目录对应80x86 的页目录指针表(PDPT),取消了页上级目录,页中间目录对应80x86的页目录,Linux的页表对应80x86的页表。
最终,64位系统使用三级还是四级分页取决于硬件对线性地址的位的划分。
那么,为什么Linux是如此地热衷使用分页技术而对分段机制表现得那么地冷淡呢,因为Linux的进程处理很大程度上依赖于分页。事实上,线性地址到物理地址的自动转换使下面的设计目标变得可行:
• 给每一个进程分配一块不同的物理地址空间,这确保了可以有效地防止寻址错误。
• 区别页(即一组数据)和页框(即主存中的物理地址)之不同。这就允许存放在某个页框中的一个页,然后保存到磁盘上,以后重新装入这同一页时又被装在不同的页框中。这就是虚拟内存机制的基本要素。
每一个进程有它自己的页全局目录和自己的页表集。当发生进程切换时,Linux把cr3控制寄存器的内容保存在前一个执行进程的描述符中,然后把下一个要执行进程的描述符的值装入cr3寄存器中。因此,当新进程重新开始在CPU上执行时,分页单元指向一组正确的页表。
把线性地址映射到物理地址虽然有点复杂,但现在已经成了一种机械式的任务。本章下面的几节中列举了一些比较单调乏味的函数和宏,它们检索内核为了查找地址和管理叶表所需的信息;其中大多数函数只有一两行。也许现在你就想跳过这部分,但是知道这些函数和宏的功能是非常有用的,因为在以后章节的讨论中你会经常看到它们。
2.4 线性地址字段处理
下列宏简化了页表处理:
PAGE_SHIFT
指定Offset字段的位数;当用于80x86处理器时,它返回的值为12。由于页内所有地址都必须放在Offset字段, 因此80x86系统的页的大小是212 =4096字节。 PAGE_MASK宏产生的值为0xfffff000,用以屏蔽Offset字段的所有位。
PMD_SHIFT
指定线性地址的Offset和Table字段的总位数;换句话说,是页中间目录项可以映射的区域大小的对数。PMD_SIZE 宏用于计算由页中间目录的一个单独表项所映射的区域大小,也就是一个页表的大小。PMD_MASK宏用于屏蔽Offset字段与Table字段的所有位。当PAE 被禁用时,PMD_SHIFT 产生的值为22(来自Offset 的12 位加上来自Table 的10 位),PMD_SIZE 产生的值为222 或 4 MB, PMD_MASK产生的值为 0xffc00000。相反,当PAE被激活时,PMD_SHIFT 产生的值为21 (来自Offset的12位加上来自Table的9位), PMD_SIZE 产生的值为221 或2 MB以及PMD_MASK产生的值为 0xffe00000。大型页不使用最后一级页表,所以产生大型页尺寸的LARGE_PAGE_SIZE 宏等于PMD_SIZE(2PMD_SHIFT),而在大型页地址中用于屏蔽Offset字段和Table字段的所有位的LARGE_PAGE_MASK宏,就等于PMD_MASK。
PUD_SHIFT
确定页上级目录项能映射的区域大小的对数。PUD_SIZE宏用于计算页全局目录中的一个单独表项所能映射的区域大小。PUD_MASK宏用于屏蔽Offset字段,Table字段,Middle Air字段和Upper Air字段的所有位。在80x86处理器上,PUD_SHIFT总是等价于PMD_SHIFT,而PUD_SIZE则等于4MB或2MB。
PGDIR_SHIFT
确定页全局页目录项能映射的区域大小的对数。 PGDIR_SIZE宏用于计算页全局目录中一个单独表项所能映射区域的大小。PGDIR_MASK宏用于屏蔽Offset, Table,Middle Air及Upper Air的所有位。当PAE 被禁止时,PGDIR_SHIFT 产生的值为22(与PMD_SHIFT 和PUD_SHIFT 产生的值相同),PGDIR_SIZE 产生的值为 222 或 4 MB,以及 PGDIR_MASK 产生的值为 0xffc00000。相反,当PAE被激活时,PGDIR_SHIFT 产生的值为30 (12 位Offset 加 9 位Table再加 9位 Middle Air), PGDIR_SIZE 产生的值为230 或 1 GB以及PGDIR_MASK产生的值为0xc0000000。
PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD以及PTRS_PER_PGD
用于计算页表、页中间目录、页上级目录和页全局目录表中表项的个数。当PAE被禁止时,它们产生的值分别为1024,1,1和1024。当PAE被激活时,产生的值分别为512,512,1和4。
2.5 页表处理
pte_t、pmd_t、pud_t和 pgd_t分别描述页表项、页中间目录项、页上级目录和页全局目录项的类型格式。当PAE被激活时它们都是64位的数据类型,否则都是32位数据类型。pgprot_t是另一个64位(PAE激活时)或32位(PAE禁用时)的数据类型,它表示与一个单独表项相关的保护标志。
五个类型转换宏(__ pte、__ pmd、__ pud、__ pgd和__ pgprot)把一个无符号整数转换成所需的类型。另外的五个类型转换宏(pte_val,pmd_val, pud_val, pgd_val和pgprot_val)执行相反的转换,即把上面提到的四种特殊的类型转换成一个无符号整数。
内核还提供了许多宏和函数用于读或修改页表表项:
• 如果相应的表项值为0,那么,宏pte_none、pmd_none、pud_none和 pgd_none产生的值为1,否则产生的值为0。
• 宏pte_clear、pmd_clear、pud_clear和 pgd_clear清除相应页表的一个表项,由此禁止进程使用由该页表项映射的线性地址。ptep_get_and_clear( )函数清除一个页表项并返回前一个值。
• set_pte,set_pmd,set_pud和set_pgd向一个页表项中写入指定的值。set_pte_atomic与set_pte作用相同,但是当PAE被激活时它同样能保证64位的值能被原子地写入。
• 如果a和b两个页表项指向同一页并且指定相同访问优先级,pte_same(a,b)返回1,否则返回0。
• 如果页中间目录项指向一个大型页(2MB或4MB),pmd_large(e)返回1,否则返回0。
宏pmd_bad由函数使用并通过输入参数传递来检查页中间目录项。如果目录项指向一个不能使用的页表,也就是说,如果至少出现以下条件中的一个,则这个宏产生的值为1:
• 页不在主存中(Present标志被清除)。
• 页只允许读访问(Read/Write标志被清除)。
• Acessed或者Dirty位被清除(对于每个现有的页表,Linux总是强制设置这些标志)。
pud_bad宏和pgd_bad宏总是产生0。没有定义pte_bad宏,因为页表项引用一个不在主存中的页,一个不可写的页或一个根本无法访问的页都是合法的。
如果一个页表项的Present标志或者Page Size标志等于1,则pte_present宏产生的值为1,否则为0。前面讲过页表项的Page Size标志对微处理器的分页部件来讲没有意义,然而,对于当前在主存中却又没有读、写或执行权限的页,内核将其Present和Page Size分别标记为0和1。这样,任何试图对此类页的访问都会引起一个缺页异常,因为页的Present标志被清0,而内核可以通过检查Page Size的值来检测到产生异常并不是因为缺页。
如果相应表项的Present标志等于1,也就是说,如果对应的页或页表被装载入主存,pmd_present宏产生的值为1。pud_present宏和pgd_present宏产生的值总是1。
下表中列出的函数用来查询页表项中任意一个标志的当前值;除了pte_file()外,其他函数只有在pte_present返回1的时候,才能正常返回页表项中任意一个标志。
函数名称 | 说明 |
pte_user( ) | 读 User/Supervisor 标志。 |
pte_read( ) | 读 User/Supervisor 标志(表示 80x86 处理器上的页不受读的保护)。 |
pte_write( ) | 读 Read/Write 标志。 |
pte_exec( ) | 读 User/Supervisor 标志( 80x86 处理器上的页不受代码执行的保护)。 |
pte_dirty( ) | 读 Dirty 标志。 |
pte_young( ) | 读 Accessed 标志。 |
pte_file( ) | 读 Dirty 标志(当 Present 标志被清除而 Dirty 标志被设置时,页属于一个非线性磁盘文件映射)。 |
下表列出的另一组函数用于设置页表项中各标志的值:
函数名称 | 说明 |
mk_pte_huge( ) | 设置页表项中的 Page Size 和 Present 标志。 |
pte_wrprotect( ) | 清除 Read/Write 标志。 |
pte_rdprotect( ) | 清除 User/Supervisor 标志。 |
pte_exprotect( ) | 清除 User/Supervisor 标志。 |
pte_mkwrite( ) | 设置 Read/Write 标志。 |
pte_mkread( ) | 设置 User/Supervisor 标志。 |
pte_mkexec( ) | 设置 User/Supervisor 标志。 |
pte_mkclean( ) | 清除 Dirty 标志。 |
pte_mkdirty( ) | 设置 Dirty 标志。 |
pte_mkold( ) | 清除 Accessed 标志(把此页标记为未访问)。 |
pte_mkyoung( ) | 设置 Accessed 标志(把此页标记为访问过)。 |
pte_modify(p,v) | 把页表项 p 的所有访问权限设置为指定的值 v 。 |
ptep_set_wrprotect() | 与 pte_wrprotect( ) 类似,但作用于指向页表项的指针。 |
ptep_set_access_flags( ) | 如果 Dirty 标志被设置为 1 则将页的访问权设置为指定的值,并调用flush_tlb_page() 函数。 |
ptep_mkdirty( ) | 与 pte_mkdirty( ) 类似,但作用于指向页表项的指针。 |
ptep_test_and_clear_dirty( ) | 与 pte_mkclean( ) 类似,但作用于指向页表项的指针并返回 Dirty 标志的旧值。 |
ptep_test_and_clear_young( ) | 与 pte_mkold( ) 类似,但作用于指向页表项的指针并返回 Accessed标志的旧值。 |
现在,我们来讨论下表中列出的宏,它们把一个页地址和一组保护标志组合成页表项,或者执行相反的操作,从一个页表项中提取出页地址。请注意这其中的一些宏对页的引用是通过 “页描述符”的线性地址,而不是通过该页本身的线性地址。
宏名称 | 说明 |
pgd_index(addr) | 找到线性地址 addr 对应的的目录项在页全局目录中的索引(相对位置)。 |
pgd_offset(mm, addr) | 接收内存描述符地址 mm 和线性地址 addr 作为参数。这个宏产生地址addr 在页全局目录中相应表项的线性地址;通过内存描述符 mm 内的一个指针可以找到这个页全局目录。 |
pgd_offset_k(addr) | 产生主内核页全局目录中的某个项的线性地址,该项对应于地址 addr 。 |
pgd_page(pgd) | 通过页全局目录项 pgd 产生页上级目录所在页框的页描述符地址。在两级或三级分页系统中,该宏等价于 pud_page() ,后者应用于页上级目录项。 |
pud_offset(pgd, addr) | 参数为指向页全局目录项的指针 pgd 和线性地址 addr 。这个宏产生页上级目录中目录项 addr 对应的线性地址。在两级或三级分页系统中,该宏产生 pgd ,即一个页全局目录项的地址。 |
pud_page(pud) | 通过页上级目录项 pud 产生相应的页中间目录的线性地址。在两级分页系统中,该宏等价于 pmd_page() ,后者应用于页中间目录项。 |
pmd_index(addr) | 产生线性地址 addr 在页中间目录中所对应目录项的索引(相对位置)。 |
pmd_offset(pud, addr) | 接收指向页上级目录项的指针 pud 和线性地址 addr 作为参数。这个宏产生目录项 addr 在页中间目录中的偏移地址。在两级或三级分页系统中,它产生 pud ,即页全局目录项的地址。 |
pmd_page(pmd) | 通过页中间目录项 pmd 产生相应页表的页描述符地址。在两级或三级分页系统中, pmd 实际上是页全局目录中的一项。 |
mk_pte(p,prot) | 接收页描述符地址 p 和一组访问权限 prot 作为参数,并创建相应的页表项。 |
pte_index(addr) | 产生线性地址 addr 对应的表项在页表中的索引(相对位置)。 |
pte_offset_kernel(dir,addr) | 线性地址 addr 在页中间目录 dir 中有一个对应的项,该宏就产生这个对应项,即页表的线性地址。另外,该宏只在主内核页表上使用。 |
pte_offset_map(dir, addr) | 接收指向一个页中间目录项的指针 dir 和线性地址 addr 作为参数,它产生与线性地址 addr 相对应的页表项的线性地址。如果页表被保存在高端存储器中,那么内核建立一个临时内核映射,并用 pte_unmap 对它进行释放。 pte_offset_map_nested 宏和 pte_unmap_nested 宏是相同的,但它们使用不同的临时内核映射。 |
pte_page( x ) | 返回页表项 x 所引用页的描述符地址。 |
pte_to_pgoff( pte ) | 从一个页表项的 pte 字段内容中提取出文件偏移量,这个偏移量对应着一个非线性文件内存映射所在的页。 |
pgoff_to_pte(offset ) | 为非线性文件内存映射所在的页创建对应页表项的内容。 |
下面我们罗列最后一组函数来简化页表项的创建和撤消。当使用两级页表时,创建或删除一个页中间目录项是不重要的。如本节前部分所述,页中间目录仅含有一个指向下属页表的目录项。所以,页中间目录项只是页全局目录中的一项而已。然而当处理页表时,创建一个页表项可能很复杂,因为包含页表项的那个页表可能就不存在。在这样的情况下,有必要分配一个新页框,把它填写为 0 ,并把这个表项加入。
如果 PAE 被激活,内核使用三级页表。当内核创建一个新的页全局目录时,同时也分配四个相应的页中间目录;只有当父页全局目录被释放时,这四个页中间目录才得以释放。当使用两级或三级分页时,页上级目录项总是被映射为页全局目录中的一个单独项。与以往一样,下表中列出的函数描述是针对 80x86 构架的。
函数名称 | 说明 |
pgd_alloc( mm ) | 分配一个新的页全局目录。如果 PAE 被激活,它还分配三个对应用户态线性地址的子页中间目录。参数 mm( 内存描述符的地址 )在 80x86 构架上被忽略。 |
pgd_free( pgd) | 释放页全局目录中地址为 pgd 的项。如果 PAE 被激活,它还将释放用户态线性地址对应的三个页中间目录。 |
pud_alloc(mm, pgd, addr) | 在两级或三级分页系统下,这个函数什么也不做:它仅仅返回页全局目录项 pgd 的线性地址。 |
pud_free(x) | 在两级或三级分页系统下,这个宏什么也不做。 |
pmd_alloc(mm, pud, addr)
| 定义这个函数以使普通三级分页系统可以为线性地址 addr 分配一个新的页中间目录。如果 PAE 未被激活,这个函数只是返回输入参数 pud 的值,也就是说,返回页全局目录中目录项的地址。如果 PAE 被激活,该函数返回线性地址 addr 对应的页中间目录项的线性地址。参数 mm 被忽略。 |
pmd_free(x) | 该函数什么也不做,因为页中间目录的分配和释放是随同它们的父全局目录一同进行的。 |
pte_alloc_map(mm, pmd, addr) | 接收页中间目录项的地址 pmd 和线性地址 addr 作为参数,并返回与 addr 对应的页表项的地址。如果页中间目录项为空,该函数通过调用函数 pte_alloc_one( ) 分配一个新页表。如果分配了一个新页表, addr 对应的项就被创建,同时 User/Supervisor 标志被设置为 1 。如果页表被保存在高端内存,则内核建立一个临时内核映射,并用 pte_unmap 对它进行释放。 |
pte_alloc_kernel(mm, pmd, addr) | 如果与地址 addr 相关的页中间目录项 pmd 为空,该函数分配一个新页表。然后返回与 addr 相关的页表项的线性地址。该函数仅被主内核页表使用。 |
pte_free(pte) | 释放与页描述符指针 pte 相关的页表。 |
pte_free_kernel(pte) | 等价于 pte_free( ) ,但由主内核页表使用。 |
clear_page_range(mmu, start,end) | 从线性地址 start 到 end 通过反复释放页表和清除页中间目录项来清除进程页表的内容。 |
3 扩展分页
从奔腾处理器开始,Intel微处理器引进了扩展分页,它允许页的大小为4MB,如图所示:
在扩展分页的情况下,分页机制把32位线性地址分成两个域:最高10位的目录域和其余22位的偏移量。
4 页面高速缓存
由于在分页情况下,每次存储器访问都要存取两级页表,这就大大降低了访问速度。所以,为了提高速度,在386中设置一个最近存取页面的高速缓存硬件机制,它自动保持32项处理器最近使用的页面地址,因此,可以覆盖128K字节的存储器地址。当进行存储器访问时,先检查要访问的页面是否在高速缓存中,如果在,就不必经过两级访问了,如果不在,再进行两级访问。平均来说,页面高速缓存大约有98%的命中率,也就是说每次访问存储器时,只有2%的情况必须访问两级分页机构。这就大大加快了速度,页面高速缓存的作用如图所示。有些书上也把页面高速缓存叫做“联想存储器”或“转换旁视缓冲器(TLB)”。