现代操作系统(一):进程与线程

文章目录

  • 一、基础知识
    • 1. 用户态和内核态
    • 2. 系统调用
    • 3. 中断
  • 二、进程
    • 1. 进程模型
    • 2. 进程的创建
    • 3. 进程的终止
    • 4. 进程的层次结构
    • 5. 进程的状态
    • 6. 进程的实现
    • 7. 进程的上下文切换
    • 8. 各种进程
  • 三、线程
    • 1. 线程的使用
    • 2. 线程模型
    • 1. POSIX线程
    • 2. 实现线程
  • 四、进程间通信
    • 1. 竞争条件
    • 2. 临界区
    • 3. 忙等待的互斥
    • 4. 睡眠与唤醒
    • 5. 信号量
    • 6. 互斥量
    • 7. 管程
    • 8. 消息队列
    • 9. 屏障
    • 10. 管道
      • 10.1 匿名管道
      • 10.2 FIFO
    • 11. 共享内存
    • 12. 信号
    • 13. socket
  • 五、调度
    • 1. 调度简介
    • 2. 批处理系统中的调度
    • 3. 交互式系统中的调度
    • 4. 实时系统中的调度

一、基础知识

1. 用户态和内核态

多数操作系统有两种运行模式:用户态和内核态,区分内核态和用户态主要是为了限制不同程序的访问权限,保护系统程序,避免用户程序直接访问系统资源。

内核态:

  • 这个状态下,操作系统具有对所有硬件的完全访问权,可以执行及其能够运行的任何指令
  • 表示一个应用进程执行系统调用后,或 I/O 中断、时钟中断后,进程便处于核心态执行。
  • 运行在内核态的程序可以访问的资源多,但可靠性、安全性要求高,维护管理都较复杂

用户态:

  • 用户态只使用了机器指令中的一个子集,那些会影响机器的控制或可进行IO操作的指令,在用户态中的程序都是禁止的
  • 用户态程序访问的资源受限,但可靠性、安全性要求低,自然编写维护起来都较简单

用户态切换到内核态的唯一途径——>中断/异常/陷入
内核态切换到用户态的途径——>设置程序状态字

2. 系统调用

  1. 指令流执行到系统调用的函数,系统调用函数会通过 int 0x80 指令进入系统调用入口程序,int 0x80 是一条硬件中断指令,意味着程序从用户态切换到内核态。与此同时,会将调用的一些参数信息传递到寄存器,然后通过硬件保存用户态的执行现场。将用户态的堆栈转化为内核态堆栈。
  2. 之后就进入内核进行操作,执行完成之后返回结果,会检查调用的进程是否处于就绪态,时间片是否用完,是否需奥发送信号等等。再进行一个恢复现场的操作。最后通过 Iret 指令返回用户态。一次系统调用就结束了。

3. 中断

CPU 执行完一条指令之后,控制单元会检查执行上一条指令的过程中是否出现过中断。如果有,就会执行如下处理中断的流程:

  • 确定中断的向量,也就是中断的类型,例如是系统调用还是缺页异常等。
  • 寻找对应中断向量的处理程序。
  • 通过硬件保存现场后,执行中断处理程序。
  • 处理程序执行完成后,将控制权还给控制单元。
  • 控制单元对现场进行恢复,继续执行源程序。

二、进程

1. 进程模型

在进程模型中,计算机上所有可运行的软件,通常也包括操作系统,被组织成若干顺序进程,简称进程,一个进程就是一个正在执行程序的实例,包括程序计数器、寄存器和变量的当前值。是资源分配的基本单位

2. 进程的创建

四种主要事件会导致进程的创建:

  • ① 系统初始化

    启动系统时,通常会创建若干个进程,一些是前台进程,和用户交互并且完成工作,一些是后台进程,与用户没有直接关系。负责例如接受电子邮件等

  • ② 正在运行的程序执行了创建进程的系统调用

    新的进程也可以日后创建,一个正在运行的进程经常发出系统调用,可以创建一个或多个新进程协助工作,例如UNIX的fork函数

  • ③ 用户请求创建一个新进程

    交互式系统中,键入一个命令或者双击一个图标就可以启动一个程序,也就是创建一个新的进程

  • ④ 一个批处理作业的初始化

    这种情况发生在大型机的批处理系统中应用,在操作系统认为有资源可以运行下一个任务时,它创建一个进程运行下一个作业

