09. 进程与线程

深入理解进程与线程的区别、PCB 结构、线程模型与并发编程基础,掌握 Linux 进程状态与进程间通信机制。

1. 进程(Process)

1.1 定义

进程是资源分配的基本单位,拥有独立的地址空间、文件描述符、代码段、数据段和堆栈。

进程地址空间布局:

高地址 → ┌─────────────┐
         │   栈(Stack) │  ← 局部变量、函数调用,向下增长
         │     ↓       │
         │   堆(Heap)  │  ← 动态分配,向上增长
         │     ↑       │
         │ BSS(未初始化数据)│
         │ 数据段(全局/静态)│
         │ 代码段(Text)   │
低地址 → └─────────────┘

1.2 进程控制块(PCB)

PCB 是操作系统管理进程的核心数据结构:

字段说明
进程 ID(PID)唯一标识
状态就绪、运行、阻塞、创建、终止
程序计数器(PC)下一条执行指令地址
寄存器CPU 寄存器状态快照
内存信息页表/段表基址、代码/数据/堆栈地址
调度信息优先级、时间片、等待时间
文件描述符表打开的文件
信号处理待处理的信号与处理函数

1.3 进程状态转换

        创建
          ↓
       [就绪] ←────── 时间片到
          ↓          ↑
    被调度选择   [运行]
          ↓          ↓
       [运行] ───→ [阻塞](等待 I/O、信号量)
                     ↓
                I/O 完成
                     ↓
                  [就绪]

1.4 进程创建(Linux)

#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child PID: %d, Parent PID: %d\n", getpid(), getppid());
        execlp("ls", "ls", "-l", NULL);  // 替换为 ls -l
    } else {
        // 父进程
        int status;
        waitpid(pid, &status, 0);  // 等待子进程结束
        printf("Child exited with status: %d\n", WEXITSTATUS(status));
    }
    return 0;
}

fork() 使用写时复制(COW):子进程复制父进程页表,仅当写入时才复制物理页。


2. 线程(Thread)

2.1 定义

线程是 CPU 调度的基本单位,同一进程的线程共享代码段、数据段、堆和文件描述符表,仅独享线程栈、程序计数器和寄存器。

进程 vs 线程资源对比:

            进程            同一进程内的线程
地址空间     独立            共享
堆           独立            共享
全局变量     独立            共享
栈           独立            独立(线程栈)
文件描述符   独立            共享
信号处理     独立            共享

2.2 Linux 线程实现

Linux 中线程通过 clone() 实现,是轻量级进程(LWP):

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>

int thread_func(void *arg) {
    printf("Thread running, arg: %s\n", (char*)arg);
    return 0;
}

int main() {
    char stack[8192];
    // CLONE_VM: 共享地址空间
    // CLONE_FS: 共享文件系统信息
    // CLONE_FILES: 共享文件描述符表
    clone(thread_func, stack + 8192, 
          CLONE_VM | CLONE_FS | CLONE_FILES, 
          "hello");
    return 0;
}

2.3 POSIX 线程(pthread)

#include <pthread.h>
#include <stdio.h>

void *worker(void *arg) {
    int num = *(int*)arg;
    printf("Thread %d working\n", num);
    pthread_exit(NULL);
}

int main() {
    pthread_t tid;
    int arg = 42;
    
    pthread_create(&tid, NULL, worker, &arg);
    pthread_join(tid, NULL);  // 等待线程结束
    
    printf("Main thread done\n");
    return 0;
}

3. 线程模型

模型特点代表
一对一(1:1)每个用户线程对应一个内核线程Linux pthread、Windows
多对一(M:1)多个用户线程映射到一个内核线程早期 Green Thread
多对多(M:N)多用户线程映射到多内核线程Go goroutine、Erlang
1:1 模型:
  用户线程 T1 ──→ 内核线程 K1
  用户线程 T2 ──→ 内核线程 K2
  (线程切换由内核调度,线程阻塞不影响其他线程)

M:1 模型:
  用户线程 T1 ──┐
  用户线程 T2 ──┼──→ 内核线程 K1
  用户线程 T3 ──┘
  (线程切换在用户态,但一个线程阻塞全阻塞)

M:N 模型:
  用户线程 T1 ──┐     ┌──→ 内核线程 K1
  用户线程 T2 ──┼─────┼──→ 内核线程 K2
  用户线程 T3 ──┘     └──→ 内核线程 K3
  (灵活调度,结合了两种模型的优点)

4. 进程间通信(IPC)

4.1 管道(Pipe)

#include <unistd.h>
#include <stdio.h>

int main() {
    int pipefd[2];
    pipe(pipefd);
    
    pid_t pid = fork();
    if (pid == 0) {
        close(pipefd[1]);  // 子进程关闭写端
        char buf[64];
        read(pipefd[0], buf, sizeof(buf));
        printf("Child received: %s\n", buf);
        close(pipefd[0]);
    } else {
        close(pipefd[0]);  // 父进程关闭读端
        write(pipefd[1], "Hello from parent", 17);
        close(pipefd[1]);
        wait(NULL);
    }
    return 0;
}

