图解操作系统之进程、线程生态关系

图解操作系统之进程、线程生态关系

一、进程、时间片(调度)、操作系统概念

  • 我们写好的⼀⾏⾏代码,为了让其⼯作起来,我们还得把它送进城(进程)⾥,那既然进了城⾥,那肯定不能胡作⾮为了。
  • 城⾥⼈有城⾥⼈的规矩,城中有个专⻔管辖你们的城管(操作系统),⼈家让你休息就休息,让你⼯作就⼯作,毕竟摊位不多,每个⼈都要占这个摊位来⼯作,城⾥要⼯作的⼈多着去了。
  • 所以城管为了公平起⻅,它使⽤⼀种策略(调度)⽅式,给每个⼈⼀个固定的⼯作时间(时间⽚),时间到了就会通知你去休息⽽换另外⼀个⼈上场⼯作。另外,在休息时候你也不能偷懒,要记住⼯作到哪了,不然下次到你⼯作了,你忘记⼯作到哪了,那还怎么继续?有的⼈,可能还进⼊了县城(线程)⼯作,这⾥相对轻松⼀些,在休息的时候,要记住的东⻄相对较少,⽽且还能共享城⾥的资源。

在这里插入图片描述

二、发⽣进程上下⽂切换有哪些场景?

为了保证所有进程可以得到公平调度,CPU 时间被划分为⼀段段的时间⽚,这些时间⽚再被轮流分配给各个进程。

就绪

这样,当某个进程的时间⽚耗尽了,进程就从运⾏状态变为就绪状态,系统从就绪队列选择另外⼀个进程运⾏;

运行

进程在系统资源不⾜(⽐如内存不⾜)时,要等到资源满⾜后才可以运⾏,这个时候进程也会被挂起,并由系统调度其他进程运⾏;

挂起

当进程通过睡眠函数 sleep 这样的⽅法将⾃⼰主动挂起时,⾃然也会重新调度;当有优先级更⾼的进程运⾏时,为了保证⾼优先级进程的运⾏,当前进程会被挂起,由⾼优先级进程来运⾏;发⽣硬件中断时,CPU 上的进程会被中断挂起,转⽽执⾏内核中的中断服务程序;以上,就是发⽣进程上下⽂切换的常⻅场景了。
在这里插入图片描述
⼀个完整的进程状态的变迁如下图:
在这里插入图片描述

再来详细说明⼀下进程的状态变迁:

  • **NULL -> 创建状态:**⼀个新进程被创建时的第⼀个状态;
  • 创建状态 -> 就绪状态:当进程被创建完成并初始化后,⼀切就绪准备运⾏时,变为就绪状态,这个过程是很快的;
  • 就绪态 -> 运⾏状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运⾏该进程;
  • 运⾏状态 -> 结束状态:当进程已经运⾏完成或出错时,会被操作系统作结束状态处理;
  • 运⾏状态 -> 就绪状态:处于运⾏状态的进程在运⾏过程中,由于分配给它的运⾏时间⽚⽤完,操作系统会把该进程变为就绪态,接着从就绪态选中另外⼀个进程运⾏;
  • 运⾏状态 -> 阻塞状态:当进程请求某个事件且必须等待时,例如请求 I/O 事件;
  • 阻塞状态 -> 就绪状态:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;

如果有⼤量处于阻塞状态的进程,进程可能会占⽤着物理内存空间,显然不是我们所希望的,毕竟物理内存空间是有限的,被阻塞状态的进程占⽤着物理内存就⼀种浪费物理内存的⾏为。所以,在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存空间换出到硬盘,等需要再次运⾏的时候,再从硬盘换⼊到物理内存。
在这里插入图片描述

三、进程间通信

由于每个进程的⽤户空间都是独⽴的,不能相互访问,这时就需要借助内核空间来实现进程间通信,原因很简单,每个进程都是共享⼀个内核空间。Linux 内核提供了不少进程间通信的⽅式,其中最简单的⽅式就是管道,

管道

