Linux 系统编程 · 第 8 章:进程基础

Linux 系统编程 · 第 8 章:进程基础

本章深入讲解 Linux 进程的核心概念:进程的本质与组成、进程控制块(PCB)、进程状态机、进程标识符体系、进程环境,以及进程的创建与终止机制。


目录

  1. 进程的本质与组成

  2. 进程控制块(PCB)

  3. 进程标识符体系

  4. 进程状态机

  5. 进程环境

  6. 进程的内存布局

  7. 进程的创建:fork

  8. 进程的终止:exit

  9. 综合实践


1. 进程的本质与组成

1.1 程序 vs 进程

复制代码
 程序(Program):
  静态的可执行文件,存储在磁盘上
  是指令和数据的集合(ELF 文件)
 ​
 进程(Process):
  程序的一次动态执行实例
  是操作系统进行资源分配和调度的基本单位
 ​
 ─────────────────────────────────────────────────────────────
  磁盘上的程序(/usr/bin/ls)
        │
        │ exec() 加载
        ▼
  内存中的进程(ls 的一次运行)
  ┌─────────────────────────────────────────────────────┐
  │ 代码段(Text)   ← 程序指令(只读,可共享)         │
  │ 数据段(Data)   ← 已初始化的全局/静态变量         │
  │ BSS 段           ← 未初始化的全局/静态变量(清零) │
  │ 堆(Heap)       ← 动态分配内存(malloc/new)       │
  │ 栈(Stack)     ← 函数调用栈、局部变量、参数       │
  │ 内核空间         ← PCB、页表、内核栈(用户不可见) │
  └─────────────────────────────────────────────────────┘
 ​
 关键区别:
  ① 同一程序可以有多个进程实例(如多个 bash 进程)
  ② 进程有独立的地址空间(虚拟内存隔离)
  ③ 进程有生命周期(创建→运行→终止)
  ④ 进程是资源的容器(fd、内存、信号处理等)
 ─────────────────────────────────────────────────────────────

1.2 进程的组成要素

复制代码
 /* 文件名:process_info.c
  * 展示进程的各种组成要素和基本信息
  */
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
 #include <sys/types.h>
 #include <sys/resource.h>
 #include <sys/time.h>
 #include <time.h>
 #include <fcntl.h>
 #include <dirent.h>
 ​
 /* 打印进程基本信息 */
 void print_process_info(void) {
     printf("=== 进程基本信息 ===\n\n");
 ​
     /* 进程标识 */
     printf("【进程标识】\n");
     printf(" PID (进程ID):         %d\n", getpid());
     printf(" PPID (父进程ID):       %d\n", getppid());
     printf(" PGID (进程组ID):       %d\n", getpgrp());
     printf(" SID (会话ID):         %d\n", getsid(0));
 ​
     /* 用户标识 */
     printf("\n【用户标识】\n");
     printf(" UID (真实用户ID):     %u\n", getuid());
     printf(" EUID (有效用户ID):     %u\n", geteuid());
     printf(" GID (真实组ID):       %u\n", getgid());
     printf(" EGID (有效组ID):       %u\n", getegid());
 ​
     /* 进程优先级 */
     printf("\n【调度信息】\n");
     int prio = getpriority(PRIO_PROCESS, 0);
     printf(" Nice 值:               %d(范围 -20~19,越小优先级越高)\n", prio);
 ​
     /* 资源限制 */
     printf("\n【资源限制(部分)】\n");
     struct rlimit rl;
     getrlimit(RLIMIT_NOFILE, &rl);
     printf(" 最大打开文件数:         %lu(软)/ %lu(硬)\n",
            (unsigned long)rl.rlim_cur,
            rl.rlim_max == RLIM_INFINITY ? (unsigned long)-1
                                        : (unsigned long)rl.rlim_max);
     getrlimit(RLIMIT_STACK, &rl);
     printf(" 栈大小限制:             %lu KB\n",
            (unsigned long)rl.rlim_cur / 1024);
     getrlimit(RLIMIT_AS, &rl);
     printf(" 虚拟内存限制:           %s\n",
            rl.rlim_cur == RLIM_INFINITY ? "无限制" : "有限制");
 ​
     /* 当前工作目录 */
     char cwd[1024];
     getcwd(cwd, sizeof(cwd));
     printf("\n【文件系统】\n");
     printf(" 当前工作目录:           %s\n", cwd);
 ​
     /* 打开的文件描述符数量 */
     int fd_count = 0;
     DIR *fd_dir = opendir("/proc/self/fd");
     if (fd_dir) {
         struct dirent *entry;
         while ((entry = readdir(fd_dir)) != NULL) {
             if (entry->d_name[0] != '.') fd_count++;
        }
         closedir(fd_dir);
    }
     printf(" 当前打开的 fd 数量:     %d\n", fd_count);
 }
 ​
 /* 读取 /proc/self/status 获取进程状态 */
 void print_proc_status(void) {
     printf("\n=== /proc/self/status 关键字段 ===\n\n");
 ​
     FILE *fp = fopen("/proc/self/status", "r");
     if (!fp) { perror("fopen"); return; }
 ​
     char line[256];
     const char *keys[] = {
         "Name", "State", "Pid", "PPid", "Threads",
         "VmSize", "VmRSS", "VmStk", "VmExe", "VmLib",
         "FDSize", "voluntary_ctxt_switches",
         "nonvoluntary_ctxt_switches", NULL
    };
 ​
     while (fgets(line, sizeof(line), fp)) {
         for (int i = 0; keys[i]; i++) {
             if (strncmp(line, keys[i], strlen(keys[i])) == 0) {
                 printf(" %s", line);
                 break;
            }
        }
    }
     fclose(fp);
 }
 ​
 /* 读取 /proc/self/stat 获取调度信息 */
 void print_proc_stat(void) {
     printf("\n=== /proc/self/stat 调度信息 ===\n\n");
 ​
     FILE *fp = fopen("/proc/self/stat", "r");
     if (!fp) { perror("fopen"); return; }
 ​
     int pid;
     char comm[256], state;
     int ppid, pgrp, session;
     long utime, stime;
     long priority, nice;
     long num_threads;
 ​
     fscanf(fp, "%d %s %c %d %d %d %*d %*d %*u %*u %*u %*u %*u "
                "%ld %ld %*d %*d %ld %ld %ld",
            &pid, comm, &state, &ppid, &pgrp, &session,
            &utime, &stime, &priority, &nice, &num_threads);
     fclose(fp);
 ​
     printf(" PID:         %d\n", pid);
     printf(" 命令名:       %s\n", comm);
     printf(" 状态:         %c\n", state);
     printf(" PPID:         %d\n", ppid);
     printf(" 进程组:       %d\n", pgrp);
     printf(" 会话:         %d\n", session);
     printf(" 用户态时间:   %ld jiffies\n", utime);
     printf(" 内核态时间:   %ld jiffies\n", stime);
     printf(" 优先级:       %ld\n", priority);
     printf(" Nice 值:     %ld\n", nice);
     printf(" 线程数:       %ld\n", num_threads);
 }
 ​
 int main(void) {
     print_process_info();
     print_proc_status();
     print_proc_stat();
     return 0;
 }
复制代码
 gcc -o process_info process_info.c
 ./process_info
 # 输出示例:
 # === 进程基本信息 ===
 #
 # 【进程标识】
 #   PID (进程ID):         12345
 #   PPID (父进程ID):       12344
 #   PGID (进程组ID):       12345
 #   SID (会话ID):         12340
 #
 # 【用户标识】
 #   UID (真实用户ID):     1000
 #   EUID (有效用户ID):     1000
 #
 # 【调度信息】
 #   Nice 值:               0(范围 -20~19,越小优先级越高)

2. 进程控制块(PCB)

2.1 PCB 的概念与内容

复制代码
 进程控制块(PCB,Process Control Block):
  Linux 内核中用 task_struct 结构体表示
  是内核管理进程的核心数据结构
 ​
 task_struct 的主要字段(简化版):
 ─────────────────────────────────────────────────────────────
  【进程标识】
    pid_t pid         进程 ID
    pid_t tgid         线程组 ID(主线程 = 进程 ID)
    struct task_struct *parent 父进程指针
    struct list_head children   子进程链表
    struct list_head sibling   兄弟进程链表
 ​
  【进程状态】
    volatile long state   进程状态(TASK_RUNNING 等)
    int exit_state       退出状态
    int exit_code         退出码
 ​
  【调度信息】
    int prio             动态优先级
    int static_prio       静态优先级(nice 值转换)
    unsigned int policy   调度策略(SCHED_NORMAL 等)
    struct sched_entity se CFS 调度实体
    u64 utime, stime     用户态/内核态 CPU 时间
 ​
  【内存管理】
    struct mm_struct *mm     用户空间内存描述符
    struct mm_struct *active_mm 当前活跃内存描述符
 ​
  【文件系统】
    struct fs_struct *fs     文件系统信息(根目录、工作目录)
    struct files_struct *files 打开文件表
 ​
  【信号处理】
    struct signal_struct *signal   信号处理信息
    struct sighand_struct *sighand 信号处理函数表
    sigset_t blocked             阻塞信号集
    sigset_t pending             待处理信号集
 ​
  【凭证(权限)】
    const struct cred *cred   进程凭证(UID/GID/capabilities)
 ​
  【时间信息】
    struct timespec64 start_time 进程启动时间
 ─────────────────────────────────────────────────────────────

