进程、线程

进程

进程就是一个数据结构,包含了程序,还有程序所需要的资源的状态。进程是cpu的抽象。因为,cpu分为两大部分,控制器和数据通路,当程序占用cpu时,实际就是占用cpu中的数据通路上的所有部件。这些部件中的状态单元所存储的信息就程序执行时的状态信息。所以说不管说进程时正在执行程序的描述,或者说是其他的,本质上指的都是对cpu的抽象。

为什么需要进程

进程是为了操作系统更好的控制程序并发,保存和恢复程序的执行状态。使得计算机可以实现(伪)并行的能力,就是在一个很短时间使多个程序得到执行。

进程的特点:

  1. 进程执行的速度时不确定的,进程执行的速度也是不可再现的(因为输入数据不同,或者是执行的执行顺序?)。
  2. 当运行同一程序多次时,都不可能产生同一进程。

进程控制结构

描述进程的数据结构是,进程控制块。进程控制块包含程序执行期间所有的状态信息。PCB是进行的唯一标识。

当进程被创建的时候,就生成一个PCB,当进程终止时,回收PCB,对进程管理式,通过PCB对进程进行管理。

PCB所包含的信息:

  • 进程标识信息:当前进程的表示,若有父进程还有父进程的标识,用户标识(进程所属哪个用户)。

  • 处理器的状态信息:(数据通路上状态单元的存储信息)

  • 进程的控制信息

PCB的组织方式

进程层级机构

Unix: 进程和它的所有子进程还有后代进程组成一个进程组。

Windows:层级关系,申请创建进程的进程获得被创建进程的一个句柄,可以通过该句柄控制被创建进程。也可以把这个句柄转移给其他进程。

进程的状态

进程的生命周期

进程的创建

系统初始化:

操作系统初始化时,操作系统会创建很多进程,以支持操作系统的运行,比如说处理中断的例程,当操作系统运行时,某个外设产生一个中断,cpu通知操作系统,操作系统根据中断信息,唤起处理该中断的例程。如果没有该例程就无法正常运行操作系统。还有就是如果该进程没有被唤醒,那么就一直处于睡眠状态。

运行进程向操作系统发起系统请求:

有些进程的工作需要其他进程的配合才能完成,那么该进程执行的时候,就向系统发起创建进程系统调用。

这种方式创建的进程有层级关系,就是说,被创建进程时申请创建进程的进程的子进程。但是父子进程各自拥有不同的地址空。就是说不共享数据,如果要共享数据只能共享不可修改的数据。

用户向操作系统发起系统请求:

比如说我们通过点击桌面的应用链接,就像操作系统发起了创建进程的系统调用。

进程的运行

操作系统通过调度算法,调度一个就绪的进程执行。

进程的等待

进程要完成一件事,但是这件事情没法立即完成,因为所需要的资源还没准备好,所以需要等待。
例如进程向操作系统发出服务请求,操作系统还没完成,还有就是进程需要其他进程的协助等等,进程都需要被阻塞。
只有进程可以阻塞自己。操作系统没法阻塞进程。

进程的唤醒

导致进程等待的原因被满足和所需资源被满足的时候,进程被唤醒,那么进程从阻塞状态进入就绪状态.

进程的终止

  • 正常退出

    当进程完成任务时,自动终止,或者触发某个事件某个事件,通知进程自动终止.
    
  • 出错退出

    操作系统所需要的资源不存在,或者参数错误时.
    比如编译java文件为clas是文件
    javac hello.java
    如果当前目录下不存在hello.java程序那么javac就会终止.
    
  • 严重错误退出

    程序运行时.产生异常,操作系统无法处理该异常那么该进程就被终止.
    
  • 被其他进程杀死

    例如在操作系统中,我们可以通过shell进程向操作系统发起调用kill进程来杀死某个进程的系统调用.
    

进程的状态模型

运行态

正在占用cpu资源的状态。

就绪态

进程所需要的一切资源均初始化完毕,就差cpu空闲时的状态.

阻塞态

进程逻辑上无法接下去运行,就是接下去运行的条件不满足时,进程就将自己阻塞.

进程的基本状态:

  • 创建状态 - > 进程正在被创建,但是还没进入就绪态之前。

  • 借宿状态 - > 进程正在从系统中消失的状态,但是pcb还没消逝。

runing -> blocked

例如当进程等待文件输入的时候,由于文件存放在外存中,外存的速度对于cpu的速度过于的慢,所以cpu就阻塞不执行等待文件资源准备好。这时此进程就需要被阻塞。

runing -> ready

操作系统为了让就绪队列中的进程都能够执行,给每个就绪进程设置了一个执行的时间片,进程时间片用完后通过进程调度算法让其他进程执行.

ready -> runing

当cpu空闲时,操作系统通过调度算法,将就绪队列中的一个进程调入cpu中执行.

blocked -> ready

当阻塞状态所需要的资源准备好时,操作系统将阻塞状态的进程调入就绪状态准备执行.

进程的挂起模型

进程没有占用内存空间的状态。就是说进程被换出内存到磁盘中,释放内存资源给其他进程使用。

阻塞挂起(blocked-suspend):阻塞进程在外存中准备导致阻塞原因得到解决。

就绪挂起(ready-suspend):进程在外存中,但是只要进入内存就可以执行。

挂起状态的转换:

阻塞 -> 阻塞挂起:当前运行的程序需要更多的内存空间,那么会执行这个状态。

就绪 -> 就绪挂起:如果被阻塞的进程有高优先级,那么就挂起一个就绪的低优先级的进程。

运行 -> 就绪挂起:抢先分时系统,当内存不够,又又高优先级进程抢占cpu资源时,那么执行这个转换。

外存中的状态转换:

阻塞挂起 -> 就绪挂起:当阻塞挂起状态的进程的资源得到满足,那么状态就发生转换,但是资源还在外存中。

解挂和激活:把进程从外存装入内存。

就绪挂起 - > 就绪:没有就绪进程,或者就绪挂起进程优先级高于其他就绪进程时。

阻塞挂起 - > 阻塞:当有足够内存时,高优先级阻塞挂起进程会装入内存,变为阻塞进程。

进程的管理

操作系统维护一组队列,来表示系统当前进程的所有状态。不同的状态分为不同的队列,例如就绪状态队列,由于不同原因导致的阻塞的阻塞状态队列。单一个进程的状态发生改变时,就将该进程的PCB添加到相应的队列中去。

线程

线程和进程差不多,线程只是进程更进一步的划分,有点像机组的流水线的概念,cpu执行一个时钟周期内只能执行一条指令,但是我们可以将指令进一步的划分成多级指令,每一级指令占用cpu上的不同硬件,缩小时钟周期的大小,使得每个时钟周期都能运行多级指令中的一级。这样就可以提高数据通路硬件的利用率。实现指令级间的并行。

那么如果我们把进程比作一条完整指令,进程中不同的操作比作被划分成多级指令的级。占用cpu比作指令级占用的硬件,那么我们就可以通过多线程,提高进程对cpu的利用率,例如,如果进程中有需要读文件的操作,这个操作会导致进程阻塞,那么我们可以给进程划分多个成多个线程,一个线程去做读文件的操作导致线程阻塞时,操作系统调用该进程的其他线程继续占用cpu直到该进程被操作系统调出cpu。

所以以上差不多说明了为什么需要线程和什么时线程。

线程和进程的区别:

首先,进程是一个具有一定功能的程序在一个数据集合上的执行过程,那么执行的过程的任务就是线程。

所以单线程的进程 = 执行程序所需要的资源(寄存器,内存,cpu啥的) + 一个线程(完成程序功能)。

多线程的进程 = 各线程的公共部分(例如:代码,数据集合)+ 各线程独有的部分(例如:堆栈,PC,SP等)

其实进程也可以完成多线程的任务,为什么不用进程,这是因为,进程是可以完成线程的任务,但是进程没有像线程那么轻量,进程间没有资源共享。还有就是进程的切换很费时间,因为比起线程进程切换后要恢复很多的状态,切换前还要保存很多的状态。如果程序存在大量的io操作,线程也可以提高程序的性能这个前面讲过。还有就是线程共享地址空间,切换线程不用切换页表,而如果切换进程我们还要切换页表,还要重置所有cache、快表的内容。

这边差不多说明了为什么要使用线程以及,线程和进程的一些区别。

线程的表现:

线程和进程一样.

进程是对真正执行程序的一种描述,进程存在的实体我认为是PCB(process controll block),

那么线程是对正在执行程序的一小部分功能的描述,线程的存在实体是TCB(thread controll block)

线程的实现

用户态上的线程

将一个进程的所有线程放到用户态中,内核对这个线程一无所知。这个操作使得不支持多线程的操作系统实现多线程。

用户线程运行在一个运行时系统上,运行时系统是一个管理线程过程的集合。就是说代替操作系统实现线程的管理,例如:创建线程时,将线程放入就绪队列中,线程阻塞时,将线程放入阻塞队列中。

每个进程都有一个线程包和一个运行时系统,运行时系统管理线程表(就是管理TCB)的数据结构。TCB和PCB的作用一致。TCB用来存放线程的状态信息。

优点:

  1. 用户线程可以在很快的时间内执行线程的切换。因为用户线程所有的信息都保存在本地,就是用户空间中(运行时系统上),不需要像内核提交线程切换的申请,只需要保存旧进程的状态信息,和更新新线程的状态信息就完成了上下文的切换。不需要从用户态到内核态,完成操作后,再从内核态到用户态的开销。(本地阻塞)
  2. 用户线程可以自定义调度算法,用户线程可以将线程表分散在各个进程中,但是内核态线程就需要一个同一的表管理所有进程的线程,如果线程非常多会占用操作系统的内存地址空间。

缺点:

  1. 如果一个进程中的线程向系统发起一个阻塞系统调用,那么内核会将整个进程阻塞掉,导致其他线程也一块被阻塞,这是因为内核没法感知这个系统调用是线程发起还是进程发起的。因为整个TCB的管理都在用户空间中进行。(解决方法,非阻塞的系统调用、包装器)
  2. 如果进程中的一个线程不自己放弃cpu的占用,俺么其他线程就无法占用cpu。(解决方法:让运行时系统请求时钟信号)。

内核态上的线程

在内核态中的线程,所有的线程的管理都交由内核来完成,就不需要运行时系统了,内核通过管理线程表进行管理线程。

内核态的线程通过增加线程切换的开销,解决用户态线程的问题,就是说如果内核态线程中有个线程阻塞了,操作系统就可以选择就绪线程对立中的一个线程继续运行,而不是将整个进程阻塞。并且根据内核态的调度算法规则,如果一个线程还没运行完,但是符合调度算法释放cpu的条件,那么就将这个线程调出,换其他就绪线程占用cpu。不会使得一个cpu一直占用cpu的情况发生。

java的线程池和这个内核中管理线程的类似情况.

由于创建线程和撤销线程的代价比较大,所以内核态中,如果一个线程完成工作退出运行,不会回收线程,就是说不会释放TCB,但是会把TCB标注为不可使用状态,当进程要创建一个新线程时候,就启动一个旧线程,只要初始化线程的数据就好了.

我想java中的线程池也是为了解决线程创建和开销的代价.

调度

cpu的调度算法是为了提高cpu利用率,提高cpu的吞吐率,降低程序的周转时间。主要还是为了能够再一定时间内更多更快的完成任务。调度算法是调度的实现。

调度的简介

进程的分类:

​ 随着存储器的速度跟不上cpu的发展速度,导致一个计算密集型的程序频繁的发生缺页中断,使得这个程序变成了io密集型的程序.

  • IO密集型

    IO密集型指的是较短时间的CPU集中使用和频繁的IO等待。
    

  • 计算密集型

    较长时间的CPU集中使用和较少的IO等待.
    

何时进行调度决策

  1. 当一个进程退出时,就要决策下一个运行的进程.
  2. 当一个进程阻塞时。
  3. 当中断发生时。是要让等待该中断的进程继续执行还是让其他进行执行。
  4. 创建一个新进程后,是决定继续运行父进程还是运行子进程。

非抢占式调度算法:

操作系统负责分配CPU时间给各个进程,一旦当前的进程使用完分配给自己的CPU时间,操作系统将决定下一个占用CPU时间的是哪一个线程。因此操作系统将定期的中断当前正在执行的线程,将CPU分配给在等待队列的下一个线程。所以任何一个线程都不能独占CPU。每个线程占用CPU的时间取决于进程和操作系统。

抢占式调度算法:

操作系统让一个进程一直占用cpu直到它阻塞了,或者进程工作完释放CPU

调度算法的分类和目标

不同的操作系统需要不同的调度算法,因为不同的操作系统面对不同的工作环境,要达到不同的目的。

批处理系统:不需要对用户的操作进行快速的响应,但是批处理系统需要再一定的时间内尽量完成多的工作,所以批处理系统对吞吐率有要求。还对周转时间有要求,尽可能快的和多的完成任务。

交互系统:需要对用户的操作进行快速的响应,例如我点开一个桌面应用,需要操作系统立刻运行该应用。还有就是均衡性,就是用户觉得有些程序需要很久的时间才能完成工作,所以他愿意等待,但是如果他觉得并不需要这么久,他就会不耐烦。所以在这个情况下操作系统要满足用户的期望,这就是均衡性

实时系统:必须满足程序运行的截止时间。

All systems:
    Fairness - giving each process a fair share of the CPU
    Policy enforcement - seeing that stated policy is carried out
    Balance - keeping all parts of the system busy
    
Batch systems:
    Throughput - maximize jobs per hour
    Turnaround time - minimize time between submission and termination
    CPU utilization - keep the CPU busy all the time
    
Interactive systems:
    Response time - respond to requests quickly
    Proportionality - meet users’ expectations
    
Real-time systems:
    Meeting deadlines - avoid losing data
    Predictability - avoid quality degradation in multimedia systems

批处理系统调度

先来先服务:FCFS(非抢占式)

根据进程到底时间顺序,依次调度进程占用cpu时间.
优点:简单
缺点:周转时间波动过大,如果是短作业先到达,那没问题,如果是长作业先到达,那么短作业的周转时间会很长.

最短作业优先:SJF

适合用于,预先知道所有就绪进程的运行时间,然后将这些进程排序,从最短运行时间的作业开始进程开始执行.

最优的最短作业优先是,只有所有作业都可以同时运行的时候,就是说所有作业要同时到达并就绪运行.
例子:
	As a counter example, consider five jobs, A through E, with run times of 2, 4, 1, 1, and 1, respectively. Their arrival times are 0, 0, 3, 3,and 3. Initially, only A or B can be chosen, since the other three jobs have not arrived yet. Using shortest job first, we will run the jobs in the order A, B, C, D, E, foran average wait of 4.6. However, running them in the order B, C, D, E, A has an average wait of 4.4.

最短剩余时间优先(SRTN)

这个是可抢占式是最短作业优先,当一个作业运行时,到达一个新的作业,他的作业时间比真正运行的作业的剩余作业时间更短,那么正在运行的作业就要被挂起,让新的作业占用cpu执行.
例如:
	有两个进程A和B,A的作业时间是10,A再0时刻到达,B的作业时间是1,B在7时刻到达,那么因为A的剩余作业时间是3大于B的作业时间,那么可以让B抢占A的cpu资源进行执行.

交互式系统调度

轮转调度(RRS)

每个进程分配一个时间片,如果该进程时间片用完,任务还没结束,那么就添加到就绪队列中,换一个进程执行。直到所有进程都完成任务。
如果时间片过小,那么会导致进程频繁的切换,浪费cpu时间。如果时间片过大,可能会导致在后面的进程周转时间过长的问题。没有达到交互式系统快速响应的目的。
所以要折中选择时间片的长度。

优先级调度

为每个进程设置一个优先级,优先级越高的进程,优先运行。
防止优先级持续占用cpu资源的方式:
	1.调度程序每隔一段时间降低正在运行程序的优先级。
	2.为一个进程赋予一个最大时间片,当该时间片用完,就让次优先级进程运行。
上面是静态设置优先级

动态优先级设置:
	为了使得IO密集型的进程先执行,使得执行IO操作的时候,CPU不空闲,可以执行其他进程。所以操作系统动态的为每个进程设置优先级,每个进程的优先级=1/f,f是上次执行时使用的cpu时长。如果一个进程是计算密集型的,那么f的值就会比IO密集型的大,优先级当每次执行时就降低,而Io密集型的进程就会上升。
	
对不同优先级的进程放置在不同优先级的队列中进行管理。

多级队列

多级队列是为了减少进程多次切换的操作带来的cpu的开销。就是为IO型的进程分配更少的时间片,为计算密集型的进程分配更多的时间片,较少Io密集型和计算密集型的进程频繁切换的现象。

设置多个不同时间片个数的队列,时间片个数越少的队列优先级越高,时间片越长的优先级越低,如果一个进程时间片用完,但是任务还没有完成,那么它就被移到下一个队列中,分配更多的时间片,但是优先级也就降低了。

最短进程优先

最短作业优先伴随较短响应时间,这就很适合交互式系统,但是最短作业优先要提前直到作业时间的长度,而短进程优先无法知道作业的长度,但是可以通过历史的执行时间长度预测未来的执行时间长度.

保证调度

为每个用户进程分配给它合适的cpu时间,就是较长作业时间的进程,我们多分配给他cpu时间,但是较短的作业进程,我们少分配点cpu时间给他。
应要获得时间 = 自进程创建时间到现在的时间 / n(用户的个数)
实际获得cpu时间由操作系统记录。
正在获得cpu时间 / 应要获得时间的比率,如果这个比率小于0.5那么就多给它分配cpu时间,如果这个比率大于2就少分配cpu时间。

彩票调度

https://www.cnblogs.com/tobe98/p/11792610.html#4933078

公平分享调度

之前的算法只考虑到一个用户的情况,但是一台计算可能由多个用户运行,例如Linux有用户id和用户组的标识,那么如果有一个用户1有9个进程,另外一个用户2有1个进程,那么百分之90的cpu使用都归于用户1。这显然不公平的。
所以为每个用户分配一个时间片,每个时间片内每个用户可以执行自己的进程.

实时操作系统的调度

硬实时:必须满足绝对的截至时间

软实时:虽然不希望偶尔错失截止时间,但是可以容忍。

策略和机制

就是让某个进程去干预调度的策略,因为例如一个主进程有很多个子进程,也只有主进程知道哪个进程最有用,优先级最高应该让它先执行,但是调度程序并不知道,所以可以通过参数的形式告诉调度程序哪个进程应该优先被调度.这就是调度机制和调度策略分离的原则.

线程调度

用户级:

用户级别的调度可以是上面的任意一个调度算法,但是由于用户级的线程,没有中断机制,没法干预线程的运行,就是说只有等待线程阻塞,或者说线程执行完成释放cpu资源的时候,运行时系统的调度算法才开始决策。
但是它并不影响其他进程的线程,只会影响和自己同在一个进程中的线程。

内核级:

不想用户级别的线程,在内核级的线程,如果一个线程用完了时间片,那么就会调度其他线程来占用cpu,但是它可能不会考虑是这个线程属于哪个进程,比如说,我现在执行了A进程的线程,下一步可能执行B进程的线程,也可能执行A进程的线程.

进程中的通信

多个进程访问一个共享的地址空间,当一个进程已经开始访问这个地址空间时,我们要通过一种机制告诉其他进程,我访问了,你别乱来。当一个进程访问完一个地址空间时,我们要通过一种机制告诉其他因为需要访问改地址空间的其他进程,我访问好了,你随意。这就是锁机制信号量管程提供的帮助。

竞争条件(race condition):就是两个进程并发访问一个共享资源,最终结果依据精确的运行时序。

临界区(critical region):对共享内存进行访问的代码片段。

互斥(mutual exclusion):避免竞争条件的发生,组织多个进程同时访问共享资源。

良好的解决竞争条件的临界区访问方案应该具有的特性:

  1. 互斥:同一时间临界区只能有一个进程访问
  2. 前进:如果一个进程想进入临界区,那么它一定会进入临界区
  3. 有限等待:使得进程不变成饥饿状态,正在有限的时间内可以进入临界区。
  4. 无忙等待:进程等待进入临界区,它可以被阻塞或者挂起。

忙等互斥

忙等指的是连续测试一个变量的值直到符合的值出现时。

屏蔽中断

中断是程序调度发生的时机之一,例如时钟中断时,磁盘将数据放入内存时等等,cpu都会向操作系统发出中断请求,使得操作系统调度中断服务例程来处理这个中断请求。如果一个进程在临界区内执行操作时,发生中断,此时进程不得不释放cpu资源给中断服务例程,让自己进入就绪队列中,等待下一次cpu的空闲。此时如果就绪队列中存在另外一个想要访问临界区的进程。当这个进程先于之前那个访问临界区时被强制释放cpu的进程访问临界区时,这时就发生的竞争条件。

所以我们当一个进程进入临界区时,就屏蔽所有中断,这样调度程序就无法中途被唤醒,那么进程就可以在临界区中一直执行,直到离开临界区。

屏蔽中断也有问题,如果屏蔽中断后,没有开启中断,就会导致系统崩溃,如果是多核cpu,屏蔽一个cpu的中断机制,并不会影响其他cpu。就是说其他cpu依旧可以向操作系统发起中断请求。

锁变量

为一个临界区,设置一个值为0的共享变量,当一个进程想要进入临界区之前,他要先访问共享变量,如果共享变量的值是0时,此时说明临界区时空闲的,进程可以进入临界区,进入时,将共享变量设置为1。离开临界区时,将变量设置为0。当一个进程访问共享变量为1时,此时说明临界区有进程访问,此进程空转等待临界区资源。

锁变量有一个问题,就是访问锁变量和设置锁变量这两个操作不是一组原子操作,所谓一组原子操作指的是,当访问锁变量和设置锁变量时,对应的机器指令只有一条。就是说这做这个操作时,不允许被打断。

由于不是原子操作还是会导致竞争条件,临界区可能存在两个进程,例如当我们访问共享变量后当要设置共享变得值时,发生系统调用,此时另外一个进程也访问共享变量,共享变量的值并没有被改变,那么它也可以进入临界区。

严格轮转法

一个进程访问完临界区设置下一个可以访问临界区的进程。
例如一开始设置一个turn变量为0,代表进程0可以访问临界区资源,当进程0访问完临界区资源后,将turn设置为1,代表进程1可以访问临界区。进程0离开临界区执行其他操作。进程1获得cpu时,如果turn为1时,进入临界区,如果turn为0时就忙等待直到turn为1,忙等时进程1不会被阻塞。当进程1离开临界区时将turn设置为0,让进程0可以访问临界区。

这个算法的主要问题就是进程0和进程1如果作业时间长度相差很大的时候,例如进程1时进程0作业时常的100倍,那么当进程0访问完临界区后将临界区让给进程1执行后,进程1用完临界区后,将临界区让给进程0,如果接下来进程0用完临界区后,将临界区让给进程1,进程0执行完其他操作向继续访问临界区,此时进程1还在做第一次访问临界区资源完后的其他操作。无法进入临界区将临界区资源释放给进程0。导致进程0无法继续执行进入原地空值。

自旋锁-忙等待锁。

Peterson算法

用两个变量判断进程是否能够进入临界区, 第一个变量turn表示现在能够进入临界区的进程,另外一个变量是一个数组flag代表进程是否有进入临界区的意愿。例如一个进程0向要进入临界区那么就设置flag[0] = true,如果进程1想要进入临界区那么就设置flag[1] = true;那么到底哪个进程能够进入临界区这取决于turn的值了。
分为两种情况:
	1.如果只有进程0想进入临界区,此时turn = 0, 而另外一个Process[other = 1]=false的,所以很快就可以跳出自旋锁,离开enter_region方法进入临界区
	2.如果当两个进程0和1都想进入临界区的话,如果是进程1后到达,将覆盖turn = 1此时进程1就会锁在while语句上,因为turn=1并且process[other=0]=ture的。所以进程一会自旋知道cpu时间片用完离开,让进程0执行,此时由于turn被进程1改写了,所以进程0可以很快的离开自旋锁进入临界区,当进程0离开临界区后,将process[0]=false,此时进程1就可以离开自旋锁进入临界区了。
#define FALSE 0
#define TRUE 1
#define N 2 /* number of processes */
int turn; /* whose turn is it? */
int interested[N]; /* all values initially 0 (FALSE) */
void enter region(int process); /* process is 0 or 1 */
{
    int other; /* number of the other process */
    other = 1 − process; /* the opposite of process */
    interested[process] = TRUE; /* show that you are interested */
    tur n = process; /* set flag */
    while (turn == process && interested[other] == TRUE) /* null statement */ ;
}
void leave region(int process) /* process: who is leaving */
{
	interested[process] = FALSE; /* indicate departure from critical region */
}

硬件的支持

通过硬件的支持提供的操作,只允许一个cpu内存lock字进行操作。提供互斥同步原语。
例如ARM指令中提供的原语,cpu将内存中lock字与寄存器的中的值进行交换,再判断寄存器中的值,如果值为0就可以访问互斥区,因为是原子操作,一个时钟周期只能执行一条指令。所以当一个做了交换操作,进入临界区,当其他cpu想进入临界区调用交互操作时,发现lock值为1,代表有其他的进程再临界区中执行,该进程就要等待。

睡眠唤醒

如果忙等互斥的忙等待的时间多于进程切换的开销的话,那么我们可以通过将一个无法进入临界区的进程阻塞而不是让他占用cpu空转等待,如果忙等的时间不长,那么我们就可以让忙等,因为忙等的开销小于进程切换的开销。

进程通信中的原因sleep和wakeup。sleep将一个忙等进程阻塞;wakeup唤醒一个睡眠进程,因为导致睡眠的原因得到解决了。

生产者消费者模型

#define N 100 /* number of slots in the buffer */
int count = 0; /* number of items in the buffer */
void producer(void)
{
    int item;
    while (TRUE) { 				/* repeat forever */
        item = produce item( ); 				/* generate next item */
        if (count == N) sleep( ); 				/* if buffer is full, go to sleep */
        inser t item(item); 				/* put item in buffer */
        count = count + 1; 				/* increment count of items in buffer */
        if (count == 1) wakeup(consumer); 				/* was buffer empty? */
    }
}
void consumer(void)
{
    int item;
    while (TRUE) { 				/* repeat forever */
        if (count == 0) sleep( ); 				/* if buffer is empty, got to sleep */
        item = remove item( );				 /* take item out of buffer */
        count = count − 1; 				/* decrement count of items in buffer */
        if (count == N − 1) wakeup(producer);				 /* was buffer full? */
        consume item(item); 				/* pr int item */
    }
}

信号量(semaphore)

信号量不同于之前的忙等互斥,信号量除了可以实现互斥操作还可以实现同步操作。

信号量的构成:

semaphore 整型,代表唤醒操作的个数。
down() 将semaphore-1代表需要一个唤醒操作,在比较semaphore的值如果小于0阻塞进程,否则进程继续执行。
up() 将semaphore+1代表有一个进程结束,可以唤醒一个等待在该信号量上的进程。

信号量的down()和up()操作是一个原子操作,包含了:将一个数减一,判断这个数是否小于0,小于0就将进程睡眠。当一个进程开始对信号量进行这个操作时,就不允许其他进程访问这个信号量。

这里说说为什么需要信号量机制:

首先看看上面那个生产者消费者模型,因为if(N == 0)语句和sleep语句不是原子操作,也就是说如果consumer做完if语句的比较之后,要开始sleep操作但是还没开始,发生了进程调度,让producer进程执行,producer此时将产品+1发现之前有一个consumer进程因为没有产品而导致进程睡眠,所以我们要通过wakeup唤醒这个进程。但是comsumer并没有执行睡眠操作,导致wakeup的信号丢失了。consumer永久睡眠,之后producer将buff填满后也进入了永久睡眠。

信号量解决wakeup信号丢失的,同时也解决了为防止信号丢失而为每个进程设置一个唤醒等待位。

首先信号量将if语句和sleep绑定为一个操作,用一个数值取代设置唤醒等待位。

用信号量解决生产者消费者模型:

 #define N 100 /* number of slots in the buffer */
 typedef int semaphore; /* semaphores are a special kind of int */
 semaphore mutex = 1; /* controls access to critical region */
 semaphore empty = N; /* counts empty buffer slots */
 semaphore full = 0; /* counts full buffer slots */
 void producer(void)
 {
     int item;
     while (TRUE) { /* TRUE is the constant 1 */
         item = produce item( ); /* generate something to put in buffer */
         down(&empty); /* decrement empty count */
         down(&mutex); /* enter critical region */
         inser t item(item); /* put new item in buffer */
         up(&mutex); /* leave critical region */
         up(&full); /* increment count of full slots */
     }
 }
 void consumer(void)
 {
     int item;
     while (TRUE) { /* infinite loop */
         down(&full); /* decrement full count */
         down(&mutex); /* enter critical region */
         item = remove item( ); /* take item from buffer */
         up(&mutex); /* leave critical region */
         up(&empty); /* increment count of empty slots */
         consume item(item); /* do something with the item */
     }
 }

互斥量(mutex)

如果我们只需要用信号量完成互斥的话,我们只需要用一个设置为1的信号量就足够了。

当进程向进入临界区之前,要先访问互斥量,如果互斥量为1,调用down操作(),离开临界区后调用up操作唤醒阻塞在该信号量上的进程。

管程(monitor)

monitor是编程语言的概念,为了更加简单的实现同步互斥操作,不同于信号量,如果编写代码的时候,down的操作顺序没有安排对,会导致所有进程都被阻塞进入一个死锁的状态。

monitor:一个由过程(方法,在Java中被标注为synchronized关键字标注的方法),变量和数据结构组合成的一个数据结构,任何时刻,只能允许一个进程进入monitor中执行。

当进程调用monitor的方法时,操作系统会先检查看monitor中有没有其他进程正在执行,如果有这个进程就阻塞在monitor外,如果没有该进程就进入monitor执行。

但是当进程进入monitor后发现monitor的变量无法满足进程的需求时该怎么办?

我们使用条件变量替代变量,并且引入wait和singal操作,这个与信号量不同。

信号量:允许一个进程对一个临界区的访问和阻塞.
条件变量:如果一个进程的条件没有被满足那么就会被阻塞,当条件满足时被唤醒.

当一个consumer进程在monitor中执行,调用了一个消耗的产品的方法导致产品数量为0,这是时候这个方法会调用singal()操作唤醒一个在empty条件变量上的阻塞的进程。此时有一个问题就是monitor中不可以有两个活跃的进程,那么当consumer进程唤醒了producer进程后怎么办。

Hansen建议:让做唤醒操作的进程执行完操作后在调用被唤醒的进程执行。

Hoare建议:让被唤醒进程执行,做唤醒操作的进程先阻塞,等到被唤醒的进程执行完毕后在执行。

posted @ 2021-10-04 15:15  _LittleBee  阅读(175)  评论(0编辑  收藏  举报