分为「匿名管道」和「命名管道」。
匿名管道顾名思义,它没有名字标识,匿名管道是特殊⽂件只存在于内存,没有存在于⽂件系统中,shell 命令中的「|」竖线就是匿名管道,通信的数据是⽆格式的流并且⼤⼩受限,通信的⽅式是单向的,数据只能在⼀个⽅向上流动,如果要双向通信,需要创建两个管道,再来匿名管道是只能⽤于存在⽗⼦关系的进程间通信,匿名管道的⽣命周期随着进程创建⽽建⽴,随着进程终⽌⽽消失。
命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使⽤命名管道的前提,需要在⽂件系统创建⼀个类型为 p 的设备⽂件,那么毫⽆关系的进程就可以通过这个设备⽂件进⾏通信。另外,不管是匿名管道还是命名管道,进程写⼊的数据都是缓存在内核中,另⼀个进程读取数据时候⾃然也是从内核中获取,同时通信数据都遵循先进先出原则,不⽀持 lseek 之类的⽂件定位操作。

消息队列

克服了管道通信的数据是⽆格式的字节流的问题,消息队列实际上是保存在内核的「消息链表」,消息队列的消息体是可以⽤户⾃定义的数据类型,发送数据时,会被分成⼀个⼀个独⽴的消息体,当然接收数据时,也要与发送⽅发送的消息体的数据类型保持⼀致,这样才能保证读取的数据是正确的。消息队列通信的速度不是最及时的,毕竟每次数据的写⼊和读取都需要经过⽤户态与内核态之间的拷⻉过程。共享内存可以解决消息队列通信中⽤户态与内核态之间数据拷⻉过程带来的开销,它直接分配⼀个共享空间,每个进程都可以直接访问,就像访问进程⾃⼰的空间⼀样快捷⽅便,不需要陷⼊内核态或者系统调⽤,⼤⼤提⾼了通信的速度,享有最快的进程间通信⽅式之名。但是便捷⾼效的共享内存通信,带来新的问题,多进程竞争同个共享资源会造成数据的错乱。那么,就需要信号量来保护共享资源,以确保任何时刻只能有⼀个进程访问共享资源,这种⽅式就是互斥访问。

信号量

不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是⼀个计数器,表示的是资源个数,其值可以通过两个原⼦操作来控制,分别是 P 操作和 V 操作。与信号量名字很相似的叫信号,它俩名字虽然相似,但功能⼀点⼉都不⼀样。信号是进程间通信机制中唯⼀的异步通信机制,信号可以在应⽤进程和内核之间直接交互,内核也可以利⽤信号来通知⽤户空间的进程发⽣了哪些系统事件,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令),⼀旦有信号发⽣,进程有三种⽅式响应信号 1. 执⾏默认操作、2. 捕捉信号、3. 忽略信号。有两个信号是应⽤进程⽆法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,这是为了⽅便我们能在任何时候结束或停⽌某个进程。
前⾯说到的通信机制,都是⼯作于同⼀台主机,如果要与不同主机的进程间通信,那么就需要 Socket 通信了

Socket

实际上不仅⽤于不同的主机进程间通信,还可以⽤于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常⻅的通信⽅式,⼀个是基于 TCP 协议的通信⽅式,⼀个是基于 UDP 协议的通信⽅式,⼀个是本地进程间通信⽅式

以上,就是进程间通信的主要机制了。

四、线程通信间

你可能会问了,那线程通信间的⽅式呢?

同个进程下的线程之间都是共享进程的资源,只要是共享变量都可以做到线程间通信,⽐如全局变量,

所以对于线程间关注的不是通信⽅式,⽽是关注多线程竞争共享资源的问题,

信号量也同样可以在线程间实现互斥与同步:

  • 互斥的⽅式,可保证任意时刻只有⼀个线程访问共享资源;
  • 同步的⽅式,可保证线程 A 应在线程 B 之前执⾏;

那到底如何解决呢?需要有⼀种新的实体,满⾜以下特性:

  • 实体之间可以并发运⾏;
  • 实体之间共享相同的地址空间;
  • 这个新的实体,就是线程( Thread ),线程之间可以并发运⾏且共享相同的地址空间。
  • 什么是线程? 线程是进程当中的⼀条执⾏流程。

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

线程的优缺点?

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