2.2 通过 /proc 访问 PCB 信息

复制代码
 /* 文件名:pcb_info.c
  * 通过 /proc 文件系统读取进程的 PCB 信息
  */
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
 #include <fcntl.h>
 #include <dirent.h>
 #include <sys/stat.h>
 #include <pwd.h>
 #include <grp.h>
 #include <time.h>
 ​
 /* 读取 /proc/[pid]/status 的指定字段 */
 int read_proc_field(pid_t pid, const char *field,
                     char *value, size_t value_size) {
     char path[64];
     snprintf(path, sizeof(path), "/proc/%d/status", pid);
 ​
     FILE *fp = fopen(path, "r");
     if (!fp) return -1;
 ​
     char line[256];
     int found = 0;
     while (fgets(line, sizeof(line), fp)) {
         if (strncmp(line, field, strlen(field)) == 0) {
             /* 跳过字段名和冒号后的空白 */
             char *val = line + strlen(field);
             while (*val == ':' || *val == ' ' || *val == '\t') val++;
             /* 去掉末尾换行 */
             size_t len = strlen(val);
             if (len > 0 && val[len-1] == '\n') val[len-1] = '\0';
             strncpy(value, val, value_size - 1);
             found = 1;
             break;
        }
    }
     fclose(fp);
     return found ? 0 : -1;
 }
 ​
 /* 打印进程的完整 PCB 摘要 */
 void print_pcb_summary(pid_t pid) {
     char value[256];
     char path[64];
 ​
     printf("╔══════════════════════════════════════════════╗\n");
     printf("║ 进程 PCB 摘要(PID = %d)\n", pid);
     printf("╠══════════════════════════════════════════════╣\n");
 ​
     /* 进程名 */
     if (read_proc_field(pid, "Name", value, sizeof(value)) == 0)
         printf("║ 进程名:     %-32s║\n", value);
 ​
     /* 状态 */
     if (read_proc_field(pid, "State", value, sizeof(value)) == 0)
         printf("║ 状态:       %-32s║\n", value);
 ​
     /* PID/PPID */
     if (read_proc_field(pid, "Pid", value, sizeof(value)) == 0)
         printf("║ PID:       %-32s║\n", value);
     if (read_proc_field(pid, "PPid", value, sizeof(value)) == 0)
         printf("║ PPID:       %-32s║\n", value);
 ​
     /* 线程数 */
     if (read_proc_field(pid, "Threads", value, sizeof(value)) == 0)
         printf("║ 线程数:     %-32s║\n", value);
 ​
     /* 内存信息 */
     printf("╠══════════════════════════════════════════════╣\n");
     if (read_proc_field(pid, "VmSize", value, sizeof(value)) == 0)
         printf("║ 虚拟内存:   %-32s║\n", value);
     if (read_proc_field(pid, "VmRSS", value, sizeof(value)) == 0)
         printf("║ 物理内存:   %-32s║\n", value);
     if (read_proc_field(pid, "VmStk", value, sizeof(value)) == 0)
         printf("║ 栈大小:     %-32s║\n", value);
     if (read_proc_field(pid, "VmExe", value, sizeof(value)) == 0)
         printf("║ 代码段:     %-32s║\n", value);
 ​
     /* 用户信息 */
     printf("╠══════════════════════════════════════════════╣\n");
     if (read_proc_field(pid, "Uid", value, sizeof(value)) == 0) {
         unsigned int ruid, euid, suid, fsuid;
         sscanf(value, "%u %u %u %u", &ruid, &euid, &suid, &fsuid);
         struct passwd *pw = getpwuid(ruid);
         printf("║ 用户:       %-32s║\n",
                pw ? pw->pw_name : "unknown");
         printf("║ UID(R/E/S): %-32s║\n", value);
    }
 ​
     /* 文件描述符数 */
     snprintf(path, sizeof(path), "/proc/%d/fd", pid);
     DIR *fd_dir = opendir(path);
     int fd_count = 0;
     if (fd_dir) {
         struct dirent *entry;
         while ((entry = readdir(fd_dir)) != NULL)
             if (entry->d_name[0] != '.') fd_count++;
         closedir(fd_dir);
    }
     printf("║ 打开 fd 数: %-32d║\n", fd_count);
 ​
     /* 可执行文件路径 */
     snprintf(path, sizeof(path), "/proc/%d/exe", pid);
     char exe_path[256] = {0};
     readlink(path, exe_path, sizeof(exe_path) - 1);
     printf("╠══════════════════════════════════════════════╣\n");
     printf("║ 可执行文件: %-32s║\n", exe_path);
 ​
     /* 命令行 */
     snprintf(path, sizeof(path), "/proc/%d/cmdline", pid);
     FILE *fp = fopen(path, "r");
     if (fp) {
         char cmdline[256] = {0};
         fread(cmdline, 1, sizeof(cmdline) - 1, fp);
         fclose(fp);
         /* cmdline 中参数用 \0 分隔,替换为空格 */
         for (int i = 0; i < 255 && cmdline[i]; i++)
             if (cmdline[i] == '\0') cmdline[i] = ' ';
         printf("║ 命令行:     %-32.32s║\n", cmdline);
    }
 ​
     printf("╚══════════════════════════════════════════════╝\n");
 }
 ​
 int main(int argc, char *argv[]) {
     pid_t pid = (argc > 1) ? atoi(argv[1]) : getpid();
 ​
     print_pcb_summary(pid);
 ​
     /* 列出系统中所有进程(遍历 /proc)*/
     printf("\n=== 系统进程列表(前15个)===\n");
     printf("%-8s %-8s %-6s %-8s %s\n",
            "PID", "PPID", "状态", "内存(KB)", "进程名");
     printf("%s\n", "─────────────────────────────────────────────");
 ​
     DIR *proc_dir = opendir("/proc");
     if (!proc_dir) { perror("opendir /proc"); return 1; }
 ​
     struct dirent *entry;
     int count = 0;
 ​
     while ((entry = readdir(proc_dir)) != NULL && count < 15) {
         /* /proc 下的数字目录就是进程 */
         pid_t p = atoi(entry->d_name);
         if (p <= 0) continue;
 ​
         char name[64] = {0}, state[32] = {0};
         char ppid_str[16] = {0}, vmrss[32] = {0};
 ​
         read_proc_field(p, "Name",  name,     sizeof(name));
         read_proc_field(p, "State", state,    sizeof(state));
         read_proc_field(p, "PPid",  ppid_str, sizeof(ppid_str));
         read_proc_field(p, "VmRSS", vmrss,    sizeof(vmrss));
 ​
         /* 提取状态字符 */
         char state_char = state[0] ? state[0] : '?';
 ​
         /* 提取内存数值 */
         long mem_kb = 0;
         sscanf(vmrss, "%ld", &mem_kb);
 ​
         printf("%-8d %-8s %-6c %-8ld %s\n",
                p, ppid_str, state_char, mem_kb, name);
         count++;
    }
     closedir(proc_dir);
 ​
     return 0;
 }
复制代码
 gcc -o pcb_info pcb_info.c
 ./pcb_info
 # 输出示例:
 # ╔══════════════════════════════════════════════╗
 # ║ 进程 PCB 摘要(PID = 12345)
 # ╠══════════════════════════════════════════════╣
 # ║ 进程名:     pcb_info                       ║
 # ║ 状态:       S (sleeping)                   ║
 # ║ PID:       12345                           ║
 # ║ PPID:       12344                           ║
 # ║ 线程数:     1                               ║
 # ╠══════════════════════════════════════════════╣
 # ║ 虚拟内存:   4096 kB                         ║
 # ║ 物理内存:   1024 kB                         ║

3. 进程标识符体系

3.1 PID、PPID、PGID、SID

复制代码
 Linux 进程标识符层次结构:
 ─────────────────────────────────────────────────────────────
  会话(Session)
  │ SID = 会话领导进程的 PID
  │
  ├── 进程组(Process Group)
  │   │ PGID = 进程组领导进程的 PID
  │   │
  │   ├── 进程(Process)
  │   │   │ PID = 进程自身的 ID
  │   │   │ PPID = 父进程的 PID
  │   │   │
  │   │   └── 线程(Thread)
  │   │       TID = 线程 ID(主线程 TID = PID)
  │   │       TGID = 线程组 ID(= 进程 PID)
  │   │
  │   └── 进程(Process)...
  │
  └── 进程组(Process Group)...
 ​
 实际例子(Shell 中执行 ls | grep txt):
  会话(SID=1000,bash 进程)
  └── 进程组(PGID=2000,管道命令组)
      ├── ls   进程(PID=2000,PPID=1000)
      └── grep 进程(PID=2001,PPID=1000)
 ─────────────────────────────────────────────────────────────
