zoukankan      html  css  js  c++  java
  • 漫谈进程和线程

    为了帮助大家理解什么是进程,以厨师做蛋糕为例。厨师做蛋糕,首先需要厨师(CPU),其次,需要食谱(程序)和原料(输入数据),而用原料做蛋糕的一些列动作的总和就是进程。某天厨师正在后厨做着蛋糕,突来听到儿子哭着跑进后厨,说自己被蜜蜂蛰了 ,厨师放下手中工具,并记录下当前做到哪一步了(保存上下文信息) ,然后拿出急救手册,按其中的说明为儿子进行处理(开始另外一个进程)。

    进程概览

    我们知道文件是对I/O设备的抽象,虚拟存储器是对文件和主存的抽象,指令集是对CPU的抽象,进程是对指令集和虚拟存储器的抽象。如下图所示 。

    image.png

    进程在内存的逻辑布局

    从上可知,进程包括指令集和虚拟存储器。我们着重介绍进程在虚拟存储器中的逻辑布局,它包括用户栈、堆、程序数据和程序代码,其中,用户栈从上往下生长,堆从下往上生长,程序数据和程序代码从可执行文件加载而来,将程序代码改写成汇编指令就是类似于movl、imul、addl等指令。如下图所示

    image.png

    此时,CPU运行到地址为304的指令, 假设CPU时间片刚好用完,就需要进行进程切换,在进行进程切换之前,需要保护现场,即保存寄存器信息、PC、打开的文件, 代码段地址、数据地址、堆栈信息等,这些信息称为进程的上下文。当操作系统切换到进程时,首先将进程2的上下文信息加载到操作系统中,找到PC,然后接着执行就可以了。

    进程控制块(PCB)

    进程的上下文信息是以某个数据结构保存在内存中的,而这种数据结构就是PCB。在Linux操作系统中PCB对应的数据结构就是task_struct,它保存着进程的重要信息。

    struct task_struct{
      pid_t pid://进程号
      long state;//状态
      cputime_t utime,stime;//cpu在用户态和 核心态下执行的时间
      struct files_struct *files;//打开的文件
      struct   mm_struct  *mm;//进程使用的内存
      ...
    }
    
    

    进程的状态

    • 进程的状态包括新建态、就绪态、等待态、运行态、退出态
    • 流程:首先进程被新建,然后进入到就绪状态,此时,进程并没有进入到运行状态,而是等待CPU调度,如果被CPU调度则进入到运行态,而当时间片用完时,进程从运行态返回到就绪态,而当等待I/O操作时,则由运行态进入阻塞态。需要注意的是:只有运行态的进程拥有CPU,而处于就绪态和等待态的进程只是处于内存,等待CPU调度,因此CPU调度是一个很关键的流程。
      image.png

    CPU调度

    像上文描述的那样,CPU调度就是到底哪个进程占有CPU,它可以分为非抢占式和抢占式。非抢占式是指调度程序一旦把CPU分配给某一进程后便让它一直运行下去,直到进程完成或发生某件事件而不能运行时,才将CPU分配给其他进程。它适合批处理系统,简单、系统开销小。抢占式是指当一个进程正在执行时,系统可以基于某种策略剥夺CPU给其他进程。剥夺的原则有优先权原则、端进程优先原则、时间片原则,它适用于交互式系统。

    • 评价标准
    1. 公平:合理的分配CPU
    2. 响应时间:从用户输入到产生反映的时间
    3. 吞吐量:单位时间完成的任务数量
    4. 但是这些目标是矛盾的,例如:我们希望前端进程能够快速得到响应,这样一来后端进程就不能得到快速响应。
    • 批处理系统中的调度
    1. 先来先服务(公平、FIFO队列、非抢占式)
    2. 最短作业优先(系统的平均等待时间最短,但是需要预先知道每个任务的运行时间)
    • 交互式调度策略
    1. 轮转,每个进程分配一个固定的时间片,但是定义时间片长度是个问题,假设进程切换一次的开销为1ms,如果时间片太短,那么很多时间都浪费在切换上,例如时间片为4ms,那么20%的时间浪费在切换上;如果时间片太长,浪费时间就减少了,但是最后一个经常等待的时间就非常久,譬如,时间片100ms,浪费的时间1%,假设有50个进程,最后一个需要等待5s。
    2. 静态优先级,给每个进程赋予优先级,优先级高的先执行,优先级低的后执行,但是该方法存在一定问题:低优先级的进程存在被饿死的情况,例如新来的进程的优先级都比原来的高,怎么办呢?我们根据等待时间的增加而调整优先级大小---多级反馈队列
    3. 动态优先级---多级反馈队列,即进程的优先级会随着等待时间的增长而增长。

    进程间同步

    我们知道,打印机有一个缓存,叫做打印队列,如下图所示,打印队列有5个空格,就是说这个打印队列最多可以容纳5个待打印文件,打印机进程就是消费者,而其他待打印进程是生产者,生产者不断地向队列中放数据,例如:A.java、B.doc等。

    • 临界区:多个进程需要互斥的访问共享资源,共享资源可以是变量、表和文件等,例如打印队列就是共享资源。

    • 当生产者将队列放满时,需要等待消费者;如果消费者把所有文件都打印完了,则需要等待生产者,这就是进程间的同步问题。

    image.png
    • 进程间同步的本质
    1. 进程调度是不可控的
    2. 在机器层面,count++,count--并不是原子操作,即一条代码,对应汇编层面多条指令。两者缺一不可,如果进程调度是可控的,那么,即使count++对应多条指令,当执行完第一条指令时,发生CPU切换,进程调度控制接下来的进程还是原来的进程控制CPU。
    • 解决方案
    1. 关闭中断
      缺点:把中断操作(CPU收到时钟中断以后,会检查当前进程的时间片是否用完,用完则切换)开放给应用程序,这是极其危险的事情,例如:当某个程序关闭中断之后,执行完毕之后,忘记打开中断,导致整个系统都终止了。

    2. 用硬件指令来实现锁

    boolean TestAndSet(boolean *lock){
      boolean rv = *lock;
      *lock = TRUE;
      return rv;
    }
    
    // 使用TestAndSet
    boolean lock = false;
    do{
      while(TestAndSet(&lock)){
        ...//什么也不做
      }
      临界区
      lock = false;
      剩余区
    }while(true);
    
    • 注意:操作系统会锁住系统总线,防止其他CPU访问内存变量
    • 注意TestAndSet函数中的三条指令是原子执行的
    1. 信号量
    • 信号量S是个整形变量,除了初始化外,有两个操作,wait()、signal()。
    • 为了确保信号量操作,需要用一种原子的方式实现它,操作系统来实现原子性。
    wait(S){
      while(S<=0){
      ...//啥也不做
      }
      S--;
    }
    signal(S){
      S++;
    }
    
    //
    semaphore mutext = 1;
    wait(mutex);
    进入临界区
    signal(mutex);
    剩余区
    
    1. 不能忙等问题

    用硬件指令实现锁的方案和信号量方案都有忙等问题,即某个进程获得了CPU时间片,但是啥事干不了,while(S < = 0){...}

    • 新增进程队列,当发现value<0,将当前队列加入到阻塞队列中,同时,阻塞进程,而不像之前的方法那样无限等待下去
    typedef struct{
      int value;
      struct process *list;
    } semaphore;
    
    wait(semaphore *s){
      s -> value--;
      if(s->value<0){
      //把当前进程加入到s->list中
      block();
    }
    signal(semaphore *s){
      s -> value++;
      if(s -> value <=0){
        //从s->list取出一个进程p
        wakeup(p);
      }
    }
    

    线程

    由于进程之间是相互独立的,所以进程间数据共享只能通过内核实现,开销很麻烦,因此我们提出了线程这个概念。线程之间的数据是共享的;一个进程可以只有一个线程,也可以有多个线程(一个进程至少有一个线程);当一个进程有多个线程时,每个线程都有一套独立的寄存器和堆栈信息,而代码、数据和文件是共享的,如下图所示。

    image.png

    线程的实现

    1. 完全在用户层实现(当用户要执行硬件设备,必须从用户空间到内核空间,这是一种保护措施,保护操作系统不被恶意程序所破坏),线程在应用层实现有一个优点就是线程切换不用内核介入,线程切换会非常的快。也就是说线程的调度策略是自己实现的。但是这里也有一个巨大的缺陷:由于内核只知道进程而不知道线程,那么进程1中的任何一个线程被阻塞,导致进程1中的其他线程也被阻塞

    2. 内核实现线程和用户空间一一对应,可以有效的解决方案一中的缺点,但是由于在内核中实现用户空间相同数量的线程数,开销比较大

    3. 用户空间中多个线程映射到内核中的一个线程,这样一来,内核中的线程就不用创建那么多, 而且阻塞的概率也降低了,这是一种平衡和折中的方式。JVM就是实现了这种方式 。JVM本身就是一个进程,JVM可以创建很多线程,然后对应内核中的线程,内核中的线程调度CPU。


    image

    欢迎关注微信公众号:木可大大,所有文章都将同步在公众号上。

  • 相关阅读:
    Photoshop 基础七 位图 矢量图 栅格化
    Photoshop 基础六 图层
    Warfare And Logistics UVALive
    Walk Through the Forest UVA
    Airport Express UVA
    Guess UVALive
    Play on Words UVA
    The Necklace UVA
    Food Delivery ZOJ
    Brackets Sequence POJ
  • 原文地址:https://www.cnblogs.com/neal-ke/p/8698464.html
Copyright © 2011-2022 走看看