3. 进程的终止

进程的终止也通常因为以下四个原因引起

  • ① 正常退出(自愿的)

    由于一个进程完成了它的工作而终止,例如点击右上角告诉操作系统,它的工作已经完成,然后UNIX系统中调用exit退出进程

  • ② 出错退出(自愿的)

    进程发现了错误,但是没有立即退出,给出错误参数之后,面向交互式系统通常并不退出,而是有一个弹窗指示下一步操作,用户可以点击退出

  • ③ 严重错误(非自愿)

    这通常是程序错误,如空指针,除数是零等

  • ④ 被其他进程杀死(非自愿)

    一个进程执行一个系统调用杀死某个其他线程,例如UNIX系统中使用kill命令

4. 进程的层次结构

某些系统中,当进程创建了另一个进程之后,父进程和子进程就以某种形式继续保持关联

在UNIX系统中,进程和他的所有的子进程以及后裔共同组成一个进程组

Windows中没有进程层次的概念,所有进程都是地位相等的,但父进程创建子进程后会得到一个令牌(称为句柄),该句柄可以用来控制子进程,但父进程有权把这个令牌传递给其他进程,这样,就不存在层次概念了。

5. 进程的状态

进程的三种状态:

  • 运行态:该时刻进程实际占用CPU
  • 就绪态:可运行,但因为其他进程正在运行而暂时停止
  • 阻塞态:除非某种外部事件发生,否则进程不能运行,例如等待输入

现代操作系统(一):进程与线程_第1张图片

6. 进程的实现

为了实现进程模型,操作系统维护着一张表格,(一个结构数组),即进程表,每个进程占用一个进程表项,即进程控制块(process control block,PCB)该表项包含了进程状态的重要信息,如下图

现代操作系统(一):进程与线程_第2张图片
在了解进程表之后,就可以对在单个(或每一个)CPU上如何维持多个顺序进程的错觉做阐述

每个进程运行过程中会被中断,之后CPU又重新分配到该进程,该进程又会返回到中断发生之前完全相同的状态

7. 进程的上下文切换

进程是由内核管理和调度的,所以进程的切换只能发⽣在内核态。

进程的上下⽂切换不仅包含了虚拟内存、栈、全局变量等⽤户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。而线程共享进程的虚拟内存以及全局变量等资源,所以只需要对寄存器和一些私有数据进行切换。

通常,会把交换的信息保存在进程的 PCB,当要运⾏另外⼀个进程的时候,我们需要从这个
进程的 PCB 取出上下⽂,然后恢复到 CPU 中,这使得这个进程可以继续执⾏。

8. 各种进程

僵尸进程

概念:一个进程使用 fork 创建子进程,如果子进程退出父进程并没有调用 wait() (父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息)或者 waitpid() 获取子进程信息,那么子进程的描述符仍然保存在系统中。这种进程就被称为僵尸进程 -------- 即 Z 进程

危害:占用资源不放,正常进程可能无法进行创建

解决方法:我们要解决的话就只能找到那个产生大量僵死进程的父进程,只有杀死掉那个父进程 (通过 kill 发送 SIGTERM 或 SIGKILL) 杀死掉那个父进程之后,那些僵死进程就成了孤儿进程,孤儿进程会被 init 进程接管,init 会 wait 掉这些孤儿进程并且释放它们在系统中占用的资源这些僵死的孤儿进程就会死去。

孤儿进程

概念: 如果父进程退出而它的一个或多个子进程还在运行,那么这些子进程就被称为孤儿进程孤儿进程最终将被 init 进程 (进程号为 1 的 init进程) 所收养并由 init 进程完成对它们的状态收集工作。

孤儿进程是没有危害的,孤儿进程是没有父进程的子进程,当孤儿进程没有父进程时,内核就会init设置为孤儿进程的父进程,init进程就会调用wait去释放那些已经退出的子进程,当孤儿进程完成其声明周期之后,init会释放掉其状态信息。

后台进程

后台进程的文件描述符也是继承于父进程,例如shell,所以并没有脱离控制台,终端关闭之后后台进程也会关闭。

守护进程

守护进程变成自己的进程组长,脱离终端独立运行。

三、线程

