大家好,这里是物联网心球。
作为一个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头文件中,其定义如下:
structtask_struct {void *stack; /* 内核栈 *//* 进程状态相关 */volatilelong state; /* 进程状态: 0=可运行, 1=可中断睡眠, 2=不可中断睡眠等 */int exit_state; /* 退出状态: 16=僵尸, 32=死亡 */unsignedint flags; /* 进程标志: PF_KTHREAD(内核线程), PF_EXITING(退出中)等 *//* 进程标识 */pid_t pid; /* 进程ID */pid_t tgid; /* 线程组ID *//* 进程关系 */structtask_struct *real_parent;/* 真实父进程 */structtask_struct *parent;/* 父进程 */structtask_struct *group_leader;/* 线程组组长 */structlist_headchildren;/* 子进程链表 */structlist_headsibling;/* 兄弟进程链表 *//* 进程调度相关 */int prio; /* 动态优先级 */int static_prio; /* 静态优先级 */int normal_prio; /* 普通优先级 */unsignedint rt_priority; /* 实时优先级*/conststructsched_class *sched_class;/* 调度类 */structsched_entityse;/* 调度实体 */structsched_rt_entityrt;/* 实时调度实体 */unsignedint policy; /* 调度策略: SCHED_FIFO, SCHED_RR, SCHED_OTHER等 *//* 内存管理相关 */structmm_struct *mm;/* 用户态虚拟地址空间 */structmm_struct *active_mm;/* 活动虚拟地址空间 *//* 文件系统相关 */structfs_struct *fs;/* 文件系统信息 */structfiles_struct *files;/* 打开的文件描述符表 *//* 信号处理相关 */structsignal_struct *signal;/* 信号处理结构 */structsighand_struct *sighand;/* 信号处理函数 */sigset_t blocked; /* 阻塞的信号集 */structsigpendingpending;/* 待处理信号 */ ......};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),其定义如下:
structrq {unsignedint nr_running;/* CPU上可运行进程的总数 */structcfs_rqcfs;/* 完全公平调度队列(红黑树)*/structrt_rqrt;/* 实时调度的运行队列(数组)*/structdl_rqdl;/* Deadline进程的运行队列 */structtask_struct *curr;/* CPU上正在运行的进程 */structtask_struct *idle;/* CPU上idle进程 */ ......};Linux系统有多种进程调度机制,其中比较有名的有:实时调度和完全公平调度。
实时调度用于实时进程(进程优先级为:0-99)。高优先级的实时进程优先被调度,系统通过链表数组(数组下标为优先级)来实现,对应struct rq的rt成员。
完全公平调度用于普通进程(进程优先级为:100-139)。每个普通进程都会维护一个虚拟运行时间(vruntime),虚拟运行时间越小,优先被调度。系统通过红黑树来实现,对应struct rq的cfs成员。红黑树最左侧节点为虚拟运行时间最小的进程,CPU每次都选中最左侧进程运行,从而实现完全公平调度。
各位小伙伴大家好,物联网心球的第一本书《图解Linux网络编程》出版了!这是一本系统性讲解Linux网络编程的图书,通过大量手绘插图对Linux网络编程的编程知识和内核实现原理进行深入地讲解,相信你看完后一定会受益匪浅。

2.神秘的0号进程
提到Linux 0号进程,很多小伙伴会觉得很陌生。相对于大名鼎鼎的1号进程(init进程),0号进程显得十分神秘。
前面提到,Linux通过继承方式来创建进程,这句话并不适用于Linux 0号进程。0号进程是Linux内核第一个被创建的进程,也是唯一一个不通过fork()/kernel_thread()动态创建,而是在编译期就以全局变量形式写进内核镜像的进程。0号进程的名称为init_task,内核定义如下:
structtask_structinit_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所示。

user_mode_thread(kernel_init, NULL, CLONE_FS);run_init_process("/init")/init是initramfs根目录下的init脚本,该脚本中有一条命令:
exec switch_root /newroot /sbin/initswitch_root命令将kernel_init内核线程转换成/sbin/init用户进程,/sbin/init是一个软链接,它指向systemd,如下:
root@raspberrypi:/# ls -l /sbin/initlrwxrwxrwx 1 root root 20 Dec 12024 /sbin/init -> /lib/systemd/systemd当然我们可以将/sbin/init链接到不同的用户程序,来自定义1号进程。switch_root命令底层依赖execve系统调用,execve将为1号进程创建一个新的用户态虚拟地址空间(mm),然后将通过文件映射方式将systemd(ELF可执行文件)中的数据段和代码段映射至mm,kernel_init内核线程就转换成用户态systemd进程了。
总结:
如果你想学习更多的Linux技术知识,想让自己的Linux网络编程技术突飞猛进,欢迎加入我们:
