3. 进程描述符 task_struct:核心字段解析与进程组织

在 Linux 内核中,每个进程都由一个称为 进程描述符 的数据结构表示,即 struct task_struct。该结构体是内核管理进程的核心,包含了进程的所有信息:从标识符、状态、调度参数,到内存映射、文件系统、信号处理等。本章将深入解析其核心字段、进程标识符 PID/TGID 以及进程链表的组织方式。

关键概念: task_struct 是 Linux 内核中最大的数据结构之一(通常超过 2KB),它被定义在 include/linux/sched.h 头文件中。理解其字段布局是掌握进程调度、内存管理、信号处理等子系统的基础。

3.1 task_struct 核心字段解析

task_struct 包含数十个字段,按功能可划分为以下几大类。下表列出了最核心的字段及其作用:

分类 字段名 类型 说明
标识符 pid pid_t 进程 ID,唯一标识一个进程
tgid pid_t 线程组 ID,主线程的 pid 值
状态与标志 state volatile long 进程当前状态(TASK_RUNNING、TASK_INTERRUPTIBLE 等)
flags unsigned int 进程标志位(PF_EXITING、PF_FORKNOEXEC 等)
调度相关 prio int 动态优先级
static_prio int 静态优先级(由 nice 值决定)
se struct sched_entity 调度实体,CFS 调度器使用
进程关系 parent struct task_struct * 指向父进程的指针
children struct list_head 子进程链表头
内存管理 mm struct mm_struct * 进程地址空间描述符
active_mm struct mm_struct * 内核线程借用其他进程的地址空间
文件系统 fs struct fs_struct * 文件系统信息(根目录、当前工作目录等)
文件描述符 files struct files_struct * 打开的文件描述符表
信号处理 signal struct signal_struct * 信号处理相关数据
调度实体(sched_entity): 从 Linux 2.6.23 开始,CFS(完全公平调度器)不再直接操作 task_struct,而是操作内嵌的 struct sched_entity se 字段。这使得调度器可以统一调度普通进程和组调度(group scheduling)。

以下 SVG 展示了 task_struct 内部核心字段的层次关系:

task_struct 核心字段层次图 task_struct 核心字段层次 struct task_struct 标识符 pid / tgid 状态与标志 state / flags 调度相关 prio / se 进程关系 parent / children 内存管理 mm / active_mm 文件系统 fs / files 信号处理 signal 说明: • 调度实体 se 是 CFS 调度器的核心操作对象,内嵌于 task_struct 中。 • 内核线程的 mm 字段为 NULL,使用 active_mm 借用其他进程的地址空间。

3.2 进程标识符:PID 与 TGID

每个进程都有唯一的数字标识符,但 Linux 内核为了支持线程(轻量级进程),引入了两个关键字段:

  • PID(Process ID):task_struct.pid,唯一标识一个进程或线程。内核通过 pidmap_array 位图管理 PID 的分配与回收。
  • TGID(Thread Group ID):task_struct.tgid,线程组 ID。对于主线程(通常由 fork() 创建),tgid == pid;对于子线程(由 clone() 创建),tgid 等于主线程的 pid。
常见误区: 用户态通过 getpid() 系统调用获取的是 tgid,而不是 pid。内核态函数 current->pid 返回的是内核视角的线程 ID。因此,在多线程程序中,所有线程的 getpid() 返回值相同(主线程的 PID),但内核中每个线程的 pid 不同。

以下表格总结了 PID 与 TGID 的区别:

字段 内核含义 用户态对应 多线程场景
pid 内核线程 ID gettid() 系统调用 每个线程唯一
tgid 线程组 ID getpid() 系统调用 同一线程组内所有线程相同

以下 SVG 展示了 PID/TGID 在多线程进程中的关系:

PID 与 TGID 关系图 多线程进程中的 PID 与 TGID 主线程 PID=1000, TGID=1000 子线程 1 PID=1001, TGID=1000 子线程 2 PID=1002, TGID=1000 线程组:TGID = 1000(所有线程共享同一个线程组 ID) 关键点: • 用户态 getpid() 返回 TGID(1000),所有线程看到相同的值。 • 内核态 current->pid 返回内核线程 ID(1000、1001、1002),每个线程不同。

3.3 进程链表组织方式

内核需要高效地遍历所有进程。Linux 使用 双向循环链表 来组织 task_struct,链表头为 init_task(即 0 号进程 idle 的 task_struct)。每个 task_struct 内嵌一个 struct list_head tasks 节点,用于链接到全局进程链表。

除了全局链表,内核还维护了多种链表以满足不同需求:

  • 全局链表(tasks):所有进程通过 tasks 字段链接,遍历宏为 for_each_process()。
  • 父子链表:parent 指向父进程,children 是子进程链表的头。
  • 兄弟链表(sibling):同一父进程的子进程之间通过 sibling 字段链接。
  • 哈希表(pidhash):以 PID 为键的哈希表,用于快速通过 PID 查找 task_struct。
性能优化: 早期内核使用哈希表(pidhash[])加速 PID 查找。从 Linux 2.6.24 开始,内核引入了 基数树(radix tree) 管理 PID 命名空间,支持更高效的 PID 查找和命名空间隔离。

以下 SVG 展示了进程链表的组织方式:

进程链表组织方式 进程链表组织方式 init_task (PID=0) 进程 A (PID=1) 进程 B (PID=2) 进程 C (PID=3) 子进程 D (PID=4) parent 链表遍历宏: • for_each_process(p):遍历全局进程链表(从 init_task 开始)。 • list_for_each(pos, &init_task->tasks):底层链表遍历。

3.4 进程链表操作核心代码

内核通过 <linux/list.h> 提供的通用双向链表 API 操作进程链表。以下代码展示了如何遍历全局进程链表并打印 PID:

#include <linux/sched.h>
#include <linux/list.h>

void print_all_pids(void)
{
    struct task_struct *p;
    
    rcu_read_lock();
    for_each_process(p) {
        pr_info("PID: %d, TGID: %d, State: %ld\n",
                p->pid, p->tgid, p->state);
    }
    rcu_read_unlock();
}
RCU 保护: 遍历进程链表时必须使用 rcu_read_lock() / rcu_read_unlock(),因为进程链表是通过 RCU(Read-Copy Update)机制保护的,允许在遍历过程中并发创建/销毁进程。

父子进程关系操作示例:

// 遍历当前进程的所有子进程
struct task_struct *child;
struct list_head *pos;

list_for_each(pos, &current->children) {
    child = list_entry(pos, struct task_struct, sibling);
    pr_info("Child PID: %d\n", child->pid);
}

3.5 本章小结

  • task_struct 是 Linux 进程管理的核心数据结构,包含标识符、状态、调度参数、内存、文件系统等所有进程信息。
  • PID 是内核线程 ID,TGID 是线程组 ID(用户态 getpid() 的返回值)。多线程程序中,所有线程共享相同的 TGID。
  • 进程通过 双向循环链表 组织,init_task 是链表头。内核提供 for_each_process() 宏进行遍历。
  • 除了全局链表,内核还维护父子链表、兄弟链表和 PID 哈希表/基数树,以支持高效的进程查找和关系管理。