深入理解Linux虚拟内存管理(四)

news2024/12/20 0:48:46

在这里插入图片描述

系列文章目录


Linux 内核设计与实现
深入理解 Linux 内核(一)
深入理解 Linux 内核(二)
Linux 设备驱动程序(一)
Linux 设备驱动程序(二)
Linux 设备驱动程序(三)
Linux设备驱动开发详解
深入理解Linux虚拟内存管理(一)
深入理解Linux虚拟内存管理(二)
深入理解Linux虚拟内存管理(三)
深入理解Linux虚拟内存管理(四)


文章目录

  • 系列文章目录
  • 一、启动内存分配
    • 1、初始化引导内存分配器
      • (1)start_kernel
      • (2)setup_arch
      • (3)setup_memory
      • (4)init_bootmem
      • (5)init_bootmem_core
      • (6)总结
    • 2、释放内存
      • (1)register_bootmem_low_pages
      • (2)free_bootmem
      • (3)free_bootmem_core
      • (4)总结
    • 3、保留大块区域的内存
      • (1)reserve_bootmem
      • (2)reserve_bootmem_core
      • (3)总结
    • 4、在启动时分配内存
      • (1)alloc_bootmem
      • (2)__alloc_bootmem
      • (3)__alloc_bootmem_core
  • 二、页表管理
    • 1、初始化页表
      • (1)paging_init
      • (2)pagetable_init
        • pgtable-2level.h
        • pgtable.h
        • page.h
        • pagetable_init
      • (3)alloc_bootmem_low_pages


一、启动内存分配

1、初始化引导内存分配器

(1)start_kernel

// init/main.c
asmlinkage void __init start_kernel(void)
{
	// ...
	setup_arch(&command_line);
	// ...
}

(2)setup_arch

// arch/i386/kernel/setup.c
void __init setup_arch(char **cmdline_p) {
	// ...
	max_low_pfn = setup_memory();
	paging_init();
	// ...	
}

(3)setup_memory

    这个函数的调用图如图 2.3 所示。它为引导内存分配器初始化自身进行所需信息的获取。它可以分成几个不同的任务。

  • 找到低端内存的 PFN 的起点和终点(min_low_pfnmax_low_pfn),找到高端内存的 PFN 的起点和终点(highstart_pfnhighend_pfn),以及找到系统中最后一页的 PFN
  • 初始化 bootmem_date 结构以及声明可能被引导内存分配器用到的页面。
  • 标记所有系统可用的页面为空闲,然后为那些表示页面的位图保留页面。
  • SMP 配置或 initrd 镜像存在时,为它们保留页面。
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;			 
}

(4)init_bootmem

// mm/bootmem.c
// 这是容易混淆的地方。参数 pages 实际上是该节点可寻址内存的 PFN 末端,而不是
// 按名字的意思:页面数。
unsigned long __init init_bootmem (unsigned long start, unsigned long pages) {
	// 如果没有依赖于体系结构的代码,则设置该节点的可寻址最大 PFN。
	max_low_pfn = pages;
	// 如果没有依赖于体系结构的代码,则设置该节点的可寻址最小 PFN。
	min_low_pfn = start;
	// 调用 init_bootmem_core()(见 E.1.3 小节),在那里完成初始化 bootmem_data 的实
	// 际工作。
	return(init_bootmem_core(&contig_page_data, start, 0, pages));
}

(5)init_bootmem_core

static unsigned long __init init_bootmem_core (pg_data_t *pgdat,
	unsigned long mapstart, unsigned long start, unsigned long end)
{
	bootmem_data_t *bdata = pgdat->bdata;
	unsigned long mapsize = ((end - start)+7)/8;

	pgdat->node_next = pgdat_list;
	pgdat_list = pgdat;

	mapsize = (mapsize + (sizeof(long) - 1UL)) & ~(sizeof(long) - 1UL);
	// 内存最低内存分配给 node_bootmem_map , 大小为 mapsize 个页面(struct page)
	bdata->node_bootmem_map = phys_to_virt(mapstart << PAGE_SHIFT);
	bdata->node_boot_start = (start << PAGE_SHIFT);
	bdata->node_low_pfn = end;

	/*
	 * Initially all pages are reserved - setup_arch() has to
	 * register free RAM areas explicitly.
	 */
	// 把所有位图初始化为 1, 即所有内存设置为保留(被占用)
	memset(bdata->node_bootmem_map, 0xff, mapsize);

	return mapsize;
}

(6)总结

    一旦 setup_memory() 确定了可用物理页面的界限,系统将从两个引导内存的初始化函数中选择一个,并以待初始化的节点的起始和终止 PFN 作为调用参数。在 UMA 结构中,init_bootmem() 用于初始化 contig_page_data,而在 NUMAinit_bootmem_node() 则初始化一个具体的节点。这两个函数主要通过调用 init_bootmem_core() 来完成实际工作。

    内核函数首先要把 pgdat_data_t 插入到 pgdat_list 链表中,因为这个节点在函数末尾很快就会用到。然后它记录下该节点的起始和结束地址(该节点与 bootmem_data_t 有关)并且分配一个位图来表示页面的分配情况。位图所需的大小以字节计算,计算公式如下:

m a p s i z e = ( e n d _ p f n − s t a r t _ p f n ) + 7 8 mapsize =\frac{(end\_pfn-start\_pfn)+7}{8} mapsize=8(end_pfnstart_pfn)+7

    该位图存放于由 bootmem_data_t→node_boot_start 指向的物理地址处,而其虚拟地址的映射由 bootmem_data_t→node_bootmem_map 指定。由于不存在与结构无关的方式来检测内存中的空洞,整个位图就被初始化为 1 来标志所有页已被分配。将可用页面的位设置为 0 的工作则由与结构相关的代码完成。在 x86 结构中,register_bootmem_low_pages() 通过检测 e820 映射图,并在每一个可用页面上调用 free_bootmem() 函数,将其位设为 1,然后再调用 reserve_bootmem() 为保存实际位图所需的页面预留空间。

2、释放内存

    在上文 setup_memory 函数中有:

static unsigned long __init setup_memory(void) {
	// ...	
	register_bootmem_low_pages(max_low_pfn);
	// ...	
	return max_low_pfn;			 
}

(1)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));
	}
}

(2)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));
}

(3)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();
	}
}

(4)总结

    由上分析可知:函数 free_bootmem_core 主要就是把对应页帧号的位图(bootmem_data_t ->node_bootmem_map)设置为 0, 来表示对应的页是空闲的。

3、保留大块区域的内存

    在上文 setup_memory 函数中有:

static unsigned long __init setup_memory(void) {
	// ...	
	// 保留页面,即相应的位图的位设置为 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
	// ...	
	return max_low_pfn;			 
}

(1)reserve_bootmem

// mm/bootmem.c
void __init reserve_bootmem (unsigned long addr, unsigned long size)
{
	reserve_bootmem_core(contig_page_data.bdata, addr, size);
}

(2)reserve_bootmem_core

// mm/bootmem.c
/*
 * Marks a particular physical memory range as unallocatable. Usable RAM
 * might be used for boot-time allocations - or it might get added
 * to the free page pool later on.
 */
static void __init reserve_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long size)
{
	unsigned long i;
	/*
	 * round up, partially reserved pages are considered
	 * fully reserved.
	 */
	// sidx 是服务页的起始索引。它的值是从请求地址中减去起始地址并除以页大小得到的。	 
	unsigned long sidx = (addr - bdata->node_boot_start)/PAGE_SIZE;
	// 末尾索引 eidx 的计算与 sidx 类似,但它的分配是向上取整到最近的页面。这意味着
	// 对保留一页中部分请求将导致整个页都被保留。
	unsigned long eidx = (addr + size - bdata->node_boot_start + 
							PAGE_SIZE-1)/PAGE_SIZE;
	// end 是受本次保留影响的最后 PFN。							
	unsigned long end = (addr + size + PAGE_SIZE-1)/PAGE_SIZE;
	// 检查是否给定了一个非零值。
	if (!size) BUG();
	// 检查起始索引不在节点起点之前。
	if (sidx < 0)
		BUG();
	// 检查末尾索引不在节点末端之后。		
	if (eidx < 0)
		BUG();
	// 检查起始索引不在末尾索引之后。		
	if (sidx >= eidx)
		BUG();
	// 检查起始地址没有超出该启动内存节点所表示的内存范围。		
	if ((addr >> PAGE_SHIFT) >= bdata->node_low_pfn)
		BUG();
	// 检查末尾地址没有超出该启动内存节点所表示的内存范围。		
	if (end > bdata->node_low_pfn)
		BUG();
	// 从 sidx 开始,到 eidx 结束,这里测试和设置启动内存分配图中表示页面已经分
	// 配的位。如果该位已经设置为 1,则打印一条消息:该位被设置了两次。		
	for (i = sidx; i < eidx; i++)
		if (test_and_set_bit(i, bdata->node_bootmem_map))
			printk("hm, page %08lx reserved twice.\n", i*PAGE_SIZE);
}

(3)总结

    由上分析可知:保留内存主要通过函数 reserve_bootmem_core 来实现的,其主要就是把对应页帧号的位图(bootmem_data_t ->node_bootmem_map)设置为 1, 来表示对应的页被占用了。

4、在启动时分配内存

(1)alloc_bootmem

// include/linux/bootmem.h
// 将 L1 硬件高速缓存页对齐,并在 DMA 中可用的最大地址后开始查找一页。
#define alloc_bootmem(x) \
	__alloc_bootmem((x), SMP_CACHE_BYTES, __pa(MAX_DMA_ADDRESS))
// 将 L1 硬件高速缓存页对齐,并从 0 开始查找。	
#define alloc_bootmem_low(x) \
	__alloc_bootmem((x), SMP_CACHE_BYTES, 0)
	
// 将已分配位对齐到一页大小,这样满页将从 DMA 可用的最大地址开始分配。
#define alloc_bootmem_pages(x) \
	__alloc_bootmem((x), PAGE_SIZE, __pa(MAX_DMA_ADDRESS))
// 将已分配位对齐到一页大小,这样满页将从物理地址 0 开始分配。	
#define alloc_bootmem_low_pages(x) \
	__alloc_bootmem((x), PAGE_SIZE, 0)

(2)__alloc_bootmem

// mm/page_alloc.c
pg_data_t *pgdat_list;

// include/linux/mmzone.h
#define for_each_pgdat(pgdat) \
	for (pgdat = pgdat_list; pgdat; pgdat = pgdat->node_next)
// mm/bootmem.c
// 
// size 是请求分配的大小。
// align 是指定的对齐方式,它必须是 2 的幂。目前,一般设置为 SMP_CACHE_BYTES 或 PAGE_SIZE。
// goal 是开始查询的起始地址。
void * __init __alloc_bootmem (unsigned long size, unsigned long align, unsigned long goal)
{
	pg_data_t *pgdat;
	void *ptr;

	// 遍历所有的可用节点,并试着轮流从各个节点开始分配。在 UMA 中,就从
	// contig_page_data 节点开始分配。
	for_each_pgdat(pgdat)
		if ((ptr = __alloc_bootmem_core(pgdat->bdata, size,
						align, goal)))
			return(ptr);

	/*
	 * Whoops, we cannot satisfy the allocation request.
	 */
	// 如果分配失败,系统将不能启动,故系统瘫痪。	 
	printk(KERN_ALERT "bootmem alloc of %lu bytes failed!\n", size);
	panic("Out of memory");
	return NULL;
}

(3)__alloc_bootmem_core

    这是从一个带有引导内存分配器的特定节点中分配内存的核心函数。它非常大,所以将其分解成如下步骤:

  • 函数开始处保证所有的参数都正确。
  • goal 参数为基础计算开始扫描的起始地址。
  • 检查本次分配是否可以使用上次分配的页面以节省内存。
  • 在位图中标记已分配页为 1,并将页中内容清 0

