进程与线程管理
章节概述
进程和线程是操作系统最核心的概念。本章将深入探讨Linux进程模型、生命周期管理、进程间通信,以及如何使用cgroup进行资源限制,为理解高级系统编程打下坚实基础。
学习目标:
- 深入理解进程和线程的本质区别
- 掌握进程的完整生命周期
- 学会使用各种进程管理工具
- 理解并避免僵尸进程和孤儿进程
- 掌握cgroup资源限制技术
核心概念
1. 进程 vs 线程
┌─────────────────────────────────────┐
│ 进程 (Process) │
│ ┌──────────────────────────────┐ │
│ │ 代码段 (Text) │ │
│ ├──────────────────────────────┤ │
│ │ 数据段 (Data/BSS) │ │
│ ├──────────────────────────────┤ │
│ │ 堆 (Heap) ↓ │ │
│ │ ... │ │
│ │ 线程1栈 ↑ │ │
│ │ 线程2栈 ↑ │ │
│ │ 线程3栈 ↑ │ │
│ │ ... │ │
│ │ 内存映射区 (mmap) │ │
│ └──────────────────────────────┘ │
│ │
│ 文件描述符表 │
│ 信号处理 │
│ 进程ID (PID) │
└─────────────────────────────────────┘
线程 (Thread) = 进程内的执行单元
- 共享:代码段、数据段、堆、文件描述符
- 独立:栈、寄存器、线程ID (TID)
对比表:
特性 | 进程 | 线程 |
---|---|---|
定义 | 资源分配单位 | CPU调度单位 |
创建开销 | 大(fork) | 小(clone) |
内存空间 | 独立 | 共享 |
通信 | IPC(管道、共享内存等) | 直接读写共享内存 |
安全性 | 高(隔离) | 低(共享) |
上下文切换 | 慢 | 快 |
2. 进程状态
Linux进程状态机:
新建
↓
[可运行 R] ←──────┐
↓ │
├──→ [运行中 R] ┘
│ ↓
│ [睡眠 S/D]
│ ↓
│ 系统调用/等待事件
│ ↓
←────────┘
│
↓
[僵尸 Z] → [终止]
↑
│
[停止 T/t]
状态详解:
状态 | 标志 | 说明 |
---|---|---|
Running | R | 正在运行或等待运行 |
Sleeping | S | 可中断睡眠(等待事件) |
Uninterruptible | D | 不可中断睡眠(通常是I/O) |
Stopped | T | 收到SIGSTOP信号 |
Traced | t | 被调试器追踪 |
Zombie | Z | 已终止,等待父进程回收 |
Dead | X | 完全终止 |
查看进程状态:
ps aux | head
# STAT列:
# R: 运行
# S: 睡眠
# D: 不可中断睡眠
# Z: 僵尸
# T: 停止
# 详细状态
cat /proc/<pid>/status | grep State
3. 进程创建:fork()
fork()工作原理:
父进程
↓ fork()
├─→ 父进程(返回子进程PID)
└─→ 子进程(返回0)
COW (Copy-On-Write):
- fork后不立即复制内存
- 父子共享只读页面
- 写时才复制(COW)
源码示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid;
int x = 100;
printf("Before fork: x=%d, PID=%d\n", x, getpid());
pid = fork();
if (pid < 0) {
// fork失败
perror("fork failed");
return 1;
} else if (pid == 0) {
// 子进程
x = 200;
printf("Child: x=%d, PID=%d, Parent PID=%d\n",
x, getpid(), getppid());
return 0;
} else {
// 父进程
x = 300;
printf("Parent: x=%d, PID=%d, Child PID=%d\n",
x, getpid(), pid);
// 等待子进程
wait(NULL);
return 0;
}
}
输出:
Before fork: x=100, PID=12345
Parent: x=300, PID=12345, Child PID=12346
Child: x=200, PID=12346, Parent PID=12345
4. exec系列函数
exec替换进程映像:
fork()创建新进程 + exec()加载新程序 = 启动新程序
exec系列:
- execl, execlp, execle
- execv, execvp, execve
execve是系统调用,其他是封装
示例:
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Before exec\n");
// 执行ls命令
execl("/bin/ls", "ls", "-l", NULL);
// 如果exec成功,下面的代码不会执行
perror("exec failed");
return 1;
}
fork + exec模式:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:执行新程序
execl("/bin/ls", "ls", "-l", NULL);
perror("exec failed");
return 1;
} else {
// 父进程:等待子进程
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("Child exited with code %d\n", WEXITSTATUS(status));
}
}
return 0;
}
5. 僵尸进程与孤儿进程
僵尸进程 (Zombie):
定义:子进程已终止,但父进程未调用wait回收资源
产生原因:
1. 子进程exit
2. 父进程未wait
危害:
- 占用进程号
- 内核需要保留进程信息
- 大量僵尸进程会耗尽PID
解决:
- 父进程调用wait/waitpid
- 父进程注册SIGCHLD处理函数
- 父进程退出(init收养并回收)
孤儿进程 (Orphan):
定义:父进程先于子进程终止
处理:
- 被init进程(PID=1)收养
- init自动回收
- 不会成为僵尸进程
示例:僵尸进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:立即退出
printf("Child process PID=%d exiting\n", getpid());
exit(0);
} else {
// 父进程:不调用wait,直接睡眠
printf("Parent process PID=%d, child PID=%d\n", getpid(), pid);
printf("Child will become zombie. Check with: ps aux | grep Z\n");
sleep(60); // 60秒内子进程是僵尸状态
}
return 0;
}
检查僵尸进程:
ps aux | grep Z
# 或
ps aux | awk '$8=="Z"'
正确处理SIGCHLD:
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
void sigchld_handler(int sig) {
// 回收所有已终止的子进程
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main() {
// 注册SIGCHLD处理函数
signal(SIGCHLD, sigchld_handler);
pid_t pid = fork();
if (pid == 0) {
printf("Child exiting\n");
exit(0);
} else {
printf("Parent waiting (no zombie)\n");
sleep(60);
}
return 0;
}
进程管理工具
1. ps命令
常用选项:
# 查看所有进程
ps aux
# 查看进程树
ps auxf
# 查看特定进程
ps -p <pid> -o pid,ppid,cmd,stat,pcpu,pmem
# 查看进程的线程
ps -T -p <pid>
# 按内存排序
ps aux --sort=-rss | head
# 按CPU排序
ps aux --sort=-pcpu | head
输出字段含义:
USER: 进程所有者
PID: 进程ID
%CPU: CPU使用率
%MEM: 内存使用率
VSZ: 虚拟内存大小(KB)
RSS: 物理内存大小(KB)
TTY: 终端
STAT: 状态
START: 启动时间
TIME: CPU时间
COMMAND: 命令
2. top/htop
top交互键:
1: 显示每个CPU核心
H: 显示线程
c: 显示完整命令
M: 按内存排序
P: 按CPU排序
k: 杀死进程
r: 重新设置nice值
htop优势:
- 更友好的界面
- 支持鼠标
- 更直观的CPU/内存/Swap显示
- 更容易的进程管理
3. pgrep/pkill
# 按名称查找进程
pgrep nginx
pgrep -f "python.*server"
# 显示详细信息
pgrep -l nginx
# 杀死进程
pkill nginx
pkill -9 -f "python.*server"
# 只杀死特定用户的进程
pkill -u www-data nginx
4. /proc文件系统
# 进程信息目录
ls /proc/<pid>/
# 常用文件:
cat /proc/<pid>/status # 进程状态
cat /proc/<pid>/cmdline # 命令行
cat /proc/<pid>/environ # 环境变量
cat /proc/<pid>/fd/ # 打开的文件描述符
cat /proc/<pid>/maps # 内存映射
cat /proc/<pid>/stat # 统计信息
cat /proc/<pid>/limits # 资源限制
️ Cgroup资源限制
1. Cgroup概述
Cgroup (Control Groups):
- 限制进程组的资源使用
- 隔离进程组
- 统计资源使用
支持的资源类型:
- cpu: CPU时间
- memory: 内存
- blkio: 块设备I/O
- net_cls: 网络分类
- devices: 设备访问
2. Cgroup v2使用
查看Cgroup版本:
mount | grep cgroup
# cgroup2表示v2
创建Cgroup:
# 创建一个cgroup
sudo mkdir /sys/fs/cgroup/demo
# 设置CPU限制(50%)
echo "50000 100000" | sudo tee /sys/fs/cgroup/demo/cpu.max
# 50000微秒 / 100000微秒 = 50%
# 设置内存限制(100MB)
echo "104857600" | sudo tee /sys/fs/cgroup/demo/memory.max
# 将进程加入cgroup
echo <pid> | sudo tee /sys/fs/cgroup/demo/cgroup.procs
实战示例:
# 创建CPU密集型进程
stress --cpu 1 &
PID=$!
# 不限制时CPU 100%
top -p $PID
# 创建cgroup并限制
sudo mkdir /sys/fs/cgroup/limited
echo "25000 100000" | sudo tee /sys/fs/cgroup/limited/cpu.max
echo $PID | sudo tee /sys/fs/cgroup/limited/cgroup.procs
# 现在CPU限制在25%
top -p $PID
3. systemd集成
查看systemd单元的cgroup:
systemctl status nginx.service
# CGroup: /system.slice/nginx.service
设置资源限制:
# 限制CPU
sudo systemctl set-property nginx.service CPUQuota=50%
# 限制内存
sudo systemctl set-property nginx.service MemoryLimit=1G
# 限制任务数
sudo systemctl set-property nginx.service TasksMax=100
持久化配置:
# /etc/systemd/system/myapp.service
[Service]
CPUQuota=50%
MemoryLimit=1G
TasksMax=100
实践示例
示例1:监控进程生命周期
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
void monitor_child(pid_t pid) {
int status;
printf("Monitoring child %d...\n", pid);
while (1) {
pid_t result = waitpid(pid, &status, WNOHANG);
if (result == 0) {
// 子进程还在运行
printf("Child %d is still running\n", pid);
sleep(1);
} else if (result == pid) {
// 子进程已终止
if (WIFEXITED(status)) {
printf("Child %d exited with code %d\n",
pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child %d killed by signal %d\n",
pid, WTERMSIG(status));
}
break;
} else {
perror("waitpid");
break;
}
}
}
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:运行10秒
printf("Child %d starting work\n", getpid());
sleep(10);
printf("Child %d finishing\n", getpid());
exit(42);
} else {
// 父进程:监控子进程
monitor_child(pid);
}
return 0;
}
示例2:进程池
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#define WORKER_COUNT 4
void worker(int id) {
printf("Worker %d (PID %d) started\n", id, getpid());
// 模拟工作
sleep(5 + (rand() % 5));
printf("Worker %d (PID %d) finished\n", id, getpid());
exit(0);
}
int main() {
pid_t workers[WORKER_COUNT];
// 创建worker进程
for (int i = 0; i < WORKER_COUNT; i++) {
pid_t pid = fork();
if (pid == 0) {
worker(i);
} else {
workers[i] = pid;
}
}
// 等待所有worker完成
for (int i = 0; i < WORKER_COUNT; i++) {
int status;
pid_t pid = waitpid(workers[i], &status, 0);
printf("Worker PID %d terminated\n", pid);
}
printf("All workers finished\n");
return 0;
}
常见问题
Q1: 如何查找并清理僵尸进程?
A:
# 查找僵尸进程
ps aux | awk '$8=="Z"'
# 找到父进程
ps -o ppid= -p <zombie_pid>
# 杀死父进程(让init收养)
kill <parent_pid>
# 或重启父进程服务
systemctl restart service_name
Q2: 进程数量有上限吗?
A: 有多个限制:
# 系统级限制
cat /proc/sys/kernel/pid_max # 最大PID
cat /proc/sys/kernel/threads-max # 最大线程数
# 用户级限制
ulimit -u # 当前用户最大进程数
# 修改限制
echo 4194304 | sudo tee /proc/sys/kernel/pid_max
Q3: fork()失败的常见原因?
A:
- 进程数量达到上限
- 内存不足
- 文件描述符耗尽
- cgroup限制
Q4: 如何优雅地终止进程?
A:
# 1. SIGTERM (15) - 优雅终止
kill <pid>
# 2. 等待10秒
sleep 10
# 3. 检查是否还在运行
if kill -0 <pid> 2>/dev/null; then
# 4. SIGKILL (9) - 强制终止
kill -9 <pid>
fi
复习题
选择题
fork()在子进程中返回什么?
- A. 子进程PID
- B. 0
- C. 父进程PID
- D. -1
僵尸进程的特征是?
- A. 占用CPU
- B. 占用内存
- C. 占用PID
- D. 不占用资源
哪个系统调用用于等待子进程?
- A. fork
- B. wait
- C. exit
- D. exec
简答题
- 解释进程和线程的区别。
- 什么是僵尸进程?如何避免?
- 描述fork() + exec()的典型用法。
- cgroup可以限制哪些资源?
实战题
编写一个程序,创建5个子进程并发执行任务,父进程等待所有子进程完成后再退出。要求正确处理SIGCHLD,不产生僵尸进程。
下一章预告: 我们将探讨网络子系统架构,从网卡接收数据包到应用程序的完整过程。