在上一篇中,我们进行Linux内核主要部分初始化,现在我们进行最后一步初始化。
接下来,调用arch_call_rest_init函数,该函数很简单,只调用rest_init函数。rest_init函数同样在init/main.c中实现。如下:
noinline void __ref rest_init(void)
{
struct task_struct *tsk;
int pid;
rcu_scheduler_starting();
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
rcu_read_lock();
tsk = find_task_by_pid_ns(pid, &init_pid_ns);
set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
rcu_read_unlock();
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();
system_state = SYSTEM_SCHEDULING;
complete(&kthreadd_done);
schedule_preempt_disabled();
cpu_startup_entry(CPUHP_ONLINE);
}rcu_scheduler_starting函数将RCU调度器标记为活跃状态。
kernel_thread函数在kernel/fork.c中实现。创建新的内核线程。它需要三个参数,fn为在新的进程里执行的函数;
arg为函数的参数,flags为标记。kernel_thread函数调用_do_fork函数,创建新的线程。通过CLONE_FS和CLONE_FILES标记是父线程和子线程直接共享文件信息和文件系统信息。在rest_init函数里我们创建了两个内核线程,pid = 1的kernel_init线程和pid = 2的kthreadd线程。
rcu_read_lock和rcu_read_unlock这两个函数分别标记RCU读的临界区的开始和结束。
set_cpus_allowed_ptr函数设置kernel_init线程允许运行的CPU。
find_task_by_pid_ns函数kernel/pid.c实现,根据pid返回对应的struct task_struct。
接下来,调用complete函数,传递了一个参数:kthreadd_done。kthreadd_done的定义如下:
static __initdata DECLARE_COMPLETION(kthreadd_done);
#define DECLARE_COMPLETION(work) \
struct completion work = COMPLETION_INITIALIZER(work)展开后,定义了一个struct completion的结构,在include/linux/completion.h中定义。该结构描述了一个代码同步机制,提供了在线程到达某个点或状态时可以提供无竞争的方案。使用完成(completions)需要三个步骤:第一步定义complete结构,我们通过COMPLETION_INITIALIZER实现;第二步调用wait_for_completion函数,在调用这个函数后,线程被调用时不在举行执行,而是等待其他没有调用complete函数的线程;第三步,调用complete函数。
schedule_preempt_disabled函数在kernel/sched/core.c中实现,该函数禁用CPU抢占。
cpu_startup_entry函数在kernel/sched/idle.c中实现,在设置CPU状态后,循环调用do_idle函数。如下:
void cpu_startup_entry(enum cpuhp_state state)
{
arch_cpu_idle_prepare();
cpuhp_online_idle(state);
while (1)
do_idle();
}cpu_startup_entry函数在后台调度init_task,cpu_startup_entry函数的主要功能是消耗空闲的CPU周期。当没有其他程序运行时,init_task开始运行。do_idle函数检查是否有其他活跃的任务可以切换。
在rest_init函数中,我们创建了两个内核线程,其中一个是init线程(调用kernel_init函数)。现在我们来看看kernel_init函数,该函数同样在init/main.c中实现。
kernel_init_freeable
kernel_init函数中首先调用的是kernel_init_freeable函数。kernel_init_freeable函数的执行过程如下:
调用wait_for_completion(&kthreadd_done)函数,等待kthreadd线程完成所有的设置;
设置gfp_allowed_mask为__GFP_BITS_MASK,意味着,调度程序已经完全设置,此时系统处于运行状态;
调用set_mems_allowed函数,允许所有的CPU和NUMA节点允许访问内存,在include/linux/cpuset.h中实现;
设置cad(Ctrl-Alt-Delete)的进程id,即:当前任务id;
调用smp_prepare_cpus函数准备启动其他的CPU,调用smp_ops.smp_prepare_cpus;
调用workqueue_init函数初始化工作队列,在kernel/workqueue.c中实现;
调用init_mm_internals函数创建mm_percpu_wq工作队列;初始化CPU状态;创建buddyinfo, pagetypeinfo, vmstat, zoneinfo的proc信息。在mm/vmstat.c中实现;
调用do_pre_smp_initcalls函数初始化早期initcalls;
调用lockup_detector_init函数初始化lockup detector(或者nmi_watchdog);在kernel/watchdog.c中实现;
调用smp_init函数初始化smp,启动所有可用的CPU。在kernel/smp.c中实现;
调用sched_init_smp函数初始化smp的调度处理城区。在kernel/sched/core.c中实现;
调用page_alloc_init_late函数进行页分配的后续初始化。在mm/page_alloc.c中实现;
调用page_ext_init函数进行扩展页的初始化。在mm/page_ext.c中实现。
do_basic_setup
接下来,调用do_basic_setup函数,在init/main.c中实现。在调用这个函数之前,内核已经完成了初始化,CPU已经已启动并且在运行,内存和进程管理正常工作,接下来,调用do_basic_setup函数进行系统功能的初始化。
调用cpuset_init_smp函数重新初始化CPU;
driver_init函数在drivers/base/init.c中实现。初始化驱动模块,包括,devtmpfs, devices, bus, class, firmware, hypervisor, devicetree等proc目录的建立,platform, cpu, memory, container子系统的注册。
init_irq_proc函数在kernel/irq/proc.c中实现。创建proc/irq目录;每一个IRQ注册proc信息;
do_ctors函数在init/main.c中实现。调用所有的构造函数,即,__ctors_start和__ctors_end之间的函数。
usermodehelper_enable函数在include/linux/umh.h中实现。启用用户模式;
do_initcalls函数init/main.c中实现。调用早期后面的initcall,包括:pure, core, postcore, arch, subsys, fs, device, late等8个层级。
- 打开
console
接下来,打开rootfs中/dev/console文件,并且复制这个文件描述符两次,即:0 ~ 2。如下:
if (ksys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
pr_err("Warning: unable to open an initial console.\n");
(void) ksys_dup(0);
(void) ksys_dup(0);- 挂载
initrd
首先,检查命令行中rdinit=参数,或设置默认的ramdisk路径;检查用户访问ramdisk的权限,并调用init/do_mounts.c中prepare_namespace函数挂载initrd。如下:
if (!ramdisk_execute_command)
ramdisk_execute_command = "/init";
if (ksys_access((const char __user *)
ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
}- 释放初始化阶段的内存
在调用kernel_init_freeable后,返回kernel_init函数,进行后续执行操作。
async_synchronize_full函数在kernel/async.c中实现,等待所有异步的函数调用都完成。
ftrace_free_init_mem函数在kernel/trace/ftrace.c中实现,释放ftrace初始化过程中使用的内存;
free_initmem函数在kernel/trace/ftrace.c中实现,释放内核镜像初始化过程使用的内存;
mark_readonly函数实现只读数据段(.rodata)内存保护;
pti_finalize函数在arch/x86/mm/pti.c实现,实现内核页表和用户页表的映射;
- 运行
init进程
经过上面的设置后,修改系统状态为运行状态(SYSTEM_RUNNING);然后,调用run_init_process或try_to_run_init_process运行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);
}
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;run_init_process函数填充argv_init后,调用do_execve函数运行指定的程序和参数。
static const char *argv_init[MAX_INIT_ARGS+2] = { "init", NULL, };
const char *envp_init[MAX_INIT_ENVS+2] = { "HOME=/", "TERM=linux", NULL, };
static int run_init_process(const char *init_filename)
{
argv_init[0] = init_filename;
pr_info("Run %s as init process\n", init_filename);
return do_execve(getname_kernel(init_filename),
(const char __user *const __user *)argv_init,
(const char __user *const __user *)envp_init);
}运行init程序的顺序为,rdinit=参数 -> init -> init=参数 -> /sbin/init -> /etc/init -> /bin/init -> /bin/sh。
如果,上面的进程均不能正常运行,调用panic,如下:
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/admin-guide/init.rst for guidance.");在rest_init函数中,我们创建了两个内核线程,其中一个是init线程(调用kernel_init函数),在上面已经描述。现在我们来看看kthreadd函数,该函数同样在kernel/kthread.c中实现。 如下:
int kthreadd(void *unused)
{
struct task_struct *tsk = current;
set_task_comm(tsk, "kthreadd");
ignore_signals(tsk);
set_cpus_allowed_ptr(tsk, cpu_all_mask);
set_mems_allowed(node_states[N_MEMORY]);
current->flags |= PF_NOFREEZE;
cgroup_init_kthreadd();
for (;;) {
...
}
return 0;
}set_task_comm函数设置task_struct名称,这里设置为kthreadd;
ignore_signals函数在kernel/signal.c中实现,设置task_struct中信号处理程序为SIG_IGN;
set_cpus_allowed_ptr和set_mems_allowed在上面描述过,设置运行的CPU和运行使用的内存。同样的,设置可以使用所有的CPU和内存;
current->flags |= PF_NOFREEZE;设置该进程不被冷冻;
在进行上述设置后,进入该线程的主体函数for(;;)。在循环里,判断kthread_create_list列表状态,列表为空时,调用schedule函数;否则,获取并移除kthread_create_list的第一个struct kthread_create_info信息,调用create_kthread进行处理。
create_kthread函数同样在kernel/kthread.c中实现。创建一个内核线程,该线程调用kthread函数。
kthread函数同样在kernel/kthread.c中实现,在进行必要的初始化和检查后,调用kthread_create_info中的threadfn函数。
本文描述了Linux内核init进程和kthread进程的初始化过程,至此,我们已经完成了Linux内核的所有初始化过程。
本系列文章翻译自linux-insides,如果你有任何问题或者建议,请联系0xAX或者创建 issue。