ARM Linux 启动过程分析
本文档基于 AT91SAM9260EK 板的,所用的 Linux 内核版本为 2.6.21
1 压缩与非压缩内核映象
非压缩内核映象是真正的 Linux 内核代码。压缩内核映象是把非压缩内核映象作为数据进行
压缩打包,并加上了解压缩代码。也就是说,它是一个自解压的可执行映象。压缩内核映象
执行时,先解压内部包含的数据块(即非压缩内核映象),再去执行非压缩内核映象。
非压缩内核映象由 make Image 命令产生。其生成过程是:
(1) 内核的各个模块经过编译,链接,在内核源代码的顶层目录下生成 vmlinux 文件,这是
一个 ELF格式的映象
(2) 用 arm-linux-objcopy命令把 vmlinux 转换为二进制格式映象 arch/arm/boot/Image
压缩内核映象由 make zImage 命令产生,其生成过程是:
(1) 用 gzip 对非压缩内核二进制映象 arch/arm/boot/ Image 进行压缩,生成
arch/arm/boot/compressed/piggy.gz文件
(2) arch/arm/boot/compressed/目录下有三个文件:piggy.s,定义了一个包含./piggy.gz 文件的
数据段;head.S包含了对gzip 压缩过的内核进行解压的代码;vmlinux-lds 是链接脚本。
这几个文件经过编译链接,在 arch/arm/boot/compressed/目录下产生 vmlinux文件,这是
一个 ELF格式的映象
(3) 用 arm-linux-objcopy 命令把 arch/arm/boot/compressed/vmlinux 转换为二进制格式映象:
arch/arm/boot/compressed/zImage
两种内核映象的产生过程如下图: ARM Linux 启动过程分析
第 2 页 共 19 页
arch/arm/boot/Image
非压缩内核,BIN格式
$(TOPDIR)/vmlinux
非压缩内核,ELF格式
objcopy
arch/arm/boot/compressed/piggy.gz
压缩内核,gzip格式
gzip
arch/arm/boot/compressed/piggy.s
汇编源文件,只定义了一个包
含./piggy.gz的数据段
arch/arm/boot/compressed/head.S
汇编源文件,包含对gzip压缩内核进
行解压的代码
arch/arm/boot/compressed/vmlinux-lds
压缩内核连接脚本
arch/arm/boot/compressed/
vmlinux
具备自解压功能的压缩内核
ELF格式
arch/arm/boot/zImage
具备自解压功能的压缩内核
BIN格式
编译连接
objcopy
2 内核入口
Linux 内核编译连接后生成的 ELF 映像文件是 vmlinux,从内核源代码顶层目录下的
Makefile(即顶层 Makefile)中可以找到 vmlinux 的生成规则:
vmlinux: $(vmlinux-lds) $(vmlinux-init) $(vmlinux-main) $(kallsyms.o) FORCE
其中$(vmlinux-lds)是编译连接脚本,对于 ARM 平台,就是 arch/arm/kernel/vmlinux-lds文件。
vmlinux-init也在顶层 Makefile 中定义:
vmlinux-init := $(head-y) $(init-y)
head-y 在 arch/arm/Makefile 中定义:
head-y:= arch/arm/kernel/head$(MMUEXT).o arch/arm/kernel/init_task.o
…
ifeq ($(CONFIG_MMU),)
MMUEXT := -nommu
endif
对于有 MMU的处理器,MMUEXT 为空白字符串,所以 arch/arm/kernel/head.O是第一个连
接的文件,而这个文件是由 arch/arm/kernel/head.S编译产生成的。 ARM Linux 启动过程分析
第 3 页 共 19 页
综合以上分析,可以得出结论,非压缩 ARM Linux 内核的入口点在 arch/arm/kernel/head.s
中。
3 汇编语言启动代码
先来看 arch/arm/kernel/head.S 的源代码(有精简):
[arch/arm/kernel/head.S]
/* 定义内核的起始物理地址和起始虚拟地址 */
#define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET)
#define KERNEL_RAM_PADDR (PHYS_OFFSET + TEXT_OFFSET)
/* 定义初始页目录表的虚拟地址 swapper_pg_dir */
#if (KERNEL_RAM_VADDR & 0xffff) != 0x8000
#error KERNEL_RAM_VADDR must start at 0xXXXX8000
#endif
.globl swapper_pg_dir
.equ swapper_pg_dir, KERNEL_RAM_VADDR - 0x4000
/* 宏定义:把初始页面目录表的物理地址装入 rd */
.macro pgtbl, rd
ldr \rd, =(KERNEL_RAM_PADDR - 0x4000)
.endm
/* 内核启动入口点
需要满足的条件(由 Bootloader 设置):
SVC 模式,关中断,MMU = off, D-cache = off, R0=0,R1=机器类型代码
*/
__INIT
.type stext, %function
ENTRY(stext)
/* 设置处理器为 SVC 模式,关中断 */
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE
/* CPUID => r9; 调用__lookup_processor_type 查找处理器信息结构,
proc_info 指针 => r10
*/
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cupid
movs r10, r5 @ invalid processor (r5=0)? ARM Linux 启动过程分析
第 4 页 共 19 页
beq __error_p @ yes, error 'p'
/* 调用__lookup_machine_type 查找机器类型信息结构,
machine_desc 指针 => r8 */
bl __lookup_machine_type @ r5=machinfo
movs r8, r5 @ invalid machine (r5=0)?
beq __error_a @ yes, error 'a'
/* 调用__create_page_tables 为内核自身创建页表 */
bl __create_page_tables
/* 调用__enable_mmu 函数后的返回地址 => r13 */
ldr r13, __switch_data @ address to jump to after mmu has been enabled
/* __enable_mmu 函数的地址 => lr */
adr lr, __enable_mmu @ return (PIC) address
/* 转移到 proc_info 结构中定义的处理器底层初始化函数,然后通过该函数的最后
一条指令“mov pc,lr”跳转到__enalbe_mmu 函数
*/
add pc, r10, #PROCINFO_INITFUNC
/*
* Setup common bits before finally enabling the MMU. Essentially
* this is just loading the page table pointer and domain access
* registers.
*/
/* __enable_mmu 函数: 打开 MMU */
.type __enable_mmu, %function
__enable_mmu:
#ifdef CONFIG_ALIGNMENT_TRAP
orr r0, r0, #CR_A
#else
bic r0, r0, #CR_A
#endif
#ifdef CONFIG_CPU_DCACHE_DISABLE
bic r0, r0, #CR_C
#endif
#ifdef CONFIG_CPU_BPREDICT_DISABLE
bic r0, r0, #CR_Z
#endif
#ifdef CONFIG_CPU_ICACHE_DISABLE
bic r0, r0, #CR_I
#endif ARM Linux 启动过程分析
第 5 页 共 19 页
mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
domain_val(DOMAIN_IO, DOMAIN_CLIENT))
mcr p15, 0, r5, c3, c0, 0 @ load domain access register
mcr p15, 0, r4, c2, c0, 0 @ load page table pointer
b __turn_mmu_on
.align 5
.type __turn_mmu_on, %function
__turn_mmu_on:
mov r0, r0
mcr p15, 0, r0, c1, c0, 0 @ write control reg
mrc p15, 0, r3, c0, c0, 0 @ read id reg
mov r3, r3
mov r3, r3
mov pc, r13
/* __create_page_tables 函数: 为内核创建页表 */
/* r8 = machinfo r9 = cupid r10 = procinfo
Returns: r0, r3, r6, r7 corrupted r4 = physical page table address
*/
.type __create_page_tables, %function
__create_page_tables:
pgtbl r4 @ page table address
/* 清零页目录表 */
mov r0, r4
mov r3, #0
add r6, r0, #0x4000
1: str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
/* 创建映射:VA= PA=物理内存起始地址,长度=1MB */
mov r6, pc, lsr #20 @ start of kernel section
orr r3, r7, r6, lsl #20 @ flags + kernel base
str r3, [r4, r6, lsl #2] @ identity mapping ARM Linux 启动过程分析
第 6 页 共 19 页
/* 为内核的代码段和数据段创建映射:
VA=0xC0008000 PA=物理内存起始地址+0x8000
长度=内核代码和数据的长度向上取整到 1MB的整数倍
*/
add r0, r4, #(KERNEL_START & 0xff000000) >> 18
str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
ldr r6, =(KERNEL_END - 1)
add r0, r0, #4
add r6, r4, r6, lsr #18
1: cmp r0, r6
add r3, r3, #1 << 20
strls r3, [r0], #4
bls 1b
/* 为物理内存的第 1 个 1MB 段(包含了 Bootloader 穿过来的启动参数)创建映射:
VA=0xC0000000 PA=物理内存起始地址 长度=1MB
*/
add r0, r4, #PAGE_OFFSET >> 18
orr r6, r7, #(PHYS_OFFSET & 0xff000000)
orr r6, r6, #(PHYS_OFFSET & 0x00e00000)
str r6, [r0]
/* 如果定义了 CONFIG_DEBUG_LL,为 IO 设备创建内存映射,
以便在页表系统初始化之前可以使用串行控制台
*/
#ifdef CONFIG_DEBUG_LL
ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags
ldr r3, [r8, #MACHINFO_PGOFFIO]
add r0, r4, r3
rsb r3, r3, #0x4000 @ PTRS_PER_PGD*sizeof(long)
cmp r3, #0x0800 @ limit to 512MB
movhi r3, #0x0800
add r6, r0, r3
ldr r3, [r8, #MACHINFO_PHYSIO]
orr r3, r3, r7
1: str r3, [r0], #4
add r3, r3, #1 << 20
teq r0, r6
bne 1b
#endif
mov pc, lr
.ltorg
ARM Linux 启动过程分析
第 7 页 共 19 页
#include "head-common.S"
文件的最后包含了 arch/arm/kernel/head-common.S,这个文件里定义了__mmap_switched,
__lookup_processor_type 和, __lookup_machine_type 等几个函数,精简后的源代码如下:
[arch/arm/kernel/head-common.S]
/* __switch_data 结构 */
.type __switch_data, %object
__switch_data:
.long __mmap_switched
.long __data_loc @ r4
.long __data_start @ r5
.long __bss_start @ r6
.long _end @ r7
.long processor_id @ r4
.long __machine_arch_type @ r5
.long cr_alignment @ r6
.long init_thread_union + THREAD_START_SP @ sp
/* __mmap_switched 函数:重定位数据段,清零 BSS 数据段,调转到 C 语言入口函数
start_kernel()
*/
.type __mmap_switched, %function
__mmap_switched:
adr r3, __switch_data + 4
ldmia r3!, {r4, r5, r6, r7}
cmp r4, r5 @ Copy data segment if needed
1: cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b
mov fp, #0 @ Clear BSS (and zero fp)
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
ldmia r3, {r4, r5, r6, sp}
str r9, [r4] @ Save processor ID
str r1, [r5] @ Save machine type
bic r4, r0, #CR_A @ Clear 'A' bit ARM Linux 启动过程分析
第 8 页 共 19 页
stmia r6, {r0, r4} @ Save control register values
b start_kernel
/* __error_p,__error_a 函数:异常处理,省略 */
.type __error_p, %function
__error_p:
…
.type __error_a, %function
__error_a:
…
/* __lookup_processor_type 函数:根据 CPUID 查找处理器信息结构 */
.type __lookup_processor_type, %function
__lookup_processor_type:
adr r3, 3f /* 标号 3 的物理地址 => r3 */
/* 标号 3 的虚拟地址 => r7
__proc_info_begin的虚拟地址 => r5 __proc_info_end 的虚拟地址 => r6
*/
ldmda r3, {r5 - r7}
sub r3, r3, r7 /* 物理地址和虚拟地址之间的偏移量=> r3 */
add r5, r5, r3 /* __proc_info_begin的物理地址 => r5 */
add r6, r6, r3 /* __proc_info_end 的物理地址 => r6 */
1: ldmia r5, {r3, r4} @ value, mask
and r4, r4, r9 @ mask wanted bits
teq r3, r4
beq 2f
add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list)
cmp r5, r6
blo 1b
mov r5, #0 @ unknown processor
2: mov pc, lr
ENTRY(lookup_processor_type)
stmfd sp!, {r4 - r7, r9, lr}
mov r9, r0
bl __lookup_processor_type
mov r0, r5
ldmfd sp!, {r4 - r7, r9, pc}
.long __proc_info_begin ARM Linux 启动过程分析
第 9 页 共 19 页
.long __proc_info_end
3: .long .
.long __arch_info_begin
.long __arch_info_end
/* __lookup_machine_type 函数:查找机器类型信息块 */
.type __lookup_machine_type, %function
__lookup_machine_type:
adr r3, 3b
ldmia r3, {r4, r5, r6}
sub r3, r3, r4 @ get offset between virt&phys
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type
teq r3, r1 @ matches loader number?
beq 2f @ found
add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc
cmp r5, r6
blo 1b
mov r5, #0 @ unknown machine
2: mov pc, lr
ENTRY(lookup_machine_type)
stmfd sp!, {r4 - r6, lr}
mov r1, r0
bl __lookup_machine_type
mov r0, r5
ldmfd sp!, {r4 - r6, pc}
通过对以上源代码的分析,总结出以下一些结论:
1) 重要常量
PAGE_OFFSET:内核空间起点,0xC0000000(3GB)
TEXT_OFFSET:内核代码段偏移量, 0x8000(32KB)
PHYS_OFFSET:物理内存起始地址,对于 AT91SAM9260EK,为 0x20000000
KERNEL_RAM_VADDR,内核起始虚拟地址 = 0xC0008000 (3GB+32KB)
KERNEL_RAM_PADDR,内核起始物理地址 = 0x20008000
从代码中还可以看出,Linux 内核的起始虚拟地址的低 16 位必须为 0x8000,
初始页目录表 swapper_pg_dir 的虚拟地址在 0xC0004000(3GB+16KB)。
2) 内核启动的执行流程 ARM Linux 启动过程分析
第 10 页 共 19 页
z 设置处理器为 SVC 模式,关中断
z 读取 CPUID,调用__lookup_processor_type 查找处理器信息结构 proc_info
z 调用__lookup_machine_type 查找机器类型信息结构 machine_desc
z 调用__create_page_tables 函数为内核创建页面映射表
z 调用处理器底层初始化函数,初始化 MMU,Cache,TLB
z 跳转到__enalbe_mmu 函数,打开 MMU
z 跳转到__mmap_switched 函数,建立 C 语言运行环境(搬移数据段,清理 BSS 段),保
存 CPUID 和机器类型代码,最后跳转到 C 语言入口函数 start_kernel()
3) 处理器信息结构和机器类型信息结构的查找
所有 CPU的信息结构都放在名为“.proc.info.init”的段中,连接后形成一个完整的段,段的
起始地址为__proc_info_begin,结束地址为__proc_info_end。按关键字在__proc_info_begin
和 __proc_info_end 之间进行搜索就可以找到指定 CPU 的信息结构。CPU 信息结构的第一
个数据成员就是关键字(CPUID)。搜索过程中要使用物理地址,因为此时 MMU还没有打开,
所以代码中有一个把__proc_info_begin 和__proc_info_end 从虚拟地址转换为物理地址的过
程,处理得十分巧妙,详情可以参见源代码及其注释。
机器类型信息结构的查找也采用类似的方法。所有的机器类型信息结构都放在
“.arch.info.init”段中,段的起始地址为__arch_info_begin,结束地址为__arch_info_end。搜
索的关键字是机器类型代码,也保存在机器类型信息结构 machine_desc 的第一个数据成员
中。
4) 存储映射
对于 AT91SAM9260EK,内核启动过程中通过__create_page_tables函数创建了以下两个地址
区间的映射:
序号 起始虚拟地址 起始物理地址 长度
1 0x20000000 0x20000000 1MB
2 0xC0000000 0x20000000 内核总长度向上调整到 1MB 的整数倍
其中第一个地址区间的映射是为了实现打开 MMU前后地址空间的平稳过渡。
5) 处理器底层初始化函数
代码中通过以下指令转移到处理器底层初始化函数:
add pc, r10, #PROCINFO_INITFUNC
此时 r10为 CPU信息结构的指针,常量 PROCINFO_INITFUNC 定义为 16,程序调转到 CPU
信息结构块偏移 16 字节的地址,这里存放的是一条转向 CPU底层初始化函数的跳转指令。
对于 AT91SAM9260EK,CPU 信息结构和底层初始化函数都在 arch/arm/mm/proc-arm926.S
中: ARM Linux 启动过程分析
第 11 页 共 19 页
[arch/arm/mm/proc-arm926.S]
/* 处理器底层初始化函数:__arm926_setup */
.type __arm926_setup, #function
__arm926_setup:
mov r0, #0
mcr p15, 0, r0, c7, c7 @ invalidate I,D caches on v4
mcr p15, 0, r0, c7, c10, 4 @ drain write buffer on v4
#ifdef CONFIG_MMU
mcr p15, 0, r0, c8, c7 @ invalidate I,D TLBs on v4
#endif
#ifdef CONFIG_CPU_DCACHE_WRITETHROUGH
mov r0, #4 @ disable write-back on caches explicitly
mcr p15, 7, r0, c15, c0, 0
#endif
adr r5, arm926_crval
ldmia r5, {r5, r6}
mrc p15, 0, r0, c1, c0 @ get control register v4
bic r0, r0, r5
orr r0, r0, r6
#ifdef CONFIG_CPU_CACHE_ROUND_ROBIN
orr r0, r0, #0x4000 @ .1.. .... .... ....
#endif
mov pc, lr
.size __arm926_setup, . - __arm926_setup
…
/* 处理器信息结构 */
.align
.section ".proc.info.init", #alloc, #execinstr
.type __arm926_proc_info,#object
__arm926_proc_info:
.long 0x41069260 @ ARM926EJ-S (v5TEJ) /* CPUID */
.long 0xff0ffff0
.long PMD_TYPE_SECT | \
PMD_SECT_BUFFERABLE | \
PMD_SECT_CACHEABLE | \
PMD_BIT4 | \
PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ
.long PMD_TYPE_SECT | \
PMD_BIT4 | \
PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ ARM Linux 启动过程分析
第 12 页 共 19 页
b __arm926_setup /* 跳转指令,转向底层初始化函数 */
.long cpu_arch_name
.long cpu_elf_name
.long
HWCAP_SWP|HWCAP_HALF|HWCAP_THUMB|HWCAP_FAST_MULT|HWCAP_EDS
P|HWCAP_JAVA
.long cpu_arm926_name
.long arm926_processor_functions
.long v4wbi_tlb_fns
.long v4wb_user_fns
.long arm926_cache_fns
.size __arm926_proc_info, . - __arm926_proc_info
可以看到,处理器底层初始化函数主要完成 MMU、Cache 和 TLB 的初始化,Write Buffer
回写策略的设置以及控制寄存器的读取等工作。ARM Linux 启动过程分析
第 13 页 共 19 页
4 start_kernel()
start_kernel()函数是内核初始化 C 语言部分的主体。这个函数完成系统底层基本机制,包
括处理器、存储管理系统、进程管理系统、中断机制、定时机制等的初始化工作。由于这个
函数过于复杂,这里仅列出源代码,留待以后再按模块逐步深入分析。
[init/main.c: start_kernel()]
asmlinkage void __init start_kernel(void)
{
char * command_line;
extern struct kernel_param __start___param[], __stop___param[];
smp_setup_processor_id();
/* Need to run as early as possible, to initialize the lockdep hash */
unwind_init();
lockdep_init();
local_irq_disable();
early_boot_irqs_off();
early_init_irq_lock_class();
/* Interrupts are still disabled. Do necessary setups, then enable them */
lock_kernel();
tick_init();
boot_cpu_init();
page_address_init();
printk(KERN_NOTICE);
printk(linux_banner);
setup_arch(&command_line); /* 架构相关的初始化,重点关注! */
setup_command_line(command_line);
unwind_setup();
setup_per_cpu_areas();
smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
/*
* 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(); /* 调度器初始化 */
/* ARM Linux 启动过程分析
第 14 页 共 19 页
* Disable preemption - early bootup scheduling is extremely
* fragile until we cpu_idle() for the first time.
*/
preempt_disable();
build_all_zonelists();
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);
if (!irqs_disabled()) {
printk(KERN_WARNING "start_kernel(): bug: interrupts were "
"enabled *very* early, fixing it\n");
local_irq_disable();
}
sort_main_extable();
trap_init(); /* 中断机制初始化 */
rcu_init();
init_IRQ(); /* 中断机制初始化 */
pidhash_init(); /* PID Hash 机制初始化 */
init_timers(); /* 定时器初始化 */
hrtimers_init();
softirq_init(); /* 软中断初始化 */
timekeeping_init();
time_init();
profile_init();
if (!irqs_disabled())
printk("start_kernel(): bug: interrupts were enabled early\n");
early_boot_irqs_on();
local_irq_enable(); /* 打开中断 */
/*
* 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 ARM Linux 启动过程分析
第 15 页 共 19 页
* 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 &&
initrd_start < min_low_pfn << PAGE_SHIFT) {
printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - "
"disabling it.\n",initrd_start,min_low_pfn << PAGE_SHIFT);
initrd_start = 0;
}
#endif
vfs_caches_init_early();
cpuset_init_early();
mem_init();
kmem_cache_init();
setup_per_cpu_pageset();
numa_policy_init();
if (late_time_init)
late_time_init();
calibrate_delay();
pidmap_init();
pgtable_cache_init();
prio_tree_init();
anon_vma_init();
#ifdef CONFIG_X86
if (efi_enabled)
efi_enter_virtual_mode();
#endif
fork_init(num_physpages);
proc_caches_init();
buffer_init();
unnamed_dev_init();
key_init();
security_init();
vfs_caches_init(num_physpages);
radix_tree_init();
signals_init();
/* rootfs populating might need page-writeback */
page_writeback_init();
#ifdef CONFIG_PROC_FS
proc_root_init();
#endif ARM Linux 启动过程分析
第 16 页 共 19 页
cpuset_init();
taskstats_init_early();
delayacct_init();
check_bugs();
acpi_early_init(); /* before LAPIC and SMP init */
/* Do the rest non-__init'ed, we're now alive */
rest_init();
}
函数完成基本的初始化工作后,最后调用了 rest_init()函数,这个函数的源代码如下:
[init/main.c:start_kernel()->rest_init()]
static void noinline rest_init(void) __releases(kernel_lock)
{
/* 创建内核线程,入口点是 init()函数 */
kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);
numa_default_policy();
unlock_kernel();
preempt_enable_no_resched();
schedule();
preempt_disable();
cpu_idle(); /* 禁止抢占,转入空闲 */
}
函数创建了一个入口点是 init()函数的内核线程,然后调用 cpu_idle()函数进入空闲状态。新
创建的内核线程是系统的 1 号任务(pid =1),放入了调度队列中,而原先的初始化代码是系
统的 0 号任务,是不在调度队列中的。因此 1 号任务投入运行,系统转而执行 init()函数。
这个函数同样定义在 init/main.c 中:
[init/main.c:init()]
static int __init init(void * unused)
{
lock_kernel();
set_cpus_allowed(current, CPU_MASK_ALL);
init_pid_ns.child_reaper = current;
cad_pid = task_pid(current);
smp_prepare_cpus(max_cpus); ARM Linux 启动过程分析
第 17 页 共 19 页
do_pre_smp_initcalls();
smp_init();
sched_init_smp();
cpuset_init_smp();
do_basic_setup(); /* 重要函数,关注!*/
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();
}
init_post();
return 0;
}
init()函数接着完成系统更高层次,比如驱动程序,根文件系统等等的初始化工作。其中的
do_basic_setup()函数比较重要,这个函数先调用 driver_init()函数完成驱动程序的初始化,又
通过 do_initcalls()函数依次调用了系统中所有的初始化函数。do_initcalls()函数的主要源代码
如下:
[init/main.c:init()->do_basic_setup()->do_initcalls()]
static void __init do_initcalls(void)
{
initcall_t *call; int count = preempt_count();
/* 循环调用_initcall_start和__initcall_end之间的所有函数指针指向的初始化函数 */
for (call = __initcall_start; call < __initcall_end; call++) {
…
result = (*call)(); /* 调用 */
…
}
…
}
符号__initcall_start和__initcall_end 在链接脚本中 arch/arm/kernel/vmlinux.lds定义:
__initcall_start = .;
*(.initcall0.init) *(.initcall0s.init) *(.initcall1.init) *(.initcall1s.init) *(.initcall2.init)
*(.initcall2s.init) *(.initcall3.init) *(.initcall3s.init) *(.initcall4.init) *(.initcall4s.init)
*(.initcall5.init) *(.initcall5s.init) *(.initcallrootfs.init) *(.initcall6.init) *(.initcall6s.init) ARM Linux 启动过程分析
第 18 页 共 19 页
*(.initcall7.init) *(.initcall7s.init)
__initcall_end = .;
这两个符号之间包含了放在.initcallX.init,initcallXs.init(X=0~7)section中的函数。这将按
照顺序调用放置在这些 section 中的函数。
根据 include/linux/init.h 中的宏定义,可以整理出各种 initcall宏和 section的对应关系:
宏定义 Section
pure_initcall .initcall0.init
core_initcall .initcall1.init
core_initcall_sync .initcall1s.init
postcore_initcall .initcall2.init
postcore_initcall_sync .initcall2s.init
arch_initcall .initcall3.init
arch_initcall_sync .initcall3s.init
subsys_initcall .initcall4.init
subsys_initcall_sync .initcall4s.init
fs_initcall .initcall5.init
fs_initcall_sync .initcall5s.init
device_initcall .initcall6.init
device_initcall_sync .initcall6s.init
late_initcall .initcall7.init
late_initcall_sync .initcall7s.init
在 init()函数的最后,调用了 init_post()函数,源代码如下:
[init/main.c:init()->init_post()]
static int noinline init_post(void)
{
free_initmem(); /* 释放初始化内存 */
unlock_kernel();
mark_rodata_ro();
system_state = SYSTEM_RUNNING;
numa_default_policy();
/* 打开控制台设备 handle=0 => stdin */
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console.\n");
/* 复制控制台设备到 handle 1,2 => stdout,stderr */
(void) sys_dup(0);
(void) sys_dup(0);
/* 尝试执行 ramdisk_execute_command 指定的程序 */ ARM Linux 启动过程分析
第 19 页 共 19 页
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
printk(KERN_WARNING "Failed to execute %s\n",
ramdisk_execute_command);
}
/* 尝试执行 execute_command 指定的程序 */
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.");
}
函数打开了控制台设备/dev/console,并复制了两个 handle,这样 stdin,stdout,stderr 都指向
/dev/console设备。然后,函数依次尝试执行以下几个外部程序:
z 由 ramdisk_execute_command 指定的外部程序,即内核启动参数“rdinit=XXX”指定的
程序
z 由 execute_command 指定的外部程序,即内核启动参数“init=XXX”指定的程序
z /sbin/init
z /etc/init
z /bin/init
z /bin/sh
这几个程序中任何一个加载执行成功,就进入了用户态,内核启动就宣告结束。
1 号任务原先是个内核线程,加载外部程序后就有了自己的用户态空间,成为一个进程,这
就是系统中所有进程的祖先 1 号进程。然后 1 号进程再执行用户态的初始化程序,例如处理
/etc/inittab,创建终端,等待用户登录等等,系统启动完成。