进程与线程概念基础知识
- 进程
- 进程的概念
- 进程控制块
- 进程状态
- 进程三状态模型
- 挂起进程模模型
- 进程的上下文切换
- 进程控制
- 线程
- 为什么引入线程
- 线程的概念
- 线程与进程的比较
- 线程的上下文切换
- 线程的实现
进程
进程的概念
1. 进程的定义: 进程是指一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程
2. 进程的组成: 进程包含了正在运行的一个程序的所有状态信息(代码、数据、状态寄存器、通用寄存器、进程占用系统资源)
3. 进程的特点:
- 动态性: 可动态地创建,结果进程;
- 并发性: 进程可以被独立调度并占用处理机运行;
- 独立性: 不同进程的工作不相互影响;
- 制约性: 因访问共享数据 / 资源或进程间同步而产生制约。
4. 进程和程序的联系:
- 进程是操作系统处于执行状态程序的抽象(动态的)
- 程序 = 文件(静态的可执行文件)
- 进程 = 执行中的程序 = 程序 + 执行状态
- 同一个程序的多次执行过程对应为不同进程,如ls多次执行对应不同进程
- 进程执行需要的资源 :内存(保存代码和数据)+ CPU(执行指令)
5. 进程和程序的区别:
- 进程是动态的,程序是静态的
- 进程是暂时的,程序是永久的
- 进程与程序的组成不同
进程控制块
描述进程的数据结构:进程控制块 (Process Control Block)
操作系统为每个进程都维护了一个PCB,用来保存与该进程有关的各种状态信息。
1. 进程控制块: 操作系统管理控制进程运行所用的信息集合。
- 操作系统用PCB来描述进程的基本情况以及运行变化的过程
- PCB是进程存在的唯一标志。
2. 使用进程控制块
- 进程的创建:为该进程生成一个PCB
- 进程的终止: 回收它的PCB
- 进程的组织管理: 通过对PCB的组织管理来实现
PCB具体包含什么信息? 如何组织的? 进程的状态转换?
3. 进程控制块内容: 进程标识信息、处理机现场保存(多进程交替运行时用)、进程控制信息
PCB含有以下三大类信息:
① 进程标志信息。如本进程的标志符,本进程的产生者标志(父进程标志);用户标志
② 处理机状态信息保存区。保存进程的运行现场信息:
-
用户可见寄存器; 用户程序可以使用的数据,地址等寄存器。
-
控制和状态寄存器; 如程序计数器(PC),程序状态字(PSW)。
-
栈指针;过程调用,系统调用,中断处理和返回时需要用到它。
③ 进程控制信息
-
调度和状态信息:进程和处理机制使用情况调度
-
进程间通信信息:为支持进程间与通信相关的各种标志,信号,信件等
-
存储管理信息:包含有指向本进程映像存储空间的数据结构。
-
进程所用资源:说明由进程打开,进程使用的系统资源。 如打开的文件等。
-
有关数据结构的链接信息:进程可以连接到一个进程队列中,或连接到相关的其他进程的PCB。
4. 进程控制块的组织:
-
链表: 同一状态的进程其PCB成一链表,多个状态对应多个不同的链表。各状态的进程形成不同的链表:就绪链表,阻塞链表
-
索引表: 同一状态的进程归入一个index表(由index指向PCB),多个状态对应多个不同的index表。各状态的进行形成不同的索引表:就绪索引表,阻塞索引表
进程状态
进程的生命周期划分: 创建、执行、等待、抢占、唤醒、结束
进程创建
引起进程创建的三种情况:
- 系统初始化时
- 用户请求 创建一个新进程
- 正在运行的程序执行了创建进程的系统调用
进程执行
内核选择一个就绪的进程,让它占用处理机并执行(后续的调度算法)
进程等待
进程进入等待(阻塞)的情况
- 请求并等待系统服务,无法马上完成
- 启动某种操作,无法马上完成
- 需要的数据没有到达
进程只能自己阻塞自己,因为只有进程自身才能知道何时需要等待某种事件的发生。
进程抢占
进程会被抢占的情况
- 高优先级进程就绪
- 进程执行当前时间完
进程唤醒
唤醒进程的情况:
- 被阻塞进程需要的资源可被满足
- 被阻塞进程等待的事件到达
进程只能被别的进程或操作系统唤醒
进程结束
进程结束的情况
- 正常退出(自愿的)
- 错误退出(自愿的)
- 致命错误(强制性的)
- 被其他进程所杀(强制性的)
sleep()系统调用对应的进程状态变化
进程切换
进程三状态模型
三状态模型:就绪、运行、等待
进程的三种基本状态:
运行状态(Running): 当一个进程正在处理机上运行时
就绪状态(Ready): 一个进程获得了除处理机之外的一切所需资源,一旦得到处理机即可运行
等待/阻塞状态(Blocked): 一个进程正在等待某一时间而暂停运行时。 如等待某资源,等待输入 / 输出完成。
进程其它的基本状态:
创建状态(New):一个进程正在被创建,还没被转到就绪状态之前的状态。
结束状态(Exit): 一个进程正在从系统中消失时的状态,这是因为进程结束或由于其它原因所导致。
可能的状态变化如下:
-
NULL → New:一个新进程被产生出来执行一个程序。
-
New → Ready: 当进程创建完成并初始化后,一切就绪准备运行时,变为就绪状态。是否会持续很久?很快。
-
Ready → Running :处于就绪态的进程被进程调度程序选中后,就分配到处理机上来运行。(怎么选中取决于后面的调度算法)
-
Running → Exit :当进程表示它已经完成或者因出错,当前运行进程会由操作系统作结束处理。
-
Running → Ready :处于运行状态的进程在其运行过程中,由于分配它的处理机时间片用完而让出处理机。谁完成?OS。
-
Running → Blocked: 当进程请求某样东西且必须等待时。例如?等待一个计时器的到达、读 / 写文件 比较慢等。
-
Blocked → Ready :当进程要等待某事件到来时,它从阻塞状态变到就绪状态。例如?事件到达等。谁完成?OS。
挂起进程模模型
Why?为了合理且充分地利用系统资源。
进程在挂起状态时,意味着进程没有占用内存空间,处在挂起状态的进程映像在磁盘上,目的是减少进程占用内存
1. 挂起状态
阻塞挂起状态(Blocked-suspend): 进程在外存并等待某事件的出现。
就绪挂起状态(Ready-suspend): 进程在外存,但只要进入内存,即可运行。
与挂起相关的状态转换
-
阻塞到阻塞挂起: 没有进程处于就绪状态或就绪进程要求更多内存资源时,会进行这种转换,以提交新进程或运行时就绪进程。
-
就绪到就绪挂起: 当有高优先级阻塞进程和低优先级就绪进程时,系统会选择挂起低优先级就绪进程。
-
运行到就绪挂起: 对抢先式分时系统,当有高优先级阻塞挂起进程因事件出现而进入就绪挂起时,系统可能会把运行进程转导就绪挂起状态。
2. 解挂 / 激活状态
把一个进程从外存转到内存,可能有以下几种情况:
-
就绪挂起到就绪: 没有就绪进程或挂起就绪进程优先级高于就绪进程时,会进行这种转换。
-
阻塞挂起到阻塞: 当一个进程释放足够内存时,系统会把一个高优先级阻塞挂起进程转换为阻塞进程。
3. 状态队列
- 由操作系统来维护一组队列,表示系统中所有进程的当前状态
- 不同队列表示不同状态,就绪队列、各种等待队列
- 根据进程状态不同,进程PCB加入相应队列,进程状态变化时,它所在的PCB会从一个队列换到另一个
进程的上下文切换
各个进程之间是共享 CPU 资源的,在不同的时候进程之间需要切换,让不同的进程可以在 CPU 执行,那么这个一个进程切换到另一个进程运行,称为进程的上下文切换
- 暂停当前运行的进程,从而运行状态变成其他状态
- 调度另一个进程从就绪状态变成运行状态
进程切换的要求
- 切换前,保存进程上下文
- 切换后,恢复进程上下文
- 快速切换
进程生命周期的信息
- 寄存器(PC,SP,…)
- CPU状态
- 内存地址空间
进程是由内核管理和调度的,所以进程的切换只能发生在内核态。
所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行,如下图所示:
上下文切换图示:
发生进程上下文切换有哪些场景?
- 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。(时间片流转)
- 进程在系统资源不足够(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程行;(内存不足被挂起)
- 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;(sleep延时函数)
- 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;(被更高优先级任务抢占)
- 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;(硬件中断)
进程控制
熟知了进程的状态变迁和进程的数据结构 PCB 后,再来看看进程的创建、终止、阻塞、唤醒的过程,这些过程也就是进程的控制。
01 创建进程
操作系统允许一个进程创建另一个进程,而且允许子进程继承父进程所拥有的资源,当子进程被终止时,其在父进程处继承的资源应当还给父进程。同时,终止父亲进程时同时也会终止其所有的子进程。
1. 创建进程的过程如下:
- 为新进程分配一个唯⼀的进程标识号,并申请⼀个空白的 PCB,若申请失败则创建失败;
- 为进程分配资源,此处如果资源不足,进程就会进入等待状态,以等待资源;
- 初始化 PCB;
- 如果进程的调度队列能够接纳新进程,那就将进程插⼊到就绪队列,等待被调度运行;
2. 创建进程相关函数介绍
Unix进程创建系统调用:fork / exec
fork()
把一个进程复制成二个进程(parent old PlD, child new PlD)exec()
用新程序来重写当前进程,PID没有改变
用fork()和exec()创建进程示例:
int pid = fork(); //创建子进程
if(pid == 0) //子进程
{
//Do anything
//加载其他任何一个程序
exec("program", argc, argv[0], argv[1], ...);
}
-
fork()
创建一个继承的子进程- 复制父进程的所有变量和内存
- 复制父进程的所有CPU寄存器(有一个寄存器除外)
-
fork()
的返回值- 子进程的
fork()
返回 0 - 父进程的
fork()
返回子进程标识符 fork()
返回值可方便后续使用,子进程可使用getpid()
获取PID
- 子进程的
3. fork()的地址空间复制
fork()
执行过程对于子进程而言,是在调用时间对父进程地址空间的一次复制,对于父进程fork()
返回child PlD, 对于子进程返回值为0
02 进程加载
执行exec()时,进程可能处于不同的状态。
4. 程序加载和执行
系统调用exec()
加载程序取代当前运行的进程。
In the parent process:
main()
...
int pid = fork(); // 创建子进程
if (pid == 0) { // 子进程
exec_status = exec("calc", argc, argv0, argv1, ...);
printf("Why would I execute?");
} else { // 父进程。合理设计:else if (pid > 0)
printf("Whose your daddy?");
...
child_status = wait(pid);
}
if (pid < 0) { /* error occurred */
执行完exec()
后,pid的id变化了,open files的路径也改变了
在实际内存中的布局图:
执行完exec()
后,PCB中的代码段完全被新的程序calc所替换,且执行地址发生了变化
5. fork的开销
fork()
的实现开销
- 对子进程分配内存
- 复制父进程的内存和CPU寄存器到子进程(有一个寄存器例外)
- 开销昂贵!!
在99%的情况下,我们在调用fork()
之后调用exec()
- 在
fork()
操作中内存复制是没有作用的 - 子进程将可能关闭打开的文件和连接
03 等待和退出
等待和退出是父子进程之间的一种交互,完成子进程的资源回收
wait()
系统调用:用于父进程等待子进程的结束,子进程结束时通过exit()向父进程返回一个值,父进程通过wait()
接受并处理返回值,存在以下几种情况:
-
子进程存活,父进程进入阻塞等待状态,等待子进程的返回结果,当子进程调用
exit()
时唤醒父进程,将exit()
返回值作为父进程中wait()
的返回值 -
有僵尸进程进程等待时(子进程
exit()
提前退出,变成僵尸进程),wait()
立即返回一个值 -
若无子进程存活是,
wait()
立刻返回
exit()
系统调用:进程的有序终止,进程结束执行时调用exit()
,完成进程资源回收,主要有以下几大功能;
-
将调用参数作为进程的 “结果”
-
关闭所有打开的资源(文件,连接等等)
-
释放内存
-
释放大部分支持进程的操作系统结构
-
检查是否父进程是存活着的:
- 若存活:保留结果的值直到僵尸进程需要它,进入僵尸(zombie)状态
- 如果不存活:释放所有的数据结构,进程结束
-
清理所有等待的僵死进程
进程终止是最终的垃圾收集(资源回收)
执行exec()时,进程可能处于不同的状态。
线程
为什么引入线程
我们举个例⼦,假设你要编写⼀个视频播放器软件,那么该软件功能的核心模块有三个:
- 从视频⽂件当中读取数据;
- 对读取的数据进⾏解压缩;
- 把解压缩后的视频数据播放出来;
对于单进程的实现⽅式,都会是以下这个⽅式:
对于单进程的这种方式,存在以下问题:
- 播放出来的画面和声音会不连贯,因为当 CPU 能力不够强的时候, Read 的时候可能进程就等在这了,这样就会导致等半天才进行数据解压和播放;
- 各个函数之间不是并发执行,影响资源的使用效率;
那改进成多进程的方式:
对于多进程的这种方式,依然会存在问题:
- 进程之间如何通信,共享数据?
- 维护进程的系统开销较大,如创建进程时,分配资源、建立 PCB;终止进程时,回收资源、撤销PCB;进程切换时,保存当前进程的状态信息;
那到底如何解决呢?需要有⼀种新的实体,满足以下特性:
- 实体之间可以并发运行;
- 实体之间共享相同的地址空间;
这个新的实体,就是 线程( Thread
),线程之间可以并发运行且共享相同的地址空间。
线程的概念
线程是进程当中的一条执行流程
线程是进程的一部分,描述指令流执行状态。它是进程中的指令执行流的最小单元,是CPU调度的基本单位。
同一个进程内多个线程之间可以共享代码段、数据段、打开的⽂件等资源,但每个线程各自都有一套独立的寄存器和栈,这样可以确保线程的控制流是相对独立的。
线程的优缺点?
线程的优点:
- ⼀个进程中可以同时存在多个线程;
- 各个线程之间可以并发执行;
- 各个线程之间可以共享地址空间和⽂件等资源;
线程的缺点:
- 当进程中的⼀个线程崩溃时,会导致其所属进程的所有线程崩溃
线程与进程的比较
进程线程的比较如下:
- 进程是资源(包括内存、打开的⽂件等)分配的单位,线程是 CPU 调度的单位;
- 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
- 线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系;
- 线程能减少并发执行的时间和空间开销;
对于,线程相比进程能减少开销,体现在:
- 线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们;
- 线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
- 同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同⼀个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较⼤的;
- 由于同⼀进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更⾼了;
线程的上下文切换
线程与进程最⼤的区别在于:线程是CPU调度的基本单位,而进程则是资源分配的基本单位
所以,所谓操作系统的任务调度,实际上的调度对象是线程,⽽进程只是给线程提供了虚拟内存、全局变量等资源。
对于线程和进程,我们可以这么理解:
-
当进程只有⼀个线程时,可以认为进程就等于线程;
-
当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下⽂切换时是不需要修改的;
另外,线程也有自己的私有数据,比=如栈和寄存器等,这些在上下文切换时也是需要保存的。
线程上下文切换的是什么?
这还得看线程是不是属于同一个进程:
-
当两个线程不是属于同一个进程,则切换的过程就跟进程上下文切换⼀样;
-
当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据;线程是进程当中的一条执行流程
所以,线程的上下文切换相比进程,开销要小很多
线程的实现
主要有三种线程的实现方式:
- 用户线程(
User Thread
):在用户空间实现的线程,不是由内核管理的线程,是由⽤户态的线程库来完成线程的管理; - 内核线程(
Kernel Thread
):在内核中实现的线程,是由内核管理的线程; - 轻量级进程(
LightWeight Process
):在内核中来⽀持用户线程;
那么,这还需要考虑一个问题,用户线程和内核线程的对应关系:
首先,第一种关系是多对一的关系,也就是多个⽤户线程对应同一个内核线程:
第二种是一对一的关系,也就是一个用户线程对应一个内核线程:
第三种是多对多的关系,也就是多个⽤户线程对应到多个内核线程:
1. 用户线程
用户线程是基于用户态的线程管理库来实现的,那么线程控制块 TCB 也是在库里面来实现的,对于操作系统而言是看不到这个 TCB 的,它只能看到整个进程的 PCB。
所以,用户线程的整个线程管理和调度,操作系统是不直接参与的,而是由用户级线程库函数来完成线程的管理,包括线程的创建、终止、同步和调度等。
用户线程的 特征/优点:
-
不依赖与操作系统的内核,内核不感知用户线程的存在,可用于不支持线程的多进程操作系统
-
在用户空间实现的线程机制,每个进程有私有的线程控制块(TCB)列表,TCB由线程库函数维护
-
同一进程内的用户线程切换速度快,无需要用户态 / 核心态切换
-
允许每个进程拥有自己的线程调度算法
用户线程的缺点:
-
线程发起系统调用而阻塞时,则整个进程进入等待状态
-
不支持基于线程的处理机抢占,除非当前运行线程主动放弃,它所在进程的其他线程无法抢占CPU
-
只能按进程分配CPU时间,多个线程进程中,每个线程的时间片较少
2. 内核线程
内核线程是由操作系统管理的,线程对应的 TCB 自然是放在操作系统里的,这样线程的创建、终止和管理都是由操作系统负责。
内核线程的优点
- 由内核维护 PCB 和 TCB
- 线程执行系统调用而被阻塞不影响其他线程
- 以线程为单位进行CPU时间分配,多线的进程可获得更多的CPU时间。
内核线程的缺点
- 线程的创建、终止和切换相对较大,通过系统调用 / 内核函数,在核内实现
3. 轻量级线程
轻量级进程(Light-weight process
, LWP)是内核⽀持的用户线程,一个进程可有一个或多个 LWP,每个 LWP 是跟内核线程一对一映射的,也就是 LWP 都是由一个内核线程支持。
在大多数系统中,LWP与普通进程的区别也在于它只有⼀个最小的执行上下文和调度程序所需的统计信息。
在 LWP 之上也是可以使用用户线程的,那么 LWP 与用户线程的对应关系就有三种:
1 : 1
,即⼀个 LWP 对应 ⼀个用户线程;N : 1
,即⼀个 LWP 对应多个用户线程;M : N
,即多个 LMP 对应多个用户线程;
LWP 模型:
1 : 1 模式
⼀个线程对应到⼀个 LWP 再对应到⼀个内核线程,如上图的进程 4,属于此模型。
- 优点:实现并行,当⼀个 LWP 阻塞,不会影响其他 LWP;
- 缺点:每⼀个用户线程,就产生⼀个内核线程,创建线程的开销较⼤。
N : 1 模式
多个用户线程对应⼀个 LWP 再对应⼀个内核线程,如上图的进程 2,线程管理是在用户空间完成的,此模式中用户的线程对操作系统不可见。
- 优点:用户线程要开几个都没问题,且上下文切换发生用户空间,切换的效率较⾼;
- 缺点:一个用户线程如果阻塞了,则整个进程都将会阻塞,另外在多核 CPU 中,是没办法充分利用CPU 的。
M : N 模式
根据前面的两个模型混搭⼀起,就形成 M:N
模型,该模型提供了两级控制,首先多个用户线程对应到多个 LWP,LWP 再一一对应到内核线程,如上图的进程 3。
- 优点:综合了前两种优点,大部分的线程上下文发生在用户空间,且多个线程又可以充分利用多核CPU 的资源。
组合模式
如上图的进程 5,此进程结合 1:1
模型和 M:N
模型。开发⼈员可以针对不同的应用特点调节内核线程的数目来达到物理并行性和逻辑并行性的最佳方案。
学习整理自【清华大学】操作系统 + 【小林Coding】图解系统