一、进程的定义与特征
1. 进程的定义与特征
进程(Process)是资源分配的基本单位。进程是动态的,是程序的一次执行过程,同一个程序执行会对应多个进程。相反,程序是即是静态的,就是存放在磁盘里的可执行文件,就是一系列的指令集合。
2. 进程的组成
程序段、数据段、PCB三部分组成了进程的实体(进程映像),引入进程实体的概念后,可把进程定义为:进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。
2.1 PCB
当进程被创建时,操作系统会为该进程分配一个唯一的、不重复的“身份证号”——PID(Process ID)。操作系统要记录 PID、进程所属用户 ID(UID),还要记录给进程分配了哪些资源(如:分配了多少内存、正在使用哪些 I/O 设备),还要记录进程的运行情况(如:CPU 使用时间、磁盘使用情况等)。
上述这些信息都被保存在一个数据结构 PCB(Process Control Block
)中,即进程控制块。操作系统需要对各个并发运行的进程进行管理,但凡管理时所需要的信息,都会被放在 PCB 中。
PCB包括了一下四个方面的信息:
- 进程描述信息:包括进程标识符PID、用户标识符UID;
- 资源分配清单:正在使用哪些文件、正在使用哪些内存区域、正在使用哪些IO设备;
- 进程控制和管理信息:正在使用哪些文件、正在使用哪些内存区域、正在使用哪些IO设备;
- 进程控制和管理信息:CPU、磁盘、网络流量使用情况,进程当前状态(就绪态/阻塞态/运行态);
- 处理机相关信息:如 PSW、PC 等等各种寄存器的值(用于实现进程切换)。
2.2 程序段、数据段
程序段是指程序的代码(指令序列),数据段是指运行过程中产生的各种数据(如:程序中定义的变量)。程序段、数据段是给进程自己用的,与进程自身的运行逻辑有关。
3. 进程的特征
程序是静态的,进程是动态的,相比于程序,进程拥有以下特征:
- 动态性:进程是程序的一次执行过程,是动态地产生、变化和消亡的
- 并发性:内存中有多个实体进程,各进程可并发执行
- 独立性:进程是能独立运行的、独立获得资源、独立接受调度的基本单位
- 异步性:各进程按各自独立的、不可预知的速度向前推进,操作系统要提供“进程同步机制”来解决异步问题
- 结构性:每个进程都会配置一个 PCB。结构上看,进程由程序段、数据段、PCB 组成。
二、进程的状态切换
在进程 PCB 中,会有一个变量 state 来表示进程的当前状态。如:1表示创建态、2表示就绪态、3表示运行态…为了对同一个状态下的各个进程进行统一管理,操作系统会将各个进程的 PCB 组织起来。
需要注意的是:
- 阻塞态→就绪态不是进程自身能控制的,是一种被动行为
- 运行态→阻塞态是一种进程自身做出的主动行为
- 不能由阻塞态直接转换成运行态,也不能由就绪态直接转换为阻塞态(因为进入阻塞态是进程主动请求的,必然需要进程在运行时才能发出这种请求)
三、进程控制
进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新的进程、撤销已有进程、实现进程状态转换等功能。简单理解:进程控制就是要实现进程状态转换。
1. 如何实现进程控制
用原语实现进程控制。原语的特点是执行期间不允许中断,只能一气呵成。这种不可被中断的操作即原子操作。原语采用关中断指令和开中断指令实现。
正常情况下,CPU 每执行完一条指令都会例行检查是否有中断信号需要处理,如果有,则暂停运行当前这段程序,转而执行相应的中断处理程序。CPU 执行了关中断指令后,就不再例行检查中断信号,直到执行开中断指令后才会恢复检查。这样,关中断、开中断之间的这些指令序列就是不可被中断的,这就实现了“原子性”。
显然,开/关中断指令的权限非常大,必然是只允许在核心态下执行的特权指令;
2. 进程相关的原语
进程控制会导致进程状态的转换。无论哪个原语,要做的无非三类事情:
- 更新PCB中的信息(如修改进程状态标志、将运行环境保存到PCB、从PCB恢复运行环境)
- 所有的进程控制原语一定都会修改进程状态标志;
- 剥夺当前运行进程的CPU使用权必然需要保存其运行环境;
- 某进程开始运行前必然要恢复期间运行环境;
- 将PCB插入合适的队列;
- 分配/回收内存;
四、进程通信
顾名思义,进程同行就是指进程之间的信息交换。进程是分配资源的单位(包括内存地址空间),因此各进程拥有的内存地址空间相互独立。为了保证安全,一个进程不能直接访问另一个进程的地址空间。都是进程之间的信息交换又是必须实现的。为了保证进程间的安全通信,操作系统提供了一些方法。
1. 共享存储
共享存储分为两种,一种是基于数据结构的共享,另一种是基于存储区的共享:
-
基于数据结构的共享:比如共享空间里只能放一个长度为 10 的数组。这种共享方式速度慢、限制多,是一种低级通信方式。
-
基于存储区的共享:在内存中画出一块共享存储区,数据的形式、存储位置都由进程控制,而不是操作系统。相比之下,这种共享方式速度更快,是一种高级通信。
注意:两个进程对共享空间的访问必须是互斥的(互斥访问通过操作系统提供的工具实现);操作系统只负责提供共享空间和同步互斥工具(如P、V操作)。
2. 管道通信
“管道”是指用于连接读写进程的一个共享文件,又名 pipe 文件。其实就是在内存中开辟一个大小固定的缓冲区。
- 管道只能采用半双工通信,某一时间段内只能实现单向的传输。如果要实现双向同时通信,则需要设置两个通道。
- 各进程要互斥地访问管道。
- 数据以字符流的形式写入管道,当管道写满时,写进程的
write()
系统调用将被阻塞,等待读进程将数据取走。当读进程将数据全部取走后,管道变空,此时读进程的read()
系统调用被阻塞。 - 如果没写满,就不允许读。如果没都空,就不允许写。
- 数据一旦被读出,就从管道中被抛弃,这就意味着读进程最多只能有一个,否则可能会有读错数据的情况。
3. 消息传递
进程间的数据交换以格式化的消息(Message
)为单位。进程通过操作系统提供的“发送消息/接受消息”两个原语进行数据交换。
一个格式化消息包括了消息头和消息体,其中消息头包括了发送进程的ID、接收进程的ID、消息类型、消息长度等格式化的信息(计算机网络中发送的“报文”其实就是一种格式化的消息)。
五、线程的概念与特点
1. 什么是线程,为什么要引入线程?
线程是独立调度的基本单位,一个进程中可以有多个线程,它们共享进程资源。QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。
有的进程可能需要“同时”做很多事,而传统的进程只能串行地执行一系列程序。为此,引入了”线程“,来增加并发度;
可以将线程理解为”轻量级进程“。线程是一个基本的CPU执行单位。也是程序执行流的最小单位。引入线程之后,不仅是进程之间可以并发,进程内的个线程之间也可以并发,从而进一步提升了系统的并发度,使得一个进程内也可以并发处理各种任务(如QQ视频、文字聊天、传文件);
引入线程后,进程只作为除CPU之外的系统资源的分配空间(如打印机、内存地址空间等都是分配给进程的)。线程则作为处理机的分配单元。
2. 线程的特性和优点
-
当切换进程时,需要保存/恢复进程运行环境,还需要切换内存地址(更新快表、更新缓存),而同一进程内的各个线程间并发,不需要切换进程运行环境和内存地址空间,省事省力。这里可以看出,引入线程机制后,并发带来的系统开销降低,系统并发性提升。
注意:从属不同进程的线程间切换,也会导致进程的切换!开销也大!
-
从属同一进程的各个线程共享进程拥有的资源。各个进程的内存地址相互独立,只能通过请求操作系统内核的帮助来完成进程间通信;而同一进程下的各个线程共享内存地址空间,可以直接通过读/写内存空间进行通信。
-
引入线程前,进程既是资源分配的基本单位,也是调度的基本单位。引入线程后,进程是资源分配的基本单位,线程是调度的基本单位,线程几乎不拥有资源,只拥有极少量的资源(线程控制块 TCB、寄存器信息、堆栈等)。线程也有运行态、就绪态、阻塞态。在多 CPU 环境下,各个线程也可以分派到不同的 CPU 上并行地执行。
3. 线程的实现方式
3.1 用户级线程(User-Level Thread, ULT)
早期的操作系统(如早期Unix)只支持进程,不支持线程。但是的”线程“是由数据库实现的。
以同时处理视频、文字聊天、文件传送的 QQ 进程为例,以下是其简单的实现代码:
int main() {
int i = 0;
while (true) {
if(i == 0){处理视频聊天的代码;}
if(i == 1){处理文字聊天的代码;}
if(i == 2){处理文件传输的代码;}
i = (i + 1) % 3; // i 的值为 0,1,2,0,1,2 ...
}
}
从代码的角度看,线程其实就是一段代码逻辑。上述三段代码逻辑上可以看作三个“线程”。while 循环就是一个最弱智的“线程库”,线程库完成了对线程的管理工作(如调度)。
用户级线程由应用程序通过线程库实现,所有的线程管理工作都由应用程序负责(包括线程切换);线程切换可以在用户态下完成,无需操作系统干预,这种线程的实现方式在用户看来是有多个线程。但是在操作系统内核看来,意识不到线程的存在。“用户级线程”就是“从用户视角看能看到的线程。”
优缺点:
优点:用户及线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高
缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行。
3.2 内核级线程(Kernel-Level Thread, KLT)
内核级线程是指由操作系统支持的线程,内核级线程的管理工作由操作系统内核完成。线程调度、切换等工作都由内核负责,因此内核级线程的切换必然需要在核心态下才能完成。
操作系统会为每个内核级线程建立相应的 TCB(Thread Control Block,线程控制块),通过 TCB 对线程进行管理。“内核级线程”就是“从操作系统内核视角能看到的线程”
优缺点:
优点:当一个线程被阻塞后,别的线程还可以继续进行,并发能力强。多线程可在多核处理机上并行运行。
缺点:一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。
4. 线程模式
在支持内核级线程的系统中,根据用户级线程和内核级线程的映射关系,可以划分为几种多线程模型。
4.1 一对一模型
一对一模型即一个用户级线程映射到一个内核级线程。每个用户进程有与用户级线程同数量的内核级线程。即上述所提到的内核级线程。
4.2 多对一模型
多对一模型:多个用户级线程映射到一个内核级线程。且一个进程只被分配一个内核级线程 。即上述提到的用户级线程。
注意:操作系统只“看得见”内核级线程,因此只有内核级线程才是处理机分配的单位。
4.3 多对多模型
多对多模型:n 用户级线程映射到 m 个内核级线程(n >= m)。每个用户进程对应 m 个内核级线程。
克服了多对一模型并发度不高的缺点(一个阻塞全体阻塞),又克服了一对一模型中一个用户进程占用太多内核级线程,开销太大的缺点。
可以这么理解:用户级线程是“代码逻辑”的载体;内核级线程是“运行机会”的载体。内核级线程才是处理机分配的单位。