kernel启动分析
kernel启动分析
一、链接脚本
内核的链接脚本是用汇编文件vmlinux.lds.S
实现的。所以必须先进行编译,然后才会生成真正的链接脚本vmlinux.lds
根据链接脚本可以找到程序入口为ENTRY(stext)
。
经过查找,发现入口在arch/arm/kernel
目录下的head.S
。
二、head.S
1.汇编阶段
内核运行的虚拟地址与物理地址
// 内核运行的虚拟地址 0xC0008000
#define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET)
// 内核运行的物理地址 0x30008000
#define KERNEL_RAM_PADDR (PHYS_OFFSET + TEXT_OFFSET)
重要注释
该注释出现在真正的启动代码前。
/*
* Kernel startup entry point.
* ---------------------------
*
* This is normally called from the decompressor code. The requirements
* are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
* r1 = machine nr, r2 = atags pointer.
*
* This code is mostly position independent, so if you link the kernel at
* 0xc0008000, you call this at __pa(0xc0008000).
*
* See linux/arch/arm/tools/mach-types for the complete list of machine
* numbers for r1.
*
* We're trying to keep crap to a minimum; DO NOT add any machine specific
* crap here - that's what the boot loader (or in extreme, well justified
* circumstances, zImage) is for.
*/
这里的代码通常被解压代码调用。zImage是,在vmlinux.o的基础上对文件进行再压缩,然后加上相应的自解压头文件后生成的。所以这里说是被解压代码调用。
这段代码被调用的要求是——关MMU,关D-cache,r0=0
,r1 = machine nr
,r2 = atags pointer
。r1
中存储的就是uboot传参时的机器码,该机器码被存放在linux/arch/arm/tools/mach-types
,可以随时查验。
这段代码几乎全是PIC码,如果想要链接kernel到某地址,可以使用__pa(specific address)
这段代码不应该和任何硬件初始化相关,这些步骤应该在boot loader中完成。
设置CPU工作模式
__HEAD
ENTRY(stext)
setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ ensure svc mode
@ and irqs disabled
提一句,__HEAD
表示#define __HEAD .section ".head.text","ax"
,是链接时需要链接的第一段代码。
汇编阶段
__lookup_processor_type
从CP15协处理器中的C0读取CPU的ID号,调用该函数进行合法性检验。如果合法则继续启动,如果不合法则停止启动,转向__error_p启动失败。
内核会维护一个本内核版本支持的CPU的ID号码数组,然后该函数就会把读取到的ID与数组中的ID作比较。
/*
* Read processor ID register (CP#15, CR0), and look up in the linker-built
* supported processor list. Note that we can't use the absolute addresses
* for the __proc_info lists since we aren't running with the MMU on
* (and therefore, we are not in the correct address space). We have to
* calculate the offset.
* 读取处理器ID,然后在链接器内置的处理器列表中寻找该ID
* r9 = cpuid
* Returns:
* r3, r4, r6 corrupted
* r5 = proc_info pointer in physical address space
* r9 = cpuid (preserved)
*/
__lookup_machine_type
/*
* Lookup machine architecture in the linker-build list of architectures.
* Note that we can't use the absolute addresses for the __arch_info
* lists since we aren't running with the MMU on (and therefore, we are
* not in the correct address space). We have to calculate the offset.
* 在内置的架构列表中,寻找机器架构码
* r1 = machine architecture number
* Returns:
* r3, r4, r6 corrupted
* r5 = mach_info pointer in physical address space
*/
__vet_atags
/*
* Determine validity of the r2 atags pointer. The heuristic requires
* that the pointer be aligned, in the first 16k of physical RAM and
* that the ATAG_CORE marker is first and present. Future revisions
* of this function may be more lenient with the physical address and
* may also be able to move the ATAGS block if necessary.
* 确认atags中tag的有效性
* r8 = machinfo
*
* Returns:
* r2 either valid atags pointer, or zero
* r5, r6 corrupted
*/
__create_page_tables
创建页表是为了启动MMU。该页表为段格式,1M为单位。
内核启动前期使用粗页表,后期就会再次建立细页表,以4KB为单位。
__switch_data -- __mmap_switched
这是一个函数指针数组,直接进入__mmap_switched
函数。
/*
* The following fragment of code is executed with the MMU on in MMU mode,
* and uses absolute addresses; this is not position independent.
* 下面的代码时在MMU模式下操作MMU,是非PIC码
* r0 = cp#15 control register
* r1 = machine ID
* r2 = atags pointer
* r9 = processor ID
*/
该函数还复制了数据段,清除了BSS段
str r9, [r4] @ Save processor ID
str r1, [r5] @ Save machine type
str r2, [r6] @ Save atags pointer
bic r4, r0, #CR_A @ Clear 'A' bit
stmia r7, {r0, r4} @ Save control register values
最终进入start_kernel。也就是进入c语言阶段。
C语言阶段
C语言阶段的函数,目前我还看不懂,所以根据打印的结果来分析。
banner
printk(KERN_NOTICE "%s", linux_banner);
用于打印内核信息。以后几乎所有的消息均用此函数打印。
内核信息的8个级别如下:
#define KERN_EMERG "<0>" /* system is unusable */
#define KERN_ALERT "<1>" /* action must be taken immediately */
#define KERN_CRIT "<2>" /* critical conditions */
#define KERN_ERR "<3>" /* error conditions */
#define KERN_WARNING "<4>" /* warning conditions */
#define KERN_NOTICE "<5>" /* normal but significant condition */
#define KERN_INFO "<6>" /* informational */
#define KERN_DEBUG "<7>" /* debug-level messages */
setup_arch
首先在setup_processor
中打印了CPU相关信息
然后再在setup_mackine
中校验机器码,并输出相关machine的信息。
接下来校验atag地址是否有效
输出解析后的参数地址(就是uboot中的bootargs,被封装进tag中)
parse_tags
解析参数,但是发现一个参数未识别。
输出解析后的参数
参数解析后console=ttySAC2,115200
,root=/dev/mmcblk0p2 rw
,init=/linuxrc
,rootfstype=ext3
。
- machine_desc结构体
static const struct machine_desc __mach_desc_SMDKV210 \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_SMDKV210, \
.name = "SMDKV210",
.phys_io = S3C_PA_UART & 0xfff00000,
.io_pg_offst = (((u32)S3C_VA_UART) >> 18) & 0xfffc,
.boot_params = S5P_PA_SDRAM + 0x100,
.init_irq = s5pv210_init_irq,
.map_io = smdkv210_map_io,
.init_machine = smdkv210_machine_init,
.timer = &s5p_systimer,
};
这个结构体用来存户硬件相关信息,至少包含机器码,名字,传入参数(bootargs)等关键信息。
set_processor
实际上调用了在汇编阶段查找处理架构的__lookup_processor_type
。同理可知setup_machine
在底层肯定也调用了__lookup_machine_type
,这个machine就是机器码。
内核在建立的时候就把各种CPU架构的信息组织成一个一个的machine_desc结构体实例,然后都给一个段属性.arch.info.init,链接的时候会保证这些描述符会被连接在一起。__lookup_machine_type
就去那个那些描述符所在处依次挨个遍历各个描述符,比对看机器码哪个相同。
该结构体中由重要函数smdkv210_machine_init
,该函数负责绑定开发板内核启动过程中会初始化的各种硬件信息。
rest_init
进入该函数,意味着内核进入启动第三阶段。
最终阶段启动
内核启动终止于cpu_idle
。
static noinline void __init_refok rest_init(void)
__releases(kernel_lock)
{
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.
*/
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);
unlock_kernel();
/*
* 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();
}
在第三阶段,OS一共维持了三个内核进程。
进程号 | 作用 |
---|---|
idle | 空闲进程 |
kernel_init | init进程,所有用户进程的父进程 |
kthreadd | 守护进程,保证内核本身的正常工作 |
init进程
init进程完成了OS由内核态到用户态的转变。
内核态下,挂载根文件系统并试图找到用户态下的init程序。init进程要从内核态过渡到用户态,就必须执行一个应用程序,要执行应用程序,就必须挂载根文件系统,因为应用程序都存储在文件系统中。
init进程在内核态下面时,通过一个函数kernel_execve来执行一个用户空间编译连接的应用程序就跳跃到用户态了。注意这个跳跃过程中进程号是没有改变的,所以一直是进程1.这个跳跃过程是单向的,也就是说一旦执行了init程序转到了用户态下整个操作系统就算真正的运转起来了,以后只能在用户态下工作了,用户态下想要进入内核态只有走API这一条路了。
init还构建了login进程,命令行进程与shell进程。
- console
打开console设备的文件描述符,然后将该文件描述符复制两份。init总共得到0,1,2三个文件描述符,分别对应stdin
,stdout
,stderr
。init进程的子进程也将继承这三个文件描述符。
/* 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);
- rootfs
挂载根文件系统。bootargs
中有两个参数描述了fs位置以及类型。
/*
* 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();
}
- init
上面一旦挂载rootfs成功,则进入rootfs中寻找应用程序的init程序,这个程序就是用户空间的进程1,找到后用run_init_process去执行。
/*
* 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);
}
/*
* 按顺序一次寻找init程序
*/
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.");
三、cmdline
物理存储
console=ttySAC2,115200
root=/dev/mmcblk0p2 rw
init=/linuxrc
rootfstype=ext3
第一种这种方式对应rootfs在SD/iNand/Nand/Nor等物理存储器上。
网络存储
root=/dev/nfs
nfsroot=192.168.1.20:/root/s3c2440/build_rootfs/aston_rootfs
ip=192.168.1.99:192.168.1.20:192.168.1.0:255.255.255.0::eth0:off
init=/linuxrc
console=ttySAC2,115200
第二种这种方式对应rootfs在nfs上。
四、mach
mach其实就是machine architecture。arch/arm
下的一个mach-xx
就代表一类以xx
为cpu做主芯片的machine。该目录下的mach-yy.c
则定义了该machine的一种开发板。
plat是platform的缩写。可以理解为SoC。该目录下全是SoC内部外设的初始化。内核种吧SoC中的外部外设操作代码称作平台设备驱动。