Featured image of post 进程管理:Linux 的生命线

进程管理:Linux 的生命线

深入理解 Linux 进程管理、进程生命周期、CFS 调度器、进程控制以及性能监控工具

进程管理: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

进程的特征

  1. 独立性:每个进程都有独立的地址空间
  2. 动态性:进程是动态创建和销毁的
  3. 并发性:多个进程可以同时运行
  4. 异步性:进程执行速度不可预测

进程描述符(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 系统的核心功能。通过这篇文章,我们了解了:

核心概念

  1. 进程 vs 程序:静态 vs 动态
  2. task_struct:内核中的进程描述符
  3. 进程生命周期:从创建到销毁
  4. fork/exec/wait:进程控制三部曲
  5. CFS 调度器:公平分配 CPU 时间

实践技能

  • 使用 pstophtop 监控进程
  • 通过 killkillall 控制进程
  • 理解进程状态和转换
  • 分析进程性能问题

下一步

在后续文章中,我们将学习:

  • 内存管理:虚拟内存、分页、交换
  • 文件系统:VFS、inode、文件权限
  • 网络编程:Socket、TCP/IP

“Processes are the living embodiment of running programs.” — Michael Kerrisk

掌握进程管理是理解 Linux 系统的关键一步。继续深入探索,你会发现更多奥秘!


← 返回系列概述: Linux 概述:从 Unix 到开源革命


参考资源

A winner is just a loser who tried one more time.
Robust AI
使用 Hugo 构建
主题 StackJimmy 设计