操作系统(1)进程状态、同步、互斥、通信和CPU调度

 

进程和线程知识体系:

1. 进程

在多程序系统中,操作系统调度CPU上的进程以获得最大的利用率,此过程称为CPU调度。 操作系统使用各种调度算法来调度过程。

这是短期调度程序的一项任务,用于调度CPU以查找作业池中存在的进程数量。 每当运行进程请求某个I/O操作时,短期调度程序就会保存进程的当前上下文(也称为PCB)并将其状态从运行状态更改为等待状态。 在此期间,进程处于等待状态; 短期调度程序从就绪队列中选择另一个进程并将CPU分配给此进程。 这个过程被称为上下文切换。

1.1 PCB

在使用多道批处理系统,多个程序并发执行,内存中就可能存在多个程序自己的程序段和数据段,那么这时候就需要一个管理单元对这些东西加以区分、描述和管理,所以就额外多了一个进程控制块,也就是 PCB(process control block)

系统会为每一个运行的程序分配 PCB 这么一个数据结构,用以描述进程的各种信息。PCB 是进程存在的唯一标志,进程与 PCB 是一一对应的的。

操作系统在进程的生命周期中维护一个进程控制块。 进程终止或终止时,进程控制块将被删除。 有以下信息保存在过程控制块中,并随过程状态而变化。

PCB 记录了关于进程的信息,这些信息包括:

  • 进程标识符:外部内部各一个
  • 处理机状态:通用寄存器、指令计数器、程序状态字 PSW,用户栈指针
  • 进程调度信息:进程状态、进程优先级、其它的进程调度信息
  • 进程控制信息:程序和数据的地址、进程同步和通信机制、资源清单、链接指针

程序段(程序代码)、数据段(变量、常量等),PCB(相关的管理信息) 共同构成了进程实体(进程映像)。一般认为 进程实体 === 进程 === PCB

进程与程序的对比:

  • 进程是动态的(程序的执行),程序是静态的(有序代码的集合)
  • 进程是暂时的,程序是永久的
  • 进程和程序的组成不同
  • 通过多次执行,一个程序可以对应多个进程;通过调用关系,一个进程可以包括多个程序

1.2 进程的状态和生命周期

1)进程的5种状态:

① 创建态:初始化 PCB,为进程分配系统资源

② 就绪态:PCB 修改相应内容并被送到就绪队列。万事俱备(运行需要的条件都有了),只欠东风(只等 CPU 调度自己)

③ 运行态: PCB 修改相应内容,出队(可能还会恢复进程运行环境)。该进程此时占有 CPU 使用权,在 CPU 上运行(对于单核处理器,一个时刻只会有一个进程)

④ 阻塞态(等待态):进程进行系统调用,或者等待事件发生时,进入阻塞态,PCB 修改相应内容并被送到相应事件的阻塞队列

⑤ 终止态(结束态):回收为进程分配的资源,撤销 PCB

PS:由于事件有多个,所以阻塞队列一般也是有多个的,一个事件对应一个阻塞队列。而就绪队列就只有一个

2)进程状态间的切换

3)引入挂起操作

(1)挂起:

前面所说的状态转换,是建立在内存资源够用的情况下 —— 当系统资源尤其是内存资源不够时,就需要将一些进程挂起(suspend),对换到外存中。

(2)原因:

引起进程挂起的原因是多样的,主要有:

  • 系统中的进程均处于阻塞态,处理器空闲,此时需要把一些阻塞进程对换出去,以腾出足够的内存装入就绪进程运行。

  • 进程竞争资源,导致系统资源不足,负荷过重,此时需要挂起部分进程以调整系统负荷,保证系统的实时性或让系统正常运行。

  • 把一些定期执行的进程(如审计程序、监控程序、记账程序)对换出去,以减轻系统负荷

  • 用户要求挂起自己的进程,以便根据中间执行情况和中间结果进行某些调试、检查和改正。

  • 父进程要求挂起自己的后代进程,以进行某些检查和改正。

  • 操作系统需要挂起某些进程,检查运行中资源使用情况,以改善系统性能;或当系统出现故障或某些功能受到破坏时,需要挂起某些进程以排除故障。

(3)状态转换

引入挂起操作后,在原来五种状态的基础上多了两个状态:就绪态变成了活动就绪态,且多了一个“静止就绪态/挂起就绪态“;原来的阻塞态变成了活动阻塞态,且多了一个“静止阻塞态/挂起阻塞态“。

状态解释
活动就绪态 → 静止就绪态操作系统根据当前资源状况和性能要求,可能会把活动就绪态对换出去,成为静止就绪态。处于静止就绪态的进程不再被调度执行
静止就绪态 → 活动就绪态内存中没有进程处于活动就绪态,或者处于静止就绪态的进程具有更高的优先级,那么静止就绪态就会被对换回来,此时才可能被调度执行
活动阻塞态→ 静止阻塞态操作系统根据当前资源状况和性能要求,可能会把活动阻塞态对换出去,成为静止阻塞态。
静止阻塞态→ 静止就绪态常见的情况是,引起进程等待的事件发生之后,相应的静止阻塞态进程将转换为静止就绪态
静止阻塞态→ 活动阻塞态但有时候,如果静止阻塞态进程的优先级高于静止就绪队列中的任何进程、并且系统有把握它等待的事件即将完成,那么就会激活为活动阻塞态
运行态→ 静止就绪态优先级较高的静止阻塞态在等待的事件完成后,可能会抢占 CPU,若此时资源不够,则可能导致正在运行的进程挂起为静止就绪态
创建态→ 静止就绪态操作系统根据当前资源状况和性能要求,可能会在进程创建完就把它对换到外存

PS:进程一旦被挂起,就意味着它被对换到了外存中,此时该进程无法再被 CPU 直接调度,除非它被对换回内存中,回到活动就绪态。比如静止就绪态、静止阻塞态,最后要得到 CPU 的调度,都必须经历回归到活动就绪态的过程。

4)进程控制

进程的生命周期有多个状态,而状态的切换实质上是通过修改 PCB 的信息、让 PCB 出队或者入队来实现的,但是是谁来控制这个过程呢?—— 答案就是进程控制,进程控制指的是对系统中所有进程,从创建到终止的全过程实行的管理和控制。而进程控制是通过操作系统内核的 原语操作 来实现的。

 

操作系统内核一览,其中一个核心就是原语操作


原语(Primitive)其实就是程序 —— 由若干条机器指令构成,用以完成特定功能的一段程序。原语操作属于原子操作(Atomic operation),具有不可中断性,一旦执行就不允许被打断。这种原子操作是依靠关中断指令实现的,在关中断指令下,即使有中断信号发射过来,也不会调用中断处理程序去处理中断,这就保证了原语操作不会被打断。而在开中断指令下,才会去处理中断。

 

原语的基本操作无非三个:

  • 更新 PCB 中的信息(修改进程状态标志、保存当前运行环境到 PCB、从 PCB 中恢复运行环境)
  • 将 PCB 插入到合适的队列
  • 分配/回收资源

创建原语和撤销原语配对,阻塞原语和唤醒原语配对,所以这里我们放在一起讲。

(1)创建原语

创建原语负责创建进程,具体包括:申请空白的 PCB,为新进程分配所需资源、初始化 PCB、将 PCB 插入到就绪队列。

引起进程创建的事件一般有四种:

  • 用户登录:分时系统中,用户登录成功,系统会为其建立一个新的进程
  • 作业调度:多道批处理系统中,从作业队列取出作业放入内存时,会为其建立一个新的进程
  • 提供服务:用户向操作系统请求服务时,会创建一个进程来处理请求
  • 应用请求:应用/用户进程主动请求创建一个子进程

(2)撤销原语

撤销进程负责终止进程,具体包括:从 PCB 集合中找到终止进程的 PCB,如果进程正在运行,则立即将它的 CPU 使用权移交给其它进程。接着终止它的所有子进程,将该进程的资源还给父进程或者操作系统,最后再删除 PCB。

引起进程终止的事件一般有三类:

  • 正常结束
  • 异常结束
  • 外界干预

(3)阻塞原语

阻塞原语负责让进程从运行态转换到阻塞态,具体包括:找到要阻塞的进程的 PCB,保存当前运行环境到 PCB(方便后续恢复),修改 PCB 状态信息。接着暂停进程的运行,将 PCB 插入相应事件的等待队列

引起进程阻塞的事件一般是:

  • 等待系统分配资源
  • 请求系统某些服务(比如打印服务)
  • 启动某种操作(比如 I/O 操作)
  • 新数据尚未到达
  • 无新工作可做

注意:进程从运行态切换到阻塞态,是一个主动的过程,这个主动体现在是进程自己调用了阻塞原语

(4)唤醒原语

唤醒原语负责让阻塞的进程重新回到就绪态,具体包括:在事件等待队列中找到 PCB,让他出队,修改 PCB 的状态信息,再将 PCB 插入到就绪队列,等待 CPU 对他进行调度

一般在等待的事件发生时,进程就会被唤醒。

注意:进程从阻塞态切换到运行态,是一个被动的过程,这个被动体现在并不是进程自己调用了唤醒原语,而是“合作”进程进行了调用(比如说 I/O 进程)

(5)切换原语

前面的原语主要都是操作一个进程,而切换原语同时操作到了两个进程。

切换原语负责让当前运行的进程从 A 切换为 B,具体包括:

  • 一方面,将 A 的运行环境保存到 PCB 中,再将其 PCB 移入到相应的队列(如果当前进程是从运行态到阻塞态,那么就进入等待队列;如果是从运行态到就绪态,那么就进入就绪队列)

  • 另一方面,选择 B 进程运行,更新其 PCB,同时可能会恢复其运行环境(考虑到 B 进程此前可能曾处于阻塞态)

引起进程切换的事件一般有四种:

  • 当前进程的时间片被消耗完
  • 有更高优先级的进程到达,抢占了当前进程正在使用的 CPU
  • 当前进程主动阻塞
  • 当前进程终止

(6)挂起原语和激活原语

挂起原语:

将进程从内存对换到外存,具体包括:找到需要挂起的进程的 PCB,检查它的状态并做相应操作(运行态、活动就绪态 ——> 静止就绪态,活动阻塞态 ——> 静止阻塞态),之后将该 PCB 复制到指定的内存区域。

引起进程挂起的事件,比如,用户进程请求将自己挂起,或父进程请求将自己的某个子进程挂起

激活原语:

将进程从外存对换回内存,检查该进程的现行状态并进行相应操作(静止就绪态——>活动就绪态,静止阻塞 ——> 活动阻塞态)。引起进程激活的事件,比如,父进程或用户进程请求激活指定进程,或者是某个进程驻留在外存而内存中已有足够的空间

1.3 进程的同步和互斥

0)多道程序批处理系统

作业存放在外存,形成“后备队列”,由作业调度程序选择若干个作业调入内存,使他们共享 CPU 和系统资源。由于有多道程序,可以及时补位 CPU 的空闲。

  • 特点:无序性(先进入内存的作业不一定先完成)、多道性、调度性
  • 优点:资源利用率高、系统吞吐量大
  • 缺点:平均周转时间长、无法交互

用一个例子来说明:

假如计算机现在需要处理 J1,J2,J3 三个作业,每个作业都需要经历输入(1秒)、计算(1秒)、输出(1秒)的过程。

如果计算机是多道批处理系统,那么处理过程大概是这样的:

可以看到,输入设备,CPU,输出设备这三者是可以并行使用的。J1 输入完成后开始计算,此时输入设备空闲,可用于进行 J2 的输入工作,J1 计算完成后开始输出,此时 CPU 空闲,可用于 J2 的计算工作,J2 进行计算的时候,输入设备空闲,可用于 J3的输入工作,以此类推……..可以看到,多道批处理系统尽可能在同一时刻让输入设备,CPU,输出设备这三者都有事可做,将资源充分调用起来。相比单道批处理系统耗时 9 秒,完成相同的工作,多道批处理系统只需要 5 秒即可。

1)进程同步

并行和并发的区别

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行,CPU多个核执行。比如下图的 ABC 三个人同时吃面包,1分钟后都吃完了,无论是从微观角度(聚焦于一秒)还是从宏观角度(聚焦于几十秒),这三者都是同时执行的

图片描述
 

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速地轮换执行,使得在宏观上具有多个进程同时执行的效果,但实际上在微观上并不是同时执行的,单个核执行。比如下图的 ABC 三个人吃面包,0-10 秒 A 吃面包,10-20 秒 B 吃面包,20-30 秒 C 吃面包,那么在微观角度上(聚焦于秒级别的比较),很显然不是同时执行的;但是在宏观角度上(聚焦于整体的1分钟),不考虑更加精细的时间,这三者是同时执行的。

图片描述

  • 进程:在系统中能够独立运行并作为资源分配的基本单位,程序创建进程后才能并发执行

在多道批处理系统中,多个进程是并发执行的,而并发执行的进程具有异步性,也就是说,各个进程以各自独立的、不可预知的速度向前推进。这样会带来什么问题呢?如果有 AB 两个进程分别进行读写数据的操作,那么写数据应该发生在读数据之前,而实际上,由于异步性的存在,可能会发生先读后写的情况,而此时由于缓冲区为空,该读数据进程就会被阻塞。

解决方案:

我们要通过进程同步来解决此类问题。

 

2)进程互斥

在多道批处理系统中,多个进程是并发执行的,而并发执行的进程不可避免地需要共享一些系统资源(比如内存、打印机、摄像头等)。这样会带来什么问题呢?实际上,有些资源在一个时间段内只允许一个进程使用,诸如各种物理设备、变量、数据、内存缓冲区等,这些称之为临界资源 —— 也就是说,一方面,并发执行的进程需要共享资源;另一方面,临界资源的访问又必须是互斥地进行(不能同时共享),很显然,这会导致资源访问上的矛盾。

解决方案:

所以,我们要通过进程互斥来解决此类问题。

与进程互斥相关的也就是间接制约关系,指的是当 A 进程在访问某个临界资源时,另一个也想要访问该资源的 B 进程就必须等着,直到 A 进程访问结束并释放资源后,B 进程才能去访问。

逻辑上将一个进程对临界资源的访问过程分为四个部分:

do {
    extry section;       // 进入区
    critical section;    // 临界区
    exit section;        // 退出区
    remainder section;   // 剩余区
} while(true)
  • 进入区:A 进程想要访问临界资源,首先会在进入区检查是否可以进入,由于此时没有其它进程占用临界资源,所以检查通过,同时它设置了一个 Flag 标志当前自己正在访问临界资源;
  • 临界区:实际访问临界资源的那段代码
  • 退出区:负责解除之前的 Flag
  • 剩余区:其它处理

2-1)互斥遵守的4个原则

我们需要用四个原则来约束这个互斥的过程:

  • 空闲让进:临界区空闲时,说明没有进程使用临界资源,此时应该让想要进入临界区的进程立刻进来
  • 忙则等待:如果已经有进程进入临界区,则其它同样想要进入的进程只能等着
  • 有限等待:不能让进程一直干等着,要保证他在有限的时间内可以进入临界区
  • 让权等待:当 A 进程进入临界区而导致 B 进程不能进入自己的临界区时,应该立刻释放处理机,防止进程陷入“忙等”状态。

2-2)软件实现互斥

① 单标志法:

单标志法的核心是用一个 Flag 来标志哪个进程可以进入临界区,在初始给定 Flag 的情况下,一定可以确保是 Flag 对应的进程可以进入临界区。而在该进程顺利进入并完成自己的任务后,它会将 Flag 改指向另一个进程。我们通过一个例子来说明:

int turn = 0;

P0 进程:                        P1 进程:
while (turn != 0);              while (turn != 1);
critical section;               critical section;
turn = 1;                       turn = 0;
remainder section;              remainder section;

在一开始我们置 Flag 指向 0 号进程。设想有两种可能:一种是 P0 进程先上处理机,那么此时不满足 while 条件,则顺利进入自己的临界区;另一种是 P1 进程先上处理机,尽管如此,由于满足 while 条件,所以陷入了死循环,一直无法进入临界区,直到消耗完了自己的时间片,轮到了 P0 运行。P0 由于不满足循环条件,所以顺利进入临界区。值得注意的是,在这个过程中,即使由于 P0 消耗完了时间片而导致处理机使用权转让给了 P1,P1 也不会实际进入临界区,而是不断循环 —— 这就确保了整个过程中,即使进程不断来回切换,始终都只有 P0 在使用临界资源,也就是做到了我们所要的“互斥访问资源”

但问题在于,观察整个过程会发现,P0 完成任务后将“使用权限“(Flag)转交给 P1,而 P1 完成后也转交给 P0,所以整个过程一直都是 P0 ——> P1 ——> P0 ——> P1……….. 这样交替进行,也就是说,即使 P0 运行完之后想要再次运行,它也不得不先等待 P1 的完成

另一个问题是,P0 如果一直不访问临界区,那么就算此时临界区空闲、且 P1 有意愿想要访问临界资源,P1 也无法访问,也就是”空闲不让进“。这很明显违背了上面所说的”空闲让进“原则。

②  Peterson 算法

Peterson 算法实际上同时结合了单标志法和双标志后检查法,它的核心就是:在一开始还是和后检查法一样,抢先进行“上锁”,但是上锁之后又将 turn 置为对方线程,表示自己虽然想要进入临界区,但是不介意“将这个机会让给对方”。尽管如此,由于 while 的限制条件增加了,而 turn 又是公用的,所以保证了最后只会有一方的 while 满足条件。既做到了互斥访问资源,也避免了双方都访问不到资源。

还是来看下面这个例子:

bool flag[2];  flag[0] = false;  flag[1] = false;
int turn = 0;     

P0 进程:                        P1 进程:
flag[0] = true;                 flag[1] = true;
turn = 1;                       turn = 0;
while (flag[1] && turn == 1);   while (flag[0] && turn == 0);
critical section;               critical section;
flag[0] = false;                flag[1] = false;
remainder section;              remainder section;

首先进入后检查法的情况,即:P0 首先表示想要进入临界区,因此它的 Flag 为 true,之后进程切换来到了 P1,P1 也表示自己想要进入临界区,因此它的 Flag 也是 true。

在后检查法中,这种情况注定了双方都陷入死循环,谁也无法进入。但是 Peterson 算法却不一样。

在这个算法中,对方进程想进入、且最后一个做出“谦让”的进程最终将无法进入临界区。继续上面的例子,此时可能:

  • 继续执行 turn = 0,while (flag[0] && turn == 0),由此进入了死循环,于是时间片用完后来到了 P0,P0 执行 turn = 1,while (flag[1] && turn == 1),同样进入了死循环,于是时间片用完后来到了 P1,注意,此时对于 P1 来说,它的 while 条件不满足,所以顺利进入了临界区,直到运行完释放“权限”,P0 的才有机会跳出自己的死循环。

    这种情况,由于 P0 是最后一个“谦让”的,所以是对方 P1 进入临界区

  • 或者,切换到 P0 执行 turn = 1,while (flag[1] && turn == 1), 由此进入了死循环,于是时间片用完后来到了 P1,执行 turn = 0,while (flag[0] && turn == 0),同样进入了死循环,于是时间片用完后来到了 P0,此时对于 P0 来说,while 条件已经不满足,所以 P0 得以顺利进入临界区。

    这种情况,由于 P1 是最后一个“谦让”的,所以是对方 P0 进入临界区

  • And others ……

考虑到进程并发的异步性,其实有很多种排列组合的情况,但是不管哪种情况,可以肯定的是:即使双方都想进入临界区,由于 turn 只有一个,也肯定有一方可以顺利跳出死循环,进入临界区。这就避免了“饥饿”现象的产生;同时,只要自己进程临界区没执行完,就永远不会释放”权限“,意味着对方进程不会乘机抢着进入临界区,这就保证了”互斥“。

用一个生活案例来解释,可能更好理解:

甲乙两个人同时去图书馆借一本书,甲说:”我很想看这本书,但是你想看的话,我不介意让你先看“,而乙也说:”我也很想看这本书,但是你这么谦让我都不好意思了,还是你先看吧“,双方就这样互相你来我往。到最后甲也累了,于是在听到乙再次说了”让你先看“之后,甲拍了拍乙的肩膀,同时把书拿了过来,说:”好吧,那我先看吧,我看完,你再看。“

Peterson 算法解决了空闲让进、忙则等待、有限等待的问题,但还是没有解决让权等待的问题。也就是说,P1 进程尽管无法进入临界区,但是在时间片轮到自己的时候还是会做无意义的死循环,白白占用了处理机,而这些资源本该是给 P0 使用的。

链接

1.4 进程的信号量机制

信号量机制可以让用户通过使用操作系统提供的一对原语来对信号量进行操作,从而方便地实现进程互斥和进程同步。

信号量(Semaphore):其实就是一个变量,它可以记录系统中某个资源的数量;

原语:指的是 wait(S) 原语和 signal(S) 原语(或者说是 P 操作和 V 操作),可以看作是两个函数。

1)整型信号量

信号量如果单纯是一个整数型的变量,那么就称为整型信号量,它的值记录了系统中资源的数量。在使用整型信号量的情况下,P 、V 操作是类似这样的:

int S = 1;
wait(int S)               
{                       
    while(S <= 0);            
    S = S-1;           
}
signal(int S)
{
    S = S+1;
}

同样以进程 P0,P1 为例进行说明:

P0:                    P1:
wait(S)                wait(S)            // 进入区
critical section       critical section   // 临界区
signal(S)              signal(S)          // 退出区 

运行流程:

1. P0 想要进入临界区,那么它就会在进入区申请资源:执行 P 操作,进行“检查”和“上锁”,由于 S 一开始是1(表示目前有一个资源可以使用),所以 P0 可以跳过循环,成功申请到资源。

2. 此后,S 减一变为 0,代表已经没有可用资源了 —— 这一步也相当于上锁;

3. 对于 P1,当他想要申请资源的时候,同样先来到进入区执行 P 操作,由于 S = 0,所以此时 P1 陷入了死循环;

4. 再回到 P0 ,他完成任务后会在退出区释放资源,S加一变为 1,这时候 P1 才有机会退出循环,进而申请资源。

整个过程其实和之前介绍的方法是很类似的,但是由于这次,“检查”和“上锁”两个操作被封装在一个原语里,所以这两个操作必定是一气呵成、无法被打断的,这就避免了某个进程钻空子的现象。但是同时我们也发现,在 P0 时间片用完后,P1 仍然会占用处理机进行没有意义的死循环,也就是仍然违背了“让权等待”的原则

于是在此基础上,又出现了记录型信号量

2)记录型信号量

与整型信号量仅用一个单一变量记录可用资源数不同,记录型信号量的数据结构类似于一个结构体,它不仅记录了可用资源数 value,而且还提供了一个等待队列 L

记录型信号量的思想是:

如果由于 P0 在使用临界资源而导致 P1 暂时无法使用,那么干脆就不给 P1 陷入循环的机会,直接让它自己去阻塞队列,这样 P1 在被唤醒之前,永远无法占用处理机,也自然就不可能出现白白占用处理机的情况。而在 P0 释放资源后,我们才来考虑唤醒 P1。

记录型信号量的结构如下所示:

typedef struct {
    int value
    sturct process *L
} semaphore

同时,记录型信号量的 P、V 操作也有所不同,如下所示:

wait (semaphore S){
    S.value--
    if(S.value < 0){
        block(S.L)
    }
}
signal(semaphore S){
    S.value++
    if(S.value <= 0){
        wakeup(S.L)
    }
}
  • 这里要注意的第一个地方是,value 是可用的资源数,当它大于 0 的时候自然是存在可用资源(供大于求),当它小于 0 的时候,则说明不仅无可用资源而且有其他进程等着用(供不应求)。
  • 第二个地方是,在进入区 value 一定会减一,表示申请到了资源,或者表示存在着某个进程有想要申请资源的意愿

下面我们用例子来说明记录型信号量工作的过程,为了加深记忆,这里用四个进程来说明:

PO:            P1              P2           P3
wait(S)        wait(S)         wait(S)      wait(S)
临界区          临界区          临界区        临界区
signal(S)      signal(S)       signal(S)    signal(S)

假设计算机中有两台可用的打印机 A 和 B(也就是说,value = 2),有四个进程需要用到打印机资源。

处理流程:

1.  假定是 P0 先占有处理机,那么 P0 就会在进入区申请资源 。由于 value 一开始是 2,所以 P0 成功申请到资源 A,之后 value 数量减一变为 1,同时来到临界区开始“干活”

2. 在 P0 的时间片完了之后,P1 占有处理机,此时同样申请到资源 B,value 由 1 变为 0,之后来到临界区“干活”。自此,两个打印机都被占用了。

3. 在 P1 的时间片完了之后,P2 占有处理机,value 由 0 变为 -1 < 0,前面我们说过,value < 0 说明无可用资源,所以此时 P2 将自己主动送到了阻塞队列;

3.  P3占有处理机,value 由 -1 变为 -2,P3 同样进入阻塞队列。P2,P3 都从运行态转为阻塞态。

4. 处理机又来到 P0,P0 很快执行完了,于是在退出区执行 P 操作释放资源,将 value 加一变为 -1,之后由于通过 if 检测到阻塞队列中有进程等着用资源,所以马上唤醒了队头的 P2 ,P2 从阻塞态回到就绪态,并直接进入临界区开始自己的工作,在完成后同样来到退出区释放资源,value 由 -1 变为 0,但是在 if 中还是检测到了队列中仍然有进程等着用资源,于是马上把队头的 P3 唤醒,P3 回到就绪态,并直接进入临界区开始工作,此后,value 由 0 变为 1,此时 if 不通过,说明队列中再也没有其它进程等着了,该拿到资源的进程都拿到了。

5. 自此,P0,P2,P3 都拿到了 A 资源,而 P1 也在不久后完成工作,在退出区释放资源 B,此时 value 从 1 变回最初的 2 ,代表占用的资源已经全数归还。

PS:当然,实际情况还可能是,P2 拿到了 A 资源,P3 拿到了 B 资源,但分析过程也是大同小异的。

显然,记录型信号量与前面介绍的所有方法最大的区别就在于,不会再有进程白白占用处理机进行没有意义的循环 —— 相反地,这些进程非常“老实”地把自己送到了阻塞队列,选择在那里慢慢地等待,等待其它进程完成后将自己唤醒,这种做法“既方便了别人,也方便了自己”。这就正好与我们多次强调的”让权等待“非常契合了。

记录型信号量明显优于整型信号量,所以在提到 PV 操作的时候,一般默认指的都是记录型信号量。 

3)进程间的同步

同步方式有:信号量、管程

信号量机制如上2节所示。‘

管程机制:

尽管信号量机制很方又高效,但是每个要访问临界资源的进程都必须必备同步操作,这就使得大量的同步操作分散在各个进程中。这不仅给系统的管理带来麻烦,还会因同步操作不当而产生死锁。为了解决上述问题,变产生了一种新的同步工具——管程。

管程有四部分组成:1.管程的名称;2.共享数据结构说明;3.对数据结构进行操作的一组过程;4.初始化语句。下面我们来看下管程的语法描述:

//管程的描述  
Monitor monitor_name {//管程名  
share variable declarations; //共享变量说明  
cond cond_declarationas; //条件变量说明  
public: //能被进程调用的过程  
void P1(...){ //对数据结构操作过程  
...  
}  
void P2(...){  
...  
}  
...  
void(...){  
...  
}  
...  
{  
initilization code; //初始化代码  
}  
} 

Java中类似于管程的机制

在Java中,如果使用关键字synchronize来描述一个函数,那么这个函数同一时间只能被一个进程调用。

如果多个线程同时调用函数,则后来者需要排队等待。

1.5 生产者 — 消费者问题

生产者 — 消费者问题描述的是一个对产品生产和消费的过程:

1. 首先,对于生产者,如果缓冲区没满,它就会生产产品并将其放入缓冲区,如果缓冲区已满,它就会进行等待;

2. 对于消费者,如果缓冲区不空,它就会取走产品并释放缓冲区,如果缓冲区已空,它就会进行等待。

对于这个问题,不难看出有两个进程,一个是生产者进程,一个是消费者进程;

同时有一个互斥关系,在同一时间内,只能有一个进程访问同一个缓冲区,要么放入产品,要么取走产品;

同时有两个同步关系,一个指的是:必定是先生产产品,后取走产品,另一个指的是:必定是先释放缓冲区,后使用缓冲区。

因此,我们在这里需要准备两个进程:一个是表示生产者进程的 producer,一个是表示消费者进程的 consumer

同时,我们需要准备三个信号量:第一个信号量是互斥信号量,实现对缓冲区这个资源的互斥访问,用 mutex = 1 表示;第二个信号量是同步信号量,表示空闲缓冲区的数量,用 empty = n 表示;第三个信号量也是同步信号量,表示非空闲缓冲区的数量,也即产品数量,用 full = 0 表示。