这是函数的前面部分,它保证参数有效。
144 参数如下:
l bdata 是要分配结构体的启动内存。
size 是请求分配的大小。
align 是分配的对齐方式,它必须是 2 的需。
goal 是上面要分配的最可能的合适大小。
151 计算末尾位索引 eidx,它返回可能用于分配的最高页面索引。
154 如果指定了 0 则调用 BUGO。
156~157 如果对齐不是 2 的罪,则调用 BUG()。
159 对齐的缺省偏移是 0。
160 如果指定了对齐方式则・・・・
161 请求的对齐方式与开始节点的对齐方式相同,这里计算偏移。
162 要使用的偏移与起始地址低位标记的请求对齐。实际上,这里的 offset 与 align 的一
般值 align 相似。

这一块计算开始的 PFN 从 goal 参数为基础的地址处开始扫描。
169 如果指定了 goal,且 goal 在该节点的开始地址之后,goal 小于该节点可寻址 PFN,
170 开始的适当偏移是 goal 减去该节点可寻址的内存起点。
173 如果不是这样,则适当偏移为 0。
175~176 考虑偏移,调整适当偏移的大小,这样该地址将可以正确对齐。
177 本次分配影响到的页面数存放在 areasize 中。
178 incr 是要跳过的页面数,如果多于一页,则它们满足对齐请求。

这一块扫描内存,寻找一块足够大的块来满足请求。
180 如果本次分配不能满足从 goal 开始,则跳到这里的标志,这样将重新扫描该映射图。
181~194 从 preferred 开始,这里线性扫描一块足够大的块来满足请求。这里以 incr 为
步长来遍历整个地址空间以满足大于一页的对齐。如果对齐小于一页,incr 为 1。
185~190 扫描下一个 areasize 大小的页面来确定它是否也可以被释放。如果到达可寻
址空间的末端(eidx)或者其中的一页已经在使用,则失败。
191~192 找到一个空闲块,所以这里记录 start,并跳转到找到的块。
195~198 分配失败,所以从开始处重新开始。
199 如果再次失败,则返回 NULL,这将导致系统瘫疾。

这一块测试以确定本次分配可以与前一次分配合并。
201~202 检查分配的起点不会在可寻址内存之之后。刚才已经检查过,所以这里是多
余的。
209~230 如果对齐小于 PAGE_SIZE,前面的页面在其中有空间(last_offset != 0),而且
前面使用的页与本次分配的页相邻,则试着与前一次分配合并。
231~234 如果没有,这里记录本次分配用到的页面和偏移,以便于下次分配时的合并。
211 更新用于对 align 请求正确分页的偏移。
212~213 如果偏移现在超过了页面边界,则调用 BUG()。这个条件需要使用一次非常
槽糕的对齐选择。由于一般使用的对齐仅是一个 PAGE_SIZE 的因子,所以不可能在平常
使用。
214 remaining_size 是以前用过的页面中处于空闲的空间。
215~221 如果在旧的页面中有足够的空间剩余,这里使用旧的页面,然后更新 bootmem_。
data 结构来反映这一点。
221~228 如果不是这样,则这里计算除了这一页外还需要多少页面,并更新 bootmem_
data。
216 这次分配中用到的页面数现在为 0。
218 更新 last_offset 为本次分配的末端。
219 计算返回成功分配的虚拟地址。
222 remaining_size 是上一次用来满足分配的页面空间。
223 计算还需要多少页面来满足分配请求。
224 计算分配开始的地址。
226 使用到的最后一页是 start 页面加上满足这次分配的额外页面数量 areasize。
227 已经计算过本次分配的末端。
229 如果偏移在页尾,则标记为 0。
231 不发生合并,所以这里记录用到的满足本次分配的最后一页。
232 记录用到的最后一页。
233 记录分配的起始虚拟地址。

这一块在位图中标记分配页为 1,并将其内容清 0。
238~240 遍历本次分配用到的所有页面,在位图中设置 1。如果它们中已经有 1,则发生
了一次重复分配,所以调用 BUG()。
241 将页面用 0 填充。
242 返回分配的地址。

/*
 * We 'merge' subsequent allocations to save space. We might 'lose'
 * some fraction of a page if allocations cannot be satisfied due to
 * size constraints on boxes where there is physical RAM space
 * fragmentation - in these cases * (mostly large memory boxes) this
 * is not a problem.
 *
 * On low memory boxes we get it right in 100% of the cases.
 */

