系列文章目录
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)总结
- 二、页表管理
- 1、初始化页表
一、启动内存分配
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_pfn,max_low_pfn),找到高端内存的 PFN 的起点和终点(highstart_pfn,highend_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);// 保留页面,即相应的位图的位设置为 1reserve_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,而在 NUMA,init_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_pfn−start_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) {// ... // 保留页面,即相应的位图的位设置为 1reserve_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, 来表示对应的页被占用了。
二、页表管理
1、初始化页表
在上文 setup_arch 函数中有:
// arch/i386/kernel/setup.c
void __init setup_arch(char **cmdline_p) {// ...max_low_pfn = setup_memory();paging_init();// ...
}
123