先考虑对互斥关系的实现。这里所谓的临界资源其实就是某一个缓冲区,生产者进程把产品放进去,消费者进程从里面取走产品,这两者不能同时进行,所以这里是互斥的关系。我们可以想象到,对每一个进程而言,他都有属于自己的一对 PV 操作,用以实现对缓冲区资源的访问。另外,生产者在进行 PV 操作之前,必定要先生产产品;而消费者在进行 PV 操作之后,必定要使用产品。这时候,初步的伪代码是这样的:

producer(){                         consumer(){
    while(1){                           while(1){
        生产产品                              P(mutex)
        P(mutex)                             从缓冲区中取走产品 
        把产品放入缓冲区                       V(mutex)
        V(mutex)                             使用产品 
    }                                   }     
}                                   }

接着考虑第一个同步关系。关注缓冲区,可以知道一定是先释放缓冲区,后使用缓冲区,所以这里“前操作”是释放缓冲区,“后操作”是使用缓冲区,根据上篇笔记所讲的 “前VP后”,我们需要在“前操作”之后针对 empty 这个信号量进行一次 V 操作,需要在“后操作”之前针对 empty 进行一次 P 操作。所以,这时候代码变成:

producer(){                         consumer(){
    while(1){                           while(1){
        生产产品                             P(mutex)
        P(empty)                            从缓冲区中取走产品 
        P(mutex)                            V(mutex)
        把产品放入缓冲区                      V(empty)  
        V(mutex)                            使用产品 
    }                                   }     
}                                   }

再考虑第二个同步关系。关注产品本身,可以知道一定是先生产产品,后使用产品,更进一步地说,一定是先生产产品并将其放入缓冲区,后从缓冲区取出产品并使用。这里划分出前后两个操作,所以再次安排一对 PV 操作。这时候,代码变成:

producer(){                         consumer(){
    while(1){                           while(1){
        生产产品                            P(full)
        P(empty)                           P(mutex)
        P(mutex)                           从缓冲区中取走产品 
        把产品放入缓冲区                     V(mutex) 
        V(mutex)                           V(empty)  
        V(full)                            使用产品 
    }                                   }     
}                                   }

这个实际上就是最后的代码了。现在我们试着跑一下流程:初始的时候 empty = n,表示所有缓冲区都是空闲的,同时 full = 0,表示一个产品都没生产出来。假如处理机首先来到 consumer 进程,那么就会通过 P(full) 检查是否有产品,这里当然是没有,所以它只能进行等待;处理机来到 producer,首先通过 P(empty) 检查是否有空闲缓冲区,这里当然有,于是它开始把生产的产品放入缓冲区,随后记录产品的数量,这个过程可以反复进行,直到所有缓冲区都被占用了,此时 producer 就会进入等待状态,等待 consumer 进程取出产品、释放缓冲区;当然还有可能的情况是,producer 尚未占用完所有缓冲区,进程就切换到 consumer 了,那么这时候 consumer 因为检查到有产品,所以也会取出产品、释放缓冲区。

1.6 六种CPU调度算法

在多道程序中,如果长期调度程序选择更多的I/O绑定进程,那么大多数时候CPU仍然是空闲的。 操作系统的任务是优化资源的利用。

如果大多数正在运行的进程将其状态从运行状态更改为等待状态,那么系统中可能始终存在死锁。 因此,为了减少这种开销,操作系统需要调度作业以获得CPU的最佳利用率并避免死锁的可能性。

0)CPU调度追求的目标:

  • CPU利用率(CPU utilization)。CPU的利用率就是非空闲进程占用时间的比例,即CPU执行非空闲进程的时间/ CPU总的执行时间。
  • 吞吐率(Throughput) — 单位时间内完成执行的进程数
  • 周转时间(Turnaround time) — 执行某一进程所耗用的CPU累积时间(进程进入就绪队列开始到拿到CPU执行结束为止的累积时间,其间有可能存在时间片到了或者高优先级抢占CPU的情况)
  • 等待时间(Waiting time) — 某一进程等待在就绪队列里面的累积时间(不包括CPU执行时间)
  • 响应时间(Response time) — 某一进程从发出调度请求,到其开始得到CPU调度器响应,其间所经历的时间。

1)FCFS调度算法(First-Come,First-Served Scheduling)先来先服务

  • 这种实现比较简单,先进入就绪队列的进程最先处理。处理方式自然是将这些进程构成一个链表,先处理先进入就绪队列的进程,然后根据next指针一步步往后。
  • Burst time,即中央处理器突发时间。是指CPU从接到命令到开始处理命令所需时间。

假设进程到达就绪队列的顺序:P2,P3,P1,则FCFS调度算法的调度结果有显著变化,如甘特图:

P2,P3,P1顺序的等待时间甘特图.png

  • 等待时间(某一进程在就绪队列里面的等待时间) P1=6,P2=0,P3=3。平均等待时间(6+0+3)/3 = 3,改善非常多。

启示:短进程先于长进程,减少等待时间。
问题:时间波动很大,不稳定。

算法优点:易于理解且实现简单,只需要一个队列(FIFO),并且相对公平。

算法缺点:有利于长进程的执行,而不利于短进程,有利于计算密集型进程(CPU密集型),而不利于I/O密集型进程。

2)Shortest-Job-First(SJF)调度算法(最短作业优先算法)

抢占式SJF算法

抢占式SJF算法.png

  • 基本理论为:更短时间的进程抢占更长时间的进程。
  • 首先,P1于0时刻进入CPU,执行2秒。
  • 紧接着,P2时间更短,进入CPU,抢占P1的CPU,又执行2秒。
  • P3于4时刻进入就绪队列,Burst time更短,抢占P2的CPU,执行一秒结束。
  • P4于5时刻进入CPU,Burst time为4,由于P2已经执行了2秒,因此它的Burst time更短,所以先执行P2,P2执行完毕,交出CPU。
  • 由于P1还剩下5秒执行时间,因此先执行P4,最后执行P1。
  • 周转时间(16+5+5+11)=37

平均等待时间 = (9+1+0+2)/4 = 3。
平均等待时间算法可以由(周转时间-Burst time)得出。
具体写下来就是:
((16-7)+(5-4)+(1-1)+(6-4))=3,即(周转时间-Burst time)。

SJF算法的缺点

  • CPU Burst time必须很准确,才能确定这种算法。然而实际情况下基本不可能预估准确时间。(预先通报是不可能做到的,这是一个致命问题,导致SJF算法无法实现)。

算法优点:相比于FCFS算法,SJF算法可改善平均周转时间和平均带权周转时间,缩短进程的等待时间,提高系统的吞吐量。

算法缺点:对长进程非常不利,可能长时间不能执行,而且不能根据进程的紧迫程度来划分进程执行的优先级,也很难准确预计进程的执行时间,从而降低调度性能。

3)轮转法(Round Robin,RR)

轮转法是交互式操作系统必要的条件。

具体步骤:

  • 每个就绪进程获得一小段CPU时间(时间片,time quantum),通常10ms - 100ms
  • 时间片用毕,这个进程被迫交出CPU,重新挂回到就绪队列
  • 当然,进程在时间片用毕之前其Burst Cycle结束,也(主动)交出CPU
  • 假设n个就绪进程,时间片q,每个就绪进程得到1/n的CPU时间。任何就绪进程最多等待(n-1)q单位时间。

