6深入理解Linux内核(第3版)

进程描述

进程管理

进程描述符task_struct进程状态

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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(取决于内核配置)。

c
/* 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;    /* 线程组链表 */

    /* ... 更多字段 ... */
};
bash
# 查看当前进程的 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中:

c
/* 进程状态定义 */
#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 → 释放
 */
c
/* 状态转换的代码示例 */

/* 进程进入可中断睡眠(等待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:

c
/* 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;      /* 当前运行的实体 */
    /* ... */
};
bash
# 查看进程树
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()系统调用创建,可以指定共享哪些资源。

c
/* 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);
c
/* 使用 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操作非常高效。

c
/* 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;
}
bash
# 查看进程和线程
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 的行为

c
/* 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 12346

3.2 分析进程的内存布局

c
/* 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

bash
# 跟踪 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的行为——子进程的修改不会影响父进程。


6/10进程描述
0%