进程、线程基础知识

先来看看一则小故事

我们写好的⼀⾏⾏代码,为了让其⼯作起来,我们还得把它送进城( 进程 )⾥,那既然进了城⾥,那肯定不能胡作⾮为了。
城⾥⼈有城⾥⼈的规矩,城中有个专⻔管辖你们的城管( 操作系统 ),⼈家让你休息就休息,让你⼯作就⼯作,毕竟摊位不多,每个⼈都要占这个摊位来⼯作,城⾥要⼯作的⼈多着去了。
所以城管为了公平起⻅,它使⽤⼀种策略( 调度 )⽅式,给每个⼈⼀个固定的⼯作时间( 时间⽚ ),时间到了就会通知你去休息⽽换另外⼀个⼈上场⼯作。
另外,在休息时候你也不能偷懒,要记住⼯作到哪了,不然下次到你⼯作了,你忘记⼯作到哪了,那还怎么继续?
有的⼈,可能还进⼊了县城( 线程 )⼯作,这⾥相对轻松⼀些,在休息的时候,要记住的东⻄相对较少, ⽽且还能共享城⾥的资源。
哎哟,难道本⽂内容是进程和线程?
可以,聪明的你猜出来了,也不枉费我瞎编乱造的故事了。 进程和线程对于写代码的我们,真的天天⻅、⽇⽇⻅了,但⻅的多不代表你就熟悉它们,⽐如简单问你⼀句,你知道它们的⼯作原理和区别吗?
不知道没关系,今天就要跟⼤家讨论 操作系统的进程和线程

进程

我们编写的代码只是⼀个存储在硬盘的静态⽂件,通过编译后就会⽣成⼆进制可执⾏⽂件,当我们运⾏这个可执⾏⽂件后,它会被装载到内存中,接着 CPU 会执⾏程序中的每⼀条指令,那么这个 运⾏中的程序, 就被称为「进程」( Process
现在我们考虑有⼀个会读取硬盘⽂件数据的程序被执⾏了,那么当运⾏到读取⽂件的指令时,就会去从硬盘读取数据,但是硬盘的读写速度是⾮常慢的,那么在这个时候,如果 CPU 傻傻的等硬盘返回数据的话, 那 CPU 的利⽤率是⾮常低的。 做个类⽐,你去煮开⽔时,你会傻傻的等⽔壶烧开吗?很明显,⼩孩也不会傻等。我们可以在⽔壶烧开之
前去做其他事情。当⽔壶烧开了,我们⾃然就会听到 嘀嘀嘀 的声⾳,于是再把烧开的⽔倒⼊到⽔杯⾥就好了。
所以,当进程要从硬盘读取数据时, CPU 不需要阻塞等待数据的返回,⽽是去执⾏另外的进程。当硬盘数据返回时,CPU 会收到个 中断 ,于是 CPU 再继续运⾏这个进程。
这种 多个程序、交替执⾏ 的思想,就有 CPU 管理多个进程的初步想法。
对于⼀个⽀持多进程的系统, CPU 会从⼀个进程快速切换⾄另⼀个进程,其间每个进程各运⾏⼏⼗或⼏百个毫秒。
虽然单核的 CPU 在某⼀个瞬间,只能运⾏⼀个进程。但在 1 秒钟期间,它可能会运⾏多个进程,这样就产⽣ 并⾏的错觉 ,实际上这是 并发
并发和并⾏有什么区别?
⼀图胜千⾔。

进程与程序的关系的类⽐
到了晚饭时间,⼀对⼩情侣肚⼦都咕咕叫了,于是男⽣⻅机⾏事,就想给⼥⽣做晚饭,所以他就在⽹上找了辣⼦鸡的菜谱,接着买了⼀些鸡⾁、辣椒、⾹料等材料,然后边看边学边做这道菜。
突然,⼥⽣说她想喝可乐,那么男⽣只好把做菜的事情暂停⼀下,并在⼿机菜谱标记做到哪⼀个步骤,把状态信息记录了下来。然后男⽣听从⼥⽣的指令,跑去下楼买了⼀瓶冰可乐后,⼜回到厨房继续做菜。
这体现了, CPU 可以从⼀个进程(做菜)切换到另外⼀个进程(买可乐),在切换前必须要记录当前进程 中运⾏的状态信息,以备下次切换回来的时候可以恢复执⾏。
所以,可以发现进程有着「 运⾏ - 暂停 - 运⾏ 」的活动规律。

进程的状态

在上⾯,我们知道了进程有着「运⾏ - 暂停 - 运⾏」的活动规。⼀般说来,⼀个进程并不是⾃始⾄终连续不停地运⾏的,它与并发执⾏中的其他进程的执⾏是相互制约的。
它有时处于运⾏状态,有时⼜由于某种原因⽽暂停运⾏处于等待状态,当使它暂停的原因消失后,它⼜进⼊准备运⾏状态。
所以, 在⼀个进程的活动期间⾄少具备三种基本状态,即运⾏状态、就绪状态、阻塞状态。

上图中各个状态的意义:
  • 运⾏状态Runing):该时刻进程占⽤ CPU
  • 就绪状态Ready):可运⾏,由于其他进程处于运⾏状态⽽暂时停⽌运⾏;
  • 阻塞状态Blocked):该进程正在等待某⼀事件发⽣(如等待输⼊/输出操作的完成)⽽暂时停⽌运⾏,这时,即使给它CPU控制权,它也⽆法运⾏;
当然,进程还有另外两个基本状态:
  • 创建状态new):进程正在被创建时的状态;
  • 结束状态Exit):进程正在从系统中消失时的状态;
于是,⼀个完整的进程状态的变迁如下图:
再来详细说明⼀下进程的状态变迁:
  • NULL -> 创建状态:⼀个新进程被创建时的第⼀个状态;
  • 创建状态 -> 就绪状态:当进程被创建完成并初始化后,⼀切就绪准备运⾏时,变为就绪状态,这个过程是很快的;
  • 就绪态 -> 运⾏状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运⾏该进程;
  • 运⾏状态 -> 结束状态:当进程已经运⾏完成或出错时,会被操作系统作结束状态处理;
  • 运⾏状态 -> 就绪状态:处于运⾏状态的进程在运⾏过程中,由于分配给它的运⾏时间⽚⽤完,操作系统会把该进程变为就绪态,接着从就绪态选中另外⼀个进程运⾏;
  • 运⾏状态 -> 阻塞状态:当进程请求某个事件且必须等待时,例如请求 I/O 事件;
  • 阻塞状态 -> 就绪状态:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;
如果有⼤量处于阻塞状态的进程,进程可能会占⽤着物理内存空间,显然不是我们所希望的,毕竟物理内存空间是有限的,被阻塞状态的进程占⽤着物理内存就⼀种浪费物理内存的⾏为。
所以,在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存空间换出到硬盘,等需要再次运⾏的时候,再从硬盘换⼊到物理内存。
那么,就需要⼀个新的状态,来 描述进程没有占⽤实际的物理内存空间的情况,这个状态就是挂起状态 。 这跟阻塞状态是不⼀样,阻塞状态是等待某个事件的返回。
另外,挂起状态可以分为两种:
  • 阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;
  • 就绪挂起状态:进程在外存(硬盘),但只要进⼊内存,即刻⽴刻运⾏;
这两种挂起状态加上前⾯的五种状态,就变成了七种状态变迁(留给我的颜⾊不多了),⻅如下图:
导致进程挂起的原因不只是因为进程所使⽤的内存空间不在物理内存,还包括如下情况:
  • 通过 sleep 让进程间歇性挂起,其⼯作原理是设置⼀个定时器,到期后唤醒进程。
  • ⽤户希望挂起⼀个程序的执⾏,⽐如在 Linux 中⽤ Ctrl+Z 挂起进程;

进程的控制结构

在操作系统中,是⽤ 进程控制块 process control block PCB )数据结构来描述进程的。
PCB 是什么呢?打开知乎搜索你就会发现这个东⻄并不是那么简单。
打住打住,我们是个正经的⼈,怎么会去看那些问题呢?是吧,回来回来。
PCB 是进程存在的唯⼀标识 ,这意味着⼀个进程的存在,必然会有⼀个 PCB ,如果进程消失了,那么 PCB 也会随之消失。
PCB 具体包含什么信息呢?
进程描述信息:
  • 进程标识符:标识各个进程,每个进程都有⼀个并且唯⼀的标识符;
  • ⽤户标识符:进程归属的⽤户,⽤户标识符主要为共享和保护服务;
进程控制和管理信息:
  • 进程当前状态,如 newreadyrunningwaiting blocked 等;
  • 进程优先级:进程抢占 CPU 时的优先级;
资源分配清单:
  • 有关内存地址空间或虚拟地址空间的信息,所打开⽂件的列表和所使⽤的 I/O 设备信息。
CPU 相关信息:
  • CPU 中各个寄存器的值,当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程 重新执⾏时,能从断点处继续执⾏。
可⻅,PCB 包含信息还是⽐较多的。
每个 PCB 是如何组织的呢?
通常是通过 链表 的⽅式进⾏组织,把具有 相同状态的进程链在⼀起,组成各种队列
⽐如:
  • 将所有处于就绪状态的进程链在⼀起,称为就绪队列
  • 把所有因等待某事件⽽处于等待状态的进程链在⼀起就组成各种阻塞队列
  • 另外,对于运⾏队列在单核 CPU 系统中则只有⼀个运⾏指针了,因为单核 CPU 在某个时间,只能运⾏⼀个程序。
那么,就绪队列和阻塞队列链表的组织形式如下图:
除了链接的组织⽅式,还有索引⽅式,它的⼯作原理:将同⼀状态的进程组织在⼀个索引表中,索引表项指向相应的 PCB ,不同状态对应不同的索引表。
⼀般会选择链表,因为可能⾯临进程创建,销毁等调度导致进程状态发⽣变化,所以链表能够更加灵活的插⼊和删除。

进程的控制

01 创建进程
操作系统允许⼀个进程创建另⼀个进程,⽽且允许⼦进程继承⽗进程所拥有的资源,当⼦进程被终⽌时,其在⽗进程处继承的资源应当还给⽗进程。同时,终⽌⽗进程时同时也会终⽌其所有的⼦进程。
注意: Linux 操作系统对于终⽌有⼦进程的⽗进程,会把⼦进程交给 1 号进程接管。本⽂所指出的进程终⽌概念是宏观操作系统的⼀种观点,最后怎么实现当然是看具体的操作系统。
创建进程的过程如下:
  • 为新进程分配⼀个唯⼀的进程标识号,并申请⼀个空⽩的 PCBPCB 是有限的,若申请失败则创建失败;
  • 为进程分配资源,此处如果资源不⾜,进程就会进⼊等待状态,以等待资源;
  • 初始化 PCB如果进程的调度队列能够接纳新进程,那就将进程插⼊到就绪队列,等待被调度运⾏;
02 终⽌进程
进程可以有 3 种终⽌⽅式:正常结束、异常结束以及外界⼲预(信号 kill 掉)。
终⽌进程的过程如下:
  • 查找需要终⽌的进程的 PCB
  • 如果处于执⾏状态,则⽴即终⽌该进程的执⾏,然后将 CPU 资源分配给其他进程;
  • 如果其还有⼦进程,则应将其所有⼦进程终⽌;
  • 将该进程所拥有的全部资源都归还给⽗进程或操作系统;
  • 将其从 PCB 所在队列中删除;
03 阻塞进程
当进程需要等待某⼀事件完成时,它可以调⽤阻塞语句把⾃⼰阻塞等待。⽽⼀旦被阻塞等待,它只能由另⼀个进程唤醒。
阻塞进程的过程如下:
  • 找到将要被阻塞进程标识号对应的PCB
  • 如果该进程为运⾏状态,则保护其现场,将其状态转为阻塞状态,停⽌运⾏;
  • 将该 PCB 插⼊到阻塞队列中去;
04 唤醒进程
进程由「运⾏」转变为「阻塞」状态是由于进程必须等待某⼀事件的完成,所以处于阻塞状态的进程是绝对不可能叫醒⾃⼰的。
如果某进程正在等待 I/O 事件,需由别的进程发消息给它,则只有当该进程所期待的事件出现时,才由发现者进程⽤唤醒语句叫醒它。
唤醒进程的过程如下:
  • 在该事件的阻塞队列中找到相应进程的 PCB
  • 将其从阻塞队列中移出,并置其状态为就绪状态;
  • 把该 PCB 插⼊到就绪队列中,等待调度程序调度;
进程的阻塞和唤醒是⼀对功能相反的语句,如果某个进程调⽤了阻塞语句,则必有⼀个与之对应的唤醒语句。

进程的上下文切换

