2.1:内存地址
逻辑地址:在x86结构中,每一个逻辑地址都由两部分组成:一个16位的存储在段寄存器中的段选择符、还有一个32位的偏移量。偏移量指明了从段开始的地方到实际地址的偏移。逻辑地址就是我们反汇编看到的地址,比如0x3f00000。而存储在段寄存器中的段选择符是从反汇编出来的文件中看不到的。
线性地址:也叫做虚拟地址,32位无符号整数,可以用来表达高达4GB的地址。是由逻辑地址生成的。
物理地址:
MMU中通过分段单元的硬件电路将逻辑地址转化成线性地址。然后在分页单元中,将线性地址转换成物理地址。
本章中,分段的目的是将逻辑地址转换成线性地址;分页的目的是将线性地址转化成物理地址。之所以又有硬件,又有软件,是因为硬件提供了寻址能力,软件要为硬件的寻址能力服务,设置好硬件寻址需要的数据结构等内容。
2.2:硬件中的分段
intel处理器用两种方法执行地址转换,分别是实地址模式以及保护模式。实地址模式主要是为了让处理器与早期系统兼容,并且让操作系统启动。分段用于将逻辑地址转化成为虚拟地址。
2.2.1:段选择符和段寄存器
x86系统中,使用32位逻辑地址加上16位存储在段寄存器中的段选择符,获得线性地址。
系统中总共有六个段寄存器:
cs:代码段寄存器;ss:栈段寄存器;ds:数据段寄存器。其他三个寄存器可以指向任意的数据段。
段选择符有16位,高13位index用于在GDT或LDT中选择段描述符。第3位TI用于表示是在全局描述附表还是局部描述符表中选择。cs段选择符的第1~2位RPL指明了当前CPU的特权级(CPL),0表示内核态,3表示用户态。
2.2.2:段描述符
段描述符存储在全局描述符表(GDT)或者局部描述符表(LDT)中,描述了段的基本信息:如:段的首地址;段的长度,段的访问权限(linux中,0表示内核态,3表示用户态)
struct desc_struct { union { struct { unsigned int a; unsigned int b; }; struct { u16 limit0; u16 base0; unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1; unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8; }; }; } __attribute__((packed)); |
系统中只有一个GDT,每个进程特有的段存储在自己的LDT中。GDT的位置大小信息存储在寄存器gdtr中,LDT的地址大小存储在寄存器ldtr中,这个寄存器中的值随着进程切换而改变。
根据段的不同,有以下几种段描述符:
段描述符将在介绍GDT的时候描述。
2.2.3:快速访问段描述符
由于每次寻址的时候,都要获得当前的段选择符(存放在段寄存器中)所对应的段描述符。而段描述符存放在GDT或LDT,也就是内存中。如果每次都要访问一次内存的话,开销很大,所以x86提供了对应六个段寄存器的寄存器,用于存储当前段寄存器中段选择符所对应的段描述符。当段寄存器中的值不发生变化的时候,这六个寄存器中的值也不会改变。这样就可以加速段描述符的获取。这六个寄存器对我们而言是不可见的。
2.2.4:分段单元
已经获得逻辑地址,段描述符,就可以获得对应的线性地址。直接让段描述符中的base和逻辑地址相加就得到了线性地址。
也就是说,建立一个虚拟地址的步骤如下:
1:获得逻辑地址;
2:根据段寄存器中的内容,查询GDT或者LDT的相应表项,获得对应的段描述符。
3:将段描述符的base字段加上逻辑地址就是线性地址。
2.3:LINUX中的分段
运行在用户态的Linux进程都使用同一个段来对指令和数据寻址(用户代码段和用户数据段),运行在内核态的Linux进程也都使用同一个段来寻址(内核代码段和内核数据段)
例如,在用户态切到内核态的时候,只需要将__KERNEL_CS放入cs段寄存器中,__KERNEL_DS放入ds段寄存器中。
并且,这些段描述符的基地址都是0,也就是说,在Linux系统中,逻辑地址和线性地址是相同的。但是,不同的段描述符的CPL是不一样的,这就保证了特权级的正常工作。
2.3.1:linux GDT
GDT的成员如下:
/* * The layout of the per-CPU GDT under Linux: * * 0 - null * 1 - reserved * 2 - reserved * 3 - reserved * * 4 - unused <==== new cacheline * 5 - unused * * ------- start of TLS (Thread-Local Storage) segments: * * 6 - TLS segment #1 [ glibc's TLS segment ] * 7 - TLS segment #2 [ Wine's %fs Win32 segment ] * 8 - TLS segment #3 * 9 - reserved * 10 - reserved * 11 - reserved * * ------- start of kernel segments: * * 12 - kernel code segment <==== new cacheline * 13 - kernel data segment * 14 - default user CS * 15 - default user DS * 16 - TSS * 17 - LDT * 18 - PNPBIOS support (16->32 gate) * 19 - PNPBIOS support * 20 - PNPBIOS support * 21 - PNPBIOS support * 22 - PNPBIOS support * 23 - APM BIOS support * 24 - APM BIOS support * 25 - APM BIOS support * * 26 - unused * 27 - unused * 28 - unused * 29 - unused * 30 - unused * 31 - TSS for double fault handler */ |
每个CPU对应一个GDT(全局描述符表),包含18个段描述符:
1:用户态和内核态下的代码段和数据段共4个。
2:任务状态段:TSS所在的段是内核数据段中的一部分
3:局部描述符表段(LDT)
4:3个局部线程存储段(TLS):它允许多线程应用程序使用最多三个局部于线程的数据段。
5:3个和AMP(高级电源管理)相关的段
6:5个和支持即插即用(PnP)相关的段
7:1个用于处理异常的特殊TSS段。
TLS段:是用于存储线程特有数据的数据段。虽然线程间共享数据,但是有时候他们也需要自己独有的数据。这些数据就会存储在TLS段中。
TSS段:存储了硬件上下文。
2.3.2:linux LDT
LDT和进程相关。如果进程没有自己定义LDT,那么使用默认的缺省的LDT。
2.4:硬件中的分页
之前的分段已经将逻辑地址转化成了线性地址。还需要借助分页,将线性地址转化成物理地址。
线性地址被分成固定大小的页。同时物理地址也被分为固定大小的页框。要知道页只是一个数据块,可以放在页框或者磁盘中。
这一节介绍32位或者64位时,硬件的分页规则。Linux实现的软件分页应该能够同时满足这两种要求。
2.4.1:常规分页
使用多级页表可以有效地减少每个进程页表需要的内存。因为如果一个进程不使用某个地址范围内的地址,那么这个进程就没有这段内存的页表。二级内存通过只为进程实际使用的那些虚拟内存请求页表来减少内存容量。
正在使用的页目录的物理地址放在控制寄存器cr3中。
2.4.2:扩展分页
扩展分页允许页的大小是4M,而不是4K。只需要将页目录项的Page Size置为1即可。
这时候只有线性地址的前10位用于寻找页目录项。剩下的22位,刚好对应4M内容。这样有利于保留TLB项。
2.4.3:硬件保护方案
分段和分页实现了两种保护方案。这两种保护方案是共同起作用的。在分段单元的保护中,使用的是段描述符的DPL。当DPL设置为0时,只能在CPL=0的时候(即在内核态的时候),才是可以访问的;而在DPL为3的时候,任何特权级都是可以访问的。
在分页单元的保护中,使用的是页表项中的某些位进行限制。
2.4.4:常规分页举例
2.4.5:物理地址扩展(PAE)分页机制
在一些情况下,虚拟地址仍然为32位,但是物理地址总线超过了32位,这样就需要一种新的机制来完成寻址。例如,物理地址有36位,虚拟地址还是32位。这时候就需要用4G的虚拟地址空间对应64G的物理地址空间,也就是使用32位虚拟地址寻址36位物理地址。
inter引入物理地址扩展PAE机制。使用PAE的时候,需要将同一线性地址映射到不同的物理地址,并没有扩大进程的线性地址,仍然为4G。
这时候,设置cr4寄存器中的PAE标志激活PAE,然后设置页目录项中的Page Size标志启用大尺寸页(扩展分页中为4M,PAE中为2M)。
2.4.6:64位系统中的分页
2.4.7:硬件高速缓存cache
cache的作用是:当CPU试图从主存中load/store数据的时候,CPU会首先查找cache,看对应的地址数据是否缓存在cache中。如果数据缓存在cache中,就直接从cache中拿取。
cache的总大小叫做cache size。我们将cache平分成很多相等的块,每个块叫做cache line。cache line是cache和主存间传输数据的最小单位。也就是说,如果cache line的大小是8字节。某一次CPU查询一个char类型的量,没有在cache中,这时候会直接加载char变量附近的8字节到cache中,而不是只加载一个字节的char变量。
2.4.7.1:直接映射缓存(单路组相连)
cache的查询方式如下所示:
假设我们使用的cache的大小是64字节,然后每个cache line是8个字节。因此,我们有8个cache line。
假设我们要访问0x0654地址上存储的数据。由于每个cache line是8个字节。因此,我们需要用地址的最后三位(offset,蓝色部分)来定位cache line中的数据。此外,总共有8个cache line。因此,我们还需要地址中的三位(index,黄色部分)来确定是哪一条cache line。很多地址的最后六位是相通的,仅凭六位地址显然无法进行区分,还需要剩下的位数。因此,将剩下的位数(绿色部分)放入tag中,每个tag和每个cache line对应。tag最前面的V表示这条cache line是否有效。
因此,使用地址进行查找的时候,就是这样的步骤。
1:使用地址的index位找到一个cache line。
2:对比地址的前26位是否和这个cache line对应的tag相同。如果相同,判断这条cache line是否有效。如果有效,说明查找成功;否则查找失败。
3:如果查找成功,那么就使用地址的offset在cache line中寻找,找到对应的字节。
直接映射缓存的优点就是设计简单,然后成本上也比较低。但是缺点就是,如果我们间隔的连续访问一些数据的时候,容易发生颠簸。
例如,整个cache的大小是64字节,然后我们访问一个二维数组a[100][16]。这时候就会发生一个问题,就是a[0][0],a[1][0],a[2][0]……,这些一定对应了同一个cache line。就会不停的换入换出。影响效率。
2.4.7.2:两路组相连缓存
路的意思,就是我们先将整个cache分成几份。比如,同样是64字节,每个cache line占据8个字节的缓存,两路组相连缓存的结构应该如下所示。
这时候引入一个新概念叫做组。索引一致,也就是下图中在同一行的cache line叫做一个组。
这时,加入我们还是需要查找地址为0x0654位置的数据。这时候,地址的最后三位(offset)还是用于从一个cache line的8个字节中选择一个字节。但是这时候的索引(index)只需要两位即可。因为cache line总共只有4行,也就是一路只有4个cache line。
因此,这时我们只需要两位index找到两个cache line。然后比较剩下的地址和每个cache line的tag。如果比对成功并且cache line有效,那么就说明查找成功。
既然之前讲直接映射缓存的时候,提了直接映射缓存的缺点,引入了两路组相联缓存。那么,两路组相联缓存是如何避免直接映射缓存的缺点的了?可以想象,还是之前那个数组的例子,这时候,我们访问数组的时候,a[0][0],a[1][0]会同时存储在cache中。这就避免了cache发生颠簸的概率。
当然,两路组相联也有缺点。每次需要比较两个tag,降低了速度,增加了硬件复杂度。
2.4.7.3:全相连缓存
全相连缓存中,没有index,这时候需要比较所有的tag。观察是否发生cache line命中。
寄存器cr0中的CD位用来表示启用或者禁用高速缓存。NW位标志高速缓存是通写还是写回策略。
2.4.7.4:e500的缓存结构
e500的缓存结构如下所示,每个cache line的大小为32字节。
通过物理地址查询缓存。因此,地址被分成三部分
typedef union{ struct{ uint32_t tag:20; //这20位用于和Address Tag比较 uint32_t set:7; //这7位用于在128个组中找到对应的组 uint32_t offset:5; //这5位用于找到cache line中对应的字节 }uint32_t bits; uint32_t value; }physical_addr; |
3.4.8:TLB
每个CPU都有自己的TLB。当CPU的cr3寄存器更新的时候,也就是页表发生切换的时候,会将TLB中的所有数据无效。
3.5:Linux中的分页
对于没有启用物理地址扩展的32位系统,两级页表就够了。可以把中间两级目录设置为1项即可。他们指向一个页表项。
如果是启用了物理地址扩展的32位系统,需要使用3级目录。
如果是64位系统,那么可以使用3级或者4级目录。
3.5.1:线性地址字段
3.5.2:页表处理
3.5.3:物理内存布局
内核将下列页框标记为保留页框:
1:在不可用物理地址范围内的页框。(这些页框映射硬件设备IO的共享内存,或者相应的页框中含有BIOS数据)
2:含有内核代码和已经初始化数据结构的页框。
保留页框中的页不能够被动态分配,或者交换到磁盘上。
默认情况下,物理内存中前1M的页框就是保留页框,用于存储BIOS检查到的系统硬件数据,还有其他原因导致的不能使用的内存。
因此,Linux一般安装在从内存中第2M开始。Linux在内存中的布局如下图所示:
3.5.4:进程页表
进程的线性地址分成两部分,0~3G的线性地址,无论进程处于用户态还是内核态都可以寻址。3~4G的线性地址,只有当进程出与内核态的时候才能寻址。
3.5.5:内核页表
内核维护着一组自己使用的页表。这就是主内核页全局目录。内核初始化自己的页表包含两个阶段:
1:内核创建一个有限的地址空间。这个内存空间包含了内核的代码段,数据段,初始页表,以及用于存放动态数据的128K空间。
2:这个阶段内核充分利用剩余的RAM并建立页表。
3.5.5.1:临时内核页表
临时页全局目录放在swapper_pg_dir变量中,临时页表在pg0变量处开始存放,紧接Linux的内存布局中的_end。假设内核使用的段,临时页表和128K内存范围能够容纳在内存的前8M空间中。
第一个阶段的目的是,在实模式和保护模式下,都能对这8M进行寻址。因此,内核必须建立如下映射:
1:虚拟地址0~0x007fffff——>物理地址0~0x007fffff
2:虚拟地址0xc0000000~0xc07fffff——>物理地址0~0x007fffff
临时页全局目录的初始化是在编译的时候
ENTRY(swapper_pg_dir) .fill 1024,4,0 |
这句话的含义是,在1024个4字节的内存空间上填0。
swapper_pg_dir是一级目录首地址。一级目录包含了1024个二级目录。每个二级目录项是4个字节,对应了1024个页表项。因此,一个二级目录对应的是4K*1024=4M空间,一个一级目录项对应的是4M*1024=4G的内存空间。因此,8M需要两个二级目录项。
临时页全局目录已经初始化完成,还需要二级目录以及页表项。startup_32函数用于建立临时页表。我们的目的是填充页全局目录的第0,1,0x300,0x301项。代码如下:
movl $(pg0 - __PAGE_OFFSET), %edi //__PAGE_OFFSET =0xC0000000,拿到第一个页表项的物理地址 movl $(swapper_pg_dir - __PAGE_OFFSET), %edx //拿到第一个页目录项的物理地址 movl $0x007, %eax /* 0x007 = PRESENT+RW+USER *///注释可知,这里设置eax的值表示页表属性 10: leal 0x007(%edi),%ecx /* Create PDE entry */ //第一次:ecx中的值为edi寄存器中的值加上0x007。edi寄存器中的值是第一个页表项的物理地址。因此,此时ecx中的值实际为第一个页表项的物理地址的前20位,加上表示权限的0x007的12位。 //第二次:ecx中的值为edi寄存器中的值加上0x007。此时edi寄存器中的值是第二个pde对应的第一个pte的物理地址。因此,此时ecx中的值实际为第二个pde对应的第一个pte的物理地址的前20位,加上表示权限的0x007的12位。 movl %ecx,(%edx) /* Store identity PDE entry *///将建立好的pde放入一级目录中。这一次是放入一级页目录的第一项。 movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry *///将同样的pde放入一级目录中。这一次是放入一级页目录的第768项。page_pde_offset=0xc00,一个pde占4字节,因此这里刚好就是一级页目录的第768项。 addl $4,%edx movl $1024, %ecx 11: stosl //将eax中保存的内容放入es:edi指向的内容中。 addl $0x1000,%eax loop 11b /* End condition: we must map up to and including INIT_MAP_BEYOND_END */ /* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */ leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp cmpl %ebp,%eax jb 10b //第一次只会映射4M空间,然后转到10,映射剩余的4M空间 |
这段代码也可以使用上图解释。最终建立了这8M的映射。
然后在后续操作中,向cr3寄存器写入此时建立的页表的地址,然后启用分页单元。
/* * Enable paging */ movl $swapper_pg_dir-__PAGE_OFFSET,%eax movl %eax,%cr3 /* set the page table pointer.. */ movl %cr0,%eax orl $0x80000000,%eax movl %eax,%cr0 /* ..and set paging (PG) bit */ |
3.5.5.2:当RAM小于896M时的最终内核页表
当RAM大小小于896M的时候,留给内核的虚拟地址空间能够映射所有的物理地址空间。
896M的来源是,留给内核的虚拟地址空间的范围是3~4G的1G范围。但是这1G中还需要留下最高的128M用于其他目的(固定映射的线性地址和非连续内存区的线性地址)。因此剩余空间就是896M。
建立页表的代码如下:
void __init paging_init(void) { pagetable_init(); load_cr3(swapper_pg_dir); __flush_tlb_all(); kmap_init(); zone_sizes_init(); } |
其中,关键函数pagetable_init的代码如下:
static void __init pagetable_init (void) { unsigned long vaddr; pgd_t *pgd_base = swapper_pg_dir; /* Enable PSE if available */ //此处是打开页扩展,当设置时,启用4M页与分页;当清除时,将分页限制在4K的页面 if (cpu_has_pse) { set_in_cr4(X86_CR4_PSE); } /* Enable PGE if available */ //此处决定是否启用全局页功能。不需要关注 if (cpu_has_pge) { set_in_cr4(X86_CR4_PGE); __PAGE_KERNEL |= _PAGE_GLOBAL; __PAGE_KERNEL_EXEC |= _PAGE_GLOBAL; } kernel_physical_mapping_init(pgd_base); //直接映射区域的页表建立 remap_numa_kva(); /* * Fixed mappings, only the page table structure has to be * created - mappings will be set by set_fixmap(): */ vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK; page_table_range_init(vaddr, 0, pgd_base); permanent_kmaps_init(pgd_base); } |
当内存大小小于896M的时候,kernel_physical_mapping_init函数的实现如下:
/* * This maps the physical memory to kernel virtual address space, a total * of max_low_pfn pages, by creating page tables starting from address * PAGE_OFFSET. */ static void __init kernel_physical_mapping_init(pgd_t *pgd_base) { unsigned long pfn; pgd_t *pgd; //指针数组,pgd是数组首地址。数组总共有1024项。每一项都是一个pgd_t,也就是一个页目录项。 pmd_t *pmd; //同上。x86架构是二级页表或者三即页表。 pte_t *pte; //指针数组。pte是数组首地址。数组总共有1024项。每一项都是一个pte_t,也就是一个页表项,里面包含的是物理页的首地址还有访问权限。 int pgd_idx, pte_ofs; pgd_idx = pgd_index(PAGE_OFFSET); // PAGE_OFFSET的值是0xC0000000,也就是内核虚拟地址的起始地址。这里拿内核虚拟地址对应的页目录项地址。 pgd = pgd_base + pgd_idx; // pgd_base就是页全局目录的首地址,需要注意的是这个也是一个指针,加的时候也是按照指针加。 pfn = 0; for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) { // PTRS_PER_PGD的值为1024,表示每个pdg_idx对应多少个指针。pgd++是指针++,因此每次是加4字节。 pmd = one_md_table_init(pgd); //pmd是页中间目录。但是此时我们只需要两级页表。因此,这里实际上就是让页中间目录指针等于页全局目录指针。 if (pfn >= max_low_pfn) continue; for (; pfn < max_low_pfn; pmd++) { unsigned int address = pfn * PAGE_SIZE + PAGE_OFFSET; //address是每次要建立映射的时候的虚拟地址。0xC0000000+n*0x1000。 /* Map with big pages if possible, otherwise create normal page tables. */ pte = one_page_table_init(pmd);//这里先不细究了。大致做的事情就是malloc出来一个4K的页表。pte是这个4K页表的首地址。这个页表中有1024个4字节页表项。每个页表项对应的是4K的内存空间。也就是说,一个pte对应的是4M空间 for (pte_ofs = 0; pte_ofs < PTRS_PER_PTE && pfn < max_low_pfn; // PTRS_PER_PTE的值为1024.表示每个PTE有多少个指针。 pte++, pfn++, pte_ofs++) { //pte也是一个指针,++表示指针++。pte指针就是每条页表项的指针 if (is_kernel_text(address)) //判断是否是内核的代码段。内核代码段从哪儿到哪儿可以从vmlinux.lds.S文件中看到 set_pte(pte, pfn_pte(pfn, PAGE_KERNEL_EXEC)); // set_pte这个宏的主要操作,就是将后面得到的值填入前面的指针指示的地址处。因此,pte应该是页表项指针。后面的部分应该是每个页表项的具体内容。 else set_pte(pte, pfn_pte(pfn, PAGE_KERNEL)); } } } } |
/* * Creates a middle page table and puts a pointer to it in the * given global directory entry. This only returns the gd entry * in non-PAE compilation mode, since the middle layer is folded. */ static pmd_t *__init one_md_table_init(pgd_t *pgd) { pud_t *pud; pmd_t *pmd_table; pud = pud_offset(pgd, 0); pmd_table = pmd_offset(pud, 0); return pmd_table; } |
#define pfn_pte(pfn, prot) __pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot)) //可以看到,这里就是构建一个页表项的内容,物理地址的前20位|这个页面的访问权限 |
3.5.5.3:当RAM大小在896M和4096M之间时的最终内核页表
在这种情况下,并不会把所有的RAM映射到内核地址空间,而是将一个896M的物理地址空间范围映射到内核线性地址空间。
这种情况下,内核使用前一种情况相同的方式来初始化页全局目录。
3.5.5.4:当RAM大小在896M和4096M之间时的最终内核页表
发生这种情况的时候,其实是满足了以下三个条件:
1:CPU模型支持物理地址扩展(PAE)。
2:RAM容量大于4GB。
3:内核以PAE支持来编译。
这种情况与之前情况的主要差异是要使用3级页表。
3.5.6:固定映射的线性地址
从上面可知,内核虚拟地址空间中,总是有128M的线性地址空间留作其他用途。因为内核使用这些线性地址空间实现非连续内存分配和固定映射。
之前对内核虚拟地址空间建立的映射,线性地址和物理线性地址的关系总是:
线性地址-PAGE_OFFSET=物理地址。
固定映射的线性地址有很多,应该和系统添加的外设等有关。每个固定映射的线性地址都在这一枚举变量中列出。每个枚举变量都表示了4K大小的,用于固定映射的线性地址。
enum fixed_addresses { FIX_HOLE, FIX_VSYSCALL, …… __end_of_permanent_fixed_addresses, /* temporary boot-time mappings, used before ioremap() is functional */ #define NR_FIX_BTMAPS 16 FIX_BTMAP_END = __end_of_permanent_fixed_addresses, FIX_BTMAP_BEGIN = FIX_BTMAP_END + NR_FIX_BTMAPS - 1, FIX_WP_TEST, __end_of_fixed_addresses }; |
如果需要使用到一个固定映射的线性地址,那么首先使用下面两个宏。
#define set_fixmap(idx, phys) \ __set_fixmap(idx, phys, PAGE_KERNEL) /* * Some hardware wants to get fixmapped without caching. */ #define set_fixmap_nocache(idx, phys) \ __set_fixmap(idx, phys, PAGE_KERNEL_NOCACHE) //phys表示这个固定映射的线性地址要映射到哪个物理地址上去。这个物理地址一般是预先确定的,某些外设的物理地址是固定的(主板决定) |
这里会根据idx,也就是每个枚举变量的值来决定固定映射的线性地址的值。这个值按照这样的算法计算:
#define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT)) // FIXADDR_TOP=0xfffff000。也就是说,留给固定映射的线性地址的空间是0xf8000000~0xfffff000,一共是128M-4K的空间。剩下的0xfffff000往上的4K是留给非连续内存分配使用的。 |
3.5.7:处理硬件高速缓存和TLB
3.5.7.1:处理硬件高速缓存
为了保证高速缓存的命中率达到最高,应该满足以下要求:
一个数据结构中,经常使用的字段放在该数据结构的低偏移部分,尽可能使他们在高速缓存的同一行中。
3.5.7.2:处理TLB