4.2 共享内存 + 信号量

#include <sys/shm.h>
#include <sys/sem.h>

// 共享内存:最快的 IPC(无需内核拷贝)
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0666);
void *addr = shmat(shmid, NULL, 0);

// 信号量同步
int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0666);
// P 操作(等待)、V 操作(信号)
struct sembuf p = {0, -1, SEM_UNDO};
struct sembuf v = {0, 1, SEM_UNDO};
semop(semid, &p, 1);  // P
// 访问共享内存...
semop(semid, &v, 1);  // V

4.3 消息队列

#include <sys/msg.h>

struct msg_buffer {
    long mtype;
    char mtext[256];
};

int msgid = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
struct msg_buffer msg = {1, "Hello"};
msgsnd(msgid, &msg, sizeof(msg.mtext), 0);
msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0);

4.4 IPC 方式对比

方式速度数据格式同步方式适用场景
管道中字节流读写阻塞父子进程通信
命名管道(FIFO)中字节流读写阻塞无亲缘关系进程
共享内存最快(无拷贝)原始内存需额外同步大量数据、高性能
消息队列中结构化消息自带同步异步消息、解耦
信号量——同步原语互斥/同步
Socket较慢字节流/报文自带跨网络、跨机器
信号快整数异步中断事件通知

5. 并发 vs 并行

单核 CPU:
  ┌───┬───┬───┬───┬───┬───┐
  │ P1│ P2│ P1│ P3│ P2│ P1│  → 并发(Concurrency)
  └───┴───┴───┴───┴───┴───┘     任务交替执行,逻辑上同时

多核 CPU:
  Core 1: ┌───┬───┬───┐
          │ P1│ P1│ P1│        → 并行(Parallelism)
  Core 2: ├───┼───┼───┤         任务真正同时执行
          │ P2│ P2│ P2│
          └───┴───┴───┘

6. 现代 CPU 架构与线程性能

在当代多核处理器上,线程调度的效率与硬件架构密切相关。理解这些底层机制,有助于写出真正高性能的并发程序。

6.1 CPU 缓存与伪共享

现代 CPU 采用三级缓存架构(L1/L2/L3)。当多个线程读写同一缓存行(通常为 64 字节)中的不同变量时,即使变量逻辑上独立,也会因为缓存一致性协议(MESI)导致频繁失效,性能急剧下降。

// 伪共享(False Sharing)示例
struct Counter {
    volatile long count1;  // 线程 A 高频写入
    volatile long count2;  // 线程 B 高频写入
    // count1 和 count2 极可能落入同一缓存行
};

解决方案:使用内存填充(Padding)确保变量各占独立缓存行。

#define CACHE_LINE 64

struct PaddedCounter {
    volatile long count;
    char padding[CACHE_LINE - sizeof(long)];  // 填充至 64 字节
} __attribute__((aligned(CACHE_LINE)));

6.2 NUMA 架构下的线程亲和性

Non-Uniform Memory Access(NUMA)架构中,每个 CPU socket 拥有本地内存,访问远端内存延迟更高。盲目跨 NUMA 节点调度线程会导致性能损失。

# Linux 查看 NUMA 拓扑
numactl --hardware

# 将进程绑定到特定 NUMA 节点的 CPU 和内存
numactl --cpunodebind=0 --membind=0 ./app

Go 语言 runtime 在 Go 1.5 后引入了 NUMA 感知的调度器改进,Java 15+ 的 G1GC 也支持 NUMA 区域的优化分配。对于 C/C++ 高性能计算程序,显式绑定线程到物理核心是常见的调优手段。

6.3 协程(Coroutine)与用户态线程

协程是比操作系统线程更轻量级的并发单元,其调度完全在用户态完成:

特性OS 线程 (pthread)协程 (goroutine/kotlin-coroutine)
栈大小默认 1-8 MB初始 2KB,动态增长
切换开销量级:微秒(需切内核态)量级:纳秒-微秒(纯用户态)
调度内核抢占式用户协作式 / M:N 调度
最大并发数数千级(受内存限制)数百万级

在 Go 中,Goroutine 通过 G-M-P 模型(Goroutine- Machine- Processor)映射到少量 OS 线程上运行。当 goroutine 阻塞在系统调用或 channel 时,Go 调度器(runtime.scheduler)会自动将其从 OS 线程上剥离,切换其他 goroutine 继续运行。这是 M:N 线程模型的成熟工业实现。

这种设计解耦了并发量与线程数:一个 Go 程序可以创建数十万个并发 goroutine,实际只占用少量(通常等于 CPU 核心数)的 OS 线程,避免了海量线程带来的内核调度和内存开销。

参考文章

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「计算机基础」更多文章

  1. 46. 排队论与容量估算:利特尔法则与尾延迟
  2. 45. 编译器优化与中间表示:SSA、内联与循环优化
  3. 44. 并发模型对比:Actor、CSP 与数据并行