进程管理:Linux 的生命线
引言
进程是 Linux 系统中正在运行的程序的实例。理解进程管理对于系统管理员、开发者和 DevOps 工程师至关重要。在这篇文章中,我们将深入探讨 Linux 如何创建、调度和管理进程。
什么是进程?
进程 vs 程序
- 程序(Program):存储在磁盘上的可执行文件(静态)
- 进程(Process):正在内存中运行的程序实例(动态)
flowchart LR
A[程序<br/>磁盘上的可执行文件] -->|execve| B[进程<br/>内存中的运行实例]
B -->|fork| C[子进程]
B -->|exit| D[僵尸进程]
style A fill:#e3f2fd
style B fill:#fff3e0
style C fill:#c8e6c9
style D fill:#ffcdd2
进程的特征
- 独立性:每个进程都有独立的地址空间
- 动态性:进程是动态创建和销毁的
- 并发性:多个进程可以同时运行
- 异步性:进程执行速度不可预测
进程描述符(task_struct)
内核使用 task_struct 结构体来描述进程:
struct task_struct {
// 进程状态
volatile long state; // 运行状态
int exit_state; // 退出状态
// 进程标识
pid_t pid; // 进程ID
pid_t tgid; // 线程组ID
// 进程关系
struct task_struct *real_parent; // 真实父进程
struct task_struct *parent; // 当前父进程
struct list_head children; // 子进程链表
// 调度相关信息
struct sched_entity se; // 调度实体
int prio; // 优先级
int static_prio; // 静态优先级
int normal_prio; // 标准优先级
unsigned int rt_priority; // 实时优先级
// 内存管理
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;
// 时间信息
cputime_t utime, stime; // 用户态和内核态时间
// ... 更多字段
};
进程标识符
# 查看进程 PID
ps -eo pid,cmd | head
# 查看当前 Shell 的 PID
echo $$
# 查看父进程 PID
echo $PPID
# 查看进程组 ID
ps -eo pid,pgid,cmd | head
关键标识符:
- PID:进程ID(1-32768,循环使用)
- PPID:父进程ID
- PGID:进程组ID
- SID:会话ID
进程生命周期
进程状态
Linux 进程有以下几种状态:
stateDiagram-v2
[*] --> 创建: fork()
创建 --> 运行: 调度器选中
运行 --> 可中断睡眠: 等待I/O事件
运行 --> 不可中断睡眠: 磁盘I/O
运行 --> 僵尸: exit()
运行 --> 停止: SIGSTOP
可中断睡眠 --> 运行: 事件完成
不可中断睡眠 --> 运行: I/O完成
停止 --> 运行: SIGCONT
僵尸 --> [*]: wait()
| 状态 | 内核定义 | 描述 |
|---|---|---|
| 运行 | TASK_RUNNING | 正在运行或等待运行 |
| 可中断睡眠 | TASK_INTERRUPTIBLE | 等待某些条件,可被信号中断 |
| 不可中断睡眠 | TASK_UNINTERRUPTIBLE | 等待某些条件,不可被中断 |
| 僵尸 | TASK_DEAD | 已终止但父进程未读取退出状态 |
| 停止 | TASK_TRACED/STOPPED | 被调试器暂停或收到 SIGSTOP |
查看进程状态
# 查看所有进程状态
ps aux | head
# STAT 列含义:
# R - Running
# S - Sleeping (interruptible)
# D - Disk sleep (uninterruptible)
# Z - Zombie
# T - Stopped
# + - 前台进程组
# s - 会话leader
# l - 多线程
# N - 低优先级
# < - 高优先级
# 查看特定进程状态
cat /proc/1/status | grep State
进程创建
fork() 系统调用
fork() 创建一个几乎与父进程相同的子进程:
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
int main() {
pid_t pid;
printf("Before fork()\n");
pid = fork(); // 创建子进程
if (pid == -1) {
// fork 失败
perror("fork failed");
return 1;
} else if (pid == 0) {
// 子进程代码
printf("Child process: PID = %d, PPID = %d\n",
getpid(), getppid());
} else {
// 父进程代码
printf("Parent process: PID = %d, Child PID = %d\n",
getpid(), pid);
}
printf("Common code\n");
return 0;
}
fork() 的工作原理:
flowchart TD
A[父进程] -->|fork| B{fork成功?}
B -->|是| C[创建子进程]
B -->|否| E[返回-1]
C --> D[复制父进程资源]
D --> F[返回子进程PID给父进程]
D --> G[返回0给子进程]
写时复制(Copy-on-Write)
现代 Linux 使用 COW 优化 fork 性能:
┌─────────────────────────────────────────────────┐
│ fork() 之前 │
│ ┌───────────────────────────────────────┐ │
│ │ 父进程内存 │ │
│ │ [代码段 | 数据段 | 堆 | 栈] │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ fork() 之后 │
│ ┌──────────────────────┐ ┌─────────────────┐ │
│ │ 父进程 │ │ 子进程 │ │
│ │ [共享只读数据] │ │ [共享只读数据] │ │
│ │ ┌──────────────┐ │ │ ┌───────────┐ │ │
│ │ │ 页表(只读) │ │ │ │ 页表(只读)│ │ │
│ │ └──────────────┘ │ │ └───────────┘ │ │
│ └──────────────────────┘ └─────────────────┘ │
│ │
│ 写入时才复制物理页面 │
└─────────────────────────────────────────────────┘
exec() 系列函数
exec() 用新程序替换当前进程的内存映像:
#include <unistd.h>
#include <stdio.h>
int main() {
printf("Before exec\n");
// 执行 ls 命令
execlp("ls", "ls", "-la", "/tmp", NULL);
// exec 成功则不会执行这里
perror("exec failed");
return 1;
}
exec 函数族:
// 带p的函数在PATH环境变量中搜索程序
execlp(const char *file, const char *arg, ...);
execvp(const char *file, char *const argv[]);
// 不带p的需要完整路径
execl(const char *path, const char *arg, ...);
execv(const char *path, char *const argv[]);
// 带l的函数使用列表传递参数
execl("/bin/ls", "ls", "-la", NULL);
// 带v的函数使用数组传递参数
char *args[] = {"ls", "-la", "/tmp", NULL};
execv("/bin/ls", args);
fork + exec 组合
这是 Shell 创建新进程的标准模式:
sequenceDiagram
participant S as Shell
participant K as Kernel
participant N as New Program
S->>K: fork()
Note over K: 创建子进程<br/>复制Shell
K->>S: 返回子进程PID
S->>K: exec(new_program)
Note over K: 替换子进程内存<br/>加载新程序
K->>N: 启动新程序
N->>S: 程序结束
S->>K: wait() 回收子进程
wait() 和 waitpid()
父进程等待子进程结束:
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
pid_t pid;
int status;
pid = fork();
if (pid == 0) {
// 子进程
printf("Child running...\n");
sleep(2);
exit(42); // 退出码
} else {
// 父进程
printf("Parent waiting for child...\n");
wait(&status); // 等待任意子进程
if (WIFEXITED(status)) {
printf("Child exited with status: %d\n",
WEXITSTATUS(status));
}
}
return 0;
}
等待宏:
WIFEXITED(status):子进程正常退出WEXITSTATUS(status):获取退出码WIFSIGNALED(status):子进程被信号终止WTERMSIG(status):获取终止信号
进程调度
CFS(完全公平调度器)
CFS 的设计目标:公平地分配 CPU 时间
虚拟运行时间(vruntime)
// vruntime 计算简化版
vruntime += (delta_exec * NICE_0_LOAD) / load.weight;
// delta_exec: 实际运行时间
// NICE_0_LOAD: nice=0 时的权重(1024)
// load.weight: 进程权重
flowchart LR
A[运行1秒] --> B{nice值?}
B -->|nice=0<br/>weight=1024| C[vruntime += 1s]
B -->|nice=5<br/>weight=312| D[vruntime += 3.2s]
B -->|nice=-5<br/>weight=3121| E[vruntime += 0.32s]
style C fill:#fff3e0
style D fill:#ffcdd2
style E fill:#c8e6c9
红黑树管理可运行进程
// CFS 运行队列
struct cfs_rq {
struct load_weight load;
u64 exec_clock;
u64 min_vruntime;
struct rb_root tasks_timeline; // 红黑树根节点
struct rb_node *rb_leftmost; // 最左节点(最小vruntime)
struct task_struct *curr; // 当前运行进程
};
// 选择下一个运行的进程
static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq) {
struct rb_node *left = cfs_rq->rb_leftmost;
return rb_entry(left, struct sched_entity, run_node);
}
调度流程:
flowchart TD
A[时钟中断] --> B[更新当前进程vruntime]
B --> C[检查是否需要调度]
C -->|不需要| D[继续运行]
C -->|需要| E[选择vruntime最小的进程]
E --> F[切换到新进程]
F --> G[新进程开始运行]
style E fill:#fff3e0
调度策略
# 查看进程调度策略
ps -eo pid,cmd,policy,rtprio,ni | grep -E "PID|ksoft|kworker"
# POLICY 列含义:
# FF - FIFO 实时调度
# RR - 轮转实时调度
# TS - 分时调度(普通进程)
# IDL - 空闲进程
# 查看实时进程
ps -eo pid,cmd,policy,rtprio | awk '$3 ~ /FF|RR/'
修改进程优先级:
# nice 值:-20(最高优先级)到 19(最低优先级)
# 默认 nice 值为 0
# 以指定 nice 值启动进程
nice -n 10 tar -czf backup.tar.gz /home
# renice:修改运行进程的优先级
renice +5 -p 1234 # 将 PID 1234 的 nice 值改为 +5
# 查看进程 nice 值
ps -eo pid,ni,cmd | head
CPU 亲和性
绑定进程到特定 CPU 核心:
# 查看进程 CPU 亲和性
taskset -p 1234 # 显示 PID 1234 的 CPU 亲和性
# pid 1234's current affinity mask: ff (所有8个CPU)
# 设置进程 CPU 亲和性
taskset -p 3 1234 # 绑定到 CPU 0 和 1(二进制 11)
taskset -pc 0,2,4 1234 # 绑定到 CPU 0, 2, 4
# 启动进程时指定 CPU 亲和性
taskset -c 0,1 ./myapp # 在 CPU 0 和 1 上运行
# 查看各 CPU 负载
mpstat -P ALL 1
进程监控工具
ps 命令
# 基本用法
ps aux # BSD 风格
ps -ef # System V 风格
# 常用组合
ps aux | grep nginx # 查找 nginx 进程
ps -eo pid,ppid,cmd,%mem,%cpu --sort=-%mem | head
# 按内存使用排序
# 查看进程树
ps axjf # ASCII 艺术进程树
ps -e --forest # 进程森林
# 查看特定用户进程
ps -u username
# 查看完整命令
ps auxww # 不截断命令行
top/htop
# top 命令
top
# 按 P:按 CPU 排序
# 按 M:按内存排序
# 按 T:按时间排序
# 按 k:杀死进程
# 按 r:renice
# 按 q:退出
# htop(更友好)
htop
# F1-F10:功能键
# /:搜索进程
# F9:调整进程优先级
# F6:排序方式
pidstat
# 安装 sysstat 包
sudo apt install sysstat
# CPU 使用统计
pidstat 2 5 # 每2秒输出一次,共5次
# 显示特定进程
pidstat -p 1234 1
# 显示线程统计
pidstat -t -p 1234
# 显示上下文切换
pidstat -w -p 1234
# 显示缺页异常
pidstat -r -p 1234
/proc 文件系统
# 查看进程信息
ls /proc/1234/
cat /proc/1234/status # 进程状态
cat /proc/1234/cmdline # 命令行参数
cat /proc/1234/environ # 环境变量
cat /proc/1234/maps # 内存映射
cat /proc/1234/fd # 文件描述符
ls -l /proc/1234/fd/ # 查看打开的文件
# 实时监控
watch -n 1 'cat /proc/1234/status | grep -E "VmSize|VmRSS"'
进程控制
发送信号
# 常用信号
# SIGTERM (15): 优雅终止
# SIGKILL (9): 强制终止
# SIGHUP (1): 重新加载配置
# SIGSTOP (19): 暂停进程
# SIGCONT (18): 继续进程
# kill 命令
kill 1234 # 默认发送 SIGTERM
kill -9 1234 # 强制终止
kill -HUP 1234 # 重新加载
# killall:按名称杀死进程
killall nginx
killall -9 firefox
# pkill:按模式匹配
pkill -f "python.*script"
pkill -u username # 杀死用户的所有进程
# 查看可用信号
kill -l
nohup 和 disown
# nohup:忽略 SIGHUP 信号
nohup ./long_process & # 终端关闭后进程继续运行
# disown:从作业列表中移除
./process &
Ctrl+Z # 暂停
bg # 后台运行
disown -h %1 # 忽略 SIGHUP
# tmux/screen:更好的会话管理
tmux new -s session # 创建新会话
实战案例
查找并杀死僵尸进程
#!/bin/bash
# kill_zombies.sh
# 查找僵尸进程
echo "Checking for zombie processes..."
ps aux | awk '{print $8, $2}' | grep -w Z | while read state pid; do
if [ -n "$pid" ]; then
echo "Found zombie process: PID $pid"
# 查找父进程
ppid=$(ps -o ppid= -p $pid | tr -d ' ')
echo "Parent PID: $ppid"
# 通知父进程回收子进程
kill -CHLD $ppid
sleep 1
# 如果仍然存在,杀死父进程
if ps -p $pid > /dev/null; then
echo "Zombie still exists, killing parent..."
kill -9 $ppid
fi
fi
done
进程监控脚本
#!/bin/bash
# monitor_process.sh
PROCESS_NAME=$1
INTERVAL=5
if [ -z "$PROCESS_NAME" ]; then
echo "Usage: $0 <process_name>"
exit 1
fi
while true; do
clear
echo "=== Monitoring: $PROCESS_NAME ==="
echo "Time: $(date)"
echo ""
pid=$(pgrep -o "$PROCESS_NAME")
if [ -n "$pid" ]; then
echo "PID: $pid"
echo ""
# CPU 和内存使用
ps -p $pid -o %cpu,%mem,cmd
echo ""
echo "--- CPU History ---"
pidstat -p $pid 1 1 | tail -1
echo ""
echo "--- Memory Usage ---"
grep -E "VmSize|VmRSS" /proc/$pid/status
echo ""
echo "--- File Descriptors ---"
ls /proc/$pid/fd | wc -l
else
echo "Process not found!"
fi
sleep $INTERVAL
done
限制进程资源
# ulimit:查看和设置资源限制
ulimit -a # 查看所有限制
ulimit -n 4096 # 设置文件描述符限制
ulimit -u 1024 # 设置进程数限制
# cgroups v2:更强大的资源控制
# 创建 cgroup
sudo mkdir /sys/fs/cgroup/myapp
# 设置内存限制
echo 500M > /sys/fs/cgroup/myapp/memory.max
# 设置 CPU 限制
echo 2 > /sys/fs/cgroup/myapp/cpu.max
# 运行程序
sudo cgexec -g memory,cpu:myapp ./myapp
总结
进程管理是 Linux 系统的核心功能。通过这篇文章,我们了解了:
核心概念
- 进程 vs 程序:静态 vs 动态
- task_struct:内核中的进程描述符
- 进程生命周期:从创建到销毁
- fork/exec/wait:进程控制三部曲
- CFS 调度器:公平分配 CPU 时间
实践技能
- 使用
ps、top、htop监控进程 - 通过
kill、killall控制进程 - 理解进程状态和转换
- 分析进程性能问题
下一步
在后续文章中,我们将学习:
- 内存管理:虚拟内存、分页、交换
- 文件系统:VFS、inode、文件权限
- 网络编程:Socket、TCP/IP
“Processes are the living embodiment of running programs.” — Michael Kerrisk
掌握进程管理是理解 Linux 系统的关键一步。继续深入探索,你会发现更多奥秘!
← 返回系列概述: Linux 概述:从 Unix 到开源革命
参考资源: