Linux内核启动过程
Bootloader完成系统初始化工作后,将运行控制权交给Linux内核。根据内核是否压缩以及内核是否在本地执行,Linux通常有以下两种可选的启动方式:
1)对于ARM 系列处理器来说,zImage 的入口程序即为 arch/arm/boot/
compressed/head.S。它依次完成以下工作:开启 MMU 和 Cache,调用 decompress_kernel()解压内核,最后通过调用 call_kernel()进入非压缩内核 Image 的启动。
Linux 非压缩内核的入口位于文件/arch/arm/kernel/head-armv.S 中的 stext 段。该段的基地址就是压缩内核解压后的跳转地址。如果系统中加载的内核是非压缩的 Image,那么bootloader将内核从 Flash中拷贝到 RAM 后将直接跳到该地址处,从而启动 Linux 内核。
2)执行镜像:解压後/非压缩镜像直接执行(linux/arch/arm/kernel/head-armv.S:ENTRY(stext)-> __entry->__ret->__switch_data->__mmap_switched->)
3)该程序通过查找处理器内核类型和处理器类型调用相应的初始化函数,再建立页表,最后跳转到 start_kernel()函数开始内核的初始化工作。(linux/init/main.c:start_kernel())
http://wenku.baidu.com/view/a8db73323968011ca30091ef.html
第一阶段:
在head-common.S
1) 配置系统寄存器;
2) 初始化ROM、RAM以及总线控制寄存器等;
3) 设置堆栈指针,将bss段清零;
4) 修改pc指针,跳转到./init/main.c中的start_kernel函数,开始Linux系统的初始化。
zImage的启动过程
1)内核启动地址的确定
1、#/arch/arm/Makefile文件中,设置内核启动的虚拟地址
#Default value
head-y :=arch/arm/kernel/head$(MMUEXT).o arch/arm/kernel/init_task.o
textofs-y :=$(CONFIG_TEXT_OFFSET)
textofs-$(CONFIG_ARCH_CLPS711X) :=0x00028000
2)#/arch/arm/boot/Makefile文件中,设置内核启动的物理地址 。
# Note: the following conditions mustalways be true:
# ZRELADDR == virt_to_phys(PAGE_OFFSET + TEXT_OFFSET)
# PARAMS_PHYS must be within 4MB of ZRELADDR
# INITRD_PHYS must be in RAM
ZRELADDR := $(zreladdr-y)
PARAMS_PHYS := $(params_phys-y)
INITRD_PHYS := $(initrd_phys-y)
3、 #/arch/arm/boot/compressed/Makefile文件中,
SEDFLAGS =s/TEXT_START/$(ZTEXTADDR)/;s/BSS_START/$(ZBSSADDR)/
使得TEXT_START = ZTEXTADDR(从flash中启动时),LOAD_ADDR = ZRELADDR
其中TEXT_START是内核ram启动的偏移地址,这个地址是物理地址
ZTEXTADDR就是解压缩代码的ram偏移地址,
LOAD_ADDR就是zImage中解压缩代码的ram偏移地址,ZRELADDR是内核ram启动的偏移地址
zImage的入口点由# /arch/arm/boot/compressed/vmlinux.lds.in决定:
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
/DISCARD/ : {
*(.ARM.exidx*)
*(.ARM.extab*)
/*
* Discard any r/w data - this produces a link error if we have any,
* which is required for PIC decompression. Local data generates
* GOTOFF relocations, which prevents it being relocated independently
* of the text/got segments.
*/
*(.data)
}
. = TEXT_START;
_text = .;
… …
}
2)内核解压缩过程
内核压缩和解压缩代码都在目录#/arch/arm/boot/compressed,编译完成后将产生vmlinux、head.o、misc.o、head-xscale.o、piggy.o这几个文件,其中head.o:内核的头部文件,负责初始设置;
misc.o:主要负责内核的解压工作,它在head.o之后;
head-xscale.o:主要针对Xscale的初始化,将在链接时与head.o合并;piggy.o:一个中间文件,其实是一个压缩的内核(kernel/vmlinux),只不过没有和
初始化文件及解压文件链接而已;
vmlinux:没有(zImage是压缩过的内核)压缩过的内核,就是由piggy.o、
head.o、misc.o、head-xscale.o组成的。
3) 在BootLoader完成系统的引导以后并将Linux 内核调入内存之后,调用
bootLinux(),这个函数将跳转到kernel的起始位置。如果kernel没有压缩,就可以启动了。
如果kernel压缩过,则要进行解压,在压缩过的kernel头部有解压程序。压缩过的kernel入口第一个文件源码位置arch/arm/boot/compressed/head.S。它将调用函数decompress_kernel(),这个函数在arch/arm/boot/compressed/ misc.c 中,decompress_kernel()又调用proc_decomp_setup(),arch_decomp_ setup()进行设置,然后使用在打印出信息“Uncompressing Linux...”后,调用gunzip()。将内核放于指定的位置。
4) 以下分析#/arch/arm/boot/compressed/head.S文件:(1) 对于各种Arm CPU的DEBUG输出设定,通过定义宏来统一操作。(2) 设置kernel开始和结束地址,保存architecture ID。
(3) 如果在ARM2以上的CPU中,用的是普通用户模式,则升到超级用户模式,
然后关中断。
(4) 分析LC0结构delta offset,判断是否需要重载内核地址(r0存入偏移量,判断
r0是否为零)。
接下来要把内核镜像的相对地址转化为内存的物理地址,即重载内核地址:
(5) 需要重载内核地址,将r0的偏移量加到BSS region和GOT table中。(6) 清空bss堆栈空间r2-r3。
(7) 建立C程序运行需要的缓存,并赋于64K的栈空间。
(8) 这时r2是缓存的结束地址,r4是kernel的最后执行地址,r5是kernel境象文件的开始地址。检查是否地址有冲突。将r5等于r2,使decompress后的kernel地址就在64K的栈之后。
9) 调用文件misc.c的函数decompress_kernel(),解压内核于缓存结束的地方(r2地址之后)。此时各寄存器值有如下变化: r0为解压后kernel的大小 r4为kernel执行时的地址 r5为解压后kernel的起始地址 r6为CPU类型值(processor ID) r7为系统类型值(architecture ID)
(10) 将reloc_start代码拷贝之kernel之后(r5+r0之后),首先清除缓存,而后执行reloc_start。
(11) reloc_start将r5开始的kernel重载于r4地址处。
(12) 清除cache内容,关闭cache,将r7中architecture ID赋于r1,执行r4开始的kernel代码。
5)我们在内核启动的开始都会看到这样的输出Uncompressing Linux...done, booting the kernel.
这也是由decompress_kernel函数内部输出的,它调用了putc()输出字符串,putc是在#/include/asm-arm/arch-pxa/uncompress.h中实现的。执行完解压过程,再返回到#/arch/arm/boot/compressed/head.S中,启动内核:
/*
* The C runtime environment should now be setup sufficiently.
* Set up some pointers, and start decompressing.
* r4 = kernel execution address
* r7 = architecture ID
* r8 = atags pointer
*/
mov r0, r4
mov r1, sp @ malloc space above stack
add r2, sp, #0x10000 @ 64k max
mov r3, r7
bl decompress_kernel
bl cache_clean_flush
bl cache_off
mov r0, #0 @ must be zero
mov r1, r7 @ restore architecture number
mov r2, r8 @ restore atags pointer
mov pc, r4 @ call kernel
6) 执行zImage镜像,到start_kernel( )
整个arm linux内核的启动可分为三个阶段:第一阶段主要是进行cpu和体系结构的检查、cpu本身的初始化以及页表的建立等;第一阶段的初始化是从内核入口(ENTRY(stext))开始到start_kernel前结束。这一阶段的代码在/arch/arm/kernel/head.S中。/arch/arm/kernel/head.S用汇编代码完成,是内核最先执行的一个文件。这一段汇编代码的主要作用,是检查cpu id,architecture number,初始化页表、cpu、bbs等操作,并跳到start_kernel函数。它在执行前,处理器的状态应满足:
r0 - should be 0
r1 - unique architecture number
MMU - off
I-cache - on or off
D-cache – off
第二阶段:
程序跳转到main.c文件中的start_kernel中,在这里完成处理器结构的初始化、中断的初始化、进程相关的初始化以及内存初始化等重要工作。
1.1将命令行拷贝到cmdline中
1.2调用open函数打开串口
1.3解压缩kernel代码
1.4解压缩ramdisk image
1.5最终初始化设备树
1.6跳到内核代码中执行
/*
* Set up kernel memory allocators
*/
static void __init mm_init(void)
{
/*
* page_cgroup requires countinous pages as memmap
* and it's bigger than MAX_ORDER unless SPARSEMEM.
*/
page_cgroup_init_flatmem();
mem_init();
kmem_cache_init();
percpu_init_late();
pgtable_cache_init();
vmalloc_init();
}
asmlinkage void __init start_kernel(void)
{
char * command_line;
extern const struct kernel_param __start___param[], __stop___param[];
#ifdef CONFIG_NKERNEL
jiffies_64 = INITIAL_JIFFIES;
#endif
smp_setup_processor_id();
/*
* Need to run as early as possible, to initialize the
* lockdep hash:
*/
lockdep_init();
debug_objects_early_init();
/*
* Set up the the initial canary ASAP:
*/
boot_init_stack_canary();
cgroup_init_early();
local_irq_disable();
early_boot_irqs_disabled = true;
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
tick_init();
boot_cpu_init();
page_address_init();
printk(KERN_NOTICE "%s", linux_banner);
/* setup_arch函数位于./arch/arm/kernel/setup.c中,它完成特定于体系结构的设置,包括初始化硬件寄存器、标识根设备和系统中可用的DRAM和Flash的大小、指定系统中可用页面的数目、文件系统大小等等。所有这些信息都以参数形式从引导装载程序传递到内核。setup_arch 还需要对Flash存储库、系统寄存器和其它特定设备执行内存映射。一旦完成了特定于体系结构的设置,控制就返回到初始化系统其余部分的 start_kernel 函数。*/
setup_arch(&command_line);
mm_init_owner(&init_mm, &init_task);
mm_init_cpumask(&init_mm);
setup_command_line(command_line);
setup_nr_cpu_ids();
setup_per_cpu_areas();
smp_prepare_boot_cpu();
build_all_zonelists(NULL);
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);
/*
* These use large bootmem allocations and must precede
* kmem_cache_init()
*/
setup_log_buf(0);
pidhash_init();
vfs_caches_init_early();
sort_main_extable();
trap_init();
/* trap_init函数在./arch/arm/kernel/traps.c文件中。它对中断向量表进行初始化,初始化处理器的一些中断处理。它通过调用一些宏对中断向量表填写中断响应程序的偏移地址。中断要等到calibrate_delay()函数运行之前才允许被调用。*/
mm_init();
/*
* Set up the scheduler prior starting any interrupts (such as the
* timer interrupt). Full topology setup happens at smp_init()
* time - but meanwhile we still have a functioning scheduler.
*/
/* sched_init函数的源程序在./arch/arm/kernel/sched.c中。该函数初始化内核pidhash[]表(这是一个快速映射进程ID到进程描述符的查询表)*/
sched_init();
/*
* Disable preemption - early bootup scheduling is extremely
* fragile until we cpu_idle() for the first time.
*/
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();
rcu_init();
radix_tree_init();
/* init some links before init_ISA_irqs() */
early_irq_init();
init_IRQ();
/* trap_init函数在./arch/arm/kernel/traps.c文件中。它对中断向量表进行初始化,初始化处理器的一些中断处理。它通过调用一些宏对中断向量表填写中断响应程序的偏移地址。中断要等到calibrate_delay()函数运行之前才允许被调用。*/
prio_tree_init();
init_timers();
hrtimers_init();
softirq_init();
timekeeping_init();
time_init();
/* sched_init函数的源程序在./arch/arm/kernel/sched.c中。该函数初始化内核pidhash[]表(这是一个快速映射进程ID到进程描述符的查询表)*/
profile_init();
call_function_init();
if (!irqs_disabled())
printk(KERN_CRIT "start_kernel(): bug: interrupts were "
"enabled early\n");
early_boot_irqs_disabled = false;
local_irq_enable();
/* Interrupts are enabled now so all GFP allocations are safe. */
gfp_allowed_mask = __GFP_BITS_MASK;
/* kmem_cache_init函数的源程序在./mm/slab.c中。该函数初始化内核SLAB内存管理子系统。SLAB是内核内部结构的动态内存管理。*/
kmem_cache_init_late();
/*
* HACK ALERT! This is early. We're enabling the console before
* we've done PCI setups etc, and console_init() must be aware of
* this. But we do want output early, in case something goes wrong.
*/
console_init();
if (panic_later)
panic(panic_later, panic_param);
lockdep_info();
/*
* Need to run this when irqs are enabled, because it wants
* to self-test [hard/soft]-irqs on/off lock inversion bugs
* too:
*/
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
page_cgroup_init();
enable_debug_pagealloc();
debug_objects_mem_init();
kmemleak_init();
setup_per_cpu_pageset();
numa_policy_init();
if (late_time_init)
late_time_init();
sched_clock_init();
/* calibrate_delay函数的源程序在./init/main.c中。该函数执行内核的BogoMips(校准Linux内部循环延时,使得延时的时间和不同速率的处理器都保持在一个大概相同的比率上)运算*/
calibrate_delay();
pidmap_init();
anon_vma_init();
#ifdef CONFIG_X86
if (efi_enabled)
efi_enter_virtual_mode();
#endif
thread_info_cache_init();
cred_init();
/* fork_init(mempages)函数的源程序在./kernel/fork.c中。该函数初始化max_threads和init_task变量。这些变量会在调用fork()函数时用到。*/
fork_init(totalram_pages);
/* fork_init(mempages)函数的源程序在./kernel/fork.c中。该函数初始化max_threads和init_task变量。这些变量会在调用fork()函数时用到。*/
proc_caches_init();
buffer_init();
key_init();
security_init();
dbg_late_init();
vfs_caches_init(totalram_pages);
signals_init();
/* rootfs populating might need page-writeback */
page_writeback_init();
/* proc_root_init函数的创建proc文件系统,源程序在./fs/proc/root.c中。该函数初始化proc文件系统,创建几个类似/proc/bus和/porc/driver的标准目录。*/
#ifdef CONFIG_PROC_FS
proc_root_init();
#endif
cgroup_init();
cpuset_init();
taskstats_init_early();
delayacct_init();
check_bugs();
acpi_early_init(); /* before LAPIC and SMP init */
sfi_init_late();
ftrace_init();
/* rest_init函数的源程序在./init/main.c中。该函数释放初始化函数占用的内存,调用init()函数创建kernel_thread结束内核启动进程,初始化内核PCI和网络,加载操作系统。*/
rest_init();
}
将会进入
static noinline void __init_refok rest_init(void)
{
int pid;
rcu_scheduler_starting();
/*
* We need to spawn init first so that it obtains pid 1, however
* the init task will end up wanting to create kthreads, which, if
* we schedule it before we create kthreadd, will OOPS.
*/
/* 创建内核线程,入口点是 init()函数 */
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
numa_default_policy();
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
rcu_read_lock();
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
rcu_read_unlock();
complete(&kthreadd_done);
/*
* The boot idle thread must execute schedule()
* at least once to get things moving:
*/
init_idle_bootup_task(current);
preempt_enable_no_resched();
schedule();
preempt_disable();
/* Call into cpu_idle with preempt disabled */
cpu_idle();/* 禁止抢占,转入空闲 */
}
函数创建了一个入口点是 init()函数的内核线程,然后调用 cpu_idle()函数进入空闲状态。新
创建的内核线程是系统的 1 号任务(pid =1),放入了调度队列中,而原先的初始化代码是系
统的 0 号任务,是不在调度队列中的。因此 1 号任务投入运行,系统转而执行 init()函数。
这个函数同样定义在 init/main.c 中:
static int __init kernel_init(void * unused)
{
/*
* Wait until kthreadd is all set-up.
*/
wait_for_completion(&kthreadd_done);
/*
* init can allocate pages on any node
*/
set_mems_allowed(node_states[N_HIGH_MEMORY]);
/*
* init can run on any cpu.
*/
set_cpus_allowed_ptr(current, cpu_all_mask);
cad_pid = task_pid(current);
smp_prepare_cpus(setup_max_cpus);
do_pre_smp_initcalls();
lockup_detector_init();
smp_init();
sched_init_smp();
do_basic_setup();
/* Open the /dev/console on the rootfs, this should never fail */
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console.\n");
(void) sys_dup(0);
(void) sys_dup(0);
/*
* check if there is an early userspace init. If yes, let it do all
* the work
*/
if (!ramdisk_execute_command)
ramdisk_execute_command = "/init";
if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
}
/*
* Ok, we have completed the initial bootup, and
* we're essentially up and running. Get rid of the
* initmem segments and start the user-mode stuff..
*/
init_post();
return 0;
}
init()函数接着完成系统更高层次,比如驱动程序,根文件系统等等的初始化工作。其中的
do_basic_setup()函数比较重要,这个函数先调用 driver_init()函数完成驱动程序的初始化,又
通过 do_initcalls()函数依次调用了系统中所有的初始化函数。do_initcalls()函数的主要源代码
如下:
static void __init do_initcalls(void)
{
initcall_t *fn;
/* 循环调用_initcall_start和__initcall_end之间的所有函数指针指向的初始化函数 */
for (fn = __early_initcall_end; fn < __initcall_end; fn++)
do_one_initcall(*fn); /* 调用 */
}
static noinline int init_post(void)
{
/* need to finish all async __init code before freeing the memory */
async_synchronize_full();
free_initmem();/* 释放初始化内存 */
mark_rodata_ro();
system_state = SYSTEM_RUNNING;
numa_default_policy();
current->signal->flags |= SIGNAL_UNKILLABLE;
/* 尝试执行 ramdisk_execute_command 指定的程序 */
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
printk(KERN_WARNING "Failed to execute %s\n",
ramdisk_execute_command);
}
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of init if we are
* trying to recover a really broken machine.
*/
if (execute_command) {
run_init_process(execute_command);
printk(KERN_WARNING "Failed to execute %s. Attempting "
"defaults...\n", execute_command);
}
/* 依次尝试执行四个外部程序 */
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel. "
"See Linux Documentation/init.txt for guidance.");
}
函数打开了控制台设备/dev/console,并复制了两个 handle,这样 stdin,stdout,stderr 都指向
/dev/console设备。然后,函数依次尝试执行以下几个外部程序:
由ramdisk_execute_command 指定的外部程序,即内核启动参数“rdinit=XXX”指定的
程序
由execute_command 指定的外部程序,即内核启动参数“init=XXX”指定的程序
/sbin/init
/etc/init
/bin/init
/bin/sh
这几个程序中任何一个加载执行成功,就进入了用户态,内核启动就宣告结束。
1 号任务原先是个内核线程,加载外部程序后就有了自己的用户态空间,成为一个进程,这
就是系统中所有进程的祖先 1 号进程。然后 1 号进程再执行用户态的初始化程序,例如处理
/etc/inittab,创建终端,等待用户登录等等,系统启动完成。