这篇博客接着上篇博客,继续介绍Linux内核启动过程中内存的初始化过程。
相比于汇编代码,分析C代码有一个优势,因为在之前的汇编代码中已经开启了分页模式,所以可以通过一些symbol直接在某些函数上设置断点,然后通过gdb进行调试。如何用gdb调试内核可以参考这篇博客。
进入x86_64_start_kernel
之前我们讲到,在secondary_startup_64
最后,我们通过far return进入了C语言实现的函数x86_64_start_kernel
,那么这篇我们就从这个函数开始讲起。
这个函数在arch/x86/kernel/head64.c
文件中,该函数有一个参数,是char * real_mode_data
,这个参数是在之前通过movq %rsi, %rdi
传进来的。
在该函数的开头,先做了一些sanity检查:
12345678910111213
/* * Build-time sanity checks on the kernel image and module * area mappings. (these are purely build-time and produce no code) */ BUILD_BUG_ON(MODULES_VADDR < __START_KERNEL_map); BUILD_BUG_ON(MODULES_VADDR - __START_KERNEL_map < KERNEL_IMAGE_SIZE); BUILD_BUG_ON(MODULES_LEN + KERNEL_IMAGE_SIZE > 2*PUD_SIZE); BUILD_BUG_ON((__START_KERNEL_map & ~PMD_MASK) != 0); BUILD_BUG_ON((MODULES_VADDR & ~PMD_MASK) != 0); BUILD_BUG_ON(!(MODULES_VADDR > __START_KERNEL)); BUILD_BUG_ON(!(((MODULES_END - 1) & PGDIR_MASK) == (__START_KERNEL & PGDIR_MASK))); BUILD_BUG_ON(__fix_to_virt(__end_of_fixed_addresses) <= MODULES_END);
主要是针对Module地址的检查(module被映射在0xffffffffa0000000
上面)。
之后做了一个操作,将之前建立的identity-map
给清除掉了,因为我们现在已经用高地址进行页表翻译了,所以那个identity-map也就没有用了:
123456789101112131415
/* Wipe all early page tables except for the kernel symbol map */static void __init reset_early_page_tables(void){ unsigned long i; for (i = 0; i < PTRS_PER_PGD-1; i++) early_level4_pgt[i].pgd = 0; next_early_pgt = 0; write_cr3(__pa(early_level4_pgt));} /* Kill off the identity-map trampoline */ reset_early_page_tables();
注意这里有一个__pa(early_level4_pgt)
,我们来看一下__pa
的定义:
123456789101112
static inline unsigned long __phys_addr_nodebug(unsigned long x){ unsigned long y = x - __START_KERNEL_map; /* use the carry flag to determine if x was < __START_KERNEL_map */ x = y + ((x > y) ? phys_base : (__START_KERNEL_map - PAGE_OFFSET)); return x;}#define __phys_addr(x) __phys_addr_nodebug(x)#define __pa(x) __phys_addr((unsigned long)(x))
之后调用clear_bss
,即将bss中的内容清零:
12345678910
/* Don't add a printk in there. printk relies on the PDA which is not initialized yet. */static void __init clear_bss(void){ memset(__bss_start, 0, (unsigned long) __bss_stop - (unsigned long) __bss_start);} /* clear bss before set_intr_gate with early_idt_handler */ clear_bss();
然后创建idtr gate:
12345678910
static inline void native_load_idt(const struct desc_ptr *dtr){ asm volatile("lidt %0"::"m" (*dtr));}#define load_idt(dtr) native_load_idt(dtr) for (i = 0; i < NUM_EXCEPTION_VECTORS; i++) set_intr_gate(i, early_idt_handlers[i]); load_idt((const struct desc_ptr *)&idt_descr);
然后调用copy_bootdata
:
123456789101112131415
static void __init copy_bootdata(char *real_mode_data){ char * command_line; unsigned long cmd_line_ptr; memcpy(&boot_params, real_mode_data, sizeof boot_params); sanitize_boot_params(&boot_params); cmd_line_ptr = get_cmd_line_ptr(); if (cmd_line_ptr) { command_line = __va(cmd_line_ptr); memcpy(boot_command_line, command_line, COMMAND_LINE_SIZE); }} copy_bootdata(__va(real_mode_data));
这里面主要是copy一些boot的参数,之后调用load_ucode_bsp
和early_printk
,这里都不详述。
然后设置init_level4_pgt
:
123
clear_page(init_level4_pgt); /* set init_level4_pgt kernel high mapping*/ init_level4_pgt[511] = early_level4_pgt[511];
后来还有一些函数调用和boot相关的,这里也不细说,最后调用start_kernel
:
12345678910111213
asmlinkage void __init x86_64_start_kernel(char * real_mode_data){ ... x86_64_start_reservations(real_mode_data);}void __init x86_64_start_reservations(char *real_mode_data){ ... start_kernel();}
start_kernel
下面进入start_kernel
函数,该函数定义在init/main.c
文件中。
里面调用了很多函数来做各种目的的初始化,其中和内存初始化相关的函数调用如下:
1234567891011121314
asmlinkage void __init start_kernel(void){ ... setup_arch(&command_line); ... setup_per_cpu_areas(); ... build_all_zonelist(NULL, NULL); page_alloc_init(); ... mm_init(); ... setup_per_cpu_pageset();}
如下图所示(截图自这里):
下面我们逐个函数进行介绍。
setup_arch
x86的setup_arch
定义在arch/x86/kernel/setup.c
文件中,其中和内存初始化相关的函数如下所示:
1234567891011121314151617181920
void __init setup_arch(char **cmdline_p){ setup_memory_map(); e820_reserve_setup_data(); init_mm.start_code = (unsigned long) _text; init_mm.end_code = (unsigned long) _etext; init_mm.end_data = (unsigned long) _edata; init_mm.brk = _brk_end; e820_add_kernel_range(); ... cleanup_highmap(); ... init_mem_mapping(); early_trap_pf_init(); ... x86_init.paging.pagetable_init(); // native_pagetable_init() -> paging_init (arch/x86/mm/init_64.c) ...}
其中,前面一直是在通过BIOS获得E820内存分布(e820请查阅这篇博客),以及初始化init_mm
。我们从cleanup_highmap
开始分析,该函数在arch/x86/mm/init_64.c
中:
1234567891011121314151617181920212223242526272829303132333435
/* * The head.S code sets up the kernel high mapping: * * from __START_KERNEL_map to __START_KERNEL_map + size (== _end-_text) * * phys_base holds the negative offset to the kernel, which is added * to the compile time generated pmds. This results in invalid pmds up * to the point where we hit the physaddr 0 mapping. * * We limit the mappings to the region from _text to _brk_end. _brk_end * is rounded up to the 2MB boundary. This catches the invalid pmds as * well, as they are located before _text: */void __init cleanup_highmap(void){ unsigned long vaddr = __START_KERNEL_map; unsigned long vaddr_end = __START_KERNEL_map + KERNEL_IMAGE_SIZE; unsigned long end = roundup((unsigned long)_brk_end, PMD_SIZE) - 1; pmd_t *pmd = level2_kernel_pgt; /* * Native path, max_pfn_mapped is not set yet. * Xen has valid max_pfn_mapped set in * arch/x86/xen/mmu.c:xen_setup_kernel_pagetable(). */ if (max_pfn_mapped) vaddr_end = __START_KERNEL_map + (max_pfn_mapped << PAGE_SHIFT); for (; vaddr + PMD_SIZE - 1 < vaddr_end; pmd++, vaddr += PMD_SIZE) { if (pmd_none(*pmd)) continue; if (vaddr < (unsigned long) _text || vaddr > end) set_pmd(pmd, __pmd(0)); }}
这段代码非常好理解,加上看注释,可以知道其功能就是将小于_text
和大于_brk_end
的地址都从页表中unmap掉。
接下来是init_mem_mapping
这个函数,该函数位于arch/x86/mm/init.c
:
1234567891011121314151617181920
void __init init_mem_mapping(void){ ... end = max_pfn << PAGE_SHIFT; /* the ISA range is always mapped regardless of memory holes */ init_memory_mapping(0, ISA_END_ADDRESS); memory_map_top_down(ISA_END_ADDRESS, end); if (max_pfn > max_low_pfn) { /* can we preseve max_low_pfn ?*/ max_low_pfn = max_pfn; } load_cr3(swapper_pg_dir); __flush_tlb_all(); early_memtest(0, max_pfn_mapped << PAGE_SHIFT);}
这里面虽然代码少,但是信息量还是蛮大的,我们一个一个来看。
首先是init_memory_mapping
:
1234567891011121314151617181920212223242526
/* * Setup the direct mapping of the physical memory at PAGE_OFFSET. * This runs before bootmem is initialized and gets pages directly from * the physical memory. To access them they are temporarily mapped. */unsigned long __init_refok init_memory_mapping(unsigned long start, unsigned long end){ struct map_range mr[NR_RANGE_MR]; unsigned long ret = 0; int nr_range, i; pr_info("init_memory_mapping: [mem %#010lx-%#010lx]\n", start, end - 1); memset(mr, 0, sizeof(mr)); nr_range = split_mem_range(mr, 0, start, end); for (i = 0; i < nr_range; i++) ret = kernel_physical_mapping_init(mr[i].start, mr[i].end, mr[i].page_size_mask); add_pfn_range_mapped(start >> PAGE_SHIFT, ret >> PAGE_SHIFT); return ret >> PAGE_SHIFT;}
这里注释中提到的PAGE_OFFSET
值为0xffff880000000000
(0xffff880000000000
到0xffffc7ffffffffff
为所有物理地址的direct mapping)。
这里有两个主要的函数,我们先来看split_mem_range
(位于arch/x86/mm/init.c
):
123456
static int __meminit split_mem_range(struct map_range *mr, int nr_range, unsigned long start, unsigned long end){ ...}
里面代码比较复杂,和之前在分析xen代码中某个函数有点像,这里就不逐段分析。简单说一下它做了什么吧。split_mem_range
的作用就是将整个物理地址段进行了一个分类,把所有地址分为三类:
然后将start
到end
的物理地址段分别塞进这些段中,然后将每个段的信息保存在mr
这个数据结构中。这个数据结构包括了每个地址段的起始地址、结束地址、以及alignment。最后有一个merge过程,将mr
中相邻且alignment相同的项进行合并。
最后分出来的地址段的结果如下图所示:
另外一个函数为kernel_physical_mapping_init
(位于arch/x86/mm/init_64.c
):
12345678910111213141516171819202122232425262728293031323334353637383940414243
unsigned long __meminitkernel_physical_mapping_init(unsigned long start, unsigned long end, unsigned long page_size_mask){ bool pgd_changed = false; unsigned long next, last_map_addr = end; unsigned long addr; start = (unsigned long)__va(start); end = (unsigned long)__va(end); addr = start; for (; start < end; start = next) { pgd_t *pgd = pgd_offset_k(start); pud_t *pud; next = (start & PGDIR_MASK) + PGDIR_SIZE; if (pgd_val(*pgd)) { pud = (pud_t *)pgd_page_vaddr(*pgd); last_map_addr = phys_pud_init(pud, __pa(start), __pa(end), page_size_mask); continue; } pud = alloc_low_page(); last_map_addr = phys_pud_init(pud, __pa(start), __pa(end), page_size_mask); spin_lock(&init_mm.page_table_lock); pgd_populate(&init_mm, pgd, pud); spin_unlock(&init_mm.page_table_lock); pgd_changed = true; } if (pgd_changed) sync_global_pgds(addr, end - 1); __flush_tlb_all(); return last_map_addr;}
这是一个非常关键的函数,它的作用就是填充页表,将所有之前探寻到并且分割好的物理地址映射到对应的虚拟内存中,并在页表中体现出来。我们来逐段分析:
首先通过__va
这个宏将物理地址转换成其对应的(direct mapping)虚拟地址,即加上0xffff880000000000
。
12
start = (unsigned long)__va(start); end = (unsigned long)__va(end);
然后就是传统的走页表过程了,这里有个宏需要说明:
123456789101112
#define swapper_pg_dir init_level4_pgt;struct mm_struct init_mm = { .pgd = swapper_pg_dir, ...}#define pgd_offset(mm, address) ((mm)->pgd + pgd_index((address)))#define pgd_offset_k(address) pgd_offset(&init_mm, (address))pgd_t *pgd = pgd_offset_k(start);
也就是说,在这个时候,pgt_dir从原来的early_level4_pgt
变成了init_level4_pgt
,这个数据结构同样是在arch/x86/kernel/head_64.S
中定义的:
12345678910111213141516
NEXT_PAGE(init_level4_pgt) .quad level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE .org init_level4_pgt + L4_PAGE_OFFSET*8, 0 .quad level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE .org init_level4_pgt + L4_START_KERNEL*8, 0 /* (2^48-(2*1024*1024*1024))/(2^39) = 511 */ .quad level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLENEXT_PAGE(level3_ident_pgt) .quad level2_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE .fill 511, 8, 0NEXT_PAGE(level2_ident_pgt) /* Since I easily can, map the first 1G. * Don't set NX because code runs from these pages. */ PMDS(0, __PAGE_KERNEL_IDENT_LARGE_EXEC, PTRS_PER_PMD)
因此,加上init_level4_pgt
这个页表后,内存的分布图如下所示:
所以kernel_physical_mapping_init
后面的代码就是根据不同mr
数据结构中的地址段将这个页表进行填充,这里就不详述了。
在执行完init_memory_mapping
之后,init_mem_mapping
函数又执行了一个memory_map_top_down
函数,里面其实也是根据不同的地址段,连续调用init_range_memory_mapping
,从而间接调用init_memory_mapping
函数。
最后,将swapper_pg_dir
加载进cr3
,完成页表的转换。
现在让我们回到setup_arch
,调用paging_init
(位于arch/x86/mm/init_64.c
)。里面主要完成一些zones的初始化,不详述。
再次回到start_kernel
,在setup_arch
之后还陆续调用了几个和percpu以及memory zones,memory allocator相关的函数,这里也不详细说了。
这个系列就先简单介绍到这里,其实后面还有很多内容没有详细介绍,主要目的是搞清楚内核是如何创建页表的。
原文地址:http://ytliu.info/blog/2016/03/15/linuxnei-cun-chu-shi-hua-c/