线程就是运行在进程上下文中的逻辑流,一个进程至少有一个线程,也可以由多个线程,它们共享进程的资源,是操作系统独立调度的基本单位。

每个线程都包含有表示执行环境所必须的信息,包括进程标识线程的线程 ID,一组寄存器的值,栈,调度优先级和策略,信号屏蔽字,errno 变量等。

1. 线程的使用

需要多线程的原因:

  • 线程拥有共享同一个地址空间和所有可用数据的能力,在许多应用中同时发生着多种活动,某些活动随着时间的推移会被阻塞,通过将这些应用分解成可用准并行运行的多个顺序线程,程序设计模型就会变得简单
  • 线程比进程更轻量级,它们更容易创建也更容易撤销
  • 对于大量的计算以及大量的IO处理,拥有多个线程允许这些活动彼此重叠进行,从而加快应用程序执行的速度

2. 线程模型

1. POSIX线程

为实现可移植的线程程序,IEEE指定了线程的标准。它定义的线程包叫做Pthread,大部分UNIX系统都支持该标准。Posix线程就是在C程序中处理线程的一个标准接口

所有Pthread线程都有某些特性。每一个都含有一个标识符、一组寄存器(包括程序计数器)和一组存储在结构中的属性。这些属性包括堆栈大小、调度参数以及使用线程需要的其他项目。

一些pthread函数调用如下

现代操作系统(一):进程与线程_第3张图片

2. 实现线程

用户空间中实现线程

优点:

  • 用户级线程包可以在不支持线程的操作系统上实现
  • 在用户空间管理线程,每个进程有其专用的线程表,记录线程的一些信息,所以保存该线程状态以及调度程序都是本地过程,所以效率更高,另外,不需要陷入内核,不需要上下文切换,也不需要对内存高速缓存进行刷新,这就使得线程调度非常快捷
  • 允许每个进程有自己定制的调度算法

缺点:

  • 实现系统阻塞调用十分困难
  • 如果一个线程开始允许,那么该进程中其他线程就无法运行,除非第一个线程放弃CPU,所以无法采用轮转调度方式调度CPU

在内核中实现线程

优点:

  • 此时不需要运行时系统,内核中有用来记录系统中所有线程的线程表。当一个线程阻塞时,内核根据其选择,可以运行同一个进程中的另一个线程或者运行另一个进程中的线程。
  • 内核线程不需要任何新的、非阻塞系统调用。

缺点:

  • 在内核中创建或销毁线程的代价比较大,某些线程会采用环保的方式,回收线程,设置为不可运行,从而避免销毁线程,当需要一个线程的时候重新使用该线程
  • 一个多线程进程创建新的进程的时候,到底创建多少个线程
  • 信号由哪个线程处理问题

混合实现

使用内核线程,然后将用户级线程与某些或者全部内核线程多路复用起来。

内核只识别内核线程,并对其进行调度。一些内核线程会被多个用户级线程多路复用。在这种模型中,每个内核级线程有一个可以轮流使用的用户级线程集合

四、进程间通信

进程间通信(Inter Process Communication,IPC)主要有以下三个问题

  • 一个进程如何把信息传递给另一个
  • 确保两个或更多的进程在关键活动中不会出现交叉
  • 保证进程以正确的顺序执行

同样这三个问题也适用于线程

1. 竞争条件

在一些操作系统中,协作的进程可能共享一些彼此都能读写的公用存储区。

两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,称为竞争条件(race condition)。

2. 临界区

以某种手段确保当一个进程在使用一个共享变量或文件时,其他进程不能做同样的操作,称为互斥(mutual exclusion)。,是避免竞争条件的重要方式

我们把对共享内存进行访问的程序片段称作临界区(critical section)。

对于一个并发好的解决方案,需要满足以下四个条件

  • 任何两个进程不能同时处于临界区。
  • 不应对CPU的速度和数量做任何假设。
  • 临界区外运行的程序不得阻塞其他进程。
  • 不得使进程无限期等待进入临界区。

3. 忙等待的互斥

这节讨论几种互斥的实现方案

① 屏蔽中断

每个进程在刚刚进入临界区后立即屏蔽所用中断,并在就要离开之前再打开中断。

适用于单核操作系统,对操作系统本身而言很有用,单对于用户进程则不是一种合适的互斥机制。

② 锁变量

