专栏文章
专栏文章
Linux 专栏
1. Linux 专栏 #01:进程与线程 2. Linux 专栏 #02:内存管理 3. Linux 专栏 #03:文件系统 4. Linux 专栏 #04:网络与 IO 模型 5. Linux 专栏 #05:中断机制 6. Linux 专栏 #05:性能分析工具 7. Linux 专栏 #06:eBPF 技术实战

Linux 专栏 #01:进程与线程

发布于 2026-06-08 07:30 👁 10 次阅读
#操作系统#linux

进程与线程

进程是资源分配的最小单位,线程是 CPU 调度的最小单位。理解二者的区别与协作机制,是写出高并发 Java/Go 程序的基础。本文覆盖进程状态机、IPC 方式、线程同步原语、fork/exec 语义以及僵尸进程的成因与处理。


目录

章节 说明
进程 vs 线程 vs 协程 三者本质差异对比
进程状态机 5 种状态及转换条件
进程生命周期 fork/exec/wait/exit
进程间通信(IPC) 管道/信号/共享内存/Socket/消息队列
线程同步 mutex/semaphore/条件变量
信号处理机制 信号的发送与捕获
孤儿进程与僵尸进程 成因与解决方案

进程 vs 线程 vs 协程

process vs thread

维度 进程 线程 协程
资源隔离 独立地址空间、文件描述符 共享进程地址空间 共享线程栈(用户态调度)
切换开销 高(页表切换、TLB 刷新) 中(保存/恢复寄存器) 极低(用户态切换)
创建开销 高(fork 复制页表) 中(pthread_create) 极低
通信方式 IPC(管道/共享内存等) 共享全局变量(需加锁) channel / yield
故障隔离 一个进程崩溃不影响其他进程 一个线程崩溃可能导致整个进程崩溃 同线程内,异常需自己处理
Java 对应 JVM 进程 Thread / ForkJoinPool Virtual Thread(JDK 21)
Go 对应 os/exec.Cmd goroutine(底层用线程池) goroutine 本质是协程

一句话:进程是"公司",线程是"员工",协程是"任务切换不需要换人"。


进程状态机

process state

Linux 进程有 5 种核心状态:

          fork()
            |
            v
        [TASK_NEW]
            |
            v
  ┌──── [RUNNING] ────┐
  │   (就绪/运行中)    │
  │         │          │
  │    调度  │  时间片耗尽│
  │         v          │
  │   [CPU 执行中]     │
  │                    │
  │  等待 I/O / 锁     │
  └──→ [INTERRUPTIBLE] │
  └──→ [UNINTERRUPTIBLE]│  (D 状态,不可被信号中断)
            │
         等待结束
            v
        [ZOMBIE]  ←── exit() 后等待父进程 wait()
            │
         父进程 wait()
            v
         [消亡]
状态 ps 显示 说明
运行/就绪 R 正在 CPU 上运行或等待调度
可中断睡眠 S 等待事件(I/O、信号),可被信号唤醒
不可中断睡眠 D 等待 I/O 完成,不能被 kill,常见于磁盘 I/O
僵尸 Z 已退出但父进程未 wait()
停止 T 被 SIGSTOP 或调试器暂停
# 查看进程状态
ps aux
# 查看 D 状态进程(不可中断,通常是 I/O 问题)
ps aux | awk '$8=="D"'

进程生命周期

fork / exec / wait

// fork:复制父进程,返回值区分父子
pid_t pid = fork();
if (pid == 0) {
    // 子进程:执行新程序
    execvp("ls", args);   // 替换进程映像
} else {
    // 父进程:等待子进程结束
    int status;
    waitpid(pid, &status, 0);
}

关键语义

写时复制(Copy-on-Write):fork 后父子进程共享物理页,只有在写入时才真正复制,大幅降低 fork 开销。

Java/Go 中的对应

// Java 创建子进程
ProcessBuilder pb = new ProcessBuilder("ls", "-l");
Process p = pb.start();
int exitCode = p.waitFor();
// Go 创建子进程
cmd := exec.Command("ls", "-l")
output, err := cmd.Output()  // 等价于 fork + exec + wait

进程间通信(IPC)

1. 管道(Pipe)

# 匿名管道:父子进程间,单向
ps -ef | grep java | awk '{print $2}' | xargs kill -9

# 命名管道(FIFO):任意进程间
mkfifo /tmp/mypipe
echo "hello" > /tmp/mypipe &   # 写端阻塞直到有读端
cat < /tmp/mypipe               # 读端

特点:单向、基于字节流、内核缓冲区有限(默认 64KB)。

2. 消息队列(Message Queue)

System V IPC,支持有类型的消息,允许接收方按类型选择读取。

# 查看系统消息队列
ipcs -q
# 删除
ipcrm -q <msqid>

3. 共享内存(Shared Memory)

最快的 IPC 方式,多个进程映射同一块物理内存,需配合信号量同步

# 查看共享内存
ipcs -m

4. 信号量(Semaphore)

用于进程间同步,不传递数据。P 操作(-1)、V 操作(+1)。

5. Socket

跨机器通信的标准方式,Unix Domain Socket 用于本机进程间通信(比 TCP 快,无需网络协议栈)。

# 查看 Unix Domain Socket
ss -xp

6. 信号(Signal)

异步通知机制,用于通知进程某事件发生。

IPC 方式对比

方式 速度 跨机器 使用场景
管道 命令行组合、父子进程
消息队列 有结构化消息需求
共享内存 最快 高频大量数据交换
Socket 分布式系统、微服务
信号 异步通知(kill、Ctrl+C)

线程同步

Mutex(互斥锁)

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&lock);
// 临界区:同一时刻只有一个线程执行
shared_data++;
pthread_mutex_unlock(&lock);

Java 对应synchronizedReentrantLock Go 对应sync.Mutex

Semaphore(信号量)

控制同时访问某资源的线程数量(Mutex 是 Semaphore 值为 1 的特例)。

Java 对应java.util.concurrent.Semaphore

条件变量(Condition Variable)

线程等待某条件成立,避免忙等(busy-wait)。

pthread_mutex_lock(&lock);
while (!condition_met) {
    pthread_cond_wait(&cond, &lock);  // 原子释放锁并等待
}
// 条件满足,执行逻辑
pthread_mutex_unlock(&lock);

// 另一个线程满足条件后:
pthread_cond_signal(&cond);  // 唤醒一个等待线程

Java 对应Object.wait() / notify() / notifyAll(),或 Condition.await() / signal()

线程数据分类

类型 说明 Java 对应
线程栈本地数据 函数局部变量,天然隔离 方法内局部变量
进程全局数据 所有线程共享,需加锁 static 字段
线程私有数据(TSD) pthread_key_create ThreadLocal<T>

Java ThreadLocal 注意:线程池场景下,线程复用导致 ThreadLocal 值残留,使用完必须调用 remove(),否则内存泄漏且数据污染。


信号处理机制

常用信号:

信号 编号 默认行为 触发场景
SIGTERM 15 终止 kill <pid>,优雅关闭
SIGKILL 9 强制终止 kill -9不可捕获
SIGINT 2 终止 Ctrl+C
SIGHUP 1 终止/重新加载 终端断开,Nginx reload
SIGCHLD 17 忽略 子进程状态变化
SIGSEGV 11 核心转储 段错误(空指针/越界)
# 发送信号
kill -SIGTERM <pid>     # 优雅终止
kill -SIGKILL <pid>     # 强制终止
kill -SIGHUP <pid>      # 重新加载配置(如 Nginx)

# 查看进程收到的信号
cat /proc/<pid>/status | grep -i sig

Java 中注册 ShutdownHook(对应 SIGTERM)

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    // 优雅关闭:关闭连接池、刷新缓存等
    log.info("Shutting down gracefully...");
}));

孤儿进程与僵尸进程

僵尸进程(Zombie)

子进程已退出,但父进程未调用 wait() 回收,进程描述符仍占用内核资源。

危害:大量僵尸进程会耗尽 PID 资源(Linux 默认最大 PID 约 32768)。

# 查找僵尸进程
ps aux | grep 'Z'

# 找到僵尸进程的父进程
ps -o ppid= -p <zombie_pid>

# 通知父进程回收(发送 SIGCHLD)
kill -SIGCHLD <parent_pid>

# 若父进程无法修改,只能杀死父进程
kill -9 <parent_pid>

Java 中防止僵尸进程

Process p = Runtime.getRuntime().exec("cmd");
// 必须消费输出流,否则可能阻塞
p.getInputStream().transferTo(OutputStream.nullOutputStream());
int exitCode = p.waitFor();  // 必须调用,否则子进程成为僵尸

孤儿进程(Orphan)

父进程先于子进程退出,子进程被 init 进程(PID=1)收养,由 init 负责 wait(),不会产生僵尸。



Linux 调度器原理

调度类层次

Linux 为不同优先级的任务设置了多个调度类(Scheduling Class),内核按以下顺序选择下一个运行的任务:

Deadline(dl_rq)> Realtime(rt_rq)> Fair(cfs_rq)> Idle

每个 CPU 维护一个运行队列(runqueue),其中包含这三类子队列。

CFS 完全公平调度器

CFS(Completely Fair Scheduler) 是 Linux 默认的普通进程调度器,目标是让所有进程"公平"地使用 CPU。

核心数据结构

struct sched_entity {
    struct load_weight  load;           // 调度实体权重(与 nice 值相关)
    struct rb_node      run_node;       // 红黑树节点
    u64                 exec_start;     // 上次被调度的时间
    u64                 sum_exec_runtime; // 累计真实执行时间
    u64                 vruntime;       // 虚拟运行时间(CFS 核心)
};

vruntime(虚拟运行时间)

vruntime 是 CFS 的灵魂。它代表"进程在理想 CPU 上已运行的时间",计算公式为: vruntime += 实际运行时间 × (NICE_0_LOAD / 进程权重)

红黑树(按 vruntime 排序)
        [vruntime=100]
       /              \
[vruntime=80]    [vruntime=150]
  ↑
  下一个运行的进程(vruntime 最小)

nice 值与权重对应关系(部分):

nice 值 权重 相对 nice=0 的 CPU 时间比
-20(最高) 88761 ~3.5x
0(默认) 1024 1x
19(最低) 15 ~0.015x
# 设置进程 nice 值(-20 到 19)
nice -n -5 command          # 启动时设置
renice -n 5 -p <pid>        # 运行中调整

# 查看进程 nice 值(NI 列)
top
ps -o pid,ni,comm -p <pid>

实时调度(SCHED_RT / SCHED_FIFO)

对延迟敏感的任务可设置为实时调度策略,实时任务总是优先于普通 CFS 任务执行。

策略 说明 优先级范围
SCHED_FIFO 先进先出,不会被同级打断,只有更高优先级或主动让出才切换 1-99(99 最高)
SCHED_RR 轮转,同优先级进程轮流执行,每次有时间片限制 1-99
SCHED_NORMAL 默认 CFS 调度 nice -20 到 19
# 将进程设置为 SCHED_FIFO,优先级 50
chrt -f -p 50 <pid>

# 查看进程调度策略
chrt -p <pid>

# 查看实时优先级(PR 列,负数表示实时)
top

注意:实时进程如果出现死循环,会饿死所有普通进程。内核通过 kernel.sched_rt_runtime_us 参数限制实时进程最多占用 95% 的 CPU 时间(默认值)。

调度域与 NUMA 亲和性

现代多处理器系统(NUMA)中,CPU 被组织成多个调度域(Scheduling Domain):

NUMA 节点 0              NUMA 节点 1
┌──────────────┐         ┌──────────────┐
│  CPU 0-7     │ ←慢→   │  CPU 8-15    │
│  本地内存    │         │  本地内存    │
└──────────────┘         └──────────────┘
# 绑定进程到指定 CPU(CPU 亲和性)
taskset -c 0,1 command          # 启动时绑定到 CPU 0 和 1
taskset -cp 0-3 <pid>           # 运行中修改

# 查看进程的 CPU 亲和性
taskset -p <pid>

# NUMA 感知的进程绑定(绑定到 NUMA 节点 0)
numactl --cpunodebind=0 --membind=0 command

# 查看 NUMA 拓扑
numactl --hardware

Java 服务的 NUMA 优化:JVM 支持 NUMA 感知的堆分配:

java -XX:+UseNUMA -XX:+UseParallelGC -jar app.jar

参考资料

  • 《趣谈 Linux 操作系统》— 10 进程、11 线程、36-42 IPC 系列(刘超,极客时间)
  • 《操作系统实战 45 讲》— 25-27 进程调度(LMOS,极客时间)
  • 《Linux 内核技术实战课》— 17 CPU 执行任务(邵亚方,极客时间)
  • 《Linux 性能优化实战》— 07-08 不可中断进程和僵尸进程(倪朋飞,极客时间)
  • 《Linux Kernel Development》— Robert Love
  • 从 C10K 到协程(多进程/多线程的局限是协程诞生的动机)
  • 协程的实现机制(协程上下文切换与 OS 线程切换的对比)
← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论