Linux 系统编程 · 第 8 章:进程基础
本章深入讲解 Linux 进程的核心概念:进程的本质与组成、进程控制块(PCB)、进程状态机、进程标识符体系、进程环境,以及进程的创建与终止机制。
目录
-
进程的本质与组成
-
进程控制块(PCB)
-
进程标识符体系
-
进程状态机
-
进程环境
-
进程的内存布局
-
进程的创建:fork
-
进程的终止:exit
-
综合实践
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