共享锁变量,初始为0。当进程想进入临界区时,首先测试这把锁。
如果锁为0,则进程将所设置为1并进入临界区。并且当退出的时候将值设置为0
如果锁为1,则进程将等待其值变为0。

这种方式同样存在竞争条件。因为对于值的查看和设置过程不是原子操作,这就相当于不是原子性的CAS

③ 严格轮换法

有一个变量记录轮到哪一个进程进入临界区,然后不同的进程在等待循环中不听的测试该变量。知道测试变量变成某个值为止,进入临界区,这成为忙等待

这种方式浪费CPU时间,通常应该避免。只有在有理由认为等待时间是非常短的情形下,才使用忙等待。用于忙等待的锁,称为自旋锁(spin lock)。

示例:

/**
 * @Description 测试忙等待
 * @Date 2021/7/14 16:25
 * @author: A.iguodala
 */
public class SpinLock {

    static volatile int flag = 0;
    public static void main(String[] args) {

        new Thread(() -> {
            while (true) {
                while (flag != 0){}
                /*         临界区 begin         */
                System.out.println("① 线程 1 进入临界区");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("① 线程 1 退出临界区");
                flag = 1;
                /*          临界区 end         */
            }
        }).start();

        new Thread(() -> {
            while (true) {
                while (flag != 1){}
                /*         临界区 begin         */
                System.out.println("② 线程 2 进入临界区");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("② 线程 2 退出临界区");
                flag = 0;
                /*          临界区 end         */
            }
        }).start();
    }
}

④ Peterson 解法

该解法也通过自旋的方式获取锁,但是解决了上面的一个问题,就是上面如果一个进程很慢,一直不执行,那么第一个进程执行完操作之后会一直自旋等待第二个线程改变锁变量,而自己无法运行

/**
 * @Description 测试Peterson解法
 * @Date 2021/7/14 16:38
 * @author: A.iguodala
 */
public class Peterson {

    /**
     * 标志位
     */
    static volatile int flag = -1;

    /**
     * 表示想进入临界区,假设就两个线程
     */
    static boolean[] interested = new boolean[2];

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            int j = i;
            new Thread(() -> {
                while (true) {
                    enter(j);
                    System.out.println(Thread.currentThread().getName() + ":我来啦");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":溜溜球");
                    exit(j);
                }
            },"线程:" + i).start();
        }
    }

    static void enter (int processId) {
        int other = 1 - processId;// 另一个进程的进程Id
        interested[processId] = true; // 表示该进程想进入临界区
        flag = processId;
        while (flag == processId && interested[other]){}
    }

    static void exit (int processId) {
        interested[processId] = false;
    }
}

⑤ TSL指令

需要硬件支持的一种方案。

某些计算机中,特别是那些设计为多处理器的计算机。都有指令TSL RX, LOCK。称为测试并加锁(Test and Set Lock),他将一个内存字lock读到寄存器RX中,然后再该内存地址上存一个非零值。读字和写字操作保证是不可分割的。即该指令结束之前其他处理器均不允许访问该内存字。执行TSL指令的CPU将内存总线锁住,禁止其他CPU在本指令结束之前访问内存

一个可替代TSL的指令时XCHG,它原子性的交换两个位置的内容。

4. 睡眠与唤醒

Peterson解法和TSL或XCHG解法都有忙等待的缺点。
这种方法不仅浪费了CPU时间,而且还可能引起预想不到的结果。

所以出现几条进程间通信原语,它们在无法进入临界区的时候阻塞,而不是忙等待(自旋消耗CPU)
最简单的就是sleep和wakeup

  • sleep是一个将引起调用进程阻塞的系统调用,即被挂起
  • wakeup调用有一个参数,即要被唤醒的进程

5. 信号量

信号量(semaphore)是Dijkstra在1965年提出的一种方法,使用一个整型变量来累计唤醒次数。

存在两种操作 P 和 V (也叫down和up),这两个都是原子操作

  • P (down):这个操作会把信号量减去 1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
  • V(up):这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运⾏;相加后如果信号量 > 0,则表明当前没有阻塞中的进程。

该方法可以用来实现同步和互斥。

6. 互斥量

如果不需要信号量的计数能力,有时可以使用信号量的一个简化版本,称为互斥量(mutex)。

互斥量是一个可以处于两态之一的变量:解锁和加锁。通过可用的TSL或XCHG指令,很容易在用户控件实现。

