进程管理(六)--进程初始化

我们知道,对于内核提供的进程管理子系统,将来肯定是要运行各种各样的进程,对于我们做Linux内核开发的同学来说,大家熟悉Linux下有3个特殊的进程,其主要内容如下:

  • Idle进程(PID = 0),本章主要讲解进程0是什么?
  • Init进程(PID = 1),本章主要讲解进程1是什么?
  • kthread(PID = 2),本章主要讲解进程2是什么?

1 进程初始化(0号进程)

内核的启动从入口函数 start_kernel() 开始;在 init/main.c 文件中,start_kernel 相当于内核的main 函数;
这个里面是各种各样初始化函数,用来初始化各个子系统。对于操作系统,开机的时候首先会创建第一个进程,也就是唯一一个没有通过fork产生的进程。首先内核就需要为init_task进程的task_struct数据结构进行分配。

1.1 进程描述符分配

第0号进程描述符变量是Init_task,在init/init_task.c文件中静态初始化,其代码实现如下:

struct task_struct init_task = INIT_TASK(init_task);
EXPORT_SYMBOL(init_task);

宏INIT_TASK的定义在文件include/linux/init_task.h中:

#define INIT_TASK(tsk)	\
{									\
	INIT_TASK_TI(tsk)						\
	.state		= 0,						\
	.stack		= init_stack,					\
	.usage		= ATOMIC_INIT(2),				\
	.flags		= PF_KTHREAD,					\
	.prio		= MAX_PRIO-20,					\
	.static_prio	= MAX_PRIO-20,					\
	.normal_prio	= MAX_PRIO-20,					\
	.policy		= SCHED_NORMAL,					\
	.cpus_allowed	= CPU_MASK_ALL,					\
	.nr_cpus_allowed= NR_CPUS,					\
	.mm		= NULL,						\
	.active_mm	= &init_mm,					\
	.restart_block = {						\
		.fn = do_no_restart_syscall,				\
	},								\
	.se		= {						\
		.group_node 	= LIST_HEAD_INIT(tsk.se.group_node),	\
	},								\
	.rt		= {						\
		.run_list	= LIST_HEAD_INIT(tsk.rt.run_list),	\
		.time_slice	= RR_TIMESLICE,				\
	},								\
	.tasks		= LIST_HEAD_INIT(tsk.tasks),			\
	...
	.comm		= INIT_TASK_COMM,				\
	.thread		= INIT_THREAD,					\
	.fs		= &init_fs,					\
	.files		= &init_files,					\
	.signal		= &init_signals,				\
	.sighand	= &init_sighand,				\
	.nsproxy	= &init_nsproxy,				\
}

从comm字段看出,进程0叫swapper,此外,系统中的所有进程的task_struct数据结构都通过list_head类型的双向链表链接在一起,因此每个进程的task_struct数据结构都包含一个list_head的tasts成员。这个进程链表的头是init_task进程,也就是所谓的进程0。

1.2 进程堆栈

init_task进程使用init_thread_union数据结构描述的内存区域作为该进程的堆栈空间,并且和自身的thread_info参数公用这一内存空间空间

#define INIT_TASK(tsk)	\
{
	...
    .stack		= init_stack,					\
    ...
}

而init_thread_info则是一段体系结构相关的定义,被定义在/arch/arm64/include/asm/thread_info.h

#define init_thread_info	(init_thread_union.thread_info)
#define init_stack		(init_thread_union.stack)

而init_thread_union则定义在init/init_task.c中

union thread_union init_thread_union __init_task_data = {
#ifndef CONFIG_THREAD_INFO_IN_TASK
	INIT_THREAD_INFO(init_task)
#endif
};
#define INIT_THREAD_INFO(tsk)						\
{									\
	.task		= &tsk,						\
	.flags		= 0,						\
	.preempt_count	= INIT_PREEMPT_COUNT,				\
	.addr_limit	= KERNEL_DS,					\
}

1.3 进程空间

由于init_task是一个运行在内核空间的内核线程, 因此其虚地址段mm为NULL, 但是必要时他还是需要使用虚拟地址的,因此avtive_mm被设置为init_mm

#define INIT_TASK(tsk)	\
{
	.mm		= NULL,						\  
	.active_mm	= &init_mm,		\
}

struct mm_struct init_mm = {
	.mm_rb		= RB_ROOT,
	.pgd		= swapper_pg_dir,
	.mm_users	= ATOMIC_INIT(2),
	.mm_count	= ATOMIC_INIT(1),
	.mmap_sem	= __RWSEM_INITIALIZER(init_mm.mmap_sem),
	.page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
	.mmlist		= LIST_HEAD_INIT(init_mm.mmlist),
	.user_ns	= &init_user_ns,
	INIT_MM_CONTEXT(init_mm)
};

对于普通进程而言,这两个指针变量的值相同。但是,内核线程不拥有任何内存描述符,所以它们的mm成员总是为NULL。当init_task内核线程得以运行时,它的active_mm成员被初始化为init_mm的值。

2 其他初始化

内核启动阶段的最后函数reset_init()函数在内部再次调用多个函数,其最终会创建1号进程2号进程

static noinline void __ref rest_init(void)
{
	int pid;

	rcu_scheduler_starting();
	/*
	 * We need to spawn init first so that it obtains pid 1, however
	 * the init task will end up wanting to create kthreads, which, if
	 * we schedule it before we create kthreadd, will OOPS.
	 */
	kernel_thread(kernel_init, NULL, CLONE_FS);                            -----------(1)
	numa_default_policy();
	pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);           -----------(2)
	rcu_read_lock();
	kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
	rcu_read_unlock();
	complete(&kthreadd_done);

	/*
	 * The boot idle thread must execute schedule()
	 * at least once to get things moving:
	 */
	init_idle_bootup_task(current);                                        ------------(3)
	schedule_preempt_disabled();                                           ------------(4)
	/* Call into cpu_idle with preempt disabled */
	cpu_startup_entry(CPUHP_ONLINE);                                       ------------(5)
}
    1. 调用kernel_thread()创建1号内核线程, 该线程随后转向用户空间, 演变为init进程
    1. 调用kernel_thread()创建kthreadd内核线程
    1. init_idle_bootup_task():当前0号进程init_task最终会退化成idle进程,所以这里调用init_idle_bootup_task()函数,让init_task进程隶属到idle调度类中。即选择idle的调度相关函数。
    1. 调用schedule()函数切换当前进程,在调用该函数之前,Linux系统中只有两个进程,即0号进程init_task和1号进程kernel_init,其中kernel_init进程也是刚刚被创建的。调用该函数后,1号进程kernel_init将会运行
    1. 调用cpu_idle(),0号线程进入idle函数的循环,在该循环中会周期性地检查。

2.1 初始化1号进程

init进程是启动过程中内核生成的进程,其PID为1,它生成所有用户进程并监视其运行,在系统结束前始终保持执行状态。kernel_thread(kernel_init, NULL, CLONE_FS) 创建第二个进程,这个是1 号进程。详细的介绍见根文件系统初见

