Skip to content

Latest commit

 

History

History
238 lines (174 loc) · 13.1 KB

File metadata and controls

238 lines (174 loc) · 13.1 KB

Linux内核初始化 (第六部分)

0 内核剩余部分初始化

在上一篇中,我们进行Linux内核主要部分初始化,现在我们进行最后一步初始化。

1 start_kernel的最后初始化(arch_call_rest_init)

接下来,调用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_FSCLONE_FILES标记是父线程和子线程直接共享文件信息和文件系统信息。在rest_init函数里我们创建了两个内核线程,pid = 1kernel_init线程和pid = 2kthreadd线程。

rcu_read_lockrcu_read_unlock这两个函数分别标记RCU读的临界区的开始和结束。 set_cpus_allowed_ptr函数设置kernel_init线程允许运行的CPU。 find_task_by_pid_ns函数kernel/pid.c实现,根据pid返回对应的struct task_struct

接下来,调用complete函数,传递了一个参数:kthreadd_donekthreadd_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_taskcpu_startup_entry函数的主要功能是消耗空闲的CPU周期。当没有其他程序运行时,init_task开始运行。do_idle函数检查是否有其他活跃的任务可以切换。

2 init进程初始化(kernel_init

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中实现; 设置cadCtrl-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, cpumemory, 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.cprepare_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_processtry_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.");

3 kthreadd进程初始化(kthreadd

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_ptrset_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函数。

4 结束语

本文描述了Linux内核init进程和kthread进程的初始化过程,至此,我们已经完成了Linux内核的所有初始化过程。

本系列文章翻译自linux-insides,如果你有任何问题或者建议,请联系0xAX或者创建 issue

如果你发现中文翻译有任何问题,请提交PR或者创建issue