进程与线程(OS)详解

发布于:2023-01-26 ⋅ 阅读:(11) ⋅ 点赞:(0) ⋅ 评论:(0)

大家好 我是积极向上的湘锅锅💪💪💪


进程

1、 基础知识

进程是什么?
我们所编译的代码可执行文件只是储存在硬盘的静态文件,运行的时候加载到内存,CPU执行内存中指令,这个运行的程序叫做 进程

比如有个.c的文件在硬盘,那我想将它跑起来,那就需要编译之后变成二进制可执行文件,编译好之后需要一个运行环境,那就是需要加载到内存,最后需要CPU去执行这个文件,最终这个运行中的可执行文件就称为进程

总结:进程是对运行时程序的封装,操作系统进行资源调度和分配的基本单位

并发与并行

这个也是老生常谈的话题了

  • 并发:对于单核CPU而言,在短时间内执行多个进程叫做并发
  • 并行:对于多核CPU而言,同时执行多个进程叫做并行

而对于并发来说,CPU需要从一个进程切换到另一个进行,这个过程需要保持过程的状态信息


2、进程的状态

某个进程在某个时刻所处的状态除了创建和结束还有以下三个状态:

  • 就绪态:暂时停止运行,等待CPU的调度
  • 运行态:该时刻进行占用CPU
  • 阻塞态:该进程在等待某一事件发生而暂停运行,比如IO

而对于阻塞状态,我们知道阻塞态的进程还是占据着物理内存,而在虚拟内存管理的操作系统中,不会让你站着茅坑不拉屎,所以会将阻塞态的进程的物理内存空间换出到硬盘,等需要再次运行的时候,再从硬盘换入到物理内存,这个状态叫做 挂起态

而我们知道就绪态也是在占用着物理内存,所以挂起态也分为以下俩种:
阻塞挂起状态: 进程在硬盘,等待某一事件发生

就绪挂起状态:进程也在硬盘,但只要进入内存,马上运行

在这里插入图片描述
注意:

  • 只有就绪态和运行态可以切换,其他都是单向转换,就绪态的进程通过调度算法从而获得CPU时间,转为运行状态
  • 运行态缺少需要的资源会转换为阻塞状态,而这个资源不包括CPU时间,缺少CPU时间会从运行态转换为就绪态

3、CPU的上下文切换

在了解进程的上下文切换的之前,知晓CPU的上下文切换是十分有必要的

什么是CPU的上下文?

① CPU 寄存器,是 CPU 内置的容量小、但速度极快的内存。
② 程序计数器,则是用来存储 CPU 正在执行的指令位置以及即将执行的下一条指令位置。

CPU 寄存器和程序计数器都是 CPU 在运行任何任务时必须的依赖环境,因此也被叫做 CPU 上下文

什么会有CPU的上下文切换?
我们知道进程都是放在内存中,由CPU去运行,但是CPU不会一次性会把任务执行完,它会记录任务目前的状态,然后去加载其他任务,回过头来继续执行这个任务

那什么是CPU的上下文切换?
所以CPU的上下文切换就是把前一个任务的 CPU 上下文保存起来(PCB),然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务
而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行


4、进程的CPU上下文切换

首先进程的CPU上下文切换要分为俩种

  • 进程内的CPU上下文切换,比如系统调用
  • 不同进程的上下文切换

1. 系统调用:(发生在同一进程当中)
如果应用程序需要使用到内核空间的资源,则需要通过系统调用来完成,比如读取磁盘的数据,所以需要从用户空间切换到内核空间,总共分为四步:

  1. 保存CPU寄存器里原来用户态的指令位置
  2. CPU寄存器需要更新为内核态指令的新位置
  3. 跳转到内核态运行内核任务
  4. 当系统调用结束后,CPU寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程

需要注意的是,系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程,所以叫做进程内的CPU上下文切换

总体来说参考IO模式,里面也涉及了很多的CPU的上下文切换

2. 不同进程的上下文切换(发生在不同进程当中)
首先,进程都是由内核来管理和调度的,进程的切换只能发生在内核态

所以相比于系统调用,进程的上下文切换需要先把虚拟内存,栈,全局变量等用户空间的资源保存下来,还包括了内核堆栈,寄存器等内核空间的资源,所以复杂很多


5、 进程的CPU上下文切换常见场景

  • 时间片耗尽
  • 主动挂起:sleep
  • 被挂起:
    1.更高优先级的进程加入
    2.资源耗尽(内存),需要页面置换
    3.中断发生,CPU执行中断程序

6、 拓展:进程切换为何比线程慢?