复制代码
 /* 文件名:pid_hierarchy.c
  * 演示进程标识符的层次结构
  */
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
 #include <sys/types.h>
 #include <sys/wait.h>
 #include <sys/syscall.h>
 ​
 /* 获取线程 ID(gettid 在 glibc 2.30+ 才有封装)*/
 static pid_t get_tid(void) {
     return (pid_t)syscall(SYS_gettid);
 }
 ​
 void print_ids(const char *label) {
     printf("【%s】\n", label);
     printf(" PID = %-6d (本进程 ID)\n",       getpid());
     printf(" PPID = %-6d (父进程 ID)\n",       getppid());
     printf(" PGID = %-6d (进程组 ID)\n",       getpgrp());
     printf(" SID = %-6d (会话 ID)\n",         getsid(0));
     printf(" TID = %-6d (线程 ID,主线程=PID)\n", get_tid());
     printf(" UID = %-6d EUID = %d\n",           getuid(), geteuid());
     printf("\n");
 }
 ​
 int main(void) {
     printf("=== 进程标识符层次结构演示 ===\n\n");
 ​
     print_ids("主进程");
 ​
     /* ── fork 后子进程的标识符 ── */
     pid_t child1 = fork();
     if (child1 == 0) {
         printf("─────────────────────────────────\n");
         print_ids("子进程1(fork 后)");
         /*
          * 子进程继承父进程的 PGID 和 SID
          * 但有自己的 PID,PPID = 父进程 PID
          */
         _exit(0);
    }
     waitpid(child1, NULL, 0);
 ​
     /* ── 创建新进程组 ── */
     pid_t child2 = fork();
     if (child2 == 0) {
         /* setpgid(0, 0):将自己设为新进程组的领导 */
         setpgid(0, 0);
         printf("─────────────────────────────────\n");
         print_ids("子进程2(新进程组领导)");
         /* 注意:PGID 现在等于自己的 PID */
         _exit(0);
    }
     waitpid(child2, NULL, 0);
 ​
     /* ── 创建新会话 ── */
     pid_t child3 = fork();
     if (child3 == 0) {
         /* setsid():创建新会话,自己成为会话领导
          * 前提:调用者不能是进程组领导
          */
         pid_t new_sid = setsid();
         printf("─────────────────────────────────\n");
         print_ids("子进程3(新会话领导)");
         printf(" 新 SID = %d(= 自己的 PID)\n\n", new_sid);
         _exit(0);
    }
     waitpid(child3, NULL, 0);
 ​
     /* ── PID 分配规律 ── */
     printf("=== PID 分配规律 ===\n\n");
     printf("PID 范围: 1 ~ %d\n", (int)sysconf(_SC_PID_MAX));
     printf("PID 1: init/systemd(所有进程的祖先)\n");
     printf("PID 2: kthreadd(内核线程的父进程)\n");
     printf("PID 分配: 从上次分配的 PID 开始递增,到最大值后回绕\n\n");
 ​
     /* 查看 PID 最大值 */
     FILE *fp = fopen("/proc/sys/kernel/pid_max", "r");
     if (fp) {
         int pid_max;
         fscanf(fp, "%d", &pid_max);
         fclose(fp);
         printf("/proc/sys/kernel/pid_max = %d\n", pid_max);
    }
 ​
     return 0;
 }
复制代码
gcc -o pid_hierarchy pid_hierarchy.c
./pid_hierarchy
# 输出示例:
# === 进程标识符层次结构演示 ===
#
# 【主进程】
#   PID  = 12345   (本进程 ID)
#   PPID = 12344   (父进程 ID)
#   PGID = 12345   (进程组 ID)
#   SID  = 12340   (会话 ID)
#   TID  = 12345   (线程 ID,主线程=PID)
#
# 【子进程1(fork 后)】
#   PID  = 12346   (本进程 ID)
#   PPID = 12345   (父进程 ID)← 父进程的 PID
#   PGID = 12345   (进程组 ID)← 继承父进程的 PGID
#   SID  = 12340   (会话 ID)  ← 继承父进程的 SID
#
# 【子进程2(新进程组领导)】
#   PID  = 12347
#   PGID = 12347   ← PGID = 自己的 PID(新进程组)
#
# 【子进程3(新会话领导)】
#   PID  = 12348
#   SID  = 12348   ← SID = 自己的 PID(新会话)

3.2 特殊进程

复制代码
/* 文件名:special_procs.c
 * 演示特殊进程:init、孤儿进程、僵尸进程
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>

int main(void) {
    /* ── 孤儿进程(Orphan Process)── */
    printf("=== 孤儿进程演示 ===\n\n");
    /*
     * 孤儿进程:父进程先于子进程退出
     * 子进程被 init(PID=1)或 subreaper 收养
     * 孤儿进程不会变成僵尸(init 会 wait 它们)
     */
    pid_t child = fork();
    if (child == 0) {
        /* 子进程:等待父进程退出后观察 PPID 变化 */
        printf("子进程 PID=%d,初始 PPID=%d\n", getpid(), getppid());
        sleep(2);   /* 等待父进程退出 */
        printf("子进程 PID=%d,父进程退出后 PPID=%d(被 init/subreaper 收养)\n",
               getpid(), getppid());
        _exit(0);
    }

    /* 父进程立即退出,子进程变为孤儿 */
    printf("父进程 PID=%d 即将退出,子进程 PID=%d 将成为孤儿\n",
           getpid(), child);
    sleep(1);
    /* 父进程退出,不等待子进程 */
    printf("父进程退出\n\n");

    /* ── 僵尸进程(Zombie Process)── */
    printf("=== 僵尸进程演示 ===\n\n");
    /*
     * 僵尸进程:子进程已退出,但父进程未调用 wait
     * 子进程的 PCB 仍保留在内核中(保存退出状态)
     * 状态显示为 Z(zombie)
     * 危害:占用 PID,大量僵尸进程会耗尽 PID 空间
     */
    pid_t zombie_child = fork();
    if (zombie_child == 0) {
        printf("子进程 PID=%d 即将退出(变为僵尸)\n", getpid());
        _exit(42);   /* 退出,但父进程不立即 wait */
    }

    /* 父进程故意不立即 wait,让子进程变为僵尸 */
    sleep(1);

    /* 查看僵尸进程状态 */
    char cmd[64];
    snprintf(cmd, sizeof(cmd),
             "ps -p %d -o pid,ppid,stat,comm 2>/dev/null", zombie_child);
    printf("僵尸进程状态(ps 输出):\n");
    system(cmd);

    /* 通过 /proc 查看 */
    char proc_path[64];
    snprintf(proc_path, sizeof(proc_path),
             "/proc/%d/status", zombie_child);
    FILE *fp = fopen(proc_path, "r");
    if (fp) {
        char line[128];
        while (fgets(line, sizeof(line), fp)) {
            if (strncmp(line, "State:", 6) == 0) {
                printf("僵尸进程 /proc 状态: %s", line);
                break;
            }
        }
        fclose(fp);
    }

    /* 父进程调用 wait 回收僵尸 */
    int status;
    pid_t reaped = waitpid(zombie_child, &status, 0);
    printf("waitpid 回收僵尸进程 PID=%d,退出码=%d\n\n",
           reaped, WEXITSTATUS(status));

    /* ── 防止僵尸进程的方法 ── */
    printf("=== 防止僵尸进程的方法 ===\n\n");
    printf("方法1: 父进程调用 wait/waitpid 回收子进程\n");
    printf("方法2: 忽略 SIGCHLD 信号(内核自动回收)\n");
    printf("方法3: 双重 fork(子进程再 fork,孙进程被 init 收养)\n");
    printf("方法4: 注册 SIGCHLD 处理函数,在其中调用 waitpid\n\n");

    /* 演示方法2:忽略 SIGCHLD */
    signal(SIGCHLD, SIG_IGN);   /* 忽略 SIGCHLD,子进程自动回收 */
    pid_t auto_child = fork();
    if (auto_child == 0) {
        _exit(0);
    }
    sleep(1);
    /* 检查子进程是否已被自动回收 */
    snprintf(cmd, sizeof(cmd),
             "ls /proc/%d 2>/dev/null | wc -l", auto_child);
    printf("忽略 SIGCHLD 后,子进程 /proc/%d 是否存在: ",
           auto_child);
    fflush(stdout);
    system(cmd);

    return 0;
}
复制代码
gcc -o special_procs special_procs.c
./special_procs
# 输出示例:
# === 孤儿进程演示 ===
# 子进程 PID=12346,初始 PPID=12345
# 父进程 PID=12345 即将退出,子进程 PID=12346 将成为孤儿
# 父进程退出
# 子进程 PID=12346,父进程退出后 PPID=1(被 init/subreaper 收养)
#
# === 僵尸进程演示 ===
# 子进程 PID=12347 即将退出(变为僵尸)
# 僵尸进程状态(ps 输出):
#   PID  PPID STAT COMMAND
# 12347 12345 Z+   zombie_child
# 僵尸进程 /proc 状态: State: Z (zombie)
# waitpid 回收僵尸进程 PID=12347,退出码=42

4. 进程状态机

4.1 Linux 进程状态

