2. Linux 进程与线程:进程控制块 PCB、线程轻量级特性、内核调度实体

在 DPDK 多核调度场景中,理解 Linux 内核如何管理进程与线程是进行任务绑定的前提。本章深入剖析进程控制块(PCB)、线程的轻量级特性以及内核调度实体(struct sched_entity)的核心机制,为后续的 CPU 亲和性设置与核间负载均衡打下坚实基础。

2.1 进程控制块(PCB)—— task_struct

Linux 内核使用 struct task_struct 来描述和管理每一个进程与线程。这个结构体是内核中最大的数据结构之一,包含了进程的所有信息,被称为进程控制块(PCB)。

核心要点: 在 Linux 中,线程被视为轻量级进程(LWP),它们与进程共享同一个 task_struct 描述符,但通过不同的标志位和资源指针来区分共享与私有资源。

PCB 的主要字段分类如下表所示:

分类 关键字段 说明
标识符 pid, tgid pid 为线程 ID,tgid 为线程组 ID(即主进程 ID)
状态 state 运行、可中断睡眠、不可中断睡眠、停止、僵尸等
调度信息 se (struct sched_entity) CFS 调度器使用的调度实体,包含虚拟运行时间等
内存管理 mm, active_mm 指向进程地址空间描述符 struct mm_struct
文件系统 fs 当前工作目录、根目录等信息
文件描述符 files 已打开文件的描述符表
信号处理 signal, sighand 信号处理函数与挂起信号
CPU 亲和性 cpus_allowed 允许该任务运行的 CPU 位掩码,DPDK 绑核的核心操作对象
DPDK 视角: 当 DPDK 通过 rte_thread_set_affinity() 或 pthread_setaffinity_np() 绑定时,实际上是在修改 task_struct 中的 cpus_allowed 位掩码,从而限制内核调度器只能将任务分配到指定的 CPU 核心上。

2.2 线程的轻量级特性

Linux 线程(POSIX 线程,pthread)的实现基于轻量级进程(LWP)概念。与传统的 fork() 创建进程相比,线程在创建和切换时开销显著降低。

线程的轻量级特性体现在以下方面:

  • 资源共享: 同一进程内的线程共享地址空间(mm 指针指向同一 mm_struct)、文件描述符表、信号处理函数等资源。
  • 创建开销小: 创建线程时不需要复制父进程的地址空间,只需分配一个新的 task_struct 和内核栈。
  • 切换速度快: 线程上下文切换仅需保存/恢复寄存器、程序计数器等少量状态,无需切换页表(TLB 刷新开销小)。
  • 私有资源: 每个线程拥有独立的 pid、内核栈、信号掩码、errno 变量和调度策略。
性能陷阱: 虽然线程切换比进程切换快,但在 DPDK 轮询模式下,应尽量避免线程切换。DPDK 通常将每个线程永久绑定到一个核心上,并采用轮询模式(无阻塞),从而将上下文切换次数降为零。

进程与线程的对比表:

对比维度 进程(fork) 线程(pthread_create)
地址空间 独立副本(写时复制) 共享同一地址空间
创建速度 慢(需复制页表) 快(仅分配 task_struct)
上下文切换 慢(TLB 刷新) 快(无需切换页表)
通信方式 IPC(管道、消息队列、共享内存) 直接读写共享内存
内核开销 高(每个进程独立 PCB) 低(共享部分内核资源)
DPDK 使用场景 多进程模型(primary/secondary) 多核分发模型(每个 lcore 一个线程)

2.3 内核调度实体(struct sched_entity)

struct sched_entity 是 CFS(完全公平调度器)的核心数据结构,它嵌入在 task_struct 的 se 字段中。调度器不直接操作 task_struct,而是通过调度实体来进行调度决策。

调度实体的关键字段:

  • load:任务的负载权重,影响 CPU 时间分配比例。
  • run_node:红黑树节点,用于将调度实体插入 CFS 运行队列的红黑树中。
  • vruntime:虚拟运行时间,CFS 通过比较 vruntime 来选择下一个运行的任务。
  • on_rq:标记该调度实体是否在运行队列上。
  • exec_start:当前任务开始执行的时间戳。
  • sum_exec_runtime:任务累计执行的实际时间。
调度流程: CFS 调度器每次选择 vruntime 最小的调度实体运行。当任务运行时,其 vruntime 以与负载权重成反比的速度增长,从而实现公平调度。

下面通过 SVG 图展示进程、线程与调度实体之间的关系:

进程、线程与内核调度实体关系图 进程 / 线程与内核调度实体关系 进程 A (PID=1000) task_struct (PCB) pid=1000, tgid=1000, state=R mm_struct (地址空间) struct sched_entity vruntime=1024, load=1024 on_rq=1, sum_exec_runtime=512ms cpus_allowed: 0x0F (CPU 0-3) 线程 (LWP) task_struct: pid=1001, tgid=1000 mm → 共享进程A的地址空间 se.vruntime=2048 (独立调度实体) 进程 B (PID=2000) task_struct (PCB) pid=2000, tgid=2000, state=S mm_struct (独立地址空间) struct sched_entity vruntime=4096, load=2048 on_rq=0, sum_exec_runtime=2048ms cpus_allowed: 0x30 (CPU 4-5) 线程 (LWP) task_struct: pid=2001, tgid=2000 mm → 共享进程B的地址空间 se.vruntime=5120 (独立调度实体) 每个 task_struct 都内嵌一个 struct sched_entity,CFS 调度器通过红黑树管理所有调度实体

2.4 调度实体与 CFS 红黑树

CFS 调度器使用红黑树来组织所有可运行的调度实体。红黑树的 key 是 vruntime,值最小的节点位于最左侧,即下一个被调度的任务。

调度实体在红黑树中的操作流程:

  1. 入队: 当任务变为可运行状态时,其调度实体被插入红黑树,位置由 vruntime 决定。
  2. 选择: 调度器每次选择红黑树最左侧的节点(vruntime 最小)作为下一个运行任务。
  3. 运行: 任务运行时,其 vruntime 随时间递增,递增速率与任务权重成反比。
  4. 出队: 任务运行完毕或主动让出 CPU 时,从红黑树中移除。
  5. 重新入队: 任务再次变为可运行状态时,重新计算 vruntime 并插入红黑树。
DPDK 优化启示: 在 DPDK 绑核场景中,每个 lcore 上的线程通常设置为 SCHED_FIFO 实时调度策略,此时 CFS 红黑树不再参与调度,而是由实时调度器直接管理。这进一步减少了调度延迟,保证了 DPDK 数据面的确定性。

2.5 本章小结

本章深入剖析了 Linux 进程与线程的内核实现:

  • PCB(task_struct) 是内核管理进程/线程的核心数据结构,包含调度、内存、文件、信号等所有信息。
  • 线程的轻量级特性 源于共享地址空间和资源,创建和切换开销远小于进程。
  • struct sched_entity 是 CFS 调度器的基本调度单位,通过 vruntime 实现公平调度。
  • cpus_allowed 位掩码是 DPDK 绑核操作的目标字段,直接限制任务可运行的 CPU 集合。

理解这些内核机制,是后续编写 CPU 亲和性脚本、实现 DPDK 多核调度优化的理论基础。