首先每一个进程都有自己独享的虚拟空间,也有自己的页表,而线程是共享进程所在的虚拟地址空间的,所以线程切换的时候不涉及虚拟地址空间的转换
当进程进行切换的时候,也要跟着切换页表,在TLB里面的记录就失效了,导致命中率降低,那就会导致虚拟地址到物理地址的过程中,还得去该进程的虚拟地址空间重新查找,经过多级页表找到该物理地址,那就要比在CPU内部的高速缓冲存储器Cache里面的TLB要慢的多

TLB:使用Cache来缓存常用的地址映射,可以加速页表查找,这个Cache就是TLB(快表)


7、进程间的通信方式

这篇挺不错的 进程间通信IPC

  1. 管道
    管道又分俩种,一种是匿名管道,一种是命名管道FIFO

    • 匿名管道
      只能存在于父进程和子进程之间,因为管道没有实体,也没有管道文件 ,原理是通过fork一个子进程来复制fd文件描述符,来达到通信的目的。
      而且管道是单向的,如果需要双向通信,则需要创建俩个管道。
      管道的实质是一个内核缓冲区大小是有限的,进程以先进先出的方式从缓冲区存取数据,管道一端的进程顺序的将数据写入缓冲区,另一端的进程则顺序的读出数据,
      管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等

    • 命名管道FIFO
      即使是不相关的俩个进程也可以通信,是因为提前创建了一个类型有名管道的文件形式存在于文件系统中,因为是以磁盘文件的方式存在,所以双方交互就可以抽象地想象为文件的读写,一个进程写入内容,另一个进程就可以读出

  2. 信号
    信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件

  3. 消息队列
    消息队列是存放在内核中的消息链表,所以只有内核重启,(即操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除
    和管道一样,都是遵循的先进先出的原则
    消息队列允许一个或多个进程向它写入与读取消息
    消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。
    总体来说消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点

  4. 共享内存
    这块应该就比较熟悉,在java关键字volatile有提及过
    使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式

  5. 信号量
    信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件

  6. 套接字(Sockets)
    此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程,参考IO模式


8、进程调度算法

  1. 批处理系统中的调度
  • 先来先服务:
    非抢占式的调度算法,按照请求的顺序进行调度
    有利于长作业,但是不利于短作业,因为短作业必须等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长
  • 最短作业优先:
    非抢占式的调度算法,按估计运行时间最短的顺序进行调度。
    长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。
  • 最短剩余时间优先:
    最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。
    当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。
  1. 交互式系统中的调度
  • 时间片轮转调度:
    将所有就绪进程按FCFS的原则排成一个队列,每次调度时,把CPU时间分配给队首进程该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把CPU时间分配给队首的进程。
  • 优先级调度:
    为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级
  • 多级队列:
    一个进程需要执行100个时间片,如果采用时间片轮转调度算法,那么需要交换100次。
    多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如1,2.4,8…。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换7次。每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。

线程

1、特点

线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程,也有PCB


2、程序计数器为什么是私有的?

程序计数器主要有下面两个作用:

字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。

所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。


3、虚拟机栈和本地方法栈为什么是私有的?

虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。


4、线程间的同步的方式

  • 互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
  • 信号量(Semaphore) :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
  • 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。

5、线程上下文切换:

线程与进程最大的区别在于,进程是资源分配的基本单位,而线程是调度的基本单位,内核中的任务调度实际上是线程;

所以我们所说的进程的上下文切换,实际上是线程的上下文切换,因为同一个进程中的所有线程共享进程的虚拟内存、全局变量等资源。

在处理多线程并发任务时,处理器会给每个线程分配CPU时间片,线程在各自分配的时间片内占用处理器并执行任务,当线程的时间片用完了,或者自身原因被迫暂停运行的时候,就会有另外一个线程来占用这个处理器,这种一个线程让出处理器使用权,另外一个线程获取处理器使用权的过程就叫做上下文切换。

这么一来,线程的上下文切换就可以分为两种情况:

① 前后两个线程属于同一个进程。此时,因为共享虚拟内存,所以在切换时,虚拟内存、全局变量这些资源就保持不动,只需要切换线程的私有数据,比如栈和寄存器等不共享的数据。
② 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟前文提到的进程的上下文切换一样,不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态的修改

所以虽然同为线程的上下文切换,但同进程内的线程切换,要比多进程间的切换消耗更少的资源,而这正是多线程代替多进程的一个优势。但在并发编程中,并不是线程越多就效率越高,线程数太少可能导致资源不能充分利用,线程数太多可能导致竞争资源激烈,导致上下文切换频繁造成系统的额外开销,因为上下文的保存和恢复过程是有成本的,需要内核在 CPU 上完成,每次切换都需要几十纳秒到数微秒的 CPU 时间,在进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,具体使用情况还是要看业务需求而定

参考:
JavaGuide
JUC多线程:系统调用、进程、线程的上下文切换