举例:RR算法,时间片设定为20个单位

 

  • 由图可见,时间片一到,只要不是在时间片内处理完进程,那么就将交出时间片。然后回到了链表的末尾。
  • 分配给同一个进程(只有一个进程)和分配给不同进程的情况有一个细微的区别:不需要做上下文切换(进程保存与转移),也就是说不需要额外开销。

轮转法的问题
当时间片到了以后,一个进程要交出CPU。如果交给其他进程,则要做一次进程上下文切换。

由图进行性能分析

  • 如果q很大,那么就很像FIFO算法。
  • 如果q很小,那么上下文切换频繁,则额外开销太大,q必须远远大于上下文切换时间。

周转时间受时间片的影响

 

  • 周转时间 — 执行某一进程所耗用的CPU累积时间(进程进入就绪队列开始到拿到CPU执行结束为止的累积时间,其间有可能存在时间片到了或者高优先级抢占CPU的情况)
  • 当时间片大于等于6时,平均周转时间稳定在10.5。因此建议用6或者6以上的时间片长度。

轮转法还有一个好处,就是他的响应时间一定优于前面的SJF。因为时间片的存在。

算法优点:时间片轮转调度算法的优点是简单易行,平均响应时间短。

算法缺点:不利于处理紧急进程。在时间片轮转算法中,时间片的大小对系统性能的影响很大,因此时间片的大小要选择恰当。

4)最高响应比优先算法(HRRN,Hight Response Ratio Next)

最高响应比优先算法是对FCFS(first come first service先来先服务)和SJF(short job first最短时间调度)的一种平衡算法。FCFS值考虑了进程等待的时间长短,而SJF值考虑了进程执行的时间长短,因此这两种算法在某种程度上会降低系统调度性能。HRRN这种算法既会考虑每个进程的等待时间长短,也会考虑进程预计执行时间长短从中选出响应比最高的进程执行。这种算法是介于FCFS算法和SJF算法中间的一种这种算法。

如果我们能为每个作业引入前面所述的动态优先权,并使作业的优先级随着等待时间的增加而以速率a 提高,则长作业在等待一定的时间后,必然有机会分配到处理机。该优先权的变化规律可描述为:



由于等待时间与服务时间之和就是系统对该作业的响应时间,故该优先权又相当于响应比RP。据此,又可表示为:

(1) 如果作业的等待时间相同,则要求服务的时间愈短,其优先权愈高,因而该算法有利于短作业.

(2) 当要求服务的时间相同时,作业的优先权决定于其等待时间,等待时间愈长,其优先权愈高,因而它实现的是先来先服务.

(3) 对于长作业,作业的优先级可以随等待时间的增加而提高,当其等待时间足够长时,其优先级便可升到很高, 从而也可获得处理机.

算法优点:对短进程比较有利,但是长进程随着等待的时间增加,也能被执行。该算法照顾了短作业,且不会使长作业长期得不到服务。

算法缺点:由于每次调度前要计算响应比,增大了系统开销。

5)优先级调度算法

为了照顾紧迫型作业,使之在进入系统后便获得优先处理,引入了最高优先权优先(FPF)调度算法。

此算法常被用于批处理系统中,作为作业调度算法,也作为多种操作系统中的进程调度算法,还可用于实时系统中。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程,这时,又可进一步把该算法分成如下两种:

5-1) 非抢占式优先权算法

在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先权最高的进程。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。

 

5-2) 抢占式优先权调度算法

在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。因此,在采用这种调度算法时,是每当系统中出现一个新的就绪进程i 时,就将其优先权Pi与正在执行的进程j 的优先权Pj进行比较。如果Pi≤Pj,原进程Pj便继续执行;但如果是Pi>Pj,则立即停止Pj的执行,做进程切换,使i 进程投入执行。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。

6)多级反馈队列

多级反馈队列调度算法是一种能使高优先级的作业得到响应又能使短作业(进程)迅速完成。

多级反馈队列
图 1 多级反馈队列


例如,一个多级反馈队列的调度程序有三个队列,从 0 到 2(图 1)。调度程序首先执行队列 0 内的所有进程。只有当队列 0 为空时,它才能执行队列 1 内的进程。类似地,只有队列 0 和 1 都为空时,队列 2 的进程才能执行。到达队列 1 的进程会抢占队列 2 的进程。同样,到达队列 0 的进程会抢占队列 1 的进程。

每个进程在进入就绪队列后,就被添加到队列 0 内。队列 0 内的每个进程都有 8ms 的时间片。如果一个进程不能在这一时间片内完成,那么它就被移到队列 1 的尾部。如果队列 0 为空,队列 1 头部的进程会得到一个 16ms 的时间片。如果它不能完成,那么将被抢占,并添加到队列 2。只有当队列 0 和 1 为空时,队列 2 内的进程才可根据 FCFS(先来先服务的原则)来运行。

这种调度算法将给那些 CPU 执行不超过 8ms 的进程最高优先级。这类进程可以很快得到 CPU,完成 CPU 执行,并且处理下个 I/O 执行。

所需超过 8ms 但不超过 24ms 的进程也会很快得以服务,但是它们的优先级要低一点。长进程会自动沉入队列 2,队列 0 和 1 不用的 CPU 周期按 FCFS 顺序来服务。

各个队列的时间片是一样的吗?不一样,这就是该算法设计的精妙之处。

各个队列的时间片是随着优先级的增加而减少的,也就是说,优先级越高的队列中它的时间片就越短。同时,为了便于那些超大作业的完成,最后一个队列QN(优先级最低的队列)的时间片一般很大(不需要考虑这个问题)。

通常,多级反馈队列调度程序可由下列参数来定义:

  1. 队列数量。
  2. 每个队列的调度算法。
  3. 用以确定何时升级到更高优先级队列的方法。
  4. 用以确定何时降级到更低优先级队列的方法。
  5. 用以确定进程在需要服务时将会进入哪个队列的方法。


多级反馈队列调度程序的定义使其成为最通用的 CPU 调度算法。

1.7 进程间的通信方式

什么是进程间的通信方式:

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。

1)匿名管道

管道/匿名管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用

进程的亲缘关系通常是指父子进程关系

通过匿名管道实现进程间通信的步骤如下:

  • 父进程创建管道,得到两个⽂件描述符指向管道的两端
  • 父进程fork出子进程,⼦进程也有两个⽂件描述符指向同⼀管道。
  • 父进程关闭fd[0],子进程关闭fd[1],即⽗进程关闭管道读端,⼦进程关闭管道写端(因为管道只支持单向通信)。⽗进程可以往管道⾥写,⼦进程可以从管道⾥读,管道是⽤环形队列实现的,数据从写端流⼊从读端流出,这样就实现了进程间通信。

这里写图片描述

管道的主要局限性正体现在它的特点上:

  • 只支持单向数据流;
  • 只能用于具有亲缘关系的进程之间;
  • 没有名字;
  • 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);
  • 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等;

2)有名管道(FIFO)

有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO)。
有名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中,这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据。值的注意的是,有名管道严格遵循先进先出(first in first out),对匿名管道及有名管道的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。有名管道的名字存在于文件系统中,内容存放在内存中。

