HiHuo
首页
博客
手册
工具
首页
博客
手册
工具
  • 系统底层修炼

    • 操作系统核心知识学习指南
    • CPU调度与上下文切换
    • CFS调度器原理与源码
    • 内存管理与虚拟内存
    • PageCache与内存回收
    • 文件系统与IO优化
    • 零拷贝与Direct I/O
    • 网络子系统架构
    • TCP协议深度解析
    • TCP问题排查实战
    • 网络性能优化
    • epoll与IO多路复用
    • 进程与线程管理
    • Go Runtime调度器GMP模型
    • 系统性能分析方法论
    • DPDK与用户态网络栈
    • eBPF与内核可观测性
    • 综合实战案例

进程与线程管理

章节概述

进程和线程是操作系统最核心的概念。本章将深入探讨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]

状态详解:

状态标志说明
RunningR正在运行或等待运行
SleepingS可中断睡眠(等待事件)
UninterruptibleD不可中断睡眠(通常是I/O)
StoppedT收到SIGSTOP信号
Tracedt被调试器追踪
ZombieZ已终止,等待父进程回收
DeadX完全终止

查看进程状态:

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:

  1. 进程数量达到上限
  2. 内存不足
  3. 文件描述符耗尽
  4. 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

复习题

选择题

  1. fork()在子进程中返回什么?

    • A. 子进程PID
    • B. 0
    • C. 父进程PID
    • D. -1
  2. 僵尸进程的特征是?

    • A. 占用CPU
    • B. 占用内存
    • C. 占用PID
    • D. 不占用资源
  3. 哪个系统调用用于等待子进程?

    • A. fork
    • B. wait
    • C. exit
    • D. exec

简答题

  1. 解释进程和线程的区别。
  2. 什么是僵尸进程?如何避免?
  3. 描述fork() + exec()的典型用法。
  4. cgroup可以限制哪些资源?

实战题

编写一个程序,创建5个子进程并发执行任务,父进程等待所有子进程完成后再退出。要求正确处理SIGCHLD,不产生僵尸进程。


下一章预告: 我们将探讨网络子系统架构,从网卡接收数据包到应用程序的完整过程。

Prev
epoll与IO多路复用
Next
Go Runtime调度器GMP模型