深入理解Linux内核学习笔记之内存寻址(续)

五 Linux中的分页


Linux采用一种同时适用于32位和64位系统的普通分页模型。
Linux直到 2.6.10 版本, 采用三级分页模型;


从2.6.11开始,采用四级分页模型(用来全力支持*86_64平台使用的对线性地址的位的划分)。
4 种页表分别为:页全局目录(Page Global Directory),页上级目录(Page Upper Directory),
页中间目录(Page Middle Directory),页表(Page Table);


线性地址因此分为五个部分(每部分的大小与具体的计算机体系结构有关):
Global Dir | Upper Dir | Middle Dir | Table | OFFSET
( 自注:Dir 改为 Index 更有助于理解,Global Dir 表示在“Page Global Directory”这张表中的Index(是个Addr);
此Index指向多个“Page Upper Directory 表”中的一个。)


对未启用物理地址扩展的32位系统,两级页表(Global Dir | Table)足够,Linux将页上级目录和页中间目录的位全置0;
但Linux为这两个目录表保留了在指针序列中的位置,通过把它们的页目录项数设为1,
并把这两个目录项映射到页全局目录的一个适当的目录项来实现。


启用了物理地址扩展的32位系统使用了三级页表。Linux的页全局目录对应80x86的PDPT,
取消了页上级目录,页中间目录对应80x86的页目录,页表对应80x86的页表。


64位系统使用三级还是四级分页取决于硬件对线性地址的位的划分。


Linux的进程处理很大程度上依赖于分页,线性地址到物理地址的自动转换使下面的设计目标变得可行:
(1) 给每个进程分配一个不同的物理地址空间,这确保有效防止寻址错误;
(2) 区别页(一组数据)和页框(主存中的物理地址)。
这就允许放在某个页框中的一个页,必要时保存到磁盘上,以后重新装入这一页时可以被装在不同的页框中。
这是虚拟内存机制的基本要素。


每个进程有自己的页全局目录和自己的页表集,
进程切换时,Linux把cr3控制寄存器的内容保存在前一个执行进程的描述符中,
然后把下一个要执行进程的描述符的值装入cr3寄存器中。
这样,当新进程开始在CPU上执行时,分页单元指向一组正确的页表。


线性地址映射到物理地址虽然有点复杂,但现在已成为一种机械式的任务。


1 页目录/页表相关的宏介绍


一些函数和宏用来检索一些信息,这些信息是内核为了查找地址和管理表所需要的。
(1) 线性地址字段相关的宏:
PAGE_SHIFT: 指定 Offset 字段的位数(当用于80x86处理器时,它产生的值为12),即以2为底,页大小的对数(logarithm)。
   理解:从一个页表项到相邻另一个页表项间的shift。
PAGE_SIZE: 使用 PAGE_SHIFT 来返回页大小。理解:一个页表项映射的物理地址范围大小。
PAGE_MASK: 产生的值为0xfffff000,用以屏蔽 Offset 字段的所有位。


PMD_SHIFT: 指定线性地址的 Offset 字段和 Table 字段的总位数 (即页中间目录项可以映射的物理区域大小的对数)。
PMD_SIZE : 用于计算由页中间目录的一个单独表项所映射区域的大小,即一个页表能映射的物理区域大小(m*PAGE_SIZE)。
  [ 每个页表项映射的物理区域为一页的大小PAGE_SIZE,有m项的页表能映射到 m * PAGE_SIZE。]
PMD_MASK : 用于屏蔽 Offset 和 Table 字段的所有位。
PAE被禁用时,PMD_SHIFT产生的值为22(12+10), PMD_SIZE产生的值是2的22次方(4MB), PMD_MASK产生的值是0xffc00000.
    注:只有PGD+Table两级,Linux将PMD的唯一项映射到了PGD的某一合适项。
PAE被激活时,PMD_SHIFT产生的值为21(12+9), PMD_SIZE产生的值是2的21次方(2MB), PMD_MASK产生的值是0xffe00000.
大型页不使用最后一级页表,所以LARGE_PAGE_SIZE宏(产生大型页尺寸)=PMD_SIZE(2的PMD_SHIFT次方),
LARGE_PAGE_MASK = PMD_MASK .


PUD_SHIFT: Offset+Table+MiddleDir+UpperDir字段的总位数。
确定页上级目录项能映射的区域大小的对数。
32位系统上,Linux将页上级目录表的项数设为1,并将这个目录项映射到页全局目录的一个适当的目录项。
所以这里的PUD_SHIFT,PUD_SIZE, PUD_MASK 就相当于针对Global Dir表的某项来说的。
PUD_SHIFT = Offset+Table+MiddleDir+UpperDir字段的总位数。
PUD_SIZE = 2的PUD_SHIFT次方,用于计算页全局目录中一个单独表项所能映射的区域大小。
PUD_MASK: 用于屏蔽Offset+Table+MiddleDir+UpperDir字段的所有位。
80x86处理器上,PUD_SHIFT = PMD_SHIFT,PUD_SIZE = 4MB 或 2MB。


PGDIR_SHIFT: Offset+Table+MiddleDir+UpperDir字段的所有位,
    用于计算页全局目录表的一个表项能映射的区域大小(是此大小的对数);
PGDIR_MASK: 用于屏蔽Offset+Table+MiddleDir+UpperDir字段的所有位。
PAE被禁止时,PGDIR_SHIFT产生的值是22( Offset+Table=12+10 ),PGDIR_SHIFT = PMD_SHIFT = PUD_SHIFT;
    PGDIR_SIZE = 4MB, PGDIR_MASK = 0xffc00000。
PAE被激活时,PGDIR_SHIFT产生的值是30( Offset+Table+Middle Dir=12+9+9 ),
    PGDIR_SIZE = 1GB, PGDIR_MASK = 0xc0000000。


PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD, PTRS_PER_PGD: 计算页表-页全局目录表中的表项数;
PAE disable 时,产生的值分别是1024,1,1,1024;
PAE Enable 时,产生的值分别为 512,512,1,4 。


(2) 页表处理相关的宏


pte_t,pmd_t,pud_t,pgd_t分别描述页表项,页中间目录项,页上级目录项,页全局目录项的格式。
PAE禁止时,它们是32位数据类型;PAE激活时,它们是64位数据类型。
pgprot_t: 表示一个与单独表项相关的保护标志,64位(PAE激活)或32位(PAE禁止)。


五个类型转换宏:__pte, __pmd, __pud, __pgd, __pgprot: 把一个无符号整数转换成所需的类型。
另外五个类型转换宏执行相反的转换:pte_value, pmd_value, pud_value, pgd_value, pgprot_value, 
将上面提到的五种特殊的类型转换成一个无符号整数。


内核还提供了许多宏和函数用于读/修改页表表项:
若相关表项值为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.
若页中间目录项e指向一个大型页(2MB或4MB),则pmd_large(e)返回1,否则0.


宏pmd_bad通过输入参数来检查页中间目录项,若目录项指向一个不能使用的页表,则宏产生的值为1.
如以下情况之一发生,都会使此宏返回1:
页不在主存中(Present标志被清除) / 页只允许读访问(Read/Write标志被清除) / 
Accessed 或 Dirty 位被清除(对每个现有页表,Linux总是强制设置这些标志)


宏pud_bad和pgd_bad总是产生0;
没有定义pte_bad宏,因为页表项引用一个不在主存中/不可写/根本无法访问的页都是合法的。


若一个页表项Present标志或Page Size标志位1,则pte_present宏产生的值是1,否则0.
页表项的Page Size标志对CPU的分页单元来讲没有意义,但对当前在主存中却没有读,写或执行权限的页,
内核将其Present和Page Size分别置为0和1。
这样,任何试图对此类页的访问都会引起一个缺页异常,因为页的Present标志被清0,内核可以检查Page Size
的值来检测到产生异常并不是因为缺页。


