C学习笔记
C 语言 进程概述 — 学习总结 (Day6)
目录
1. 基础概念:什么是进程
1.1 进程的定义
进程(Process)= 正在运行的程序
- 程序:存在磁盘上的静态文件(如编译好的可执行文件
a.out) - 进程:程序被加载到内存中,由 CPU 执行的那个动态实例
程序(磁盘上的文件) ──加载到内存──▶ 进程(正在运行的实体)
a.out PID=3210
1.2 进程 = 资源分配的最小单位
操作系统以进程为基准来分配系统资源,每个进程拥有:
| 资源 | 说明 |
|---|---|
| 独立的地址空间 | 每个进程有自己的虚拟内存(代码段、数据段、堆、栈) |
| 进程 ID(PID) | 系统内唯一标识一个进程的正整数 |
| 打开的文件描述符表 | 每个进程独立维护自己打开的文件 |
| 寄存器上下文 | CPU 寄存器状态,用于进程切换时保存/恢复 |
同一用户同时运行同一个程序多次,会产生多个不同的进程,每个有不同的 PID。
1.3 进程的地址空间布局
┌──────────────────┐ 高地址
│ 内核空间 │
├──────────────────┤
│ 栈 (stack) │ ← 局部变量、函数调用返回地址
│ ↓↓↓ │
│ │
│ ↑↑↑ │
│ 堆 (heap) │ ← malloc 分配的内存
├──────────────────┤
│ 未初始化数据段 │ ← BSS 段 (全局未初始化变量)
├──────────────────┤
│ 已初始化数据段 │ ← 全局已初始化变量、静态变量
├──────────────────┤
│ 代码段 │ ← 程序指令(只读)
└──────────────────┘ 低地址
2. 查看进程 — ps 命令
2.1 基本用法
ps [options]
| 选项 | 作用 |
|---|---|
| (无选项) | 显示当前终端下的进程 |
-e |
显示系统中所有进程 |
-u |
以用户友好格式显示详细信息 |
-o |
自定义输出格式,如 pid,ppid,comm |
-t |
查看指定终端的进程 |
2.2 常见组合
# 1. 查看全部进程的详细信息
ps -e u
# 输出列:USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
# 2. 自定义格式:只看 PID、父进程 PID、命令名
ps -o pid,ppid,command
# 3. 查看 tty2 终端上的进程
ps -t tty2
# 4. 结合 grep 筛选
ps -e u |grep fish # 筛选包含 "fish" 的进程
2.3 管道(pipe)与 grep
Shell 的 | 管道可以将前一命令的输出传给后一命令:
ps -e u | grep bash
# ps -e u 的全部输出 ──通过管道──▶ grep 筛选含 "bash" 的行
3. 进程的状态
3.1 ps 输出的 STAT 字段含义
| 状态码 | 含义 | 说明 |
|---|---|---|
| R | Running / Runnable | 正在运行或就绪(在运行队列中等待 CPU) |
| S | Sleeping (可中断睡眠) | 等待某个事件完成(如读终端输入、文件 I/O) |
| D | Disk Sleep (不可中断睡眠) | 等待 I/O 完成,不能被信号打断 |
| T | Stopped | 被暂停(如 Ctrl+Z) |
| Z | Zombie(僵尸) | 子进程已结束,但父进程还未回收其退出状态 |
| W | Paging | 没有足够的内存页可分配(极少见) |
附加标记:
| 标记 | 含义 |
|---|---|
s |
该进程是会话领导者(session leader),拥有子进程 |
+ |
位于前台进程组 |
3.2 为什么程序多数时候是 S(睡眠)状态?
#include <stdio.h>
#include <unistd.h>
int main() {
sleep(2); // 进入 S 状态,等 2 秒后唤醒
getchar(); // 进入 S 状态,等键盘输入
printf("done\n");
return 0;
}
sleep():主动让进程进入睡眠,到期后系统唤醒getchar()/scanf():阻塞等待 I/O,也进入 S 状态- 只有 CPU 计算密集型的短时刻进程才处于 R 状态
3.3 进程状态转换图
┌──────────┐
创建 ───▶│ READY │ ◀─────────┐
│ 就绪态 │ │
└────┬─────┘ │
调度 │ 时间片用完 │
▼ │
┌──────────┐ │
│ RUNNING │ ───────────┘
│ 运行态 │
└────┬─────┘
等待事件 │ 事件完成
┌───▼───────┐
│ SLEEP │
│ 睡眠态 │
└───────────┘
此外还有:T(暂停态)← Ctrl+Z
Z(僵尸态)→ 父进程 wait() 回收后消失
3.4 前台与后台运行
# 前台运行(默认)
./myprogram
# 后台运行(命令行末尾加 &)
./myprogram &
# 查看后台任务
jobs
# Ctrl+C → 终止前台进程
# Ctrl+Z → 暂停前台进程(进入 T 状态)
3.5 进程调度命令
| 操作 | 命令 | 说明 |
|---|---|---|
| 终止前台进程 | Ctrl + C |
发送 SIGINT 信号 |
| 暂停前台进程 | Ctrl + Z |
进入 T(Stopped)状态 |
| 调回前台 | fg %job号 |
将暂停/后台进程调回前台 |
| 放入后台 | bg %job号 |
让暂停的进程在后台继续运行 |
| 杀死进程 | kill PID 或 kill %job号 |
发送终止信号 |
4. 多进程的运行原理
4.1 核心矛盾
一台机器的 CPU 核心数通常远小于正在运行的进程数。
4.2 时间片轮转(Time Slice)
操作系统通过极快地轮流切换来制造"同时运行"的假象:
时间轴: ─────────────────────────────────────────────────▶
CPU: [进程A][进程B][进程C][进程A][进程D][进程B][进程C]...
← 每个时间片只有几毫秒到几十毫秒 →
| 概念 | 说明 |
|---|---|
| 时间片 | 每个进程一次能连续运行的最大时间(通常 1~100ms) |
| 上下文切换 | 保存当前进程状态,恢复下一个进程状态 |
| 调度器 | 内核中决定"下一个该谁运行"的模块 |
| 优先级 | 高优先级进程获得更频繁或更长的时间片 |
*不代表低优先级进程不会运行,只是高优先级进程运行更加频繁
4.3 调度策略
- 采用抢占式多任务:高优先级进程可以打断低优先级进程
- 不可抢占的部分(如内核代码、不可中断睡眠 D 状态)必须等执行完
- 时间片用完 → 强制切走,重新排队
5. 进程的创建
5.1 两个关键 ID
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); // 获取当前进程的 PID
pid_t getppid(void); // 获取当前进程的父进程 PID
示例:
#include <stdio.h>
#include <unistd.h>
int main() {
printf("我的 PID = %d\n", getpid());
printf("父进程 PID = %d\n", getppid());
return 0;
}
// 输出示例:
// 我的 PID = 3210
// 父进程 PID = 2891 (通常是 bash)
5.2 system() — 执行 Shell 命令
#include <stdlib.h>
int system(const char *command);
在当前进程中调用 Shell 执行一条命令,命令结束后返回。
原理:
你的程序 ──system("ps -e")──▶ Shell ──fork──▶ 新进程执行 ps -e
(你的程序阻塞等待)
ps 结束后控制权返回你的程序
示例:
#include <stdlib.h>
int main() {
// 执行 ps 命令查看进程
system("ps -e u | grep bash");
return 0;
}
⚠️
system()会阻塞当前进程,直到命令执行完毕。
⚠️ 使用system()有安全风险:如果 command 由用户输入构成,可能被注入恶意命令。
5.3 fork() — 创建子进程
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
fork()是 Linux 下创建新进程的唯一方法。
调用一次,返回两次!
返回值详解
| 返回值 | 所在进程 | 含义 |
|---|---|---|
> 0 |
父进程中 | 返回值 = 子进程的 PID |
== 0 |
子进程中 | 表示当前是子进程 |
< 0 |
父进程中 | fork() 失败(如内存不足) |
fork 执行流程
调用 fork()
父进程 ──────────────────────────▶
PID=3210 继续执行 fork 之后的代码
返回值 = 3211 (子进程 PID)
┌─────────────────
│ 子进程
│ PID=3211
│ 返回值为 0
└──▶ 继续执行 fork 之后的代码

