x86 CPU启动的第一个动作CS:EIP=FFFF:0000H(换算为物理地址为000FFFF0H,因为16位CPU有20根地址线),即BIOS程序的位置。
BIOS例行程序检测完硬件并完成相应的初始化之后就会寻找可引导介质,找到后把引导程序加载到指定内存区域后,就把控制权交给了引导程序。这里一般是把硬盘的第一个扇区MBR和活动分区的引导程序加载到内存(即加载BootLoader),加载完整后把控制权交给BootLoader。
引导程序BootLoader开始负责操作系统初始化,然后起动操作系统。启动操作系统时一般会指定kernel、initrd和root所在的分区和目录,比如root (hd0,0),kernel (hd0,0)/bzImage root=/dev/ram init=/bin/ash,initrd (hd0,0)/myinitrd4M.img
内核启动过程包括start_kernel之前和之后,之前全部是做初始化的汇编指令,之后开始C代码的操作系统初始化,最后执行第一个用户态进程init。
而这次的实验我们重点来说下init这个进程。
BIOS例行程序检测完硬件并完成相应的初始化之后就会寻找可引导介质,找到后把引导程序加载到指定内存区域后,就把控制权交给了引导程序。这里一般是把硬盘的第一个扇区MBR和活动分区的引导程序加载到内存(即加载BootLoader),加载完整后把控制权交给BootLoader。
引导程序BootLoader开始负责操作系统初始化,然后起动操作系统。启动操作系统时一般会指定kernel、initrd和root所在的分区和目录,比如root (hd0,0),kernel (hd0,0)/bzImage root=/dev/ram init=/bin/ash,initrd (hd0,0)/myinitrd4M.img
内核启动过程包括start_kernel之前和之后,之前全部是做初始化的汇编指令,之后开始C代码的操作系统初始化,最后执行第一个用户态进程init。
而这次的实验我们重点来说下init这个进程。
首先我们先来加载运行一个简易的内核系统,本次的实验都是基于这个简易内核的,
接下来便是通过gdb来跟踪调试内核,我试着用了两个断点来跟踪内核运行,第一步是在内核启动时冻结cpu
第二步就是使用gdb来进行跟踪,通过(gdb)target remote:1234,建立起gdb与gdbserver之间的连接,再按c让qemu上的Linux继续运行,
接下来我在start_kernel和rest_init处分别设置了一个断点来跟踪内核的运行状态:
以上便是我实验做的内容。
接下来我们要分析的是从start_kernel到init进程启动的具体过程
首先我们可以看到这个函数里面有个全局变量init_task,这个init_task即手工创建的PCB,是0号进程,也是最终的idle进程。接着读程序,接下来就是对一些模块的初始化,读到rest_init时,我们可以看到
从rest_init开始,Linux开始产生进程。从上面这个函数我们可以看出,kernel_thread这个函数通过变量kernel_init和kthreadd又生成了1号和2号进程,由于之前通过init_task已经创建了0号进程,而在rest_init函数中,内核将通过下面的代码产生第一个真正的进程(pid=1):
<init/main.c>
static noinline void __init_refok rest_init(void)
{
...
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
...
cpu_idle();
}
<init/main.c>
static noinline void __init_refok rest_init(void)
{
...
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
...
cpu_idle();
}
kernel_init函数可以通过调用kernel_execve来执行根文件系统下的/sbin/init文件。而此时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函数以便在运行队列中有新进程加入时切换到该新进程上。
这正好符合了道生一(start_kernel....cpu_idle),一生二(kernel_init和kthreadd),二生三(即前面0、1和2三个进程),三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先),新内核的核心代码已经优化的相当干净,都符合中国传统文化精神了。
实验总结:
对于整个实验,我们可以清晰的看到内核是怎么创建进程的,这跟道家的道生一(start_kernel....cpu_idle),一生二(kernel_init和kthreadd),二生三(即前面0、1和2三个进程),三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先)比较贴切,先是有start_kernel这个函数通过init_task创建了0号进程,再由函数kernel_thread通过变量kernel_init和kthreadd又生成了1号和2号进程,而1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先,继而可以创建更多的进程了。
作者:叶涛
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000