pud_present和pgd_present宏产生的值总是1.


若相应表项的Present标志为1(即对应的页或页表被载入主存),则pmd_present宏产生的值是1.


以下函数用于查询页表项中任意一个标志的当前值,
不过只有pte_present返回1时,才能正常返回页表项中任一个标志。(但pte_file()除外)。
读页标志的函数:


pte_user() / pte_write() / pte_dirty() / pte_young() 
分别读user/supervisor标志,Read/Write标志,dirty标志,Accessed标志;


pte_read() 读user/supervisor标志 (表示80x86上的页不受读的保护)
pte_exec() 读user/supervisor标志 (表示80x86上的页不受代码执行的保护)
pte_file() 读dirty标志 (当present被清除而Page Size被设置时,页属于一个非线性磁盘映射)


以下函数用于设置页表项中各标志的值:
mk_pte_huge(): 设置页表项中Present和Page Size标志;
pte_wrprotect():清除Read/Write标志;
ptep_set_wrprotect(): 与pte_wrprotect()类似,但作用于指向页表项的指针;
pte_mkwrite():设置Read/Write标志;
pte_rdprotect():清除user/supervisor标志;
pte_exprotect():清除user/supervisor标志;
pte_mkread():设置user/supervisor标志;
pte_mkexec():设置user/supervisor标志;
pte_mkclean():清除dirty标志;
ptep_test_and_clear_dirty(): 与pte_mkclean()类似,但作用于指向页表项的指针并返回Dirty标志的旧值;
pte_mkdirty():设置dirty标志;
ptep_mkdirty():与pte_mkdirty()类似,但作用于指向页表项的指针;
pte_mkold():清除Accessed标志(把此页标记为未访问);
ptep_test_and_clear_young(): 与pte_mkold()类似,但作用于指向页表项的指针并返回Accessed标志的旧值;
pte_mkyoung():设置Accessed标志(把此页标记为访问过);
pte_modify(p,v): 把页表项p的所有访问权限设置为指定的值v;
ptep_set_access_flags():若Dirty被设置为1,则将页的存取权限设置为指定的值,并调用flush_tlb_page()函数。


(3)对页表项操作的宏
这些宏把一个页地址和一组保护标志组合成页表项或从一个页表项中提取出页地址。
其中一些宏对页的引用是通过"页描述符(见第18章)"的线性地址,而不是通过该页本身的线性地址。
pgd_index(addr) : 线性地址addr对应的目录项在页全局目录中的索引(相对位置)。 
pgd_offset(mm, addr) : 此宏产生线性地址addr在页全局目录中相应表项的线性地址,
      通过内存描述符mm(见第9章)里的一个指针可以找到这个页全局目录.
pgd_offset_k(addr) : 产生主内核页全局目录中某个项的线性地址,该项对应于地址addr。
pgd_page(pgd) : 通过全局目录项pgd产生页上级目录所在页框的页描述符地址。
在两级或三级目录中,该宏等价于pud_page(), 后者应用于页上级目录项。


pud_offset(pgd,addr) : 该宏产生页上级目录中目录项addr对应的线性地址。
        在两级或三级分页系统中,该宏产生pgd,即一个页全局目录项的地址。
      参数:pgd - 指向页全局目录项的指针,addr - 线性地址 
pud_page(pud): 通过页上级目录项pud产生页中间目录的线性地址。
      在两级分页系统中,该宏等价于pmd_page(), 后者应用于页中间目录项。


pmd_index(addr) : 产生线性地址addr在页中间目录中所对应的目录项的索引(相对位置)。 
pmd_offset(pud, addr) : 此宏产生目录项addr在页中间目录中的偏移地址,
在两级或三级分页系统中,它产生pud,即页全局目录项的地址。
接收指向页上级目录项的指针pud和线性地址addr作为参数。
pmd_page(pmg) : 通过页中间目录项pmd产生相应页表的页描述符地址。
在两级或三级分页系统中,pmd实际上是页全局目录中的一项。
mk_pte(p,prot) : 接收页描述符地址p和一组存取权限prot作为参数,并创建相应的页表项。


