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

linux内存寻址解析 (二)

2016-05-14 10:30 387 查看


5.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存储器中。在前一种情况下,程序因产生了无效地址而必须被终止。

对于后一种情况,该无效的地址实际上是请求操作系统的虚拟存储管理系统,把存放在磁盘上的页传送到物理存储器中,使该页能被程序所访问。由于无效页通常是与虚拟存储系统相联系的,这样的无效页通常称为未驻留页,并且用页表属性位中叫做存在位的属性位进行标识。未驻留页是程序可访问的页,但它不在主存储器中。对这样的页进行访问,形式上是发生异常,实际上是通过异常进行缺页处理。

5.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也没有做特殊之用

5.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。

5.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。

 

    位的值(即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上执行时,分页单元指向一组正确的页表。

把线性地址映射到物理地址虽然有点复杂,但现在已经成了一种机械式的任务。本章下面的几节中列举了一些比较单调乏味的函数和宏,它们检索内核为了查找地址和管理叶表所需的信息;其中大多数函数只有一两行。也许现在你就想跳过这部分,但是知道这些函数和宏的功能是非常有用的,因为在以后章节的讨论中你会经常看到它们。

5.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。

5.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( )
pte_read( )
pte_write( )
pte_exec( )
pte_dirty( )
pte_young( )
pte_file( )
下表列出的另一组函数用于设置页表项中各标志的值:
函数名称
mk_pte_huge( )
pte_wrprotect( )
pte_rdprotect( )
pte_exprotect( )
pte_mkwrite( )
pte_mkread( )
pte_mkexec( )
pte_mkclean( )
pte_mkdirty( )
pte_mkold( )
pte_mkyoung( )
pte_modify(p,v)
ptep_set_wrprotect()
ptep_set_access_flags( )
ptep_mkdirty( )
ptep_test_and_clear_dirty( )
ptep_test_and_clear_young( )
现在,我们来讨论下表中列出的宏,它们把一个页地址和一组保护标志组合成页表项,或者执行相反的操作,从一个页表项中提取出页地址。请注意这其中的一些宏对页的引用是通过 “页描述符”的线性地址,而不是通过该页本身的线性地址。
宏名称
pgd_index(addr)
pgd_offset(mm, addr)
pgd_offset_k(addr)
pgd_page(pgd)
pud_offset(pgd, addr)
pud_page(pud)
pmd_index(addr)
pmd_offset(pud, addr)
pmd_page(pmd)
mk_pte(p,prot)
pte_index(addr)
pte_offset_kernel(dir,addr)
pte_offset_map(dir, addr)
pte_page( x )
pte_to_pgoff( pte )
pgoff_to_pte(offset )
下面我们罗列最后一组函数来简化页表项的创建和撤消。当使用两级页表时,创建或删除一个页中间目录项是不重要的。如本节前部分所述,页中间目录仅含有一个指向下属页表的目录项。所以,页中间目录项只是页全局目录中的一项而已。然而当处理页表时,创建一个页表项可能很复杂,因为包含页表项的那个页表可能就不存在。在这样的情况下,有必要分配一个新页框,把它填写为 0 ,并把这个表项加入。

如果 PAE 被激活,内核使用三级页表。当内核创建一个新的页全局目录时,同时也分配四个相应的页中间目录;只有当父页全局目录被释放时,这四个页中间目录才得以释放。当使用两级或三级分页时,页上级目录项总是被映射为页全局目录中的一个单独项。与以往一样,下表中列出的函数描述是针对 80x86 构架的。
函数名称
pgd_alloc( mm )
pgd_free( pgd)
pud_alloc(mm, pgd, addr)
pud_free(x)
pmd_alloc(mm, pud, addr)
pmd_free(x)
pte_alloc_map(mm, pmd, addr)
pte_alloc_kernel(mm, pmd, addr)
pte_free(pte)
pte_free_kernel(pte)
clear_page_range(mmu, start,end)


6.Linux中物理页面的映射



上图反映了如下信息:

1、 进程的4G 线性空间被划分成三个部分:进程空间(0-3G)、内核直接映射空间(3G – high_memory)、内核动态映射空间(VMALLOC_START
- VMALLOC_END)

2、 三个空间使用同一张页目录表,通过 CR3 可找到此页目录表。但不同的空间在页目录表中页对应不同的项,因此互相不冲突

3、内核初始化以后,根据实际物理内存的大小,计算出 high_memory、VMALLOC_START、VMALLOC_END 的值。并为“内核直接映射”空间建立好映射关系,所有的物理内存都可以通过此空间进行访问。

4、“进程空间”和“内核动态映射空间”的映射关系是动态建立的(通过缺页异常)

假设在有三个线性地址 addr1, addr2, addr3 ,分别属于三个线性空间,但是最终都映射到物理页面1:

1、 三个地址对应不同的页表和页表项

2、 但是页表项的高 20bit 肯定是1,表示物理页面的索引号是1

3、 同时,根据高 20 bit,可以从 mem_map[] 中找到对应的 struct page 结构,struct
page 用于管理实际的物理页面(红线)

4、 从线性地址,根据页目录表,页表,可以找到物理地址

5、 和物理地址之间很容易互相转换

6、 从物理地址,可以很容易的反推出在内核直接映射空间的线性地址(蓝线)。内核空间的虚拟地址和物理地址相差3G,而要想得到在进程空间或者内核动态映射空间的对应的线性地址,则需要遍历相应的“虚存区间”链表。

关于页目录表:

1、 每个进程有一个属于自己的页目录表,可通过 CR3 寄存器找到

2、 而内核也有一个独立于其它进程的页目录表,保存在 swapper_pg_dir[] 数组中

3、 当进程切换的时候,只需要将新进程的页目录把地址加载到 CR3 寄存器中即可

4、 创建一个新进程的时候,需要为它分配一个 page,作为页目录表,并将 swapper_pg_dir[] 的高 256 项拷贝过来,低 768 项则清0

 


7.页面的映射总结

用户空间不是进程共享的,而是进程隔离的。每个进程最大都可以有3GB的用户空间。一个进程对其中一个地址的访问,与其它进程对于同一地址的访问绝不冲突。比如,一个进程从其用户空间的地址0x1234ABCD处可以读出整数8,而另外一个进程从其用户空间的地址0x1234ABCD处可以读出整数20,这取决于进程自身的逻辑。

         任意一个时刻,在一个CPU上只有一个进程在运行。所以对于此CPU来讲,在这一时刻,整个系统只存在一个4GB的虚拟地址空间,这个虚拟地址空间是面向此进程的。当进程发生切换的时候,虚拟地址空间也随着切换。由此可以看出,每个进程都有自己的虚拟地址空间,只有此进程运行的时候,其虚拟地址空间才被运行它的CPU所知。在其它时刻,其虚拟地址空间对于CPU来说,是不可知的。所以尽管每个进程都可以有4
GB的虚拟地址空间,但在CPU眼中,只有一个虚拟地址空间存在。虚拟地址空间的变化,随着进程切换而变化。

从上面我们知道,一个程序编译连接后形成的地址空间是一个虚拟地址空间,但是程序最终还是要运行在物理内存中。因此,应用程序所给出的任何虚地址最终必须被转化为物理地址,所以,虚拟地址空间必须被映射到物理内存空间中,这个映射关系需要通过硬件体系结构所规定的数据结构来建立。这就是我们所说的段描述符表和页表,Linux主要通过页表来进行映射。

        于是,我们得出一个结论,如果给出的页表不同,那么CPU将某一虚拟地址空间中的地址转化成的物理地址就会不同。所以我们为每一个进程都建立其页表,将每个进程的虚拟地址空间根据自己的需要映射到物理地址空间上。既然某一时刻在某一CPU上只能有一个进程在运行,那么当进程发生切换的时候,将页表也更换为相应进程的页表,这就可以实现每个进程都有自己的虚拟地址空间而互不影响。所以,在任意时刻,对于一个CPU来说,只需要有当前进程的页表,就可以实现其虚拟地址到物理地址的转化。

        内核空间对所有的进程都是共享的,其中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据,不管是内核程序还是用户程序,它们被编译和连接以后,所形成的指令和符号地址都是虚地址,而不是物理内存中的物理地址。

       虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000)开始的,之所以这么规定,是为了在内核空间与物理内存之间建立简单的线性映射关系。其中,3GB(0xC0000000)就是物理地址与虚拟地址之间的位移量,在Linux代码中就叫做PAGE_OFFSET。

        我们来看一下在include/asm/i386/page.h头文件中对内核空间中地址映射的说明及定义:

#define __PAGE_OFFSET                 (0xC0000000)

         ……

#define PAGE_OFFSET                 ((unsigned long)__PAGE_OFFSET)

 #define __pa(x)                 ((unsigned long)(x)-PAGE_OFFSET)

#define __va(x)                 ((void *)((unsigned long)(x)+PAGE_OFFSET))

对于内核空间而言,给定一个虚地址x,其物理地址为“x- PAGE_OFFSET”,给定一个物理地址x,其虚地址为“x+ PAGE_OFFSET”。

这里再次说明,宏__pa()仅仅把一个内核空间的虚地址映射到物理地址,而决不适用于用户空间,用户空间的地址映射要复杂得多,它通过分页机制完成。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  linux mm