示例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid;
int x = 8;
pid = fork(); // 从这里开始,两个进程并行执行
if (pid < 0) {
// fork 失败
perror("fork failed");
return -1;
} else if (pid == 0) {
// 子进程进入这里
x = 100;
printf("子进程: PID=%d, 父PID=%d, x=%d\n",
getpid(), getppid(), x);
} else {
// 父进程进入这里
x = 200;
printf("父进程: PID=%d, 子PID=%d, x=%d\n",
getpid(), pid, x);
}
// 两个进程都会执行到这里
printf("进程 %d 结束\n", getpid());
return 0;
}
可能的输出:
父进程: PID=3210, 子PID=3211, x=200
进程 3210 结束
子进程: PID=3211, 父PID=3210, x=100
进程 3211 结束
🔑 关键理解:子进程修改
x = 100并不影响父进程的x(仍是 200)——
因为fork()后它们拥有独立的内存空间!

写时复制(Copy-On-Write)
现代 Linux 中,fork() 并不立刻复制所有内存,而是:
- 父子进程共享同一物理内存页(只读)
- 当任意一方写入某页时,内核才复制该页(COW)
- 大大减少了
fork()的开销
5.4 execlp() — 执行另一个程序
#include <unistd.h>
int execlp(const char *file, const char *arg0, ... /* (char *)NULL */);
用另一个程序替换当前进程的内存空间,执行新程序。
成功时不返回,失败返回-1。
| 参数 | 说明 |
|---|---|
file |
要执行的程序名(PATH 环境变量中查找) |
arg0 |
习惯上 = 程序名本身(argv[0]) |
... |
后续参数,必须以 NULL 结尾 |
典型用法:fork + execlp
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:用 execlp 执行 ps 命令
execlp("ps", "ps", "-e", "u", NULL);
// 如果 execlp 执行成功,永远不会执行到下一条语句
perror("execlp failed");
return -1;
} else if (pid > 0) {
// 父进程:等待子进程结束
wait(NULL);
printf("子进程 ps 已执行完毕\n");
}
return 0;
}
execlp 执行过程
父进程 fork() 后:
子进程内存: execlp("ps",...) 后:
┌─────────────┐ ┌─────────────┐
│ 原程序代码 │ │ ps 的代码 │
│ 原程序数据 │ ───▶ │ ps 的数据 │
│ 原程序堆栈 │ │ ps 的堆栈 │
└─────────────┘ └─────────────┘
PID 不变! PID 还是原来的
执行了
execlp后,子进程不再执行原程序中的任何代码,完全变为ps进程。
PID 不变,这就是为什么ps看到的进程号和 fork 出来的子进程号一致。
6. 进程管理
6.1 wait() — 等待子进程结束
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
父进程调用
wait()会阻塞,直到任意一个子进程结束。
返回结束的子进程 PID,失败返回-1。
| 参数 | 说明 |
|---|---|
wstatus |
传出参数,存储子进程退出状态;不关心可传 NULL |
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
printf("子进程 PID=%d 开始工作...\n", getpid());
sleep(2);
printf("子进程结束\n");
exit(42); // 退出码 42
} else {
int status;
pid_t finished = wait(&status);
printf("父进程: 子进程 %d 已结束\n", finished);
if (WIFEXITED(status)) {
printf("退出码: %d\n", WEXITSTATUS(status));
}
}
return 0;
}
退出状态解析宏:
| 宏 | 作用 |
|---|---|
WIFEXITED(status) |
子进程是否正常退出(exit() / return) |
WEXITSTATUS(status) |
获取正常退出的退出码 |
WIFSIGNALED(status) |
子进程是否被信号杀死 |
WTERMSIG(status) |
获取杀死子进程的信号编号 |
6.2 waitpid() — 等待指定子进程
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
比
wait()更灵活,可以指定等待哪一个子进程。
| 参数 | 说明 |
|---|---|
pid |
子进程 PID;-1 = 任意子进程(等同 wait) |
wstatus |
传出退出状态 |
options |
0(阻塞等待)或 WNOHANG(非阻塞轮询) |
示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t child1 = fork();
if (child1 == 0) { sleep(3); return 111; }
pid_t child2 = fork();
if (child2 == 0) { sleep(1); return 222; }
// 等待特定的子进程 child2
int status;
waitpid(child2, &status, 0);
printf("子进程 %d 退出码: %d\n", child2, WEXITSTATUS(status));
// 再等 child1
waitpid(child1, &status, 0);
printf("子进程 %d 退出码: %d\n", child1, WEXITSTATUS(status));
return 0;
}
wait vs waitpid
wait() |
waitpid() |
|
|---|---|---|
| 等待目标 | 任意子进程 | 指定 PID 的子进程 |
| 阻塞方式 | 必须阻塞 | 可选非阻塞(WNOHANG) |
| 灵活性 | 低 | 高 |
6.3 孤儿进程与僵尸进程
孤儿进程(Orphan Process)
父进程先于子进程结束,子进程变成孤儿,由 init 进程(PID=1) 收养。
父进程终止 ──▶ 子进程的 PPID 变成 1(init 收养)
孤儿进程不会造成系统问题,init 进程会自动回收它。
僵尸进程(Zombie Process)⭐ 重点
子进程已结束,但父进程没有调用
wait()回收它的退出状态。
子进程变成僵尸(STAT=Z),占用进程表条目但不占用其他资源。
子进程结束 ──▶ 父进程没 wait() ──▶ 僵尸(Z)
│
└── wait() 后,僵尸被清理
危害: 大量僵尸进程会耗尽 PID 资源,无法创建新进程。
解决方法: 父进程中必须调用 wait() 或 waitpid() 回收子进程。
僵尸进程演示:
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
printf("子进程 %d 结束\n", getpid());
return 0; // 子进程结束
} else {
printf("父进程 %d 休眠,不 wait\n", getpid());
sleep(30); // 这时子进程变成僵尸!
// 没有 wait(),子进程状态 = Z
}
return 0;
}
// 在另一个终端运行:ps -e u | grep Z
// 可看到子进程状态为 Z(僵尸)
7. 思考与总结
7.1 核心函数速查表
| 函数 | 作用 | 函数定义 | 关键特点 |
|---|---|---|---|
getpid() |
获取当前进程 PID | pid_t getpid(void); |
总是成功,无参数 |
getppid() |
获取父进程 PID | pid_t getppid(void); |
孤儿进程返回 1(init) |
system() |
执行 Shell 命令 | int system(const char *command); |
返回命令终止状态,有安全风险 |
fork() |
创建子进程 | pid_t fork(void); |
调用一次返回两次,父返回子 PID,子返回 0,COW 优化 |
execlp() |
替换进程为另一个程序 | int execlp(const char *file, const char *arg, ...); |
成功不返回,失败返回 -1,PID 不变 |
wait() |
等待任意子进程 | pid_t wait(int *status); |
阻塞等待,防止僵尸进程,返回终止子进程 PID |
waitpid() |
等待指定子进程 | pid_t waitpid(pid_t pid, int *status, int options); |
支持非阻塞(WNOHANG),可指定进程 |
7.2 fork + execlp 经典范式
创建工作子进程执行其他程序的标准写法:
pid_t pid = fork();
if (pid == 0) {
// 子进程中
execlp("程序名", "程序名", "参数1", "参数2", ..., NULL);
perror("execlp failed"); // 只有失败才会走到这里
_exit(1);
} else if (pid > 0) {
// 父进程中
int status;
waitpid(pid, &status, 0); // 等子进程结束,防止僵尸
}
7.3 关键对比
| 对比项 | fork() | system() |
|---|---|---|
| 创建方式 | 复制自身 | 调用 Shell 再 fork |
| 新进程内容 | 与父进程相同 | 执行指定命令 |
| 是否阻塞 | 不阻塞,父子并行 | 阻塞等待命令完成 |
| 灵活性 | 高,可精确控制 | 低,只能执行一条命令 |
| 效率 | 高(COW) | 较低(多一层 Shell) |
7.4 常见陷阱
- 忘记 wait() → 产生僵尸进程,PID 资源泄漏
- fork 返回值没判断 → 父子和错误情况分不清
- execlp 之后还写代码 → 成功执行 execlp 后不会返回,后面的代码只在失败时才走到
- execlp 参数必须以 NULL 结尾 → 忘记 NULL 会导致未定义行为
- system() 的用户输入 →
system(user_input)可能被注入rm -rf /等恶意命令

浙公网安备 33010602011771号