大家好,这里是物联网心球。
作为一个Linux开发者,大名鼎鼎的1号进程(init进程)我们一定都听说过。在Linux系统的进程家族中,1号进程是内核启动后创建的第一个用户进程,是所有用户进程的祖先,本文我们将揭秘1号进程的底层实现原理。
1.一张图搞懂Linux进程
在详细介绍Linux 1号进程之前,我们必须先搞清楚Linux进程相关的知识,如图1所示。

图1 Linux进程相关知识
Linux进程有两个重要的概念:task_struct结构和进程调度。
在Linux系统中,无论是进程还是线程,在内核中都统一被称为任务(task),task通过task_struct进行管理,task_struct定义在include/linux/sched.h头文件中,其定义如下:
struct task_struct {
void *stack; /* 内核栈 */
/* 进程状态相关 */
volatile long state; /* 进程状态: 0=可运行, 1=可中断睡眠, 2=不可中断睡眠等 */
int exit_state; /* 退出状态: 16=僵尸, 32=死亡 */
unsigned int flags; /* 进程标志: PF_KTHREAD(内核线程), PF_EXITING(退出中)等 */
/* 进程标识 */
pid_t pid; /* 进程ID */
pid_t tgid; /* 线程组ID */
/* 进程关系 */
struct task_struct *real_parent;/* 真实父进程 */
struct task_struct *parent;
/* 父进程 */
struct task_struct *group_leader;/* 线程组组长 */
struct list_head children;
/* 子进程链表 */
struct list_head sibling;
/* 兄弟进程链表 */
/* 进程调度相关 */
int prio; /* 动态优先级 */
int static_prio; /* 静态优先级 */
int normal_prio; /* 普通优先级 */
unsigned int rt_priority; /* 实时优先级*/
const struct sched_class *sched_class;/* 调度类 */
struct sched_entity se;
/* 调度实体 */
struct sched_rt_entity rt;/* 实时调度实体 */
unsigned int policy; /* 调度策略: SCHED_FIFO, SCHED_RR, SCHED_OTHER等 */
/* 内存管理相关 */
struct mm_struct *mm;
/* 用户态虚拟地址空间 */
struct mm_struct *active_mm;/* 活动虚拟地址空间 */
/* 文件系统相关 */
struct fs_struct *fs;
/* 文件系统信息 */
struct files_struct *files;/* 打开的文件描述符表 */
/* 信号处理相关 */
struct signal_struct *signal;
/* 信号处理结构 */
struct sighand_struct *sighand;/* 信号处理函数 */
sigset_t blocked; /* 阻塞的信号集 */
struct sigpending pending;
/* 待处理信号 */
......
};
task_struct结构体很庞大(超过200个字段),我们不需要将每个字段都弄清楚,只需要掌握一些关键字段就能对进程有比较深入的理解。
进程是代码和数据的集合,每个进程都必须存储代码和数据。对于用户进程来说,代码和数据存储在task_struct结构的mm成员(用户态虚拟地址空间),而对于内核线程来说,代码和数据存储在task_struct结构的active_mm成员。所以,用户进程和内核线程的一个区别是内核线程没有用户态虚拟地址空间(即mm=NULL)。
当进程被CPU选中时,CPU会执行进程中的代码来加工进程中的数据。由于CPU资源有限(系统中的进程数量远远大于CPU数量),所以一个CPU通常要执行多个进程的代码,这时,就产生了进程调度的问题,关于进程调度,我们稍后会讲解。CPU需要不断地切换进程,保证每个进程都能够被执行到。当一个进程被CPU切换出去又切换回来时,如何才能保证CPU从进程上一次的代码位置继续执行呢?答案是上下文切换。
上下文切换(context switch)是指CPU从一个进程切换到另一个进程时,保存当前进程的相关状态信息,并加载下一个进程的状态信息的过程,这些状态信息被称为上下文,主要包括:通用寄存器、程序计数器(PC)、栈指针(SP)、页表基址寄存器、TLB状态等。task_struct结构中需要有对应的成员来记录上下文,当进程切换出去时,将上下文信息记录下来,进程切换回来时,通过记录的上下文恢复现场,让进程继续执行。
Linux通过克隆方式来创建进程,内核会创建一个新的task_struct结构(子进程),然后将父进程的task_struct中的字段复制至子进程的task_struct来创建子进程。前面提到,task_struct结构很庞大,所以复制task_struct的过程时,需要将每个字段准确地进行复制。
接下来,我们来聊聊进程调度。进程调度的作用是确保运行在CPU上的多个进程能够相对公平地使用CPU资源。在Linux系统中,每个CPU都有一个运行队列(struct rq),其定义如下:
struct rq {
unsigned int nr_running;/* CPU上可运行进程的总数 */
struct cfs_rq cfs;
/* 完全公平调度队列(红黑树)*/
struct rt_rq rt;
/* 实时调度的运行队列(数组)*/
struct dl_rq dl;
/* Deadline进程的运行队列 */
struct task_struct *curr;/* CPU上正在运行的进程 */
struct task_struct *idle;/* CPU上idle进程 */
......
};
Linux系统有多种进程调度机制,其中比较有名的有:实时调度和完全公平调度。
实时调度用于实时进程(进程优先级为:0-99)。高优先级的实时进程优先被调度,系统通过链表数组(数组下标为优先级)来实现,对应struct rq的rt成员。
完全公平调度用于普通进程(进程优先级为:100-139)。每个普通进程都会维护一个虚拟运行时间(vruntime),虚拟运行时间越小,优先被调度。系统通过红黑树来实现,对应struct rq的cfs成员。红黑树最左侧节点为虚拟运行时间最小的进程,CPU每次都选中最左侧进程运行,从而实现完全公平调度。
2.神秘的0号进程
提到Linux 0号进程,很多小伙伴会觉得很陌生。相对于大名鼎鼎的1号进程(init进程),0号进程显得十分神秘。
前面提到,Linux通过继承方式来创建进程,这句话并不适用于Linux 0号进程。0号进程是Linux内核第一个被创建的进程,也是唯一一个不通过fork()/kernel_thread()动态创建,而是在编译期就以全局变量形式写进内核镜像的进程。0号进程的名称为init_task,内核定义如下:
struct task_struct init_task = {
.__state = 0,
.stack = init_stack,
.usage = REFCOUNT_INIT(2),
.flags = PF_KTHREAD,
.prio = MAX_PRIO - 20,
.static_prio = MAX_PRIO - 20,
.normal_prio = MAX_PRIO - 20,
.policy = SCHED_NORMAL,
.cpus_ptr = &init_task.cpus_mask,
.user_cpus_ptr = NULL,
.cpus_mask = CPU_MASK_ALL,
.nr_cpus_allowed= NR_CPUS,
.mm = NULL,
.active_mm = &init_mm,
......
};
0号进程是Linux所有进程(包括1号和2号进程)的祖先,0号进程详细介绍如图2所示。