复制代码
Linux 进程状态转换图:
─────────────────────────────────────────────────────────────

  fork()
    │
    ▼
  ┌─────────┐   调度器选中    ┌─────────────┐
  │ READY   │ ─────────────► │   RUNNING   │
  │(就绪)  │ ◄───────────── │  (运行中)  │
  └─────────┘   时间片用完    └──────┬──────┘
                                     │
              ┌──────────────────────┼──────────────────────┐
              │                      │                      │
              ▼                      ▼                      ▼
       ┌─────────────┐      ┌──────────────┐      ┌──────────────┐
       │ INTERRUPTIBLE│      │UNINTERRUPTIBLE│      │   STOPPED    │
       │  SLEEP (S)  │      │  SLEEP  (D)  │      │    (T/t)     │
       │(可中断睡眠)│      │(不可中断睡眠)│      │  (已停止)  │
       └──────┬──────┘      └──────┬───────┘      └──────┬───────┘
              │                    │                      │
              │ 等待条件满足        │ I/O 完成             │ SIGCONT
              └────────────────────┘                      │
                       │                                   │
                       ▼                                   ▼
                    READY ◄──────────────────────────── READY

              ┌──────────────┐
              │   ZOMBIE (Z) │ ← 进程退出,等待父进程 wait
              │  (僵尸)    │
              └──────┬───────┘
                     │ 父进程 wait()
                     ▼
                   消亡

状态码(/proc/[pid]/status 中的 State 字段):
  R  Running/Runnable    运行中或就绪
  S  Sleeping            可中断睡眠(等待事件)
  D  Disk sleep          不可中断睡眠(等待 I/O)
  T  Stopped             被信号停止(SIGSTOP/SIGTSTP)
  t  Tracing stop        被调试器停止(ptrace)
  Z  Zombie              僵尸进程
  X  Dead                已死亡(极短暂,几乎看不到)
  I  Idle                空闲内核线程(Linux 4.14+)
─────────────────────────────────────────────────────────────

4.2 进程状态观察

复制代码
/* 文件名:proc_states.c
 * 演示各种进程状态的创建和观察
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

/* 读取进程状态 */
char get_proc_state(pid_t pid) {
    char path[64];
    snprintf(path, sizeof(path), "/proc/%d/status", pid);
    FILE *fp = fopen(path, "r");
    if (!fp) return '?';

    char line[128];
    char state = '?';
    while (fgets(line, sizeof(line), fp)) {
        if (strncmp(line, "State:", 6) == 0) {
            state = line[7];   /* "State:\t X ..." */
            break;
        }
    }
    fclose(fp);
    return state;
}

/* 打印进程状态 */
void print_state(pid_t pid, const char *desc) {
    char state = get_proc_state(pid);
    const char *state_name;
    switch (state) {
        case 'R': state_name = "Running/Runnable"; break;
        case 'S': state_name = "Sleeping(可中断)"; break;
        case 'D': state_name = "Sleeping(不可中断)"; break;
        case 'T': state_name = "Stopped"; break;
        case 'Z': state_name = "Zombie"; break;
        case 'I': state_name = "Idle"; break;
        default:  state_name = "Unknown";
    }
    printf("  PID=%-6d  状态=%c (%s)  描述: %s\n",
           pid, state, state_name, desc);
}

int main(void) {
    printf("=== 进程状态演示 ===\n\n");

    /* ── S 状态:可中断睡眠 ── */
    printf("【S 状态:可中断睡眠】\n");
    pid_t sleep_pid = fork();
    if (sleep_pid == 0) {
        sleep(10);   /* 等待事件,进入 S 状态 */
        _exit(0);
    }
    usleep(100000);   /* 等子进程进入睡眠 */
    print_state(sleep_pid, "sleep(10) 中");
    kill(sleep_pid, SIGKILL);
    waitpid(sleep_pid, NULL, 0);

    /* ── T 状态:被信号停止 ── */
    printf("\n【T 状态:被 SIGSTOP 停止】\n");
    pid_t stop_pid = fork();
    if (stop_pid == 0) {
        while (1) pause();   /* 等待信号 */
        _exit(0);
    }
    usleep(100000);
    kill(stop_pid, SIGSTOP);   /* 发送 SIGSTOP 停止进程 */
    usleep(100000);
    print_state(stop_pid, "收到 SIGSTOP 后");

    kill(stop_pid, SIGCONT);   /* 发送 SIGCONT 继续进程 */
    usleep(100000);
    print_state(stop_pid, "收到 SIGCONT 后");
    kill(stop_pid, SIGKILL);
    waitpid(stop_pid, NULL, 0);

    /* ── Z 状态:僵尸进程 ── */
    printf("\n【Z 状态:僵尸进程】\n");
    pid_t zombie_pid = fork();
    if (zombie_pid == 0) {
        _exit(0);   /* 立即退出 */
    }
    usleep(100000);   /* 父进程不立即 wait */
    print_state(zombie_pid, "已退出但未被 wait");
    waitpid(zombie_pid, NULL, 0);
    printf("  waitpid 后僵尸进程已回收\n");

    /* ── R 状态:运行中 ── */
    printf("\n【R 状态:运行中(CPU 密集型)】\n");
    pid_t run_pid = fork();
    if (run_pid == 0) {
        /* CPU 密集型循环,保持 R 状态 */
        volatile long long x = 0;
        while (1) x++;
        _exit(0);
    }
    usleep(100000);
    print_state(run_pid, "CPU 密集型循环");
    kill(run_pid, SIGKILL);
    waitpid(run_pid, NULL, 0);

    /* ── 观察系统中各状态进程数量 ── */
    printf("\n=== 系统进程状态统计 ===\n\n");
    system("ps aux | awk 'NR>1{print $8}' | sort | uniq -c | sort -rn | "
           "awk '{printf \"  状态 %-3s: %d 个进程\\n\", $2, $1}'");

    return 0;
}
复制代码
gcc -o proc_states proc_states.c
./proc_states
# 输出示例:
# === 进程状态演示 ===
#
# 【S 状态:可中断睡眠】
#   PID=12346  状态=S (Sleeping(可中断))  描述: sleep(10) 中
#
# 【T 状态:被 SIGSTOP 停止】
#   PID=12347  状态=T (Stopped)  描述: 收到 SIGSTOP 后
#   PID=12347  状态=S (Sleeping(可中断))  描述: 收到 SIGCONT 后
#
# 【Z 状态:僵尸进程】
#   PID=12348  状态=Z (Zombie)  描述: 已退出但未被 wait
#   waitpid 后僵尸进程已回收
#
# 【R 状态:运行中(CPU 密集型)】
#   PID=12349  状态=R (Running/Runnable)  描述: CPU 密集型循环

5. 进程环境

5.1 环境变量

复制代码
/* 文件名:proc_environ.c
 * 演示进程环境变量的访问与修改
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* 外部变量:指向环境变量数组 */
extern char **environ;

int main(int argc, char *argv[], char *envp[]) {
    printf("=== 进程环境变量 ===\n\n");

    /* ── 方式1:通过 main 的第三个参数 envp ── */
    printf("【方式1:main 的 envp 参数(前5个)】\n");
    for (int i = 0; i < 5 && envp[i]; i++) {
        printf("  envp[%d] = %s\n", i, envp[i]);
    }

    /* ── 方式2:通过全局变量 environ ── */
    printf("\n【方式2:全局变量 environ(统计总数)】\n");
    int env_count = 0;
    for (char **ep = environ; *ep; ep++) env_count++;
    printf("  共 %d 个环境变量\n", env_count);

    /* ── 方式3:getenv 获取指定变量 ── */
    printf("\n【方式3:getenv 获取指定变量】\n");
    const char *vars[] = { "PATH", "HOME", "USER", "SHELL",
                           "LANG", "TERM", "NONEXISTENT", NULL };
    for (int i = 0; vars[i]; i++) {
        char *val = getenv(vars[i]);
        printf("  %-15s = %s\n", vars[i],
               val ? val : "(未设置)");
    }

    /* ── setenv / unsetenv / putenv ── */
    printf("\n【环境变量修改】\n");

    /* setenv:设置环境变量(overwrite=1 表示覆盖)*/
    setenv("MY_VAR", "hello_world", 1);
    printf("setenv(MY_VAR=hello_world): %s\n", getenv("MY_VAR"));

    /* 修改已有变量 */
    setenv("MY_VAR", "updated_value", 1);
    printf("再次 setenv(覆盖):        %s\n", getenv("MY_VAR"));

    /* setenv overwrite=0:不覆盖已有变量 */
    setenv("MY_VAR", "should_not_change", 0);
    printf("setenv(不覆盖):           %s\n", getenv("MY_VAR"));

    /* putenv:格式为 "NAME=VALUE"(注意:不复制字符串!)*/
    char env_str[] = "PUTENV_VAR=putenv_value";
    putenv(env_str);
    printf("putenv:                    %s\n", getenv("PUTENV_VAR"));

    /* unsetenv:删除环境变量 */
    unsetenv("MY_VAR");
    printf("unsetenv(MY_VAR) 后:       %s\n",
           getenv("MY_VAR") ? getenv("MY_VAR") : "(已删除)");

    /* ── 环境变量传递给子进程 ── */
    printf("\n【环境变量传递给子进程】\n");
    setenv("PARENT_VAR", "from_parent", 1);

    pid_t child = fork();
    if (child == 0) {
        /* 子进程继承父进程的环境变量 */
        printf("  子进程读取 PARENT_VAR = %s\n",
               getenv("PARENT_VAR") ? getenv("PARENT_VAR") : "(未找到)");

        /* 子进程修改不影响父进程 */
        setenv("PARENT_VAR", "modified_by_child", 1);
        printf("  子进程修改后 PARENT_VAR = %s\n",
               getenv("PARENT_VAR"));
        _exit(0);
    }

    int status;
    waitpid(child, &status, 0);
    printf("  父进程的 PARENT_VAR = %s(子进程修改不影响父进程)\n",
           getenv("PARENT_VAR"));

    /* ── clearenv:清空所有环境变量 ── */
    printf("\n【clearenv 清空环境变量】\n");
    int before = 0;
    for (char **ep = environ; *ep; ep++) before++;
    clearenv();
    int after = 0;
    for (char **ep = environ; *ep; ep++) after++;
    printf("清空前: %d 个,清空后: %d 个\n", before, after);

    return 0;
}
复制代码
gcc -o proc_environ proc_environ.c
./proc_environ
# 输出示例:
# === 进程环境变量 ===
#
# 【方式1:main 的 envp 参数(前5个)】
#   envp[0] = SHELL=/bin/bash
#   envp[1] = HOME=/home/alice
#   envp[2] = USER=alice
#   envp[3] = PATH=/usr/local/bin:/usr/bin:/bin
#   envp[4] = LANG=en_US.UTF-8
#
# 【方式3:getenv 获取指定变量】
#   PATH            = /usr/local/bin:/usr/bin:/bin
#   HOME            = /home/alice
#   NONEXISTENT     = (未设置)

