此前的博文均是以uboot为分析的目标,而此篇则是对内核的代码进行分析,抓大放小,分析linux系统的启动过程。由于牛油自己能力有限,欢迎大家交流指正!
分析的开始:从链接脚本开始找到程序的入口:
链接脚本:vmlinux.lds:内核提供一个arch/arm/kernel/vmlinux.lds.S文件,然后在编译时再去动态生成vmlinux.lds链接脚本。这样动态加载的原因:将vmlinux.lds.S这个条件编译的文件经过一个预处理,最终得到一个不用条件编译的vmlinux.lds文件来指导编译过程的进行。
通过对链接脚本的分析得知内核文件的入口程序:
ENTRY(stext)
追踪stext得知这个入口程序就在head.s汇编文件中。
__HEAD
ENTRY(stext)
。。。。。。。。。
下面对head.S文件进行分析,分析启动的汇编阶段的原理:
1.定义了运行的虚拟地址和物理地址:
#define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET)
#define KERNEL_RAM_PADDR (PHYS_OFFSET + TEXT_OFFSET)
进过分析得知PAGE_OFFSET = 0xc0000000 TEXT_OFFSET=0x00008000。
则虚拟地址为:0xc0008000;物理地址为:0x30008000。 这也验证了uboot中传参与内核中设置的位置相同
2.内核的真正入口:
ENTRY(stext)
3.校验CPU的id号,机器码,内核传参:
校验校验CPU的ID号:
mrc p15, 0, r9, c0, c0 @ get processor id
调用__lookup_processor_type()函数检验cpu的合法性,如果合法则继续启动:
bl __lookup_processor_type
在内核中会维护一份支持的CPU的ID号,这里获取的ID号和维护的那份挨个进行比对查找,从而来保证内核启动的安全性。
校验机器码的真确性,合法则继续启动:
bl __lookup_machine_type @ r5=machinfo
校验uboot通过tag给内核传参格式是否正确:
bl __vet_atags
4.页表的建立:
bl __create_page_tables
这里尽快的建立页表,进入虚拟地址进行操作。
而kernel建立页表实际分为两步:前期kernel先建立一个以1MB为单位的段式页表(在这里进行建立),4G内存需要4096个页表,每个页表4字节,页表会占用16KB的内存;后期内核启动之后再次建立一个更细的页表。
5.跳转执行一个类似于函数指针数组内的函数:
ldr r13, __switch_data @ address to jump to after
__switch_data:
.long __mmap_switched
.long __data_loc @ r4
.long _data @ r5
.long __bss_start @ r6
.long _end @ r7
.long processor_id @ r4
.long __machine_arch_type @ r5
.long __atags_pointer @ r6
.long cr_alignment @ r7
.long init_thread_union + THREAD_START_SP @ sp
这个数组内先将前面校验的传参进行了保存。
其中最重要的是跳转至 __mmap_switched 函数指针指向的地址执行一些很重要的操作:
大致可总结为:拷贝代码段,清除BSS段,为C语言的执行提供软件环境:
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
保存传递的参数:
ARM( ldmia r3, {r4, r5, r6, r7, sp})
THUMB( ldmia r3, {r4, r5, r6, r7} )
THUMB( ldr sp, [r3, #16] )
str r9, [r4] @ Save processor ID
str r1, [r5] @ Save machine type
str r2, [r6] @ Save atags pointer
跳转执行C语言部分的启动代码:
b start_kernel
此后进入C语言的部分进行系统的启动。索引得知这个函数在Main.c中。
C语言阶段的分析:Main.c中的start_kernel()函数:
1.一些比较杂的初始化:
(1)多核性的对称多处理器的初始化:
smp_setup_processor_id();
(2)内核的调试模块,与内核的自旋锁,死锁有关:
lockdep_init();
(3)处理进程组的技术:
cgroup_init_early();
2.内核信息的打印:
printk(KERN_NOTICE "%s", linux_banner);
printk是内核中负责从console打印信息的函数,内核编程时不能使用标准库函数,与printf的功能基本相同,只不过printk可以选定消息输出的级别(共计0~7八个级别,控制台的消息显示级别为4)。
linux_banner经过分析为:
const char linux_banner[] ="Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@"LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "\n";
而linux_banner中的很多信息的是在编译之后从一些生成的文件中动态加载过来的
3.确定当前内核运行的机器:
setup_arch(&command_line);
这个函数内记录着很多的硬件信息,打印出CPU的信息,将机器码进行比对。
而传入的那个参数就是uboot给kernel传入的启动参数bootagrs。而关于这个参数condline的设置,内核中自己设置了一份基本的comdilne,然后在启动时uboot如果不进行传参则会使用这份基本的comdline,否则会使用传参的comdline。
4.很多的内核工作模块的初始化部分:
(1)处理和命令行相关的东西:
setup_command_line(command_line);
(2)解析cmdline传参和其他的传参:
page_alloc_init();
(3)设置异常向量表:
trap_init()
(4)内存管理模块初始化 :
mm_init()
(5)内核调度系统的初始化:
sched_init();
(6)中断初始化:
early_irq_init&init_IRQ
(7)控制台初始化:
console_init
5.函数中最后一个也是很重要的一个函数rest_init():
/* Do the rest non-__init'ed, we're now alive */
rest_init();
函数中启动了2个内核线程:kernel_init和kthreadd:
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
numa_default_policy();
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
调用schedule()函数开启内核的调度系统,至此linux系统开始运行:
schedule();
最后linux系统进入死循环,也是一个空闲进程:
cpu_idle();
void cpu_idle(void){
local_fiq_enable();
wile(1){
......
}
}
这个死循环内如果需要可以进行其他进程的调度。
到这里,linux系统的三个进程0,1,2就开启不停的运行,他们分别是:
进程0:idle空闲进程;
进程1:kernel_init()函数,常被称为init进程;
进程2:kthreadd()函数,常被称为守护进程。
到这里Main.c中的start_kernel()函数执行完毕,内核成功启动,下面将接着对进程1这个init重要的进程做详细的分析:
进程1:init进程的功能介绍:
1.完成由内核态到用户态的转换:内核刚开始运行时是内核态,然后通过运行用户态下面的程序将自己强行转换成用户态,此后后续的其他进程都可以工作在用户态下。跳跃的实现是通过一个函数kernel_execve()完成的。从此只能工作在用户态下,要使用内核态只能使用API函数。
2.内核态下的工作:挂载根文件系统;试图找到用户态下的那个init()函数完成内核态到用户态的转换,而其他的所有用户进程都派生自init进程。(内核态下的进程:内核源代码中的所有函数的执行都属于内核态下的任务)
3.用户态下的进程:应用程序,程序不属于内核源码,脱离了内核态,这就需要根文件系统,之后的程序都运行在用户态下。
4.构建了用户交互界面:启动了login进程,使得用户可以进行登录;启动了命令行进程(shell进程),命令行进程又启动了其他用户进程。
总结:挂载了根文件系统,找到了第一个用户空间下的应用程序(init进程),并执行了这个应用程序,从此进入了用户态下的空间。
kernel_init()函数的分析:
1.打开控制台设备,构建文件描述符:
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);
复制后就得到了三个文件描述符:0 1 2 标准输入、输出、错误文件描述符。
2.挂载根文件系统:
prepare_namespace();
通过prepare_namespace()函数挂载根文件系统。
而挂载前uboot已经将需要的参数传递给了kernel,例如:
const char *cmdline= "console=ttySAC0,115200 init=/linuxrc root = /dev/nfs nfsroot=192.168.1.163:work/nfs_root/ws rootfstypes=ext3";
规定了控制台信息打印的串口是串口0 规定了串口0的波特率;
规定了init进程是执行的是根文件系统下/linuxrc程序;
规定了根文件系统存放的位置(这里使用的是nhs进行根文件系统的挂载)。
3.根文件系统挂载的重启机制:
init_post();
若在传参的地址无法找到根文件系统的位置,内核会调用init_post()函数在其他的文件内尝试找到根文件系统进而启动。
4.挂载完根文件系统后运行用户态的init进程:
(1)用户态的init进程一般会在cmdline中指定,通常是根文件系统下的/linux可执行文件;
(2)如果cmdline中位指定,则在init_post()函数中有其他的预设方案:
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
会接着去找正四个路径对应的可执行文件进而执行。
(3)如果都不成功,内核也不能正常的启动。
End。。。。。。