在构架相关的汇编代码运行完之后,程序跳入了构架无关的内核C语言代码:init/main.c中的start_kernel函数,在这个函数中Linux内核开始真正进入初始化阶段,
下面我就顺这代码逐个函数的解释,但是这里并不会过于深入每个函数,因为这样就会只见树木,不见森林。分析代码首先要从构架上宏观地理解,然后再去考虑细节问题(这和小时候学语文要概括中心思想和段落大意是一个道理)。今后对于每个子系统的初始化,特别是内存子系统,有空都会分析一遍。
- asmlinkage void __init start_kernel(void)
- {
- char * command_line;
- extern const struct kernel_param __start___param[], __stop___param[];
- 这里关键是这两个变量的地址是如何确定的。
- 这两个变量为地址指针,指向内核启动参数处理相关结构体段在内存中的位置(虚拟地址)。
- 这里是外部变量,定义的位置在arch/../../vmlinux.lds.S,而大多数平台是放到kernel\include\asm-generic\vmlinux.lds.h中,定义如下:
- 362 /* 内建模块的参数处理段. */ \
- 363 __param : AT(ADDR(__param) - LOAD_OFFSET) { \
- 364 VMLINUX_SYMBOL(__start___param) = .; \
- 365 *(__param) \
- 366 VMLINUX_SYMBOL(__stop___param) = .; \
- 367 }
- 对于ARM平台,似乎位于 kernel\include\asm-generic\vmlinux.lds.h
- 这个段中数据的数据结构:
- kernel_param结构体的定义是:
- 36 struct kernel_param_ops {
- 37 /* Returns 0, or -errno. arg is in kp->arg. */
- 38 int (*set)(const char *val, const struct kernel_param *kp);
- 39 /* Returns length written or -errno. Buffer is 4k (ie. be */
- 40 int (*get)(char *buffer, const struct kernel_param *kp);
- 41 /* Optional function to free kp->arg when module unloaded. */
- 42 void (*free)(void *arg);
- 43 };
- 44
- 45 /* Flag bits for kernel_param.flags */
- 46 #define KPARAM_ISBOOL 2
- 47
- 48 struct kernel_param {
- 49 const char *name;
- 50 const struct kernel_param_ops *ops;
- 51 u16 perm;
- 52 u16 flags;
- 53 union {
- 54 void *arg;
- 55 const struct kparam_string *str;
- 56 const struct kparam_array *arr;
- 57 };
- 58 };
- 59
- 60 /* Special one for strings we want to copy into */
- 61 struct kparam_string {
- 62 unsigned int maxlen;
- 63 char *string;
- 64 };
- 65
- 66 /* Special one for arrays */
- 67 struct kparam_array
- 68 {
- 69 unsigned int max;
- 70 unsigned int elemsize;
- 71 unsigned int *num;
- 72 const struct kernel_param_ops *ops;
- 73 void *elem;
- 74 };
- smp_setup_processor_id();
- 这个函数是针对SMP处理器的,经查阅资料,其作用是获取当前CPU的的硬件ID。
- 如果不是多处理器构架,在其他文件中就不会定义这个函数,此时使用本文件定义的弱引用函数:
- void __init __weak smp_setup_processor_id(void)
- {
- }
- /*
- * 必须尽早运行这个程序, 作用是初始化
- * lockdep 模块的hash表:
- */
- lockdep_init();
- lockdep是一个内核调试模块,用来检查内核互斥机制(尤其是自旋锁)潜在的死锁问题。
- 由于自旋锁以查询方式等待,不释放处理器,比一般互斥机制更容易死锁,故引入lockdep检查以下几种可能的死锁情况:
- 同一个进程递归地加锁同一把锁;
- 一把锁既在中断(或中断下半部)使能的情况下执行过加锁操作, 又在中断(或中断下半部)里执行过加锁操作。这样该锁有可能在锁定时由于中断发生又试图在同一处理器上加锁;
- 加锁后导致依赖图产生成闭环,这是典型的死锁现象。
- debug_objects_early_init();
- 在启动早期初始化hash buckets 和链接静态的 pool objects对象到 poll 列表. 在这个调用完成后 object tracker 已经开始完全运作了.
- /*
- * 初始化栈canary值:
- */
- boot_init_stack_canary();
- cgroup_init_early();
- local_irq_disable();
- early_boot_irqs_disabled = true;
- /*
- * 中断依然被禁用。做必要的设置后,
- * 再使能它
- */
- tick_init();
- 初始化内核时钟系统
- -->clockevents_register_notifier(&tick_notifier)
- 往内核通知链中注册内核时钟时间的通知函数
- 参考资料:《Linux 时钟处理机制》 《Linux 时钟管理》
- boot_cpu_init();
- 激活当前CPU(在内核全局变量中将当前CPU的状态设为激活状态)
- 参考资料:《激活第一个CPU》
- page_address_init();
- printk(KERN_NOTICE "%s", linux_banner);
- 打印内核版本信息,也就是平时我们在内核启动时在串口中看到的:
- Linux version 2.6.37+ (tekkaman@tekkaman-desktop) (gcc version 4.3.3 (Sourcery G++ Lite 2009q1-203) ) #40 Tue Mar 20 17:49:58 CST 2012
- setup_arch(&command_line);
- 内核构架相关初始化函数,可以说是非常重要的一个初始化步骤。其中包含了处理器相关参数的初始化、内核启动参数(tagged list)的获取和前期处理、内存子系统的早期的初始化(bootmem分配器)。
- 对于ARM构架来说,这个函数位于:arch/arm/kernel/setup.c
- 以后会详细分析这个函数。
- mm_init_owner(&init_mm, &init_task);
- mm_init_cpumask(&init_mm);
- 初始化代表内核本身内存使用的管理结构体init_mm。
- ps:每一个任务都有一个mm_struct结构以管理内存空间,init_mm是内核的mm_struct,其中:
- 设置成员变量* mmap指向自己,意味着内核只有一个内存管理结构;
- 设置* pgd=swapper_pg_dir,swapper_pg_dir是内核的页目录(在arm体系结构有16k, 所以init_mm定义了整个kernel的内存空间)。
- 这些内容涉及到内存管理子系统,以后再仔细分析。
- setup_command_line(command_line);
- 对cmdline进行备份和保存:
- /* 为处理的command line备份 (例如eg. 用于 /proc) */
- char *saved_command_line;
- /* 用于参数处理的command line */
- static char *static_command_line;
- setup_nr_cpu_ids();
- setup_per_cpu_areas();
- smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
- 针对SMP处理器的内存初始化函数,如果不是SMP系统则都为空函数。
- 他们的目的是给每个CPU分配内存,并拷贝.data.percpu段的数据。为系统中的每个CPU的per_cpu变量申请空间并为boot CPU设置一些数据。
- 在SMP系统中,在引导过程中使用的CPU称为boot CPU
- build_all_zonelists(NULL);
- page_alloc_init();
- 设置内存管理相关的node(节点,每个CPU一个内存节点)和其中的zone(内存域,包含于节点中,如)数据结构,以完成内存管理子系统的初始化,并设置bootmem分配器。
- page_alloc_init函数暂时不知其目的
- printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);
- parse_early_param();
- parse_args("Booting kernel", static_command_line, __start___param,
- __stop___param - __start___param,
- &unknown_bootoption);
- 打印从内核启动参数中获取的cmdline字符串。
- 解析cmdline中的启动参数。
- /*
- * 使用大量bootmem分配,且必须先于
- * kmem_cache_init()
- */
- 以上注释的含义在于bootmem是内核启动时使用的临时内存分配器。之后由slab接替。
- kmem_cache_init()初始化了内核高速缓存分配器(slab分配器),这个函数标标志着bootmem的终结,同时内核的内存管理系统正式启用了。
- 所以在kmem_cache_init()之后,bootmem的API不再可用,所以bootmem分配必须先于kmem_cache_init()。
- setup_log_buf(0);
- pidhash_init();
- 使用bootmem分配并初始化PID散列表,由PID分配器管理空闲和已指派的PID
- vfs_caches_init_early();
- sort_main_extable();
- 对内核异常表( exception table )按照异常向量号大小进行排序。
- trap_init();
- 对内核陷阱异常进行初始化,在ARM构架中为空函数。
- mm_init();
- 初始化内核内存分配器,其包含6个子函数,作用如下:
- 1、page_cgroup_init_flatmem(); 获取page_cgroup 所需内存
- 2、mem_init(); 关闭并释放bootmem分配器,打印内存信息。在内核启动时看到的类似如下信息,就是其子函数mem_init输出的:
-
- Memory: 86MB 39MB = 125MB total
- Memory: 120768k/120768k available, 99392k reserved, 0K highmem
- Virtual kernel memory layout:
- vector : 0xffff0000 - 0xffff1000 ( 4 kB)
- fixmap : 0xfff00000 - 0xfffe0000 ( 896 kB)
- DMA : 0xffc00000 - 0xffe00000 ( 2 MB)
- vmalloc : 0xde800000 - 0xf8000000 ( 408 MB)
- lowmem : 0xc0000000 - 0xde400000 ( 484 MB)
- pkmap : 0xbfe00000 - 0xc0000000 ( 2 MB)
- modules : 0xbf000000 - 0xbfe00000 ( 14 MB)
- .init : 0xc0008000 - 0xc003b000 ( 204 kB)
- .text : 0xc003b000 - 0xc04c0000 (4628 kB)
- .data : 0xc04c0000 - 0xc0501a80 ( 263 kB)
- 3、kmem_cache_init(); 初始化slab分配器
- 4、percpu_init_late(); PerCPU变量系统后期初始化
- 5、pgtable_cache_init();页表缓存初始化,对于ARM,“介是一个空函数”
- 6、vmalloc_init();初始化虚拟内存分配器
- /*
- * 在开启任何中断(比如定时器中断)前设置调度器。
- * 完整的拓扑设置发生在smp_init()中
- * -但与此同时,我们仍然有一个正常运作的调度器。
- */
- sched_init();
- /*
- * 禁用强占 - 早期启动时的调度是极为脆弱的,
- * 直到cpu_idle()的首次运行。
- */
- preempt_disable();
- if (!irqs_disabled()) {
- printk(KERN_WARNING "start_kernel(): bug: interrupts were "
- "enabled *very* early, fixing it\n");
- local_irq_disable();
- }
- idr_init_cache();
-
- perf_event_init();
-
- CPU性能监视机制初始化
- 此机制包括CPU同一时间执行指令数,cache miss数,分支预测失败次数等性能参数
- rcu_init();
- 内核RCU(Read-Copy Update:读取-复制-更新)机制初始化
- radix_tree_init();
- /* 在init_ISA_irqs()之前初始化一些链接 */
- early_irq_init();
- init_IRQ();
- 硬件中断系统初始化:
- early_irq_init();前期外部中断描述符初始化,主要初始化数据结构。
- init_IRQ;对应构架特定的中断初始化函数,在ARM构架中:
- void __init init_IRQ(void)
- {
- machine_desc->init_irq();
- }
- 也就是运行设备描述结构体中的init_irq函数,此函数一般在板级初始化文件(arch/*/mach-*/board-*.c)中定义。
- prio_tree_init();
- 初始化内核基于radix数的优先级搜索树(PST),主要是对其结构体进行初始化。
- init_timers();
- hrtimers_init();
- softirq_init();
- timekeeping_init();
- time_init();
- 以上几个函数主要是初始化内核的软中断及时钟机制:
- 前面几个函数主要是注册一些内核通知函数到cpu和hotcpu通知链,并开启部分软中断(tasklet等)。
- 最后的time_init是构架相关的,旨在开启一个硬件定时器,开始产生系统时钟。对于ARM构架:
- void __init time_init(void)
- {
- system_timer = machine_desc->timer;
- system_timer->init();
- #ifdef CONFIG_HAVE_SCHED_CLOCK
- sched_clock_postinit();
- #endif
- }
- 其实就是调用板级初始化文件(arch/arm/mach-*/board-*.c)中定义“设备描述结构体”中的timer成员的初始化函数。
- profile_init();
- 初始化内核profile子系统,她是内核的性能调试工具。
- call_function_init();
- 初始化所有CPU的call_single_queue(具体作用还没搞明白),并注册CPU热插拔通知函数到CPU通知链中。
- if (!irqs_disabled())
- printk(KERN_CRIT "start_kernel(): bug: interrupts were "
- "enabled early\n");
- early_boot_irqs_disabled = false;
- local_irq_enable();
- 检测硬件中断是否开启,如果开启了就打印出警告。
- 设置启动早期IRQ使能标志,允许IRQ使能。
- 最后开启总中断(ARM构架是这样,其他构架可能也是这个意思)。
- /* 中断已经开启,因此所有GFP分配是安全的. */
- gfp_allowed_mask = __GFP_BITS_MASK;
- 开启所有GFP分配允许标志
- GFP(get free page)
- kmem_cache_init_late();
- slab分配器的后期初始化。如果使用的是slob或slub,则为空函数。
- /*
- * HACK ALERT! hack警告!这个是早期的。我们在完成PCI设置等工作前
- * 使能控制台,且console_init()必须意识到这个。
- * 但是我们的确想要早点输出信息,以防某些错误的发生。
- */
- console_init();
- 初始化控制台,这样可以早点看到启动信息,避免出错时无法查找原因。
- if (panic_later)
- panic(panic_later, panic_param);
- lockdep_info();
- /*
- * 当irq使能的时候必须运行这个函数,因为它也要自检
- * [hard/soft]-irqs 开/关 锁反转的bug:
- *
- */
- locking_selftest();
- #ifdef CONFIG_BLK_DEV_INITRD
- if (initrd_start && !initrd_below_start_ok &&
- page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
- printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - "
- "disabling it.\n",
- page_to_pfn(virt_to_page((void *)initrd_start)),
- min_low_pfn);
- initrd_start = 0;
- }
- #endif
- 检查initrd的位置是否符合要求。
- min_low_pfn是系统可用的最小的pfn(页帧号)。
- 也就是判断传递进来initrd_start对应的物理地址是否正常。如果有误就打印错误信息,并清零initrd_start。
- page_cgroup_init();
- enable_debug_pagealloc();
- debug_objects_mem_init();
- kmemleak_init();
- setup_per_cpu_pageset();
- 设置每个CPU的页组,并初始化。此前只有启动页组。
- numa_policy_init();
- if (late_time_init)
- late_time_init();
- sched_clock_init();
- calibrate_delay();
- 计算BogoMIPS值,他是衡量一个CPU性能的标志。
- pidmap_init();
- anon_vma_init();
- 匿名虚拟内存域( anonymous VMA)初始化
- #ifdef CONFIG_X86
- if (efi_enabled)
- efi_enter_virtual_mode();
- #endif
- thread_info_cache_init();
- 获取thread_info缓存空间,大部分构架为空函数(包括ARM)
- cred_init();
- 任务信用系统初始化。详见:Documentation/credentials.txt
- fork_init(totalram_pages);
- 进程创建机制初始化。为内核"task_struct"分配空间,计算最大任务数。
- proc_caches_init();
- 初始化进程创建机制所需的其他数据结构,为其申请空间。
- buffer_init();
- 缓存系统初始化,创建缓存头空间,并检查其大小限时。
- key_init();
- security_init();
- dbg_late_init();
- vfs_caches_init(totalram_pages);
- signals_init();
- /* 根文件系统的填充可能需要也回写机制 */
- page_writeback_init();
- #ifdef CONFIG_PROC_FS
- proc_root_init();
- #endif
- cgroup_init();
- cpuset_init();
- taskstats_init_early();
- 任务状态早期初始化函数:为结构体获取高速缓存,并初始化互斥机制。
- delayacct_init();
- check_bugs();
- acpi_early_init(); /* 在 LAPIC 和 SMP 前初始化 */
- ACPI早期初始化函数。
- ACPI - Advanced Configuration and Power Interface高级配置及电源接口
- sfi_init_late();
- SFI 初始程序晚期设置函数,
- SFI - SIMPLE FIRMWARE INTERFACE。
- ftrace_init();
- 功能跟踪调试机制初始化,ftrace 是 function trace 的简称。
- /* 所剩下的非-__init的初始化, 内核现在已经启动了 */
- rest_init();
- 虽然从名字上来说是剩余的初始化。
- 但是这个函数中的初始化包含了很多的内容,后面我回单独写一篇来分析。
- }
在看完上面的代码之后,你会发现内容很多。但是归纳起来,我认为需要注意的有以下几点:
- 内核启动参数的获取和处理
- setup_arch(&command_line);函数
- 内存管理的初始化(从bootmem到slab)
- rest_init();函数
其他的部分都是对内核各个组件的数据结构申请内存,并初始化。
-----------------------------------------------------------
参考资料: