内存地址
使用80x86微处理器时,需区分三种不同地址。
1.逻辑地址,每一个逻辑地址都由一个段和偏移量组成。
2.线性地址(虚拟地址),如果是32位系统,则位一个32位无符号整数。可表达高达4GB地址。
3.物理地址,用于内存芯片级内存单元寻址。与从微处理器的地址引脚发到内存总线上的点信号相对应。物理地址由32位或36位无符号整数表示。内存控制单元通过分段单元的硬件电路把一个逻辑地址转换为线性地址;接着,第二个称为分页单元的硬件电路把线性地址转换成一个物理地址。多处理器系统中,所有CPU共享同一内存。RAM芯片上读或写操作必须串行执行,因此一种内存仲裁器的硬件电路插在总线和每个RAM芯片间。
硬件中的分段
从80286开始,Intel微处理器以两种不同的方式执行地址转换,称为实模式,保护模式。实模式存在的主要原因是维持处理器与早期模型兼容,并让操作系统自举。
段选择子和段寄存器
段选择子占据16比特位。包含索引号,T标志,RPL。段寄存器中:
cs 代码段寄存器
ss 栈段寄存器
ds 数据段寄存器
cs中段选择子的RPL就是CPL。
段描述符
每个段由一个8字节的段描述符表示。段描述符放在GDT或LDT中。GDT在主存中的地址和大小存放在gdtr控制寄存器中,当前正被使用的LDT地址和大小放在ldtr控制寄存器中。
字段名 | 描述 |
---|---|
Base | 段首字节线性地址 |
G | 0,段大小以字节为单位;1,段大小以4096字节为单位 |
Limit | 段中最后一个内存单元偏移量。 |
S | 0,系统段;1,非系统段 |
Type | 段类型及其存取权限 |
DPL | 为访问这个段所要求的CPU最小的特权级 |
P | 0,段不在内存;1,段在内存 |
D或B | |
AVL | linux下忽略 |
快速访问段描述符
为加速逻辑地址到线性地址的转换,80x86处理器提供一种附加的非编程的寄存器。
每个非编程的寄存器含有8个字节的段描述符,由相应的段寄存器中的段选择子来指定。这样,针对段选择子的逻辑地址转换就可不再访问GDT或LDT,直接引用附加的非编程寄存器中的段描述符即可。
GDT的第一项总是0。
分段单元
逻辑地址转换成线性地址,依赖分段单元。分段单元执行以下操作:
1.检查段选择子的T标志,以决定段描述符存在GDT还是LDT。
2.从段选择子的index字段,结合所在描述符基地址计算段描述符地址。从段描述符可以得到段基地址。
3.把逻辑地址的偏移量与段描述符Base字段值相加就得到了线性地址。
ps:只有段寄存器的内容被改变时才需执行1,2。
Linux中的分段
分段可以给每个进程分配不同的线性地址空间。分页可把同一线性地址空间映射到不同的物理空间。2.6版linux只有在80x86结构下才需要使用分段。运行在用户态的所有Linux进程都使用一对相同的段来对指令和数据寻址。即用户代码段,用户数据段。运行在内核态的所有Linux进程都使用一对相同的段对指令和数据寻址。即内核代码段,内核数据段。
对32位系统,与段相关的线性地址从0开始,达到2^32-1的寻址限长。
当CPL=3时,ds寄存器必须含有用户数据段的段选择子,ss必须指向一个用户数据段中的用户栈。
当CPL=0时,ds寄存器必须含有内核数据段的段选择子;ss必须指向一个内核数据段中的内核栈。
Linux GDT
在单处理器系统中只有一个GDT,在多处理器系统中每个CPU对应一个GDT。
所有的GDT都存放在cpu_gdt_table数组中,所有GDT的地址和它们的大小(初始化gdtr寄存器时使用)被存放在cpu_gdt_descr数组中。
Linux全局描述符表 | 段选择子 |
---|---|
null | 0x0 |
reserved | |
reserved | |
reserved | |
not used | |
not used | |
TLS#1 | 0x33 |
TLS#2 | 0x3b |
TLS#3 | 0x43 |
reserved | |
reserved | |
reserved | |
kernel code | 0x60 |
kernel data | 0x68 |
usr code | 0x70 |
user data | 0x78 |
TSS | 0x80 |
LDT | 0x88 |
PNPBIOS 32-bit code | 0x90 |
PNPBIOS 16-bit code | 0x98 |
PNPBIOS 16-bit data | 0xa0 |
PNPBIOS 16-bit data | 0xa8 |
PNPBIOS 16-bit data | 0xb0 |
APMBIOS 32-bit code | 0xb8 |
APMBIOS 16-bit code | 0xc0 |
APMBIOS data | 0xc8 |
not used | |
not used | |
not used | |
not used | |
not used | |
double fault TSS | 0xf8 |
Linux LDT
大多数用户态下的Linux程序不用局部描述符表,这样内核就定义了一个缺省的LDT供大多数进程共享。缺省的局部描述符表存放在default_ldt数组中。内核仅用了其中两项:用于iBCS执行文件的调用门;Solaris/x86可执行文件的调用门。当处理器开始执行拥有自定义局部描述符表的进程时,该CPU的GDT表中的LDT对应表项相应被修改。
硬件中的分页
分页单元把线性地址转换成物理地址。为了效率起见,线性地址被分成以固定长度为单位的组,称为页。页内部连续的线性地址被映射到连续的物理地址中。分页单元把所有的RAM分成固定长度的页框(有时叫物理页)。每个页框包含一个页。页框是主存的一部分,因此也是一个存储区域。把线性地址映射到物理地址的数据结构称为页表。页表存放在主存中,并在启用分页单元之前必须由内核对页表进行适当的初始化。从80386开始,所有的80x86处理器都支持分页。分页关闭下,线性地址就被解释为物理地址。
常规分页
从80386起,Intel处理器的分页单元处理4KB的页。32位的线性地址被分成3个域:
名称 | 位域 |
---|---|
Directory | 最高10位 |
Table | 中间10位 |
Offset | 最低12位 |
每个活动进程必须有一个分配给它的页目录表。但没必要马上为进程的所有页表都分配RAM。正在使用的页目录表的物理地址存放在控制寄存器CR3中。
每一页含有4096字节的数据。每个页目录表,每个页表占据一页。可容纳1024个4字节表项。页目录表和页表的表项有同样的结构,每项包含下面的字段。
字段 | 描述 |
---|---|
Present | 1,所指向的页在主存中;0,指向的页不在主存。 |
Field | 页框物理地址高20位。每个页框的起始物理地址必须是4096的倍数。故,物理地址低12位总是0。 |
Accessed | 每当分页单元对相应页框进行寻址时就设置这个标志。 |
Dirty | 只应用于页表项。每当对一个页框进行写操作时就设置这个标志。 |
Read/Write | 页框的读写权限 |
User/Supervisor | 访问页框所需的特权级 |
PCD和PWT | 控制硬件高速缓存处理页或页表的方式。 |
PageSize | 只应用于页目录项。1,则页目录项指向的是2MB或4MB的页框。 |
Global | 只应用于页表项。只有在CR4的页全局启用置位时,此标志才起作用。 |
扩展分页
从Pentium开始,80x86微处理器引入了扩展分页,它允许页框大小为4MB而非4KB。这时,内核可以不用中间页表进行地址转换,从而节省页表占据内存,提高TLB缓存命中。通过设置页目录项的Page Size标志启用扩展分页功能。此时,分页单元把32位线性地址分成两个字段。
字段 | 描述 |
---|---|
Directory | 最高10位 |
Offset | 其余22位 |
扩展分页和正常分页的页目录项基本相同。除了:
1.Page Size标志必须被设置。
2.20位物理地址字段只有最高10位是有意义的。
通过设置CR4处理器寄存器的PSE标志能使扩展分页与常规分页共存。
硬件保护方案
尽管80x86处理器允许一个段使用4种可能的特权级别。但与页和页表相关的特权级只有两个:
User/Supervisor标志为0时,只有CPL小于3时才能对页寻址。
User/Supervisor标志为1时,总能对页寻址。
此外,与段的存取权限(读,写,执行)不同。页的存取权限只有两种(读,写)。Read/Write标志等于0,说明相应的页表或页是只读的。Read/Write标志等于1,说明相应的页表或页是读写的。
物理地址扩展(PAE)分页机制
处理器所支持的RAM容量受连接到地址总线上的地址管脚数限制。早期Intel处理器从80386到Pentium使用32位物理地址。理论上,这样的系统可按照高达4GB的RAM。实际上,由于用户进程线性地址空间的需要,内核不能直接对1GB以上的RAM进行寻址。
为了使用大于4GB的RAM,Intel通过在它的处理器上把管脚数从32增加到36已经满足了这些需求。不过,只有引入一种新的分页机制把32位线性地址转换为36位物理地址才能使用所增加的物理地址。从Pentium Pro处理器开始,Intel引入一种叫做物理地址扩展的机制。另外一种叫做页大小扩展的机制在Pentium III处理器种引入,但Linux没采用这种机制。通过设置CR4控制寄存器的物理地址扩展标志激活PAE。页目录项中的页大小标志PS启用大尺寸页。(在PAE启用时为2MB)
Intel为支持PAE已经改变了分页机制。
1.64GB的RAM被分成为2^24个页框。页表项的物理地址字段从20位扩展到了24位。因为PAE页表项需包含12个标志位和24个物理地址位。为此,页表项大小从32位变为64位。这样,一个4KB的页表包含512个表项而不是1024个表项。
2.引入一个叫做页目录指针表的页表新级别。它由4个64位表项组成。
3.CR3控制寄存器包含一个27位的页目录指针表(PDPT)基地址字段。因为PDPT存放在RAM的前4GB中,并在32字节的倍数上对齐。因此,27位足以表示这种表的基地址。
4.当把线性地址映射到4KB的页时(页目录项中的PS置为0),32位线性地址按下列方式解释:
位31~30
指向PDPT中4个项中的一个
位29~21
指向页目录中512个项中的一个
位20~12
指向页表中512个项中的一个
位11~0
4KB页中的偏移量
5.当把线性地址映射到2MB的页时(页目录项中的PS置为1),32位线性地址按下列方式解释:
位31~30
指向PDPT中4个项中的一个
位29~21
指向页目录中512个项中的一个
位20~0
2MB页中的偏移量
PAE并没有扩大进程的线性地址空间,因为它只处理物理地址。
64位系统中的分页
32位微处理器普遍采用两级分页。两级分页并不适用采用64位系统的计算机。所有64位处理器的硬件分页系统都使用了额外的分页级别。使用的级别数量取决于处理器的类型。下表总结了一些Linux所支持64位平台使用的硬件分页系统的主要特征。
平台名称 | 页大小 | 寻址使用的位数 | 分页级别数 | 线性地址分级 |
---|---|---|---|---|
alpha | 8KB | 43 | 3 | 10+10+10+13 |
ia64 | 4KB | 39 | 3 | 9+9+9+12 |
ppc64 | 4KB | 41 | 3 | 10+10+9+12 |
sh64 | 4KB | 41 | 3 | 10+10+9+12 |
x86_64 | 4KB | 48 | 4 | 9+9+9+9+12 |
Linux提供了一种通用分页模型,它适用于绝大多数所支持的硬件分页系统。
硬件高速缓存
当今微处理器时钟频率接近几个GHz,而动态RAM芯片的存取时间是时钟周期的数百倍。这意味着,从RAM中取操作数或向RAM中存放结果这样的指令执行时,CPU可能等待很长时间。为缩小CPU和RAM间的速度不匹配,引入硬件高速缓存内存。80x86体系结构引入了一个叫行的新单位。行由几十个连续的字节组成,它们以脉冲突发模式在慢速DRAM和快速的用来实现高速缓存的片上静态RAM(SRAM)之间传送,用来实现高速缓存。
高速缓存再被细分为行的子集。主存中的一个行存储到高速缓存的行。高速缓存单元插在分页单元和主内存之间。它包含一个硬件高速缓存内存,一个高速缓存控制器。
高速缓存内存存放内存中真正的行。高速缓存控制器存放一个表项数组。每个表项对应高速缓存内存中的一个行。每个表项有一个标签和描述高速缓存行状态的几个标志。这个标签由一些位组成,这些位让高速缓存控制器能辨别由这个行当前所映射的内存单元。这种内存物理地址通常分为3组:最高几位对应标签,中间几位对应高速缓存控制器的子集索引,最低几位对应行内的偏移量。
当访问一个RAM存储单元时, CPU从物理地址中提取出子集的索引号,并把子集中所有行的标签与物理地址高几位相比较。如发现一个行的标签与这个物理地址的高位相同,则CPU命中一个高速缓存;否则,高速缓存没有命中。
当命中一个高速缓存时,高速缓存控制器进行不同的操作。具体取决于存取类型。对于读操作,控制器从高速缓存行中选择数据并送到CPU寄存器;不需要访问RAM因而节约了CPU时间。对于写操作,控制器可能采用以下两个基本策略之一,分别称为通写,回写。在通写中,控制器总是既写RAM也写高速缓存行。回写中,只更新高速缓存行,不改变RAM的内容。RAM最终必须被更新。只有当CPU执行一条要求刷新高速缓存表项的指令时,或当一个FLUSH硬件信号产生时(通常在高速缓存不命中后),高速缓存控制器才把高速缓存行写回到RAM中。
当高速缓存没有命中时,高速缓存行被写回到内存(以便腾出空间)。如有必要的话,把正确的行从RAM中取出放到高速缓存的表项中。多处理器系统的每一个处理器有一个单独的硬件高速缓存。因此,它们需要额外的硬件电路用于保持高速缓存内容的同步。只要一个CPU修改了 它的硬件高速缓存,它就必须检查同样的数据是否包含在其他的硬件高速缓存中;如是,它必须通知其他CPU用适当的值对其更新。常把这种活动叫做高速缓存侦听。所有这一切,在硬件级处理。
多级高速缓存之间的一致性由硬件实现。Linux忽略这些硬件细节并假定只有一个单独的高速缓存。处理器的CR0寄存器的CD标志位用来启用或禁用高速缓存电路。这个寄存器中的NW标志指明高速缓存是使用通写还是回写策略。
Pentium处理器高速缓存的另一个有趣特点是,让操作系统把不同的高速缓存管理策略与每一个页框相关联。为此,每一个页目录项和每一个页表项都包含两个标志。PCD指明当访问包含在这个页框中的数据时,高速缓存功能必须被启用还是禁用。PWT标志指明当把数据写到页框时,必须使用的策略是回写策略还是通写策略。Linux清除了所有页目录项和页表项中的PCD和PWT标志。结果是:对于所有的页框都启用高速缓存,对于写操作总是采用回写策略。
转换后援缓冲器
80x86还包含了另一个称为转换后援缓冲器或TLB的高速缓存用于加快线性地址的转换。当一个线性地址被第一次使用时,通过慢速访问RAM中的页表计算出相应的物理地址。此时线性地址,物理地址被存放在一个TLB表项中,以便以后对同一个线性地址的引用可以快速地得到转换。
多处理器系统中,每个CPU都有自己的TLB,叫该CPU的本地TLB。
TLB中的对应项不必同步,因为运行在不同CPU上的进程可以使同一线性地址与不同的物理地址发生联系。当CPU的cr3控制寄存器被修改时,硬件自动使本地TLB中的所有项都无效,这是因为新的一组页表被启用而TLB指向的是旧数据。
Linux中的分页
两级页表对32位系统来说已经足够了,但64位系统要更多数量的分页级别。从2.6.11开始,Linux采用四级分页模型。
1.页全局目录
2.页上级目录
3.页中间目录
4.页表
这样,线性地址被分成五个部分,每一部分的大小与具体的计算机体系结构有关。对于没有启用物理地址扩展的32位系统,两级页表已经足够了。Linux通过使"页上级目录"位和"页中间目录"位全为0,从根本上取消了页上级目录和页中间目录字段。不过,页上级目录,页中间目录在指针序列中的位置被保留。内核为页上级目录和页中间目录保留了一个位置,这是通过把它们的页目录项数设置为1,并把这两个页目录项映射到页全局目录的一个适当的目录项而实现的。
启用了物理地址扩展的32位系统使用了三级页表。取消了页上级目录。64位系统使用三级还是四级分页取决于硬件对线性地址的位的划分。Linux的进程处理很大程度依赖于分页。事实上,线性地址到物理地址的自动转换使下面的设计目标变得可行:
1.给每一个进程分配一块不同的物理地址空间。
2.区别页和页框。
线性地址字段
下列宏简化了页表处理
PAGE_SHIFT
当用于80x86处理器时,产生的值为12。
PMD_SHIFT
指定线性地址的Offset字段和Table字段的总位数。
当PAE被禁用时,PMD_SHIFT产生的值为22。当PAE被激活时,PMD_SHIFT产生的值为21。大型页不使用最后一级页表。
PUD_SHIFT
确定页上级目录项能映射的区域大小的位数。
PGDIR_SHIFT
确定页全局目录项能映射的区域大小的对数。
PTRS_PER_PTE,PTRS_PER_PMD,PTRS_PER_PUD,PTRS_PER_PGD
用于计算页表,页中间目录,页上级目录,页全局目录表中表项的个数。
当PAE被禁止时,它们产生的值分别为1024,1,1,1024。
当PAE被激活时,产生的值分别为512,512,1,4。
页表处理
pte_t,pmd_t,pud_t和pgd_t分别描述页表项,页中间目录项,页上级目录和页全局目录项的格式。当PAE被激活时,它们都是64位的数据类型。否则,都是32位数据类型。
物理内存布局
初始化阶段,内核必须建立一个物理地址映射来指定哪些物理地址范围对内核可以, 哪些不可用。内核将下列页框记为保留:
1.在不可用的物理地址范围内的页框
2.含有内核代码和已初始化的数据结构的页框。
保留页框中的页不能被动态分配或交换到磁盘上。
一般,Linux内核安装在RAM中从物理地址0x00100000开始的地方。典型的配置所得到的内核可以被安装在小于3MB的RAM中。
内核没安装在0x00000000?因为PC体系结构上:
1.页框0由BIOS使用。
2.物理地址从0x000a0000到0x000fffff的范围通常留给BIOS例程,并且映射ISA图形卡上的内部内存。
3.第一个MB内的其他页框可能由特定计算机模型保留。
在启动过程的早期阶段,内核询问BIOS并了解物理内存的大小。在新近的计算机中,内核也调用BIOS过程建立一组物理地址范围和其对应的内存类型。随后,内核执行machine_specific_memory_setup,该函数建立物理地址映射。如果这张表是可获取的,则内核在BIOS列表的基础上构建。否则,内核按保守的缺省设置构建这张表。setup_memory在machine_specific_memory_setup执行后被调用:它分析物理内存区域表,并初始化一些变量来描述内核的物理内存布局。
为了避免把内核装入一组不连续的页框里,Linux更愿跳过RAM的第一个MB。
假设内核需要小于3MB的RAM。Linux填充前3MB的RAM的一个示例。
页框范围 | 描述 |
---|---|
[0x0,0x1) | 不可用页框 |
[0x1,0x9f) | 可用页框 |
[0x9f,0x100) | 不可用页框 |
[0x100,x1) | 内核代码 |
[x1,x2) | 已初始化的内核数据 |
[x2,x3) | 未初始化的内核数据 |
[x3,0x300) | 可用页框 |
进程页表
对32位系统,进程的线性地址空间分成两部分:
1.从0x00000000到0xbfffffff的线性地址。无论进程运行在用户态还是内核态都可寻址。
2.从0xc0000000到0xffffffff的线性地址。只有内核态的进程才能寻址。
宏PAGE_OFFSET产生的值是0xc0000000,这就是进程在线性地址空间中的偏移量。也是内核生存空间的开始之处。
内核页表
内核维持着一组自己使用的页表,驻留在所谓的主内核页全局目录中。内核映像刚刚被装入内存后,CPU仍然运行于实模式,所以分页功能没有被启用。
第一个阶段,内核创建一个有限的地址空间,包括内核的代码段,数据段,初始页表,用于存放动态数据结构的共128KB大小的空间。这个最小限度的地址空间仅够将内核装入RAM和对其初始化的核心数据结构。
第二个阶段,内核充分利用剩余的RAM并适当建立分页表。
临时内核页表
临时页全局目录是在内核编译过程中静态地初始化的,临时页表是由startup_32初始化的。
临时页全局目录放在swapper_pg_dir变量中。临时页表在pg0变量处开始存放。
我们假设内核使用的段,临时页表,128KB的内存范围能容纳于RAM前8MB空间里。
分页第一阶段目标是允许实模式下, 保护模式下能很容易对这8MB寻址。
内核需建立一个映射把线性地址[0x00000000,0x007fffff]和[0xc0000000,0xc07fffff]映射到物理地址[0x00000000,0x007fffff]。
汇编语言函数startup_32启用分页单元。
当RAM小于896MB时的最终内核页表
由内核页表所提供的最终映射必须把从0xc0000000开始的线性地址转为从0开始的物理地址。
主内核页全局目录仍然保存在swapper_pg_dir变量中。它由paging_init函数初始化。函数执行如下操作:
1.调用pagetable_init适当地建立页表项。
2.把swapper_pg_dir的物理地址写入cr3控制寄存器。
3.如CPU支持PAE且如果内核编译时支持PAE,则将cr4控制寄存器的PAE标志置位。
4.调__flush_tlb_all使TLB的所有项无效。
假设,我们的计算机有小于896MB的RAM,32位物理地址足以对所有可用RAM进行寻址,因而没必要激活PAE机制。
假定CPU是支持4MB页和"全局"TLB表项的最新80x86微处理器。由startup_32创建的物理内存前8MB的恒等映射用来完成内核的初始化阶段。当这种映射不再必要时,内核调zap_low_mappings清除对应的页表项。
当RAM大小在896MB和4096之间时的最终内核页表
这种情况下,并不把RAM全部映射到内核地址空间。
Linux在初始化阶段可以做的最好的事是把一个具有896MB的RAM窗口映射到内核线性地址空间。如果一个程序需要对现有RAM的其余部分寻址,那就必须把某些其他的线性地址间隔映射到所需的RAM。
当RAM大于4096时的最终内核页表
我们处理以下发生的情况:
1.CPU模型支持物理地址扩展
2.RAM容量大于4GB
3.内核以PAE支持来编译
Linux映射一个896MB的RAM窗口到内核线性地址空间;剩余RAM留着不映射,并由动态重映射来处理。支持PAE的所有CPU模型也支持大型2MB页和全局页。
固定映射的线性地址
固定映射的线性地址基本上是一种类似于0xffffc000这样的常量线性地址,其对应的物理地址不必等于线性地址减去0xc0000000,而是可以以任意方式建立。
每个固定映射的线性地址都映射一个物理内存的页框。固定映射的线性地址概念上类似于对RAM前896MB映射的线性地址。不过,固定映射的线性地址可以映射任何物理地址。每个固定映射的线性地址都由定义于enum fixed_addresses数据结构中的整型索引来表示。
enum fixed_addresses{
FIX_HOLE,
FIX_VSYSCALL,
FIX_APIC_BASE,
FIX_IO_APIC_BASE_0,
[...],
__end_of_fixed_addresses
};
每个固定映射的线性地址都存放在线性地址第四个GB的末端。fix_to_virt计算从给定索引开始的常量线性地址
inline unsigned long fix_to_virt(const unsigned int idx) {
if(idx >= __end_of_fixed_addresses)
__this_fixmap_does_not_exist();
return (0xfffff000UL - (idx << PAGE_SHIFT));
}
为了把一个物理地址与一个固定映射的线性地址关联起来,内核使用set_fixmap(idx, phys)和set_fixmap_nocache(idx, phys)。
解除关联使用clear_fixmap(idx)。
处理硬件高速缓存和TLB
内核开发者采用一些技术来减少高速缓存和TLB的未命中次数。
处理硬件高速缓存
硬件高速缓存是通过高速缓存行寻址的。L1_CACHE_BYTES宏产生以字节为单位的高速缓存行的大小。
早于Pentium 4的Intel模型中,这个宏产生的值为32;在Pentium 4上,它产生的值为128。
为了使高速缓存的命中率达到最优化,内核在下列决策中考虑体系结构:
1.一个数据结构中最常使用的字段放在该数据结构内的低偏移部分,以便它们能处于高速缓存的同一行中。
2.当为一大组数据结构分配空间时,内核试图把它们都放在内存中,以便所有高速缓存行按同一方式使用。
80x86微处理器自动处理高速缓存的同步。
处理TLB
处理器不能自动同步它们自己的TLB高速缓存。Intel微处理器只提供了两种使TLB无效的技术:
1.在向cr3寄存器写入值时,所有Pentium处理器自动刷新相对于非全局页的TLB表项。
2.在Pentium Pro及以后处理中,invlpg使映射指定线性地址的单个TLB表项无效。
内核在下列情况下将避免TLB被刷新:
1.当两个使用相同页表集的普通进程之间执行进程切换时。
2.当在一个普通进程和一个内核线程间执行进程切换时。
刷新TLB场景:
1.更改cr3寄存器切换页表时。
2.内核为某个用户态进程分配页框并将它的物理地址存入页表项时,必须刷新与相应线性地址对应的任何本地TLB表项。
多处理器中,如多个CPU在使用相同的页表集,内核还必须刷新这些CPU上使用相同页表集的TLB表项。
为避免多处理器系统上无用的TLB刷新,内核使用一种叫做懒惰TLB模式的技术。基本思想是,如果几个CPU在使用相同的页表,且必须对这些CPU上的一个TLB表项(用户态线性空间)刷新,则,在某些情况下,正在运行内核线程(内核态不会访问用户态地址空间)的那些CPU上的刷新就可以延迟。
当某个CPU开始运行一个内核线程时,内核把它置为懒惰TLB模式。当发出清除TLB表项(指向用户态线性地址)的请求时,处于懒惰TLB模式的每个CPU都不刷新相应的表项;但是,CPU记住它的当前进程正运行在一组页表上,而这组页表的TLB表项对用户态地址是无效的。只要处于懒惰TLB模式的CPU用一个不同的页表集切换到一个普通进程,硬件就自动刷新TLB表项,同时,内核把CPU设置为非懒惰TLB模式。如果,处于懒惰TLB模式的CPU切换到的进程(用户态进程)与刚才运行的内核线性拥有相同的页表集,那么,任何使TLB无效的延迟操作必须由内核实施(由内核显式对延迟操作执行刷新)。
为实现懒惰TLB模式,需一些额外的数据结构。
cpu_tlbstate变量是一个具有NR_CPUS个结构的静态数组。这个结构有两个字段,一个是指向当前进程内存描述符的active_mm字段,一个是具有两个状态值的state字段:TLBSTATE_OK或TLBSTATE_LAZY。
此外,每个内存描述符中包含一个cpu_vm_mask字段,该字段存放的是CPU下标(这些CPU要接收与TLB刷新相关的处理器间中断);
当一个CPU开始执行内核线程时,内核把该CPU的cpu_tlbstate元素的state置为TLBSTATE_LAZY;此外,活动内存描述符的cpu_vm_mask字段存放系统中所有CPU(所有有关cpu)的下标。
对于与给定页表集相关的所有CPU的TLB表项,当另外一个CPU想使这些页表项无效时,该CPU就把一个处理器间中断发送给下标处于对应内存描述符的cpu_vm_mask字段中的那些CPU。当CPU接受到一个与TLB刷新相关的处理器间中断,并验证它影响了其当前进程的页表集时,就检查它的cpu_tlbstate元素的state字段是否等于TLBSTATE_LAZY;如等于,内核就拒绝使TLB表项无效,并从内存描述符的cpu_vm_mask字段删除该CPU下标(这样后续刷新均不会通知到自己,因为自己目前状态不需接收通知,收到也无实际操作。不如不收)。文章来源:https://www.toymoban.com/news/detail-537305.html
这有两种结果:
1.只要CPU还处于懒惰TLB模式,它将不接受其他与TLB刷新相关的处理器间中断。
2.如果CPU切换到另一个进程,而这个进程(用户态)与刚被替换的内核线程使用相同的页表集,则内核调用__flush_tlb使该CPU的所有非全局TLB表项无效。(内核负责此时统一执行一次TLB刷新来达到同步)文章来源地址https://www.toymoban.com/news/detail-537305.html
到了这里,关于深入理解Linux内核--内存寻址的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!