匿名管道和有名管道总结:
(1)管道是特殊类型的文件,在满足先入先出的原则条件下可以进行读写,但不能进行定位读写。
(2)匿名管道是单向的,只能在有亲缘关系的进程间通信;有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
(3)无名管道阻塞问题:无名管道无需显示打开,创建时直接返回文件描述符,在读写时需要确定对方的存在,否则将退出。如果当前进程向无名管道的一端写数据,必须确定另一端有某一进程。如果写入无名管道的数据超过其最大值,写操作将阻塞,如果管道中没有数据,读操作将阻塞,如果管道发现另一端断开,将自动退出。
(4)有名管道阻塞问题:有名管道在打开时需要确实对方的存在,否则将阻塞。即以读方式打开某管道,在此之前必须一个进程以写方式打开管道,否则阻塞。此外,可以以读写(O_RDWR)模式打开有名管道,即当前进程读,当前进程写,不会阻塞。

3)信号(Signal)

信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

  • 信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
  • 如果该进程当前并未处于执行状态,则该信号就有内核保存起来,知道该进程回复执行并传递给它为止。
  • 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。

Linux系统中常用信号:
(1)SIGHUP:用户从终端注销,所有已启动进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程。
(2)SIGINT:程序终止信号。程序运行过程中,按Ctrl+C键将产生该信号。
(3)SIGQUIT:程序退出信号。程序运行过程中,按Ctrl+\\键将产生该信号。
(4)SIGBUS和SIGSEGV:进程访问非法地址。
(5)SIGFPE:运算中出现致命错误,如除零操作、数据溢出等。
(6)SIGKILL:用户终止进程执行信号。shell下执行kill -9发送该信号。
(7)SIGTERM:结束进程信号。shell下执行kill 进程pid发送该信号。
(8)SIGALRM:定时器信号。
(9)SIGCLD:子进程退出信号。如果其父进程没有忽略该信号也没有处理该信号,则子进程退出后将形成僵尸进程。

信号来源
信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件主要有两个来源:

  • 硬件来源:用户按键输入Ctrl+C退出、硬件异常如无效的存储访问等。
  • 软件终止:终止进程信号、其他进程调用kill函数、软件异常产生信号。

信号生命周期和处理流程
(1)信号被某个进程产生,并设置此信号传递的对象(一般为对应进程的pid),然后传递给操作系统;
(2)操作系统根据接收进程的设置(是否阻塞)而选择性的发送给接收者,如果接收者阻塞该信号(且该信号是可以阻塞的),操作系统将暂时保留该信号,而不传递,直到该进程解除了对此信号的阻塞(如果对应进程已经退出,则丢弃此信号),如果对应进程没有阻塞,操作系统将传递此信号。
(3)目的进程接收到此信号后,将根据当前进程对此信号设置的预处理方式,暂时终止当前代码的执行,保护上下文(主要包括临时寄存器数据,当前程序位置以及当前CPU的状态)、转而执行中断服务程序,执行完成后在回复到中断的位置。当然,对于抢占式内核,在中断返回时还将引发新的调度。

4)消息队列(Message Queue)

 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

  • 消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。
  • 与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
  • 另外与管道不同的是,消息队列在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达。

消息队列特点总结:
(1)消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识.
(2)消息队列允许一个或多个进程向它写入与读取消息.
(3)管道和消息队列的通信数据都是先进先出的原则。
(4)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比FIFO更有优势。
(5)消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。
(6)目前主要有两种类型的消息队列:POSIX消息队列以及System V消息队列,系统V消息队列目前被大量使用。系统V消息队列是随内核持续的,只有在内核重起或者人工删除时,该消息队列才会被删除。

5)信号量(emaphore)

信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。
为了获得共享资源,进程需要执行下列操作:
(1)创建一个信号量:这要求调用者指定初始值,对于二值信号量来说,它通常是1,也可是0。
(2)等待一个信号量:该操作会测试这个信号量的值,如果小于0,就阻塞。也称为P操作。
(3)挂出一个信号量:该操作将信号量的值加1,也称为V操作。

为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作

为此,信号量通常是在内核中实现的。

Linux环境中,有三种类型:Posix有名信号量(使用Posix IPC名字标识)Posix基于内存的信号量(存放在共享内存区中)System V信号量(在内核中维护)。这三种信号量都可用于进程间或线程间的同步。

两个进程使用一个二值信号量

 

两个进程所以用一个Posix有名二值信号量

一个进程两个线程共享基于内存的信号量

信号量与普通整型变量的区别:
(1)信号量是非负整型变量,除了初始化之外,它只能通过两个标准原子操作:wait(semap) , signal(semap) ; 来进行访问;
(2)操作也被成为PV原语(P来源于荷兰语proberen"测试",V来源于荷兰语verhogen"增加",P表示通过的意思,V表示释放的意思),而普通整型变量则可以在任何语句块中被访问;

信号量与互斥量之间的区别:
(1)互斥量用于线程的互斥,信号量用于线程的同步。这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。
在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源
(2)互斥量值只能为0/1,信号量值可以为非负整数。
也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。
(3)互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。

 

6) 共享内存(share memory)

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC (Inter Process Communication,进程间通信)方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量来配合使用,来实现进程间的同步和通信。

  • 使得多个进程可以可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。
  • 为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。
  • 由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。

 


7. 套接字(Socket)
 

套接字是一种通信机制,凭借这种机制,为客户进程、服务器进程系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。

Socket是应用层和传输层之间的桥梁

 

套接字是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。

套接字特性
套接字的特性由3个属性确定,它们分别是:域、端口号、协议类型。
(1)套接字的域
它指定套接字通信中使用的网络介质,最常见的套接字域有两种:
一是AF_INET,它指的是Internet网络。当客户使用套接字进行跨网络的连接时,它就需要用到服务器计算机的IP地址和端口来指定一台联网机器上的某个特定服务,所以在使用socket作为通信的终点,服务器应用程序必须在开始通信之前绑定一个端口,服务器在指定的端口等待客户的连接。
另一个域AF_UNIX,表示UNIX文件系统,它就是文件输入/输出,而它的地址就是文件名。
(2)套接字的端口号
每一个基于TCP/IP网络通讯的程序(进程)都被赋予了唯一的端口和端口号,端口是一个信息缓冲区,用于保留Socket中的输入/输出信息,端口号是一个16位无符号整数,范围是0-65535,以区别主机上的每一个程序(端口号就像房屋中的房间号),低于256的端口号保留给标准应用程序,比如pop3的端口号就是110,每一个套接字都组合进了IP地址、端口,这样形成的整体就可以区别每一个套接字。
(3)套接字协议类型
因特网提供三种通信机制:
一是流套接字,流套接字在域中通过TCP/IP连接实现,同时也是AF_UNIX中常用的套接字类型。流套接字提供的是一个有序、可靠、双向字节流的连接,因此发送的数据可以确保不会丢失、重复或乱序到达,而且它还有一定的出错后重新发送的机制。
二个是数据报套接字,它不需要建立连接和维持一个连接,它们在域中通常是通过UDP/IP协议实现的。它对可以发送的数据的长度有限制,数据报作为一个单独的网络消息被传输,它可能会丢失、复制或错乱到达,UDP不是一个可靠的协议,但是它的速度比较高,因为它并一需要总是要建立和维持一个连接。
三是原始套接字,原始套接字允许对较低层次的协议直接访问,比如IP、 ICMP协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为RAW SOCKET可以自如地控制Windows下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。比如,我们可以通过RAW SOCKET来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包。网络监听技术很大程度上依赖于SOCKET_RAW。

原始套接字与标准套接字的区别在于:
原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。

套接字通信的建立

Socket通信基本流程

 

** 服务器端**
(1)首先服务器应用程序用系统调用socket来创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,它不能与其他的进程共享。
(2)然后,服务器进程会给套接字起个名字,我们使用系统调用bind来给套接字命名。然后服务器进程就开始等待客户连接到这个套接字。
(3)接下来,系统调用listen来创建一个队列并将其用于存放来自客户的进入连接。
(4)最后,服务器通过系统调用accept来接受客户的连接。它会创建一个与原有的命名套接不同的新套接字,这个套接字只用于与这个特定客户端进行通信,而命名套接字(即原先的套接字)则被保留下来继续处理来自其他客户的连接(建立客户端和服务端的用于通信的流,进行通信)。

客户端
(1)客户应用程序首先调用socket来创建一个未命名的套接字,然后将服务器的命名套接字作为一个地址来调用connect与服务器建立连接。
(2)一旦连接建立,我们就可以像使用底层的文件描述符那样用套接字来实现双向数据的通信(通过流进行数据传输)。

 

1.8 进程通信和进程同步的区别

进程互斥、同步的概念是并发进程下存在的概念,有了并发进程,就产生了资源的竞争与协作,从而就要通过进程的互斥、同步、通信来解决资源的竞争与协作问题。

 

在多道程序设计系统中,同一时刻可能有许多进程,这些进程之间存在两种基本关系:竞争关系和协作关系。

进程的互斥、同步、通信都是基于这两种基本关系而存在的。

为了解决进程间竞争关系(间接制约关系)而引入进程互斥;

为了解决进程间松散的协作关系( 直接制约关系)而引入进程同步;

为了解决进程间紧密的协作关系而引入进程通信。

进程同步的目的是:对多个相关进程在 执行次序上进行协调,以使并发执行的各个进程之间能有效地共享资源和相互合作,从而使 程序的执行具有可再现性。

例如,当多个进程去争用一台打印 机时,有可能使多个进程的输出结果交织在一起,难于区分;而当多个进程去争用共享变 量、表格、链表时,有可能致使数据处理出错。

进程通信的概念

进程用户空间是相互独立的,一般而言是不能相互访问的。但很多情况下进程间需要互相通信,来完成系统的某项功能。进程通过与内核及其它进程之间的互相通信来协调它们的行为。

进程通信的应用场景:

数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。

共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。

通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。

进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

1. 进程通信包含了进程同步的功能,比如在多个进程共享同样的资源。

2. 进程通信拥有更多的功能,如时间通知,数据传输。而进程同步主要是在进程并发执行条件下,为了保证共享资源的顺序执行。

 

1.9 死锁

1)什么是死锁

死锁现象指的是:在并发环境下,两个或者以上的进程由于竞争资源而造成的一种互相等待(你等我,我等你)的现象,在这种情况下,A 进程拿着 A 资源,需要 B 资源,B 进程拿着 B 资源,需要 C 资源 …… 各个进程互相等待,都会被阻塞,无法继续向前推进。

所占用的资源或者需要它们进行某种合作的其它进程就会相继陷入死锁,最终可能导致整个系统处于瘫痪状态。 

如经典的哲学家就餐问题:

针对上面银行家算法,防止死锁发生可采取的措施:

(1)最多允许4个哲学家同时坐在桌子周围。
(2)仅当一个哲学家左右两边的筷子都可用时,才允许他拿筷子。

(3)给所有哲学家编号,奇数号的哲学家必须首先拿左边的筷子,偶数号的哲学家则反之。

这里给出一个不会发生死锁的哲学家进餐过程的算法描述。

 

semaphore S[5] = {1,1,1,1,1};//5双筷子
semaphore mutex = 4;//最多允许4个哲学家
Pi()
{
    while(1)
    {
        P(mutex);
        P(S[i]);
        拿起左边的叉子;
        P(S[i+1]mod5);
        拿起右边的叉子;
        吃通心粉;
        放下左边的叉子;
        V(S[i]);
        放下右边的叉子;
        V(S[i+1]mod5);
        V(mutex);
    }
}

 

2)什么是 死锁,饥饿,死循环

3)死锁的必要条件

只有同时满足以下四个条件,才会发生死锁现象:

① 互斥:

要求进程竞争的资源必须是互斥使用的资源。因为如果是可以共享使用的资源,多个进程直接同时使用就好,不会陷入等待的僵局。

② 非抢占:

要求进程占有的资源只能由进程使用完之后自己主动释放,其它进程不能抢占该资源。因为如果其它进程可以抢占资源,那么就是直接拿到资源了,也不会陷入等待的僵局。

③ 占有和请求:

要求进程是在占有(holding)至少一个资源的前提下,请求(waiting)新的资源的。由于新的资源被其它进程占有,此时,发出请求的进程就会带着自己占有的资源进入阻塞状态。假设 P1,P2 分别都需要 R1,R2 资源,如果是下面这种方式:

P1:          P2:
request(R1)  request(R2)
request(R2)  request(R1) 

如果 P1 请求到了 R1 资源之后,P2 请求到了 R2 资源,那么此后不管是哪个进程再次请求资源,都是在占有资源的前提下请求的,此时就会带着这个资源陷入阻塞状态。P1 和 P2 需要互相等待,发生了死锁。

换一种情况:

P1:          P2:
request(R1)  request(R1)
request(R2)  request(R2) 

如果 P1 请求到了 R1 资源,那么 P2 在请求 R1 的时候虽然也会阻塞,但是是在不占有资源的情况下阻塞的,不像之前那样占有 R2。所以,此时 P1 可以正常完成任务并释放 R1,P2 拿到 R1 之后再去执行任务。这种情况就不会发生死锁。

④ 循环等待:

要求存在一条进程资源的循环等待链,链中的每一个进程占有的资源同时被另一个进程所请求。

发生死锁时一定有循环等待(因为是死锁的必要条件),但是发生循环等待的时候不一定会发生死锁。这是因为,如果循环等待链中的 P1 和 链外的 P6 都占有某个进程 P2 请求的资源,那么 P2 完全可以选择不等待 P1 释放该资源,而是等待 P6 释放资源。这样就不会发生死锁了。

4)死锁的预防

死锁的预防是通过破坏产生死锁的必要条件之一,使系统不会产生死锁。简单方法是在系统运行之前就采取措施,即在系统设计时确定资源分配算法,消除发生死锁的任何可能性。该方法虽然比较保守、资源利用率低,但因简单明了并且安全可靠,仍被广泛采用。这是一种预先的静态策略。

破坏互斥条件

 

破坏不可剥夺条件

 

破坏请求和保持条件

破坏循环等待条件

产生死锁的四个必要条件中,互斥条件和不可剥夺条件由共享资源本身的使用特性所决定的,因此不好破坏,相反还应加以保证,实用的死锁预防办法就是通过破坏产生死锁的占用并请求条件和循环等待条件。

 

---------------------------  end  ---------------------------

项目推荐:

2000多G的计算机各行业电子资源分享(持续更新)

2020年微信小程序全栈项目之喵喵交友【附课件和源码】

Spring Boot开发小而美的个人博客【附课件和源码】

Java微服务实战296集大型视频-谷粒商城【附代码和课件】

Java开发微服务畅购商城实战【全357集大项目】-附代码和课件

最全最详细数据结构与算法视频-【附课件和源码】

在这里插入图片描述

-----------------------------------------------------------------------------

 

 

 

  • 1
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值