5.2 命令行参数

复制代码
/* 文件名:cmdline_args.c
 * 演示命令行参数的解析
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <getopt.h>

/* 通过 /proc/self/cmdline 读取命令行 */
void read_cmdline_from_proc(void) {
    printf("=== /proc/self/cmdline ===\n");
    FILE *fp = fopen("/proc/self/cmdline", "r");
    if (!fp) return;

    char buf[1024] = {0};
    size_t n = fread(buf, 1, sizeof(buf) - 1, fp);
    fclose(fp);

    printf("原始内容(\\0 分隔): ");
    for (size_t i = 0; i < n; i++) {
        if (buf[i] == '\0') printf(" | ");
        else printf("%c", buf[i]);
    }
    printf("\n\n");
}

/* 使用 getopt_long 解析命令行参数 */
void parse_args(int argc, char *argv[]) {
    printf("=== getopt_long 参数解析 ===\n\n");

    /* 短选项字符串:
     * "h"   → -h(无参数)
     * "o:"  → -o arg(必须参数)
     * "v::" → -v [arg](可选参数)
     */
    const char *short_opts = "ho:v::n:";

    /* 长选项数组 */
    static struct option long_opts[] = {
        { "help",    no_argument,       NULL, 'h' },
        { "output",  required_argument, NULL, 'o' },
        { "verbose", optional_argument, NULL, 'v' },
        { "number",  required_argument, NULL, 'n' },
        { NULL, 0, NULL, 0 }
    };

    int opt;
    int verbose_level = 0;
    char *output_file = NULL;
    int number = 0;

    while ((opt = getopt_long(argc, argv, short_opts,
                               long_opts, NULL)) != -1) {
        switch (opt) {
            case 'h':
                printf("用法: %s [-h] [-o file] [-v[level]] [-n num]\n",
                       argv[0]);
                break;
            case 'o':
                output_file = optarg;
                printf("输出文件: %s\n", output_file);
                break;
            case 'v':
                verbose_level = optarg ? atoi(optarg) : 1;
                printf("详细级别: %d\n", verbose_level);
                break;
            case 'n':
                number = atoi(optarg);
                printf("数字参数: %d\n", number);
                break;
            case '?':
                fprintf(stderr, "未知选项: %c\n", optopt);
                break;
        }
    }

    /* 处理非选项参数 */
    printf("\n非选项参数(optind=%d):\n", optind);
    for (int i = optind; i < argc; i++) {
        printf("  argv[%d] = %s\n", i, argv[i]);
    }
}

int main(int argc, char *argv[]) {
    printf("=== 命令行参数 ===\n\n");
    printf("argc = %d\n", argc);
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = \"%s\"\n", i, argv[i]);
    }
    printf("argv[%d] = NULL(哨兵)\n\n", argc);

    read_cmdline_from_proc();
    parse_args(argc, argv);

    return 0;
}
复制代码
gcc -o cmdline_args cmdline_args.c
./cmdline_args -o output.txt -v2 -n 42 file1.txt file2.txt
# 输出示例:
# argc = 7
# argv[0] = "./cmdline_args"
# argv[1] = "-o"
# argv[2] = "output.txt"
# ...
# 输出文件: output.txt
# 详细级别: 2
# 数字参数: 42
# 非选项参数(optind=6):
#   argv[6] = file1.txt
#   argv[7] = file2.txt

6. 进程的内存布局

6.1 虚拟地址空间布局

复制代码
/* 文件名:memory_layout.c
 * 展示进程的完整内存布局
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>

/* 各段的代表性变量 */
int    global_init   = 100;          /* 数据段(已初始化全局变量)*/
int    global_uninit;                /* BSS 段(未初始化全局变量)*/
static int static_var = 200;         /* 数据段(静态变量)*/
const  int const_var  = 300;         /* 只读数据段(rodata)*/

void some_function(void) {}          /* 代码段(函数)*/

