深入理解Linux虚拟内存管理(四)(上):https://developer.aliyun.com/article/1597778
3、释放内存
(1)free_bootmem
// mm/bootmem.c void __init free_bootmem (unsigned long addr, unsigned long size) { // 调用核心函数,以 contig_page_data 的启动内存数据为参数。 return(free_bootmem_core(contig_page_data.bdata, addr, size)); }
(2)free_bootmem_core
static void __init free_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long size) { unsigned long i; unsigned long start; /* * round down end of usable mem, partially free pages are * considered reserved. */ unsigned long sidx; // 计算受影响的末端索引 eidx。 unsigned long eidx = (addr + size - bdata->node_boot_start)/PAGE_SIZE; // 如果末端地址不是页对齐,则它为受影响区域的末端向下取整到最近页面。 unsigned long end = (addr + size)/PAGE_SIZE; // 如果释放了大小为 0 的页面,则调用 BUG()。 if (!size) BUG(); // 如果末端 PFN 在该节点可寻址内存之后,则这里调用 BUG()。 if (end > bdata->node_low_pfn) BUG(); /* * Round up the beginning of the address. */ // 如果起始地址不是页对齐的,则将其向上取整到最近页面。 start = (addr + PAGE_SIZE-1) / PAGE_SIZE; // 计算要释放的起始索引。 sidx = start - (bdata->node_boot_start/PAGE_SIZE); // 释放全部满页面,这里清理在启动位图中的位。如果已经为 0,则表示是一次重 // 复释放或内存从未使用,这里调用 BUG()。 for (i = sidx; i < eidx; i++) { if (!test_and_clear_bit(i, bdata->node_bootmem_map)) BUG(); } }
(3)总结
由上分析可知:函数 free_bootmem_core 主要就是把对应页帧号的位图(bootmem_data_t ->node_bootmem_map)设置为 0, 来表示对应的页是空闲的。
4、释放引导内存分配器
在系统启动后,引导内存分配器就不再需要了,这些函数负责销毁不需要的引导内存分配器结构,并将其余的页面传入到普通的物理页面分配器中。
(1)mem_init
这个函数的调用图如图 5.2 所示。引导内存分配器的这个函数的重要部分是它调用 free_pages_init() 。这个函数分成如下几部分:
函数前面部分为高端内存地址设置在全局 mem_map 中的 PFN,并将系统范围的 0 页面清零。
调用 free_pages_init() 。
打印系统中可用内存的提示信息。
如果配置项可用,则检查 CPU 是否支持 PAE,并测试 CPU 中的 WP 位。这很重要,如果没有 WP 位,就必须调用 verify_write() 对内核到用户空间的每一次写检查。这仅应用于像 386 一样的老处理器。
填充 swapper_pg_dir 的 PGD 用户空间部分的表项,其中有内核页表。0 页面映射到所有的表项。
// arch/i386/mm/init.c void __init mem_init(void) { int codesize, reservedpages, datasize, initsize; // ... // 这个函数记录从 mem_map(highmem_start_page) 处高端内存开始的 PFN,系统中最 // 大的页面数 (max_mapnr 和 num_physpages),以及最后是可被内核映射的最大页面数 // (num_mappedpages)。 set_max_mapnr_init(); // high_memory 是高端内存开始处的虚拟地址。 high_memory = (void *) __va(max_low_pfn * PAGE_SIZE); /* clear the zero-page */ // 将系统范围内的 0 页面清 0。 memset(empty_zero_page, 0, PAGE_SIZE); // 调用 free_pages_init ,在那里告知引导内存分配器释放它自身以 // 及初始化高端内存的所有页面,以用于伙伴分配器。 reservedpages = free_pages_init(); // 计算用于初始化代码和数据的代码段、数据段和内存大小。(所有标识为 __init 的函 // 数都将在这一部分) 。 codesize = (unsigned long) &_etext - (unsigned long) &_text; datasize = (unsigned long) &_edata - (unsigned long) &_etext; initsize = (unsigned long) &__init_end - (unsigned long) &__init_begin; // ... if (boot_cpu_data.wp_works_ok < 0) test_wp_bit(); #ifndef CONFIG_SMP // 遍历 swapper_pg_dir 的用户空间部分用到的每个 PGD,并将 0 页面与之映射。 zap_low_mappings(); #endif }
(a)⇐ set_max_mapnr_init
// mm/memory.c unsigned long max_mapnr; unsigned long num_physpages; unsigned long num_mappedpages; void * high_memory; struct page *highmem_start_page; // ============================================================================== // arch/i386/mm/init.c static void __init set_max_mapnr_init(void) { #ifdef CONFIG_HIGHMEM highmem_start_page = mem_map + highstart_pfn; max_mapnr = num_physpages = highend_pfn; num_mappedpages = max_low_pfn; #else max_mapnr = num_mappedpages = num_physpages = max_low_pfn; #endif }
(2)free_pages_init
这个函数有 3 个重要的功能:调用 free_all_bootmem(),销毁引导内存分配器,以及释放伙伴分配器的所有高端内存。
// arch/i386/mm/init.c static int __init free_pages_init(void) { extern int ppro_with_ram_bug(void); int bad_ppro, reservedpages, pfn; // 在奔腾 Pro 版本中有一个 bug,阻止高端内存中的某些页被使用。 // 函数 ppro_with_ram_bug() 检查是否存在这个 bug。 bad_ppro = ppro_with_ram_bug(); /* this will put all low memory onto the freelists */ // 调用 free_all_bootmem() 来销毁引导内存分配器。 totalram_pages += free_all_bootmem(); // 遍历所有的内存,计数保留给引导内存分配器的页面数。 reservedpages = 0; for (pfn = 0; pfn < max_low_pfn; pfn++) { /* * Only count reserved RAM pages */ if (page_is_ram(pfn) && PageReserved(mem_map+pfn)) reservedpages++; } #ifdef CONFIG_HIGHMEM // 对高端内存中的每一页,这里调用 one_highpage_init()。这个 // 函数清除 PG_reserved 位,设置 PG_high 位,设置计数为 1,调用 __free_pages() 来给 // 伙伴分配器分配页面,增加 totalhigh_pages 计数。杀死有 bug 的奔腾 Pro 的页面将被跳过。 for (pfn = highend_pfn-1; pfn >= highstart_pfn; pfn--) one_highpage_init((struct page *) (mem_map + pfn), pfn, bad_ppro); totalram_pages += totalhigh_pages; #endif return reservedpages; }
(3)one_highpage_init
这个函数初始化高端内存中的页面信息,并检查以保证页面不会在某些奔腾 Pro 上报 bug。它仅在编译时指定了 CONFIG_HIGHMEM 的情况下存在。
// arch/i386/mm/init.c #ifdef CONFIG_HIGHMEM void __init one_highpage_init(struct page *page, int pfn, int bad_ppro) { // 如果在 PFN 处不存在页面,则这里标记 struct page 为保留的,所以不会使用该页面。 if (!page_is_ram(pfn)) { SetPageReserved(page); return; } // 如果当前运行的 CPU 是有奔腾 Pro bug 的,而这个页面会导致崩责 (page_kill_ppro() // 进行这项检查) ,这里就标记页面为保留的,它也不会被分配。 if (bad_ppro && page_kills_ppro(pfn)) { SetPageReserved(page); return; } // 从这里开始,就会使用高端内存的页面,所以这里首先清除保留位,然后将它们分配 // 给伙伴分配器。 ClearPageReserved(page); // 设置 PG_highmem 位表示它是一个高端内存页面。 set_bit(PG_highmem, &page->flags); // 初始化页面的使用计数为 1,它由伙伴分配器设为 0。 atomic_set(&page->count, 1); // 利用 __free_page() 来释放页面,这样伙伴分配器会将高端内存页面 // 加到它的空闲链表中。 __free_page(page); // 将可用的高内存页面总数 (totalhigh_pages) 加 1。 totalhigh_pages++; } #endif /* CONFIG_HIGHMEM */
(4)free_all_bootmem
// mm/bootmem.c // 对 NUMA 而言,这里仅是简单地调用以特定 pgdat 为参数的核心函数。 unsigned long __init free_all_bootmem_node (pg_data_t *pgdat) { return(free_all_bootmem_core(pgdat)); } // 对 UMA 而言,这里调用仅以节点 contig_page_data 为参数的核心函数。 unsigned long __init free_all_bootmem (void) { return(free_all_bootmem_core(&contig_page_data)); }
(5)free_all_bootmem_core
这是销毁引导内存分配器的核心函数。它分为如下两个主要部分:
- 对该节点已知的未分配页面,它完成如下步骤:
- 清除结构页面中的 PG_reserved 标志。
- 置计数为 1。
- 调用 __free_pages(),这样伙伴分配器可以构建它的空闲链表。
- 释放用于位图的页面,并将其释放给伙伴分配器。
// mm/bootmem.c static unsigned long __init free_all_bootmem_core(pg_data_t *pgdat) { struct page *page = pgdat->node_mem_map; bootmem_data_t *bdata = pgdat->bdata; unsigned long i, count, total = 0; unsigned long idx; // 如果没有映射图,则意味着这个节点已经被释放了,且肯定在依赖于体系结构的代码 // 中出现了错误,所以这里调用 BUG()。 if (!bdata->node_bootmem_map) BUG(); // 将页面数的运行数传给伙伴分配器。 count = 0; // idx 是该节点最后可寻址的索引。 idx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT); // 遍历该节点可寻址的所有页面。 for (i = 0; i < idx; i++, page++) { // 如果该页标记为空闲,则 ..... if (!test_bit(i, bdata->node_bootmem_map)) { // 将传给伙伴分配器页面数的运行数加 1。 count++; // 清除 PG_reserved 标志。 ClearPageReserved(page); // 设置计数为 1,这样伙伴分配器将考虑这是页面的最后一个用户,并将其放入到空闲 // 链表中。 set_page_count(page, 1); // 用伙伴分配器的释放函数,这样页面将被加入到空闲链表中。 __free_page(page); } } // total 将设为由此函数传递的页面总数。 total += count; /* * Now free the allocator bitmap itself, it's not * needed anymore: */ // 这一块释放分配器位图并返回。 // // 获得在启动内存映射图顶端的 struct page。 page = virt_to_page(bdata->node_bootmem_map); // 位图释放的页面数。 count = 0; // 对该位图使用的所有页面,这里与前面的代码一样,将其释放给伙伴分配器。 for (i = 0; i < ((bdata->node_low_pfn-(bdata->node_boot_start >> PAGE_SHIFT))/8 + PAGE_SIZE-1)/PAGE_SIZE; i++,page++) { count++; ClearPageReserved(page); set_page_count(page, 1); __free_page(page); } total += count; // 设启动内存映射图为 NULL,以阻止其意外地被第 2 次释放。 bdata->node_bootmem_map = NULL; // 返回该函数释放的页面总数,或者说,返回加人到伙伴分配器空闲链表的页面总数。 return total; }
(a)⇐ mm.h
// include/linux/mm.h #define get_page(p) atomic_inc(&(p)->count) #define put_page(p) __free_page(p) #define put_page_testzero(p) atomic_dec_and_test(&(p)->count) #define page_count(p) atomic_read(&(p)->count) #define set_page_count(p,v) atomic_set(&(p)->count, v) #define SetPageReserved(page) set_bit(PG_reserved, &(page)->flags) #define ClearPageReserved(page) clear_bit(PG_reserved, &(page)->flags)
(b)⇒ __free_page
二、页表管理
1、初始化页表
(1)paging_init
当这个函数返回时,页面已经完全建立完成。注意这里都是与 x86 相关的。
// arch/i386/mm/init.c /* * paging_init() sets up the page tables - note that the first 8MB are * already mapped by head.S. * * This routines also unmaps the page at virtual kernel address 0, so * that we can trap those pesky NULL-reference errors in the kernel. */ void __init paging_init(void) { // pagetable_init() 负责利用 swapper_pg_dir 设立一个静态页表作为 PGD。 pagetable_init(); // 将初始化后的 swapper_pg_dir 载入 CR3 寄存器,这样 CPU 可以使用它。 load_cr3(swapper_pg_dir); #if CONFIG_X86_PAE /* * We will bail out later - printk doesn't work right now so * the user would just see a hanging kernel. */ // 如果 PAE 可用,则在 CR4 寄存器中设置相应的位。 if (cpu_has_pae) set_in_cr4(X86_CR4_PAE); #endif // 清洗所有 (包括在全局内核中) 的 TLB。 __flush_tlb_all(); #ifdef CONFIG_HIGHMEM // kmap_init() 调用 kmap() 初始化保留的页表区域。 kmap_init(); #endif // zone_sizes_init() (见 B.1.2 小节) 记录每个管理区的大小,然后调用 free_area_init() // (见 B.1.3 小节)来初始化各个管理区。 zone_sizes_init(); }
(2)pagetable_init
这个函数负责静态初始化一个从静态定义的称为 swapper_pg_dir 的 PDG 开始的页表。不管怎样,PTE 将可以指向在 ZONE_NORMAL 中的每个页面帧。
(a)⇐ pgtable-2level.h
// include/asm-i386/pgtable-2level.h /* * traditional i386 two-level paging structure: */ #define PGDIR_SHIFT 22 #define PTRS_PER_PGD 1024 /* * the i386 is two-level, so we don't really have any * PMD directory physically. */ #define PMD_SHIFT 22 #define PTRS_PER_PMD 1 #define PTRS_PER_PTE 1024 /* * (pmds are folded into pgds so this doesnt get actually called, * but the define is needed for a generic inline function.) */ #define set_pmd(pmdptr, pmdval) (*(pmdptr) = pmdval) #define set_pgd(pgdptr, pgdval) (*(pgdptr) = pgdval) static inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address) { return (pmd_t *) dir; } #define __mk_pte(page_nr,pgprot) __pte(((page_nr) << PAGE_SHIFT) | pgprot_val(pgprot))
(b)⇐ pgtable.h
// ======================================================================== // include/asm-i386/pgtable.h #define PMD_SIZE (1UL << PMD_SHIFT) // 4M #define PMD_MASK (~(PMD_SIZE-1)) #define PGDIR_SIZE (1UL << PGDIR_SHIFT) // 4M #define PGDIR_MASK (~(PGDIR_SIZE-1)) #define page_pte(page) page_pte_prot(page, __pgprot(0)) #define pmd_page(pmd) \ ((unsigned long) __va(pmd_val(pmd) & PAGE_MASK)) /* to find an entry in a page-table-directory. */ #define pgd_index(address) ((address >> PGDIR_SHIFT) & (PTRS_PER_PGD-1)) #define __pgd_offset(address) pgd_index(address) #define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address)) /* to find an entry in a kernel page-table-directory */ #define pgd_offset_k(address) pgd_offset(&init_mm, address) #define __pmd_offset(address) \ (((address) >> PMD_SHIFT) & (PTRS_PER_PMD-1)) #define mk_pte(page, pgprot) __mk_pte((page) - mem_map, (pgprot)) /* This takes a physical page address that is used by the remapping functions */ #define mk_pte_phys(physpage, pgprot) __mk_pte((physpage) >> PAGE_SHIFT, pgprot)
(c)⇐ page.h
// ======================================================================== // include/asm-i386/page.h #define pmd_val(x) ((x).pmd) #define pgd_val(x) ((x).pgd) #define pgprot_val(x) ((x).pgprot) #define __pte(x) ((pte_t) { (x) } ) #define __pmd(x) ((pmd_t) { (x) } ) #define __pgd(x) ((pgd_t) { (x) } ) #define __pgprot(x) ((pgprot_t) { (x) } )
(d)pagetable_init
页地址扩展(PAE,Page Address Extension),页面大小扩展(PSE,大概是 Page Size Extension 的简称),用于扩展 32 位寻址。因此 不研究 PAE 启用情况。
// arch/i386/mm/init.c static void __init pagetable_init (void) { unsigned long vaddr, end; pgd_t *pgd, *pgd_base; int i, j, k; pmd_t *pmd; pte_t *pte, *pte_base; // 这里初始化 PGD 的第一块。它把每个表项指向全局 0 页面。需要引用的在 ZONE_NORMAL // 中可用内存的表项将在后面分配。 /* * This can be zero as well - no problem, in that case we exit * the loops anyway due to the PTRS_PER_* conditions. */ // 变量 end 标志在 ZONE_NORMAL 中物理内存的末端。 end = (unsigned long)__va(max_low_pfn*PAGE_SIZE); // pgd_base 设立指向静态声明的 PGD 起始位置。 pgd_base = swapper_pg_dir; #if CONFIG_X86_PAE // 如果 PAE 可用,仅将每个表项设为 0 (实际上,将每个表项指向全局 0 页面) 就 // 不够了,因为每个 pgd_t 是一个结构。所以,set_pgd 必须在每个 pgd_t 上调用以使每个表项 // 都指向全局 0 页面。PTRS_PER_PGD(1024) for (i = 0; i < PTRS_PER_PGD; i++) set_pgd(pgd_base + i, __pgd(1 + __pa(empty_zero_page))); #endif // i 初始化为 PGD 中的偏移,与 PAGE_OFFSET 相对应。或者说,这个函数将仅初始 // 化线性地址空间的内核部分。可以不用关心这个用户空间部分。 i = __pgd_offset(PAGE_OFFSET); // pgd 初始化为 pgd_t,对应于线性地址空间中内核部分的起点。 pgd = pgd_base + i; // 这个循环开始指向有效 PMD 表项。在 PAE 的情形下,页面由 alloc_bootmem_low_pages() // 分配,然后设立相应的 PGD。没有 PAG 时,就没有中间目录,所以就折返到 PGD 以保 // 留三层页表的假象。 // // i 已经初始化为线性地址空间的内核部分起始位置,所以这里一直循环到最后 // PTRS_PER_PGD(1024) 处的 pgd_t。 for (; i < PTRS_PER_PGD; pgd++, i++) { // 计算这个 PGD 的虚拟地址。PGDIR_SIZE(4M),即每个 PGD 代表 4M,PGD 共 1024 个 vaddr = i*PGDIR_SIZE; // 如果到达 ZONE_NORMAL 的末端,则跳出循环,因为不再需要另外的页表项。 if (end && (vaddr >= end)) break; #if CONFIG_X86_PAE // 如果 PAE 可用,则为 PMD 分配一个页面,并利用 set_pgd() 将页面插入到页表中。 pmd = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE); set_pgd(pgd, __pgd(__pa(pmd) + 0x1)); #else // 如果 PAE 不可用,仅设置 pmd 指向当前 pgd_t。这就是模拟三层页表的 "折返" 策略。 pmd = (pmd_t *)pgd; #endif // 这是一个有效性检查,以保证 PMD 有效。 if (pmd != pmd_offset(pgd, 0)) BUG(); // 这一块初始化 PMD 中的每个表项。这个循环仅在 PAE 可用时进行。请记住,没有 PAE // 时 PTRS_PER_PMD 是 1。 for (j = 0; j < PTRS_PER_PMD; pmd++, j++) { // 计算这个 PMD 的虚拟地址。PGDIR_SIZE(4M),PMD_SIZE(4M) vaddr = i*PGDIR_SIZE + j*PMD_SIZE; if (end && (vaddr >= end)) break; // 如果 CPU 支持 PSE,使用大 TLB 表项。这意味着,对内核页面而言,一个 TLB // 项将映射 4 MB 而不是平常的 4 KB,将不再需要三层 PTE。 if (cpu_has_pse) { unsigned long __pe; set_in_cr4(X86_CR4_PSE); boot_cpu_data.wp_works_ok = 1; // __pe 设置为内核页表的标志(_KERNPG_TABLE),以及表明这是一个映射 4 MB(_PAGE_PSE) // 的标志,然后利用__pa()表明这块虚拟地址的物理地址。这意味着 4 MB 的物理 // 地址不由页表映射。 __pe = _KERNPG_TABLE + _PAGE_PSE + __pa(vaddr); /* Make it "global" too if supported */ // 如果 CPU 支持 PGE,则为页表项设置它。它标记表项为全局的,并为所有进程可见。 if (cpu_has_pge) { set_in_cr4(X86_CR4_PGE); __pe += _PAGE_GLOBAL; } // 由于 PSE 的缘故,所以不需要三层页表。现在利用 set_pmd() 来设置 PMD,并 // 继续到下一个 PMD。 set_pmd(pmd, __pmd(__pe)); continue; } // 相反,如果 PSE 不被支持,需要 PTE 的时候,为它们分配一个页面。 pte_base = pte = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE); // 这一块初始化 PTE。 // 对每个 pte_t, 计算当前被检查的虚拟地址, 创建一个 PTE 来指向相应的物理页面帧。 // PTRS_PER_PTE(1024),PGDIR_SIZE(4M),PMD_SIZE(4M),PAGE_SIZE(4K) for (k = 0; k < PTRS_PER_PTE; pte++, k++) { vaddr = i*PGDIR_SIZE + j*PMD_SIZE + k*PAGE_SIZE; if (end && (vaddr >= end)) break; *pte = mk_pte_phys(__pa(vaddr), PAGE_KERNEL); } // PTE 已经被初始化, 所以设置 PMD 来指向包含它们的页面。 set_pmd(pmd, __pmd(_KERNPG_TABLE + __pa(pte_base))); // 保证表项已经正确建立。 if (pte_base != pte_offset(pmd, 0)) BUG(); } } // 在这点上,已经设立页面表项来引用 ZONE_NORMAL 的所有部分。需要的其他区域是 // 那些固定映射以及那些需要利用 kmap() 映射高端内存的区域。 /* * Fixed mappings, only the page table structure has to be * created - mappings will be set by set_fixmap(): */ // 固定地址空间被认为从 FIXADDR_TOP 开始,并在地址空间前面结束。__fix_to_virt() // 将一个下标作为参数,并返回在固定虚拟地址空间中的第 index 个下标后续页面帧 (从 // FIXADDR_TOP 开始)。 __end_of_fixed_addresses 是上一个由固定虚拟地址空间用到的下 // 标。或者说,这一行返回的应该是固定虚拟地址空间起始位置的 PMD 虚拟地址。 vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK; // 这个函数传递 0 作为 fixrange_init() 的结束, 它开始于 vaddr,并创建有效 PGD 和 // PMD 直到虚拟地址空间的末端,对这些地址而言不需要 PTE。 fixrange_init(vaddr, 0, pgd_base); #if CONFIG_HIGHMEM // 利用 kmap()来设立页表。 /* * Permanent kmaps: */ vaddr = PKMAP_BASE; fixrange_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base); // 利用 kmap() 获取对应于待用到的区域首址的 PTE。 pgd = swapper_pg_dir + __pgd_offset(vaddr); pmd = pmd_offset(pgd, vaddr); pte = pte_offset(pmd, vaddr); pkmap_page_table = pte; #endif #if CONFIG_X86_PAE /* * Add low memory identity-mappings - SMP needs it when * starting up on an AP from real-mode. In the non-PAE * case we already have these mappings through head.S. * All user-space mappings are explicitly cleared after * SMP startup. */ // 这里设立一个临时标记来映射虚拟地址 0 和物理地址 0。 pgd_base[0] = pgd_base[USER_PTRS_PER_PGD]; #endif }
(3)⇒ alloc_bootmem_low_pages
上文 pagetable_init 函数中有调用 alloc_bootmem_low_pages 函数。
static void __init pagetable_init (void) { // ... pte_base = pte = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE); // ... }
alloc_bootmem_low_pages 函数具体分析可参考 ⇒ alloc_bootmem 一节
(4)fixrange_init
上文 pagetable_init 函数中有调用 fixrange_init 函数。
static void __init pagetable_init (void) { // ... fixrange_init(vaddr, 0, pgd_base); // ... }
这个函数为固定虚拟地址映射创建有效的 PGD 和 PMD。
// arch/i386/mm/init.c static void __init fixrange_init (unsigned long start, unsigned long end, pgd_t *pgd_base) { pgd_t *pgd; pmd_t *pmd; pte_t *pte; int i, j; unsigned long vaddr; // 设置虚拟地址 (vaddr) 作为请求起始地址的一个参数。 vaddr = start; // 获取对应于 vaddr 的 PGD 内部索引。 i = __pgd_offset(vaddr); // 获取对应于 vaddr 的 PMD 内部索引。 j = __pmd_offset(vaddr); // 获取 pgd_t 的起点。 pgd = pgd_base + i; // 一直循环直到到达 end。当 pagetable_init() 传入 0 时,将继续循环直到 PGD 的末端。 // PTRS_PER_PGD(1024) for ( ; (i < PTRS_PER_PGD) && (vaddr != end); pgd++, i++) { #if CONFIG_X86_PAE // 在有 PAE 时,若没有为 PMD 分配页面,这里就为 PMD 分配一个页面。 if (pgd_none(*pgd)) { pmd = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE); set_pgd(pgd, __pgd(__pa(pmd) + 0x1)); if (pmd != pmd_offset(pgd, 0)) printk("PAE BUG #02!\n"); } pmd = pmd_offset(pgd, vaddr); #else // 没有 PAE 时,也没有 PMD,所以这里把 pgd_t 看作 pmd_t。 pmd = (pmd_t *)pgd; #endif // 对 PMD 中的每个表项,这里将为 pte_t 表项分配一个页面,并在页表中设置。 // 注意 vaddr 是以 PMD 大小作为一步增加的。 // PTRS_PER_PMD(1)。 for (; (j < PTRS_PER_PMD) && (vaddr != end); pmd++, j++) { if (pmd_none(*pmd)) { pte = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE); set_pmd(pmd, __pmd(_KERNPG_TABLE + __pa(pte))); if (pte != pte_offset(pmd, 0)) BUG(); } vaddr += PMD_SIZE; } j = 0; } }
(5)kmap_init
上文 paging_init 函数中有调用 kmap_init 函数。
// arch/i386/mm/init.c void __init paging_init(void) { // ... #ifdef CONFIG_HIGHMEM // kmap_init() 调用 kmap() 初始化保留的页表区域。 kmap_init(); #endif // ... }
(a)⇐ fixmap.h
// include/asm-i386/fixmap.h /* * used by vmalloc.c. * * Leave one empty page between vmalloc'ed areas and * the start of the fixmap, and leave one page empty * at the top of mem.. */ #define FIXADDR_TOP (0xffffe000UL) #define __FIXADDR_SIZE (__end_of_permanent_fixed_addresses << PAGE_SHIFT) #define FIXADDR_START (FIXADDR_TOP - __FIXADDR_SIZE) #define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT))
(b)⇐ init_task.c
结构体 mm_struct 可参考 ⇒ 4.3 进程地址空间描述符
// include/linux/sched.h #define INIT_MM(name) \ { \ mm_rb: RB_ROOT, \ pgd: swapper_pg_dir, \ mm_users: ATOMIC_INIT(2), \ mm_count: ATOMIC_INIT(1), \ mmap_sem: __RWSEM_INITIALIZER(name.mmap_sem), \ page_table_lock: SPIN_LOCK_UNLOCKED, \ mmlist: LIST_HEAD_INIT(name.mmlist), \ } // arch/i386/kernel/init_task.c struct mm_struct init_mm = INIT_MM(init_mm);
(c)⇐ pgtable.h
// include/asm-i386/page.h #define pmd_val(x) ((x).pmd) #define pgd_val(x) ((x).pgd) #define pgprot_val(x) ((x).pgprot) // ========================================================================= // include/asm-i386/pgtable-2level.h /* * traditional i386 two-level paging structure: */ #define PGDIR_SHIFT 22 #define PTRS_PER_PGD 1024 /* * the i386 is two-level, so we don't really have any * PMD directory physically. */ #define PMD_SHIFT 22 #define PTRS_PER_PMD 1 #define PTRS_PER_PTE 1024 static inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address) { return (pmd_t *) dir; } // ========================================================================= // include/asm-i386/pgtable.h #define pmd_page(pmd) \ ((unsigned long) __va(pmd_val(pmd) & PAGE_MASK)) /* Find an entry in the third-level page table.. */ #define __pte_offset(address) \ ((address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1)) #define pte_offset(dir, address) ((pte_t *) pmd_page(*(dir)) + \ __pte_offset(address)) /* to find an entry in a page-table-directory. */ #define pgd_index(address) ((address >> PGDIR_SHIFT) & (PTRS_PER_PGD-1)) #define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address)) /* to find an entry in a kernel page-table-directory */ #define pgd_offset_k(address) pgd_offset(&init_mm, address)
(d)kmap_init
这个函数仅存在于如果在编译时设置了 CONFIG_HIGHMEM 的情况下。它负责获取 kma 区域的首址,引用它的 PTE 以及保护页表。这意味着在使用 kmap() 时不一定都需要检查 PGD。
/* * NOTE: pagetable_init alloc all the fixmap pagetables contiguous on the * physical space so we can cache the place of the first one and move * around without checking the pgd every time. */ #if CONFIG_HIGHMEM pte_t *kmap_pte; pgprot_t kmap_prot; // 由于 fixrange_init() 已经设立了有效的 PGD 和 PMD,所以就不需要再一次检查 // 它们,这样 kmap_get_fixmap_pte() 可以快速遍历页表。 #define kmap_get_fixmap_pte(vaddr) \ pte_offset(pmd_offset(pgd_offset_k(vaddr), (vaddr)), (vaddr)) void __init kmap_init(void) { unsigned long kmap_vstart; /* cache the first kmap pte */ // 缓存 kmap_vstart 中 kmap 区域的虚拟地址。 kmap_vstart = __fix_to_virt(FIX_KMAP_BEGIN); // 缓存 PTE 作为 kmap_pte 中 kmap 区域的首址。 kmap_pte = kmap_get_fixmap_pte(kmap_vstart); // 利用 kmap_prot 缓存页表表项的保护项。 kmap_prot = PAGE_KERNEL; } #endif /* CONFIG_HIGHMEM */
(6)⇒ zone_sizes_init
上文 paging_init 函数中有调用 zone_sizes_init 函数。
// arch/i386/mm/init.c void __init paging_init(void) { // ... // zone_sizes_init() (见 B.1.2 小节) 记录每个管理区的大小,然后调用 free_area_init() // (见 B.1.3 小节)来初始化各个管理区。 zone_sizes_init(); // ... }
zone_sizes_init 函数具体分析可参考 ⇒ zone_sizes_init 一节
深入理解Linux虚拟内存管理(四)(下):https://developer.aliyun.com/article/1597786