本周的博客依旧依托于一个实验来展开,即跟踪调试linux内核启动的过程,着重分析一下从start_kernel函数开始到init进程开始执行的过程。如有理解不到位地方,望批评指正。
实验环境依旧采用实验楼,http://www.shiyanlou.com/courses/195 。
首先,用下面的命令来冰冻系统启动,进而可以开始一步一步跟踪调试代码,看看每一步发生了什么。并如下图。
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
接下来,再打开一个shell,用以下命令配置一下gdb,就可以开始调试了。
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之前,也可以在之后
在start_kernel处设置断点,然后开始continue。
来到第一个函数:lockdep_init()
,这个函数官方给的文档是,尽早执行这个函数并且只初始化一次,所以它作为内核启动函数的第一个调用函数,那么,到底它是干嘛的呢?观其代码,原来是初始化一张hash表,这张hash表又是什么样的存在呢?
{
int i;
return;
for (i = 0; i < CLASSHASH_SIZE; i++)
for (i = 0; i < CHAINHASH_SIZE; i++)
}
这张hash表表示的是一个全局的锁,锁链表,这里做一下初始化。不细究。
void set_task_stack_end_magic(struct task_struct *tsk)
{
unsigned long *stackend;
stackend = end_of_stack(tsk);
*stackend = STACK_END_MAGIC;
}
这是将一个0号进程的栈底处,将一个魔数写入,由其注释可以知道,为了防止overflow,这个魔数是
#define STACK_END_MAGIC 0x57AC6E9D
继续next,下面着重看一些我能看得懂的函数吧,其他的就略过了。。
smp_setup_processor_id();//针对SMP处理器,如果不是,则是弱引用函数
debug_objects_early_init();//初始化debug kernel相关
cgroup_init_early()
这个函数,主要是初始化cgroup中的数据结构,cgroup是一组进程的行为控制。
trap_init();这个函数主要是初始化内核的一些中断向量,截取其中部分代码:
set_intr_gate(X86_TRAP_DE, divide_error);
set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK);
set_system_intr_gate(X86_TRAP_OF, &overflow);
set_intr_gate(X86_TRAP_BR, bounds);
set_intr_gate(X86_TRAP_UD, invalid_op);
set_intr_gate(X86_TRAP_NM, device_not_available);
#ifdef CONFIG_X86_32
set_task_gate(X86_TRAP_DF, GDT_ENTRY_DOUBLEFAULT_TSS);
#else
set_intr_gate_ist(X86_TRAP_DF, &double_fault, DOUBLEFAULT_STACK);
#endif
set_intr_gate(X86_TRAP_OLD_MF, coprocessor_segment_overrun);
set_intr_gate(X86_TRAP_TS, invalid_TSS);
set_intr_gate(X86_TRAP_NP, segment_not_present);
set_intr_gate(X86_TRAP_SS, stack_segment);
set_intr_gate(X86_TRAP_GP, general_protection);
set_intr_gate(X86_TRAP_SPURIOUS, spurious_interrupt_bug);
set_intr_gate(X86_TRAP_MF, coprocessor_error);
set_intr_gate(X86_TRAP_AC, alignment_check);
mm_init()
这是一个内核内存管理的初始化函数,其中,这个在main.c同文件中的mm_init()函数中包括的mem_init();就初始化了内存中的信息。
sched_init(),这个函数初始化进程调度器,并初始化调度队列。
紧接着sched_init()是preempt_disable();是禁用抢占和中断,在内核启动的早期,调度是极其脆弱的。
在跟踪到console_init()时候,qemu中会打印如下图所示,也就是初始化控制台。
接下来还有 key_init(),内核密钥管理系统初始化;security_init();内核安全框架初始化等等。接下来重点分析一下rest_init(),这个函数主要为了创建第一个init进程。
static noinline void __init_refok rest_init(void)
{
int pid;
rcu_scheduler_starting();
kernel_thread(kernel_init, NULL, CLONE_FS);
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);
init_idle_bootup_task(current);
schedule_preempt_disabled();
cpu_startup_entry(CPUHP_ONLINE);
}
看一下kernel_thread(kernel_init,
NULL, CLONE_FS);
这个kernel_init是个函数指针,kernel_thread
启动一个内核态线程,
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
return do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
(unsigned long)arg, NULL, NULL);
}
可以看到这里这个kernel_thread
dofork了一个新的进程,再跟踪到这个函数指针fn中,也就是这个kernel_init中,step into
到这个kernel_init里面,可以观察到这么一段代码:
if (!try_to_run_init_process("/sbin/init")
||
!try_to_run_init_process("/etc/init")
||
!try_to_run_init_process("/bin/init")
||
!try_to_run_init_process("/bin/sh"))
return 0;
try_to_run_init_process就是通过execve()来运行init程序。这里首先运行“/sbin/init”,如果失败再运行“/etc/init”,然后是“/bin/init”,然后是“/bin/sh”(也就是说,init可执行文件可以放在上面代码中寻找的4个目录中都可以),如果都失败,则可以通过在系统启动时在添加的启动参数来指定init,比如init="/home/init"自己定义。
在这个过程中,可以看到产生了两个进程,即init用户态进程以及kthreadd进程,kthread的pid为2。init的pid为1。下图为qemu启动之后的menu小系统的样纸。
总结:
从start_kernel开始到结束的过程就是整个内核初始化的过程,在初始化之前,那个后来沦为idle进程的也就是内核静态创建的一个pcb,也就是0号进程,通过rest_init进一步fork出了两个进程,一个是init进程,另外一个是kthread进程。当用户态进程启动之后,也就是没idle进程什么事了~