前言:
目前各位老铁们对“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号进程都是他们的父进程。其中用户态的不带中括号,内核态地带中括号。其关系图如下图所示: