系列文章目录
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_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);
// 保留页面,即相应的位图的位设置为 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,而在 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) {
// ...
// 保留页面,即相应的位图的位设置为 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_dir 的 PDG 开始的页表。不管怎样,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