从进程和线程的角度看操作系统

进程和线程是对正在运行程序的一个抽象,对进程和线程有一个非常好的认识对于操作系统的学习有重要的作用

  • 进程
  • 进程模型
  在进程模型中,计算机上所有可以运行的软件,也包括操作系统,被组织成若干个顺序进程,简称进程(process)。一个进程就是一个正在执行程序的实例,包括程序计数器、寄存器和变量的当前值。每一个进程都有自己虚拟的CPU,其实就是CPU在不同的进程中来回切换,实现多进程。
  • 进程创建
  1. 系统初始化
  2. 一个进程发出系统调用,创建一个新进程协作其工作
  3. 用户请求创建一个新的进程
  4. 一个批处理昨夜的初始化
  • 进程的终止
  1. 正常退出(自愿的)
  2. 出错退出(自愿的)
  3. 严重错误(非自愿)
  4. 被其它进程杀死(非自愿)
  • 进程的状态
从进程和线程的角度看操作系统

进程有三种状态:运行、阻塞、就绪

  • 进程的实现
进程的实现是通过一张由操作系统维护的进程表,每个进程占用一个进程表项,该项目中包含进程状态的重要信息(程序计数器,堆栈指针),根据这些信息进行调度。通过中断向量来实现不同进程间的切换。当该进程结束后,操作系统会显示一个提示符并等待新的命令

  • 线程
  • 线程定义
  线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。
  • 多线程
  多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,
也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了CPU的利用率。 
  • 多线程缺点
  1.     线程也是程序,所以线程需要占用内存,线程越多占用内存也越多
  2.    多线程需要协调和管理,所以需要CPU时间跟踪线程
  3.    线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题
  4.    线程太多会导致控制太复杂,容易出现错误
  • 线程与进程
  1.     线程开销小
  2.    线程资源共享性好
  3.    线程共享资源需要耗费一定的锁资源,同步相对复杂
  4.    一个线程崩溃可能导致整个进程崩溃,这个当然是自己的应用程序有问题
  • 进程间通信
        不同的进程和线程都存在公共资源和空间的问题,那么为了避免两个进程对资源同时占有的问题提出了互斥和信号量两个概念。其中互斥是指这个空间或者资源只允许一方使用,当有人使用时其他人不能使用;而信号量则是允许几个人同时使用,但有一定的使用方限制
从进程和线程的角度看操作系统
当A处于临界区时,B将不能进入而产生阻塞直到A离开临界区
  • 调度
  • 调度介绍
  当多个进程或线程,如果有两个或者多个进程处于就绪的状态时,他们同时要使用CPU,这时就需要调度算法来实现哪一个先被CPU使用
  • 进程分类
从进程和线程的角度看操作系统

(a)CPU密集型进程:CPU计算时间长,等待I/O输入时间短
(b)I/O密集型进程:等待I/O输入时间长,CPU计算时间短
  • 主要调度算法(转)

1.先来先服务(FCFS)调度算法

  FCFS是一种最简单的调度算法,该调度算法既可以用于作业调度也可以用于进程调度。在作业调度中,算法每次从后备作业队列中选择最先进入该队列的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列
  下面通过一个实例来说明FCFS调度算法的性能。假设系统中有4个作业,它们的提交时间分别是8、8.4、8.8、9,运行时间依次是2、1、0.5、0.2,系统釆用FCFS调度算法,这组作业的平均等待时间、平均周转时间和平均带权周转时间

作业号 提交时间 运行时间 开始时间 等待时间 完成时间 周转时间 带权周转时间
1 8 2 8 0 10 2 1
2 8.4 1 10 1.6 11 2.6 2.6
3 8.8 0.5 11 2.2 11.5 2.7 5.4
4 9 0.2 11.5 2.5 11.7 2.7 13.5

平均等待时间 t = (0+1.6+2.2+2.5)/4=1.575
平均周转时间 T = (2+2.6+2.7+2.7)/4=2.5
平均带权周转时间 W = (1+2.6+5.牡13.5)/4=5.625
  FCFS调度算法的特点是算法简单,但效率低;对长作业比较有利,但对短作业不利(相对SJF和高响应比);有利于CPU繁忙型作业,而不利于I/O繁忙型作业。

2.短作业优先(SJF)调度算法

短作业(进程)优先调度算法是指对短作业(进程)优先调度的算法。短作业优先(SJF)调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法,则是从就绪队列中选择一个估计运行时间最短的进程,将处理机分配给它,使之立即执行,直到完成或发生某事件而阻塞时,才释放处理机。
例如,考虑表2-3中给出的一组作业,若系统釆用短作业优先调度算法,其平均等待时间、平均周转时间和平均带权周转时间

作业号 提交时间 运行时间 开始时间 等待时间 完成时间 周转时间 带权周转时间
1 8 2 8 0 10 2 1
2 8,4 1 10.7 2.3 11.7 3.3 3.3
3 8.8 0.5 10.2 1.4 10.7 1.9 3.8
4 9 0.2 10 1 10.2 1.2 6

平均等待时间 t = (0+2.3+1.4+1)/4=1.175
平均周转时间 T = (2+3.3+1.9+1.2)/4=2.1
平均带权周转时间 W = (1+3.3+3.8+6)/4=3.525

SJF调度算法的平均等待时间、平均周转时间最少。

3.时间片轮转算法

时间片轮转算法(Round Robin,RR)的基本思路是:把系统当中的所有就绪任务按照先来先服务的原则,排成一个队列,然后再每次调度的时候,把处理器分派给队列当中的第一个任务,让它去执行一小段CPU时间(即时间片,time slice)。当这个时间片结束时,如果任务还没有执行完成的话,将会发生时钟中断,在时钟中断里面,调度器将会暂停当前任务的执行,并把它送到就绪队列的末尾,然后执行当前的队首任务。反之,如果一个任务在它的时间片用完之前就已经结束了或者阻塞了,那么它就会立即让出CPU给其他任务。

从进程和线程的角度看操作系统 
图3-25所示是时间片轮转法的示意图。图a表示初始状态,总共有四个任务位于就绪队列中,先后顺序是B、C、D、A,其中任务B位于队列之首。当CPU空闲时,调度器就会选择B去执行。假设当B运行完一个时间片后,即没有结束也没有被阻塞,这时操作系统就会通过时钟中断来中止它的运行,并把它送到就绪队列的末尾。于是就成了图b的状态,此时,C位于队列之首,C就被调度执行。当C时间片用完后也会送到队列末尾,以此内推。当某个任务运行结束了,就会退出就绪队列,或者某任务被阻塞也也会退出,并加入阻塞队列中去。

  • 优点:公平性,各个就绪任务能得到相同的时间片;活动性,每个就绪任务能一直保持活动。

  • 缺点:时间片的大小q要适当选取,如果选择不当,将会影响到系统的性能和效率。如果q太大,每个任务都在一个时间片内完成,这就退化为先来先服务算法了。如果q太小,每个任务需要更多的时间片才能运行结束,这就使任务之间的切换次数增加,从而增大了系统的管理开销,降低了CPU的使用效率。一般来说,q值选取在20ms~50ms比较合适。

4.优先级算法

优先级调度算法(priority)的基本思路是:给每个任务都设置一个优先级,然后在任务调度的时候,在所有处于就绪状态的任务中选择优先级最高的任务去运行。上文提到的短作业优先算法其实也是一种优先级算法,每个任务的优先级就是它的运行时间,运行时间越短,优先级越高。

优先级算法可以分为可抢占方式不可抢占方式。区别在于,一个任务正在运行时,如果来了一个优先级更高的任务,可抢占方式则会立即抢占CPU去运行高优先级的任务,不可抢占方式则会等当前任务运行结束后再执行。

在任务优先级的确定方式上,可以分为静态方式和动态方式。

  • 静态优先级方式:在创建任务的时候就确定任务的优先级,并且一直保持不变直到任务结束。
  • 动态优先级方式:任务的优先级在任务运行过程中可以动态改变。

在优先级算法中,高优先级的任务将抢占低优先级的任务,但如果两个任务的优先级相同,又该如何处理呢?通常的做法是把任务按照不同的优先级进行分组,然后在不同组之间使用优先级算法,在同一组的各个任务之间使用时间片轮转法。(如图) 
从进程和线程的角度看操作系统


补:多线程的三种创建方式
  1. 建线程类继承Thread类创建线程类

    (1)定义Thread类的子类,并重写类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run方法称为执行体

    (2)创建Thread子类的实例,即创建了线程对象

    (3)调用线程对象的start()方法来启动线程


  2. 通过Runnable接口创建线程类

    (1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

    (2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

    (3)调用线程对象的start()方法来启动该线程。


  3. 通过Callable和Future创建线程

    (1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

    (2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

    (3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

    (4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值