static int __ref kernel_init(void *unused)
{
	int ret;

	kernel_init_freeable();
	/* need to finish all async __init code before freeing the memory */
	async_synchronize_full();                                         -----------------(1)
	free_initmem();
	mark_readonly();
	system_state = SYSTEM_RUNNING;
	numa_default_policy();

	rcu_end_inkernel_boot();

	if (ramdisk_execute_command) {                                    -----------------(2)
		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) {                                            -----------------(3)
		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") ||                     -----------------(4)
	    !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.");
}
  • async_synchronize_full中结束所有非同步操作,准备释放内存,在free_initmem中释放所有初始化函数和函数使用的.init.data内存区域
  • 内核态的1号kernel_init进程将会转换为用户空间内的1号进程init。户进程init将根据/etc/inittab中提供的信息完成应用程序的初始化调用。然后init进程会执行/bin/sh产生shell界面提供给用户来与Linux系统进行交互,调用run_init_process()创建用户模式1号进程。

2.2 初始化2号进程

内核线程守护进程是执行内核线程的守护进程,在内核启动时生成注册到kthread_create_list的所有内核线程,下面是kthreadd函数:

int kthreadd(void *unused)
{
	struct task_struct *tsk = current;                     //获取当前恩物

	/* Setup a clean context for our children to inherit. */
	set_task_comm(tsk, "kthreadd");                        //配置2号进程的名字kthreadd
	ignore_signals(tsk);                                   //将任务信号处理设置为忽略所有信号
	set_cpus_allowed_ptr(tsk, cpu_all_mask);            //允许kthreadd在任意CPU上运行,设置亲和性
	set_mems_allowed(node_states[N_MEMORY]);

	current->flags |= PF_NOFREEZE;
	cgroup_init_kthreadd();

	for (;;) {
//首先将线程状态设置为 TASK_INTERRUPTIBLE,没有要创建的线程则主动放弃 CPU 完成调度.此进程变为阻塞态
		set_current_state(TASK_INTERRUPTIBLE);
		if (list_empty(&kthread_create_list))//没有需要创建的内核线程
			schedule();						 //执行一次调度, 让出CPU
		__set_current_state(TASK_RUNNING);  //运行到此表示 kthreadd 线程被唤醒
		//设置进程运行状态为 TASK_RUNNING
		spin_lock(&kthread_create_lock);
		while (!list_empty(&kthread_create_list)) {                    --------------(1)
			struct kthread_create_info *create;

			create = list_entry(kthread_create_list.next,
					    struct kthread_create_info, list);
			list_del_init(&create->list);
			spin_unlock(&kthread_create_lock);

			create_kthread(create);

			spin_lock(&kthread_create_lock);
		}
		spin_unlock(&kthread_create_lock);
	}

	return 0;
}

代码1是kthread函数的核心部分,如果内核线程列表kthread_create_list不为空,就调用list_entry函数使kthread_create_info结构体的create成员执行kthread_create_list的第一个成员。生成的内核线程的信息,其定义如下:

kthread_create_list成员struct kthread_create_info
{
	/* Information passed to kthread() from kthreadd. */
	int (*threadfn)(void *data);                                 //要执行的函数
	void *data;                                                  //传递给函数的数据
	int node;                                                    

	/* Result passed back to kthread_create() from kthreadd. */
	struct task_struct *result;                                  //生成内核线程后的任务
	struct completion *done;                                     //通知已结束
 
 	struct list_head list;                                       //连接到kthread_create_list成员
};

所以该代码大致做了以下几件事情

  • 设置当前进程的名称为kthreadd,也就是task_struct的comm字段
  • 然后就是for循环,设置当前的进程状态为TASK_INTERRUPTIBLE是可以中断的
  • 判断kthread_create_list链表是否为空,如果是空就调度出去,让出CPU;如果不是空,则从链表中取出,然后调用create_kthread去创建内核线程
  • 所以所有的内核线程的父进程都是2号进程,也就是kthread

3. idle进程

Linux Kernel 会在系统启动完成后,在 Idle 进程中,处理 CPUIdle 相关的事情。在多核系统中,CPU 启动的过程是,先启动主 CPU,启动过程和传统的单核系统类似。其函数调用关系如下:

stext –> start_kernel –> rest_init –> cpu_startup_entry

而启动其它 CPU,可以有多种方式,例如 CPU hotplug 等,启动过程:

secondary_startup –> __secondary_switched –> secondary_start_kernel –> cpu_startup_entry

进程管理(六)--进程初始化_第1张图片

在这个函数中,最终程序会掉进无限循环里 cpu_idle_loop。到此,Idle 进程创建完成,以下是 Idle 进程的代码实现

static void cpu_idle_loop(void)
{
	int cpu = smp_processor_id();

	while (1) {
		__current_set_polling();
		quiet_vmstat();
		tick_nohz_idle_enter();                        //关闭周期tick,CONFIG_NO_HZ_IDLE必须打开

		while (!need_resched()) {                      //如果系统当前不需要调度,执行后续动作
			check_pgt_cache();
			rmb();

			if (cpu_is_offline(cpu)) {
				cpuhp_report_idle_dead();
				arch_cpu_idle_dead();
			}

			local_irq_disable();                      //关闭irq中断
			arch_cpu_idle_enter();                   //arch相关的cpuidle enter
													// 主要执行注册到 idle 的 notify callback
			if (cpu_idle_force_poll || tick_check_broadcast_expired())
				cpu_idle_poll();					//idle pill
			else
				cpuidle_idle_call();               //进入cpu的idle模式,进行省电

			arch_cpu_idle_exit();                  //idle退出,主要执行注册idle的notify callback
		}
        //如果系统当前需要调度,就退出idle进程
		preempt_set_need_resched();
		tick_nohz_idle_exit();                    //打开周期tick
		__current_clr_polling();

		smp_mb__after_atomic();

		sched_ttwu_pending();
		schedule_preempt_disabled();             //让出cpud,是调度器调度其他优先级更高的进程
	}
}

系统的周期tick可动态地关闭和打开,这个功能可以通过内核配置项CONFIG_NO_HZ打开,而IDLE正是使用这项技术,使系统尽量长时间处于空闲状态,从而尽可能节省功耗。这个内容比较多,后续再单独学习。

4. 总结

Linux启动的第一个进程是0号进程,是静态创建的,然后0号进程启动后会创建两个进程,分别是1号和2号进程

  • 0号进程是系统创建的第一个进程,也是唯一一个没有通过fork或kernel_thread产生的进程,完成加载系统后,演变为idle进程

  • 1号(init)进程由idle通过kernel_thread创建,在内核空间完成初始化后,最终会调用init可执行文件,init进程最终会去创建所有的应用进程,是其他用户进程的祖先。

  • 2号(kthread)进程由idle进程通过kernel_thread创建,并始终运行在内核空间,负责所有内核线程的调度和管理

进程管理(六)--进程初始化_第2张图片

从上面的图示可以看出,PID=1的进程是init,PID=2的进程是kthreadd,而他们的父进程PPID=0,也就是0号进程。在往后面看,所有的内核线程的PPID=2,页就是说内核线程的父进程都是kthreadd进程。

进程管理(六)--进程初始化_第3张图片

所有用户态的进程的父进程PPID=1,也就是1号进程都是他们的父进程。其中用户态的不带中括号,内核态的带中括号。其关系图如下图所示
进程管理(六)--进程初始化_第4张图片

你可能感兴趣的:(进程管理,linux,操作系统,内核,进程管理)