五分钟扫盲:进程与线程基础必知
全文脉络思维导图如下:
1. 进程与线程的简单解释
进程(Process)和线程(Thread)是操作系统的基本概念,但是它们比较抽象,不容易掌握。以下这个解释出自阮一峰老师的博客(http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html),虽然不是非常严谨,但是足够形象,看完之后能对进程和线程有个非常直观的印象,这样也方便理解后文。
① 计算机的核心是 CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。
② 假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个 CPU 一次只能运行一个任务。
③ 进程就好比工厂的车间,它代表 CPU 所能处理的单个任务。任一时刻,CPU 总是运行一个进程,其他进程处于非运行状态。
④ 一个车间里,可以有很多工人。他们协同完成一个任务。
⑤ 线程就好比车间里的工人。一个进程可以包括多个线程。
⑥ 车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。
⑦ 可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。
⑧ 一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫'互斥锁'(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。
⑨ 还有些房间,可以同时容纳 n 个人,比如厨房。也就是说,如果人数大于 n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。
⑩ 这时的解决方法,就是在门口挂 n 把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做 '信号量'(Semaphore),用来保证多个线程不会互相冲突。
不难看出,互斥锁 Mutex 是信号量 semaphore 的一种特殊情况(n = 1时)。也就是说,完全可以用后者替代前者。但是,因为 Mutex 较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。
2. 进程基础扫盲
① 什么是进程
结合上文的简单解释,下面给出进程的科学定义:进程是程序在某个数据集合上的一次运行活动,也是操作系统进行资源分配和保护的基本单位。
通俗来说,进程就是程序的一次执行过程,程序是静态的,它作为系统中的一种资源是永远存在的。而进程是动态的,它是动态的产生,变化和消亡的,拥有其自己的生命周期。
举个例子:同时挂三个 QQ 号,它们就对应三个 QQ 进程,退出一个就会杀死一个对应的进程。但是,就算你把这三个 QQ 全都退出了,QQ 这个程序死亡了吗?显然没有。
进程不仅包含正在运行的程序实体,并且包括这个运行的程序中占据的所有系统资源,比如说 CPU、内存、网络资源等。很多小伙伴在回答进程的概念的时候,往往只会说它是一个运行的实体,而会忽略掉进程所占据的资源。比如说,同样一个程序,同一时刻被两次运行了,那么他们就是两个独立的进程。
② 进程的组成
进程主要由三个部分组成:
1)进程控制块 PCB。包含如下几个部分:
- 进程描述信息
- 进程控制和管理信息
- 资源分配清单
- CPU 相关信息
2)数据段。即进程运行过程中各种数据(比如程序中定义的变量)
3)程序段。就是程序的代码(指令序列)
举个例子:同时挂三个 QQ 号,会对应三个 QQ 进程,它们的 PCB、数据段各不相同,但程序段的内容都是相同的 (都是运行着相同的 QQ 程序)
PCB 是提供给操作系统用的,而程序段、数据段是给进程自己用的。
进程控制块 PCB
每个进程有且仅有一个进程控制块(Process Control Block,PCB),或称进程描述符,它是进程存在的唯一标识,是操作系统用来记录和刻画进程状态及环境信息的数据结构,也是操作系统掌握进程的唯一资料结构和管理进程的主要依据。所以说 PCB 是提供给操作系统使用的。
通俗的解释:操作系统需要对各个进程进行管理,但凡管理时所需要的信息,都会被放在 PCB 中,PCB 是进程存在的唯一标志。创建进程和撤销进程等都是指对 PCB 的操作,当进程被创建时,操作系统为其创建 PCB,当进程结束时,会回收其 PCB。
一般来说,PCB 会包含如下四类信息:
1)进程描述信息:用来让操作系统区分各个进程
- 当进程被创建时,操作系统会为该进程分配一个唯一的、不重复的 “身份证号”— PID(ProcessID,进程 ID)
- 另外,进程描述信息还包含进程所属的用户 ID(UID)
2)进程控制和管理信息:记录进程的运行情况。比如 CPU 的使用时间、磁盘使用情况、网络流量使用情况等。
3)资源分配清单:记录给进程分配了哪些资源。比如分配了多少内存、正在使用哪些 I/O 设备、正在使用哪些文件等。
4)CPU 相关信息:进程在让出 CPU 时,必须保存该进程在 CPU 中的各种信息,比如各种寄存器的值。用于实现进程切换,确保这个进程再次运行的时候恢复 CPU 现场,从断点处继续执行。这就是所谓的保存现场信息。
③ 进程的状态
尽管每一个进程都是独立的实体,有其自己的 PCB 和内部状态,但是进程之间经常需要相互作用。一个进程的输出结果可能是另一个进程的输入。假设进程 A 的输入依赖进程 B 的输出,那么在进程 B 的输出结果没有出来之前,进程 A 就无法执行,它就会被阻塞。这就是进程的阻塞态。
经典的进程三态模型如下:
- 运行态(running):进程占有 CPU 正在运行。
- 就绪态(ready):进程具备运行条件,等待系统分配 CPU 以便运行。
- 阻塞态 / 等待态(wait):进程不具备运行条件,正在等待某个事件的完成。
上图中的时间片用完,可以这样理解:
进程是并发执行的嘛,宏观上在一段时间内能同时运行多个程序,但其实微观上是交替发生的。也就是说 CPU 一般不会让一个进程一次性执行完,为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。某个进程的时间片用完后这个进程就会进入就绪态,而其他被分配到时间片的进程就会进入运行态。这个处于就绪态的进程就需要等待进程调度程序的下一次调度,为其分配 CPU 时间片后才能再次恢复运行。
需要注意的是:阻塞态是由于缺少需要的资源从而由运行态转换而来,但是该资源不包括 CPU 时间片,缺少 CPU 时间片会从运行态转换为就绪态。
很多系统中都增加了新建态(new)和终止态(exit),形成五态模型:
- 新建态(new):进程正在被创建时的状态
- 终止态(exit):进程正在从系统中消失时的状态
从上图可以发现,只有就绪态和运行态可以相互转换,其它的都是单向转换。
这些不同状态的进程操作系统是如何进行管理的呢?上文说过,PCB 是提供给操作系统使用的,是操作系统管理进程的主要依据。没错,操作就是通过 PCB 来管理这些拥有不同状态的进程的。
进程的 PCB 会通过某种方式组织起来,一般来说,操作系统会把处于同一状态的所有进程的 PCB 链接在一起,这种数据结构就称为进程队列(Process Queue)。
④ 进程控制
所谓进程控制就是对系统中的所有进程实施有效的管理,实现进程状态转换功能。包括创建进程、阻塞进程、唤醒进程、终止进程等,这些功能均由原语来实现,操作系统通过原语来完成进程原理,包括进程的同步和互斥、进程的通信和管理。
什么是原语?原语是一种特殊的程序,它的执行具有原子性。 也就是说,这段程序的运行必须一气呵成,不可中断。原语是操作系统内核里的一段程序:
思考一下:为什么进程控制(进程状态转换)的过程要一气呵成,不可中断?
答:如果进程状态转换的过程不能一气呵成,就有可能导致操作系统中的某些关键数据结构信息不统一,这会影响操作系统进行别的管理工作。
进程的创建
操作系统初始启动时会创建承担系统资源分配和控制管理的一些系统进程,同时还会创建一个所有用户进程的祖先,其他用户进程是在应用程序运行时创建的。
操作系统允许一个进程创建另一个进程,而且允许子进程继承父进程所拥有的资源,当子进程被终止时,其在父进程处继承的资源应当还给父进程。同时,终止父进程时同时也会终止其所有的子进程。
创建进程的过程,也就是创建原语包含的内容如下:
- 在进程列表中增加一项,从 PCB 池中申请一个空闲的 PCB(PCB 是有限的,若申请失败则创建失败),为新进程分配一个唯一的进程标识符;
- 为新进程分配地址空间,由进程管理程序确定加载至进程地址空间中的程序;
- 为新进程分配各种资源;
- 初始化 PCB,如进程标识符、CPU 初始状态等;
- 把新进程的状态设置为就绪态,并将其移入就绪队列,等待被调度运行。
什么事件会触发进程的创建呢?有如下四种情况:
- 用户登录:分时系统中,用户登录成功,系统会为其建立一个新的进程
- 作业调度:多道批处理系统中,有新的作业放入内存中,会为其建立一个新的进程
- 提供服务:用户向操作系统提出某些请求时,会新建一个进程处理该请求
- 应用请求:由用户进程主动请求创建一个子进程
进程的终止
进程的终止也称为撤销,进程完成特定工作或出现严重错误后必须被终止。引起进程终止的事件有三种:
- 正常结束:进程自己请求终止(exit 系统调用)
- 异常结束:比如整数除 0,非法使用特权指令,然后被操作系统强行终止
- 外界干预:Ctrl + Alt + delete 打开进程管理器,用户手动杀死进程
终止(撤销)进程的过程,也就是撤销原语包含的内容如下:
- 从 PCB 集合中找到终止进程的 PCB;
- 若进程处于运行态,则立即剥夺其 CPU,终止该进程的执行,然后将 CPU 资源分配给其他进程;
- 如果其还有子进程,则应将其所有子进程终止;
- 将该进程所拥有的全部资源都归还给父进程或操作系统;
- 回收 PCB 并将其归还至 PCB 池。
进程的阻塞和唤醒
进程阻塞是指进程让出 CPU 资源转而等待一个事件,如等待资源、等待 I/O 操作完成等。进程通常使用阻塞原语来阻塞自己,所以阻塞是进程的自主行为,是一个同步事件。当等待事件完成时会产生一个中断,激活操作系统,在系统的控制下将被阻塞的进程唤醒,也就是唤醒原语。
进程的阻塞和唤醒显然是由进程切换来完成的。
进程的阻塞步骤,也就是阻塞原语的内容为:
- 找到将要被阻塞的进程对应的 PCB;
- 保护进程运行现场,将 PCB 状态信息设置为阻塞态,暂时停止进程运行;
- 将该 PCB 插入相应事件的阻塞队列(等待队列)。
进程的唤醒步骤,也就是唤醒原语的内容为:
- 在该事件的阻塞队列中找到相应进程的 PCB;
- 将该 PCB 从阻塞队列中移出,并将进程的状态设置为就绪态;
- 把该 PCB 插入到就绪队列中,等待被调度程序调度。
阻塞原语和唤醒原语的作用正好相反,阻塞原语使得进程从运行态转为阻塞态,而唤醒原语使得进程从阻塞态转为就绪态。如果某个进程使用阻塞原语来阻塞自己,那么他就必须使用唤醒原语来唤醒自己,因何事阻塞,就由何事唤醒,否则被阻塞的进程将永远处于阻塞态。因此,阻塞原语和唤醒原语是成对出现的。
⑤ 进程上下文切换
所谓进程的上下文切换,就是说各个进程之间是共享 CPU 资源的,不可能一个进程永远占用着 CPU 资源,不同的时候进程之间需要切换,使得不同的进程被分配 CPU 资源,这个过程就是进程的上下文切换,一个进程切换到另一个进程运行。
因为进程是由内核进行管理和调度的,所以进程的上下文切换一定发生在内核态。
进程上下文的切换也是一个原语操作,称为切换原语,其内容如下:
- 首先,将进程 A 的运行环境信息存入 PCB,这个运行环境信息就是进程的上下文(Context)
- 然后,将 PCB 移入相应的进程队列;
- 选择另一个进程 B 进行执行,并更新其 PCB 中的状态为运行态
- 当进程 A 被恢复运行的时候,根据它的 PCB 恢复进程 A 所需的运行环境
引起进程上下文切换的事件,也就是某个占用 CPU 资源运行的当前进程被赶出 CPU 的原因有如下:
- 当前进程的时间片到
- 有更高优先级的进程到达
- 当前进程主动阻塞
- 当前进程终止
3. 线程基础扫盲
① 什么是线程
结合文章开头的简单解释,一个进程中可以有多个线程,它们共享这个进程的资源。
举个例子,QQ 和 Chrome 浏览器是两个进程,Chrome 进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。
② 为什么要引入线程
早期的操作系统都是以进程作为独立运行的基本单位的,直到后期计算机科学家们又提出了更小的能独立运行的基本单位,也就是线程。这就好比物理学家研究物质组成一样:先发现了分子,然后继续细分发现原子,再后来是原子核和电子、夸克等等。
那么,为什么要引入线程呢?我们只需要记住这句话:线程又称为迷你进程,但是它比进程更容易创建,也更容易撤销。
从上文我们知道,进程是拥有资源的基本单位,而且还能够进行独立调度,这就犹如一个随时背着粮草的士兵,这必然会造成士兵的执行命令(战斗)的速度。所以,一个简单想法就是:分配两个士兵执行同一个命令:一个负责携带所需粮草随时供给,另一个士兵负责执行命令(战斗)。这就是线程的思想,轻装上阵的士兵就是线程。
用严谨的语言描述来说就是:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,需要较大的时空开销,限制了并发程度的进一步提高。为减少进程切换的开销,把进程作为资源分配单位和调度单位这两个属性分开处理,即进程还是作为资源分配的基本单位,但是不作为调度的基本单位(很少调度或切换),把调度执行与切换的责任交给线程,即线程成为独立调度的基本单位,它比进程更容易(更快)创建,也更容易撤销。
记住这句话!引入线程前,进程是资源分配和独立调度的基本单位。引入线程后,进程是资源分配的基本单位,线程是独立调度的基本单位。
③ 线程优缺点
线程的特征和进程差不多,进程有的他基本都有,比如:
- 线程具有就绪、阻塞、运行三种基本状态,同样具有状态之间的转换关系;
- 线程间可以并发执行
- 在多 CPU 环境下,各个线程也可以分派到不同的 CPU 上并行执行
线程的优点:
- 一个进程中可以同时存在多个线程,这些线程共享该进程的资源。进程间的通信必须请求操作系统服务(因为 CPU 要切换到内核态),开销很大。而同进程下的线程间通信,无需操作系统干预,开销更小。不过,需要注意的是:从属于不同进程的线程间通信,也必须请求操作系统服务。
- 线程间的并发比进程的开销更小,系统并发性提升。同样,需要注意的是:从属于不同进程的线程间切换,它是会导致进程切换的,所以开销也大。
线程的缺点:
- 当进程中的一个线程奔溃时,会导致其所属进程的所有线程奔溃。
举个例子,对于游戏的用户设计,就不应该使用多线程的方式,否则一个用户挂了,会影响其他同个进程的线程。
4. 总结
操作系统的设计,从进程和线程的角度来说,可以归结为三点:
- 以多进程形式,允许多个任务同时运行;
- 以多线程形式,允许单个任务分成不同的部分运行;
- 提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。