/*
 * alignment has to be a power of 2 value.
 */
static void * __init __alloc_bootmem_core (bootmem_data_t *bdata, 
	unsigned long size, unsigned long align, unsigned long goal)
{
	unsigned long i, start = 0;
	void *ret;
	unsigned long offset, remaining_size;
	unsigned long areasize, preferred, incr;
	unsigned long eidx = bdata->node_low_pfn - (bdata->node_boot_start >>
							PAGE_SHIFT);

	if (!size) BUG();

	if (align & (align-1))
		BUG();

	offset = 0;
	if (align &&
	    (bdata->node_boot_start & (align - 1UL)) != 0)
		offset = (align - (bdata->node_boot_start & (align - 1UL)));
	offset >>= PAGE_SHIFT;

	/*
	 * We try to allocate bootmem pages above 'goal'
	 * first, then we try to allocate lower pages.
	 */
	if (goal && (goal >= bdata->node_boot_start) && 
			((goal >> PAGE_SHIFT) < bdata->node_low_pfn)) {
		preferred = goal - bdata->node_boot_start;
	} else
		preferred = 0;

	preferred = ((preferred + align - 1) & ~(align - 1)) >> PAGE_SHIFT;
	preferred += offset;
	areasize = (size+PAGE_SIZE-1)/PAGE_SIZE;
	incr = align >> PAGE_SHIFT ? : 1;

restart_scan:
	for (i = preferred; i < eidx; i += incr) {
		unsigned long j;
		if (test_bit(i, bdata->node_bootmem_map))
			continue;
		for (j = i + 1; j < i + areasize; ++j) {
			if (j >= eidx)
				goto fail_block;
			if (test_bit (j, bdata->node_bootmem_map))
				goto fail_block;
		}
		start = i;
		goto found;
	fail_block:;
	}
	if (preferred) {
		preferred = offset;
		goto restart_scan;
	}
	return NULL;
found:
	if (start >= eidx)
		BUG();

	/*
	 * Is the next page of the previous allocation-end the start
	 * of this allocation's buffer? If yes then we can 'merge'
	 * the previous partial page with this allocation.
	 */
	if (align <= PAGE_SIZE
	    && bdata->last_offset && bdata->last_pos+1 == start) {
		offset = (bdata->last_offset+align-1) & ~(align-1);
		if (offset > PAGE_SIZE)
			BUG();
		remaining_size = PAGE_SIZE-offset;
		if (size < remaining_size) {
			areasize = 0;
			// last_pos unchanged
			bdata->last_offset = offset+size;
			ret = phys_to_virt(bdata->last_pos*PAGE_SIZE + offset +
						bdata->node_boot_start);
		} else {
			remaining_size = size - remaining_size;
			areasize = (remaining_size+PAGE_SIZE-1)/PAGE_SIZE;
			ret = phys_to_virt(bdata->last_pos*PAGE_SIZE + offset +
						bdata->node_boot_start);
			bdata->last_pos = start+areasize-1;
			bdata->last_offset = remaining_size;
		}
		bdata->last_offset &= ~PAGE_MASK;
	} else {
		bdata->last_pos = start + areasize - 1;
		bdata->last_offset = size & ~PAGE_MASK;
		ret = phys_to_virt(start * PAGE_SIZE + bdata->node_boot_start);
	}
	/*
	 * Reserve the area now:
	 */
	for (i = start; i < start+areasize; i++)
		if (test_and_set_bit(i, bdata->node_bootmem_map))
			BUG();
	memset(ret, 0, size);
	return ret;
}

二、页表管理

1、初始化页表

    在上文 setup_arch 函数中有:

// arch/i386/kernel/setup.c
void __init setup_arch(char **cmdline_p) {
	// ...
	max_low_pfn = setup_memory();
	paging_init();
	// ...	
}

(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_dirPDG 开始的页表。不管怎样,PTE 将可以指向在 ZONE_NORMAL 中的每个页面帧。

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))

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)

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) } )

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

// include/linux/bootmem.h
#define alloc_bootmem_low_pages(x) \
	__alloc_bootmem((x), PAGE_SIZE, 0)

123

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/612397.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

精选一线企业最佳生产实践,《Apache Doris 用户案例集》重磅发布!

过去的一年势必是 Apache Doris 在发展历程中有着浓墨重彩的一年&#xff0c;凭借对技术创新的执着与用户体验的追求&#xff0c;Apache Doris 已俘获全球范围内超过 2000 家企业的认可&#xff0c;拥有了极为广泛的用户规模、在企业实时数据分析的多种场景中得到广泛应用&…

热门AI通用大模型对比盘点(附论文)

今天我来和大家聊聊通用大模型&#xff0c;垂直领域大模型等整理完了再和大家分享。大家可以先关注一下我&#xff0c;有更新可以立马看见。 本文文末有整理好的通用大模型论文&#xff0c;都是各个大模型的原始论文&#xff0c;强烈建议大模型方向的同学&#xff0c;或者对大…

突破软件交付不可能三角,企业级无代码如何实现卓越交付?

一、VUCA时代下项目交付面临的困境 软件开发或软件项目交付一直以来都存在着“不可能三角”&#xff0c;即成本、效率和质量三者难以兼得。 交付周期长、成本高、满意度低等一直是行业内长期存在的现象&#xff0c;甚至软件交付双方都习以为常。传统项目管理与软件实施过程难…

Niagara—— Texture Sample 与 Particle Subuv 区别

一&#xff0c;Texture Sample 此节点是最基本的采样节点&#xff0c;依据UV坐标来采样Texture&#xff1b; MipValueMode&#xff0c;设置采样的Mipmap Level&#xff1b; None&#xff0c;根据当前Texture大小和物理缩放&#xff0c;自动选择合适的 Mipmap Level &#xff1b…

行为型设计模式03-观察者模式

&#x1f9d1;‍&#x1f4bb;作者&#xff1a;猫十二懿 &#x1f3e1;账号&#xff1a;CSDN 、个人博客 、Github &#x1f38a;公众号&#xff1a;猫十二懿 观察者模式 1、观察者模式介绍 观察者模式是一种行为型设计模式&#xff0c;也被称为发布-订阅模式&#xff0c;它定…

Hive学习---4、函数(单行函数、高级聚合函数、炸裂函数、窗口函数)

1、函数 1.1 函数简介 Hive会将常用的逻辑封装成函数给用户进行使用&#xff0c;类似java中的函数。 好处&#xff1a;避免用户反复写逻辑&#xff0c;可以直接拿来使用 重点&#xff1a;用户需要知道函数叫什么&#xff0c;能做什么 Hive提供了大量的内置函数&#xff0c;按…

《相信》读后感

近日阅读了蔡磊的《相信》一书&#xff0c;蔡磊先生曾是京东集团副总裁&#xff0c;中国电子发票的推动者。上天给了他优越的智商条件&#xff0c;从上学到工作&#xff0c;前半生几乎顺风顺水、获誉无数&#xff0c;却在初为人父、本该享受家庭幸福的时候&#xff0c;接到突患…

Python字典及用法详解

Python中的字典&#xff08;Dictionary&#xff09;是一种无序、可变的数据类型&#xff0c;用于存储键&#xff08;Key&#xff09;和值&#xff08;Value&#xff09;之间的映射关系。字典是一种高效的数据结构&#xff0c;可以用于快速查找和检索数据。 1.创建字典 可以使…

MobileViT详解:轻型,通用,移动友好的视觉变压器

MobileViT详解&#xff1a;轻型&#xff0c;通用&#xff0c;移动友好的视觉变压器 0. 引言1. 网络结构2. 模型详解2.1 MobileViT Block2.1.1 Local representations2.1.2 Transformers as Convolutions (global representations)2.1.3 Fusion 2.2 MV2 3. 简化版理解4. 总结 0.…

Ubuntu系统搭建FTP服务器

Ubuntu 系统版本&#xff1a;Ubuntu 22.04.2 LTS 安装 vsftpd 软件包 sudo apt-get update sudo apt-get install vsftpd查看版本&#xff0c;验证是否安装成功&#xff1a;vsftpd -v 配置文件 以下是我翻译后的默认配置文件&#xff08;地址 /etc/vsftpd.conf&#xff09;&a…