pte_index(addr) : 产生线性地址addr对应的表项在页表中的索引(相对位置)。
pte_offset_kernel(dir,addr): 线性地址addr在页中间目录dir中有一个对应的项,该宏就产生这个对应项,
    即页表的线性地址,另外,该宏只在主内核页表上使用。
pte_offset_map(dir,addr) : 产生于线性地址addr相对应的页表项的线性地址。
  参数是指向一个页中间目录项的指针dir和线性地址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通过反复释放页表和清除页中间目录项来清除进程页表的内容。


2 物理内存布局
初始化阶段,内核必须建立一个物理地址映射来指定哪些物理地址可用哪些不可用。
不可用的物理地址:可能映射了硬件设备I/O的共享内存或者因为相应的页框含有BIOS数据。


内核将下列页框记为保留(保留页框中的页决不能被动态分配或交换到磁盘上):
在不可用的物理地址范围内的页框,含有内核代码和已初始化数据结构的页框。


一般来说,Linux内核安装在RAM中从物理地址0x00100000开始的地方,即第二个MB开始。
所需页框总数取决于内核的配置方案,典型配置所得到的内核可以被安装在小于3MB的RAM中。


内核之所以没有被安装在RAM第一个MB开始的地方是因为PC体系结构有几个独特的地方需要被考虑到:
(1) 页框0由BIOS使用,存放POST期间检查到的系统硬件配置(POST: Power-on Selft-Test,开机自检),
    因此,很多膝上型电脑的BIOS甚至在系统初始化后还将数据写到该页框。
(2) 物理地址从0x000a0000-0x000fffff的范围通常留给BIOS例程,并且映射ISA图形卡上的全部内存。
    这个区域就是所有IBM兼容PC上从640KB-1MB之间著名的洞:物理地址存在但被保留,对应的页框不能由OS使用。
(3) 第一个MB内的其他页框可能由特定计算机模型保留。如:IBM ThinkPad把0xa0页框映射到0x9f页框。


在启动过程的早期阶段,内核询问BIOS并了解物理内存的大小。
在新近的计算机中,内核也调用BIOS过程建立一组物理地址范围和其对应的内存模型。
随后,内核执行machine_specific_memory_setup()函数,该函数建立物理地址映射。
当然,若这张表示可获取的,那是内核在BIOS列表基础上构建的;
否则,内核按保守的缺省设置构建这张表:从0x9f(LOWMEMSIZE())到0x100(HIGH_MEMORY)号的所有页框都记为保留。


BIOS提供的物理地址映射: 开始   结束   类型 (具体略)  
类型有Usable, Reserved, ACPI data, ACPI NVS等。


BIOS没有在上表中提供的物理地址范围信息,为安全可靠起见,Linux都假定这样的范围不可用。


内核可能不会见到BIOS报告的所有物理内存。
若未使用PAE支持来编译,即使有更大物理内存可用,内核页只能寻址4GB大小的RAM.
setup_memory()在machine_specific_memory_setup()执行后被调用,
它分析物理内存区域表并初始化一些变量来描述内核的物理内存布局,相关变量如下:
num_physpages  : 最高可用页框的页框号
totalram_pages : 可用页框总数量
min_low_pfn    : ram中在内核映射后第一个可用页框的页框号
max_pfn        : 最后一个可用页框的页框号
max_low_pfn    : 被内核直接映射的最后一个可用页框的页框号(低地址内存)
totalhigh_pages: 内核非直接映射的页框的总数(高地址内存)
highstart_pfn  : 内核非直接映射的第一个页框的页框号
highend_pfn    : 内核非直接映射的最后一个页框的页框号


为避免将内核装入一组不连续的页框里,Linux更愿跳过RAM的第一个MB。
Linux用PC体系结构未保留的页框来动态存放所分配的页。


Linux怎样填充3MB的RAM(假设内核需要小于3MB的RAM):
3MB可以分为768个页框(一页框4KB),页框号从0-767, 从页框号0x100(即1MB处)开始存放内核代码_text,
_text物理地址0x00100000,内核代码结束位置_etext。
内核数据分为两部分: _etext-_edata是已初始化代码,_edata-_end是未初始化代码。
以上符号不是在Linux源代码中定义,而是编译内核时产生,可以在system.map中找到这些符号的线性地址。
system.map是编译内核后创建的。


3 进程页表
进程的线性地址空间分为两部分:
0x00000000 - 0xbfffffff 的线性地址,无论进程运行在用户态还是内核态都可以寻址。
0xc0000000 - 0xffffffff 的线性地址,只有内核态的进程可以寻址。
进程运行在用户态时,它产生的线性地址小于0xc0000000;
运行在内核态时,它执行内核代码,所产生的地址大于0xc0000000。
某些情况下,内核为了检索或存放数据必须访问用户态线性地址空间。
PAGE_OFFSET宏产生的值就是0xc0000000,这就是进程在线性地址空间中的偏移量,也是内核生存空间的开始之处。


页全局目录的第一部分表项映射的线性地址小于0xc0000000(PAE未启用时是前768项,PAE启用时是前3项),
剩余表项对所有进程来说都应该是相同的,它们等于主内核页全局目录的相应表项。



4 内核页表
内核维持着一组自己使用的页表,驻留在主内核页全局目录(master kernel Page Global Directory)中。
系统初始化后,这组页表还从未被任何进程或任何内核线程直接使用。
主内核页全局目录的最高目录项部分作为参考模型,为系统中每个普通进程对应的页全局目录项提供参考模型。


内核映像刚刚被装入内存后,CPU仍然运行于实模式,所以分页功能没有被启用。
内核初始化自己的页表分两个阶段:
第一阶段:内核创建一个有限的地址空间,包括内核的代码段和数据段,
          初始页表和用于存放动态数据结构的128KB大小的空间。
 这个最小限度的地址空间仅够将内核装入RAM和对其初始化的核心数据结构。
第二阶段:内核充分利用剩余的RAM并适当的建立分页表。


(1) 临时内核页表
临时页全局目录是在内核编译过程中静态地初始化的,
临时页表是由startup_32()汇编语言函数初始化的(startup_32()定义于arch/i386/kernel/head.S),
此阶段PAE支持并未激活,页上级和中间目录相当于页全局目录项。
临时页全局目录放在swapper_pg_dir变量中,临时页表在pg0变量处开始存放,紧接在内核未初始化的数据段(_end)后面。
为简单起见,假定内核使用的段,临时页表和128K的内存范围能容纳于RAM前8MB空间里。
为映射RAM前8MB空间,需要用到两个页表。
分页第一阶段的目标是允许在实模式和保护模式下都能很容易对这8MB寻址,因此,内核必须创建一个映射,
把从0x00000000-0x007fffff的线性地址和从0xc0000000到0xc07fffff的线性地址映射到0x00000000-0x007fffff的物理地址。
换言之,内核在初始化的第一阶段,可以通过与物理地址相同的线性地址
或通过从0xc0000000开始的8MB线性地址对RAM的前8MB进行寻址。
内核通过把swapper_pg_dir所有项都填充为0来创建期望的映射,但0,1,0x300(十进制768),0x301(十进制769)这四项除外。


后两项包含了从0xc0000000到0xc07fffff间的所有线性地址。
0,1,0x300,0x301按以下方式初始化:
0和0x300项的地址字段置为pg0的物理地址,1和0x301项的地址字段置为紧随pg0后的页框的物理地址。
将这四项中的Present,Read/Write,User/Supervisor标志置位。
将这四项中的Accessed,Dirty,PCD, PWD和Page Size标志清0。


