mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-11-01 03:23:42 +00:00
Update 计算机操作系统 - 进程管理.md
补充 Linux 中进程与线程的创建方式、有栈与无栈协程,附录 A4。
This commit is contained in:
+49
-1
@@ -31,6 +31,7 @@
|
||||
* [A1 上下文切换](#a1-上下文切换)
|
||||
* [A2 平台线程与虚拟线程的开销](#a2-平台线程与虚拟线程的开销)
|
||||
* [A3 POSIX 共享内存](#a3-posix-共享内存)
|
||||
* [A4 fork 与 pthread_create 的 clone 标志](#a4-fork-与-pthread_create-的-clone-标志)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
@@ -69,6 +70,15 @@ QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 H
|
||||
- 通信方面:线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC;
|
||||
- 健壮性:进程之间相互隔离,一个进程崩溃不影响其它进程,而一个线程访问非法内存触发段错误时,整个进程连同其它线程都会被终止。
|
||||
|
||||
在 Linux 内核中,进程和线程都用 task_struct 表示,都是调度的对象,区别只在于和创建者共享了多少资源。fork 和 pthread_create 最终都调用 clone 系统调用,只是传入的标志不同(strace 实测,见[附录 A4](#a4-fork-与-pthread_create-的-clone-标志)):
|
||||
|
||||
```text
|
||||
fork() clone(flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD)
|
||||
pthread_create() clone(flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|...)
|
||||
```
|
||||
|
||||
CLONE_VM 表示共享地址空间,CLONE_FILES 表示共享文件描述符表,CLONE_THREAD 表示加入创建者的线程组。没有这些标志时,子进程得到一份地址空间的副本(写时复制),修改全局变量不影响父进程;有这些标志时,新线程修改的就是同一个变量。getpid() 返回的是线程组 ID,同一进程中的线程都相同,gettid() 返回每个线程自己的 ID,top -H、ps -L 中看到的就是它。
|
||||
|
||||
因此选择多进程还是多线程,主要看更需要隔离还是共享:
|
||||
|
||||
- 任务之间需要隔离、一个出错不能拖垮其它任务时用多进程,例如 Chrome 把网页放在单独的渲染进程中,一个网页崩溃不影响浏览器和其它标签页;执行不受信任的代码(例如 Agent 运行模型生成的脚本)也应该放在单独的进程甚至容器中;
|
||||
@@ -102,7 +112,9 @@ Go 的 GMP 模型是一个典型实现:G 是 goroutine,M 是系统线程,P
|
||||
- 占用的内存少,可以同时存在大量协程,而每个线程都要预留独立的栈,例如 Linux x64 上 Java 线程的栈默认为 1 MB(ARM64 的 Linux 和 macOS 上为 2 MB),线程数量有限;
|
||||
- 同一个线程上的协程不能并行执行,要利用多核仍然需要多个线程;协程中如果执行了阻塞的系统调用,所在的线程会被阻塞,所以协程的运行时通常把 I/O 改为非阻塞的方式。
|
||||
|
||||
Java 21 正式引入的虚拟线程([JEP 444](https://openjdk.org/jeps/444))就是 JVM 实现的协程:虚拟线程由 JVM 调度到少量的平台线程上运行,执行阻塞 I/O 时从平台线程上卸载,让平台线程去执行其它虚拟线程。调度器是一个工作窃取的 ForkJoinPool,平台线程数默认等于 CPU 核数,JDK 21 不按时间片抢占虚拟线程。虚拟线程的创建成本很低,不需要用线程池复用,需要限制并发数时使用信号量。在 JDK 21 中,虚拟线程在 synchronized 块内阻塞时无法卸载,JDK 24 起([JEP 491](https://openjdk.org/jeps/491))基本消除了这个限制。
|
||||
协程按是否有自己的调用栈分为两类。有栈协程(stackful)可以在任意深度的函数调用中挂起,挂起时保存整个调用栈,例如 goroutine 和 Java 的虚拟线程;无栈协程(stackless)由编译器把 async 函数改写成状态机,只能在 await 处挂起,局部变量保存在状态对象中,例如 Python 的 async/await 和 Kotlin 协程。无栈协程更省内存,但 async 会沿调用链传染,调用它的函数也要写成 async。
|
||||
|
||||
Java 21 正式引入的虚拟线程([JEP 444](https://openjdk.org/jeps/444))就是 JVM 实现的协程:虚拟线程由 JVM 调度到少量的平台线程上运行,执行阻塞 I/O 时从平台线程上卸载,让平台线程去执行其它虚拟线程。调度器是一个工作窃取的 ForkJoinPool,平台线程数默认等于 CPU 核数,JDK 21 不按时间片抢占虚拟线程。虚拟线程卸载时,它的栈帧以栈块对象(stack chunk)的形式保存在堆中,等 I/O 就绪后再挂载到某个平台线程上继续执行,所以阻塞的虚拟线程只占用与调用深度相当的堆内存,不占用平台线程。虚拟线程的创建成本很低,不需要用线程池复用,需要限制并发数时使用信号量。在 JDK 21 中,虚拟线程在 synchronized 块内阻塞时无法卸载,JDK 24 起([JEP 491](https://openjdk.org/jeps/491))基本消除了这个限制。
|
||||
|
||||
例如在 JDK 21 中启动 1 万个阻塞在同一个 CountDownLatch 上的线程(Ubuntu 24.04 容器实测,见[附录 A2](#a2-平台线程与虚拟线程的开销)):
|
||||
|
||||
@@ -640,6 +652,41 @@ $ gcc -O2 -o shm shm.c && ./shm
|
||||
读到:order 1024 paid
|
||||
```
|
||||
|
||||
### A4 fork 与 pthread_create 的 clone 标志
|
||||
|
||||
Docker Desktop 中的 Ubuntu 24.04 容器(aarch64),gcc 13.3,strace 6.8。程序先 fork 一个子进程,子进程把全局变量 counter 加 1;再用 pthread_create 创建一个线程,线程也把 counter 加 1:
|
||||
|
||||
```c
|
||||
int counter = 0;
|
||||
static void *worker(void *arg) {
|
||||
counter++;
|
||||
printf("线程 pid=%d tid=%d\n", getpid(), gettid());
|
||||
return NULL;
|
||||
}
|
||||
// main 中:
|
||||
pid_t c = fork();
|
||||
if (c == 0) { counter++; printf("子进程 pid=%d tid=%d\n", getpid(), gettid()); _exit(0); }
|
||||
waitpid(c, NULL, 0);
|
||||
printf("fork 之后 counter=%d\n", counter);
|
||||
pthread_create(&t, NULL, worker, NULL);
|
||||
pthread_join(t, NULL);
|
||||
printf("线程结束后 counter=%d\n", counter);
|
||||
```
|
||||
|
||||
```text
|
||||
$ strace -f -e trace=clone,clone3 ./pt
|
||||
主线程 pid=14 tid=14
|
||||
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0xffff8716dfd0) = 15
|
||||
子进程 pid=15 tid=15
|
||||
fork 之后 counter=0
|
||||
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, ..., stack_size=0x80ea60, ...}, 88) = -1 ENOSYS (Function not implemented)
|
||||
clone(child_stack=0xffff86e0ea60, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, ...) = 16
|
||||
线程 pid=14 tid=16
|
||||
线程结束后 counter=1
|
||||
```
|
||||
|
||||
Docker 默认的 seccomp 规则不允许 clone3,glibc 收到 ENOSYS 后改用 clone,标志相同。线程的栈大小 0x80ea60 约 8 MB,来自 ulimit -s 的默认值。
|
||||
|
||||
## 参考资料
|
||||
|
||||
- Tanenbaum A S, Bos H. Modern Operating Systems [M]. Pearson, 2014.
|
||||
@@ -651,3 +698,4 @@ $ gcc -O2 -o shm shm.c && ./shm
|
||||
- [JEP 444: Virtual Threads](https://openjdk.org/jeps/444)
|
||||
- [Go 1.14 Release Notes](https://go.dev/doc/go1.14)
|
||||
- [Multi-process Architecture — The Chromium Projects](https://www.chromium.org/developers/design-documents/multi-process-architecture/)
|
||||
- [clone(2) — Linux manual page](https://man7.org/linux/man-pages/man2/clone.2.html)
|
||||
|
||||
Reference in New Issue
Block a user