[NOI2009] 描边

题目描述 小 Z 是一位杰出的数学家。聪明的他特别喜欢研究一些数学小问题。 有一天&#xff0c;他在一张纸上选择了 n 个点&#xff0c;并用铅笔将它们两两连接起来&#xff0c;构成 (&#xfffd;−1)22n(n−1)​ 条线段。由于铅笔很细&#xff0c;可以认为这些线段的宽度为…

ROS:参数的使用与编程方法

目录 一、参数模型二、 创建功能包三、参数命令行的使用(rosparam)四、使用程序来使用参数&#xff08;C&#xff09;4.1创建代码4.2编译4.3运行 一、参数模型 在ROS Master中&#xff0c;存在一个参数服务器&#xff08;Parameter Server&#xff09;&#xff0c;它是一个全局…

Python高光谱遥感数据处理与机器学习实践技术丨Matlab高光谱遥感数据处理与混合像元分解

目录 Python高光谱遥感数据处理与机器学习实践技术 第一章 高光谱基础 第二章 高光谱开发基础&#xff08;Python&#xff09; 第三章 高光谱机器学习技术&#xff08;python&#xff09; 第四章 典型案例操作实践 Matlab 高光谱遥感数据处理与混合像元分解 第一章 理论…

java SSM 互助旅游管理系统myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

一、源码特点 java SSM 互助旅游管理系统是一套完善的web设计系统&#xff08;系统采用SSM框架进行设计开发&#xff0c;springspringMVCmybatis&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采…

预制菜进击万亿市场,谁能更快上桌“吃菜”?

文 | 螳螂观察 作者 | 图霖 消费行业很少有可持续的风口&#xff0c;这两年的预制菜算其中一个。 艾媒咨询发布的行业预测显示&#xff0c;2026年我国预制菜市场规模有望达到10720亿元。 过去这一年&#xff0c;武汉、大同等地已相继召开了预制菜相关的产业峰会。峰会规模有…

gismo-3维IGA

文章目录 前言一、简单示例二、gismo-3维IGA3维程序中的几何模型 三、xml文件的理解1、xml文件示例2、gismo中二维示例文件-一个曲面&#xff08;简单&#xff09; 四、三维程序中xml文件的理解三维几何模型边界信息 五、三维程序运行细化四次细化5次 总结 #pic_center 前言 只…

C#读写FDX-B ISO11784/85协议动物标签源码

一个FDX-B ISO11784/85协议动物标签内包括了以下信息&#xff1a; 11位的前导码&#xff1b;38位的SN序号&#xff1b;10位国家代码&#xff1b;1位data block标识&#xff1b;14位保留位&#xff1b;1位Animal动物标识&#xff1b;以上64位数据的crc16ccitt校验码&#xff0c…

短视频矩阵源码系统打包.源码

Masayl是一款基于区块链技术的去中心化应用程序开发平台&#xff0c;可帮助开发者快速、便捷地创建去中心化应用程序。Masayl拥有丰富的API和SDK&#xff0c;为开发者们提供了支持。此外&#xff0c;Masayl还采用了高效的智能合约技术&#xff0c;确保应用程序的稳定、安全和高…

项目集管理—项目集治理

一、概述 项目集治理是实现和执行项目集决策&#xff0c;为支持项目集而制定实践&#xff0c;并维持项目集监督的绩效领域。 本章包括&#xff1a; 项目集治理实践项目集治理角色项目集治理设计与实施 项目集治理包括为了满足组织战略和运营目标的要求&#xff0c;对项目集实…

【虹科案例】虹科数字化仪在激光雷达大气研究中的应用

01 莱布尼茨研究所使用激光雷达进行大气研究 图 1&#xff1a;在 Khlungsborn 的 IAP 办公室测试各种激光器 大气研究使用脉冲激光束通过测量大气中 100 公里高度的多普勒频移和反向散射光来测量沿光束的温度和风速。返回的光信号非常微弱&#xff0c;会被阳光阻挡&#xff0c…