龙空技术网

「Linux内核」进程0是什么?Init进程(PID=1)

嵌入式Linux内核 303

前言:

目前各位老铁们对“init进程id”可能比较关怀,我们都想要了解一些“init进程id”的相关知识。那么小编在网络上网罗了一些对于“init进程id””的相关资讯,希望同学们能喜欢,各位老铁们快快来了解一下吧!

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

Idle进程(PID = 0),本章主要讲解进程0是什么?Init进程(PID = 1),本章主要讲解进程1是什么?kthread(PID = 2),本章主要讲解进程2是什么?进程初始化(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。

更多Linux内核视频教程文档资料免费领取后台私信【内核】自行获取。

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)}
调用kernel_thread()创建1号内核线程, 该线程随后转向用户空间, 演变为init进程调用kernel_thread()创建kthreadd内核线程init_idle_bootup_task():当前0号进程init_task最终会退化成idle进程,所以这里调用init_idle_bootup_task()函数,让init_task进程隶属到idle调度类中。即选择idle的调度相关函数。调用schedule()函数切换当前进程,在调用该函数之前,Linux系统中只有两个进程,即0号进程init_task和1号进程kernel_init,其中kernel_init进程也是刚刚被创建的。调用该函数后,1号进程kernel_init将会运行调用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

在这个函数中,最终程序会掉进无限循环里 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创建,并始终运行在内核空间,负责所有内核线程的调度和管理

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

所有用户态的进程的父进程PPID=1,也就是1号进程都是他们的父进程。其中用户态的不带中括号,内核态地带中括号。其关系图如下图所示:

标签: #init进程id #init进程号