各个进程之间是共享 CPU 资源的,在不同的时候进程之间需要切换,让不同的进程可以在 CPU 执⾏,那么这个 ⼀个进程切换到另⼀个进程运⾏,称为进程的上下⽂切换
在详细说进程上下⽂切换前,我们先来看看 CPU 上下⽂切换
⼤多数操作系统都是多任务,通常⽀持⼤于 CPU 数量的任务同时运⾏。实际上,这些任务并不是同时运⾏的,只是因为系统在很短的时间内,让各个任务分别在 CPU 运⾏,于是就造成同时运⾏的错觉。
任务是交给 CPU 运⾏的,那么在每个任务运⾏前, CPU 需要知道任务从哪⾥加载,⼜从哪⾥开始运⾏。
所以,操作系统需要事先帮 CPU 设置好 CPU 寄存器和程序计数器
CPU 寄存器是 CPU 内部⼀个容量⼩,但是速度极快的内存(缓存)。我举个例⼦,寄存器像是你的⼝袋,内存像你的书包,硬盘则是你家⾥的柜⼦,如果你的东⻄存放到⼝袋,那肯定是⽐你从书包或家⾥柜⼦取出来要快的多。
再来,程序计数器则是⽤来存储 CPU 正在执⾏的指令位置、或者即将执⾏的下⼀条指令位置。
所以说, CPU 寄存器和程序计数 CPU 在运⾏任何任务前,所必须依赖的环境,这些环境就叫做 CPU 上下⽂
既然知道了什么是 CPU 上下⽂,那理解 CPU 上下⽂切换就不难了。
CPU 上下⽂切换就是先把前⼀个任务的 CPU 上下⽂( CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下⽂到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运⾏新任务。
系统内核会存储保持下来的上下⽂信息,当此任务再次被分配给 CPU 运⾏时, CPU 会重新加载这些上下⽂,这样就能保证任务原来的状态不受影响,让任务看起来还是连续运⾏。
上⾯说到所谓的「任务」,主要包含进程、线程和中断。所以,可以根据任务的不同,把 CPU 上下⽂切换分成: 进程上下⽂切换、线程上下⽂切换和中断上下⽂切换
进程的上下⽂切换到底是切换什么呢?
进程是由内核管理和调度的,所以进程的切换只能发⽣在内核态。
所以, 进程的上下⽂切换不仅包含了虚拟内存、栈、全局变量等⽤户空间的资源,还包括了内核堆栈、寄 存器等内核空间的资源。
通常,会把交换的信息保存在进程的 PCB ,当要运⾏另外⼀个进程的时候,我们需要从这个进程的 PCB取出上下⽂,然后恢复到 CPU 中,这使得这个进程可以继续执⾏,如下图所示:
⼤家需要注意,进程的上下⽂开销是很关键的,我们希望它的开销越⼩越好,这样可以使得进程可以把更多时间花费在执⾏程序上,⽽不是耗费在上下⽂切换。
发⽣进程上下⽂切换有哪些场景?
  • 为了保证所有进程可以得到公平调度,CPU 时间被划分为⼀段段的时间⽚,这些时间⽚再被轮流分配给各个进程。这样,当某个进程的时间⽚耗尽了,进程就从运⾏状态变为就绪状态,系统从就绪队列选择另外⼀个进程运⾏;
  • 进程在系统资源不⾜(⽐如内存不⾜)时,要等到资源满⾜后才可以运⾏,这个时候进程也会被挂起,并由系统调度其他进程运⾏;
  • 当进程通过睡眠函数 sleep 这样的⽅法将⾃⼰主动挂起时,⾃然也会重新调度;
  • 当有优先级更⾼的进程运⾏时,为了保证⾼优先级进程的运⾏,当前进程会被挂起,由⾼优先级进程来运⾏;
  • 发⽣硬件中断时,CPU 上的进程会被中断挂起,转⽽执⾏内核中的中断服务程序;
以上,就是发⽣进程上下⽂切换的常⻅场景了。

线程

在早期的操作系统中都是以进程作为独⽴运⾏的基本单位,直到后⾯,计算机科学家们⼜提出了更⼩的能独⽴运⾏的基本单位,也就是 线程。

为什么使用线程

我们举个例⼦,假设你要编写⼀个视频播放器软件,那么该软件功能的核⼼模块有三个:
  • 从视频⽂件当中读取数据;
  • 对读取的数据进⾏解压缩;把解压缩后的视频数据播放出来;
  • 对于单进程的实现⽅式,我想⼤家都会是以下这个⽅式:

对于单进程的这种⽅式,存在以下问题:
  • 播放出来的画⾯和声⾳会不连贯,因为当 CPU 能⼒不够强的时候, Read 的时候可能进程就等在这了,这样就会导致等半天才进⾏数据解压和播放;
  • 各个函数之间不是并发执⾏,影响资源的使⽤效率;
那改进成多进程的⽅式:
对于多进程的这种⽅式,依然会存在问题:
  • 进程之间如何通信,共享数据?
  • 维护进程的系统开销较⼤,如创建进程时,分配资源、建⽴ PCB;终⽌进程时,回收资源、撤销PCB;进程切换时,保存当前进程的状态信息;
那到底如何解决呢?需要有⼀种新的实体,满⾜以下特性:
  • 实体之间可以并发运⾏;
  • 实体之间共享相同的地址空间;
这个新的实体,就是 线程 ( Thread ) ,线程之间可以并发运⾏且共享相同的地址空间。

什么是线程

线程是进程当中的⼀条执⾏流程。
同⼀个进程内多个线程之间可以共享代码段、数据段、打开的⽂件等资源,但每个线程各⾃都有⼀套独⽴的寄存器和栈,这样可以确保线程的控制流是相对独⽴的。

线程的优缺点?
线程的优点:
  • ⼀个进程中可以同时存在多个线程;
  • 各个线程之间可以并发执⾏;
  • 各个线程之间可以共享地址空间和⽂件等资源;
线程的缺点:
  • 当进程中的⼀个线程崩溃时,会导致其所属进程的所有线程崩溃。
举个例⼦,对于游戏的⽤户设计,则不应该使⽤多线程的⽅式,否则⼀个⽤户挂了,会影响其他同个进程的线程。

线程与进程的比较

线程与进程的⽐较如下:
  • 进程是资源(包括内存、打开的⽂件等)分配的单位,线程是 CPU 调度的单位;
  • 进程拥有⼀个完整的资源平台,⽽线程只独享必不可少的资源,如寄存器和栈;
  • 线程同样具有就绪、阻塞、执⾏三种基本状态,同样具有状态之间的转换关系;
  • 线程能减少并发执⾏的时间和空间开销;
对于,线程相⽐进程能减少开销,体现在:
  • 线程的创建时间⽐进程快,因为进程在创建的过程中,还需要资源管理信息,⽐如内存管理信息、⽂件管理信息,⽽线程在创建的过程中,不会涉及这些资源管理信息,⽽是共享它们;
  • 线程的终⽌时间⽐进程快,因为线程释放的资源相⽐进程少很多;
  • 同⼀个进程内的线程切换⽐进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同⼀个进程的线程都具有同⼀个⻚表,那么在切换的时候不需要切换⻚表。⽽对于进程之间的切换,切换的时候要把⻚表给切换掉,⽽⻚表的切换过程开销是⽐较⼤的;
  • 由于同⼀进程的各线程间共享内存和⽂件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更⾼了;
所以,不管是时间效率,还是空间效率线程⽐进程都要⾼。

线程的上下文切换

在前⾯我们知道了,线程与进程最⼤的区别在于: 线程是调度的基本单位,而进程则是资源拥有的基本单
所以,所谓操作系统的任务调度,实际上的调度对象是线程,⽽进程只是给线程提供了虚拟内存、全局变量等资源。
对于线程和进程,我们可以这么理解:
  • 当进程只有⼀个线程时,可以认为进程就等于线程;
  • 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下⽂切换时是不需要修改的;
另外,线程也有⾃⼰的私有数据,⽐如栈和寄存器等,这些在上下⽂切换时也是需要保存的。

线程上下⽂切换的是什么?
这还得看线程是不是属于同⼀个进程:
  • 当两个线程不是属于同⼀个进程,则切换的过程就跟进程上下⽂切换⼀样;
  • 当两个线程是属于同⼀个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据
所以,线程的上下⽂切换相⽐进程,开销要⼩很多。

线程的实现

主要有三种线程的实现⽅式:
  • 用户线程(User Thread:在⽤户空间实现的线程,不是由内核管理的线程,是由⽤户态的线程库来完成线程的管理;
  • 内核线程(Kernel Thread:在内核中实现的线程,是由内核管理的线程;
  • 轻量级进程(LightWeight Process:在内核中来⽀持⽤户线程;
那么,这还需要考虑⼀个问题,⽤户线程和内核线程的对应关系。
⾸先,第⼀种关系是 多对⼀ 的关系,也就是多个⽤户线程对应同⼀个内核线程:
第⼆种是 ⼀对⼀ 的关系,也就是⼀个⽤户线程对应⼀个内核线程:
第三种是 多对多 的关系,也就是多个⽤户线程对应到多个内核线程:
⽤户线程如何理解?存在什么优势和缺陷?
⽤户线程是基于⽤户态的线程管理库来实现的,那么 线程控制块( Thread Control Block, TCB 也是在库⾥⾯来实现的,对于操作系统⽽⾔是看不到这个 TCB 的,它只能看到整个进程的 PCB
所以, 用户线程的整个线程管理和调度,操作系统是不直接参与的,⽽是由⽤户级线程库函数来完成线程 的管理,包括线程的创建、终⽌、同步和调度等。
⽤户级线程的模型,也就类似前⾯提到的 多对⼀ 的关系,即多个⽤户线程对应同⼀个内核线程,如下图所示:

⽤户线程的 优点
  • 每个线程都需要有它私有的线程控制块(TCB)列表,⽤来跟踪记录它各个线程状态信息(PC、栈指针、寄存器),TCB 由⽤户级线程库函数来维护,可⽤于不⽀持线程技术的操作系统;
  • ⽤户线程的切换也是由线程库函数来完成的,⽆需⽤户态与内核态的切换,所以速度特别快
⽤户线程的 缺点
  • 由于操作系统不参与线程的调度,如果⼀个线程发起了系统调⽤⽽阻塞,那进程所包含的⽤户线程都不能执⾏了。
  • 当⼀个线程开始运⾏后,除⾮它主动地交出 CPU 的使⽤权,否则它所在的进程当中的其他线程⽆法运⾏,因为⽤户态的线程没法打断当前运⾏中的线程,它没有这个特权,只有操作系统才有,但是⽤户线程不是由操作系统管理的。
  • 由于时间⽚分配给进程,故与其他进程⽐,在多线程执⾏时,每个线程得到的时间⽚较少,执⾏会⽐较慢;
以上,就是⽤户线程的优缺点了。
那内核线程如何理解?存在什么优势和缺陷?
内核线程是由操作系统管理的,线程对应的 TCB 自 然是放在操作系统⾥的,这样线程的创建、终止和管理 都是由操作系统负责。
内核线程的模型,也就类似前⾯提到的 ⼀对⼀ 的关系,即⼀个⽤户线程对应⼀个内核线程,如下图所示:
内核线程的 优点
  • 在⼀个进程当中,如果某个内核线程发起系统调⽤⽽被阻塞,并不会影响其他内核线程的运⾏
  • 分配给线程,多线程的进程获得更多的 CPU 运⾏时间;
内核线程的 缺点
  • 在⽀持内核线程的操作系统中,由内核来维护进程和线程的上下⽂信息,如 PCB TCB
  • 线程的创建、终⽌和切换都是通过系统调⽤的⽅式来进⾏,因此对于系统来说,系统开销⽐较⼤;
以上,就是内核线程的优缺点了。
最后的轻量级进程如何理解?
轻量级进程( Light-weight process LWP )是内核支持的用户线程,⼀个进程可有⼀个或多个 LWP ,每 LWP 是跟内核线程⼀对⼀映射的,也就是 LWP 都是由⼀个内核线程⽀持。
另外, LWP 只能由内核管理并像普通进程⼀样被调度, Linux 内核是⽀持 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 模型。开发⼈员可以针对不同的应⽤特点调节内核线程的数⽬来达到物理并⾏性和逻辑并⾏性的最佳⽅案。

调度

进程都希望⾃⼰能够占⽤ CPU 进⾏⼯作,那么这涉及到前⾯说过的进程上下⽂切换。
⼀旦操作系统把进程切换到运⾏状态,也就意味着该进程占⽤着 CPU 在执⾏,但是当操作系统把进程切换
到其他状态时,那就不能在 CPU 中执⾏了,于是操作系统会选择下⼀个要运⾏的进程。
选择⼀个进程运⾏这⼀功能是在操作系统中完成的,通常称为 调度程序 scheduler )。
那到底什么时候调度进程,或以什么原则来调度进程呢?

调度时机

在进程的⽣命周期中,当进程从⼀个运⾏状态到另外⼀状态变化的时候,其实会触发⼀次调度。
⽐如,以下状态的变化都会触发操作系统的调度:
  • 从就绪态 -> 运⾏态:当进程被创建时,会进⼊到就绪队列,操作系统会从就绪队列选择⼀个进程运⾏;
  • 从运⾏态 -> 阻塞态:当进程发⽣ I/O 事件⽽阻塞时,操作系统必须另外⼀个进程运⾏;
  • 从运⾏态 -> 结束态:当进程退出结束后,操作系统得从就绪队列选择另外⼀个进程运⾏;
因为,这些状态变化的时候,操作系统需要考虑是否要让新的进程给 CPU 运⾏,或者是否让当前进程从CPU 上退出来⽽换另⼀个进程运⾏。
另外,如果硬件时钟提供某个频率的周期性中断,那么可以根据如何处理时钟中断
,把调度算法分为两类:
  • ⾮抢占式调度算法挑选⼀个进程,然后让该进程运⾏直到被阻塞,或者直到该进程退出,才会调⽤另外⼀个进程,也就是说不会理时钟中断这个事情。
  • 抢占式调度算法挑选⼀个进程,然后让该进程只运⾏某段时间,如果在该时段结束时,该进程仍然在运⾏时,则会把它挂起,接着调度程序从就绪队列挑选另外⼀个进程。这种抢占式调度处理,需要在时间间隔的末端发⽣时钟中断,以便把 CPU 控制返回给调度程序进⾏调度,也就是常说的时间⽚机

调度原则

原则⼀ :如果运⾏的程序,发⽣了 I/O 事件的请求,那 CPU 使⽤率必然会很低,因为此时进程在阻塞等待硬盘的数据返回。这样的过程,势必会造成 CPU 突然的空闲。所以, 为了提⾼ CPU 利⽤率,在这种发送 I/O 事件致使 CPU 空闲的情况下,调度程序需要从就绪队列中选择⼀个进程来运⾏。
原则⼆ :有的程序执⾏某个任务花费的时间会⽐较⻓,如果这个程序⼀直占⽤着 CPU ,会造成系统吞吐量(CPU 在单位时间内完成的进程数量)的降低。所以, 要提⾼系统的吞吐率,调度程序要权衡⻓任务和短 任务进程的运⾏完成数量。
原则三 :从进程开始到结束的过程中,实际上是包含两个时间,分别是进程运⾏时间和进程等待时间,这两个时间总和就称为周转时间。进程的周转时间越⼩越好, 如果进程的等待时间很⻓⽽运⾏时间很短,那 周转时间就很⻓,这不是我们所期望的,调度程序应该避免这种情况发⽣。
原则四 :处于就绪队列的进程,也不能等太久,当然希望这个等待的时间越短越好,这样可以使得进程更快的在 CPU 中执⾏。所以, 就绪队列中进程的等待时间也是调度程序所需要考虑的原则。
原则五 :对于⿏标、键盘这种交互式⽐较强的应⽤,我们当然希望它的响应时间越快越好,否则就会影响⽤户体验了。所以, 对于交互式⽐较强的应⽤,响应时间也是调度程序需要考虑的原则。
针对上⾯的五种调度原则,总结成如下:
  • CPU 利⽤率:调度程序应确保 CPU 是始终匆忙的状态,这可提⾼ CPU 的利⽤率;
  • 系统吞吐量:吞吐量表示的是单位时间内 CPU 完成进程的数量,⻓作业的进程会占⽤较⻓的 CPU 资源,因此会降低吞吐量,相反,短作业的进程会提升系统吞吐量;
  • 周转时间:周转时间是进程运⾏和阻塞时间总和,⼀个进程的周转时间越⼩越好;
  • 等待时间:这个等待时间不是阻塞状态的时间,⽽是进程处于就绪队列的时间,等待的时间越⻓,⽤户越不满意;
  • 响应时间:⽤户提交请求到系统第⼀次产⽣响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准。
说⽩了,这么多调度原则,⽬的就是要使得进程要「快」。

调度算法

不同的调度算法适⽤的场景也是不同的。
接下来,说说在 单核 CPU 系统 中常⻅的调度算法。

先来先服务调度算法

最简单的⼀个调度算法,就是⾮抢占式的 先来先服务( First Come First Seved, FCFS )算法 了。
顾名思义,先来后到, 每次从就绪队列选择最先进⼊队列的进程,然后⼀直运⾏,直到进程退出或被阻 塞,才会继续从队列中选择第⼀个进程接着运⾏。
这似乎很公平,但是当⼀个⻓作业先运⾏了,那么后⾯的短作业等待的时间就会很⻓,不利于短作业。
FCFS 对⻓作业有利,适⽤于 CPU 繁忙型作业的系统,⽽不适⽤于 I/O 繁忙型作业的系统。

最短作业优先调度算法

最短作业优先( Shortest Job First, SJF )调度算法 同样也是顾名思义,它会 优先选择运⾏时间最短的进 程来运⾏ ,这有助于提⾼系统的吞吐量。
这显然对⻓作业不利,很容易造成⼀种极端现象。
⽐如,⼀个⻓作业在就绪队列等待运⾏,⽽这个就绪队列有⾮常多的短作业,那么就会使得⻓作业不断的往后推,周转时间变⻓,致使⻓作业⻓期不会被运⾏。

高响应比优先调度算法

前⾯的「先来先服务调度算法」和「最短作业优先调度算法」都没有很好的权衡短作业和⻓作业。
那么, 高响应⽐优先 Highest Response Ratio Next, HRRN )调度算法 主要是权衡了短作业和⻓作业。 每次进⾏进程调度时,先计算「响应⽐优先级」,然后把「响应⽐优先级」最⾼的进程投⼊运⾏ ,「响应 ⽐优先级」的计算公式:
从上⾯的公式,可以发现:
  • 如果两个进程的「等待时间」相同时,「要求的服务时间」越短,「响应⽐」就越⾼,这样短作业的进程容易被选中运⾏;
  • 如果两个进程「要求的服务时间」相同时,「等待时间」越⻓,「响应⽐」就越⾼,这就兼顾到了⻓作业进程,因为进程的响应⽐可以随时间等待的增加⽽提⾼,当其等待时间⾜够⻓时,其响应⽐便可以升到很⾼,从⽽获得运⾏的机会;

时间⽚轮转调度算法

最古⽼、最简单、最公平且使⽤最⼴的算法就是 时间片轮转( Round Robin, RR )调度算法
每个进程被分配⼀个时间段,称为时间片( Quantum ),即允许该进程在该时间段中运⾏。
  • 如果时间⽚⽤完,进程还在运⾏,那么将会把此进程从 CPU 释放出来,并把 CPU 分配给另外⼀个进程;
  • 如果该进程在时间⽚结束前阻塞或结束,则 CPU ⽴即进⾏切换;
另外,时间⽚的⻓度就是⼀个很关键的点:
  • 如果时间⽚设得太短会导致过多的进程上下⽂切换,降低了 CPU 效率;
  • 如果设得太⻓⼜可能引起对短作业进程的响应时间变⻓。
⼀般来说,时间⽚设为 20ms~50ms 通常是⼀个⽐较合理的折中值。

最高优先级调度算法

前⾯的「时间⽚轮转算法」做了个假设,即让所有的进程同等重要,也不偏袒谁,⼤家的运⾏时间都⼀样。
但是,对于多⽤户计算机系统就有不同的看法了,它们希望调度是有优先级的,即希望调度程序能 从就绪 队列中选择最⾼优先级的进程进⾏运⾏,这称为最⾼优先级( Highest Priority First HPF )调度算法
进程的优先级可以分为,静态优先级和动态优先级:
  • 静态优先级:创建进程时候,就已经确定了优先级了,然后整个运⾏时间优先级都不会变化;
  • 动态优先级:根据进程的动态变化调整优先级,⽐如如果进程运⾏时间增加,则降低其优先级,如果进程等待时间(就绪队列的等待时间)增加,则升⾼其优先级,也就是随着时间的推移增加等待进程的优先级
该算法也有两种处理优先级⾼的⽅法,⾮抢占式和抢占式:
  • ⾮抢占式:当就绪队列中出现优先级⾼的进程,运⾏完当前进程,再选择优先级⾼的进程。
  • 抢占式:当就绪队列中出现优先级⾼的进程,当前进程挂起,调度优先级⾼的进程运⾏。
但是依然有缺点,可能会导致低优先级的进程永远不会运⾏。

多级反馈队列调度算法

多级反馈队列( Multilevel Feedback Queue )调度算法 是「时间⽚轮转算法」和「最⾼优先级算法」的综合和发展。
顾名思义:
  • 「多级」表示有多个队列,每个队列优先级从⾼到低,同时优先级越高时间片越短
  • 「反馈」表示如果有新的进程加⼊优先级⾼的队列时,⽴刻停⽌当前正在运⾏的进程,转⽽去运⾏优先级⾼的队列;
来看看,它是如何⼯作的:
  • 设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越
  • 新的进程会被放⼊到第⼀级队列的末尾,按先来先服务的原则排队等待被调度,如果在第⼀级队列规定的时间⽚没运⾏完成,则将其转⼊到第⼆级队列的末尾,以此类推,直⾄完成;
  • 当较⾼优先级的队列为空,才调度较低优先级的队列中的进程运⾏。如果进程运⾏时,有新进程进⼊ 较⾼优先级的队列,则停⽌当前运⾏的进程并将其移⼊到原队列末尾,接着让较⾼优先级的进程运⾏;
可以发现,对于短作业可能可以在第⼀级队列很快被处理完。对于⻓作业,如果在第⼀级队列处理不完,可以移⼊下次队列等待被执⾏,虽然等待的时间变⻓了,但是运⾏时间也变更⻓了,所以该算法很好的 顾了⻓短作业,同时有较好的响应时间。
看的迷迷糊糊?那我拿去银⾏办业务的例⼦,把上⾯的调度算法串起来,你还不懂,你锤我!
办理业务的客户相当于进程,银⾏窗⼝⼯作⼈员相当于 CPU
现在,假设这个银⾏只有⼀个窗⼝(单核 CPU ),那么⼯作⼈员⼀次只能处理⼀个业务。
那么最简单的处理⽅式,就是先来的先处理,后⾯来的就乖乖排队,这就是 先来先服务( FCFS )调度算 。但是万⼀先来的这位⽼哥是来贷款的,这⼀谈就好⼏个⼩时,⼀直占⽤着窗⼝,这样后⾯的⼈只能⼲等,或许后⾯的⼈只是想简单的取个钱,⼏分钟就能搞定,却因为前⾯⽼哥办⻓业务⽽要等⼏个⼩时,你说⽓不⽓⼈?
有客户抱怨了,那我们就要改进,我们⼲脆优先给那些⼏分钟就能搞定的⼈办理业务,这就是 短作业优先 SJF )调度算法 。听起来不错,但是依然还是有个极端情况,万⼀办理短业务的⼈⾮常的多,这会导致⻓业务的⼈⼀直得不到服务,万⼀这个⻓业务是个⼤客户,那不就捡了芝麻丢了⻄⽠
那就公平起⻅,现在窗⼝⼯作⼈员规定,每个⼈我只处理 10 分钟。如果 10 分钟之内处理完,就⻢上换下⼀个⼈。如果没处理完,依然换下⼀个⼈,但是客户⾃⼰得记住办理到哪个步骤了。这个也就是 时间⽚轮 转( RR )调度算法 。但是如果时间⽚设置过短,那么就会造成⼤量的上下⽂切换,增⼤了系统开销。如果时间⽚过⻓,相当于退化成 FCFS 算法了。
既然公平也可能存在问题,那银⾏就对客户分等级,分为普通客户、 VIP 客户、 SVIP 客户。只要⾼优先级的客户⼀来,就第⼀时间处理这个客户,这就是 最⾼优先级( HPF )调度算法 。但依然也会有极端的问题,万⼀当天来的全是⾼级客户,那普通客户不是没有被服务的机会,不把普通客户当⼈是吗?那我们把优先级改成动态的,如果客户办理业务时间增加,则降低其优先级,如果客户等待时间增加,则升⾼其优先级。
那有没有兼顾到公平和效率的⽅式呢?这⾥介绍⼀种算法,考虑的还算充分的, 多级反馈队列( MFQ )调 度算法 ,它是时间⽚轮转算法和优先级算法的综合和发展。它的⼯作⽅式:
  • 银⾏设置了多个排队(就绪)队列,每个队列都有不同的优先级,各个队列优先级从⾼到低,同时每个队列执⾏时间⽚的⻓度也不同,优先级越⾼的时间⽚越短
  • 新客户(进程)来了,先进⼊第⼀级队列的末尾,按先来先服务原则排队等待被叫号(运⾏)。如果时间⽚⽤完客户的业务还没办理完成,则让客户进⼊到下⼀级队列的末尾,以此类推,直⾄客户业务办理完成。
  • 当第⼀级队列没⼈排队时,就会叫号⼆级队列的客户。如果客户办理业务过程中,有新的客户加⼊到较⾼优先级的队列,那么此时办理中的客户需要停⽌办理,回到原队列的末尾等待再次叫号,因为要把窗⼝让给刚进⼊较⾼优先级队列的客户。
可以发现,对于要办理短业务的客户来说,可以很快的轮到并解决。对于要办理⻓业务的客户,⼀下⼦解决不了,就可以放到下⼀个队列,虽然等待的时间稍微变⻓了,但是轮到⾃⼰的办理时间也变⻓了,也可以接受,不会造成极端的现象,可以说是综合上⾯⼏种算法的优点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程小猹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值