操作系统之进程

程序和进程

处理器、内存和指令中对处理器做了一些简单的介绍,而CPU要做什么操作,都是由程序中的指令来决定的,那进程又是什么呢,为什么在程序的概念上又出现了进程的概念。想象一个场景,我们在使用Word时,可能同时处理多份文档,但是对于打开的文档来说,我们都是使用的Word这个程序,这个程序只有一个标识,就是它的程序名,因此对于多份打开的Word来说,我们就不能再用Word程序这个去区分他们,这个时候,对于操作系统来说,每一个正在被处理的Word就是一个进程,进程是动态的,而程序是固定的,进程是程序的一次执行过程,而某一次执行的过程可能处理的是不同的数据,因此程序和进程是不一样的概念,而对于操作系统来说,程序就是存在计算机中的一些列指令,而进程是动态的过程,它是有的生命周期

多进程

现在我们都知道,我们的计算机是多进程的,就是同时存在多个进程在执行,而对于CPU来说,某一个时间段(非常小的时间段)它只能执行某个进程,那么计算机是怎么实现多进程的呢?我们知道处理器的速度非常快,因此它通过快速的切换不同的进程执行,而对于计算机的使用者来说,就好像是我们的计算机同时在做很多事情,因为CPU太快了

并行与并发

那么计算机的多进程是并发还是并行执行的呢,要知道是并行和并发,我们还需要知道它们之间的区别,因为并行与并发的概念是不同的

  1. 并发:不同进程交替执行
  2. 并行:不同进程同时执行

明白并行和并发的概念以后,我们就可以区分计算机是并行还是并发的了:对于单CPU来说,是并发,而只有多CPU才能做到并行

为什么要多进程

对于我们的进程来说,它的整个过程并不只是需要让CPU来执行指令,它们还需要进行I/O操作,就是等待用户输入或者输入结果到输出设备中,如果CPU不并发或者并行来处理进程,那么在这时,CPU就空闲下来了,为了不浪费CPU的能力,在空闲的这个时间,我们让其他的进程来执行,这就可以保证我们的CPU一直是在运转的。同时,我们不能让某一进程一直占用着CPU,而是让所有的进程都能公平的使用CPU(当然进程有优先级),而这一切都是由操作系统来控制和调度的,因此操作系统除了要控制进程对CPU的使用,还要让这一切有序、公平的进行。操作系统进行多进程的过程就是进程的上下文切换,上下文切换时,CPU需要将寄存器中存放的上一个进程的数据变为新进程的数据,而将上一个进程的数据保存到该进程的PCB中。上下文切换是额外的开销,对于程序的执行是无意义的,操作系统设计时,这种切换的时间和开销越少越好,但是多进程的意义更大,所以这种开销不能避免

进程控制块(PCB)

在操作系统中,我们用进程控制块(PCB)来标识一个进程,它是一个数据结构,在这个数据结构中,有进程的状态标识、进程号(不能重复)、指令计数器(下一条执行的指令)、CPU调度信息等等。由于每一个进程对应一个PCB,因此操作系统必须控制它的大小,所以我们只将进程独占的信息才放进PCB数据结构中,为了更快的访问PCB,因此将PCB常驻内存

进程的状态

为了更好的调度进程,操作系统给了进程一个状态的标识,比如一个进程正在等待输入时,它处于等待状态,操作系统不会将CPU给它使用(给它它也不会用)。除了等待状态,进程还有其他一些状态,这些状态都是为了操作系统更好的调度进程,但是不同操作系统对进程状态的定义和给进程的状态数是不同的,不同的操作系统对进程状态的定义称为操作系统的进程状态字,比如Linux操作系统进程的状态有:

  • R (task_running) : 可执行状态
  • S (task_interruptible): 可中断的睡眠状态
  • D (task_uninterruptible): 不可中断的睡眠状态
  • T (task_stopped or task_traced):暂停状态或跟踪状态
  • Z (task_dead - exit_zombie):退出状态,进程成为僵尸进程
  • X (task_dead - exit_dead):退出状态,进程即将被销毁

