页表管理方法

之前也讲过页表的结构,现在更加详细的讲解一下,页表最主要的作用就是将虚拟地址转化为物理地址,其实他还有两个作用,一个是管理cpu对物理页的访问权限(读写执行权限),另一个是隔离各个进程的地址空间,使其互不影响,从而提供系统的安全性。

页表的使用者大多数不是cpu,而是mmu。打开mmu后,cpu访问的都是虚拟地址,当cpu访问一个虚拟地址的时候,会通过cpu内部的mmu来查询物理地址,mmu首先通过虚拟地址在tlb中查找,如果找到相应表项,直接获得物理地址;如果tlb没有找到,就会通过虚拟地址从页表基地址寄存器保存的页表基地址开始查询多级页表,最终查询到找到相应表项,会将表项缓存到tlb中,然后从表项中获得物理地址。我们可以直接通过页表查找下一级,说明页表基地址寄存器和各级页表项中存放的都是物理地址,而不是虚拟地址。

页表存放在物理内存中,如果需要修改页表,需要将页表所在的物理地址映射到虚拟地址才能访问页表(如内核初始化后会将物理内存线性映射,这样通过物理地址和虚拟地址的偏移就可以获得页表物理地址对应的虚拟地址)。

但是有时候打开mmu后,对没有页表映射的虚拟内存访问或者有页表映射但是没有访问权限都会发生处理器异常,内核选择杀死进程或者panic;通过页表给一段内存设置用户态不可访问, 这样可以做到用户态的用户进程不能访问内核地址空间的内容;而由于用户进程各有一套自己的页表,所以彼此看不到对方的地址空间,更别提访问,造成每个进程都认为自己拥有所有虚拟内存的错觉;通过页表给一段内存设置只读属性,那么就不容许修改这段内存内容,从而保护了这段内存不被改写;对应用户进程地址空间映射的物理内存,内核可以很方便的进行页面迁移和页面交换,而对使用虚拟地址的用户进程来说是透明的;通过页表,很容易实现内存共享,使得一份共享库很多进程都可以映射到自己地址空间使用;通过页表,可以小内存加载大应用程序运行,在运行时按需加载和映射。

假设应用程序需要 2MB 的内存,如果操作系统以 4KB 作为分页的单位,则需要 512 个页面,进而在 TLB 中需要 512 个表项,同时也需要 512 个页表项,操作系统需要经历至少 512 次 TLB Miss 和 512 次缺页中断才能将 2MB 应用程序空间全部映射到物理内存;然而,当操作系统采用 2MB 作为分页的基本单位时,只需要一次 TLB Miss 和一次缺页中断,就可以为 2MB 的应用程序空间建立虚实映射,并在运行过程中无需再经历 TLB Miss 和缺页中断(假设未发生 TLB 项替换和 Swap)。

之前也讲过页表的结构,多级也页表的优势可以节省内存并且按需分配各级页表,把页表离散存储再内存中。但是他也有不好的一面,需要遍历多级页表,需要多次访问内存,实现复杂度高点。所以Linux内核综合考虑,以时间换空间,可以将各级页表放到物理内存的任何地方,无论是硬件遍历还是内核遍历,比一级页表更复杂,但是为了节省内存,内核选择多级页表结构。最后,linux为减小多级页表遍历做了一些优化
1)mmu中添加tlb 。来缓存最近访问的页表表项,根据程序的时间和空间的局部性原理,tlb能有很高的命中率。
2)使用巨型页。减少访存次数(如使用1G或2M巨型页),可以减少tlb miss和缺页异常。

一、页表缓存(TLB)

处理器厂商在内存管理单元(MMU)里增加一个TLB(Translation Lookaside Buffer)的高速缓存,TLB直译为转译后备缓冲器,也被翻译为页表缓存。TLB为CPU的一种缓存,由存储器管理单元用于改进虚拟地址到物理地址的转译速度。TLB 用于缓存一部分标签页表条目。TLB可介于 CPU 和CPU缓存之间,或在 CPU 缓存和主存之间,这取决于缓存使用的是物理寻址或是虚拟寻址。

