diff --git a/notes/pics/process-vs-thread.png b/notes/pics/process-vs-thread.png new file mode 100644 index 00000000..1ec519d7 Binary files /dev/null and b/notes/pics/process-vs-thread.png differ diff --git a/notes/pics/virtual-thread-unmount.png b/notes/pics/virtual-thread-unmount.png new file mode 100644 index 00000000..00b2cf6e Binary files /dev/null and b/notes/pics/virtual-thread-unmount.png differ diff --git a/notes/计算机操作系统 - 进程管理.md b/notes/计算机操作系统 - 进程管理.md index 736701e0..fd42e5c5 100644 --- a/notes/计算机操作系统 - 进程管理.md +++ b/notes/计算机操作系统 - 进程管理.md @@ -4,9 +4,9 @@ * [进程与线程](#进程与线程) * [1. 进程](#1-进程) * [2. 线程](#2-线程) - * [3. 区别](#3-区别) + * [3. 区别](#3-区别) 🔥🔥 * [4. 上下文切换](#4-上下文切换) - * [5. 协程](#5-协程) + * [5. 协程](#5-协程) 🔥🔥 * [进程状态的切换](#进程状态的切换) * [进程调度算法](#进程调度算法) * [1. 批处理系统](#1-批处理系统) @@ -20,7 +20,7 @@ * [经典同步问题](#经典同步问题) * [1. 哲学家进餐问题](#1-哲学家进餐问题) * [2. 读者-写者问题](#2-读者-写者问题) - * [进程通信](#进程通信) + * [进程通信](#进程通信) 🔥🔥 * [1. 管道](#1-管道) * [2. FIFO](#2-fifo) * [3. 消息队列](#3-消息队列) @@ -62,7 +62,7 @@ QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 H 需要注意的是,各线程的栈只是分开使用,并没有隔离:拿到另一个线程栈上变量的地址,同样可以读写它。 -### 3. 区别 +### 3. 区别 🔥🔥 - 拥有资源:进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源; - 调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换; @@ -79,6 +79,8 @@ pthread_create() clone(flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_ CLONE_VM 表示共享地址空间,CLONE_FILES 表示共享文件描述符表,CLONE_THREAD 表示加入创建者的线程组。没有这些标志时,子进程得到一份地址空间的副本(写时复制),修改全局变量不影响父进程;有这些标志时,新线程修改的就是同一个变量。getpid() 返回的是线程组 ID,同一进程中的线程都相同,gettid() 返回每个线程自己的 ID,top -H、ps -L 中看到的就是它。 +

+ 因此选择多进程还是多线程,主要看更需要隔离还是共享: - 任务之间需要隔离、一个出错不能拖垮其它任务时用多进程,例如 Chrome 把网页放在单独的渲染进程中,一个网页崩溃不影响浏览器和其它标签页;执行不受信任的代码(例如 Agent 运行模型生成的脚本)也应该放在单独的进程甚至容器中; @@ -98,7 +100,7 @@ CLONE_VM 表示共享地址空间,CLONE_FILES 表示共享文件描述符表 例如把两个执行流固定在同一个 CPU 上,通过两根管道来回传 1 个字节,让它们交替运行,两个线程之间、两个进程之间每次切换都约 1.1 微秒,包含一次 read 或 write 系统调用(Linux 实测,见[附录 A1](#a1-上下文切换))。数据量很小时两者几乎没有差别,进程切换的额外开销主要在工作集较大时体现出来。线程过多、频繁阻塞时,CPU 会把相当一部分时间花在切换上,可以用 vmstat 的 cs 列或 pidstat -w 观察每秒的切换次数。 -### 5. 协程 +### 5. 协程 🔥🔥 协程(Coroutine)是在用户态实现的轻量级线程,由程序自己调度,而不是由操作系统内核调度。协程在等待 I/O 时主动让出执行权,运行时再把其它协程调度到同一个线程上执行。 @@ -116,6 +118,8 @@ Go 的 GMP 模型是一个典型实现:G 是 goroutine,M 是系统线程,P 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-平台线程与虚拟线程的开销)): - 平台线程用时约 1.4 秒,进程的常驻内存增加约 290 MB,平均每个约 30 KB,这是因为 2 MB(aarch64)的栈只是预留的虚拟地址,用到的页才占用物理内存; @@ -482,7 +486,7 @@ void writer() { 这种实现在读者源源不断时可能导致写者饥饿,写者优先和按到达顺序排队的公平实现见[进阶:读者-写者问题](计算机操作系统%20-%20进程管理%20-%20进阶.md#读者-写者问题)。 -## 进程通信 +## 进程通信 🔥🔥 进程同步与进程通信很容易混淆,它们的区别在于: