现代操作系统 进程与线程 学习笔记

  • 进程: 一个进程就是一个正在执行程序的实例。
  • 在每个程序运行时,它的逻辑程序计数器被装入实际的程序计数器中。当该程序执行结束(或暂停执行时),物理程序计数器被保存在内存中该进程的逻辑计数器中。(也就是记住现在执行到哪了。)
  • 简单来说,多进程的切换就是,单个进程执行程序的某一段之后,主动或被动地暂停,将目前的执行状态保存在逻辑计数器当中,以供下一次执行的时候运行。
  • 进程的特点:一个进程是某种类型的活动,它有程序、输入、输出以及状态。
  • 创建进程的四种事件:
    – 系统初始化
    – 执行了正在运行的进程所调用的进程创建系统调用。(一个进程通过系统调用创建了一个新的进程)
    – 用户请求了一个新的进程
    – 一个批处理作业的初始化
  • 守护进程:停留在后台处理诸如电子邮件、web页面、新闻、打印之类活动的进程称为守护进程。
  • 进程的终止事件
    – 正常退出(自愿的)
    – 出错退出(自愿的),比如说,在终端输入一条错误、无法执行的指令。
    – 严重错误(非自愿的),比如说,代码里面发现异常,但是又没有处理。如,10/0
    – 被其他进程杀死(非自愿的)
  • 进程的层级关系:一个进程创建了另一个进程,就是父进程与子进程的关系。这些关系组成了一个树的结构。(UNIX中)
  • 进程的三种状态
    – 运行态:该时刻进程实际占用CPU
    – 就绪态:可运行,但是其他进程占用了CPU,现在这个进程只能暂停运行
    – 阻塞态:除非某种外部事件发生,否则进程不能运行(比如说,正在等待输入)
  • 进程状态间的转换
    在这里插入图片描述
    – 运行态->阻塞态(1):操作系统发现进程不能继续执行的时候,发生转换。在某些操作系统中,进程可以执行一个pause的系统调用来实现进入拥塞态。
    – 运行态->就绪态(2):系统认为某个进程占用处理器时间过长,会进入转换。进程进入就绪态。排队等待。
    – 就绪态->运行态(3):重新轮到该进程运行,就会发生转换。
    – 阻塞态->就绪态(4):当有外部事件发生,可以重新执行了,就进入就绪态排队等待。
    – 转换2、3都是由进程调度程序执行。
  • 进程的实现
    – 进程表:操作系统维护着一张进程表,里面每一个进程占用一个进程表项。
    – 进程表项:包含了进程状态的一些重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时必须保存的信息。

  • 线程:存在同一个地址空间准并行运行多个控制线程的情形。
  • 三个不同的进程不能在同一个文件上进行操作,但是线程可以。
  • 多线程中,包含分派线程和工作线程。当分派线程获取请求之后,分派线程挑选空闲的工作进程,将工作进程从阻塞态转成就绪态。
  • 进程用某种方法把相关的资源集合在一起。进程有存放程序正文、数据以及其他资源的地址空间。
  • 进程用于把资源集合到一起,而线程则是在CPU上被调度执行的实体。

  • 竞争条件:两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,称为竞争条件。
  • 临界区域/临界区:对共享内存进行访问的程序片段称作临界区域或临界区。如果适当安排,是的两个进程不可能同时处于临界区中,就能够避免竞争关系。
  • 避免竞争条件的四个条件
    – 任何两个进程不能同时处于其临界区
    – 不应对CPU的速度和数量做任何假设
    – 临界区外运行的进程不得阻塞其他进程
    – 不得使进程无限期等待进入临界区
  • 实现互斥的几种方案:
  1. 屏蔽中断:当进程进入临界区的时候,立即屏蔽所有中断,并在离开的时候再打开中断。(CPU只有发生时钟中断或其他中断时才会进行进程切换),存在缺点如下:
    1.将屏蔽中断权限给到用户进程,如果用户进程不再打开中断权限,系统就会崩溃。
    2.只对单个CPU有效,当系统是多处理器,就没用了。

  2. 锁变量:通过共享锁变量,初始值为0。当进程要进入临界区的时候,首先检查这把锁是否为0。如果为0,则进入临界区并把锁置为1;如果锁为1,则等待直到值为0。
    但是,存在一个致命的缺点:锁本身也会有竞争条件。当一个进程刚好读到锁为0,进入了临界区,但还没将锁置为1之前,切换到了另一个进程,该进程也读到了锁为0,也进入了临界区。这样就会出现问题了!

  3. 严格轮换法:通过一个变量turn记录轮到哪一个进程进入临界区了,并检查或更新共享内容。当进程0检查到turn为0时,进入临界区;进程1检查到turn为0时,就处于忙等待的状态,一直查询直到turn为1。
    存在的问题是:如果一个进程比另一个进程慢很多,轮流进入临界区就会发生问题。进程0有可能因为进程1在做其他事情而被禁止进入临界区。这违反了避免竞争条件的第三个条件:进程被一个临界区以外的进程阻塞了。

    // 进程0
    while(TRUE){
          
          
    	while(turn!=0);
    	critical_region();  // 临界区
    	turn=1;
    	noncritical_region();  // 非临界区
    }
    
    // 进程1
    while(TRUE){
          
          
    	while(turn!=1);
    	critical_region();  // 临界区
    	turn=0;
    	noncritical_region();  // 非临界区
    }
    
  4. Peterson解法(皮特森算法)
    基于以上的解法,Peterson解法对以上解法进行了优化,里面加了一个interested标志位。当且仅当自己想进且别人已经比自己早一步表示感兴趣的时候,才等待。
    比如:一开始没有任何进程处于临界区中。现在进程0调用enter_region,通过设置数组元素以及turn置为0表示自己希望进入临界区。由于进程0并不想进入临界区,所以enter_region很快便返回。如果现在进程1要进入,进程1就要等待interested[0]为FALSE的时候,才能进入。

    #define FALSE 0
    #define TRUE 1
    #define N 2
    
    int turn; // 表示现在轮到谁了
    int interested[N]; // 表示谁想进去
    
    void enter_region(int process){
          
            // process为进程号,该函数表示进入区域
    	int other;
    	other = 1 - process; // 另一方进程的进程号
    	interested[process] = TRUE; // 表示自己想进去
    	turn=process;  // 表示自己准备进去啦!但这个不表示真正的能够进入临界区
    	while(turn==process && interested[other] == TRUE);  // 当且仅当自己要进去了,而且别人提前表示自己想进去,才在原地等待。
    }
    
    void leave_region(int process){
          
            //离开
    	interested[process] = FALSE;
    }
    
  5. TSL指令:它将一个内存字lock读到寄存器RX中,然后在该内存地址上存一个非零值。读字和写字操作保证是不可分割的,即该指令结束之前其他处理器均不允许访问该内存字。执行TSL指令的CPU将锁住那内存总线,以禁止其他CPU在本指令结束之前访问内存。

  6. Peterson和TSL解法都是正确的,但是都有忙等待的缺点。

  • 睡眠与唤醒。sleep引起调用进程阻塞的系统调用,即挂起,直到另一个进程唤醒。wakeup调用有一个参数,即要被唤醒的进程。
  • 生产者与消费者问题:当缓存区已满的时候,此时生产者还想往里面放数据,解决办法就是让生产者睡眠,消费者消费了一个或多个产品之后,才唤醒他;消费者要拿数据,发现缓存区为空的时候,就进入睡眠,生产者生产了一个或多个产品之后,才唤醒他 。
    #defien N 100;  // 缓存区的数量
    int count = 0;
    
    void producer(void){
          
          
    	int item;
    	while(TRUE){
          
          
    		item = produce_item(); // 生产下一个新的数据量
    		if(count==N) sleep();  // 如果缓存区处于满状态,那就进入sleep状态
    		insert_item(item);  // 把数据放进缓存区
    		count++;
    		if(count == 1) wakeup(consumer);  // 如果count==1,consumer很有可能目前处于休眠状态,那就赶紧将它唤醒
    	}
    }
    
    void consumer(void){
          
          
    	if(count == 0) sleep(); //如果没有库存了,那就进入休眠状态
    	item = remove_item(); // 从缓存区取出一个数据项
    	count--;
    	if(count == N - 1) wakeup(producer);  // 如果处于N-1,很有可能producer处于休眠状态,赶紧唤醒。
    	consumer_item(item);  // 打印数据项		
    }
    
    存在问题:当刚好count刚好为0时,消费者读到count为0之后,就切换到了生产者进程。生产者生产了一个之后,就发出wakeup指令。此时消费者还没睡,到消费者进程的时候,由于之前读到了count=0,所以进入睡眠。后面生产者不知道消费者还在睡眠,然后自己一直在生产,直到自己也睡眠了。
  • 信号量(semaphore):用一个整数常量来累计唤醒次数,以供后面使用。
    – 信号量取值为0(表示没有保存下来的唤醒操作)或正值(有一个或多个唤醒操作)
    – 对信号量执行down操作的时候,如果该值大于0,则将其值减1并继续;如果该值为0,则进程进入睡眠,而且此时down操作并未结束。
    – 信号量具有原子性,检查数值、修改变量值以及可能发生的睡眠操作均是一个单一的不可分割的原子操作。一旦信号量操作开始,则在该操作完成或阻塞之前,其他进程均不允许访问该信号量。
    – up操作对信号量加1。当存在一个或多个down操作未完成,系统则任意选择一个down操作,对其进行结束。此时,信号量还是1,但是少了一个睡眠的进程。
  • 基于信号量的生产者与消费者问题
    #define N 100
    typedef int semaphore;  // 信号量是一种特殊的整型数据
    semaphore mutex = 1;  // 控制临界区的访问
    semaphore empty = N; // 计数缓冲区的空槽数目
    semaphore full = 0; // 计数缓冲区的满槽数目
    
    void producer(void){
          
          
    	int item;
    	while(TRUE){
          
          
    		item = produce_item(); // 产生放在缓冲区的数据
    		down(&empty);  // 将空槽的数量减一。如果已经是0了,那就进入睡眠状态
    		down(&mutex); // 进入临界区。这段时间内,其他进程无法进入。变成0了,如果还有其他进程要进入,那就被睡眠了。
    		insert_item(item);  // 将数据放入缓冲区
    		up(&mutex);  // 离开临界区	
    		up(&full);  // 满槽数目加1,如果刚好有消费者正在休眠,则将其唤醒。
    	}
    }
    
    void consumer(void){
          
          
    	int item;
    	while(TRUE){
          
          
    		down(&full);  // 满槽数量减1,如果已经为空,那就进入休眠。
    		down(&empty);  // 将空槽的数量减一。如果已经是0了,那就进入睡眠状态
    		down(&mutex); // 进入临界区。这段时间内,其他进程无法进入。
    		item = remove_item();  // 将数据放入缓冲区
    		up(&mutex);  // 离开临界区
    		up(&empty); // 空槽数目加1,如果刚好生产者正在休眠,则将其唤醒。
    	}
    }
    
    mutex用于互斥,保证任一时刻只有一个进程读写缓冲区和相关变量。
    fullempty用于实现同步,保证某种事件的顺序发生或不发生。
    简单理解,互斥信号锁,是自己加上锁了,别人想进入的时候,就会被迫进入睡眠,除非你解锁。同步信号量,就是自己主动进入的时候,被迫进入睡眠状态,需要别人满足某些条件之后,给自己解锁。
    如果交换一下producer的两个down位置,就会发生死锁。mutex进入临界区之后,发现空槽为0的,然后进入了休眠状态。但是此时,消费者因为mutex锁住了,自己也进入了睡眠状态。两者同时睡眠,就会发生死锁现象。
  • 互斥量:如果不需要信号量的计数能力,可以使用信号量的简化版本,叫互斥量(mutex)。存在两种状态,解锁和加锁。
  • 条件变量:允许线程由于一些未达到的条件而阻塞。最重要的两个操作wait(进入阻塞,直到其他线程解锁)、signal(解锁其他线程)
  • 管程:是一个由过程、变量及数据结构等组成的一个集合,它们组成一个特殊的模块或软件包。
    – 重要特性:任一时刻,只有一个活跃进程,这一特性使得管程能有效地完成互斥。当一个进程调用管程过程的时候,该过程中的前几条指令将检查在管程中是否有其他的活跃进程,如果有,调用进程将被挂起,直到另一个进程离开管程将其唤醒。
      public class ProducerConsumer {
          
          
      static final int N = 100;  // 缓冲区大小的常量
      static Producer p = new Producer();  // 初始化一个新的生产者线程
      static Consumer c = new Consumer();  // 初始化一个新的消费者线程
      static OurMonitor ourMonitor = new OurMonitor();  // 初始化一个新的管程
    
      public static void main(String[] args) {
          
          
        p.start();
        c.start();
      }
    
      static class Producer extends Thread {
          
          
        @Override
        public void run() {
          
          
          while (true) {
          
          
            int item = produceItem();
            ourMonitor.insert(item);
          }
        }
    
        private int produceItem() {
          
          
          int item = (int) (Math.random() * 100);
          System.out.println("Produce " + item);
          return item;
        }
    
      }
    
      static class Consumer extends Thread {
          
          
        @Override
        public void run() {
          
          
          while (true) {
          
          
            int item = ourMonitor.remove();
            consumeItem(item);
          }
        }
    
        private void consumeItem(int item) {
          
          
          System.out.println("Consume" + item);
        }
    
      }
    
      static class OurMonitor {
          
             // 这是一个管程
        private int[] buffer = new int[N];
        private int count = 0;  // 计数器
        private int lo = 0;  // 左边的索引
        private int hi = 0;  // 右边的索引
    
        public synchronized void insert(int val) {
          
            // synchronized 实现一个监视器,只允许由一个线程进来
          if (count >= N) goToSleep();
          buffer[hi] = val; // 将数值插进数组中
          hi = (hi + 1) % N;  // 右边的坐标,偏移。当到了尾部的时候,就重新在头部插入
          count++;
          if (count == 1) {
          
          
            notify();   // 如果count==1,就尝试唤醒消费者线程
          }
        }
    
        public synchronized int remove() {
          
          // synchronized 实现一个监视器,只允许由一个线程进来
          if (count == 0) goToSleep();
          int item = buffer[lo];
          lo = (lo + 1) % N;
          count--;
          if (count < N) notify();
          return item;
        }
    
        private void goToSleep() {
          
             // 进入睡眠状态
          try {
          
          
            System.out.println(Thread.currentThread().getName() + " go to sleep...");
            wait();
          } catch (InterruptedException e) {
          
          
            e.printStackTrace();
          }
        }
      }
    }
    
    • 消息传递
      – 进程间的通信,使用两条原语,send和receive。前一个向目标发送一条消息,后一个调用从一个给定的源接收一条消息。
      – 为了防止丢失,发送方和接收方可以达成如下一致:一旦接收到消息,接收方马上回送一条特殊的确认消息。如果发送方在一段时间间隔内没有收到回复,那就重发。
      – 为了区分新消息还是旧消息,通常采用在原始消息中插入序号的方式。如果序号上已经有了消息,那就忽略。
    • 屏障: 用于进程组而不是用于双进程。有些应用设定了多个阶段,可以通过在每个阶段的结尾安置屏障来实现只有所有进程都就绪准备好了,才进入下一阶段 。

    • 调度:当计算机系统是多道程序设计系统时,就会有多个进程或多个线程竞争CPU。只要有两个或更多的进程处于就绪状态,这种情形就会发生。如果只有一个CPU可用,必须选择下一个要运行的进程。在操作系统中,完成选择工作的这一部分成为调度程序,该程序使用的算法成为调度算法。
    • 花费绝大多时间在计算上的进程,叫做计算密集型;花费绝大多数时间在I/O上的,叫做I/O密集型。
    • 何时调度
      – 创建一个新进程之后,需要决定是运行父进程还是运行子进程。
      – 在一个进程退出时,做出调度决策,在就绪进程集中选择另外某个进程。
      – 当一个进程阻塞在I/O和信号量上或由于其他原因阻塞时,必须选择另一个进程运行。
      – I/O中断完成之后,需要重新在就绪进程集中选择。
    • 抢占式调度和非抢占式调度
      – 非抢占式调度:非抢占式调度算法挑选一个进程,然后让该进程运行直至被阻塞,或直到该进程自动释放CPU。
      – 抢占式调度:抢占式调度算法挑选一个进程,并让该进程运行某个固定时间段的最大值。如果在该时间段结束时,该进程让在运行,它就被挂起,调度程序挑选另一个进程运行。在时间间隔某端,会发生时钟中断,以便将CPU控制权交还给调度程序。
  • 调度算法的分类
    – 批处理:处理周期性的作业。一般使用非抢占式的。减少进程的切换进而改善了性能。
    – 交互式:为了避免一个进程霸占CPU拒绝为其他进程服务,抢占式必须的。
    – 实时:抢占有时候是不需要的,因为进程了解它们可能会长时间得不到运行,所以通常很快地完成各自的工作并阻塞。
  • 调度算法的目标
    – 公平:相似的进程,应该得到相似的服务。不同类型的进程可以采用不同方式处理。(给每个进程公平的CPU份额)
    – 系统策略强制执行:看到所宣布的策略执行
    – 平衡:保持系统的所有部分都忙碌,每秒完成更多的工作。
  • 批处理系统的三个指标
    – 吞吐量:系统每小时完成的作业数量。
    – 周转时间:从一个批处理作业提交时刻开始直到该作业完成时刻为止的统计平均时间。
    – CPU利用率:CPU利用率越高越好。
  • 交互式系统的两个指标
    – 最小响应时间:发出命令到得到响应之间的时间。用户请求启动一个程序或打开一个文件应该优于后台的工作。
    – 均衡性:满足用户期望的响应时间。
  • 实时系统
    – 满足截止时间:避免丢失数据
    – 可预测形:在多媒体系统中,避免品质降低。
  • 批处理系统的调度
    – 先来先服务:用队列的形式,排队作业。不会中断改作业。当运行态的进程进入拥塞态,那就到队尾,队列的第一个进程开始运行。
    – 最短作业优先:当所有可以运行的作业中,挑选运行时间最短的作业开始运行。
    – 最短剩余时间优先:抢占式版本的最短作业优先。调度程序总是选择那个剩余运行时间最短的那个进程运行。
  • 交互系统的切换
    – 轮转调度:每个进程被分配一个时间段,成为时间片,即允许该进程在该时间段中运行。如果该进程在时间片结束前阻塞或结束,则CPU立即进行切换。
    时间片设得太短会导致过多的进程切换,降低CPU效率;设得太长,又可能引起对短的交互请求的响应时间变长,导致消失了因为抢占而改善的性能。
    – 优先级调度:每个进程被赋予一个优先级,允许优先级最高的可运行进程先运行。为了防止搞优先级进程无休止得运行下去,调度程序可以在每个时钟滴答(即每个时钟中断)降低当前进程的优先级。缺点:有可能会导致低优先级的进程产生饥饿现象。
    – 多级队列:属于最高优先级的类的进程运行一个时间片,属于次高优先级的类运行2个时间片,再次一级运行4个时间片,以此类推。当一个进程用完时间片之后,就会被移到下一类。
    – 最短进程优先:首先运行最短的作业来使响应时间最短。通过进程过去的行为,推测估计运行时间最短的那个。
    – 保证调度:当用户工作时有n个用户登录,则用户将获得CPU处理能力的1/n的CPU时间。
    – 彩票调度:向进程提供各种系统资源的彩票。一旦需要做出一项调度决策时,就随机抽出一张彩票,拥有该彩票的进程获得该资源。
    – 公平分享调度:无论用户有多少个进程,用户都分得相同比例的CPU时间。

  • 哲学家就餐问题
    #define N 5 // 哲学家数目
    #define LEFT (i+N-1)%N  //左边的哲学家编号
    #define RIGHT (i+1)%N  //右边的哲学家编号
    #define THIMKING 0 //哲学家在思考
    #define HUNGRY 1  // 哲学家尝试拿起刀叉
    #define EATING 2  // 哲学家进餐
    typedef int semaphore;  // 信号量
    int state[N]  // 记录哲学家状态的数组
    semaphore mutex = 1;  // 临界区互斥
    semaphore s[N];    // 每个哲学家一个信号量
    
    void philosopher(int i){
          
          
    	while(TRUE){
          
          
    	}
    }
    
    void take_forks(int i){
          
             // 尝试进餐
    	down(&mutex)//进入临界区,冻结所有哲学家的状态
    	state[i] = HUNGRY;   // 记录哲学家现在的状态
    	test(i);   // 尝试获取两把叉子
    	up(&mutex);   // 离开临界区
    	down(&s[i]);  // 如果没有得到叉子,就会进入拥塞态。直到邻居给他解锁,然后就可以进餐了。
    }
    
    void put_forks(int i){
          
            // 完成就餐
    	down(&mutex);  // 进入临界区
    	state[i] = THINKING;   // 就餐完毕,恢复状态
    	test(LEFT);   // 检查左边能不能进餐。不能就算了。
    	test(RIGHT);   // 检查右边能不能进餐,不能就算了。
    	up(&mutex);  // 离开临界区		
    }
    
    void test(int i){
          
            // 测试是否能进餐,能进餐的话,那就进入进餐状态。
    // 当且仅当,哲学家想吃,且左右两个哲学家都没在吃的时候,才进入。
    	if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING){
          
          
    		state[i] = EATING;  // 将该哲学家修改为吃饭状态
    		up(&s[i]);  // 释放指定哲学家的冻结态
    	}
    }
    
    修改哲学家的状态时,需要互斥,因此使用mutex。而哲学家自身睡眠之后,需要其他哲学家唤醒,属于同步信号量。
  • 读者问题
    typedef int semaphore;
    semaphore mutex = 1; // 控制对读者数量的控制
    semaphore db = 1; // 控制对数据库的访问
    int rc = 0;  // 记录读者的数量
    
    void reader(void) {
          
             //读者
    	while(TRUE) {
          
          
    		down(&mutex);  // 进入修改读者数量的临界区
    		rc++;  // 读者数量加1
    		if(rc == 1) down(&db);   // 有第一个读者进入了,要将db锁住
    		up(&mutex);  // 释放临界区
    		read_data_base();   // 读取数据库数据
    		// 读取完之后,可退出了
    		down(&mutex);   // 获取对rc的互斥访问
    		rc--;
    		if(rc == 0) up(&db);  // 释放db,可以访问了
    		up(&mutex);  // 释放临界区
    	}
    }
    
    void writer(void){
          
          
    	while(TRUE) {
          
          
    		down(&db);   //想进入db,如果已经被锁住了,那就进入阻塞
    		write_data_base();  // 写数据库
    		up(&db);   // 写完可以释放锁了
    	}
    }
    

猜你喜欢

转载自blog.csdn.net/weixin_42524843/article/details/113745905