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 线程,避免了海量线程带来的内核调度和内存开销。
参考文章
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。