图2 init_task进程
如果把Linux内核比作一个公司,0号进程就是公司的创始人。0号进程需要完成内核框架的搭建,让后续子孙进程有一个可靠的环境运行。内核刚启动时,CPU执行内核初始化代码,此时0号进程还躺在内核数据段,当CPU执行一条设置栈指针寄存器的指令时(即将RSP设置为init_stack,init_stack为0号进程内核栈,全局变量,在编译阶段被定义),0号进程被激活,CPU运行环境切换至0号进程。
后续的内核初始化操作都将被0号进程接管,0号进程将完成:内存管理、文件系统、设备管理、进程调度系统、中断、定时器等子系统的初始化。完成进程调度系统初始化后,CPU运行队列的idle成员将指向0号进程,0号进程将转换为idle进程。当CPU运行队列为空,即没有其他可运行的进程时,调度器会选择idle进程执行,idle进程会执行特定的低功耗指令,让CPU进入空闲状态,从而降低系统功耗。
内核初始化末期,0号进程会创建1号进程和2号进程,三者对比见表1。

表1 0号、1号和2号进程对比
1号和2号进程创建完毕后,会加入CPU进程调度系统,0号进程会启动进程调度系统,三个进程开始被调度,0号进程(idle进程)执行while循环,让CPU适当进入低功耗状态。
3.init进程的诞生
1号进程指的是init或systemd进程,新的Linux系统通常将systemd作为1号进程,1号进程启动过程如图3所示。

图3 1号进程启动过程
内核初始化阶段,0号进程执行以下代码创建1号内核线程kernel_init:
user_mode_thread(kernel_init, NULL, CLONE_FS);
此时,kernel_init还只是一个内核线程,kernel_init函数中会解压initramfs,获取一些重要的系统文件,initramfs相关知识请阅读我的这篇文章:图文详解Linux根文件系统。接着,0号进程执行以下代码将kernel_init内核线程转换为1号用户进程:
run_init_process("/init")
/init是initramfs根目录下的init脚本,该脚本中有一条命令:
exec switch_root /newroot /sbin/init
switch_root命令将kernel_init内核线程转换成/sbin/init用户进程,/sbin/init是一个软链接,它指向systemd,如下:
root@raspberrypi:/# ls -l /sbin/init
lrwxrwxrwx 1 root root 20 Dec 1 2024 /sbin/init -> /lib/systemd/systemd
当然我们可以将/sbin/init链接到不同的用户程序,来自定义1号进程。switch_root命令底层依赖execve系统调用,execve将为1号进程创建一个新的用户态虚拟地址空间(mm),然后将通过文件映射方式将systemd(ELF可执行文件)中的数据段和代码段映射至mm,kernel_init内核线程就转换成用户态systemd进程了。
