Skip to content

进程管理

进程管理是牵涉多个 linux 的子系统的瓜田李下之地,理解进程管理需要从多个角度理解。

资源容器

进程是程序在运行中的一次实例,是系统资源分配的基本单位,是一个资源容器,容器提供了

  • 运行环境维持,依赖提供
  • 资源管理:组织、限制、隔离、保护、持久化、迁移
  • 生命周期管理,生命周期钩子函数

进程代表了一个应用层程序的实例化对象及其运行的容器。每个进程都有自己的地址空间、文件描述符表、内存映射、栈、寄存器等。在程序的执行中,系统将为进程分配 CPU 资源、为程序许诺一个独立而广阔的虚拟内存空间、并单独管理该程序的 IO 文件描述符资源等等,供程序使用。

任务

进程是一个任务,任务支持并发。并发可以允许操作系统以一定的顺序,先后执行多个不同的任务,并且每个任务仅仅只执行一个较小时间段,例如:40 ms,然后就暂停执行,切换到另一个任务上继续执行。由于这个轮换的时间很短,从而给人一种假象,多个程序仿佛是同时在运行。并发的意义在于可以让多个程序快速交替执行,从而模拟多任务同时运行的假象。

另外,并发是基于调度的。并发依赖于系统对于每个任务的时间片分配,每个任务都有机会被调度,从而完成特定的任务。任务的调度需要采用特定的规则和精心设计的算法,以保证任务的调度公平。

在 Linux 中,进程使用一个 task_struct 来描述,这也是线程的结构体定义。因为在 linux 中,进程和线程都被抽象为一个任务——可调度对象。进程和线程使用一些标志位和变量来加以识别和区分,除此之外,二者几乎一致。

进程结构体往往作为程序的主线程进行调用,在主线程中,程序可以创建额外的子线程,子线程结构体通过指针与主线程的结构体共享一些变量,从而实现在同一个进程中,不同的线程能够访问相同的资源的效果。

c
struct task_struct {
    pid_t pid;
    long state;
    struct mm_struct *mm;
    struct files_struct *files;
    struct task_struct *parent;
    struct list_head children;
    // ...
    struct sched_entity se;
    struct sched_rt_entity rt;
    // ...
};
属性进程A(主线程)线程B(属于A)
mm(内存空间)共享共享
files(文件表)共享共享
fs(工作目录)共享共享
独立独立
task_struct独立独立

内存模型

进程的内存是操作系统许诺的一个虚假空间,分为两个部分,一部分是内核空间,一部分是用户空间,内核空间预先存放了操作系统为用户提供过的一些数据和信息,用户的代码和数据存放在用户空间,用户可以在自己的空间中执行程序,同时也可以通过执行特定的指令,从而触发中断让操作系统介入,进入内核空间中去让操作系统执行内核代码,以使用内核提供的 API 执行更加底层的操作,包括:IO 操作,硬件控制,系统控制等等。

内存模型

  • 栈区,为函数调用提供局部变量空间、保存返回地址,每个线程都有自己的栈空间,内存由操作系统自动维护;
  • 堆区,为程序提供动态分配内存(如 malloc, new),内存需要程序员手动管理;

进程拓扑

在 linux 中,所有的进程都是通过其他进程 fork 而来的,由此形成了进程之间的父子派生关系。每个进程都必须要有父进程,即使你看到一个进程是完全 detached 的,其实它也会默认找 1 号进程作为父进程。使用 fork 调用可以创建子进程,子进程默认和父进程位于同一个进程组。

Linux 系统中的所有进程都可以看作一棵以 init(PID 1)为根的树状结构。每个进程都有一个父进程(parent),可以有多个子进程(children)。通过 pstree 命令可以直观地查看进程树结构。所有的进程都是通过其他进程创建而来的(除了 1 号进程),由此形成了进程之间的父子派生关系。使用 fork 系列调用可以创建子进程,子进程默认和父进程位于同一个进程组。

进程组是一个或多个相关进程的集合,通常用于信号的批量管理。每个进程组有一个唯一的进程组 ID(PGID),等于该组组长进程的 PID。进程组常用于 shell 管理前台/后台任务。

会话是一个或多个进程组的集合,通常由用户登录 shell 创建。每个会话有一个会话首进程(Session Leader),会话用于终端控制和作业管理。

孤儿进程与僵尸进程

  • 孤儿进程:父进程终止后,子进程未结束,此时子进程会被 init 进程(PID 1)收养。
  • 僵尸进程:子进程已结束,但父进程尚未调用 wait/waitpid 获取其状态,子进程会以僵尸状态保留在进程表中,直到父进程处理其退出信息。

进程在 VFS 中的虚拟文件

每个进程在 /proc/[pid]/ 下有一组虚拟文件:

文件名含义
/proc/[pid]/status进程状态和信息
/proc/[pid]/cmdline启动命令
/proc/[pid]/fd/打开的文件描述符
/proc/[pid]/maps内存映射信息

生命周期

进程具有从创建到死亡的阶段,特殊地还有睡眠阶段,

进程的生命周期

生命周期可以使用进程相关的系统调用来进行管理,以 C 函数的形式存在。具体参考系统调用

进程加载

当一个程序被执行时:

  1. 将程序的 ELF 文件(Executable and Linkable Format)内容加载到内存中
  2. 分配到进程的虚拟地址空间中的相应位置(位置分配由编译器决定)
  3. 操作系统为进程创建页表,维护虚拟内存到物理内存的映射逻辑

休眠

阻塞、空转、死锁

  • 阻塞:进程因等待 I/O、锁、信号量等进入阻塞状态,CPU 时间片被让出。
  • 空转:进程处于无意义的循环等待,浪费 CPU 资源,常见于设计缺陷或 bug。

从操作系统层面,当进程调用阻塞式系统调用时:

  1. 进程进入阻塞状态
  2. 操作系统不会把 CPU 的时间片分配给这个进程
  3. 直到调用结束,进程从阻塞状态重新进入执行状态

常见的阻塞式调用包括:

  1. 进程调度和中断

    • 进程本身的调度和中断机制通过阻塞方式实现
    • 这个过程对于进程自身来说是无感知的
  2. 阻塞式 IO 调用

    c
    // 阻塞式函数
    read(), write(), sleep(), wait()
  3. 锁和同步机制

    • 互斥锁(Mutex)
    • 信号量(Semaphore)
    c
    // 线程互斥锁
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    
    pthread_mutex_init(&mutex, NULL);
    // 尝试获取互斥锁,如果无法获取则进入阻塞状态
    pthread_mutex_lock(&mutex);
    // 开始访问被保护的资源
    // ...
    // 释放锁
    pthread_mutex_unlock(&mutex);
    
    // 信号量
    sem_t sem;
    sem_init(&sem, 0, 0); // 初始化信号量为0
    sem_wait(&sem); // 等待信号量

与阻塞不同,空转指的是进程进入无意义的空循环状态,可能通过不断检查条件来等待某个条件的达成。这可能是程序 bug 或刻意设计。在这种状态下,进程处于正常执行状态但不执行有用逻辑,导致 CPU 浪费。

死锁时,锁住的两个或多个进程会被阻塞,然后被操作系统挂起,CPU 占用为 0,这是典型的死锁特征。

死亡