linux内核的init线程
(注)
-
本文代码出自linux内核版本:
4.1.15
(小生的文章若无特别说明,都将基于此版本) -
本文代码的出处文件以(/)开头,代表linux内核的安装目录。(小生的文章若无特别说明,都有这样约定)
文章目录
一、概述
在rest_init()
函数中会调用kthread_init()
函数创建init
线程,如下代码:
kernel_thread(kernel_init, NULL, CLONE_FS);
在linux内核中,由kernel_init
代表的是1号init
线程,当linux内核启动完成后,将启动用户空间的init
程序,使linux内核从内核态向用户态转变。因此,本文将分析init
线程的线程函数和调用机制。
二、init线程的入口函数kernel_init()
init线程的入口函数是kernel_init
,如下所示(/init/main.c):
static int __ref kernel_init(void *unused)
{
int ret;
kernel_init_freeable();
async_synchronize_full();
free_initmem();
mark_rodata_ro();
system_state = SYSTEM_RUNNING;
numa_default_policy();
flush_delayed_fput();
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n",
ramdisk_execute_command, ret);
}
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of init if we are
* trying to recover a really broken machine.
*/
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
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;
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/init.txt for guidance.");
}
三、kernel_init_freeable()分析
在kernel_init()
init线程函数中将调用kernel_init_freeable()
函数:
static noinline void __init kernel_init_freeable(void)
{
/* 等待kthreadd线程全部启动完毕 */
wait_for_completion(&kthreadd_done);
/* 代码运行在此处,scheduler调度器已经完全启动了,现在开始进行阻塞分配 */
gfp_allowed_mask = __GFP_BITS_MASK;
/* 设置init线程可以在任何node上分配pages */
set_mems_allowed(node_states[N_MEMORY]);
/* 设置init线程可以在任何cpu上运行 */
set_cpus_allowed_ptr(current, cpu_all_mask);
/* 获取当前线程的pid */
cad_pid = task_pid(current);
//在smp下,执行启动其他cpu的准备操作
smp_prepare_cpus(setup_max_cpus);
//执行smp之前的初始化调用
do_pre_smp_initcalls();
lockup_detector_init();
//SMP初始化。由引导处理器调用以激活剩余部分
smp_init();
//SMP调度初始化。
sched_init_smp();
//执行一些基础的启动操作。
do_basic_setup();
//在rootfs中打开/dev/console。这个地方不允许失败
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
pr_err("Warning: unable to open an initial console.\n");
(void) sys_dup(0);
(void) sys_dup(0);
/* 检查是否设置了早期用户空间的init程序,如果设置了,就让其执行。ramdisk_execute_command可以由“rdinit=”指定 */
if (!ramdisk_execute_command)
ramdisk_execute_command = "/init";
if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
}
integrity_load_keys();
load_default_modules();
}
上述代码31行:调用do_basic_setup()
执行一些基础的启动操作。该函数在linux内核启动过程中,比较重要,主要涉及了linux内核重要组件的初始化,例如:驱动模型的初始化等。
第37-38行,使用sys_dup()
系统调用函数将标准输入的文件描述符复制2次,一个作为标准输出,一个作为标准错误。通过该操作,将标准输入、输出、错误设置成/dev/console。在开发中,console
参数可以通过u-boot的bootargs环境变量设置,从而指定console参数值!!!
第41-42行代码:检查是否设置了早期用户空间的init程序,如果设置了,就让其执行 。ramdisk_execute_command可以由“rdinit=”指定 。如果ramdisk_execute_command
没有被执行,则linux将其设置为/init
。
第44-47行代码:通过sys_access
系统调用检查进程是否可以访问该文件(由ramdisk_execute_command指定)的路径名。调用prepare_namespace();
来挂载根文件系统。根文件系统也可由u-boot的命令行下环境变量参数指定。例如:root=/dev/mmcblk1p2 rootwait rw”就表示根文件系统在/dev/mmcblk1p2 中,也就是 EMMC 的分区 2 中。
第49-50行代码:代码运行到此处,linux内核已经完成了初始启动流程,实际上已经启动并运行了。接下来需要释放initmem
段占用的内存空间,并进入用户空间模式。这部分由kernel_init_freeable()
函数的后续代码完成。至此,rootfs
根文件系统是可用的,故调用integrity_load_keys()
尝试加载公钥和调用load_default_modules()
加载默认模块 。
下文将回到kernel_init()
函数,继续后续的分析。
四、kernel_init()余下部分的分析
当kernel_init_freeable()
函数执行完成后,linux内核的一些基础组件:例如设备驱动模型等、根文件系统已经准备完毕,接下来就将启动用户空间程序
了。从而使linux从内核态向用户态转变。
(4-1)一些基础的操作
如下代码片段:
async_synchronize_full();
free_initmem();
//(该函数似乎与调试相关,小生暂也不知道有啥作用。haha)
mark_rodata_ro();
system_state = SYSTEM_RUNNING;
numa_default_policy();
flush_delayed_fput();
(1)async_synchronize_full()
函数:用于同步所有的异步调用。
(2)free_initmem()
函数:用于释放初始化时的内存空间。
(3)使用system_state = SYSTEM_RUNNING;
设置系统状态为SYSTEM_RUNNING
。
(4)numa_default_policy()
函数(/mm/mempolicy.c):将当前线程的NUMA存储管理策略重置为默认值。
(5)flush_delayed_fput()
函数:如果内核线程确实需要完成它已经完成的最后一个fput(),才调用这个函数。在kernel_init()
函数中现在唯一的操作则是引导。因此需要确保在initramfs
上对二进制文件的写入操作,不会导致打开的struct file
等待__fput()
。如果没有这个flush_delayed_fput()
,execve()
将无法工作。
(4-2)启动ramdisk_execute_command指定的程序
(注:以下代码片段出自kernel_init()函数)
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n",
ramdisk_execute_command, ret);
}
ramdisk_execute_command
是一个全局的 char 指针变量,值为“/init”,也就是根目录下的 init 程序。ramdisk_execute_command 可以通过 uboot 在环境变量中指定,在 u-boot的bootargs
中使用“rdinit=xxx”即可指定,其中xxx 为具体的 init 程序名字。
如果ramdisk_execute_command
制定了(即不为NULL),则调用run_init_process()
函数启动它,启动成功则退出。
(4-3)启动execute_command指定的程序
(注:以下代码片段出自kernel_init()函数)
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
同样的,execute_command
也是一个全局的 char 指针变量,值可以通过u-boot 传递,如下代码片段(/init/main.c):
static int __init init_setup(char *str)
{
unsigned int i;
execute_command = str;
for (i = 1; i < MAX_INIT_ARGS; i++)
argv_init[i] = NULL;
return 1;
}
__setup("init=", init_setup);
由以上代码可见,在u-boot的启动参数bootargs
中使用“init=xxxx”就可以指定execute_command
的值,比如“init=/linuxrc”表示根文件系统中的linuxrc 就是要执行的用户空间的 init 程序。
同样的,如果execute_command
指定了,则调用run_init_process()
启动程序,启动成功则退出。
(4-4)启动linux内核默认的用户空间程序
当前面两种方式都没有启动用户空间的init
程序,那么就执行linux内核默认的程序了。
(注:以下代码片段出自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;
可见,在linux内核中默认指定了4个用户空间的程序:/sbin/init
、/etc/init
、/bin/init
、/bin/sh
。调用try_to_run_init_process()
函数启动它们。如果其中的任意一个程序被启动了,则退出。
(4-5)linux内核“罢工”
当通过以上三种方式都还没有启动用户空间的init
进程。linux内核则调用panic
给出信息
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/init.txt for guidance.");
至此,linux内核也没有启动用户空间的init
程序。系统停止/罢工啦。。。。。
工作中,当看到这条消息,说明代码运行到这个位置啦。既激动,又懊恼!!!
小生由于知识和精力有限,如果文章存在有不妥的地方,还请看官们多多批评。也可与我交流(e-mail:iriczhao@163.com),O(∩_∩)O谢谢啦。
扫一扫关注微信公众号,获取更多精彩内容>>>>