快速用户区互斥量futex

如果等待时间长,自旋锁很消耗CPU,所以引入futex,当锁没有被占用则直接获取锁,占用的话加入等待队列,等释放锁之后内核唤醒等待队列中的一个或多个线程

7. 管程

一个管程(monitor)是一个由过程、变量及数据结构等组成的一个集合,它们组成一个特殊的模块或软件包。

一个进程调用管程过程时,如果管程有其他活跃进程,则被挂起,直到另一个进程离开管程把它唤醒,或者没有其他活跃进程直接进入,通常用一个互斥量或者二元信号量实现

为了解决进程在无法继续运行的时候阻塞,引入条件变量和 wait signal 两个操作

  • 执行wait方法,会阻塞调用该方法的进程并且另一个等在管程之外的进程调入管程。
  • 执行signal操作该进程立刻退出管程,并且系统调度程序在管程之外的进程中选择一个恢复运行

8. 消息队列

消息传递使用两条原语send 和 receive ,它们像信号量而不像管程,是系统调用而不是语言成分。

消息队列解决了命名管道读取和写入的效率问题,例如,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。同理,B 进程要给 A 进程发送消息也是如此。

消息队列是保存在内核中的消息链表,在发送数据时,会分成⼀个⼀个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。

缺点:

  • 通信不及时。
  • 不适合大数据的传输,在内核中每个消息体都有一个最大长度的限制,且总长度也有限制。
  • 通信过程中,应用进程将消息写入内核,需要从用户缓冲拷贝到内核缓冲,另一个应用进程要读取信息,也需要从内核缓冲拷贝到用户缓冲。

9. 屏障

屏障是用于进程组而不是用于双进程的生产者-消费者情形的。

就是将某些应用中划分了若干阶段,到所有进程都执行到该屏障才继续执行,否则任何进程都不能进入下一个阶段。

可以通过在每个阶段的结尾安置屏障(barrier)来实现。

如Java中的CyclicBarrier

10. 管道

管道是UNIX系统IPC 的最古老形式,所有UNIX系统都提供此种通信机制。

10.1 匿名管道

ps auxf | grep java

对于该命令来说,| 就代表一个管道,将前一段命令(ps auxf)的输出,作为后一段命令(grep java)的输入,同时,这种管道是没有名字的,称为匿名管道。

int pipe(int fd[2])
  • 匿名管道通过该方式创建:
    • 通过传入的参数fd[2],返回两个描述符:
      • ⼀个是管道的读取端描述符 fd[0] ;
      • 另⼀个是管道的写入端描述符 fd[1];
    • 匿名管道是特殊的文件,只存在于内存,不存于文件系统中。

现代操作系统(一):进程与线程_第4张图片

所谓的管道,就是内核里面的⼀串缓存。从管道的⼀段写入的数据,实际上是缓存在内核中的,另⼀端读取,也就是从内核中读取这段数据。另外,管道传输的数据是无格式的流且大小受限。

单进程的管道几乎没有任何作用,通常,进程会先调用pipe,接着调用 fork 从而创建从父进程到子进程的IPC通道。创建的子进程会复制父进程的文件描述符。从而就拥有两组对同一个管道的读端和写端。

现代操作系统(一):进程与线程_第5张图片

由于管道是半全双工只能一端读取,另一端写入,上图这种形式很容易造成混乱,所以通常,父进程关闭fd[0] 读端,子进程关闭 fd[1] 写端,如果要进行双向通信,则需要建立两个管道。

现代操作系统(一):进程与线程_第6张图片

对于开头的 Shell 程序来说,ps auxf | grep java ,ps 进程和 grep 进程都是 shell 的子进程,通过fork子进程再关闭一些读写端之后,再进行通信。

对于匿名管道,他只能在两个具有相同祖先关系的进程间通信。因为匿名管道没有实体,没有管道文件,只能通过 fork 来复制父进程 fd 文件描述符,以此来进行通信。

10.2 FIFO

FIFO有时被称为命名管道。通过命名管道,不相关的进程也能进行通信。

现代操作系统(一):进程与线程_第7张图片

  • 使用命名管道,先通过 mkfifo 创建一个管道。
  • 接下来往管道中写入数据,之后会发现命令行被阻塞住了,因为管道的内容只有被读取了,才会正常退出。

在这里插入图片描述

在这里插入图片描述

  • 之后打开另一个窗口,对内容进行读取,则输入端也正常退出。

该管道通信方式效率低,不适合进程间频繁地交换数据,但是,通过创建一个管道类型的文件,进程都可以使用这个文件,从而对于不相关的进程之间也能相互通信。

11. 共享内存

共享存储允许两个或多个进程共享一个给定的存储区。可以通过多个进程将同一个文件映射到他们的地址空间中来实现。因为数据不需要在客户端和服务端之间相互复制,所以这是最快的一种进程间通信方式。

内核会为每个共享存储段维护一个结构,如下:

现代操作系统(一):进程与线程_第8张图片

  • 首先通常调用 shmget 来获取一个共享存储段的标识符(是获取一块新的地址,还是一块现存的地址)。
  • 然后使用 shmctl 可以对共享存储段做一些操作,包括锁定解锁,删除共享存储段等。
  • 共享存储段被成功创建之后,进程就可以调用 shmat 将共享存储段连接到自己的地址空间。
  • 最后,如果对于该存储的操作结束,则使用 shmdt 与该段分离。

shmget 和 mmap 对于共享内存的区别

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间。

二者本质上是类似的,mmap可以看到文件的实体,而 shmget 对应的文件在交换分区上的 shm 文件系统内,无法直接 cat 查看。

这篇博客有总结区别

12. 信号

对于异常情况下的工作模式,就需要⽤「信号」的方式来通知进程。信号是软件中断。很多比较重要的应用程序都需处理信号。信号提供了一种处理异步事件的方法。每个信号都有一个名字。这些名字都以3个字符SIG开头。

信号是进程间通信机制中唯⼀的异步通信机制,因为可以在任何时候发送信号给某⼀进程,⼀旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。

  • 执行默认操作。Linux 对每种信号都规定了默认操作,例如, SIGTERM 信号,就是终止进程的意思。
  • .捕捉信号。我们可以为信号定义⼀个信号处理函数。当信号发⽣时,我们就执行相应的信号处理函数。
  • 忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应⽤进程⽆法捕捉和忽略的,即 SIGKILL 和 SEGSTOP ,它们用于在任何时候中断或结束某⼀进程。

13. socket

Socket 只要是针对于不同主机间的通信。

五、调度

多道程序设计系统中,多个线程或进程同时竞争CPU,当只有一个CPU可用时,那么就必须选择下一个要运行的进程。

完成选择工作的这一部分称为调度程序(scheduler),该程序使用的算法称为调度算法(scheduling algoritgm)。

1. 调度简介

① 进程行为

几乎所有进程的(磁盘)I/O请求或计算都是交替突发的。CPU不停顿地运行一段时间,然后发出一个系统调用以便读写文件。在完成系统调用之后,CPU又开始计算,直到它需要读取更多的数据或写更多的数据为止

  • IO密集型:花绝大多数时间在等待IO
  • CPU密集型:花费绝大多数时间在计算上

② 何时调度

  • 创建一个新进程之后,决定运行父进程还是子进程
  • 一个进程退出时做出决策
  • 当一个进程阻塞在IO和信号量上或由于其他原因阻塞时
  • 在一个IO发生中断时

根据如何处理时钟中断,可以把调度算法分为两类

  1. 非抢占式调度算法:挑选一个进程运行直至阻塞(阻塞在IO或者等待另一个线程),或者直到自己主动释放CPU
  2. 抢占式调度算法:进程运行某个时段固定大的值,如果结束该时段,仍在运行就会被挂起

③ 调度算法分类

不同的环境需要不同的调度算法

分为三种环境

  • 批处理:非抢占式或者长时间周期的抢占式算法,减少进程切换
  • 交互式:抢占式:避免一个进程霸占CPU拒绝其他服务
  • 实时:抢占有时是不需要的,通常很快完成各自的工作并阻塞

④ 调度算法的目标

  • 所有系统
    • 公平——给每个进程公平的CPU份额
    • 策略强制执行——看到锁宣布的策略执行
    • 平衡——保持系统的所有部分都忙碌
  • 批处理系统
    • 吞吐量——每小时最大作业数
    • 周转时间——从提交到终止间的最小时间
    • CPU利用率——保持CPU时钟忙碌
  • 交互式系统
    • 响应时间——快速响应请求
    • 均衡性——满足用户的期望
  • 实时系统
    • 满足截止时间——避免丢失数据
    • 可预测性——在多媒体系统中避免品质降低

2. 批处理系统中的调度

  • 先来先服务(first-come first-served)
    • 最简单的非抢占式调度算法,每次从就绪队列选择最先进⼊队列的进程,然后⼀直运⾏,直到进程退出或被阻塞,才会继续从队列中选择第⼀个进程接着运⾏
    • 对⻓作业有利,短作业体验差,适⽤于 CPU 繁忙型作业的系统,⽽不适⽤于 I/O 繁忙型作业的系统
  • 最短作业优先(shorts job first)
    • 优先选择运⾏时间最短的进程来运⾏,这有助于提⾼系统的吞吐量
    • 对长作业不利,可能导致饥饿(长作业一直不被调用)
  • 最短剩余时间优先(shorts remaining time next)
    • 选择剩余时间最短的进程运行,和上一个大体类似

3. 交互式系统中的调度

  • 轮转调度(round robin)

    • 每个进程被分配⼀个时间段,称为时间⽚(Quantum),即允许该进程在该时间段中运⾏
    • 如果时间⽚⽤完,进程还在运⾏,那么将会把此进程从 CPU 释放出来,并把 CPU 分配另外⼀个进程;如果该进程在时间⽚结束前阻塞或结束,则 CPU ⽴即进⾏切换
  • 最⾼优先级调度(highest priority first)

    • 从就绪队列中选择最⾼优先级的进程进⾏运⾏
    • 进程的优先级可以分为,静态优先级或动态优先级
      • 静态优先级:创建进程时候,就已经确定了优先级了,然后整个运⾏时间优先级都不会变化;
      • 动态优先级:根据进程的动态变化调整优先级,⽐如如果进程运⾏时间增加,则降低其优先级,如果进程等待时间(就绪队列的等待时间)增加,则升⾼其优先级,也就是随着时间的推移增加等待进程的优先级
    • 该算法也有两种处理优先级⾼的⽅法,⾮抢占式和抢占式
      • ⾮抢占式:当就绪队列中出现优先级⾼的进程,运⾏完当前进程,再选择优先级⾼的进程
      • 抢占式:当就绪队列中出现优先级⾼的进程,当前进程挂起,调度优先级⾼的进程运⾏
    • 可能会造成饥饿
  • 多级反馈队列(multilevel feedback queue)

    • 时间⽚轮转算法 和 最⾼优先级算法 的综合和发展
    • 「多级」表示有多个队列,每个队列优先级从⾼到低,同时优先级越⾼时间⽚越短。
    • 「反馈」表示如果有新的进程加⼊优先级⾼的队列时,⽴刻停⽌当前正在运⾏的进程,转⽽去运⾏优先级⾼的队列;
    • 工作流程
      • 多个队列优先级从高到低,时间片长度从短到长
      • 新加入的任务放在第一级队列,根据先来先服务等待调用,在时间片内没有运行完,则加入下一级队列
      • 上一级队列都运行结束,则运行下一级队列,如果运行时第一级队列加入新任务则停⽌当前运⾏的进程并将其移⼊到原队列末尾,接着让较⾼优先级的进程运⾏
    • 兼顾了长作业和短作业,同时有较好的响应时间
  • 最短进程优先:根据进程过去的行为进行推测,估计运行时间最短的那一个

  • 保证调度:像用户做出明确保证然后实现,例如有n个用户登录,那每个人都获得1/n的CPU时间

  • 彩票调度:进程抽取彩票调度

  • 公平分享调度:每个用户分一部分CPU时间,不关注进程

4. 实时系统中的调度

实时系统的调度算法可以是静态的或动态的。前者在系统开始运行之前做出调度决策;后者在运行过程中进程调度决策。

补充一个本书没有提到的:

⾼响应⽐优先调度算法(Highest Response Ratio Next, HRRN)

⾼响应⽐优先调度算法主要是权衡了短作业和⻓作业。每次进⾏进程调度时,先计算「响应⽐优先级」,然后把「响应⽐优先级」最⾼的进程投⼊运⾏,通过如下公式计算优先级

现代操作系统(一):进程与线程_第9张图片

你可能感兴趣的:(操作系统,java,c语言,操作系统)