线程与进程的⽐较 线程与进程的⽐较如下:

  • 进程是资源(包括内存、打开的⽂件等)分配的单位,线程是 CPU 调度的单位;
  • 进程拥有⼀个完整的资源平台,⽽线程只独享必不可少的资源,如寄存器和栈;
  • 线程同样具有就绪、阻塞、执⾏三种基本状态,同样具有状态之间的转换关系;
  • 线程能减少并发执⾏的时间和空间开销;

对于,线程相⽐进程能减少开销,体现在:线程的创建时间⽐进程快,因为进程在创建的过程中,还需要资源管理信息,⽐如内存管理信息、⽂件管理信息,⽽线程在创建的过程中,不会涉及这些资源管理信息,⽽是共享它们;

  • 线程的终⽌时间⽐进程快,因为线程释放的资源相⽐进程少很多;

同⼀个进程内的线程切换⽐进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同⼀个进程的线程都具有同⼀个⻚表,那么在切换的时候不需要切换⻚表。⽽对于进程之间的切换,切换的时候要把⻚表给切换掉,⽽⻚表的切换过程开销是⽐较⼤的;由于同⼀进程的各线程间共享内存和⽂件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更⾼了;

所以,不管是时间效率,还是空间效率线程⽐进程都要⾼。

线程的上下⽂切换 在前⾯我们知道了,线程与进程最⼤的区别在于:线程是调度的基本单位,⽽进程则是资源拥有的基本单位。

所以,所谓操作系统的任务调度,实际上的调度对象是线程,⽽进程只是给线程提供了虚拟内存、全局变量等资源。

对于线程和进程,我们可以这么理解:当进程只有⼀个线程时,可以认为进程就等于线程;当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下⽂切换时是不需要修改的;

另外,线程也有⾃⼰的私有数据,⽐如栈和寄存器等,这些在上下⽂切换时也是需要保存的。

线程上下⽂切换的是什么?

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

线程的实现 主要有三种线程的实现⽅式:

  • ⽤户线程(User Thread):在⽤户空间实现的线程,不是由内核管理的线程,是由⽤户态的线程库来完成线程的管理;
  • 内核线程(Kernel Thread):在内核中实现的线程,是由内核管理的线程;
  • 轻量级进程(LightWeight Process):在内核中来⽀持⽤户线程;那么,这还需要考虑⼀个问题,⽤户线程和内核线程的对应关系。

⽤户线程和内核线程的对应关系

⾸先,第⼀种关系是多对⼀的关系,也就是多个⽤户线程对应同⼀个内核线程:

在这里插入图片描述
在这里插入图片描述

同步的概念

互斥解决了并发进程/线程对临界区的使⽤问题。这种基于临界区控制的交互作⽤是⽐较简单的,只要⼀个进程/线程进⼊了临界区,其他试图想进⼊临界区的进程/线程都会被阻塞着,直到第⼀个进程/线程离开了临界区。

我们都知道在多线程⾥,每个线程并不⼀定是顺序执⾏的,它们基本是以各⾃独⽴的、不可预知的速度向前推进,但有时候我们⼜希望多个线程能密切合作,以实现⼀个共同的任务。例⼦,线程 1 是负责读⼊数据的,⽽线程 2 是负责处理数据的,这两个线程是相互合作、相互依赖的。线程 2 在没有收到线程 1 的唤醒通知时,就会⼀直阻塞等待,当线程 1 读完数据需要把数据传给线程 2 时,线程 1 会唤醒线程 2,并把数据交给线程 2 处理。

所谓同步,就是并发进程/线程在⼀些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步。举个⽣活的同步例⼦,你肚⼦饿了想要吃饭,你叫妈妈早点做菜,妈妈听到后就开始做菜,但是在妈妈没有做完饭之前,你必须阻塞等待,等妈妈做完饭后,⾃然会通知你,接着你吃饭的事情就可以进⾏了。

注意,同步与互斥是两种不同的概念:
同步就好⽐:「操作 A 应在操作 B 之前执⾏」,「操作 C 必须在操作 A 和操作 B 都完成之后才能执⾏」等;
互斥就好⽐:「操作 A 和操作 B 不能在同⼀时刻执⾏」;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

是刘彦宏吖

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

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

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

打赏作者

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

抵扣说明:

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

余额充值