不同处理器架构的TLB表项的格式不同。ARM64处理器的每条TLB表项不仅包含虚拟地址和物理地址,也包含属性:内存类型、缓存策略、访问权限、地址空间标识符((ASID)及虚拟机标识符(VMID)。地址空间标识符区分不同进程的页表项,虚拟机标识符区分不同虚拟机的页表项。
a.地址空间标识符
为了减少在进程切换时清空页表缓存的需要,ARM64处理器的页表缓存使用非全局 (not global, nG)位区分内核和进程的页表项,使用地址空间标识符(Address Space Identifer,ASID)区分不同进程的页表项。 ARM64处理器ASID长度是由具体实现定义的,可以选择8位或者16位,寄存器 ID_AA64MMFRO_EL1(AArch64内存模型特性寄存器0,AArch64 Memory Model Feature Register 0)的字段ASIDBits存放处理器支持的ASID长度。
平时为了方便描述,假设ASID长度是8位,ASID只有256个值,其中0是保留值,可分配ASID范围是1-255,进程的数量可能超过255个,两个进程ASID可能相同,如何解决此问题,内核引入ASID版本号:

  1. 每个进程有一个64位的软件ASID,低8位存放硬件ASID,高56位存放AS ID版本号;
  2. 64位全局变量的高56位保存全局ASID版本号;
  3. 当进程被调度时,比较进程的ASID版本号和全局ASID版本号,如果版本号相同,直接使用上次分配的硬件ASID。否则需要给进程重新分配硬件ASID。

引入ASID版本号好处在哪里:避免每次进程切换都需要清空页表缓存,只需要在硬件ASID回绕时把处理器的页表缓存清空。
内存描述符的成员context存放架构特定的内存管理上下文,数据类型是结构体mm_context_t,我们之前在mm_struct中讲过,是mm_struct的其中明一个成员 。当全局ASID版本号加1时,每个处理器需要清空页表缓存。ARM64架构定义结构体类型如下:

typedef struct {atomic64_t   id;// 存放内核给进程分配的软件ASIDvoid      *vdso;//虚拟动态链接共享对象,实现vsyscall将内核态的调用映射到用户态的地址空间中unsigned long   flags;
} mm_context_t;

b.虚拟机标识符
虚拟机里面运行的客户操作系统的虚拟地址换成物理地址分两个阶段:第1阶段把虚拟 地址转换成中间物理地址,第2阶段把中间物理地址转换成物理地址。第1阶段转换由客户操作 系统的内存控制,和非虚拟化的转换过程相同。第2阶段转换由虚拟机监控器控制,虚拟机监 控器为每个虚拟机维护一个转换表,分配一个虚拟机标识符(Virtual Machine Identifier,VMID),寄存器VTTBR_EL2(虚拟化转换表基准寄存器,Virtualization Translation Table Base Register)存放当前虚拟机的阶段2转换表的物理地址。每个虚拟机有独立的 ASID 空间,页表缓存使用虚拟机标识符区分不同虚拟机的转换表项,可以避免每次虚拟机切换都需要清空页表缓存,只需要在虚拟机标识符回绕时把处理器的页表缓存清空。

再看看TLB使用的方法,若内核修改了可能缓存在TLB里面的页表项,那么内核必须负责使用旧的TLB表项失效,内核定义每种处理器架构必须实现的函数,可以删除TLB数据的函数。那有没有写入数据的函数呢?当TLB没有命中时,ARM64处理器的内存管理单元自动遍历内存中的页表,把页表复制到TLB,不需要软件把页表写到TLB,所以ARM64架构没有提供写TLB的指令。
举个例子,arm64架构操作TLB的基本函数在arch/arm64/include/asm/tlbflush.h文件中:

/**  TLB Management* ==============**  The TLB specific code is expected to perform whatever tests it needs*   to determine if it should invalidate the TLB for each call.  Start* addresses are inclusive and end addresses are exclusive; it is safe to* round these addresses down.**   flush_tlb_all()**       Invalidate the entire TLB.**    flush_tlb_mm(mm)**      Invalidate all TLB entries in a particular address space.*      - mm    - mm_struct describing address space**  flush_tlb_range(mm,start,end)**     Invalidate a range of TLB entries in the specified address*     space.*     - mm    - mm_struct describing address space*       - start - start address (may not be aligned)*       - end   - end address (exclusive, may not be aligned)** flush_tlb_page(vaddr,vma)**     Invalidate the specified page in the specified address range.*      - vaddr - virtual address (may not be aligned)*     - vma   - vma_struct describing address range** flush_kern_tlb_page(kaddr)**        Invalidate the TLB entry for the specified page.  The address*      will be in the kernels virtual memory space.  Current uses*     only require the D-TLB to be invalidated.*      - kaddr - Kernel virtual memory address*/
//使当前cpu的所有的tlb表失效
static inline void local_flush_tlb_all(void)
{//nshst和ishst区别:nsh是非共享,ish是多核共享dsb(nshst);//数据同步屏障,确保屏障前的当前cpu的存储指令执行完毕/*vmalle1:vm:需要匹配VMIDall:所有ASIDe1:异常级别(有三个异常级别e1 e2 e3)is:表示多核共享*/__tlbi(vmalle1);//使当前核上匹配VMID的,异常级别1的所有TLB失效dsb(nsh);//确保之前的tlb失效指令执行完毕isb();//指令同步屏障,冲刷处理器流水线,重新读取屏障后的所有指令
}//使所有的tlb表失效
static inline void flush_tlb_all(void)
{//nshst和ishst区别:nsh是非共享,ish是多核共享dsb(ishst);//数据同步屏障,确保屏障前的所有cpu的存储指令执行完毕/*vmalle1:vm:需要匹配VMIDall:所有ASIDe1:异常级别(有三个异常级别e1 e2 e3)is:缺少is表示单核非共享*/__tlbi(vmalle1is);//使所有核上匹配VMID的,异常级别1的所有TLB失效dsb(ish);//确保之前的tlb失效指令执行完毕isb();//指令同步屏障,冲刷处理器流水线,重新读取屏障后的所有指令
}//使指定用户地址空间的所有的tlb表失效,参数mm是进程的内存描述符
static inline void flush_tlb_mm(struct mm_struct *mm)
{unsigned long asid = __TLBI_VADDR(0, ASID(mm));dsb(ishst);__tlbi(aside1is, asid);__tlbi_user(aside1is, asid);dsb(ish);
}//使指定用户地址空间的指定页面的tlb表失效
static inline void flush_tlb_page(struct vm_area_struct *vma,unsigned long uaddr)
{unsigned long addr = __TLBI_VADDR(uaddr, ASID(vma->vm_mm));dsb(ishst);__tlbi(vale1is, addr);__tlbi_user(vale1is, addr);dsb(ish);
}

vmalle1is和vmalle1参数含义:
字段<type> 常见选项:( ALL:所有表项。VMALL:当次虚拟机的阶段1的所有表项,即表项的VMID是当前虚拟机的VMID,虚拟机里面运行客户操作系统的虚拟地址转换成物理地址分成两个阶段,第1阶段把虚拟地址转换成中间物理地址,第2阶段把中间物理地址转换成物理地址。ASID:匹配寄存器Xt指定的ASID的表项。VA:匹配寄存器Xt指定的虚拟地址和ASID的表项。VAA:匹配寄存器Xt指定的虚拟地址并且ASID可以是任意值的表项。)
字段< level> 指定异常级别:( E0:异常级别0。 E1:异常级别1。E2:异常级别2。E3:异常级别3。)
字段<IS> 表示内部共享,即多个核共享:如果不使用字段IS,表示非共享,只被一个核使用。
字段<Xt>是X0-X31中的任何一个寄存器。

除了上面的函数外,还有其他基于是上面的复合函数,这里就不一一讲了。而x86只能flush掉系统中当前cpu core的tlb,如果想要flush掉系统中多有cpu core的tlb,只能是通过IPI通知到其他cpu进行处理。

二、巨型页

当运行内存需求量较大的应用程序时,如果使用长度为4KB的页,将会产生较多的TLB未命中和缺页异常,严重影响应用程序的性能。如果使用长度为2MB甚至更大的巨型页,可以大幅减少TLB未命中和缺页异常的数量,大幅提高应用程序的性能。这才是内核引入巨型页(Huge Page)的真正原因。
巨型页首先需要处理器能够支持,然后需要内核支持,内核有两种实现方式:
• 使用hugetlbfs伪文件系统实现巨型页
hugetlbfs文件系统是一个假的文件系统,只是利用了文件系统的编程接口。使用hugetlbfs文件系统实现的巨型页称为传统巨型页、或者称标准巨型页。
• 透明巨型页
透明巨型页,标准巨型页的优点是预先分配巨型页到巨型页池,进程申请巨型页的时候从巨型池取,成功的概率很高,缺点是应用程序需要使用文件系统的编程接口。透明巨型页的优点是对应用程序透明,缺点是动态分配,在内存碎片化的时候分配成功的概率很低。

ARM64处理器支持巨型页的方式有两种:

  1. 通过块描述符支持巨型页
    假如:如果页长度是 4kb, 那么使用 4 级转换表,0级转换表不能使用块描述符, 1 级转换表的块描述符指向 1GB巨型页, 2 级转换表的块描述符指向 2MB 巨型页。

  2. 通过页/块描述符的连续位支持巨型页
    页/块描述符中的连续位指示表项是一个连续表项集合中的一条表项,一个连续表项集合可以被缓存在一条 TLB 表项里面。通常所说的,进程申请了 n 页的虚拟内存区域,然后申请了 n 页的物理内存区域,使用 n 个连续的页表项把每个虚拟页映射到物理页,每个页表项设置了连续标志位,当处理器的内存管理单元遍历内存的页表时,访问到 n 个页表项中的任|可一个页表项。发现页表项设置了连续标志位,就会把 n 个页表项合并以后填充到 TLB 表项。
    假设:页长度为 4KB, 那么使用 4 级转换表, 1 级转换表的块描述符不能使用连续位; 2 级转换表的块描述符支持 16 个连续块,即支持 (162MB=32MB) 巨型页, 3 级转换表的页描述符支持 16 个连续页,即支持 (164KB=64KB) 巨型页。

  3. 巨型页池
    内核使用巨型页池管理巨型页。有的处理器架构支持多种巨型页长度,每种巨型页长度对应一个巨型页池,有一个默认的巨型页长度,默认只创建巨型页长度是默认长度的巨型页池。比如ARM64架构在页长度为4KB的时候支持巨型页长度是1GB 32MB 2MB 64KB,默认的巨型页长度是2MB,默认只有创建巨型页长度是2MB的巨型页池。

巨型页池中的巨型页可分为两种:

  • 永久巨型页:是保留的,不能有其他用途,被预先分配到巨型页池,当进程释放永久巨型页的时候,永久巨型页被归还到巨型页池。
  • 临时巨型页:也称为多余的(surplus)巨型页,当永久巨型页用完的时候,可以从页分配器分配临时巨型页;进程释放临时巨型页的时候,直接释放到页分配器。当设备长时间运行后,内存可能碎片化,分配临时巨型页可能会失败。

巨型页应用操作
为了能以最小的代价实现大页面支持,Linux 操作系统采用了基于 hugetlbfs 特殊文件系统 2M 字节大页面支持。这种采用特殊文件系统形式支持大页面的方式,使得应用程序可以根据需要灵活地选择虚存页面大小,而不会被强制使用 2MB 大页面。现在针对 hugetlb 大页面的应用和内核实现两个方面进行简单的介绍。
hugetlb相当于hugepages页面管理者,页面的分配及释放,都是由此模块负责。hugetlbfs则用于向用户提供一套基于文件系统的巨型页使用界面,下层功能的实现,主要依赖于hugetlb。
巨型页使用准备步骤:

  1. 使用 hugetlbfs 之前,首先需要在编译内核 (make menuconfig) 时配置CONFIG_HUGETLB_PAGE和CONFIG_HUGETLBFS选项,这两个选项均可在 File systems 内核配置菜单中找到。
  2. 内核编译完成并成功启动内核之后,将 hugetlbfs 特殊文件系统挂载到根文件系统的某个目录上去,以使得 hugetlbfs 可以访问。命令:mount none /mnt/huge -t hugetlbfs

做完后,只要是在 /mnt/huge/ 目录下创建的文件,将其映射到内存中时都会使用 2MB 作为分页的基本单位。值得一提的是,hugetlbfs 中的文件是不支持读 / 写系统调用 ( read或write) 的,一般对它的访问都是以内存映射的形式进行的。由于 hugetlbfs 是一个伪文件系统,在磁盘上没有相应的副本,因此在该文件系统中创建一个文件的过程也仅仅是分配虚拟文件系统(VFS)层的 inode、dentry 等结构。甚至连物理内存页面都不会分配,而是在对该文件映射后并访问时,才通过缺页中断进入内核分配大页面并建立虚实映射。在实际应用中,为了使用大页面,一般需要通过调用开源库libhugetlb(github:https://github.com/libhugetlbfs/libhugetlbfs)。libhugetlb库对malloc()/free()等常用的内存相关的库函数进行了重载,以使得应用程序的数据可以放置在采用大页面的内存区域中,以提高内存性能。下面给出两个大页面应用的例子:
申请和释放例子:

/** libhugetlbfs - Easy use of Linux hugepages* Copyright (C) 2005-2006 David Gibson & Adam Litke, IBM Corporation.** This library is free software; you can redistribute it and/or* modify it under the terms of the GNU Lesser General Public License* as published by the Free Software Foundation; either version 2.1 of* the License, or (at your option) any later version.** This library is distributed in the hope that it will be useful, but* WITHOUT ANY WARRANTY; without even the implied warranty of* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU* Lesser General Public License for more details.** You should have received a copy of the GNU Lesser General Public* License along with this library; if not, write to the Free Software* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA*/#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>#include "hugetests.h"/** We cannot test mapping size against huge page size because we are not linked* against libhugetlbfs so gethugepagesize() won't work.  So instead we define* our MIN_PAGE_SIZE as 64 kB (the largest base page available) and make sure* the mapping page size is larger than this.*/
#define MIN_PAGE_SIZE 65536static int block_sizes[] = {sizeof(int), 1024, 128*1024, 1024*1024, 16*1024*1024,32*1024*1024,
};
#define NUM_SIZES   (sizeof(block_sizes) / sizeof(block_sizes[0]))int main(int argc, char *argv[])
{int i;char *env1, *env2, *exe;int expect_hugepage = 0;char *p;test_init(argc, argv);exe = strrchr(test_name, '/');if (exe)exe++;     /* skip over "/" */elseexe = test_name;env1 = getenv("HUGETLB_MORECORE");verbose_printf("HUGETLB_MORECORE=%s\n", env1);env2 = getenv("HUGETLB_RESTRICT_EXE");verbose_printf("HUGETLB_RESTRICT_EXE=%s\n", env2);if (env1 && (!env2 || strstr(env2, exe)))expect_hugepage = 1;verbose_printf("expect_hugepage=%d\n", expect_hugepage);for (i = 0; i < NUM_SIZES; i++) {int size = block_sizes[i];unsigned long long mapping_size;p = malloc(size);if (! p)FAIL("malloc()");verbose_printf("malloc(%d) = %p\n", size, p);memset(p, 0, size);mapping_size = get_mapping_page_size(p);if (expect_hugepage && (mapping_size <= MIN_PAGE_SIZE))FAIL("Address is not hugepage");if (!expect_hugepage && (mapping_size > MIN_PAGE_SIZE))FAIL("Address is unexpectedly huge");free(p);}PASS();
}

映射和修改例子:

/** libhugetlbfs - Easy use of Linux hugepages* Copyright (C) 2005-2006 David Gibson & Adam Litke, IBM Corporation.** This library is free software; you can redistribute it and/or* modify it under the terms of the GNU Lesser General Public License* as published by the Free Software Foundation; either version 2.1 of* the License, or (at your option) any later version.** This library is distributed in the hope that it will be useful, but* WITHOUT ANY WARRANTY; without even the implied warranty of* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU* Lesser General Public License for more details.** You should have received a copy of the GNU Lesser General Public* License along with this library; if not, write to the Free Software* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA*/#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/types.h>#include <hugetlbfs.h>#include "hugetests.h"#define P0 "ffffffff"
#define IOSZ 4096
char buf[IOSZ] __attribute__ ((aligned (IOSZ)));
#define TMPFILE "/tmp/direct"int main(int argc, char *argv[])
{long hpage_size;int fd, dfd;void *p;size_t ret;test_init(argc, argv);hpage_size = check_hugepagesize();fd = hugetlbfs_unlinked_fd();if (fd < 0)FAIL("hugetlbfs_unlinked_fd()");dfd = open(TMPFILE, O_CREAT|O_EXCL|O_DIRECT|O_RDWR, 0600);if (dfd < 0)CONFIG("Failed to open direct-IO file: %s", strerror(errno));unlink(TMPFILE);p = mmap(NULL, hpage_size, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);if (p == MAP_FAILED)FAIL("mmap hugetlbfs file: %s", strerror(errno));memcpy(p, P0, 8);/* Direct write from huge page */ret = write(dfd, p, IOSZ);if (ret == -1)FAIL("Direct-IO write from huge page: %s", strerror(errno));if (ret != IOSZ)FAIL("Short direct-IO write from huge page");if (lseek(dfd, 0, SEEK_SET) == -1)FAIL("lseek: %s", strerror(errno));/* Check for accuracy */ret = read(dfd, buf, IOSZ);if (ret == -1)FAIL("Direct-IO read to normal memory: %s", strerror(errno));if (ret != IOSZ)FAIL("Short direct-IO read to normal memory");if (memcmp(P0, buf, 8))FAIL("Memory mismatch after Direct-IO write");if (lseek(dfd, 0, SEEK_SET) == -1)FAIL("lseek: %s", strerror(errno));/* Direct read to huge page */memset(p, 0, IOSZ);ret = read(dfd, p, IOSZ);if (ret == -1)FAIL("Direct-IO read to huge page: %s\n", strerror(errno));if (ret != IOSZ)FAIL("Short direct-IO read to huge page");/* Check for accuracy */if (memcmp(p, P0, 8))FAIL("Memory mismatch after Direct-IO read");close(dfd);unlink(TMPFILE);PASS();
}

通过文件“cat /proc/sys/nr_hugepages”指定巨型页池中永久巨型页的数量。
通过文件”cat /proc/sys/vm/nr_overcommit_hugepages“指定巨型页池中临时巨型页的数量,当永久巨型页用完的时候,可以从页分配器申请临时巨型页。
注意:nr_hugepages是巨型页池的最小长度,(nr_hugepages+nr_overcommit_hugepages)是巨型页池的最大长度,这两个参数默认值都是0,至少要设置一个,否则分配巨型页会失败。

巨型页内核实现
下面是巨型页的一些基本定义和hstate 结构体,每种巨型页就是通过hstate 结构体来管理的,文件位于include/linux/hugetlb.h:

int hugetlb_max_hstate __read_mostly;//巨型页池中的巨型页数量
unsigned int default_hstate_idx;//默认的巨型页池的索引
struct hstate hstates[HUGE_MAX_HSTATE];//数组元素表示一种大小的page,系统支持HUGE_MAX_HSTATE种巨型页/* Defines one hugetlb page size */
struct hstate {int next_nid_to_alloc;//记录下一个内存节点,加速巨型页申请int next_nid_to_free;//记录下一个内存节点,方便巨型页释放unsigned int order;//表示巨型页大小unsigned long mask;unsigned long max_huge_pages;//最大支持巨型页数量unsigned long nr_huge_pages;//系统支持巨型页数量unsigned long free_huge_pages;//系统空闲巨型页数量unsigned long resv_huge_pages;//系统保留巨型页数量unsigned long surplus_huge_pages;//系统剩余巨型页数量unsigned long nr_overcommit_huge_pages;//系统支持临时巨型页数量struct list_head hugepage_activelist;//指向已经分配的巨型页队列struct list_head hugepage_freelists[MAX_NUMNODES];//指向空闲的巨型页队列unsigned int nr_huge_pages_node[MAX_NUMNODES];//NUMA下各节点的巨型页数量unsigned int free_huge_pages_node[MAX_NUMNODES];//NUMA下各节点的空闲巨型页数量unsigned int surplus_huge_pages_node[MAX_NUMNODES];//NUMA下各节点的剩余巨型页数量
#ifdef CONFIG_CGROUP_HUGETLB/* cgroup control files */struct cftype cgroup_files[5];
#endifchar name[HSTATE_NAME_LEN];//巨型页名称,一般表示为巨型页大小(/sys/kernel/mm/hugepages/)
};

看完hstates结构体,我们看看巨型页初始化函数,hugetlb_init()是Huge Page初始化入口,属于subsys_initcall(),在arch_initcall()之后,fs_initcall()之前。


static int __init hugetlb_init(void)
{int i;if (!hugepages_supported())return 0;//设置巨型页大小,如果command line设置了则跳过,否则使用内核设置的HPAGE_SIZEif (!size_to_hstate(default_hstate_size)) {if (default_hstate_size != 0) {pr_err("HugeTLB: unsupported default_hugepagesz %lu. Reverting to %lu\n",default_hstate_size, HPAGE_SIZE);}default_hstate_size = HPAGE_SIZE;//设置为默认的2M巨型页if (!size_to_hstate(default_hstate_size))hugetlb_add_hstate(HUGETLB_PAGE_ORDER);//将HUGE_MAX_HSTATE中巨型页池添加到hstate数组中并且初始化好}default_hstate_idx = hstate_index(size_to_hstate(default_hstate_size));//设置巨型页在hstates中对应索引号if (default_hstate_max_huge_pages) {//最大默认页数为0,否则对每种巨型页设置一下if (!default_hstate.max_huge_pages)default_hstate.max_huge_pages = default_hstate_max_huge_pages;}hugetlb_init_hstates();//根据当前hstate->order,初始化巨型页池hstates结构体gather_bootmem_prealloc();report_hugepages();//输出当前系统支持的不同巨型页大小以及分配页数。hugetlb_sysfs_init();//在/sys/kernel/mm/hugepages目录下针对不同大小的巨型页创建目录hugetlb_register_all_nodes();//处理NUMA架构下不同node的巨型页//创建/sys/fs/cgroup/hugetlb下节点:hugetlb.2MB.failcnt、hugetlb.2MB.limit_in_bytes、hugetlb.2MB.max_usage_in_bytes、hugetlb.2MB.usage_in_byteshugetlb_cgroup_file_init();#ifdef CONFIG_SMPnum_fault_mutexes = roundup_pow_of_two(8 * num_possible_cpus());
#elsenum_fault_mutexes = 1;
#endifhugetlb_fault_mutex_table =kmalloc_array(num_fault_mutexes, sizeof(struct mutex),GFP_KERNEL);BUG_ON(!hugetlb_fault_mutex_table);for (i = 0; i < num_fault_mutexes; i++)mutex_init(&hugetlb_fault_mutex_table[i]);//初始化巨型页TLB锁return 0;
}
subsys_initcall(hugetlb_init);

其中主要是hugetlb_add_hstate把每一种巨型页添加到hstates对应数组中,我们看看其实现:

void __init hugetlb_add_hstate(unsigned int order)
{struct hstate *h;unsigned long i;if (size_to_hstate(PAGE_SIZE << order)) {//避免相同大小巨型页两次加入pr_warn("hugepagesz= specified twice, ignoring\n");return;}BUG_ON(hugetlb_max_hstate >= HUGE_MAX_HSTATE);BUG_ON(order == 0);//下面是设置hstates中对应巨型页池属性。h = &hstates[hugetlb_max_hstate++];//获取最新巨型页池地址h->order = order;//设置巨型页池中巨型页大小h->mask = ~((1ULL << (order + PAGE_SHIFT)) - 1);h->nr_huge_pages = 0;h->free_huge_pages = 0;for (i = 0; i < MAX_NUMNODES; ++i)INIT_LIST_HEAD(&h->hugepage_freelists[i]);//NUMA下初始化各个节点的巨型页队列头INIT_LIST_HEAD(&h->hugepage_activelist);//初始化巨型页已经使用的巨型页队列h->next_nid_to_alloc = first_memory_node;//下一个可以申请的巨型页初始化为第一个内存节点h->next_nid_to_free = first_memory_node;//下一个可以释放的巨型页初始化为第一个内存节点snprintf(h->name, HSTATE_NAME_LEN, "hugepages-%lukB",huge_page_size(h)/1024);parsed_hstate = h;
}

hstates初始化就这些,下面说说hugetlbfs的初始化,否则我们无法调用其接口来使用巨型页,文件位于fs/hugetlbfs/inode.c:

static int __init init_hugetlbfs_fs(void)
{struct hstate *h;int error;int i;if (!hugepages_supported()) {pr_info("disabling because there are no supported hugepage sizes\n");return -ENOTSUPP;}error = -ENOMEM;//初始化hugetlbfs文件系统inode slab缓存,后续hugetlbfs的inode从这里面分配hugetlbfs_inode_cachep = kmem_cache_create("hugetlbfs_inode_cache",sizeof(struct hugetlbfs_inode_info),0, SLAB_ACCOUNT, init_once);if (hugetlbfs_inode_cachep == NULL)goto out2;error = register_filesystem(&hugetlbfs_fs_type);//注册hugetlbfs文件系统,将hugetlbfs_fs_type加入到全局file_systems链表中if (error)goto out;i = 0;for_each_hstate(h) {char buf[50];unsigned ps_kb = 1U << (h->order + PAGE_SHIFT - 10);snprintf(buf, sizeof(buf), "pagesize=%uK", ps_kb);//创建hugetlbfs的super_block、entry、inode,并建立它们之间的相互映射hugetlbfs_vfsmount[i] = kern_mount_data(&hugetlbfs_fs_type,buf);if (IS_ERR(hugetlbfs_vfsmount[i])) {pr_err("Cannot mount internal hugetlbfs for ""page size %uK", ps_kb);error = PTR_ERR(hugetlbfs_vfsmount[i]);hugetlbfs_vfsmount[i] = NULL;}i++;}/* Non default hstates are optional */if (!IS_ERR_OR_NULL(hugetlbfs_vfsmount[default_hstate_idx]))return 0;out:kmem_cache_destroy(hugetlbfs_inode_cachep);out2:return error;
}
fs_initcall(init_hugetlbfs_fs)

hugetlbfs这个伪文件系统位于fs/hugetlbfs/inode.c,有兴趣可以自己看,这里仅仅做一个引子,后面文件系统系列会详细讲解,这里就不多描述了。
看完初始化流程,下面将描述进程在设置为大页面的虚存区域产生 Page Fault 时的缺页中断处理流程,我们还记得普通页的缺页处理吗?其调用过程为do_page_fault —> __do_page_fault —> handle_mm_fault ,在handle_mm_fault 中会判断是否巨型页缺页,不是巨型页就会进入普通页的缺页处理函数hugetlb_fault,而巨型页会进入巨型页的缺页处理函数hugetlb_fault,函数位于mm/hugetlb.c:

vm_fault_t hugetlb_fault(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long address, unsigned int flags)
{pte_t *ptep, entry;spinlock_t *ptl;vm_fault_t ret;u32 hash;pgoff_t idx;struct page *page = NULL;struct page *pagecache_page = NULL;struct hstate *h = hstate_vma(vma);struct address_space *mapping;int need_wait_lock = 0;unsigned long haddr = address & huge_page_mask(h);ptep = huge_pte_offset(mm, haddr, huge_page_size(h));//根据addr查找对应的pte表项的地址(pgd->pud->pmd->pte)if (ptep) {entry = huge_ptep_get(ptep);//查找到就说明并没有缺页,要找到对应的entryif (unlikely(is_hugetlb_entry_migration(entry))) {//如果巨型页迁移了migration_entry_wait_huge(vma, mm, ptep);return 0;} else if (unlikely(is_hugetlb_entry_hwpoisoned(entry)))//如果是巨型透明页return VM_FAULT_HWPOISON_LARGE |VM_FAULT_SET_HINDEX(hstate_index(h));} else {ptep = huge_pte_alloc(mm, haddr, huge_page_size(h));//查找失败则分配pte页表项if (!ptep)return VM_FAULT_OOM;}mapping = vma->vm_file->f_mapping;idx = vma_hugecache_offset(h, vma, haddr);/** Serialize hugepage allocation and instantiation, so that we don't* get spurious allocation failures if two CPUs race to instantiate* the same page in the page cache.*/hash = hugetlb_fault_mutex_hash(h, mm, vma, mapping, idx, haddr);mutex_lock(&hugetlb_fault_mutex_table[hash]);//巨型页tlb上锁entry = huge_ptep_get(ptep);//要找到对应的entryif (huge_pte_none(entry)) {//巨型页缺页处理函数,用于分配物理内存、建立虚实映射ret = hugetlb_no_page(mm, vma, mapping, idx, address, ptep, flags);goto out_mutex;}ret = 0;/** entry could be a migration/hwpoison entry at this point, so this* check prevents the kernel from going below assuming that we have* a active hugepage in pagecache. This goto expects the 2nd page fault,* and is_hugetlb_entry_(migration|hwpoisoned) check will properly* handle it.*///判断当前entry是不是活动的,如果是,说明缓存中有一个活动的大页面if (!pte_present(entry))goto out_mutex;/** If we are going to COW the mapping later, we examine the pending* reservations for this page now. This will ensure that any* allocations necessary to record that reservation occur outside the* spinlock. For private mappings, we also lookup the pagecache* page now as it is used to determine if a reservation has been* consumed.*///如果引发缺页中断的内存操作是写操作,且该大页面被设置为只读,则预先做一次写时复制操作,避免产生缺页中断而影响性能if ((flags & FAULT_FLAG_WRITE) && !huge_pte_write(entry)) {if (vma_needs_reservation(h, vma, haddr) < 0) {ret = VM_FAULT_OOM;goto out_mutex;}/* Just decrements count, does not deallocate */vma_end_reservation(h, vma, haddr);if (!(vma->vm_flags & VM_MAYSHARE))pagecache_page = hugetlbfs_pagecache_page(h,vma, haddr);}ptl = huge_pte_lock(h, mm, ptep);//巨型页tlb上锁/* Check for a racing update before calling hugetlb_cow */if (unlikely(!pte_same(entry, huge_ptep_get(ptep))))goto out_ptl;/** hugetlb_cow() requires page locks of pte_page(entry) and* pagecache_page, so here we need take the former one* when page != pagecache_page or !pagecache_page.*/page = pte_page(entry);//根据entry查找到巨型页if (page != pagecache_page)if (!trylock_page(page)) {need_wait_lock = 1;goto out_ptl;}get_page(page);//进行细粒度页面上锁//如果引发缺页中断的内存操作是写操作if (flags & FAULT_FLAG_WRITE) {if (!huge_pte_write(entry)) {//巨型页写时复制操作函数ret = hugetlb_cow(mm, vma, address, ptep,pagecache_page, ptl);goto out_put_page;}entry = huge_pte_mkdirty(entry);}entry = pte_mkyoung(entry);//设置页表项的访问标志位,表示页数据刚刚被访问了(热页),避免被换出//设置巨型页属性,调用update_mmu_cache更新内存管理单元的页表高速缓存cacheif (huge_ptep_set_access_flags(vma, haddr, ptep, entry,flags & FAULT_FLAG_WRITE))update_mmu_cache(vma, haddr, ptep);
out_put_page:if (page != pagecache_page)unlock_page(page);put_page(page);//进行细粒度页面解锁
out_ptl:spin_unlock(ptl);if (pagecache_page) {unlock_page(pagecache_page);put_page(pagecache_page);//进行细粒度页面解锁}
out_mutex:mutex_unlock(&hugetlb_fault_mutex_table[hash]);/** Generally it's safe to hold refcount during waiting page lock. But* here we just wait to defer the next page fault to avoid busy loop and* the page is not used after unlocked before returning from the current* page fault. So we are safe from accessing freed page, even if we wait* here without taking refcount.*/if (need_wait_lock)wait_on_page_locked(page);return ret;
}

其中主要巨型页缺页处理函数hugetlb_no_page:

static vm_fault_t hugetlb_no_page(struct mm_struct *mm,struct vm_area_struct *vma,struct address_space *mapping, pgoff_t idx,unsigned long address, pte_t *ptep, unsigned int flags)
{struct hstate *h = hstate_vma(vma);vm_fault_t ret = VM_FAULT_SIGBUS;int anon_rmap = 0;unsigned long size;struct page *page;pte_t new_pte;spinlock_t *ptl;unsigned long haddr = address & huge_page_mask(h);bool new_page = false;/** Currently, we are forced to kill the process in the event the* original mapper has unmapped pages from the child due to a failed* COW. Warn that such a situation has occurred as it may not be obvious*/if (is_vma_resv_set(vma, HPAGE_RESV_UNMAPPED)) {pr_warn_ratelimited("PID %d killed due to inadequate hugepage pool\n",current->pid);return ret;}/** Use page lock to guard against racing truncation* before we get page_table_lock.*/
retry:page = find_lock_page(mapping, idx);//使用页锁来防止被截断if (!page) {size = i_size_read(mapping->host) >> huge_page_shift(h);if (idx >= size)goto out;/** Check for page in userfault range*///如果用户丢失vma信息,说明之前已经分配了物理页if (userfaultfd_missing(vma)) {u32 hash;struct vm_fault vmf = {.vma = vma,.address = haddr,.flags = flags,/** Hard to debug if it ends up being* used by a callee that assumes* something about the other* uninitialized fields... same as in* memory.c*/};/** hugetlb_fault_mutex must be dropped before* handling userfault.  Reacquire after handling* fault to make calling code simpler.*/hash = hugetlb_fault_mutex_hash(h, mm, vma, mapping,idx, haddr);mutex_unlock(&hugetlb_fault_mutex_table[hash]);//这里主要处理一下用户异常后返回ret = handle_userfault(&vmf, VM_UFFD_MISSING);mutex_lock(&hugetlb_fault_mutex_table[hash]);goto out;}page = alloc_huge_page(vma, haddr, 0);//分配巨型页页面if (IS_ERR(page)) {ret = vmf_error(PTR_ERR(page));goto out;}clear_huge_page(page, address, pages_per_huge_page(h));__SetPageUptodate(page);new_page = true;if (vma->vm_flags & VM_MAYSHARE) {//如果是共享页,将该页面加入到该hugetlb文件对应的Page Cache中,以便可以与其它进程共享该大页面int err = huge_add_to_page_cache(page, mapping, idx);if (err) {put_page(page);if (err == -EEXIST)goto retry;goto out;}} else {lock_page(page);if (unlikely(anon_vma_prepare(vma))) {//确保anon_vma已经分配成功ret = VM_FAULT_OOM;goto backout_unlocked;}anon_rmap = 1;}} else {/** If memory error occurs between mmap() and fault, some process* don't have hwpoisoned swap entry for errored virtual address.* So we need to block hugepage fault by PG_hwpoison bit check.*/if (unlikely(PageHWPoison(page))) {ret = VM_FAULT_HWPOISON |VM_FAULT_SET_HINDEX(hstate_index(h));goto backout_unlocked;}}/** If we are going to COW a private mapping later, we examine the* pending reservations for this page now. This will ensure that* any allocations necessary to record that reservation occur outside* the spinlock.*///如果引发缺页中断的内存操作是写操作,且该大页面被设置为只读,则预先做一次写时复制操作,避免产生缺页中断而影响性能if ((flags & FAULT_FLAG_WRITE) && !(vma->vm_flags & VM_SHARED)) {if (vma_needs_reservation(h, vma, haddr) < 0) {ret = VM_FAULT_OOM;goto backout_unlocked;}/* Just decrements count, does not deallocate */vma_end_reservation(h, vma, haddr);}ptl = huge_pte_lock(h, mm, ptep);//巨型页页表上锁size = i_size_read(mapping->host) >> huge_page_shift(h);if (idx >= size)goto backout;ret = 0;if (!huge_pte_none(huge_ptep_get(ptep)))goto backout;if (anon_rmap) {ClearPagePrivate(page);hugepage_add_new_anon_rmap(page, vma, haddr);} elsepage_dup_rmap(page, true);new_pte = make_huge_pte(vma, page, ((vma->vm_flags & VM_WRITE)&& (vma->vm_flags & VM_SHARED)));set_huge_pte_at(mm, haddr, ptep, new_pte);hugetlb_count_add(pages_per_huge_page(h), mm);//分配成功,hugetlb计数修改//如果引发缺页中断的内存操作是写操作,而且是共享的,则在这里调用hugetlb_cow分配物理页if ((flags & FAULT_FLAG_WRITE) && !(vma->vm_flags & VM_SHARED)) {/* Optimization, do the COW without a second fault */ret = hugetlb_cow(mm, vma, address, ptep, page, ptl);}spin_unlock(ptl);/** Only make newly allocated pages active.  Existing pages found* in the pagecache could be !page_huge_active() if they have been* isolated for migration.*///如果激活新分配的页面,设置它们状态是否可以被隔离以进行迁移。 if (new_page)set_page_huge_active(page);unlock_page(page);
out:return ret;backout:spin_unlock(ptl);
backout_unlocked:unlock_page(page);restore_reserve_on_error(h, vma, haddr, page);put_page(page);goto out;
}

其中主要分配巨型页页面的函数是alloc_huge_page:

struct page *alloc_huge_page(struct vm_area_struct *vma,unsigned long addr, int avoid_reserve)
{struct hugepage_subpool *spool = subpool_vma(vma);struct hstate *h = hstate_vma(vma);struct page *page;long map_chg, map_commit;long gbl_chg;int ret, idx;struct hugetlb_cgroup *h_cg;idx = hstate_index(h);/** Examine the region/reserve map to determine if the process* has a reservation for the page to be allocated.  A return* code of zero indicates a reservation exists (no change).*///系统初始化时为每个NUMA node都初始化了相应的空闲大页面链表hugepage_freelists,并分配了全部的大页面//检查区域/预留映射,以确定流程是否预留了要分配的页面map_chg = gbl_chg = vma_needs_reservation(h, vma, addr);if (map_chg < 0)return ERR_PTR(-ENOMEM);/** Processes that did not create the mapping will have no* reserves as indicated by the region/reserve map. Check* that the allocation will not exceed the subpool limit.* Allocations for MAP_NORESERVE mappings also need to be* checked against any subpool limit.*/if (map_chg || avoid_reserve) {//如果没有预留gbl_chg = hugepage_subpool_get_pages(spool, 1);//则检查分配是否超过子池限制if (gbl_chg < 0) {vma_end_reservation(h, vma, addr);return ERR_PTR(-ENOSPC);}/** Even though there was no reservation in the region/reserve* map, there could be reservations associated with the* subpool that can be used.  This would be indicated if the* return value of hugepage_subpool_get_pages() is zero.* However, if avoid_reserve is specified we still avoid even* the subpool reservations.*/if (avoid_reserve)gbl_chg = 1;}ret = hugetlb_cgroup_charge_cgroup(idx, pages_per_huge_page(h), &h_cg);if (ret)goto out_subpool_put;spin_lock(&hugetlb_lock);/** glb_chg is passed to indicate whether or not a page must be taken* from the global free pool (global change).  gbl_chg == 0 indicates* a reservation exists for the allocation.*///在hugepage_freelists中查找需要的vmapage = dequeue_huge_page_vma(h, vma, addr, avoid_reserve, gbl_chg);if (!page) {spin_unlock(&hugetlb_lock);page = alloc_buddy_huge_page_with_mpol(h, vma, addr);//找不到,需要再分配if (!page)goto out_uncharge_cgroup;if (!avoid_reserve && vma_has_reserves(vma, gbl_chg)) {SetPagePrivate(page);h->resv_huge_pages--;}spin_lock(&hugetlb_lock);list_move(&page->lru, &h->hugepage_activelist);/* Fall through */}hugetlb_cgroup_commit_charge(idx, pages_per_huge_page(h), h_cg, page);spin_unlock(&hugetlb_lock);set_page_private(page, (unsigned long)spool);//设置页面属性map_commit = vma_commit_reservation(h, vma, addr);//根据预留的消耗调整预留映射if (unlikely(map_chg > map_commit)) {/** The page was added to the reservation map between* vma_needs_reservation and vma_commit_reservation.* This indicates a race with hugetlb_reserve_pages.* Adjust for the subpool count incremented above AND* in hugetlb_reserve_pages for the same page.  Also,* the reservation count added in hugetlb_reserve_pages* no longer applies.*/long rsv_adjust;rsv_adjust = hugepage_subpool_put_pages(spool, 1);hugetlb_acct_memory(h, -rsv_adjust);}return page;out_uncharge_cgroup:hugetlb_cgroup_uncharge_cgroup(idx, pages_per_huge_page(h), h_cg);
out_subpool_put:if (map_chg || avoid_reserve)hugepage_subpool_put_pages(spool, 1);vma_end_reservation(h, vma, addr);return ERR_PTR(-ENOSPC);
}

巨型页的缺页处理就讲到这里。

linux内存管理(十)-页表管理相关推荐

  1. linux内存不足时缩减缓存,Linux内存及页面缓存管理概要总结

    物理内存管理 页面内存管理 Linux把物理内存划分为若干个大小相同(通常是4k)的页面,每个页面使用struct page描述,在内核初始化时会根据物理内存大小和页面大小,初始化一个struct p ...

  2. Linux内核机制总结内存管理之页表缓存(十九)

    文章目录 1 页表缓存 1.1 TLB表项格式 1.2 TLB管理 1.3 地址空间标识符 1.4 虚拟机标识符 重要:本系列文章内容摘自<Linux内核深度解析>基于ARM64架构的Li ...

  3. Linux内存page,【原创】(十四)Linux内存管理之page fault处理

    背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...

  4. Linux内存管理 (2)页表的映射过程

    专题:Linux内存管理专题 关键词:swapper_pd_dir.ARM PGD/PTE.Linux PGD/PTE.pgd_offset_k. Linux下的页表映射分为两种,一是Linux自身的 ...

  5. linux 进程装入 物理内存 页表,linux内存管理解析----linux物理,线性内存布局及页表的初始化...

    主要议题: 1分页,分段模式及实模式 2Linux分页 3linux内存线性地址空间布局及物理内存空间布局 4linux页表初始化及代码解析 1.1.1内存寻址和保护模式 在X86平台上,内存控制单元 ...

  6. linux内存管理(八)-不连续页分配和页表

    一.不连续页 1.不连续页的接口函数 a.用户台接口函数 //分配不连续的物理页并且把物理页映射到连续的虚拟地址空间: void *vmalloc(unsigned long size);//释放vm ...

  7. linux cma内存,【原创】(十六)Linux内存管理之CMA,

    [原创](十六)Linux内存管理之CMA, 背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. ...

  8. 伙伴系统之避免碎片--Linux内存管理(十六)

    原文链接:https://blog.csdn.net/gatieme/article/details/52694362 日期 内核版本 架构 作者 GitHub CSDN 2016-09-28 Lin ...

  9. Linux内存管理二(页表)

    1.综述 用来将虚拟地址空间映射到物理地址空间的数据结构称为页表,即页表用于建立用户进程的虚拟地址空间和系统物理内存(内存.页帧)之间的关联 实现两个地址空间的关联最容易的方法是使用数组,对虚拟地址空 ...

最新文章

  1. 2021深度学习的研究方向推荐!Transformer、Self-Supervised、Zero-Shot和多模态
  2. 我的世界java版怎么添加光影,《我的世界》中国版光影添加教程 国服怎么添加光影?...
  3. Spring Boot 2.4版本前后的分组配置变化及对多环境配置结构的影响
  4. 大规模markpoint特效
  5. JDBC连接mysql数据库操作
  6. dsp怪胎_2012年6月最佳怪胎文章
  7. 6张图,带你深入理解GitOps,真硬核!
  8. 阿里云构建千万级别架构演变之路
  9. 【Elasticsearch】Elasticsearch如何物理删除给定期限的历史数据?
  10. [AX]AX2012 C#使用IIS宿主AIF服务的一些问题
  11. linux下mysql 8.0忘记密码后重置密码
  12. Emacs+Lisp环境搭建
  13. 基于javaweb+mysql数据库实现的宠物领养|流浪猫狗网站项目源代码
  14. HUSTOJ安装记录
  15. CardView的基本使用、DrawerLayout 滑动菜单、Fragment
  16. 《富爸爸穷爸爸》书摘-为什么要教授财务知识
  17. -bash: vim: 未找到命令
  18. Partial Dependence Plots 从原理到实战
  19. linux 查看内存fru,linux – 查找NIC的网络百分比
  20. WEB前端项目实战/酒仙网开发-李强强-专题视频课程

热门文章

  1. Cocos2d-x移植Android 常见问题处理办法
  2. ssh 执行多条命令包含awk的用法
  3. Python之路,Day2 - Python基础,列表,循环
  4. jquery中的页面加载方法load()
  5. HTML Table 冻结行列
  6. 软件测试--环境讲解
  7. 疫情之下,精准测试的智能可信模式正在成为中流砥柱
  8. Linux磁盘分区详解(parted)
  9. mysql jpa uuid_在spring data jpa使用UUID
  10. 03-09 toast 控件识别