第6章:进程描述(Process Description)
一、导读
6.1 本章学习目标
第6章深入讲解Linux内核如何描述和管理进程。进程是Linux中最核心的抽象概念——每一个运行中的程序、每一个后台服务、每一个Shell会话,在内核中都用一个进程描述符(task_struct)来表示。理解进程描述符的结构和组织方式,是理解进程调度、进程间通信、内存管理等后续章节的基础。
本章的核心学习目标包括:
- 掌握task_struct的结构:进程描述符包含哪些信息,各字段的作用
- 理解进程的各种状态:TASK_RUNNING、TASK_INTERRUPTIBLE等状态的含义和转换
- 了解进程描述符的组织方式:链表、哈希表、红黑树如何组织进程
- 理解线程的实现方式:Linux中线程和进程的关系,clone系统调用
- 掌握fork和exec的实现:进程创建和程序加载的机制
- 理解写时复制(COW)的优化:fork如何避免不必要的数据复制
6.2 为什么本章如此重要
进程是操作系统调度的基本单位,是资源分配的保护边界。不理解进程描述符,就无法理解调度器如何选择下一个运行的进程,无法理解信号如何发送给特定进程,也无法理解虚拟内存如何与进程关联。
Linux的进程模型有一个独特的设计:线程就是进程。Linux没有为线程设计单独的数据结构,而是通过clone()系统调用让多个task_struct共享部分资源(如地址空间、文件表),从而实现线程。这种统一的设计简化了内核代码,但也带来了一些特殊性。
6.3 核心问题引导
在阅读本章时,请带着以下问题思考:
task_struct有多大?它包含了哪些信息?为什么需要这么多信息?
进程描述符存储在哪里?为什么存储在内核栈的底部?
进程有哪些状态?状态之间的转换条件是什么?
Linux中线程和进程的本质区别是什么?
fork()创建的子进程为什么不需要立即复制父进程的所有数据?
内核如何快速找到一个特定PID的进程?
二、核心概念详解
2.1 task_struct 结构
task_struct是Linux内核中最重要的数据结构之一,它包含了描述一个进程所需的全部信息。这个结构体定义在include/linux/sched.h中,大小约为1.7KB(取决于内核配置)。
/* task_struct 的核心字段(大幅简化) */
/* include/linux/sched.h */
struct task_struct {
/* 调度相关 */
volatile long state; /* 进程状态 */
unsigned int flags; /* 进程标志 */
int prio; /* 动态优先级 */
int static_prio; /* 静态优先级 */
int normal_prio; /* 普通优先级 */
const struct sched_class *sched_class; /* 调度类 */
struct sched_entity se; /* CFS调度实体 */
struct sched_rt_entity rt; /* 实时调度实体 */
unsigned int policy; /* 调度策略 */
int nr_cpus_allowed; /* 允许的CPU数量 */
cpumask_t cpus_ptr; /* 允许的CPU集合 */
/* 进程标识 */
pid_t pid; /* 进程ID */
pid_t tgid; /* 线程组ID(= 主线程的pid) */
struct task_struct __rcu *parent; /* 父进程 */
struct list_head children; /* 子进程链表 */
struct list_head sibling; /* 兄弟进程链表 */
/* 进程信号 */
struct signal_struct *signal; /* 共享的信号处理信息 */
struct sighand_struct *sighand; /* 信号处理函数 */
sigset_t blocked; /* 被阻塞的信号集 */
sigset_t real_blocked; /* 实际阻塞集 */
sigset_t saved_sigmask; /* 保存的信号掩码 */
struct sigpending pending; /* 待处理信号 */
/* 内存管理 */
struct mm_struct *mm; /* 内存描述符 */
struct mm_struct *active_mm; /* 活跃的内存描述符 */
/* 文件系统 */
struct fs_struct *fs; /* 文件系统信息(cwd等) */
struct files_struct *files; /* 打开的文件表 */
/* 信号处理和栈 */
unsigned long stack; /* 内核栈指针 */
/* 进程凭证 */
const struct cred __rcu *cred; /* 凭证(uid, gid等) */
/* 进程名称 */
char comm[TASK_COMM_LEN]; /* 进程名(可执行文件名) */
/* 时间统计 */
cputime_t utime; /* 用户态CPU时间 */
cputime_t stime; /* 内核态CPU时间 */
cputime_t cutime; /* 子进程用户态时间 */
cputime_t cstime; /* 子进程内核态时间 */
/* 退出状态 */
int exit_code; /* 退出码 */
int exit_state; /* 退出状态 */
/* 线程相关 */
struct thread_struct thread; /* 线程特定数据 */
struct list_head thread_group; /* 线程组链表 */
/* ... 更多字段 ... */
};# 查看当前进程的 task_struct 信息(通过 /proc)
cat /proc/self/status
# Name: cat
# Umask: 0022
# State: R (running)
# Tgid: 12345
# Pid: 12345
# PPid: 12340
# TracerPid: 0
# Uid: 1000 1000 1000 1000
# Gid: 1000 1000 1000 1000
# FDSize: 256
# VmPeak: 12345 kB
# VmSize: 12340 kB
# VmRSS: 2345 kB
# Threads: 1
# 查看进程的详细信息
cat /proc/self/stat
# 12345 (cat) R 12340 12345 12340 34816 12345 ...
# 字段含义:pid comm state ppid pgrp session tty_nr ...2.2 进程状态
Linux进程有多种状态,定义在include/linux/sched.h中:
/* 进程状态定义 */
#define TASK_RUNNING 0x0000 /* 正在运行或在运行队列中 */
#define TASK_INTERRUPTIBLE 0x0001 /* 可中断的睡眠 */
#define TASK_UNINTERRUPTIBLE 0x0002 /* 不可中断的睡眠 */
#define __TASK_STOPPED 0x0004 /* 已停止 */
#define __TASK_TRACED 0x0008 /* 被调试器跟踪 */
#define EXIT_ZOMBIE 0x0020 /* 僵尸进程 */
#define EXIT_DEAD 0x0040 /* 即将被释放 */
#define TASK_WAKEKILL 0x0080 /* 致命信号可唤醒 */
#define TASK_WAKING 0x0100 /* 正在被唤醒 */
#define TASK_PARKED 0x0200 /* 线程已停放 */
#define TASK_STATE_MAX 0x0400
/* 状态转换图:
*
* fork()
* ↓
* ┌── TASK_RUNNING ──┐
* │ (运行/就绪) │
* │ ↑ │
* schedule()│ │wake_up()
* │ ↓ │
* ┌─────────┴─── TASK_INTERRUPTIBLE
* │ │ │
* │ 信号到达│ 等待事件│
* │ │ 到达 │
* │ ↓ ↓
* │ TASK_RUNNING TASK_RUNNING
* │
* │ ┌── TASK_UNINTERRUPTIBLE
* │ │ (不可中断睡眠)
* │ │ │
* │ │ 等待事件│
* │ │ 到达 │
* │ │ ↓
* │ │ TASK_RUNNING
* │ │
* │ ├── __TASK_STOPPED (SIGSTOP)
* │ │ │
* │ │ SIGCONT│
* │ │ ↓
* │ │ TASK_RUNNING
* │ │
* └────┴── exit() → EXIT_ZOMBIE → EXIT_DEAD → 释放
*//* 状态转换的代码示例 */
/* 进程进入可中断睡眠(等待I/O) */
/* drivers/char/keyboard.c 简化 */
static ssize_t kbd_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos) {
DECLARE_WAITQUEUE(wait, current);
add_wait_queue(&kbd_wait, &wait);
for (;;) {
set_current_state(TASK_INTERRUPTIBLE);
if (kbd_data_available())
break;
if (signal_pending(current))
return -ERESTARTSYS;
schedule(); /* 让出CPU,进入睡眠 */
}
set_current_state(TASK_RUNNING);
remove_wait_queue(&wait, current);
/* 读取数据... */
return count;
}
/* 中断处理函数唤醒等待的进程 */
static irqreturn_t kbd_interrupt(int irq, void *dev_id) {
/* 处理键盘中断,将按键数据放入缓冲区 */
kbd_enqueue(scancode);
/* 唤醒等待的进程 */
wake_up_interruptible(&kbd_wait);
return IRQ_HANDLED;
}2.3 进程描述符的组织
内核需要高效地管理数千个进程。Linux使用多种数据结构组织task_struct:
/* 1. 进程链表:串联所有进程 */
/* include/linux/sched.h */
extern struct list_head init_task; /* 初始进程(PID 0) */
/* 通过 list_for_each_entry 遍历所有进程 */
/* kernel/fork.c */
struct task_struct *for_each_process(struct task_struct *p) {
/* 使用 tasks 链表遍历 */
}
/* 2. PID 哈希表:按 PID 快速查找 */
/* include/linux/pid.h */
struct pid {
atomic_t count; /* 引用计数 */
struct hlist_head tasks[PIDTYPE_MAX]; /* PID哈希链表 */
struct rcu_head rcu; /* RCU释放 */
unsigned int level; /* 命名空间层级 */
struct upid numbers[1]; /* 各命名空间的PID */
};
/* PID 哈希表查找 */
struct task_struct *find_task_by_pid_ns(pid_t nr,
struct pid_namespace *ns) {
struct pid *pid = find_pid_ns(nr, ns);
if (!pid)
return NULL;
return pid_task(pid, PIDTYPE_PID);
}
/* 3. 红黑树:按 vruntime 排序(CFS调度器使用) */
/* kernel/sched/fair.c */
struct cfs_rq {
struct rb_root tasks_timeline; /* 红黑树根 */
struct sched_entity *curr; /* 当前运行的实体 */
/* ... */
};# 查看进程树
pstree -p
# systemd(1)─┬─ModemManager(678)─┬─{ModemManager}(690)
# │ └─{ModemManager}(695)
# ├─NetworkManager(680)─┬─{NetworkManager}(710)
# │ └─{NetworkManager}(715)
# ├─sshd(1234)───sshd(5678)───bash(5679)───pstree(9999)
# └─...
# 查看进程列表
ps -ef --forest
# UID PID PPID C STIME TTY TIME CMD
# root 1 0 0 Oct01 ? 00:00:03 /sbin/init
# root 2 0 0 Oct01 ? 00:00:00 [kthreadd]
# root 3 2 0 Oct01 ? 00:00:00 \_ [ksoftirqd/0]
# ...2.4 线程的实现
Linux中线程和进程使用相同的数据结构(task_struct),区别在于共享的资源范围不同。线程通过clone()系统调用创建,可以指定共享哪些资源。
/* clone 系统调用的标志 */
/* include/uapi/linux/sched.h */
#define CSIGNAL 0x000000ff /* 退出信号 */
#define CLONE_VM 0x00000100 /* 共享内存空间 */
#define CLONE_FS 0x00000200 /* 共享文件系统信息 */
#define CLONE_FILES 0x00000400 /* 共享文件描述符表 */
#define CLONE_SIGHAND 0x00000800 /* 共享信号处理 */
#define CLONE_PIDFD 0x00001000 /* 创建PID文件描述符 */
#define CLONE_PTRACE 0x00002000 /* 跟踪子进程 */
#define CLONE_VFORK 0x00004000 /* vfork语义 */
#define CLONE_PARENT 0x00008000 /* 设置相同的父进程 */
#define CLONE_THREAD 0x00010000 /* 加入同一线程组 */
#define CLONE_NEWNS 0x00020000 /* 新的命名空间 */
#define CLONE_SYSVSEM 0x00040000 /* 共享信号量撤销值 */
#define CLONE_SETTLS 0x00080000 /* 设置TLS */
#define CLONE_PARENT_SETTID 0x00100000
#define CLONE_CHILD_CLEARTID 0x00200000
#define CLONE_DETACHED 0x00400000
#define CLONE_CHILD_SETTID 0x01000000
/*
* 不同创建方式的标志组合:
*
* fork(): 0(不共享任何资源,使用COW)
* vfork(): CLONE_VFORK | CLONE_VM | CLONE_SIGNAL
* 线程: CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD
*/
/* clone 的系统调用原型 */
/* kernel/fork.c */
long clone(unsigned long flags, void *stack,
int __user *parent_tidptr, int __user *child_tidptr,
unsigned long tls);/* 使用 clone 创建线程(简化示例) */
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024) /* 1MB 栈 */
static int thread_func(void *arg) {
printf("Thread: PID=%d, TID=%d\n", getpid(), gettid());
printf("Thread: arg = %s\n", (char *)arg);
return 42;
}
int main() {
char *stack = malloc(STACK_SIZE);
if (!stack) {
perror("malloc");
return 1;
}
/* 创建线程(共享地址空间) */
int flags = CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND |
CLONE_THREAD | SIGCHLD;
pid_t tid = clone(thread_func, stack + STACK_SIZE, flags, "hello");
if (tid < 0) {
perror("clone");
return 1;
}
printf("Main: PID=%d, created thread TID=%d\n", getpid(), tid);
/* 等待线程结束 */
/* 注意:CLONE_THREAD 的线程不能用 wait() 等待 */
sleep(1);
free(stack);
return 0;
}2.5 fork 的实现
fork()系统调用创建一个新的子进程,子进程是父进程的副本。Linux使用写时复制(COW)优化,使得fork操作非常高效。
/* fork 的实现(简化) */
/* kernel/fork.c */
SYSCALL_DEFINE0(fork) {
return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
}
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls) {
struct task_struct *p;
int nr;
/* 1. 分配新的 task_struct */
p = dup_task_struct(current); /* 复制当前进程的 task_struct */
/* 2. 复制内存地址空间(使用COW) */
p->mm = copy_mm(clone_flags, current->mm);
/* 3. 复制文件描述符表 */
p->files = copy_files(clone_flags, current->files);
/* 4. 复制信号处理 */
p->sighand = copy_sighand(clone_flags, current->sighand);
/* 5. 设置父子关系 */
p->parent = current;
p->real_parent = current;
list_add_tail_rcu(&p->sibling, &p->parent->children);
/* 6. 分配新的 PID */
p->pid = alloc_pid();
/* 7. 唤醒子进程 */
wake_up_new_task(p);
return p->pid; /* 在父进程中返回子进程PID */
/* 在子进程中返回0(通过寄存器设置) */
}
/* 复制内存描述符(COW 实现) */
struct mm_struct *copy_mm(unsigned long clone_flags,
struct mm_struct *oldmm) {
struct mm_struct *mm;
/* 如果是线程(共享地址空间),直接返回 */
if (clone_flags & CLONE_VM) {
atomic_inc(&oldmm->mm_users);
return oldmm;
}
/* 分配新的 mm_struct */
mm = allocate_mm();
if (!mm)
return NULL;
/* 复制页表(但不复制物理页面) */
/* 将父子进程的页表项都设为只读 */
/* 当任一方写入时触发缺页异常 */
/* 缺页处理函数复制物理页面 */
copy_page_range(mm, oldmm);
return mm;
}# 查看进程和线程
ps -eLf
# UID PID LWP PPID C STIME TTY TIME CMD
# root 1 1 0 0 Oct01 ? 00:00:03 /sbin/init
# root 678 678 1 0 Oct01 ? 00:00:01 /usr/sbin/ModemManager
# root 678 690 1 0 Oct01 ? 00:00:00 /usr/sbin/ModemManager
# root 678 695 1 0 Oct01 ? 00:00:00 /usr/sbin/ModemManager
# LWP (Light Weight Process) = 线程ID
# 同一个PID的多个LWP表示多线程进程
# 查看线程信息
ls /proc/self/task/
# 12345
# 查看进程的线程
ls /proc/678/task/
# 678 690 695三、实战案例
3.1 观察 fork 的行为
/* fork_demo.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int global_var = 100;
int main() {
int local_var = 200;
printf("Before fork: PID=%d, global=%d, local=%d\n",
getpid(), global_var, local_var);
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
} else if (pid == 0) {
/* 子进程 */
global_var += 10;
local_var += 20;
printf("Child: PID=%d, PPID=%d, global=%d, local=%d\n",
getpid(), getppid(), global_var, local_var);
exit(0);
} else {
/* 父进程 */
wait(NULL); /* 等待子进程结束 */
printf("Parent: PID=%d, global=%d, local=%d\n",
getpid(), global_var, local_var);
printf("Parent: child PID was %d\n", pid);
}
return 0;
}运行结果:
Before fork: PID=12345, global=100, local=200
Child: PID=12346, PPID=12345, global=110, local=220
Parent: PID=12345, global=100, local=200
Parent: child PID was 123463.2 分析进程的内存布局
/* memory_layout.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int global_init = 42; /* 数据段 */
int global_uninit; /* BSS段 */
static int static_var = 99; /* 数据段 */
void func(void) { /* 代码段 */
printf("func at %p\n", func);
}
int main(int argc, char *argv[]) {
int stack_var;
int *heap_var = malloc(1024);
char *mmap_var = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
printf("=== Process Memory Layout ===\n");
printf("Code (main): %p\n", main);
printf("Code (func): %p\n", func);
printf("Data (init): %p\n", &global_init);
printf("Data (static): %p\n", &static_var);
printf("BSS (uninit): %p\n", &global_uninit);
printf("Heap (malloc): %p\n", heap_var);
printf("Stack (local): %p\n", &stack_var);
printf("mmap region: %p\n", mmap_var);
printf("\n");
printf("PID: %d\n", getpid());
printf("PPID: %d\n", getppid());
/* 查看 /proc/self/maps */
printf("\n=== /proc/self/maps ===\n");
FILE *f = fopen("/proc/self/maps", "r");
char line[256];
while (fgets(line, sizeof(line), f))
printf("%s", line);
fclose(f);
free(heap_var);
munmap(mmap_var, 4096);
return 0;
}3.3 使用 strace 观察 fork/exec
# 跟踪 shell 执行命令的过程
strace -f -e trace=fork,vfork,clone,execve bash -c "ls /tmp" 2>&1 | head -20
# clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|...) = 12346
# [pid 12346] execve("/usr/bin/ls", ["ls", "/tmp"], ...) = 0
# [pid 12345] wait4(12346, ...四、本章小结
本章深入讲解了Linux内核的进程描述机制。task_struct是进程描述符,包含了进程的所有信息:状态、优先级、内存描述符、文件表、信号处理等。进程状态定义了进程当前的行为(运行、睡眠、停止、僵尸等),状态之间的转换由内核事件驱动。
Linux使用链表、PID哈希表和红黑树等多种数据结构组织进程,分别支持遍历、快速查找和调度排序。线程在Linux中就是共享了部分资源的进程,通过clone()系统调用创建,共享标志决定了线程和进程的区别。
fork()使用写时复制(COW)技术优化了进程创建的性能,子进程最初与父进程共享物理页面,只有在写入时才复制。这种设计使得fork操作变得轻量级,即使进程的地址空间很大。
五、思考与练习
思考题
为什么task_struct中需要同时记录pid和tgid?在单线程进程和多线程进程中,它们的关系分别是什么?
僵尸进程(Zombie)是什么?为什么需要这个状态?如何避免僵尸进程?
vfork和fork有什么区别?为什么vfork的子进程必须先exec或exit,父进程才能继续运行?
内核栈和进程是什么关系?为什么每个进程需要单独的内核栈?
编程练习
编写一个程序,使用fork()创建多个子进程,让每个子进程打印自己的PID和PPID,验证父子关系。
编写一个程序,使用clone()系统调用创建线程,共享地址空间和文件描述符表,验证线程间可以访问相同的变量和文件。
编写一个程序,在fork()前后修改一个全局变量,验证COW的行为——子进程的修改不会影响父进程。