深入理解Linux虚拟内存管理(四)(中):https://developer.aliyun.com/article/1597781
2、遍历页表
(1)follow_page
这个函数返回在 mm 页表中 address 处 PTE 用到的 struct page 。
// mm/memory.c /* * Do a quick page-table lookup for a single page. */ // 这个函数是 mm 中待遍历的页表。address 与 struct page 和 write 有关,它表明该页 // 是否将被写。 static struct page * follow_page(struct mm_struct *mm, unsigned long address, int write) { pgd_t *pgd; pmd_t *pmd; pte_t *ptep, pte; // 获取 address 处的 PGD 并保证它存在且有效。 pgd = pgd_offset(mm, address); if (pgd_none(*pgd) || pgd_bad(*pgd)) goto out; // 获取address处的PMD并保证它存在且有效。 pmd = pmd_offset(pgd, address); if (pmd_none(*pmd) || pmd_bad(*pmd)) goto out; // 获取address处的PTE并保证它存在且有效。 ptep = pte_offset(pmd, address); if (!ptep) goto out; pte = *ptep; // 如果PTE当前存在,则可以返回一些东西。 if (pte_present(pte)) { // 如果调用者指定了将发生写操作,则检查以保证PTE有写权限。如果是这样, 将PTE设为脏。 if (!write || (pte_write(pte) && pte_dirty(pte))) // 如果PTE存在并有权限,则返回由PTE映射的struct page return pte_page(pte); } out: // 返回0表明该address没有相应的struct page return 0; }
3、⇔ 页表
(1)pgd_alloc
// include/asm-i386/pgalloc.h #define pgd_alloc(mm) get_pgd_fast()
(a)get_pgd_fast
// include/asm-i386/pgalloc.h static inline pgd_t *get_pgd_fast(void) { unsigned long *ret; if ((ret = pgd_quicklist) != NULL) { pgd_quicklist = (unsigned long *)(*ret); ret[0] = 0; pgtable_cache_size--; } else ret = (unsigned long *)get_pgd_slow(); return (pgd_t *)ret; }
(b)get_pgd_slow
// include/asm-i386/pgalloc.h static inline pgd_t *get_pgd_slow(void) { pgd_t *pgd = (pgd_t *)__get_free_page(GFP_KERNEL); if (pgd) { memset(pgd, 0, USER_PTRS_PER_PGD * sizeof(pgd_t)); memcpy(pgd + USER_PTRS_PER_PGD, swapper_pg_dir + USER_PTRS_PER_PGD, (PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t)); } return pgd; }
(2)pgd_free
// include/asm-i386/pgalloc.h #define pgd_free(pgd) free_pgd_slow(pgd)
(a)free_pgd_slow
// include/asm-i386/pgalloc.h static inline void free_pgd_slow(pgd_t *pgd) { #if defined(CONFIG_X86_PAE) int i; for (i = 0; i < USER_PTRS_PER_PGD; i++) free_page((unsigned long)__va(pgd_val(pgd[i])-1)); kmem_cache_free(pae_pgd_cachep, pgd); #else free_page((unsigned long)pgd); #endif }
(3)pmd_alloc
// include/linux/mm.h /* * On a two-level page table, this ends up being trivial. Thus the * inlining and the symmetry break with pte_alloc() that does all * of this out-of-line. */ // x86 两级页表,pmd 即为 pgd ? static inline pmd_t *pmd_alloc(struct mm_struct *mm, pgd_t *pgd, unsigned long address) { if (pgd_none(*pgd)) return __pmd_alloc(mm, pgd, address); return pmd_offset(pgd, address); }
(a) ⇐ pgtable-2level.h
// include/asm-i386/pgtable-2level.h static inline int pgd_none(pgd_t pgd) { return 0; } static inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address) { return (pmd_t *) dir; }
(4)__pmd_alloc
// mm/memory.c /* * Allocate page middle directory. * * We've already handled the fast-path in-line, and we own the * page table lock. * * On a two-level page table, this ends up actually being entirely * optimized away. */ pmd_t *__pmd_alloc(struct mm_struct *mm, pgd_t *pgd, unsigned long address) { pmd_t *new; /* "fast" allocation can happen without dropping the lock.. */ new = pmd_alloc_one_fast(mm, address); if (!new) { spin_unlock(&mm->page_table_lock); new = pmd_alloc_one(mm, address); spin_lock(&mm->page_table_lock); if (!new) return NULL; /* * Because we dropped the lock, we should re-check the * entry, as somebody else could have populated it.. */ if (!pgd_none(*pgd)) { pmd_free(new); goto out; } } pgd_populate(mm, pgd, new); out: return pmd_offset(pgd, address); }
(a) ⇐ pgalloc.h
// include/asm-i386/pgalloc.h /* * allocating and freeing a pmd is trivial: the 1-entry pmd is * inside the pgd, so has no extra memory associated with it. * (In the PAE case we free the pmds as part of the pgd.) */ #define pmd_alloc_one_fast(mm, addr) ({ BUG(); ((pmd_t *)1); }) #define pmd_alloc_one(mm, addr) ({ BUG(); ((pmd_t *)2); }) #define pmd_free_slow(x) do { } while (0) #define pmd_free_fast(x) do { } while (0) #define pmd_free(x) do { } while (0) #define pgd_populate(mm, pmd, pte) BUG()
(b) ⇒ pmd_alloc_one
// include/asm-x86_64/pgalloc.h static inline pmd_t *pmd_alloc_one (struct mm_struct *mm, unsigned long addr) { return (pmd_t *)get_zeroed_page(GFP_KERNEL); }
(c) ⇒ get_zeroed_page
// mm/page_alloc.c unsigned long get_zeroed_page(unsigned int gfp_mask) { struct page * page; page = alloc_pages(gfp_mask, 0); if (page) { void *address = page_address(page); clear_page(address); return (unsigned long) address; } return 0; }
(5)pte_alloc
// mm/memory.c /* * Allocate the page table directory. * * We've already handled the fast-path in-line, and we own the * page table lock. */ pte_t *pte_alloc(struct mm_struct *mm, pmd_t *pmd, unsigned long address) { if (pmd_none(*pmd)) { pte_t *new; /* "fast" allocation can happen without dropping the lock.. */ new = pte_alloc_one_fast(mm, address); if (!new) { spin_unlock(&mm->page_table_lock); new = pte_alloc_one(mm, address); spin_lock(&mm->page_table_lock); if (!new) return NULL; /* * Because we dropped the lock, we should re-check the * entry, as somebody else could have populated it.. */ if (!pmd_none(*pmd)) { pte_free(new); goto out; } } pmd_populate(mm, pmd, new); } out: return pte_offset(pmd, address); }
(a) ⇒ pte_alloc_one
// include/asm-i386/pgalloc.h static inline pte_t *pte_alloc_one(struct mm_struct *mm, unsigned long address) { pte_t *pte; pte = (pte_t *) __get_free_page(GFP_KERNEL); if (pte) clear_page(pte); return pte; }
三、描述物理内存
1、初始化管理区
(1)setup_memory
这个函数的调用图如图 2.3 所示。它为引导内存分配器初始化自身进行所需信息的获取。它可以分成几个不同的任务。
- 找到低端内存的 PFN 的起点和终点(min_low_pfn,max_low_pfn),找到高端内存的 PFN 的起点和终点(highstart_pfn,highend_pfn),以及找到系统中最后一页的 PFN。
- 初始化 bootmem_date 结构以及声明可能被引导内存分配器用到的页面。
- 标记所有系统可用的页面为空闲,然后为那些表示页面的位图保留页面。
- 在 SMP 配置或 initrd 镜像存在时,为它们保留页面。
// arch/i386/kernel/setup.c static unsigned long __init setup_memory(void) { unsigned long bootmap_size, start_pfn, max_low_pfn; // 将物理地址向上取整到下一页面,返回页帧号。由于_end 是已载入内核 // 镜像的底端地址,所以 start_pfn 现在是可能被用到的第一块物理页面帧的偏移。 start_pfn = PFN_UP(__pa(&_end)); // 遍历 e820 图,查找最高的可用 PFN。 find_max_pfn(); // 在 ZONE_NORMAL 中找到可寻址的最高页面帧。 max_low_pfn = find_max_low_pfn(); #ifdef CONFIG_HIGHMEM // 如果高端内存可用,则从高端内存区域的 0 位置开始。如果内存在 max_low_pfn 后, // 则把高端内存区的起始位置(highstart_pfn)定在那里,而其结束位置定在 max_pfn, // 然后打印可用高端内存的提示消息。 highstart_pfn = highend_pfn = max_pfn; if (max_pfn > max_low_pfn) { highstart_pfn = max_low_pfn; } printk(KERN_NOTICE "%ldMB HIGHMEM available.\n", pages_to_mb(highend_pfn - highstart_pfn)); #endif // 为 config_page_data 节点初始化 bootmem_data 结构。 // 它设置节点的物理内存起始点(页帧号 start_pfn)和终点(页帧号 max_low_pfn ), // 分配一张位图来表示这些页面,并将所有的页面设置为初始时保留。 bootmap_size = init_bootmem(start_pfn, max_low_pfn); // 读入 e820 图,然后为运行时系统中的所有可用页面 // 调用 free_bootmem() 这将标记页面在初始化时为空闲(即可分配页面)。 register_bootmem_low_pages(max_low_pfn); // 保留页面,即相应的位图的位设置为 1 reserve_bootmem(HIGH_MEMORY, (PFN_PHYS(start_pfn) + bootmap_size + PAGE_SIZE-1) - (HIGH_MEMORY)); // 保留 0 号页面,因为 0 号页面是 BIOS 用到的一个特殊页面。 reserve_bootmem(0, PAGE_SIZE); #ifdef CONFIG_SMP // 保留额外的页面为跳板代码用。跳板代码处理用户空间如何进入内核空间。 reserve_bootmem(PAGE_SIZE, PAGE_SIZE); #endif #ifdef CONFIG_ACPI_SLEEP // 如果加入了睡眠机制,就需要为它保留内存。这仅为那些有挂起功能的手提 // 电脑所用到。它已经超过本书的范围。 acpi_reserve_bootmem(); #endif // ... return max_low_pfn; }
(a)⇒ init_bootmem
(b)⇒ register_bootmem_low_pages
// arch/i386/kernel/setup.c // 读入 e820 图,然后为运行时系统中的所有可用页面 // 调用 free_bootmem() 这将标记页面在初始化时为空闲(即可分配页面)。 static void __init register_bootmem_low_pages(unsigned long max_low_pfn) { int i; for (i = 0; i < e820.nr_map; i++) { unsigned long curr_pfn, last_pfn, size; // ... size = last_pfn - curr_pfn; free_bootmem(PFN_PHYS(curr_pfn), PFN_PHYS(size)); } }
(c)⇒ reserve_bootmem
(2)zone_sizes_init
这是一个用于初始化各管理区的高层函数。PFN 中管理区大小在 setup_memory() 过程中发现。这个函数填充一个管理区大小的数组,并把它传给 free_area_init()。
// arch/i386/mm/init.c static void __init zone_sizes_init(void) { // 初始化大小为 0。 unsigned long zones_size[MAX_NR_ZONES] = {0, 0, 0}; unsigned int max_dma, high, low; // 计算最大可能 DMA 寻址的 PFN。 // #define MAX_DMA_ADDRESS (PAGE_OFFSET+0x1000000) 16M max_dma = virt_to_phys((char *)MAX_DMA_ADDRESS) >> PAGE_SHIFT; // max_low_pfn 是 ZONE_NORMAL 可用的最高 PFN。 low = max_low_pfn; // highest_pfn 是 ZONE_HIGHMEM 可用的最高 PFN。 high = highend_pfn; // 如果在 ZONE_NORMAL 中的最高 PFN 低于 MAX_DMA_ADDRESS,则仅 // 把 ZONE_DMA 的大小赋值给它,其他管理区仍然为 0。 if (low < max_dma) zones_size[ZONE_DMA] = low; else { // 设置 ZONE_DMA 中的页面数。 zones_size[ZONE_DMA] = max_dma; // ZONE_NORMAL 的大小等于 max_low_pfn 减去 ZONE_DMA 中的页面数。 zones_size[ZONE_NORMAL] = low - max_dma; #ifdef CONFIG_HIGHMEM // ZONE_HIGHMEM 的大小是可能的最高 PFN 减去 ZONE_NORMAL 中可能的最 // 高 PFN(max_low_pfn)。 zones_size[ZONE_HIGHMEM] = high - low; #endif } free_area_init(zones_size); }
(3)free_area_init
这是一个与体系结构无关的函数,用于设置 UMA 体系结构。它仅是调用核心函数,传入静态 contig_page_data 作为节点。不同的是,NUMA 体系结构将使用 free_area_node()。
// mm/page_alloc.c void __init free_area_init(unsigned long *zones_size) { free_area_init_core(0, &contig_page_data, &mem_map, zones_size, 0, 0, 0); }
0 是该节点的节点标识(NID),这里为 0。
contig_page_data 是静态全局 pg_data_t。
mem_map 用于跟踪 struct page 的全局 mem_map。 函数 free_area_init_core() 将为这个数组分配内存。
zones_sizes 是由 zone_sizes_init() 填充的管理区大小的数组。
0,这个 0 是物理地址的起始点。
0,第 2 个 0 是内存空洞大小的数组,但不用于 UMA 体系结构。
0,最后一个 0 是一个指向该节点局部 mem_map 的一个指针,用于 NUMA 体系结构
(4)free_area_init_node
这个函数有 2 个版本。第 1 个版本除了使用了一个不同的物理地址起点外,几乎与 free_area_init() 一样。这个函数还用于仅有一个节点的体系结构(所以它们使用 contig_page_data),但是它们的物理地址不是 0。
在页表初始化后调用的版本用于初始化系统中的每个 pgdat。 如果希望在特定的体系结构中调优它们的位置,调用者可以有选择地分配它们自己的 mem_map 部分并将它们作为参数传递给这个函数。如果选择不,则 mem_map 部分就会在后面由 free_area_init_core() 来分配。
// mm/numa.c /* * Nodes can be initialized parallely, in no particular order. */ // nid是传入的pgdat的NID。 // pgdat是将被初始化的节点。 // pmap是指向将要用到的节点的mem_map部分的指针,它经常传入NULL,并在以后分配空间。 // zones_size是该节点中容量为管理区大小的一个数组。 // zone_start_paddr是节点的物理地址起点。 // zholes_size是容量为每个管理区中空洞大小的一个数组。 void __init free_area_init_node(int nid, pg_data_t *pgdat, struct page *pmap, unsigned long *zones_size, unsigned long zone_start_paddr, unsigned long *zholes_size) { int i, size = 0; struct page *discard; // 如果设置全局的mem_map,就将其设置在线性地址空间中内核部分的起点。请 // 记住,在NUMA中,mem_map是一个分块的虚拟数组,数组由每个节点中的局部映射填充。 if (mem_map == (mem_map_t *)NULL) mem_map = (mem_map_t *)PAGE_OFFSET; // 调用free_area_init_core()。注意,discard作为第3个参数传入,这是因为NUMA 中 // 并不需要设置全局的mem_map。 free_area_init_core(nid, pgdat, &discard, zones_size, zone_start_paddr, zholes_size, pmap); // 记录 pgdat 的 NID。 pgdat->node_id = nid; /* * Get space for the valid bitmap. */ // 计算NID的总大小。 for (i = 0; i < MAX_NR_ZONES; i++) size += zones_size[i]; // 重新计算位数,以满足这样大小的每个字节都有一位。 size = LONG_ALIGN((size + 7) >> 3); // 分配一张位图,表示节点中存在的有效管理区。实际上,这条语句仅用于Sparc体系结构, // 所以对其他体系结构而言是浪费内存。 pgdat->valid_addr_bitmap = (unsigned long *)alloc_bootmem_node(pgdat, size); // 开始时,所有的区域都无效。有效区域由后面所述Sparc中的mem_init()函数标记。 // 其他体系结构仅是忽略掉这张位图。 memset(pgdat->valid_addr_bitmap, 0, size); }
(5)free_area_init_core
这个函数负责初始化所有的区域,并在节点中分配它们的局部 Imem_map。在 UMA 体系结构中,调用这个函数将初始化全局 mem_map 数组。在 NUMA 体系结构中,这个数组被看作是一个稀疏分布的虚拟数组。
// mm/page_alloc.c /* * Set up the zone data structures: * - mark all pages reserved * - mark all memory queues empty * - clear the memory bitmaps */ void __init free_area_init_core(int nid, pg_data_t *pgdat, struct page **gmap, unsigned long *zones_size, unsigned long zone_start_paddr, unsigned long *zholes_size, struct page *lmem_map) { unsigned long i, j; unsigned long map_size; unsigned long totalpages, offset, realtotalpages; // 该块主要负责计算每个区域的大小。 // 这个区域必须邻近由伙伴分配器分配的最大大小的块,从而进行位级操作。 // MAX_ORDER 为 10 const unsigned long zone_required_alignment = 1UL << (MAX_ORDER-1); // 如果物理地址不是按页面排列的,就是一个 BUG()。 if (zone_start_paddr & ~PAGE_MASK) BUG(); // 为这个节点初始化 totalpages 为 0。 totalpages = 0; // 通过遍历 zone_sizes 来计算节点的总大小。 for (i = 0; i < MAX_NR_ZONES; i++) { unsigned long size = zones_size[i]; totalpages += size; } // 通过减去 zholes_size 的空洞大小来计算实际的内存量。 realtotalpages = totalpages; if (zholes_size) for (i = 0; i < MAX_NR_ZONES; i++) realtotalpages -= zholes_size[i]; // 打印提示信息告知用户这个节点可用的内存量。 printk("On node %d totalpages: %lu\n", nid, realtotalpages); /* * Some architectures (with lots of mem and discontinous memory * maps) have to search for a good mem_map area: * For discontigmem, the conceptual mem map array starts from * PAGE_OFFSET, we need to align the actual array onto a mem map * boundary, so that MAP_NR works. */ // 这一块在需要时分配局部 lmem_map,设置 gmap 位。在 UMA 体系结构中,gmap 实际 // 上就是 mem_map,所以这就是为它分配的内存。 // // 计算数组所需的内存量。它等于页面总数量乘以 struct page 的大小。 map_size = (totalpages + 1)*sizeof(struct page); // 如果映射图还没有分配,就在这里分配。 if (lmem_map == (struct page *)0) { // 从引导内存分配器中分配一块内存。 lmem_map = (struct page *) alloc_bootmem_node(pgdat, map_size); // MAP_ALIGN() 将在一个 struct page 大小范围内排列数组,从而计算在 mem_map // 中基于物理地址 MAP_NR() 宏的内部偏移。 lmem_map = (struct page *)(PAGE_OFFSET + MAP_ALIGN((unsigned long)lmem_map - PAGE_OFFSET)); } // 设置 gmap 和 pgdat-node_mem_map 变量以分配 lmem_map。在 UMA 体系结构 // 中,仅设置 mem_map。 *gmap = pgdat->node_mem_map = lmem_map; // 记录节点大小。 pgdat->node_size = totalpages; // 记录起始物理地址。 pgdat->node_start_paddr = zone_start_paddr; // 记录节点所占 mem_map 中的偏移。 pgdat->node_start_mapnr = (lmem_map - mem_map); // 初始化管理区计数为 0,这将在函数的后面设置。 pgdat->nr_zones = 0; // offset 现在是 lmem_map 开始的局部部分中 mem_map 的偏移。 offset = lmem_map - mem_map; // 从这块管理区开始循环,初始化节点中的每个 zone_t。该初始化过程开始时设置已存在 // 基本字段的值。 // // 追历节点中所有管理区。 for (j = 0; j < MAX_NR_ZONES; j++) { zone_t *zone = pgdat->node_zones + j; unsigned long mask; unsigned long size, realsize; // 记录在 zone_table 中指向该管理区的指针,见 2.6 节。 zone_table[nid * MAX_NR_ZONES + j] = zone; // 计算管理区的实际大小。它基于 zones_sizes 的总大小减去 zholes_size 的空洞大小。 realsize = size = zones_size[j]; if (zholes_size) realsize -= zholes_size[j]; // 打印提示信息告知在这个管理区中的页面数。 printk("zone(%lu): %lu pages.\n", j, size); // 记录管理区的大小。 zone->size = size; // zone_names 是管理区的大小,这里是为了打印的需要。 zone->name = zone_names[j]; // 初始化管理区的其他字段,如它的父 pgdat。 zone->lock = SPIN_LOCK_UNLOCKED; zone->zone_pgdat = pgdat; zone->free_pages = 0; zone->need_balance = 0; // 如果管理区没有内存,就转到下一个管理区,因为不需要其他操作了。 if (!size) continue; /* * The per-page waitqueue mechanism uses hashed waitqueues * per zone. */ // 这一块初始化管理区的等待队列。等待该管理区中页面的进程将会使用这个哈希表选择 // 一个队列等待。这意味着在页面解锁时并不需要唤醒所有的等待进程,仅需要唤醒其中的一 // 个子集。 // wait_table_size() 计算所用哈希表的大小。它基于管理区的页面数和队列数与页面 // 数之间的特定比率完成计算。该哈希表不会大于 4 KB。 zone->wait_table_size = wait_table_size(size); // 计算哈希算法的因子。 zone->wait_table_shift = BITS_PER_LONG - wait_table_bits(zone->wait_table_size); // 分配可以容纳 zone→wait_table_size 项的 wait_queue_head_t 表。 zone->wait_table = (wait_queue_head_t *) alloc_bootmem_node(pgdat, zone->wait_table_size * sizeof(wait_queue_head_t)); // 初始化所有的等待队列。 for(i = 0; i < zone->wait_table_size; ++i) init_waitqueue_head(zone->wait_table + i); // 这一块计算管理区极值并记录管理区地址。这个极值记为管理区大小的比率。 // // 首先,若激活了一个新的管理区,更新节点中的管理区数量。 pgdat->nr_zones = j+1; // 计算掩码(将用于 page_min 极值)为管理区的大小除以管理区的平衡因子。所有管 // 理区的平衡因子在 mm/page_alloc.c 的首部声明为 128。 mask = (realsize / zone_balance_ratio[j]); // 所有管 理 区的 zone_balance_min 比率都为 20,这意味着 page_min 将不低于 20 。 if (mask < zone_balance_min[j]) mask = zone_balance_min[j]; else if (mask > zone_balance_max[j]) // 类似地,所有的 zone_balance_max 都为 255,所以 pages_min 将不会超过 255。 mask = zone_balance_max[j]; // pages_min 设为 mask。 zone->pages_min = mask; // pages_low 是 pages_min 页面数量的 2 倍。 zone->pages_low = mask*2; // pages_high 是 pages_min 页面数量的 3 倍。 zone->pages_high = mask*3; // 记录管理区的第 1 个 struct page 在 mem_map 中的地址。 zone->zone_mem_map = mem_map + offset; // 记录 mem_map 中管理区起点的索引。 zone->zone_start_mapnr = offset; // 记录起始的物理地址。 zone->zone_start_paddr = zone_start_paddr; // 利用伙伴分配器保证管理区已经正确的排列可用。否则,伙伴分配器用到的位 // 级操作就会失败。 if ((zone_start_paddr >> PAGE_SHIFT) & (zone_required_alignment-1)) printk("BUG: wrong zone alignment, it will crash\n"); /* * Initially all pages are reserved - free ones are freed * up by free_all_bootmem() once the early boot process is * done. Non-atomic initialization, single-pass. */ // 初始时,管理区中所有的页面都标记为保留,因为没有办法知道引导内存分配 // 器使用的是哪些页面。当引导内存分配器在 free_all_bootmem() 中收回时, // 未使用页面中的 PG_reserved 会被清除。 for (i = 0; i < size; i++) { // 获取页面的偏移。 struct page *page = mem_map + offset + i; // 页面所在的管理区由页面标志编码,见 2.6 节。 set_page_zone(page, nid * MAX_NR_ZONES + j); // 设置计数为 0,因为管理区未被使用。 set_page_count(page, 0); // 设置保留标志,然后,若页面不再被使用,引导内存分配器将清除该位。 SetPageReserved(page); // 初始化页面链表头。 INIT_LIST_HEAD(&page->list); // 如果页面可用且页面在低端内存,设置 page virtual 字段。 if (j != ZONE_HIGHMEM) set_page_address(page, __va(zone_start_paddr)); // 将 zone_start_paddr 增加一个页面大小,该参数将用于记录下一个管理区的起点。 zone_start_paddr += PAGE_SIZE; } // 这一块初始化管理区的空闲链表,分配伙伴分配器在记录页面伙伴状态时的位图。 offset += size; // 从 0 循环到 MAX_ORDER -1。 for (i = 0; ; i++) { unsigned long bitmap_size; // 初始化当前次序为 i的 free_list 链表。 INIT_LIST_HEAD(&zone->free_area[i].free_list); // 如果处于最后,设置空闲区域映射为 NULL,表示空闲链表的末端。 if (i == MAX_ORDER-1) { zone->free_area[i].map = NULL; break; } /* * Page buddy system uses "index >> (i+1)", * where "index" is at most "size-1". * * The extra "+3" is to round down to byte * size (8 bits per byte assumption). Thus * we get "(size-1) >> (i+4)" as the last byte * we can access. * * The "+1" is because we want to round the * byte allocation up rather than down. So * we should have had a "+7" before we shifted * down by three. Also, we have to add one as * we actually _use_ the last bit (it's [0,n] * inclusive, not [0,n[). * * So we actually had +7+1 before we shift * down by 3. But (n+8) >> 3 == (n >> 3) + 1 * (modulo overflows, which we do not have). * * Finally, we LONG_ALIGN because all bitmap * operations are on longs. */ // 下面两句: // bitmap_size = (size-1) >> (i+4); // bitmap_size = LONG_ALIGN(bitmap_size+1); // 相当于如下代码: // bitmap_size = size >> i; // 内存块个数 // bitmap_size = (bitmap_size + 7) >> 3; // 因为一个字节有8个位, 所以要除以8 // bitmap_size = LONG_ALIGN(bitmap_size); // // 计算 bitmap_size 为容纳整个位图所需的字节数。位图中每一位表示一个有 2^i 数量 // 页面的伙伴对。 bitmap_size = (size-1) >> (i+4); // 利用 LONG_ALIGN() 转化容量值为一个长整型,因为所有的位操作都在长整型上进行。 bitmap_size = LONG_ALIGN(bitmap_size+1); // 分配映射用到的内存。 zone->free_area[i].map = (unsigned long *) alloc_bootmem_node(pgdat, bitmap_size); } // 循环到下一个管理区。 } // 利用 build_zonelists()来构造节点的管理区回退链表。 build_zonelists(pgdat); }
(a)⇒ alloc_bootmem_node
(b)⇒ 初始化 mem_map
核心函数 free_area_init_core() 为已经初始化过的节点分配局部 lmem_map。而该数组的内存通过引导内存分配器中的 alloc_bootmem_node() 分配得到。在 UMA 结构中,新分配的内存变成了全局的 mem_map。
(c)⇐ zone_t
每个管理区由一个 zone_t 描述,具体可参考 ⇒ 2.2 管理区,2.6 页面映射到管理区
(d)⇒ wait_table_size
/* * Helper functions to size the waitqueue hash table. * Essentially these want to choose hash table sizes sufficiently * large so that collisions trying to wait on pages are rare. * But in fact, the number of active page waitqueues on typical * systems is ridiculously low, less than 200. So this is even * conservative, even though it seems large. * * The constant PAGES_PER_WAITQUEUE specifies the ratio of pages to * waitqueues, i.e. the size of the waitq table given the number of pages. */ #define PAGES_PER_WAITQUEUE 256 // 这一块初始化管理区的等待队列。等待该管理区中页面的进程将会使用这个哈希表选择 // 一个队列等待。这意味着在页面解锁时并不需要唤醒所有的等待进程,仅需要唤醒其中的一 // 个子集。 // wait_table_size() 计算所用哈希表的大小。它基于管理区的页面数和队列数与页面 // 数之间的特定比率完成计算。该哈希表不会大于 4 KB。 // 每 PAGES_PER_WAITQUEUE(256)个使用同一个值 static inline unsigned long wait_table_size(unsigned long pages) { unsigned long size = 1; pages /= PAGES_PER_WAITQUEUE; while (size < pages) size <<= 1; /* * Once we have dozens or even hundreds of threads sleeping * on IO we've got bigger problems than wait queue collision. * Limit the size of the wait table to a reasonable size. */ size = min(size, 4096UL); return size; }
(e)⇒ wait_table_bits
// mm/page_alloc.c /* * This is an integer logarithm so that shifts can be used later * to extract the more random high bits from the multiplicative * hash function before the remainder is taken. */ static inline unsigned long wait_table_bits(unsigned long size) { return ffz(~size); }
(e)⇒ wait.h
// include/linux/wait.h struct __wait_queue_head { wq_lock_t lock; struct list_head task_list; #if WAITQUEUE_DEBUG long __magic; long __creator; #endif }; typedef struct __wait_queue_head wait_queue_head_t; static inline void init_waitqueue_head(wait_queue_head_t *q) { #if WAITQUEUE_DEBUG if (!q) WQ_BUG(); #endif q->lock = WAITQUEUE_RW_LOCK_UNLOCKED; INIT_LIST_HEAD(&q->task_list); #if WAITQUEUE_DEBUG q->__magic = (long)&q->__magic; q->__creator = (long)current_text_addr(); #endif }
(f)⇒ mm.h
// include/linux/mm.h /* * The zone field is never updated after free_area_init_core() * sets it, so none of the operations on it need to be atomic. */ #define NODE_SHIFT 4 #define ZONE_SHIFT (BITS_PER_LONG - 8) struct zone_struct; extern struct zone_struct *zone_table[]; static inline zone_t *page_zone(struct page *page) { return zone_table[page->flags >> ZONE_SHIFT]; } static inline void set_page_zone(struct page *page, unsigned long zone_num) { page->flags &= ~(~0UL << ZONE_SHIFT); page->flags |= zone_num << ZONE_SHIFT; } #define set_page_count(p,v) atomic_set(&(p)->count, v) #define SetPageReserved(page) set_bit(PG_reserved, &(page)->flags) /* * In order to avoid #ifdefs within C code itself, we define * set_page_address to a noop for non-highmem machines, where * the field isn't useful. * The same is true for page_address() in arch-dependent code. */ #if defined(CONFIG_HIGHMEM) || defined(WANT_PAGE_VIRTUAL) #define set_page_address(page, address) \ do { \ (page)->virtual = (address); \ } while(0) #else /* CONFIG_HIGHMEM || WANT_PAGE_VIRTUAL */ #define set_page_address(page, address) do { } while(0) #endif /* CONFIG_HIGHMEM || WANT_PAGE_VIRTUAL */
(g)⇐ 概览
(h)⇐ 伙伴算法
(6)build_zonelists
这个函数在所请求的节点中为每个管理区构造回退管理区。这是为了在不能满足一个分配时可以考察下一个管理区而设立的。在考察结束时,分配将从 ZONE_HIGHMEM 回退到 ZONE_NORMAL,在分配从 ZONE_NORMAL 回退到 ZONE_DMA 后就不再回退。
/* * Builds allocation fallback zone lists. */ static inline void build_zonelists(pg_data_t *pgdat) { int i, j, k; // 遍历最大可能数量的管理区。GFP_ZONEMASK(15) for (i = 0; i <= GFP_ZONEMASK; i++) { zonelist_t *zonelist; zone_t *zone; // 获取管理区的 zonelist 并归 0。 zonelist = pgdat->node_zonelists + i; memset(zonelist, 0, sizeof(*zonelist)); // 与 ZONE_DMA 对应,j 从 0 开始。 j = 0; // 设置 k 为当前检查过的管理区类型。 k = ZONE_NORMAL; if (i & __GFP_HIGHMEM) k = ZONE_HIGHMEM; if (i & __GFP_DMA) k = ZONE_DMA; switch (k) { default: BUG(); /* * fallthrough: */ case ZONE_HIGHMEM: // 获取 ZONE_HIGHMEM。 zone = pgdat->node_zones + ZONE_HIGHMEM; // 如果管理区有内存,ZONE_HIGHMEM 就是从高端内存分配的相应管理区。 // 如果 ZONE_HIGHMEM 没有内存,ZONE_NORMAL 将在下一次变成相应的管理区,这是 // 因为 j 并没有在空管理区时加 1。 if (zone->size) { #ifndef CONFIG_HIGHMEM BUG(); #endif zonelist->zones[j++] = zone; } // 设置下一个相应的管理区中分配的为 ZONE_NORMAL,同样,如果管理区没 // 有内存就不使用它。 case ZONE_NORMAL: zone = pgdat->node_zones + ZONE_NORMAL; if (zone->size) zonelist->zones[j++] = zone; case ZONE_DMA: // 设置最后的回退管理区为 ZONE_DMA。这个检查也对拥有内存的 ZONE_DMA // 进行。在 NUMA 体系结构中,不是所有的节点都有 ZONE_DMA。 zone = pgdat->node_zones + ZONE_DMA; if (zone->size) zonelist->zones[j++] = zone; } zonelist->zones[j++] = NULL; } }
(a)⇐ mmzone.h
// include/linux/mmzone.h #define ZONE_DMA 0 #define ZONE_NORMAL 1 #define ZONE_HIGHMEM 2 #define MAX_NR_ZONES 3 /* * One allocation request operates on a zonelist. A zonelist * is a list of zones, the first one is the 'goal' of the * allocation, the other zones are fallback zones, in decreasing * priority. * * Right now a zonelist takes up less than a cacheline. We never * modify it apart from boot-up, and only a few indices are used, * so despite the zonelist table being relatively big, the cache * footprint of this construct is very small. */ typedef struct zonelist_struct { zone_t * zones [MAX_NR_ZONES+1]; // NULL delimited } zonelist_t; #define GFP_ZONEMASK 0x0f
build_zonelists 函数执行后,pgdat->node_zonelists 为 15 个大小。其大致如下:
15 个 zonelist_t, k 的值依次为 ZONE_NORMAL、ZONE_DMA、ZONE_HIGHMEM、ZONE_DMA,每 4 个循环一次。而每个 zonelist_t 中有 4 个 zone_t,其值为:
当为 ZONE_NORMAL 时,4 个 zone_t 依次为指向 NORMAL、DMA、NULL、NULL
当为 ZONE_DMA 时,4 个 zone_t 依次为指向 DMA、NULL、NULL、NULL
当为 ZONE_HIGHMEM 时,4 个 zone_t 依次为指向 HIGHMEM、NORMAL、DMA、NULL
123