《Linux内核分析》(三)——跟踪分析Linux内核的启动过程
2015-08-16 21:29
671 查看
作者:Sandy 原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”
实验环境:c+Linux64位 (32位系统可能结果会不同)
依照学术诚信条款,我保证此回答为本人原创,所有回答中引用的外部材料已经做了出处标记。
实验环境:ubuntu14.04操作系统,x86体系结构
实验要求:使用gdb跟踪调试内核从start_kernel到init进程启动
第一步,Linux内核代码结构
本课程提供了一个Linux的内核源码,其结构如下图:
内核地址地址:http://codelab.shiyanlou.com/xref/linux-3.18.6/
不同的文件夹代表了内核的不同模块,其含义是:
arch/ 是体系结构相关的代码,其中的/x86 文件夹下的内容是x86体系结构相关代码,是内核分析的重要分析目标。
init/ 是内核启动相关的代码,是本文的重点分析对象。/init/main.c 文件是内核启动的起点,是分析内核启动流程的首要分析对象。
fs/ 文件系统(file system?)
kernel/ 内核相关的代码,一些内核中使用到的结构体、函数等重要对象的定义都在这里面。
mm/ 内存管理的相关代码(memory managment?)
第二步,Linux内核启动过程分析
/init/main.c 文件中的start_kernel()函数是一切的起点,在这个函数被调用之前都是系统的初始化工作(汇编语言),所以对内核的启动分析一般都从这个函数开始;main.c中没有main函数,start_kernel()这个函数就相当于是c程序中的main函数。下面从这个函数开始对内核的启动流程进行分析。
由于写博文时没有随时保存的习惯,再加上手贱的原因,所以这是重新写的博文,因为时间的原因实验截图就不放上了,直接写一点自己的理会吧
start_kernel()函数的原型是:
这个函数在执行的过程中初始化、定义了内核中一些十分重要的内容,其执行过程几乎涉及到了内核的所有模块;首先,是init_task
init_task的定义在/linux-3.18.6/init/init_task.c
它其实就是一个task_struct,与用户进程的task_struct一样, task_struct中保存了一个进程的所有基本信息,如进程状态,栈起始地址,进程号pid等;init_task的特殊之处在于它的pid=0,也就是通常所说的0号进程(当然最终会进化成为idle进程,在下面会分析);关于0号进程的重要意义在下面会继续分析,在这里只要记得它是被start_kernel()函数创建的就可以啦。
在创建了0号进程之后,start_kernel()函数继续调用各个系统模块进行各种初始化之类的工作,比如:
trap_init()是中断向量的相关设置
mm_init()是内存管理的设置
sched_init()是调度模块的初始化
至于调用的多少模块以及其相关的功能在这里就不仔细写了(其实因为弄丢了原来的博客来不及重写啊),因为我看到了同样学习了这门课的卢晅同学的博客对这个问题分析的很详细,所以就转载一下吧:
上面这张图片转载自卢晅同学的博客:/article/1916367.html
在执行了上面的各项工作之后,是start_kernel()函数的最后一行代码:
这样子就调用了另一个非常重要的函数rest_init(),它的位置在/linux-3.18.6/init/main.c,其代码如下:
rest_init()函数中这行代码:
kernel_thread这个函数(/linux-3.18.6/kernel/fork.c)的源码是:
从注释部分就可以看出来这个函数的功能是创建一个内核线程(应该是进程吧喂!!不然哪里会有pid啊喂!!)
kernel_thread函数中第一个参数是一个函数指针,也就是说内核此时fork出了一个新进程来执行kernel_init函数(低版本内核中这个函数名为init,为了区分init进程所以将其改为了kernel_init);在kernel_init函数(/linux-3.18.6/init/main.c)正式启动了init进程:
至此rest_init()函数启动了另一个大名鼎鼎的init进程,也就是1号进程,关于它的重要性以及功能同样放在下面分析;接下来是rest_init()函数的这一行代码:
由上面的分析可以知道这个行代码folk了一个新的进程来执行函数kthreadd,这个函数的源码在这里/linux-3.18.6/kernel/kthread.c,上面这一行代码实现的功能是创建一个pid=2的内核进程,来管理内核的一些资源。
rest_init()在创建了1号、2号进程之后,我们忽略其余的部分(其实是因为看不懂吧喂!!)直接来分析其最后一行代码:
调用了cpu_startup_entry( /linux-3.18.6/kernel/sched/idle.c )函数,其源码:
而cpu_idle_loop中其实就是进入了一个无限循环:
也就是说,0号进程在fold了1号进程并且做了其余的启动工作之后,最后”进化“成为了idle进程。
至此,由start_kernel()函数所开始的内核启动告一段落,系统此时已经可以”正常“的接受任务进行工作了。
上述启动流程的图示:
由于只是一次简单的实验,对于复杂的内核启动流程自然不可能仅仅用上面一张图片总结,本文的参考文献中提供了大量的内核启动相关流程,有兴趣的话请参阅
第三,0号进程与1号进程
0号进程:所有进程的”祖先“,由start_kernel()函数在内核启动过程中”手动“创建的一个内核进程,始终处于内核态。在内核启动的之后0号进程完全“进化”成idle进程,系统开始无限循环
1号进程:
补充:init可执行文件是可以在/sbin/init,/etc/init,/bin/init,/bin/sh之中的
上面两张图片截图自课程提供的文档:
/article/8611418.html
http://teamtrac.ustcsz.edu.cn/raw-attachment/wiki/Linux2012/Linux-init-process-analyse.pdf
/article/1495165.html
http://www.linuxidc.com/Linux/2014-10/108033p5.htm
http://itdreamerchen.com/从源码中跟踪linux-kernel的启动过程/
/article/7866179.html
/article/1916367.html
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”
实验环境:c+Linux64位 (32位系统可能结果会不同)
依照学术诚信条款,我保证此回答为本人原创,所有回答中引用的外部材料已经做了出处标记。
实验环境:ubuntu14.04操作系统,x86体系结构
实验要求:使用gdb跟踪调试内核从start_kernel到init进程启动
第一步,Linux内核代码结构
本课程提供了一个Linux的内核源码,其结构如下图:
内核地址地址:http://codelab.shiyanlou.com/xref/linux-3.18.6/
不同的文件夹代表了内核的不同模块,其含义是:
arch/ 是体系结构相关的代码,其中的/x86 文件夹下的内容是x86体系结构相关代码,是内核分析的重要分析目标。
init/ 是内核启动相关的代码,是本文的重点分析对象。/init/main.c 文件是内核启动的起点,是分析内核启动流程的首要分析对象。
fs/ 文件系统(file system?)
kernel/ 内核相关的代码,一些内核中使用到的结构体、函数等重要对象的定义都在这里面。
mm/ 内存管理的相关代码(memory managment?)
第二步,Linux内核启动过程分析
/init/main.c 文件中的start_kernel()函数是一切的起点,在这个函数被调用之前都是系统的初始化工作(汇编语言),所以对内核的启动分析一般都从这个函数开始;main.c中没有main函数,start_kernel()这个函数就相当于是c程序中的main函数。下面从这个函数开始对内核的启动流程进行分析。
由于写博文时没有随时保存的习惯,再加上手贱的原因,所以这是重新写的博文,因为时间的原因实验截图就不放上了,直接写一点自己的理会吧
start_kernel()函数的原型是:
这个函数在执行的过程中初始化、定义了内核中一些十分重要的内容,其执行过程几乎涉及到了内核的所有模块;首先,是init_task
init_task的定义在/linux-3.18.6/init/init_task.c
struct task_struct init_task = INIT_TASK(init_task);
它其实就是一个task_struct,与用户进程的task_struct一样, task_struct中保存了一个进程的所有基本信息,如进程状态,栈起始地址,进程号pid等;init_task的特殊之处在于它的pid=0,也就是通常所说的0号进程(当然最终会进化成为idle进程,在下面会分析);关于0号进程的重要意义在下面会继续分析,在这里只要记得它是被start_kernel()函数创建的就可以啦。
在创建了0号进程之后,start_kernel()函数继续调用各个系统模块进行各种初始化之类的工作,比如:
trap_init()是中断向量的相关设置
mm_init()是内存管理的设置
sched_init()是调度模块的初始化
至于调用的多少模块以及其相关的功能在这里就不仔细写了(其实因为弄丢了原来的博客来不及重写啊),因为我看到了同样学习了这门课的卢晅同学的博客对这个问题分析的很详细,所以就转载一下吧:
上面这张图片转载自卢晅同学的博客:/article/1916367.html
在执行了上面的各项工作之后,是start_kernel()函数的最后一行代码:
这样子就调用了另一个非常重要的函数rest_init(),它的位置在/linux-3.18.6/init/main.c,其代码如下:
393static noinline void __init_refok rest_init(void) 394{ 395 int pid; 396 397 rcu_scheduler_starting(); 398 /* 399 * We need to spawn init first so that it obtains pid 1, however 400 * the init task will end up wanting to create kthreads, which, if 401 * we schedule it before we create kthreadd, will OOPS. 402 */ 403 kernel_thread(kernel_init, NULL, CLONE_FS); 404 numa_default_policy(); 405 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); 406 rcu_read_lock(); 407 kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns); 408 rcu_read_unlock(); 409 complete(&kthreadd_done); 410 411 /* 412 * The boot idle thread must execute schedule() 413 * at least once to get things moving: 414 */ 415 init_idle_bootup_task(current); 416 schedule_preempt_disabled(); 417 /* Call into cpu_idle with preempt disabled */ 418 cpu_startup_entry(CPUHP_ONLINE); 419}
rest_init()函数中这行代码:
kernel_thread(kernel_init, NULL, CLONE_FS);
kernel_thread这个函数(/linux-3.18.6/kernel/fork.c)的源码是:
从注释部分就可以看出来这个函数的功能是创建一个内核线程(应该是进程吧喂!!不然哪里会有pid啊喂!!)
kernel_thread函数中第一个参数是一个函数指针,也就是说内核此时fork出了一个新进程来执行kernel_init函数(低版本内核中这个函数名为init,为了区分init进程所以将其改为了kernel_init);在kernel_init函数(/linux-3.18.6/init/main.c)正式启动了init进程:
至此rest_init()函数启动了另一个大名鼎鼎的init进程,也就是1号进程,关于它的重要性以及功能同样放在下面分析;接下来是rest_init()函数的这一行代码:
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
由上面的分析可以知道这个行代码folk了一个新的进程来执行函数kthreadd,这个函数的源码在这里/linux-3.18.6/kernel/kthread.c,上面这一行代码实现的功能是创建一个pid=2的内核进程,来管理内核的一些资源。
rest_init()在创建了1号、2号进程之后,我们忽略其余的部分(其实是因为看不懂吧喂!!)直接来分析其最后一行代码:
cpu_startup_entry(CPUHP_ONLINE);
调用了cpu_startup_entry( /linux-3.18.6/kernel/sched/idle.c )函数,其源码:
256void cpu_startup_entry(enum cpuhp_state state) 257{ 258 /* 259 * This #ifdef needs to die, but it's too late in the cycle to 260 * make this generic (arm and sh have never invoked the canary 261 * init for the non boot cpus!). Will be fixed in 3.11 262 */ 263#ifdef CONFIG_X86 264 /* 265 * If we're the non-boot CPU, nothing set the stack canary up 266 * for us. The boot CPU already has it initialized but no harm 267 * in doing it again. This is a good place for updating it, as 268 * we wont ever return from this function (so the invalid 269 * canaries already on the stack wont ever trigger). 270 */ 271 boot_init_stack_canary(); 272#endif 273 arch_cpu_idle_prepare(); 274 cpu_idle_loop(); 275} 276
而cpu_idle_loop中其实就是进入了一个无限循环:
189static void cpu_idle_loop(void) 190{ 191 while (1) { 192 /* 193 * If the arch has a polling bit, we maintain an invariant: 194 * 195 * Our polling bit is clear if we're not scheduled (i.e. if 196 * rq->curr != rq->idle). This means that, if rq->idle has 197 * the polling bit set, then setting need_resched is 198 * guaranteed to cause the cpu to reschedule. 199 */ 200 201 __current_set_polling(); 202 tick_nohz_idle_enter(); 203 204 while (!need_resched()) { 205 check_pgt_cache(); 206 rmb(); 207 208 if (cpu_is_offline(smp_processor_id())) 209 arch_cpu_idle_dead(); 210 211 local_irq_disable(); 212 arch_cpu_idle_enter(); 213 214 /* 215 * In poll mode we reenable interrupts and spin. 216 * 217 * Also if we detected in the wakeup from idle 218 * path that the tick broadcast device expired 219 * for us, we don't want to go deep idle as we 220 * know that the IPI is going to arrive right 221 * away 222 */ 223 if (cpu_idle_force_poll || tick_check_broadcast_expired()) 224 cpu_idle_poll(); 225 else 226 cpuidle_idle_call(); 227 228 arch_cpu_idle_exit(); 229 } 230 231 /* 232 * Since we fell out of the loop above, we know 233 * TIF_NEED_RESCHED must be set, propagate it into 234 * PREEMPT_NEED_RESCHED. 235 * 236 * This is required because for polling idle loops we will 237 * not have had an IPI to fold the state for us. 238 */ 239 preempt_set_need_resched(); 240 tick_nohz_idle_exit(); 241 __current_clr_polling(); 242 243 /* 244 * We promise to call sched_ttwu_pending and reschedule 245 * if need_resched is set while polling is set. That 246 * means that clearing polling needs to be visible 247 * before doing these things. 248 */ 249 smp_mb__after_atomic(); 250 251 sched_ttwu_pending(); 252 schedule_preempt_disabled(); 253 } 254} 255
也就是说,0号进程在fold了1号进程并且做了其余的启动工作之后,最后”进化“成为了idle进程。
至此,由start_kernel()函数所开始的内核启动告一段落,系统此时已经可以”正常“的接受任务进行工作了。
上述启动流程的图示:
由于只是一次简单的实验,对于复杂的内核启动流程自然不可能仅仅用上面一张图片总结,本文的参考文献中提供了大量的内核启动相关流程,有兴趣的话请参阅
第三,0号进程与1号进程
0号进程:所有进程的”祖先“,由start_kernel()函数在内核启动过程中”手动“创建的一个内核进程,始终处于内核态。在内核启动的之后0号进程完全“进化”成idle进程,系统开始无限循环
1号进程:
补充:init可执行文件是可以在/sbin/init,/etc/init,/bin/init,/bin/sh之中的
上面两张图片截图自课程提供的文档:
/article/8611418.html
http://teamtrac.ustcsz.edu.cn/raw-attachment/wiki/Linux2012/Linux-init-process-analyse.pdf
参考文献:
对于复杂的内核启动流程自然不能用一片文章就解释清楚,虽然在实验中做了一些跟踪分析,不过为了完成此文还是参考了不少优秀文献,许多文章中对内核的启动分析都比本文要详细透彻的多,在此对作者表示感谢!/article/1495165.html
http://www.linuxidc.com/Linux/2014-10/108033p5.htm
http://itdreamerchen.com/从源码中跟踪linux-kernel的启动过程/
/article/7866179.html
/article/1916367.html
相关文章推荐
- 《Linux内核分析》(一)——反汇编一个简单的C程序并分析其汇编代码的执行
- Linux下的调试工具
- Linux上有两种时间,一种是硬件时间,一种是系统时间
- GDT(全局描述符表)和LDT(局部描述符表)
- Linux中的内存管理
- linux下进程的最大线程数、进程最大数、进程打开的文件数
- 实模式和保护模式
- linuxPAM认证配置文件解析
- 在Linux下安装windows后解决Linux不能启动能问题
- 让linux每天定时备份MySQL数据库并删除五天前的备份文件
- linux中文件搜索相关的locate,find,whereis与which,grep的使用
- Linux Makefile 详细语法
- 关于linux下的fork()函数
- major、minor宏在linux头文件位置
- CentOS单独编译安装PHP gd库扩展
- Linux突然断电后文件丢失的问题
- linux下配置jdk
- 一些 Linux 桌面小技巧
- 我的linux分区方案
- linux下的输出重定向和快捷键