我们知道,对于内核提供的进程管理子系统,将来肯定是要运行各种各样的进程,对于我们做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)
}
-
- 调用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号进程都是他们的父进程。其中用户态的不带中括号,内核态的带中括号。其关系图如下图所示
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。