进程调度队列

操作系统将不同状态的进程放入不同的队列中,比如在可执行进程的队列里会有多个处于可执行状态的进程,可执行进程的队列只有一个(可能也有多个,下面会解释),而处于等待状态进程的队列可能会有多个,因为等待状态的队列等待的资源不同,有等待I/O设备的、等待子进程执行的等待,将等待同一资源的进程放入同一队列中,因此操作系统对进程的调度,就是处理进程在不同队列之间的迁移以及用一定的策略从队列中取出进程去使用CPU

进程操作

进程的创建

计算机启动以后,需要创建不同的进程,比如操作系统本身就是由很多进程构成的。计算机启动后,首先创建0号进程,然后通过0号进程创建它的子进程,再由子进程创建它更多的子进程,在创建时,对资源和运行方式都有两种方式,对于资源来说

  1. 子进程复制共享父进程资源(Duplicate而不是Copy)
  2. 全新装入一个程序,不共享父进程的资源

运行方式也有两种:

  1. 和父进程并发运行
  2. 父进程在子进程执行时等待,子进程执行完毕以后父进程才执行

对于Linux操作系统,子进程对资源的创建有实现两种:fork()exec(),分别对应资源创建方式的1和2,而且Linux中1、2方式可以同时使用,先共享,再重新载入。而子进程的运行方式只有一种:子进程和父进程并发执行。

进程的终止

进程终止的方式有两种:

  1. 进程完成最后一条指令后终止
  2. 父进程终止子进程的执行

而父进程终止后,对子进程的处理也有两种方式:

  1. 终止所有子进程
  2. 不终止子进程

Linux中由于子进程和父进程是并发的,因此父进程终止后不会去终止子进程,但是我们可以通过一些方式去实现这样的需求,就是父进程终止后终止它的子进程,能这么做的原因是因为进程间存在关系

进程间的关系

进程之间是相互独立的,但不是完全的独立,进程间会有一些合作,因为一些必要的合作意义很大,进程间要合作,就必须要进行通信(IPC),例如我们的的Linux操作系统提供的进程间通信的方式有:

  • 管道(Pipe)及有名管道(Named Pipe):生产者与消费者
  • 信号(Signal):生成和捕获
  • 报文(Message)队列(消息队列):比较管道,它的容量更乐观
  • 共享内存
  • 信号量(Semaphore):信号的个数是固定的,信号量是变量
  • 套接口(Socket)

CPU调度器

CPU调度器需要在就绪队列中去选出一个PCB,让它来使用CPU,然后调度器需要让CPU的各项指标尽量高,这些指标包括:

  • CPU利用率:CPU忙的时间和总使用时间的比例,越高越好
  • 吞吐率:单位时间完成执行的进程数,越高越好
  • 周转时间:执行某一进程所耗用的CPU累积实际,越短越好
  • 等待时间:某一进程在就绪队里面的累积时间,越短越好
  • 响应时间:某一进程从发起调度,到其得到CPU调度响应其间所经历的时间,越短越好

CPU调度的时机分为:非抢占式调度抢占式调度,不同操作系统CPU调度器调度的时机有很多,也有各种不同情况的调度时机,比如根据优先级、进程时间片时间到了和进程执行结束等等,如果是自愿交出CPU就是非抢占式调度,如果是被动交出CPU则为抢占式调度,所有的调度时机都可以分到非抢占式调度和抢占式调度中

CPU分配器

CPU调度器完成调度以后,CPU分配器还要负责:

  • 上下文切换
  • 从内核态转移到用户态
  • 跳转至用户程序的计数器位置执行

有的操作系统CPU调度器和CPU分配器由同一个模块完成,比如Linux

调度算法

先来先服务(FCFS)

实现非常方便,用队列实现,每次直接从队列中取一个PCB来执行,但是这种情况的如果执行时间比较长的进程在队列的前面,就会造成后面进程等待时间非常的长

最短作业优先(SJF)

这种算法的前提是进入队列的进程要告诉CPU调度器它的执行时间,然后CPU调度器根据执行时间来调度,这种情况分为非抢占式调度和抢占式调度:

在进程执行完成以后再进行调度,每一次调度,选择最短的来执行,因此和进程进入就绪的时间没有关系

这种情况的调度时间是根据新进入队列的进程时间是否更更短来决定,如果较短则抢占执行。P1到P4的周转时间分别为16、5、1、6,等待时间就为周转时间(注意进入的时间点)减去执行时间,这种方式计算更直观。但是最短作业优先的调度算法是不能实现的,因为进程的执行时间是无法准确预估的,为什么呢,比如我们等待用户输入的时间无法控制

最先响应比优先算法

HRN = (W + T) / T

W表示等待时间,T代表预估CPU时间,原理就是如果你的执行时间越长,你的等待时间就应该长一点,如果短就等待短一点

优先权法

每一个进程都有一个优先数,然后CPU调度器选择优先权最高的进程执行,SJF也属于优先权的一种,它的优先指标为执行时间。但是单纯的优先权法可能会导致进程饥饿,就是优先权低的进程可能永远都无法得到CPU,解决这个问题的办法就是将进程在就绪队列的等待时间折叠到优先权中,因此,随着等待时间的增加,它的优先权就会相应的增加。优先权法也可以分为抢占式优先权法和非抢占式优先权法,比如一个优先权更高的进程进入就绪队列以后抢占当前进程的CPU执行,就属于抢占式,而如果等待当前进程执行完毕以后再进行调度,则是非抢占式优先权法

轮转法(RR)

用时间片辅助,每完成一个时间片的时间就去进行CPU的调度,每一个进程时间片用完就交出CPU,还有一种情况就是在时间片内进程执行完毕,也会交出CPU,轮转法大多数情况是抢占式调度。轮转法的上下文切换开销很大,时间片的长度选择会决定上下文切换的次数

多层队列

将进程队列拆分成多个队列,例如将前台进程放入前台队列中,批处理进程放入后台进程队列中,然后不同的队列采用不同调度算法。这个算法还可以进一步改进为多层反馈队列,在多层队列的前提下,让进程可以在不同的队列之间迁移。多层队列算法要决定队列个数、每个队列的算法、决定进程进入哪一个队列等设计

实时调度

调度及时,这种算法主要用于实时性要求很高的系统,比如火箭的发射系统,一般通用操作系统不可能完成实时调度

  • 硬实时系统:某个时间必须完成
  • 软实时系统:不能保证在某个时间完成,但是尽量在该时间帮你完成

调度算法的指标

  1. 实现并观察(成本高)
  2. 确定模型法 不够准确
  3. 排队模型法 模型选择很重要
  4. 仿真 实际的数据放进仿真的程序

Linux调度算法基于优先权和轮转法,可以是优先权、轮转法或者优先权和轮转法相结合

线程

如果没有线程,我们的每一个进程都是按照一定的顺序和逻辑在执行,不同的进程它们所使用的资源也不一样,例如内存空间,如果要用做另外一件事,就必须再创建一个进程,就造成了资源的浪费,于是有了多线程来解决这个问题,多线程共享同一份代码和内存空间等资源,但是却可以做不同的任务,当然各个线程之间也有自己独立的资源,在一个进程中,至少有一个线程

用户级线程

线程的创建、管理、资源申请和通信都是由用户态完成的,而不是操作系统的内核态完成的,不会有内核的开销

内核级线程

线程的管理是由操作系统的内核完成的,因此操作系统是同时支持进程和线程

操作系统多线程模型

早期的操作系统不支持内核级线程,多线程由用户态程序自己实现,随着操作系统的演变,不同的操作系统对多线程支持的模型就不一样:

  • 一对一:一个用户级线程对应一个内核级线程(一个PCB对于一个线程)
  • 多对一:多个用户级线程对应一个进程,由这个进程来给各个线程分配
  • 多对多:同时可以一对一也可以多对一,内核比较复杂的操作系统
  • Two-level:和多对多差不多,突出一个内核级的情况
坚持原创分享,您的支持将鼓励我不断前行!