int main(void) {
    int    local_var  = 400;         /* 栈(局部变量)*/
    int   *heap_var   = malloc(100); /* 堆(动态分配)*/
    void  *mmap_var   = mmap(NULL, 4096,
                              PROT_READ | PROT_WRITE,
                              MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

    printf("=== 进程内存布局 ===\n\n");
    printf("地址空间(从高到低):\n\n");

    printf("  内核空间(用户不可见)\n");
    printf("  ─────────────────────────────────────────\n");
    printf("  栈(向下增长)\n");
    printf("    局部变量:          %p\n", (void *)&local_var);
    printf("    main 函数参数:     (栈顶附近)\n");
    printf("    环境变量:          (栈顶)\n");
    printf("  ─────────────────────────────────────────\n");
    printf("  mmap 映射区域(向下增长)\n");
    printf("    mmap 匿名映射:     %p\n", mmap_var);
    printf("    共享库(libc等):  (此区域)\n");
    printf("  ─────────────────────────────────────────\n");
    printf("  堆(向上增长)\n");
    printf("    malloc 分配:       %p\n", (void *)heap_var);
    printf("  ─────────────────────────────────────────\n");
    printf("  BSS 段(未初始化数据)\n");
    printf("    未初始化全局变量:  %p(值=%d,自动清零)\n",
           (void *)&global_uninit, global_uninit);
    printf("  ─────────────────────────────────────────\n");
    printf("  数据段(已初始化数据)\n");
    printf("    已初始化全局变量:  %p(值=%d)\n",
           (void *)&global_init, global_init);
    printf("    静态变量:          %p(值=%d)\n",
           (void *)&static_var, static_var);
    printf("  ─────────────────────────────────────────\n");
    printf("  只读数据段(rodata)\n");
    printf("    常量:              %p(值=%d)\n",
           (void *)&const_var, const_var);
    printf("    字符串字面量:      %p\n", (void *)"hello");
    printf("  ─────────────────────────────────────────\n");
    printf("  代码段(Text,只读可执行)\n");
    printf("    main 函数:         %p\n", (void *)main);
    printf("    some_function:     %p\n", (void *)some_function);
    printf("  ─────────────────────────────────────────\n");
    printf("  地址 0x0(不可访问,NULL 指针保护)\n\n");

    /* 验证各段的相对位置 */
    printf("=== 地址大小关系验证 ===\n\n");
    printf("代码段 < 数据段 < BSS < 堆 < mmap < 栈:\n");
    printf("  %p < %p < %p < %p < %p < %p: %s\n",
           (void *)main,
           (void *)&global_init,
           (void *)&global_uninit,
           (void *)heap_var,
           mmap_var,
           (void *)&local_var,
           (unsigned long)main < (unsigned long)&global_init &&
           (unsigned long)&global_init < (unsigned long)heap_var &&
           (unsigned long)heap_var < (unsigned long)mmap_var &&
           (unsigned long)mmap_var < (unsigned long)&local_var
           ? "✓ 正确" : "✗ 错误(地址随机化影响)");

    /* 打印 /proc/self/maps */
    printf("\n=== /proc/self/maps(内存映射)===\n");
    system("cat /proc/self/maps | head -20");

    free(heap_var);
    munmap(mmap_var, 4096);
    return 0;
}
复制代码
gcc -o memory_layout memory_layout.c
./memory_layout
# 输出示例:
# === 进程内存布局 ===
#
# 地址空间(从高到低):
#
#   内核空间(用户不可见)
#   ─────────────────────────────────────────
#   栈(向下增长)
#     局部变量:          0x7ffd1234abcd
#   ─────────────────────────────────────────
#   mmap 映射区域(向下增长)
#     mmap 匿名映射:     0x7f8a3b2c1000
#   ─────────────────────────────────────────
#   堆(向上增长)
#     malloc 分配:       0x55a1b2c3d010
#   ─────────────────────────────────────────
#   代码段(Text,只读可执行)
#     main 函数:         0x55a1b2c10234

7. 进程的创建:fork

7.1 fork 的工作原理

复制代码
/* 文件名:fork_demo.c
 * 深入演示 fork 的工作原理和行为
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>

/* 全局变量(测试 fork 后的独立性)*/
int g_counter = 0;

int main(void) {
    printf("=== fork 工作原理演示 ===\n\n");

    /* ── fork 的基本行为 ── */
    printf("【fork 基本行为】\n");
    printf("fork 前: PID=%d, g_counter=%d\n\n", getpid(), g_counter);

    /*
     * fork() 的工作原理:
     * 1. 复制父进程的 PCB(task_struct)
     * 2. 复制父进程的页表(写时复制,不立即复制物理页)
     * 3. 复制父进程的文件描述符表
     * 4. 子进程从 fork() 返回处开始执行
     *
     * 返回值:
     *   父进程:子进程的 PID(> 0)
     *   子进程:0
     *   失败:-1(父进程中)
     */
    pid_t pid = fork();

    if (pid == -1) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        /* ── 子进程 ── */
        printf("子进程: PID=%d, PPID=%d, fork返回值=%d\n",
               getpid(), getppid(), pid);

        /* 子进程修改变量(写时复制,不影响父进程)*/
        g_counter = 100;
        printf("子进程修改 g_counter = %d\n", g_counter);
        _exit(0);
    } else {
        /* ── 父进程 ── */
        printf("父进程: PID=%d, fork返回值=%d(子进程PID)\n",
               getpid(), pid);

        waitpid(pid, NULL, 0);
        printf("父进程的 g_counter = %d(子进程修改不影响父进程)\n\n",
               g_counter);
    }

    /* ── fork 与文件描述符 ── */
    printf("【fork 与文件描述符】\n");
    /*
     * fork 后父子进程共享同一打开文件表项
     * 共享文件偏移量!
     * 这是 fork 的一个重要特性(也是常见陷阱)
     */
    int fd = open("/tmp/fork_fd_test.txt",
                  O_RDWR | O_CREAT | O_TRUNC, 0644);
    write(fd, "PARENT_WRITE\n", 13);

    pid = fork();
    if (pid == 0) {
        /* 子进程:继续从父进程的偏移量写入 */
        write(fd, "CHILD_WRITE\n", 12);
        close(fd);
        _exit(0);
    }
    waitpid(pid, NULL, 0);
    close(fd);

    printf("fork 后父子进程共享文件偏移量,文件内容:\n");
    system("cat /tmp/fork_fd_test.txt");
    unlink("/tmp/fork_fd_test.txt");

    /* ── fork 与 stdio 缓冲区 ── */
    printf("\n【fork 与 stdio 缓冲区(重要陷阱)】\n");
    /*
     * ⚠️ 陷阱:printf 的输出可能在缓冲区中
     * fork 后父子进程各有一份缓冲区副本
     * 如果不 fflush,可能导致输出重复
     */
    printf("这行在 fork 前(可能被缓冲)");
    fflush(stdout);   /* fork 前必须 fflush! */

    pid = fork();
    if (pid == 0) {
        printf("\n子进程输出\n");
        _exit(0);
    }
    waitpid(pid, NULL, 0);
    printf("\n父进程输出\n");

    /* ── vfork:不复制页表(已废弃,了解即可)── */
    printf("\n【vfork vs fork】\n");
    printf("fork:  复制页表(写时复制),子进程有独立地址空间\n");
    printf("vfork: 不复制页表,子进程与父进程共享地址空间\n");
    printf("       子进程必须立即 exec 或 _exit,不能修改变量\n");
    printf("       现代系统中 fork 已足够高效,vfork 已废弃\n");

    return 0;
}
复制代码
gcc -o fork_demo fork_demo.c
./fork_demo
# 输出示例:
# === fork 工作原理演示 ===
#
# 【fork 基本行为】
# fork 前: PID=12345, g_counter=0
#
# 父进程: PID=12345, fork返回值=12346(子进程PID)
# 子进程: PID=12346, PPID=12345, fork返回值=0
# 子进程修改 g_counter = 100
# 父进程的 g_counter = 0(子进程修改不影响父进程)
#
# 【fork 与文件描述符】
# fork 后父子进程共享文件偏移量,文件内容:
# PARENT_WRITE
# CHILD_WRITE

7.2 fork 炸弹与资源限制

复制代码
/* 文件名:fork_limit.c
 * 演示进程数量限制和安全的 fork 使用
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/resource.h>

int main(void) {
    printf("=== fork 资源限制 ===\n\n");

    /* 查看进程数限制 */
    struct rlimit rl;
    getrlimit(RLIMIT_NPROC, &rl);
    printf("当前用户最大进程数:\n");
    printf("  软限制: %lu\n", (unsigned long)rl.rlim_cur);
    printf("  硬限制: %lu\n\n",
           rl.rlim_max == RLIM_INFINITY
           ? (unsigned long)-1 : (unsigned long)rl.rlim_max);

    /* 系统级进程数限制 */
    FILE *fp = fopen("/proc/sys/kernel/threads-max", "r");
    if (fp) {
        int threads_max;
        fscanf(fp, "%d", &threads_max);
        fclose(fp);
        printf("系统最大线程数: %d\n", threads_max);
    }

    fp = fopen("/proc/sys/kernel/pid_max", "r");
    if (fp) {
        int pid_max;
        fscanf(fp, "%d", &pid_max);
        fclose(fp);
        printf("系统最大 PID:   %d\n\n", pid_max);
    }

    /* 安全地创建多个子进程 */
    printf("=== 安全创建子进程池 ===\n\n");

    #define NUM_WORKERS 4
    pid_t workers[NUM_WORKERS];

    for (int i = 0; i < NUM_WORKERS; i++) {
        workers[i] = fork();
        if (workers[i] == -1) {
            fprintf(stderr, "fork 失败(第%d个): %s\n",
                    i + 1, strerror(errno));
            /* 等待已创建的子进程 */
            for (int j = 0; j < i; j++) {
                kill(workers[j], SIGTERM);
                waitpid(workers[j], NULL, 0);
            }
            return 1;
        }
        if (workers[i] == 0) {
            /* 子进程工作 */
            printf("  工作进程 %d(PID=%d)启动\n", i + 1, getpid());
            usleep(100000 * (i + 1));
            printf("  工作进程 %d(PID=%d)完成\n", i + 1, getpid());
            _exit(i);   /* 退出码 = 工作编号 */
        }
    }

    /* 父进程等待所有子进程 */
    printf("\n父进程等待所有工作进程...\n");
    for (int i = 0; i < NUM_WORKERS; i++) {
        int status;
        pid_t done = waitpid(workers[i], &status, 0);
        if (WIFEXITED(status)) {
            printf("  工作进程 PID=%d 退出,退出码=%d\n",
                   done, WEXITSTATUS(status));
        }
    }
    printf("所有工作进程已完成\n");

    return 0;
}

8. 进程的终止:exit

8.1 进程终止的方式

复制代码
/* 文件名:exit_demo.c
 * 演示进程终止的各种方式和退出状态
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>

/*
 * 进程终止的方式:
 *
 * 正常终止:
 *   1. main 函数 return(等价于 exit(返回值))
 *   2. exit(status):调用 atexit 注册的函数,刷新 stdio 缓冲区,然后终止
 *   3. _exit(status):直接终止,不调用 atexit,不刷新缓冲区
 *   4. _Exit(status):同 _exit(C99 标准)
 *
 * 异常终止:
 *   5. 收到终止信号(SIGKILL、SIGSEGV、SIGABRT 等)
 *   6. abort():发送 SIGABRT 信号
 */

/* atexit 注册的清理函数 */
void cleanup1(void) { printf("  cleanup1: 清理资源1\n"); }
void cleanup2(void) { printf("  cleanup2: 清理资源2\n"); }
void cleanup3(void) { printf("  cleanup3: 清理资源3\n"); }

int main(void) {
    printf("=== 进程终止方式演示 ===\n\n");

    /* ── atexit:注册退出时的清理函数 ── */
    printf("【atexit 注册清理函数】\n");
    /*
     * atexit 注册的函数在 exit() 时按 LIFO 顺序调用
     * 最后注册的最先调用
     */
    atexit(cleanup1);
    atexit(cleanup2);
    atexit(cleanup3);
    printf("已注册 cleanup1, cleanup2, cleanup3\n");
    printf("exit 时调用顺序(LIFO): cleanup3 → cleanup2 → cleanup1\n\n");

    /* ── exit vs _exit 的区别 ── */
    printf("【exit vs _exit 的区别】\n");

    pid_t child = fork();
    if (child == 0) {
        /* 子进程:测试 exit(会刷新缓冲区)*/
        printf("子进程(exit): 这行会被刷新输出");
        /* 注意:不加 \n,测试缓冲区刷新 */
        exit(0);   /* exit 会刷新 stdio 缓冲区 */
    }
    waitpid(child, NULL, 0);

    child = fork();
    if (child == 0) {
        /* 子进程:测试 _exit(不刷新缓冲区)*/
        printf("子进程(_exit): 这行可能不会输出");
        _exit(0);   /* _exit 不刷新 stdio 缓冲区 */
    }
    waitpid(child, NULL, 0);
    printf("\n\n");

    /* ── 退出状态码 ── */
    printf("【退出状态码(exit status)】\n");
    /*
     * exit(status):status 的低8位作为退出码
     * 范围:0-255(0 通常表示成功,非0表示失败)
     * 父进程通过 wait/waitpid 获取退出状态
     */
    struct {
        int exit_code;
        const char *desc;
    } tests[] = {
        { 0,   "成功" },
        { 1,   "一般错误" },
        { 2,   "命令行参数错误" },
        { 127, "命令未找到(shell 约定)" },
        { 130, "Ctrl+C 中断(128+SIGINT)" },
        { 0, NULL }
    };

    for (int i = 0; tests[i].desc; i++) {
        child = fork();
        if (child == 0) {
            _exit(tests[i].exit_code);
        }
        int status;
        waitpid(child, &status, 0);

        if (WIFEXITED(status)) {
            printf("  exit(%3d): WEXITSTATUS=%d  含义: %s\n",
                   tests[i].exit_code,
                   WEXITSTATUS(status),
                   tests[i].desc);
        }
    }

    /* ── 信号导致的异常终止 ── */
    printf("\n【信号导致的异常终止】\n");

    /* SIGSEGV:段错误 */
    child = fork();
    if (child == 0) {
        int *null_ptr = NULL;
        *null_ptr = 42;   /* 触发 SIGSEGV */
        _exit(0);
    }
    int status;
    waitpid(child, &status, 0);
    if (WIFSIGNALED(status)) {
        printf("  SIGSEGV: WTERMSIG=%d (%s),%s\n",
               WTERMSIG(status),
               strsignal(WTERMSIG(status)),
               WCOREDUMP(status) ? "产生 core dump" : "无 core dump");
    }

    /* SIGKILL:强制终止 */
    child = fork();
    if (child == 0) {
        sleep(10);
        _exit(0);
    }
    kill(child, SIGKILL);
    waitpid(child, &status, 0);
    if (WIFSIGNALED(status)) {
        printf("  SIGKILL: WTERMSIG=%d (%s)\n",
               WTERMSIG(status),
               strsignal(WTERMSIG(status)));
    }

    /* ── waitpid 的 status 解析宏 ── */
    printf("\n【waitpid status 解析宏】\n");
    printf("  WIFEXITED(status)   → 是否正常退出\n");
    printf("  WEXITSTATUS(status) → 正常退出码(0-255)\n");
    printf("  WIFSIGNALED(status) → 是否被信号终止\n");
    printf("  WTERMSIG(status)    → 终止信号编号\n");
    printf("  WCOREDUMP(status)   → 是否产生 core dump\n");
    printf("  WIFSTOPPED(status)  → 是否被信号停止(WUNTRACED)\n");
    printf("  WSTOPSIG(status)    → 停止信号编号\n");
    printf("  WIFCONTINUED(status)→ 是否被 SIGCONT 继续\n");

    printf("\n(atexit 注册的函数将在 main 返回时调用)\n");
    return 0;
}
复制代码
gcc -o exit_demo exit_demo.c
./exit_demo
# 输出示例:
# === 进程终止方式演示 ===
#
# 【atexit 注册清理函数】
# 已注册 cleanup1, cleanup2, cleanup3
# exit 时调用顺序(LIFO): cleanup3 → cleanup2 → cleanup1
#
# 【退出状态码(exit status)】
#   exit(  0): WEXITSTATUS=0   含义: 成功
#   exit(  1): WEXITSTATUS=1   含义: 一般错误
#   exit(127): WEXITSTATUS=127 含义: 命令未找到(shell 约定)
#
# 【信号导致的异常终止】
#   SIGSEGV: WTERMSIG=11 (Segmentation fault),无 core dump
#   SIGKILL: WTERMSIG=9 (Killed)
#
# (atexit 注册的函数将在 main 返回时调用)
#   cleanup3: 清理资源3
#   cleanup2: 清理资源2
#   cleanup1: 清理资源1

9. 综合实践

9.1 进程监控工具

复制代码
/* 文件名:proc_monitor.c
 * 实现一个简单的进程监控工具(类似 top 的简化版)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <pwd.h>
#include <time.h>

#define MAX_PROCS 512

typedef struct {
    pid_t  pid;
    pid_t  ppid;
    char   state;
    char   name[64];
    char   user[32];
    long   vmrss;      /* 物理内存 KB */
    long   vmsize;     /* 虚拟内存 KB */
    long   utime;      /* 用户态 CPU 时间 */
    long   stime;      /* 内核态 CPU 时间*/
    int    threads;
} ProcInfo;

/* 读取单个进程信息 */
int read_proc_info(pid_t pid, ProcInfo *info) {
    char path[64], line[512];
    FILE *fp;

    memset(info, 0, sizeof(*info));
    info->pid = pid;

    /* 读取 status */
    snprintf(path, sizeof(path), "/proc/%d/status", pid);
    fp = fopen(path, "r");
    if (!fp) return -1;

    while (fgets(line, sizeof(line), fp)) {
        if (strncmp(line, "Name:", 5) == 0)
            sscanf(line + 5, " %63s", info->name);
        else if (strncmp(line, "State:", 6) == 0)
            sscanf(line + 6, " %c", &info->state);
        else if (strncmp(line, "PPid:", 5) == 0)
            sscanf(line + 5, " %d", &info->ppid);
        else if (strncmp(line, "VmRSS:", 6) == 0)
            sscanf(line + 6, " %ld", &info->vmrss);
        else if (strncmp(line, "VmSize:", 7) == 0)
            sscanf(line + 7, " %ld", &info->vmsize);
        else if (strncmp(line, "Threads:", 8) == 0)
            sscanf(line + 8, " %d", &info->threads);
        else if (strncmp(line, "Uid:", 4) == 0) {
            uid_t uid;
            sscanf(line + 4, " %u", &uid);
            struct passwd *pw = getpwuid(uid);
            strncpy(info->user, pw ? pw->pw_name : "?",
                    sizeof(info->user) - 1);
        }
    }
    fclose(fp);

    /* 读取 CPU 时间(stat)*/
    snprintf(path, sizeof(path), "/proc/%d/stat", pid);
    fp = fopen(path, "r");
    if (fp) {
        /* 跳过前13个字段 */
        int dummy_i; char dummy_s[256]; char dummy_c;
        fscanf(fp, "%d %s %c %d %d %d %d %d %u %lu %lu %lu %lu %ld %ld",
               &dummy_i, dummy_s, &dummy_c,
               &dummy_i, &dummy_i, &dummy_i, &dummy_i, &dummy_i,
               (unsigned *)&dummy_i,
               (unsigned long *)&dummy_i, (unsigned long *)&dummy_i,
               (unsigned long *)&dummy_i, (unsigned long *)&dummy_i,
               &info->utime, &info->stime);
        fclose(fp);
    }

    return 0;
}

/* 比较函数:按内存使用量排序 */
int cmp_by_mem(const void *a, const void *b) {
    const ProcInfo *pa = (const ProcInfo *)a;
    const ProcInfo *pb = (const ProcInfo *)b;
    return (int)(pb->vmrss - pa->vmrss);
}

