diff --git a/notes/计算机操作系统 - 进程管理.md b/notes/计算机操作系统 - 进程管理.md index 2a147595..71cf390b 100644 --- a/notes/计算机操作系统 - 进程管理.md +++ b/notes/计算机操作系统 - 进程管理.md @@ -71,10 +71,10 @@ QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 H - 拥有资源:进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源; - 调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换; - 系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,两者切换时都要保存和恢复寄存器,进程切换还要切换地址空间,开销更大(见[上下文切换](#4-上下文切换)); -- 通信方面:线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC; -- 健壮性:进程之间相互隔离,一个进程崩溃不影响其它进程,而一个线程访问非法内存触发段错误时,整个进程连同其它线程都会被终止。 +- 通信方面:线程间可以通过直接读写同一进程中的数据进行通信,但要用互斥量、信号量等手段同步(见[进程同步](#进程同步)),进程通信则需要借助 IPC; +- 健壮性:进程之间相互隔离,一个进程崩溃不影响其它进程,而一个线程访问非法内存触发段错误时,SIGSEGV 信号虽然发给出错的线程,默认动作却是终止整个进程,其它线程也一起结束。 -在 Linux 内核中,进程和线程都用 task_struct 表示,都是调度的对象,区别只在于和创建者共享了多少资源。fork 和 pthread_create 最终都调用 clone 系统调用,只是传入的标志不同(strace 实测,见[附录 A4](#a4-fork-与-pthread_create-的-clone-标志)): +在 Linux 内核中,进程和线程都用 task_struct 表示,都是调度的对象,区别只在于和创建者共享了多少资源。fork 和 pthread_create 最终都调用 clone 或 clone3 系统调用,只是传入的标志不同(strace 实测,见[附录 A4](#a4-fork-与-pthread_create-的-clone-标志)): ```text fork() clone(flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD) @@ -83,7 +83,7 @@ pthread_create() clone(flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_ CLONE_VM 表示共享地址空间,CLONE_FILES 表示共享文件描述符表,CLONE_THREAD 表示加入创建者的线程组。没有这些标志时,子进程得到一份地址空间的副本([写时复制](计算机操作系统%20-%20内存管理.md#分页系统地址映射)),修改全局变量不影响父进程;有这些标志时,新线程修改的就是同一个变量。getpid() 返回的是线程组 ID,同一进程中的线程都相同,gettid() 返回每个线程自己的 ID,top -H、ps -L 中看到的就是它。 -fork 调用一次、返回两次:父进程中返回子进程的 PID,子进程中返回 0。多线程的进程 fork 后,子进程中只有调用 fork 的那个线程。 +fork 调用一次、返回两次:父进程中返回子进程的 PID,子进程中返回 0。多线程的进程 fork 后,子进程中只有调用 fork 的那个线程,但互斥量等对象的状态也被复制过来。如果复制时某个只在进程内使用的互斥量正被其它线程持有,子进程中没有线程会释放它,再加锁就可能死锁。所以子进程通常尽快调用 exec,在此之前只调用异步信号安全的函数。

@@ -102,15 +102,15 @@ fork 调用一次、返回两次:父进程中返回子进程的 PID,子进 需要注意的是,系统调用只是从用户态进入内核态,执行完后通常回到同一个线程,不一定发生上下文切换。 -同一进程中的线程切换不需要换地址空间;切换到另一个进程时还要切换页表,TLB 和 CPU 缓存中原来的地址映射和数据大多用不上,要重新加载,这部分间接开销通常比保存寄存器更大。x86 的 PCID 可以给 TLB 表项标上所属的地址空间,切换页表时不必清空整个 TLB,没有 PCID 时,每次切换页表都会清空 TLB 中除全局页(内核映射)以外的表项。 +同一进程中的线程切换不需要换地址空间;切换到另一个进程时还要切换页表。x86 的 PCID、ARM 的 ASID 可以给 TLB 表项标上所属的地址空间,切换页表时不必清空整个 TLB,切回来时原来的映射可能还在;没有这类标记时,每次切换页表都会清空 TLB 中除全局页(内核映射)以外的表项,之后访问内存要重新查页表。切换之后 TLB 和 CPU 缓存的未命中还会增加执行时间,增加多少取决于硬件和两个执行流各自访问的数据量,同一进程的线程切换也可能发生缓存未命中。 -例如把两个执行流固定在同一个 CPU 上,通过两根管道来回传 1 个字节,让它们交替运行,两个线程之间、两个进程之间每次切换都约 1.1 微秒,其中还包含一次 write 和一次 read 系统调用(Linux 实测,见[附录 A1](#a1-上下文切换))。数据量很小时两者几乎没有差别,进程切换的额外开销主要在工作集较大时体现出来。线程过多、频繁阻塞时,CPU 会把相当一部分时间花在切换上,可以用 vmstat 的 cs 列或 pidstat -w 观察每秒的切换次数。 +例如把两个执行流固定在同一个 CPU 上,通过两根管道来回传 1 个字节,让它们交替运行,两个线程之间、两个进程之间每次切换都约 1.1 微秒,其中还包含一次 write 和一次 read 系统调用(Linux 实测,见[附录 A1](#a1-上下文切换))。数据量很小时两者几乎没有差别。线程过多、频繁阻塞时,CPU 会把相当一部分时间花在切换上,可以用 vmstat 的 cs 列或 pidstat -w 观察每秒的切换次数。 线程也不能无限创建:Linux 上受 ulimit -u、/proc/sys/kernel/threads-max 和 pid_max 限制,超出时 Java 抛出 OutOfMemoryError: unable to create native thread(见[附录 A5](#a5-线程数的上限))。 ### 5. 协程 🔥🔥 -协程(Coroutine)是在用户态实现的轻量级线程,由程序自己调度,而不是由操作系统内核调度。协程在等待 I/O 时主动让出执行权,运行时再把其它协程调度到同一个线程上执行。 +协程(Coroutine)是在[用户态](计算机操作系统%20-%20概述.md#系统调用)实现的轻量级线程,由程序自己调度,而不是由操作系统内核调度。协程在等待 I/O 时主动让出执行权,运行时再把其它协程调度到同一个线程上执行。 协程要解决的是高并发 I/O 的写法问题:用 [epoll](Socket.md#epoll) 时少量线程就能处理几万个连接,但处理逻辑要拆成一段段回调;协程让代码仍按阻塞的方式顺序写,运行时在底层改用非阻塞 I/O 和 epoll 这类多路复用接口,等待时挂起协程,就绪后再恢复。 @@ -221,7 +221,7 @@ Linux 的进程状态和僵尸进程、孤儿进程见 [Linux](Linux.md#进程 分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。 -实际的操作系统会组合使用这些思想。例如 Linux 2.6.23 起的完全公平调度器(CFS)给每个进程记录按优先级加权后的虚拟运行时间,总是选择虚拟运行时间最小的进程运行,让各进程按权重分享 CPU;Linux 6.6 起开始改用 EEVDF 调度器,在公平分享的基础上考虑每个进程的截止时间,让对延迟敏感的进程更快得到运行。实时进程使用单独的 SCHED_FIFO、SCHED_RR 策略,优先于普通进程。 +实际的操作系统会组合使用这些思想。例如 Linux 2.6.23 起的完全公平调度器(CFS)给每个进程记录按优先级加权后的虚拟运行时间,总是选择虚拟运行时间最小的进程运行,让各进程按权重分享 CPU;Linux 6.6 起开始改用 EEVDF 调度器,同样按权重公平分享,但只在还没有用超自己份额的进程中选择,选其中虚拟截止时间最早的运行;虚拟运行时间和权重相同时,请求的时间片越短,虚拟截止时间越早,有利于对延迟敏感的进程更快得到运行。实时进程使用单独的 SCHED_FIFO、SCHED_RR 策略,优先于普通进程。 ## 进程同步 @@ -474,6 +474,8 @@ void check(int i) { } ``` +避免死锁还有更简单的做法:给筷子编号,每个哲学家先拿两根中编号小的一根,等待只能从小编号指向大编号,不会形成环,见 [破坏环路等待](计算机操作系统%20-%20死锁.md#4-破坏环路等待);或者最多允许 4 个哲学家同时去拿筷子,至少有一个人能拿到两根。 + ### 2. 读者-写者问题 允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。 @@ -527,7 +529,7 @@ void writer() { | 消息队列 | 带类型的消息 | 保留消息边界,可以按类型读取 | 多个进程向同一个队列投递任务 | | 共享存储 | 直接读写同一块内存 | 最快,需要自己同步 | 大量数据的交换 | | 信号量 | 不传数据 | 计数器,用于同步和互斥 | 配合共享存储 | -| 信号 | 信号编号(sigqueue 可附带一个整数) | 异步通知 | kill、[SIGCHLD](Linux.md#sigchld) | +| 信号 | 信号编号(sigqueue 可附带一个整数或指针) | 异步通知 | kill、[SIGCHLD](Linux.md#sigchld) | | 套接字 | 字节流或数据报 | 可以跨机器 | 网络服务、Unix 域套接字 | ### 1. 管道