这一组系统调用是 Linux / Unix 进程管理里最经典的一组面试题。很多同学一开始会把它们混在一起,觉得都和“创建进程、执行程序”有关,但其实它们三者分别负责完全不同的事情:
fork:创建子进程exec:让当前进程执行新的程序wait:父进程等待并回收子进程
最典型的组合关系是:
父进程先
fork出子进程,子进程再exec执行新程序,父进程最后通过wait回收子进程。
这一组系统调用是 Linux / Unix 进程管理里最经典的一组面试题。很多同学一开始会把它们混在一起,觉得都和“创建进程、执行程序”有关,但其实它们三者分别负责完全不同的事情:
fork:创建子进程exec:让当前进程执行新的程序wait:父进程等待并回收子进程最典型的组合关系是:
父进程先
fork出子进程,子进程再exec执行新程序,父进程最后通过wait回收子进程。
这一组概念非常容易混,因为它们看起来都和“锁”有关,但其实不在同一层面。
理解这组概念最重要的一句话是:
悲观锁和乐观锁,是处理并发冲突的思路;自旋锁和互斥锁,是具体的加锁实现方式。
如果不先把这两层分开,就很容易越学越乱。
这是:
并发控制策略
也就是说,它们描述的是:
死锁是并发控制里最经典、也最容易在面试中被单独追问的一题。它和前面学过的:
都是连在一起的。
最核心的一句话是:
死锁是多个线程或进程因为争夺资源而互相等待,导致谁都无法继续执行的状态。
死锁可以理解成一种“大家都卡住了,而且谁都没法主动往前走”的状态。
最典型的例子是:
讲完 IPC 之后,下一步最自然就是线程同步与互斥。
因为线程之间不像进程那样需要复杂的 IPC,它们共享同一个进程的地址空间,所以通信本身通常并不困难,真正困难的是:
多个线程同时访问同一份共享数据时,怎么保证结果不乱。
这就是线程同步与互斥要解决的问题。
同一进程内的线程共享:
讲完进程之后,下一步最自然就是线程。
线程是面试里最常见也最容易讲乱的概念之一。很多人只会背:
但如果不理解“为什么要有线程”,这些话就容易变成死记硬背。
线程出现的核心目的,是在同一个进程内部实现更轻量的并发。
如果所有并发都用进程来做,会有几个问题:
线程这块最容易让人越学越绕,因为它同时牵扯:
如果这些层次混在一起,很容易越看越糊。所以这一篇我们只先抓住最重要的主线:
用户线程、内核线程、LWP 分别是什么,它们是怎么对应起来的。
用户线程可以理解成:
在线程库或运行时里实现和管理的线程。
调度这部分,本质上在回答:
CPU 先给谁用、给多久、什么时候换人。
单核 CPU 在同一时刻只能真正执行一个任务,因此操作系统必须从就绪任务中做选择,这个选择过程就是调度。
调度程序(scheduler)的职责,就是在多个可运行进程 / 线程之间做选择:
如果没有调度,系统就无法在多个任务之间高效分配 CPU 资源。
进程管理这部分,最先要搞清楚的不是各种系统调用,而是:
什么是进程,操作系统为什么要管理进程,以及它到底是怎么管理的。
很多同学一开始会把进程理解成“一个正在运行的程序”,这句话本身没错,但还不够。面试里如果只答这一句,后面的状态、PCB、上下文切换就很难接上。
程序本身是磁盘上的静态文件,只有被装入内存并开始执行后,才会变成进程。
所以可以直接记成:
进程之间默认是相互隔离的,彼此看不到对方的用户空间。所以如果多个进程想交换数据、协作执行、同步先后顺序,就必须借助操作系统提供的进程间通信机制,也就是 IPC(Inter-Process Communication)。
理解 IPC 时,最重要的一句话是:
因为进程间用户空间默认隔离,所以通信通常要借助内核空间,或者借助操作系统建立的共享映射。
这篇就把常见 IPC 方式系统整理一遍,并补一些容易混的点。
线程之间因为共享同一进程的地址空间,所以通过共享变量就能直接通信。进程不一样,进程 A 不能直接访问进程 B 的用户空间,因此必须通过操作系统提供的通信机制来实现: