内核我爱编程

跟踪分析Linux内核的启动过程

2015-05-04  本文已影响1078人  那只大象

当Power on PC时,BIOS的代码开始执行,然后是Linux初始化的代码,这其中大约很长一段时间Linux都没有进程这一概念,但是这不影响CPU执行它的二进制代码。如果不是多任务以及进程调度的需要,Linux内核可以一直这样走下去

但是因为多任务的需求,Linux必须能支持任务这一特性,任务即进程,或者更简单地说由task_struct对象实例所代表的一段代码的集合,用以完成特定的任务。所以Linux内核初始化过程中必须为进程以及进程调度做准备

追踪操作系统 内核态 的初始化过程,从 init/main.c 中的 start_kernel() 开始



使用gdb跟踪调试内核

qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -S -s

# 关于-S和-s选项的说明:
# -S freeze CPU at startup (use ’c’ to start execution)
# -s shorthand for -gdb tcp::1234
# 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项

使用gdb跟踪调试内核

另开一个shell窗口

gdb
  (gdb) file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加载符号表
  (gdb) target remote:1234 # 建立gdb和gdbserver之间的连接,按c让qemu上的Linux继续运行
  (gdb) break start_kernel # 断点的设置可以在target remote之前,也可以在之后

另开一个shell窗口

详细分析从start_kernel到init进程启动的过程

start_kernel()      /linux-3.18.6/init/main.c(500行)

start_kernel()是内核的汇编与C语言的交接点,在该函数以前,内核的代码都是用汇编写的,完成一些最基本的初始化与环境设置工作,比如内核代码载入内存并解压缩(现在的内核一般都经过压缩),CPU的最基本初始化,为C代码的运行设置环境(C代码的运行是有一定环境要求的,比如stack的设置等,具体见 C语言函数调用堆栈框架 )

start_kernel()

全局变量init_task,即手工创建的(0号进程的)PCB,0号进程即最终的idle进程

全局变量init_task

init_task进程在Linux中属于一个比较特殊的进程,它是内核开发者人为制造出来的,而不是其他进程通过do_fork来完成,init_task进程的内核栈通过静态方式分配

所有的模块在初始化的时候都是通过调用 start_kernel() 进行初始化,例如中断模块(trap_init)、内存管理模块(mm_init)、调度模块(sched_init)等等,研究特定的内核的模块,都需要了解 main.c 中的 start_kernel(),不管分析内核的哪一部分都会涉及到 start_kernel()


trap_init()      /linux-3.18.6/arch/x86/kernel/traps.c(792行)

涉及一些中断,初始化一些中断向量

trap_init()

set_intr_gate,设置了很多中断门

set_intr_gate,设置了很多中断门

set_system_trap_gate,设置系统陷阱门,系统调用

set_system_trap_gate,设置系统陷阱门,系统调用

分析中断的时候也主要是分析系统调用,因为硬件中断不好模拟,而系统调用也是一种中断,和中断的机制是一样的,它只是用指令的方式来触发一个中断

Linux在无进程概念的情况下将一直从初始化部分的代码执行到start_kernel(),在start_kernel()中Linux将完成整个系统的内核初始化。内核初始化的最后一步就是调用rest_init(),启动init进程这个所有进程的祖先

rest_init():Linux内核初始化的尾声

从rest_init开始,Linux开始产生进程,因为init_task是静态制造出来的,pid=0,它试图将从最早的汇编代码一直到start_kernel的执行都纳入到init_task进程上下文中。在rest_init函数中,内核将通过下面的代码产生第一个真正的进程(pid=1):

rest_init():Linux内核初始化的尾声

kernel_thread():创建一个内核线程,实际上就是内核进程,Linux内核是不支持类似Windows NT一样的线程概念的。Linux本质上只支持进程。这里的kernel_init只是一个函数

kernel_init():会通过调用do_execve来执行根文件系统下的/sbin/init文件(所以此前根文件系统必须已经就绪),do_execve对用户空间程序/sbin/init的调用发起自int $0x80,这是个从内核空间发起的系统调用

kernel_init() 运行init程序 run_init_process()

run_init_process():实际上是通过嵌入汇编构建一个类似用户态代码一样的do_execve()调用,其参数就是要执行的可执行文件名,也就是这里的init进程在磁盘上的文件

这里的run_init_process就是通过execve()来运行init程序。这里首先运行“/sbin/init”,如果失败再运行“/etc/init”,然后是 “/bin/init”,然后是“/bin/sh”(也就是说,init可执行文件可以放在上面代码中寻找的4个目录中都可以),如果都失败,则可以通过在系统启动时再添加的启动参数来指定init,比如init=/home/rootfs/init。这里是内核初始化结束并开始用户态初始化的阴阳界

init进程是Linux系统的第一个用户态进程,为1号进程,没有父进程,由Linux内核直接启动

接下来还创建了一个kthreadd内核线程,来管理系统的资源

创建了一个kthreadd内核线程

此时init_task的任务基本上已经完全结束了,它将沦落为一个idle task,事实上在更早前的sched_init()函数中,通过init_idle(current, smp_processor_id())函数的调用就已经把init_task初始化成了一个idle task,init_idle函数的第一个参数current就是&init_task,在init_idle中将会把init_task加入到cpu的运行队列中,这样当运行队列中没有别的就绪进程时,init_task(也就是idle task)将会被调用,它的核心是一个while(1)循环,在循环中它将会调用schedule函数以便在运行队列中有新进程加入时切换到该新进程上

Summary

内核启动过程包括start_kernel之前和之后,之前全部是做初始化的汇编指令(硬件平台相关),之后开始C代码的操作系统初始化(硬件平台无关),最后执行第一个用户态进程init

结合中国传统文化的角度看,道生一(start_kernel....cpu_idle),一生二(kernel_init和kthreadd),二生三(即前面0、1和2三个进程),三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先)

(完)


上一篇下一篇

猜你喜欢

热点阅读