汇编语言函数startup_32()也启用分页单元,通过向cr3控制寄存器装入swapper_pg_dir的地址及
设置cr0控制寄存器的PG标志来达到这一目的。


(2) 当RAM小于896MB时的最终内核页表
由内核页表所提供的最终映射必须把从0xc0000000开始的线性地址转换为从0开始的物理地址。
宏__pa用于把从PAGE_OFFSET开始的线性地址转换成相应的物理地址,宏__va做相反的转化。
主内核全局目录仍然保存在swapper_pg_dir变量中。它由paging_init()函数初始化,该函数进行如下操作:
(I) 调用pagetable_init()适当地建立页表项;
    pagetable_init()执行的操作既依赖于现有RAM的容量,也依赖于CPU模型。
(II) 把swapper_pg_dir的物理地址写入cr3寄存器;
(III) 若CPU支持PAE且内核编译时支持PAE, 则将cr4控制寄存器的PAE标志置位;
(IV) 调用__flush_tlb_all()使TLB的所有项无效。
小于896MB的RAM没有必要激活PAE机制。
(线性地址的最高128MB留给几种映射去用,因此映射RAM所剩空间为1GB-128MB=896MB)
假定CPU是支持4MB页和“全局”TLB表项的最新80x86微处理器。注意若全局目录项对应0xc0000000之上的线性地址,
则把所有这些项的User/Supervisor标志清0,由此拒绝用户态进程访问内核地址空间。
还要注意若Page Size被置位使得内核可以通过使用大型页来对RAM进行寻址。
由startup_32()函数创建的物理内存前8MB的恒等映射用来完成内核的初始化阶段。
当这种映射不再必要时,内核调用zap_low_mappings()函数清除对应的页表项。


(3) 当RAM大小在896MB和4096MB之间时的最终内核页表
此种情况并不把RAM全部映射到内核地址空间。
Linux在初始化阶段可以做的最好的事是把一个具有896MB的RAM窗口映射到内核线性地址空间。
若一个程序需要对现有RAM的其余部分寻址,那就必须把某些其它的线性地址间隔映射到所需的RAM 。
这意味着修改某些页表项的值(动态重映射)。
内核使用与前一种情况相同的代码来初始化页全局目录。


(4) 当RAM大于4096MB时的最终内核页表
要处理这些情况:CPU支持PAE,RAM容量大于4G,内核以PAE支持来编译;
尽管PAE处理36位物理地址,但线性地址依然是32位。
如前所述,Linux映射一个896MB的RAM窗口到内核线性地址空间,剩余RAM留着不映射,并由动态重映射来处理。
与前一种情况的主要差异是使用三级分页模型,因此页全局目录的初始化代码与前面不同。
页全局目录中的前三项与用户线性地址空间相对应,内核用一个空页(empty_zero_page)的地址对这三项进行初始化。
第四项用页中间目录(pmd)的地址初始化,该页中间目录是通过调用alloc_bootmem_low_pages()分配的。
页中间目录的前448项用RAM前896MB的物理地址填充,剩余512-448=64项留给非连续内存分配。
注意:支持PAE的所有CPU模型也支持2MB大型页和全局页,只要可能,Linux使用大型页来减少页表数。


(5) 固定映射的线性地址


每个固定映射的线性地址都映射一个物理内存的页框。
内核使用固定映射的线性地址来代替指针变量,因为这些指针变量的值从不改变。
间接引用一个指针变量比间接引用一个立即常量地址多一次内存访问,
引用指针前需对其值检查而常量线性地址不需要。
每个固定映射的线性地址都存放在线性地址第四个GB的末端。
由第4GB初始部分的线性地址所建立的映射是线性的(线性地址X映射物理地址X-PAGE_OFFSET)。
fix_to_virt()函数(inline)计算从给定索引开始的常量线性地址,编译器展开到最后就用一常量线性地址来代替此调用。
内核使用set_fixmap(idx,phys)和set_fixmap_nocache(idx,phys)两个宏关联一个物理地址和固定映射的线性地址。
此二函数都把fix_to_virt(idx)线性地址对应的一个页表项初始化为物理地址phys。
第二个函数也把页表项的PCD标志置位,因此,访问这个页框中的数据时禁用硬件高速缓存。
clear_fixmap(idx)用来撤消固定映射线性地址idx和物理地址间的连接。


5 处理硬件高速缓存和TLB
(1) 处理硬件高速缓存
通过高速缓存行(cache line)寻址;
L1_CACHE_BYTES宏产生以字节为单位的高速缓存行的大小(Pentium 4 上此宏产生的值为128,P4前的Intel模型值为32);


为最优化高速缓存命中率,内核在下列决策中考虑体系结构:
一个数据结构中最常使用的字段放在该数据结构的低偏移部分,以便它们能够处于高速缓存的同一行。
当为一大组数据结构分配空间时,内核试图把它们都存放在内存中,以便所有高速缓存行按同一方式使用。
80x86微处理器自动处理高速缓存的同步,所以应用于这种处理器的Linux内核并不处理任何硬件高速缓存的刷新,
不过内核却为不能同步高速缓存的处理器提供了高速缓存刷新接口。


(2) 处理TLB
cpu不能自动同步它们自己的TLB高速缓存,因为决定线性地址和物理地址之间映射何时不再有效的是内核,而非硬件。
Linux2.6提供了几种在合适时机应当运用的TLB刷新方法,这取决于页表更换的类型。
除此之外,每个微处理器都提供了更受限制的一组使TLB无效的汇编语言指令,Intel微处理器提供了两种:
向cr3寄存器写入值时所有Pentium处理器自动刷新相对于非全局页的TLB表项;
在Pentium Pro及之后的的处理器中,invlpg汇编语言指令使映射指定线性地址的单个TLB表项无效。
下面列出了采用这种硬件技术的linux宏,这些宏是实现独立于系统的方法;
__flush_tlb() 将cr3寄存器的当前值重新写cr3;
__flush_tlb_global() 通过清除cr4的PGE标志禁用全局页,将cr3寄存器的当前值重新写cr3,并再次设置PGE标志;
__flush_tlb_single(addr) 以addr为参数执行invlpg汇编语言指令;


一般来说,任何进程切换都会暗示更换活动页表集。相对于过期页表,本地TLB表项必须被刷新,这个过程在内核把新的
页全局目录的地址写入cr3控制寄存器时会自动完成。不过内核在下列情况下将避免TLB被刷新:
(1) 当两个使用相同页表集的普通进程之间执行进程切换时;
(2) 当在一个普通进程和一个内核线程之间执行进程切换时。
(内核线程并不拥有自己的页表集,它们使用刚在CPU上执行过的普通进程的页表集)


除进程切换外,还有几种情况下内核需要刷新TLB中的一些表项。如:
当内核为某个用户态进程分配页框并将它的物理地址存入页表项时,它必须刷新与相应线性地址对应的任何本地TLB表项。
在多处理器系统中,若有多个CPU在使用相同的页表集,那内核还必须刷新这些CPU上使用相同页表集的TLB项。


为避免多处理器系统上的无用的TLB刷新,内核使用一种叫做懒惰TLB(Lazy TLB)模式的技术, 其基本思想:
若几个CPU正在使用相同的页表,而且必须对这些CPU上的一个TLB表项刷新,那么,在某些情况下,
正在运行内核线程的那些CPU上的刷新就可以延迟。


内核线程并不拥有自己的页表集,它们使用刚在CPU上执行过的普通进程的页表集,
不过,没有必要使一个用户态线性地址对应的TLB表项无效,因为内核线程不访问内核态地址空间。


当某个cpu开始运行一个内核线程时,内核将它置为懒惰TLB模式。
当发出清除TLB表项的请求时,处于懒惰TLB模式的每个CPU都不刷新相应的表项。
但CPU记住它的当前进程正运行在一组页表上,而这组页表的TLB表项对用户态地址是无效的。
只要处于Lazy TLB模式的CPU用一个不同的页表集切换到一个普通进程,硬件就自动刷新TLB表项,
同时内核把CPU设置为非懒惰TLB模式。
然而,若处理Lazy TLB模式的CPU切换到的进程与刚才运行的内核线程拥有相同的页表集,那么,
任何使TLB无效的延迟操作必须由内核有效地实施;
这种使TLB无效的“懒惰”操作可以通过刷新CPU的所有非全局TLB项来有效地获取。


为实现懒惰TLB模式,内核需要一些额外的数据结构。
cpu_tlbstate变量是一个具有NR_CPUS(默认32,代表系统中CPU最大数量)个结构的静态数组,
这个结构有两个字段,一个是指向当前进程内存描述符的active_mm字段,一个是具有两个状态值的state字段:
TLBSTATE_OK(非懒惰TLB模式), TLBSTATE_LAZY(懒惰TLB模式)。
每个内存描述符中包含一个cpu_vm_mask字段,该字段存放的CPU(这些CPU将要接收与TLB刷新相关的处理器间中断)下标,
只有当内存描述符属于当前运行的一个进程时这个字段才有意义。


当一个CPU开始执行内核线程时,内核把该CPU的cpu_tlbstate元素的state字段置为TLBSTATE_LAZY,
此外,活动(active)内存描述符的cpu_vm_mask字段存放系统中所有CPU(包括进入懒惰TLB模式的CPU)的下标。


对于与给定页表集相关的所有CPU的TLB表项,当另外一个CPU想使这些表项无效时,
该CPU就把一个处理器间中断发送给下标处于对应内存描述符的cpu_vm_mask字段中的那些CPU。


当CPU接收到一个与TLB刷新相关的处理器间中断,并验证了它影响了其当前进程的页表集时,
就检查它的cpu_tlbstate元素的state字段是否等于TLBSTATE_LAZY, 如果等于,内核就拒绝使TLB表项无效,
并从内存描述符的cpu_vm_mask字段删除该CPU下标。这有两种结果:
只要CPU还处于懒惰TLB模式,它将不接受其他与TLB刷新相关的处理器间中断;
若CPU切换到另一个进程,而这个进程与刚被替换的内核线程使用相同的页表集,

那么内核调用__flush_tlb()使该CPU的所有非全局TLB表项失效。









  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
深入理解Linux内核是一个非常广泛的话题,需要对计算机体系结构、操作系统理论、计算机网络等多个领域有深入的了解。下面是一些学习Linux内核的建议。 1. 操作系统原理:学习操作系统的基本原理,包括进程管理、内存管理、文件系统、设备管理等方面的知识。可以参考经典教材《操作系统概念》、《现代操作系统》等。 2. C语言编程:Linux内核主要使用C语言编写,因此需要熟练掌握C语言的语法和常用库函数。可以参考经典教材《C程序设计语言》、《C和指针》等。 3. 计算机体系结构:学习计算机的硬件体系结构,包括处理器、内存、I/O设备等。可以参考经典教材《计算机组成原理》、《现代操作系统》等。 4. Linux内核源码:深入理解Linux内核需要阅读和理解Linux内核源码。可以从最基础的启动代码、内存管理、进程管理等模块开始,逐步深入到文件系统、网络等模块。可以参考《Linux内核源代码情景分析》、《深入Linux内核架构》等书籍。 5. 内核调试工具:学习使用内核调试工具,如gdb、strace、ltrace等工具,可以帮助理解内核的执行过程和调用关系。 6. 社区参与:Linux内核是一个开放的社区项目,可以通过参与社区讨论、提交代码等方式深入了解内核的运作机制。可以参考Linux内核源码仓库、LWN.net等网站。 需要注意的是,深入理解Linux内核是一个非常庞大的工程,需要付出长期的努力和耐心。建议从基础知识开始逐步深入,不断扩大知识面和阅读范围,多动手实践,不断提升自己的编程和调试能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值