找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2133

积分

0

好友

300

主题
发表于 3 天前 | 查看: 6| 回复: 0

在Linux系统的进程体系中,1号进程,也就是我们常说的init进程或systemd进程,占据着至关重要的地位。它是内核启动后创建的第一个用户进程,是所有用户进程的祖先。本文将深入解析1号进程的底层实现原理。

一张图搞懂Linux进程

在深入探讨Linux 1号进程之前,我们有必要先厘清与Linux进程相关的核心知识,如下图所示。

Linux进程状态与调度核心关系图

理解Linux进程有两个关键切入点:task_struct 结构体和进程调度机制。

在Linux中,无论是进程还是线程,在内核中都被统一抽象为任务(task),每个任务都由一个 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_structmm 成员所指向的用户态虚拟地址空间中。而对于内核线程,由于其运行在内核态,没有独立的用户空间,因此其 mm 成员通常为 NULL,代码和数据通过 active_mm 来管理。这也是用户进程与内核线程的一个重要区别。

当进程被CPU选中执行时,CPU运行其代码并处理其数据。由于系统进程数量远多于CPU核心数,一个CPU需要轮流执行多个进程,这就引出了进程调度的问题。为了在进程被切换出去又切换回来时能无缝衔接,系统需要进行上下文切换。

上下文切换指的是CPU从一个进程切换到另一个进程时,保存当前进程的状态信息(上下文),并加载下一个进程状态信息的过程。这些上下文信息主要包括通用寄存器、程序计数器(PC)、栈指针(SP)等。task_struct 结构中有相应的成员来保存这些信息,确保进程恢复运行时能从上一次中断的位置继续执行。

Linux采用克隆的方式来创建新进程。内核会新建一个子进程的 task_struct 结构,然后将父进程 task_struct 中的关键信息复制过去。正因为 task_struct 结构复杂,这个复制过程必须精确无误。

接下来我们聊聊进程调度。它的核心目标是让多个进程能够相对公平地共享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系统实现了多种调度机制,其中最为人熟知的是实时调度完全公平调度(CFS)

  • 实时调度用于优先级为0-99的实时进程。它采用“优先级越高,越先被调度”的策略,通过一个链表数组(数组下标对应优先级)来实现,对应 struct rq 中的 rt 成员。
  • 完全公平调度用于优先级为100-139的普通进程。它为每个进程维护一个虚拟运行时间(vruntime),vruntime 越小的进程越优先被调度。CFS使用红黑树来组织进程,vruntime 最小的进程位于树的最左侧,调度器每次都选择它来运行,从而在宏观上实现公平,这对应 struct rq 中的 cfs 成员。

神秘的0号进程

相对于大名鼎鼎的1号进程,0号进程对许多开发者而言显得有些陌生。它之所以神秘,是因为其创建方式与众不同。

前文提到Linux通过克隆创建进程,但这并不适用于0号进程。0号进程是Linux内核中第一个被创建的进程,也是唯一一个不是通过 fork()kernel_thread() 动态创建,而是在编译期就以全局变量形式写入内核镜像的进程。它的名称是 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号进程是所有进程(包括1号和2号进程)的祖先。可以将其比作公司的创始人,它需要搭建好整个内核的基本框架,为后续进程提供可靠的运行环境。其详细角色与启动流程如下图所示。

Linux内核启动与0号进程初始化流程图

内核启动初期,CPU执行初始化汇编代码,此时0号进程 init_task 还只是数据段中的一个静态变量。当CPU执行一条设置栈指针寄存器(RSP)的指令,将其指向 init_stack(0号进程的内核栈,同样是编译期定义的全局变量)时,0号进程便被“激活”,CPU的运行环境也切换到了0号进程。

此后,内核的初始化工作便由0号进程接管。它会逐一完成内存管理、文件系统、设备管理、进程调度系统、中断、定时器等核心子系统的初始化。待进程调度系统初始化完成后,CPU运行队列的 idle 成员会指向0号进程,0号进程随即转变为 idle 进程。当运行队列中没有其他可运行的任务时,调度器就会选择 idle 进程来运行,它会执行特定的低功耗指令,让CPU进入空闲状态以节省能耗。

在内核初始化的最后阶段,0号进程会创建出1号进程和2号进程。这三个“始祖”进程的对比如下表所示。

Linux系统0、1、2号进程核心职责对比表

1号和2号进程创建完毕后,会被加入到CPU的进程调度系统中。随后,0号进程启动调度系统,三个进程开始被调度器管理。而0号进程(此时已是 idle 进程)则进入一个循环,在CPU空闲时管理其低功耗状态。这一系列精密的初始化步骤,是理解操作系统启动和进程管理的基础。

init进程的诞生

如今,大多数现代Linux发行版都将 systemd 作为1号进程。1号进程从内核线程蜕变为用户进程的完整过程,可以用下图清晰地展示。

1号进程从内核线程到用户进程的启动与内存布局图

在内核初始化的特定阶段,0号进程会执行以下代码,创建出名为 kernel_init 的1号内核线程:

user_mode_thread(kernel_init, NULL, CLONE_FS);

此时的 kernel_init 还是一个纯粹的内核线程。在 kernel_init 函数内部,它会解压 initramfs(初始内存文件系统),获取启动用户空间所必需的关键系统文件和驱动。接着,0号进程(实际上是 kernel_init 线程自身在执行)会尝试执行以下代码,尝试将内核线程转变为用户进程:

run_init_process("/init");

这里的 /initinitramfs 根目录下的一个脚本文件。该脚本中通常包含一条关键命令:

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

当然,管理员也可以修改这个链接,将其指向其他程序,从而实现自定义1号进程。switch_root 命令底层依赖于 execve 系统调用。execve 会为1号进程创建一个全新的用户态虚拟地址空间(mm),然后通过文件映射的方式,将 systemd 这个ELF可执行文件的数据段和代码段映射到该地址空间中。至此,kernel_init 内核线程便成功蜕变为运行在用户态的 systemd 进程,肩负起启动和管理所有其他用户进程的重任。这个过程深刻地体现了计算机科学中关于进程创建和执行的底层原理。

希望这篇关于Linux 1号进程的深度解析能对你有所帮助。如果你想持续探索更多类似的技术内核知识,欢迎关注云栈社区的后续内容。




上一篇:C++内存泄漏检测实战:无工具环境下的手动方案与应急排查
下一篇:NextUI零体积React UI库深度解析:基于RSC技术如何提升前端性能
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-14 18:38 , Processed in 0.239003 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表