int main(void) {
    ProcInfo procs[MAX_PROCS];
    int count = 0;

    /* 遍历 /proc 收集进程信息 */
    DIR *proc_dir = opendir("/proc");
    if (!proc_dir) { perror("opendir"); return 1; }

    struct dirent *entry;
    while ((entry = readdir(proc_dir)) != NULL && count < MAX_PROCS) {
        pid_t pid = atoi(entry->d_name);
        if (pid <= 0) continue;
        if (read_proc_info(pid, &procs[count]) == 0)
            count++;
    }
    closedir(proc_dir);

    /* 按内存排序 */
    qsort(procs, (size_t)count, sizeof(ProcInfo), cmp_by_mem);

    /* 打印表头 */
    printf("═══════════════════════════════════════════════════════════════\n");
    printf("  进程监控(共 %d 个进程,按内存使用量排序)\n", count);
    printf("═══════════════════════════════════════════════════════════════\n");
    printf("%-7s %-7s %-8s %-6s %-8s %-8s %-4s %s\n",
           "PID", "PPID", "USER", "STATE",
           "RSS(KB)", "VSZ(KB)", "THR", "NAME");
    printf("─────────────────────────────────────────────────────────────\n");

    /* 打印前20个进程 */
    int show = count < 20 ? count : 20;
    for (int i = 0; i < show; i++) {
        char state_str[2] = { procs[i].state, '\0' };
        printf("%-7d %-7d %-8s %-6s %-8ld %-8ld %-4d %s\n",
               procs[i].pid,
               procs[i].ppid,
               procs[i].user,
               state_str,
               procs[i].vmrss,
               procs[i].vmsize,
               procs[i].threads,
               procs[i].name);
    }

    /* 统计信息 */
    long total_rss = 0;
    int state_count[256] = {0};
    for (int i = 0; i < count; i++) {
        total_rss += procs[i].vmrss;
        state_count[(unsigned char)procs[i].state]++;
    }

    printf("─────────────────────────────────────────────────────────────\n");
    printf("总计: %d 个进程,物理内存使用: %.1f MB\n",
           count, total_rss / 1024.0);
    printf("状态分布: R=%d S=%d D=%d T=%d Z=%d\n",
           state_count['R'], state_count['S'],
           state_count['D'], state_count['T'],
           state_count['Z']);

    return 0;
}
复制代码
gcc -O2 -o proc_monitor proc_monitor.c
./proc_monitor
# 输出示例:
# ═══════════════════════════════════════════════════════════════
#   进程监控(共 187 个进程,按内存使用量排序)
# ═══════════════════════════════════════════════════════════════
# PID     PPID    USER     STATE  RSS(KB)  VSZ(KB)  THR  NAME
# ─────────────────────────────────────────────────────────────
# 1234    1       alice    S      102400   512000   12   code
# 5678    1       alice    S       51200   256000    4   chrome
# ...
# 总计: 187 个进程,物理内存使用: 2048.5 MB
# 状态分布: R=2 S=180 D=0 T=0 Z=0

9.2 进程树可视化脚本

复制代码
#!/bin/bash
# 文件名:proc_tree.sh
# 功能:可视化进程树(类似 pstree)

set -euo pipefail

ROOT_PID="${1:-1}"
MAX_DEPTH="${2:-5}"

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'

# 获取进程信息
get_proc_info() {
    local pid=$1
    local name state ppid threads mem
    name=$(cat /proc/$pid/comm 2>/dev/null || echo "?")
    state=$(awk '/^State:/{print $2}' /proc/$pid/status 2>/dev/null || echo "?")
    threads=$(awk '/^Threads:/{print $2}' /proc/$pid/status 2>/dev/null || echo "1")
    mem=$(awk '/^VmRSS:/{print $2}' /proc/$pid/status 2>/dev/null || echo "0")
    echo "$name $state $threads $mem"
}

# 获取子进程列表
get_children() {
    local ppid=$1
    # 遍历 /proc 找所有 PPID = ppid 的进程
    for pid_dir in /proc/[0-9]*/; do
        local pid=$(basename "$pid_dir")
        local parent=$(awk '/^PPid:/{print $2}' "$pid_dir/status" 2>/dev/null || echo "0")
        if [ "$parent" = "$ppid" ]; then
            echo "$pid"
        fi
    done
}

# 递归打印进程树
print_tree() {
    local pid=$1
    local depth=$2
    local prefix=$3
    local is_last=$4

    [ "$depth" -gt "$MAX_DEPTH" ] && return

    # 获取进程信息
    read -r name state threads mem <<< "$(get_proc_info $pid)"

    # 状态颜色
    local color=$NC
    case "$state" in
        R) color=$GREEN ;;
        S) color=$CYAN ;;
        D) color=$RED ;;
        T) color=$YELLOW ;;
        Z) color=$RED ;;
    esac

    # 打印当前节点
    local connector
    if [ "$is_last" = "1" ]; then
        connector="└──"
    else
        connector="├──"
    fi

    printf "%s%s ${color}%s${NC} [%d] (状态:%s 线程:%s 内存:%sKB)\n" \
           "$prefix" "$connector" "$name" "$pid" "$state" "$threads" "$mem"

    # 计算子节点的前缀
    local child_prefix
    if [ "$is_last" = "1" ]; then
        child_prefix="${prefix}    "
    else
        child_prefix="${prefix}│   "
    fi

    # 获取并递归打印子进程
    local children=()
    while IFS= read -r child; do
        children+=("$child")
    done < <(get_children "$pid")

    local child_count=${#children[@]}
    for ((i=0; i<child_count; i++)); do
        local child_pid="${children[$i]}"
        local child_is_last=0
        [ "$i" -eq "$((child_count-1))" ] && child_is_last=1
        print_tree "$child_pid" "$((depth+1))" "$child_prefix" "$child_is_last"
    done
}

echo "═══════════════════════════════════════════════════════"
echo "  进程树(根 PID=$ROOT_PID,最大深度=$MAX_DEPTH)"
echo "═══════════════════════════════════════════════════════"

# 打印根进程
read -r name state threads mem <<< "$(get_proc_info $ROOT_PID)"
printf "${GREEN}%s${NC} [%d] (状态:%s 线程:%s 内存:%sKB)\n" \
       "$name" "$ROOT_PID" "$state" "$threads" "$mem"

# 打印子树
children=()
while IFS= read -r child; do
    children+=("$child")
done < <(get_children "$ROOT_PID")

child_count=${#children[@]}
for ((i=0; i<child_count; i++)); do
    child_pid="${children[$i]}"
    is_last=0
    [ "$i" -eq "$((child_count-1))" ] && is_last=1
    print_tree "$child_pid" 1 "" "$is_last"
done
复制代码
chmod +x proc_tree.sh
./proc_tree.sh $$    # 显示当前 shell 的进程树
./proc_tree.sh 1 3   # 显示 init 进程树(深度3)

知识点总结

复制代码
第 8 章 核心知识图谱
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

┌──────────────────────────────────────────────────────────┐
│                    进程基础                               │
└────────┬──────────┬──────────┬──────────┬────────────────┘
         │          │          │          │
  ┌──────▼───┐ ┌────▼─────┐ ┌──▼──────┐ ┌▼──────────────┐
  │  进程概念 │ │   PCB    │ │进程状态 │ │  进程操作     │
  └──────┬───┘ └────┬─────┘ └──┬──────┘ └┬──────────────┘
         │          │          │          │
  程序vs进程    task_struct   R/S/D/T/Z  fork/exit
  内存布局      /proc/[pid]   状态转换   atexit/wait
  地址空间      PCB字段       孤儿进程   waitpid宏
  虚拟内存      进程树        僵尸进程   信号终止

进程标识符层次:
  会话(SID) → 进程组(PGID) → 进程(PID) → 线程(TID)

进程状态速查:
  R  运行/就绪    S  可中断睡眠   D  不可中断睡眠
  T  已停止       Z  僵尸         I  空闲内核线程

fork 关键特性:
  ① 子进程继承父进程的 fd 表(共享文件偏移量!)
  ② 写时复制(COW):修改变量不影响对方
  ③ fork 前必须 fflush(stdout)(避免缓冲区重复输出)
  ④ 子进程应用 _exit 而非 exit(避免重复调用 atexit)

黄金法则:
  ① fork 后父进程必须 wait/waitpid,否则产生僵尸进程
  ② 孤儿进程被 init(PID=1) 收养,不会变成僵尸
  ③ exit 调用 atexit + 刷新缓冲区;_exit 直接终止
  ④ WEXITSTATUS 只在 WIFEXITED 为真时有效
  ⑤ 忽略 SIGCHLD 可让内核自动回收子进程
  ⑥ fork 前 fflush,fork 后子进程用 _exit
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

📚 参考资料

  • man 2 fork / man 2 getpid / man 2 getppid

  • man 2 exit / man 3 exit / man 2 waitpid

  • man 2 setpgid / man 2 setsid / man 2 getpriority

  • man 5 proc --- /proc 文件系统详细说明

  • man 3 atexit / man 3 getenv / man 3 setenv

  • man 3 getopt_long --- 命令行参数解析

  • 《Linux/UNIX 系统编程手册》第 24、25、26 章 --- Michael Kerrisk

  • 《UNIX 环境高级编程(APUE)》第 7、8 章 --- W. Richard Stevens

  • 《深入理解 Linux 内核》第 3 章 进程 --- Daniel P. Bovet

相关推荐
古城小栈2 小时前
Unix 与 Linux 异同小叙
linux·服务器·unix
凡人叶枫3 小时前
Effective C++ 条款42:了解 typename 的双重意义
java·linux·服务器·c++
2601_951643883 小时前
C语言长文整理,关键字和数据类型
c语言·数据类型·关键字·嵌入式开发·格式化输出
2601_961875244 小时前
决战申论100题2026|最新|范文
linux·容器·centos·debian·ssh·fabric·vagrant
java_cj4 小时前
深入kube-apiserver认证机制:从Bearer Token到mTLS的完整认证链解析
linux·运维·服务器·云原生·容器·kubernetes
森G4 小时前
75、服务器源码解析---------云视频服务项目
linux·服务器·网络·c++·qt
阿米亚波4 小时前
【Windows】QEMU 启动 openEuler aarch64/arm64 架构系统 + 离线软件源
linux·windows·经验分享·笔记·架构·arm
张飞飞飞飞飞4 小时前
Tmux命令使用教程
linux·服务器·ubuntu
Fcy6484 小时前
Linux下 可重入函数、volatile关键字和SIGCHLD信号
linux·可重入函数·volatile关键字·sigchld