操作系统(二)进程管理

操作系统 - 进程管理

进程与线程

1. 进程

进程是程序(进程实体)的一次执行过程,是系统自行资源分配的独立单位,在线程出现之前也是系统调度的基本单位。

进程的特征

  • 动态性:进程是程序的一次执行过程,具有一定的生命期,而程序则只是一组有序指令的集合

  • 并发性:OS的基本特征,各进程之间是可以并发执行的

  • 独立性:进程是一个能独立运行、 分配资源和调度的基本单位

  • 异步性:OS的基本特征,各进程按独立的、不可预知的速度向前推进

  • 结构特征:由程序段、相关的数据段和 PCB 三部分构成进程实体(进程映像)

为什么要引入进程?

为了并发,例如,在一个未引入进程的系统中,在属于同一个应用程序的计算子程序和 I/O 子程序之间,两者只能是顺序执行,但在引入进程后,分别为计算程序和 I/O 程序各建立一个进程,则这两个进程便可并发执行,等待计算完成时可以执行IO,等待IO完成时亦可执行计算。

程序与进程?

一个「程序 program 」是一个正在执行着的「进程 process」,永远不要说一个程序可能有多个进程。

程序是一个静态实体,由机器指令和资源文件组成,当它被分配了内存资源(PCB、堆、栈)、处理机资源后就是一个进程。

程序和应用程序?

程序 ≠ 应用程序,但中文里我们对两个词好像区分度不高,看下英语就清楚了,「程序 program」和「应用程序 application」

进程与进程?

对于同一个程序的打开两次,它们是两个进程,没有关联,具有各自的PCB,但是它们的代码段是共享的

对于一个应用程序(比如chrome),会有十多个进程,其实就是十多个程序,它们都是独立的,操作系统给他们分配了彼此独立的内存,相互执行不受彼此约束,同等的机会竞争CPU

父进程与子进程?

进程作为资源分配的基本单位,有各自的内存块,是没有主从关系的,只有父子关系,但也只在fork的那一瞬间,之后它们之间是并发、独立运行的。

2. 线程

线程是处理机独立调度的基本单位,基本上不拥有资源(详见下文),隶属于同一个进程的线程共享进程资源。

为什么要引入线程?

引入进程是为了并发,引入线程是为了更好地并发

在引入线程之前,进程作为操作系统中可以拥有资源并作为独立运行和调度的基本单位。由于进程拥有自己的资源,故使调度开销较大。所以,人们提出了线程,线程基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效地提高系统内多个程序间并发执行的程度。

线程切换时,有可能会发生进程切换(隶属于两个进程的线程),但也有可能不发生进程切换(隶属于同一个进程的线程),总体而言每次切换所需的开销就变小了,也就能够让更多的线程参与并发。

线程拥有哪些资源?为什么需要拥有这些资源?

隶属于同一个进程的线程共享进程的资源(代码、数据,文件),线程本身拥有属于自己的一小部分资源

  • 线程 id:用于唯一的标识线程
  • 寄存器值:线程需要并发执行,则用于保存自身执行状态的PC和寄存器是必不可少的
  • 栈:用于保存其运行状态,程序运行路线和局部变量(堆是共享进程的)
  • 优先级:线程是处理机独立调度的基本单位,每个线程应当具有自己的调度优先级(并不确定,但我认为这个优先级应该不是有操作系统管理,因为线程太多了)
  • 错误返回码:不同的线程应该拥有自己的错误返回码变量

多线程和多任务?

多线程是指在一个程序(进程)可以定义多个线程,在多(核)处理器下可以同时运行它们,每个线程可以执行不同的任务。

多任务是针对操作系统而言的,代表操作系统可以同时执行多个应用程序;

3. 比较

**总述:**进程是分离的,所以效率低;线程是不分离的,所以效率高。(分离指共享资源还是各自管理自己的资源,这儿的分不分离、效率高低只是相较于对方而言)

以下表述为 多线程OS

  • 拥有资源:

    • 进程是系统资源分配的单位
    • 线程基本不拥有资源,可以访问隶属进程的资源
  • 调度

    • 进程不能被处理机调度
    • 线程是处理机独立调度的单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
  • 系统开销

    • 进程的切换,还可能需要切换虚拟地址空间(页表置换),以及切换之后对于 TLB 和 Cache 的影响
    • 线程的切换,只需要切换栈和硬件上下文(寄存器),开销很小
  • 并发性

    • 线程的并发性更好(切换开销小了,相同时间段中能够并发的程序就多了)
  • 通信方面

    • 线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC

注意

由此开始,后文虽然通篇都在描述的主角是「进程」,但只要当进程充当的角色是「处理机调度的基本单位」时,都可以类比为「线程」

进程组织

1. 进程的资源

进程是资源分配的基本单位,一般认为具有以下资源:

  • 程序段(代码段)

    程序段是允许共享的,但是程序中的临界区仍然需要互斥访问

  • 数据段

  • **全局,静态区:**已初始化的全局变量,static变量

  • **常量数据区:**常量

  • **BSS段:**未初始化的全局变量,static变量

  • **栈:**局部变量(每个线程独有,其余均共享)

  • **堆:**动态申请

每个进程都拥有两个堆栈:用户空间的堆栈和内核空间堆栈,分别供进程在用户态和系统态下使用

2. 进程控制块

进程控制块 (Process Control Block, PCB) 存储进程的基本信息和运行状态,是进程存在的唯一标志,所谓的创建进程和撤销进程,都是指对 PCB 的操作。创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤消而撤消。

因为 PCB 经常被系统访问,尤其是被运行频率很高的进程及分派程序访问, 所以 PCB 常驻内存,在虚拟存储技术中,换入换出的是进程的其他部分。 系统将所有的 PCB 组织成若干个链表(或队列),存放在操作系统中专门开辟的 PCB 区内。

PCB 应当包含哪些信息?

  • 【进程管理】:进程标识符、进程状态CPU状态信息(包括程序计数器、程序状态字、栈指针、通用寄存器等)、进程调度信息(如进程的优先数等)、链接指针(用于将PCB链入各种状态队列)等项信息
  • 【存储管理】:代码、数据、堆栈在内存和外存的地址、长度等信息
  • 【设备管理】:所需资源和已分配到的资源清单
  • 【文件管理】:用户文件描述符表,用来登记用户打开的各个文件,并可以通过它找到在内存的相应文件的FCB(如UNIX中的内存索引结点)

进程状态的切换

每种状态都会用一个「队列」维护,进入xx状态,便挂在xx队列尾部,某些 OS 也可能会根据阻塞原因的不同,存在多个阻塞队列。

关于就绪队列的实现方式?(其它队列同理)

按照数据结构的思维,显然是链式结构更适合管理,因为需要频繁的插入或删除,而且队列的长度是不确定的。但事实上目前很多OS是用顺序结构来实现这个队列的,其最主要的原因是因为Cache的工作机制。如果是用数组实现的,那么进程的调度就相当于遍历数组,地址连续,符合局部性原理;如果是链表实现的,遍历链表,链表各节点在物理内存上是不连续的,Cache 可能会需要频繁的进行调入调出,这种开销是非常大的。虽然使用数组会带来一些空间上的浪费,但这是值得的;

就绪状态:等待被调度,进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。一般有以下情况:

  • 刚创建的进程
  • 刚从阻塞态唤醒的进程
  • 正常情况下处理机(比如时间片用完,而不是因为请求I/O下处理机)

执行状态:进程获得CPU,其程序正在执行

阻塞状态:正在执行的进程由于发生某事件而暂时无法继续执行时,便放弃处理机,一般有以下情况:

  • 请求I/O
  • 申请缓冲空间

被阻塞的进程,自己不能动了,由谁来唤醒?

进程因为谁阻塞的,就由谁负责来唤醒。例如,进程需要使用打印机,向打印机发送请求,但是打印机忙,则进程会阻塞自己,待打印机空闲,唤醒该进程。

中断是进程切换的必要不充分条件

假如某一时刻发生了进程切换,则该时刻一定执行了处理机调度程序,而处理机调度程序属于操作系统内核程序,运行于管态,说明在该时刻之间处理机状态由目态转换到管态。因为中断是系统度由目态转换为管态的必要条件,所以在该时刻一定发生过中断,即中断是进程切换的必要条件。

而触发中断的情况有很多,访管中断,IO中断,显然中断不是进程切换的充分条件。

进程调度算法

0. 调度层次

对于批量型作业而言,通常需要经历作业调度(又称高级调度或长程调度)和进程调度(又称低级调度或短程调度)两个过程后方能获得处理机;对于终端型作业,则通常只需经过进程调度即可获得处理机。在较完善的操作系统中,为提高内存的利用率,往往还设置了中级调度(又称中程调度)。

a. 高级调度(作业调度)

从外存后备作业中挑选作业,分配内存、I/O设备等必要的资源,并建立相应的进程(即程序装入的过程)

作业 Job 是一个总任务,由用户提交的,进程是作业的各个子项,由系统创建的,提交了作业系统才会创建进程

b. 中级调度(内存调度)

将那些暂时不能运行的进程调至外存上去等待,当这些进程重又具备运行条件且内存有空闲时,再由中级调度决定哪个进程重新调入内存

中级调度实际上就是存储器管理中的对换功能,为了提高内存利用率和系统吞吐量

c. 低级调度(进程调度)

低级调度用于决定就绪队列中的哪个进程(或内核级线程)应获得处理机,即本小节的内容

d. 比较与联系
  • 作业调度为进程活动做准备,进程调度使进程正常活动起来,中级调度将暂时不能运行的进程挂起
  • 作业调度次数少,中级调度次数略多,进程调度频率最高

为了合理地处理计算机的软硬件资源,故需要进行处理机调度(进程调度)。

若没有处理机调度,意味着要等到当前运行的进程执行完毕后,下一个进程才能执行(单批道处理系统),而实际情况中,进程时常需要等待一些外部设备的输入,而外部设备的速度与处理机相比是非常缓慢的,若让处理机总是等待外部设备,则对处理机的资源是极大的浪费。而引进处理机调度后,可在运行进程等待外部设备时,把处理机调度给其他进程,从而提高处理机的利用率。

不同环境的调度目标不同,因此需要针对不同环境来讨论调度算法。

1. 批处理系统

a. 先来先服务 FCFS

不可抢占式的调度算法,按照请求的顺序进行调度,可以用于作业调度和进程调度。

  • 有利于长作业(进程),而不利于短作业(进程)。从表面上看,它对所有作业都是公平的,但若一个长作业先到达系统,则短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长

  • 有利于CPU繁忙型作业,而不利于I/O繁忙型作业

b. 短作业(进程)优先调度 SJ§F

非抢占式的调度算法,按估计运行时间最短的顺序进行调度,可以用于作业调度和进程调度。

  • 作业的运行时间是用户提供的,不一定真实
  • 对长作业不利,长作业可能会产生饥饿,甚至饿死
  • 该算法完全未考虑作业的紧迫程度,无法保证紧迫性的作业被及时执行
c. 最短剩余时间优先 SRTN

最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。

  • 存在的问题同 SJF

2. 分时系统

分时是指多个用户分享使用同一台计算机,多个程序分时共享硬件和软件资源,我们平常使用的计算机系统就是分时系统。

分时系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。

a. 时间片轮转

将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。

时间片轮转算法的效率和时间片的大小有很大关系,因为进程切换具有一定开销:

  • 如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。
  • 如果时间片太长,那么实时性就不能得到保证,可能退化为 FCFS 策略
b. 优先级调度

优先级调度算法又称优先权调度算法,它不是特指某一种算法,而是指以某种指标作为优先准则的调度算法,它既可用于作业调度,又可用于进程调度。该算法中的优先级用于描述作业运行的紧迫程度。

根据是否可抢占:

  • 非剥夺式优先级调度算法

    当一个进程正在处理机上运行时,即使有更为紧迫的进程进入就绪队列,依然让正在运行的进程继续运行,直到其主动让出处理机时。

  • 剥夺式优先级调度算法

    当一个进程正在处理机上运行时,若有某个更为紧迫的进程进入就绪队列,则立即暂停正在运行的进程,将处理机分配给更重要或紧迫的进程

根据优先权类型:

  • 静态优先级

    优先级是在创建进程吋确定的,且在进程的整个运行期间保持不变。确定静态优先级的主要依据有进程类型、进程对资源的要求、用户要求

  • 动态优先级

    在进程运行过程中,根据进程情况的变化动态调整优先级。动态调整优先级的主要依据有进程占有CPU时间的长短、就绪进程等待CPU时间的长短,典型的就是高响应比优先调度算法
    响 应 比 R p = 要 求 服 务 时 间 + 等 待 时 间 要 求 服 务 时 间 响应比R_p=\frac{要求服务时间+等待时间}{要求服务时间} Rp=+

进程优先级设置原则:

  • 系统进程高于用户进程:系统进程作为系统的管理者,理应拥有更高的优先级

  • 交互型进程高于非交互型进程(前台进程高于后台进程)

  • I/O型进程高于计算型进程

    I/O型进程,是指频繁使用I/O设备的进程,而计算型进程是指频繁使用CPU的进程(很少使用I/O设备)。I/O设备(如打印机)的处理速度要比CPU慢得多,因此若将I/O型进程的优先级设置得更高,就更有可能让I/O设备尽早开始工作,进而提升系统的整体效率。

c. 多级反馈队列调度算法

一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。

多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,…。进程在第一个队列用完了所有时间片,就会被移到下一个队列。

每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。

在这里插入图片描述

现代OS普遍采用的,可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合,符合各类型用户的需求

3. 实时系统

实时系统是一种时间起着主导作用的系统,要求对于请求在确定时间内得到响应。实时系统可以分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。

实时系统使用的调度算法无非也是类似上面几种策略,具体调度算法应当结合系统的实际场景进行设计。

进程同步

为什么需要进程同步?

避免因并发导致的异步性

程序的顺序执行及其特征:顺序性;封闭性:程序的执行结果只取决于本身,不受其他进程影响;可再现性:只要程序执行时的环境和初始条件相同,重复执行时都将获得相同的结果

程序的并发执行及其特征:间断性;失去封闭性;不可再现性

1. 临界区


需要被共享,但是不允许同时访问的资源称为临界资源,像私有数据这样的,不可能被共享的数据就不属于临界资源

每个进程中访问临界资源的那段代码称为临界区,临界区的代码必须互斥执行,每个进程在进入临界区之前,需要先进行检查,否则可能造成不可预知的后果。

2. 同步与互斥

  • **互斥(间接相互制约关系):**是一种竞争关系,进程之间竞争使用某种系统资源,只能逐个使用
    • 比如说进程A需要从缓冲区读取进程B产生的信息,当缓冲区为空时,进程B因为读取不到信息而被阻塞。而当进程A产生信息放入缓冲区时,进程B才会被唤醒,这便是同步。
  • **同步(直接相互制约关系):**是一种合作关系,进程间的合作完成任务
    • 为了避免缓冲区同时被A,B进行读写导致的错误,同一时刻只允许被一个进程访问,这便是互斥。

3. 信号量

信号量(Semaphore)是一个整型变量,可以对其执行 wait (-1)和 signal (+1)操作,也就是常见的 P 和 V 「原语」。

原语」是由若干条机器指令构成的用以完成特定功能的一段程序,而这段程序只能在系统态下执行,执行期间是不可分割的,强调指令集合的连续性不可中断性。原语的作用是为了实现进程的通信和控制,系统对进程的控制如不使用原语,就会造成其状态的不确定性,从而达不到进程控制的目的。

定义原语的直接办法是关中断(屏蔽中断)

  • wait : 执行 -1 操作(申请/使用资源)
    • 如果新的信号量值大于等于 0 ,则进入临界区;
    • 如果新的信号量的值小于 0,该进程将被阻塞,被添加到 semaphore 的 waiting queue 队尾。
  • signal :执行 +1 操作(释放/生产资源)
    • 如果递增之前,semaphore 的值大于等于 0,则直接退出临界区;
    • 如果递增之前,semaphore 的值小于 0(表示有进程正在等待获取资源),从 waiting queue 中挑选一个进程到 ready queue 中,再退出临界区。

如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。

typedef int semaphore;
semaphore mutex = 1;
void P1() {
    wait(&mutex);
    // 临界区
    signal(&mutex);
}

void P2() {
    wait(&mutex);
    // 临界区
    signal(&mutex);
}

4. 管程

每个要访问临界资源的进程都必须自备同步操作 P 和 V。这就使大量的同步操作分散在各个进程中。这不仅给系统的管理带来了麻烦,而且还可能因同步操作的使用不当而导致系统死锁。故可使用管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更简洁。

每次仅允许一个进程进入管程( take_awaygive_back 函数),从而实现互斥访问(由编译器实现),一个管程 monitor 管理一种临界资源

使用管程实现生产者-消费者问题

// 管程
monitor ProducerConsumer
    condition full, empty; //条件变量 `condition` 对应着一种阻塞原因
    integer count := 0; // 被共享的资源数量

    procedure take_away(item: integer);
    begin
        if count = N then wait(full);
        insert_item(item);
        count := count + 1;
        if count = 1 then signal(empty);
    end;

    function give_back: integer;
    begin
        if count = 0 then wait(empty);
        remove = remove_item;
        count := count - 1;
        if count = N -1 then signal(full);
    end;
end monitor;

// 生产者客户端
procedure producer
begin
    while true do
    begin
        item = produce_item;
        ProducerConsumer.take_away(item);
    end
end;

// 消费者客户端
procedure consumer
begin
    while true do
    begin
        item = ProducerConsumer.give_back;
        consume_item(item);
    end
end;

经典进程同步问题

waitP 申请资源

signalV 释放资源

1. 生产者-消费者问题

普通

一组生产者进程和一组消费者进程共享一个初始为空、大小为 n n n 的缓冲区,缓冲区是临界资源,只允许一个生产者放入消息,或者一个消费者从中取出消息

semaphore mutex = 1;//互斥信号量
semaphore empty = n;//缓冲区中还有多少位置是空的
semaphore full = 0;//缓冲区中有多少位置是已占用的

void producer() {
	while (true) {
		// 生产
		P(empty);// 可用缓冲区减一
		P(mutex);// 互斥访问临界资源
		// 放入缓冲区
		V(mutex);
		V(full);// 已用缓冲区加一
	}
}

void costumer() {
	while (true) {
		P(full);
		P(mutex);
		// 从缓冲区中取出一个物品
		V(mutex);
		V(empty);
		// 消费
	}
}

申请资源时的顺序不可乱,对于生产者而言,必须先确认缓冲区是否还有空位,再进入临界区;反过来的话,先进入临界区,然后发现缓冲区没有空位,则会造成死锁;消费者同理。

P(empty);

P(mutex);

释放资源时的先后顺序无所谓,消费者同理。

V(mutex);

V(full);

复杂

桌子上有一个盘子,每次只能向其中放入ー个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等吃盘子中的橘子,女儿专等吃盘子中的苹果。只有盘子为空时, 爸爸或妈妈才可向盘子中放一个水果,仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出。

semaphore plate = 1;
semaphore apple = 0;
semaphore orange = 0;

void father() {
	while (true) {
		P(plate);
		//放苹果
		V(apple);
	}
}
void mother() {
	while (true) {
		P(plate);
		//放橘子
		V(orange);
	}
}

void son() {
	while (true) {
		P(apple);
		//拿苹果
		V(plate);
	}
}

void daughter() {
	while (true) {
		P(orange);
		//拿橘子
		V(plate);
	}
}

2. 读者-写者问题

问题描述:有读者和写者两组并发进程,共享一个文件,当两个或以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:

  • 允许多个读者可以同时对文件执行读操作;

  • 只允许一个写者往文件中写信息;任一写者在完成写操作之前不允许其他读者或写者工作;

  • 写者执行写操作前,应让已有的读者和写者全部退出。

count 是临界资源,需要使用一个互斥信号量

int count = 0;//读者数量,这是个临界资源
semaphore mutex = 1;

semaphore read_write = 1;

void reader() {
	P(mutex);
	if (count == 0)
		P(read_write);
	count++;
	V(mutex);
	//读书
	P(mutex);
	count--;
	if(count==0)
		V(read_write);
	V(mutex);
}

void writer() {
	P(read_write);
	//写书
	V(read_write);
}

3. 哲学家进餐问题

**问题描述:**一张圆桌边上坐着5名哲学家,每两名哲学家之间的桌上摆一根筷子,两根筷子中间是一碗米饭。哲学家们倾注毕生精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿时,才试图拿起左、右两根筷子(一根根地拿起)。若筷子已在他人手上,则需要等待。饥饿的哲学家只有同时拿到了两根筷子才可以开始进餐,进餐完毕后,放下筷子继续思考。

如果仅仅是让每个哲学家依次拿左边、右边的筷子,则有可能出现死锁的情况。

互斥取筷子

这种情况,即使某个哲学家拿筷子时被阻塞了,也不会有下一个哲学家拿筷子了,等到之前的哲学家吃完,放下筷子,便可拿起。

semaphore chopstick[5] = { 1,1,1,1,1 };
semaphore mutex = 1;
void philosopher(int i) {
	while (true) {
		P(mutex);
		P(chopstick[i]);//拿左边筷子
		P(chopstick[(i + 1) % 5]);//拿右边筷子
		V(mutex);
		//吃
		V(chopstick[i]);
		V(chopstick[(i + 1) % 5]);
		//想
	}
}

按编号区分取筷子的次序

编号为奇数的哲学家先取左边的筷子,偶数的先取右边的筷子。

这种情况下,不需要对取筷子进行互斥,不会出现死锁情况。

semaphore chopstick[5] = { 1,1,1,1,1 };
void philosopher(int i) {
	while (true) {
		if (i & 1) {
			P(chopstick[i]);//拿左边筷子
			P(chopstick[(i + 1) % 5]);//拿右边筷子
		}
		else {
			P(chopstick[(i + 1) % 5]);//拿右边筷子
			P(chopstick[i]);//拿左边筷子
		}
		//吃
		V(chopstick[i]);
		V(chopstick[(i + 1) % 5]);
		//想
	}
}

4. 吸烟者问题

**问题描述:**假设一个系统有三个抽烟者进程和一个供应者进程。每个抽烟者不停地卷烟并抽掉它,但要卷起并抽掉一支烟,抽烟者需要有三种材料:烟草、纸和胶水。三个抽烟者中,第一个拥有烟草,第二个拥有纸,第三个拥有胶水。供应者进程无限地提供三种材料,供应者每次将两种材料放到桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者一个信号告诉已完成,此时供应者就会将另外两种材料放到桌上,如此重复(让三个抽烟者轮流地抽烟)。

semaphore offer1 = 0, offer2 = 0, offer3 = 0;//三种原材料

semaphore finish = 0;//是否抽完烟

void provider() {
	while (true) {
		if (order == 1) {
			V(offer1);
		}
		else if (order == 2) {
			V(offer2);
		}
		else if (order == 3) {
			V(offer3);
		}
		//材料放到桌子上,因为吸烟是要求轮流,也就是互斥的,那么桌子就不需要互斥了。
		order = (order + 1) % 3 + 1;
		P(finish);
	}
}

void P1() {
	while (true) {
		P(offer1);
		//抽烟
		V(finish);
	}
}

进程通信

interprocess communication IPC

这儿的通信是各进程之间的,或者说不同进程的线程,通信的目的无非就是交换信息与数据,隶属于统一进程的线程共享进程的内存空间,它们不需要通信。

为什么进程之间通信必须在管态?

用户进程空间一般都是独立的,在操作系统和硬件的地址保护机制下,进程无法访问其他进程的地址空间,所以必须借助于操作系统的系统调用函数实现进程之间的通信

1. 匿名管道

一种特殊的消息传递,属于间接通信,半双工通信,只能用于具有血缘关系的进程中使用。管道以一种特殊文件形式存在于文件系统中,我们可以使用普通的I/O流进行操作,但其本质上是一种固定大小的内存缓冲区。

这是一个我们在学习Linux命令行的时候就会引入的一个很重要的概念,系统操作执行命令的时候,经常有需求要将一个程序的输出交给另一个程序进行处理,这种操作可以使用输入输出重定向加文件搞定,比如:

ls  -l /etc/ > etc.txt
wc -l etc.txt
181 etc.txt

但是这样未免显得太麻烦了,因为我们只是需要最后的统计结果,不需要中间文件,所以,管道的概念应运而生,我们可以使用“|”连接两个命令,shell会将前后两个进程的输入输出用一个管道相连,以便达到进程间通信的目的

ls -l /etc/ | wc -l
183

2. 命名管道 FIFO

匿名管道应用的一个重大限制是它没有名字,因此,只能用于具有亲缘关系的进程间通信,因此就引入了 FIFO,FIFO 提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信。

从名字也能看出来FIFO实现的是一个队列的功能,常用于客户-服务器应用程序中,FIFO 用作汇聚点,用于服务器与(多个)客户进程处理请求时传递数据。

3. 消息队列

管道的本质是内存上的一片缓冲区,被不同进程共享的临界区,那么在打开与读取时可能会被阻塞。

于是出现了消息队列,其优势在于:

1、消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。

2、同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。

3、接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。

4. 信号量

进程之间的互斥和同步(信号量),由于其所交换的信息量少而被归结为低级通信方式

5. 共享存储

在存储器中划出一块共享存储区,进程可通过对共享存储区中数据的读或写来实现通信

6. 套接字

套接字用于实现不同机器间的进程通信,详见计算机网络传输层

死锁 Deadlock

死锁」指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

  • 「死锁」的进程一定处于阻塞态,死锁至少有两个进程同时发生

  • 「饥饿」是指某个进程长期得不到调度,进程可能处于阻塞态(等待IO设备)或就绪态(等待CPU执行),饥饿状态可能只有一个进程。

1. 产生原因

a. 竞争资源

当系统中供多个进程共享的资源如打印机、公用队列等,其数目不足以满足诸进程的需要时,会引起诸进程对资源的竞争而产生死锁。

只有对不可剥夺资源的竞争才有可能引起死锁,对可剥夺资源的竞争不会引起死锁。

b. 进程间推进顺序非法

进程在运行过程中,请求和释放资源的顺序不当,也同样会导致产生进程死锁。

信号量使用不当,也会产生进程死锁。

2. 必要条件

a. 互斥条件

临界资源,需互斥访问,但资源被一个进程占用时,其它进程不可申请。

b. 请求和保持条件

已经得到了某个资源的进程可以再请求新的资源。

c. 不剥夺条件

指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放,对不可剥夺资源的竞争才有可能引起死锁。

d. 环路等待条件

满足一种进程资源循环等待链,参考哲学家进程问题中,当每一个哲学家都拿起左边的筷子时,每个哲学家都在等待自己右边的筷子。

有环路不一定死锁,但若系统中每种资源数只有一个,则循环等待条件就成了死锁的充要条件

3. 处理策略

a. 预防死锁(破坏死锁的必要条件)

通过设置某些限制条件,在程序运行之前破坏产生死锁的必要条件,从而预防发生死锁(破坏了必要条件,所以预防死锁,一定不会死锁),但由于所施加的限制条件往往太严格,因而可能会导致系统资源利用率和系统吞吐量降低

  • 破坏互斥条件

破坏互斥条件不可行,而且在有的场合应该保护这种互斥性。若允许系统资源都能共享使用,则系统不会进入死锁状态。但有些资源根本不能同时访问,如打印机等临界资源只能互斥使用(即便是spooling技术,允许若干个进程同时向打印机输出,但事实上还是互斥访问,唯一真正请求物理打印机的进程是打印机守护进程)。

  • 破坏不剥夺条件

当一个已经保持了某些资源的进程,再提岀新的资源请求而不能立即得到满足时,必须释放(即剥夺)它已经保持了的所有资源,待以后需要时再重新申请。如何像进程调度一样进行“上下文切换”是比较复杂的,反复“切换”会增加系统开销,降低系统吞吐量。这种方法常用于状态易于保存和恢复的资源,如CPU的寄存器及内存资源,一般不能用于打印机之类的资源

  • 破坏请求并保持条件

采用预先静态分配方法,规定所有进程在开始执行前请求所需要的全部资源,运行期间不允许再提出其他资源请求,这样就可以保证系统不会发生死锁。

系统资源被严重浪费,其中有些资源可能仅在运行初期或运行快结束时才使用,甚至根本不使用。可能其它进程需要的资源一直不被释放,而导致“饥饿”现象。

  • 破坏循环等待条件

采用顺序资源分配法。首先给系统中的资源编号,规定每个进程必须按编号递增的顺序请求资源,在采用这种策略时,总有某个进程 A 占据了最高序号的资源,此序号之后的资源申请必然畅通无阻,进程 A 必然可以一直向前推进

这种预防死锁的策略与前两种策略比较,其资源利用率和系统吞吐量都有较明显的改善。但也存在一些问题

  • 不方便增加新设备,因为有可能要为所有设备重新编号
  • 进程实际使用资源的顺序可能和编号递增的顺序不一致,那么小编号的资源必须在大编号资源之前申请来,即便很久后才会用到,会导致资源浪费
  • 必须按规定次序申请资源,不便于用户编程
b. 避免死锁

在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。只需事先施加较弱的限制条件,便可获得较高的资源利用率及系统吞吐量

  • 系统安全状态

安全状态指系统能按某种进程顺序(序列为安全序列),来为每个进程分配其所需资源,使每个进程都可顺利地完成。如果无法找到这样一个安全序列,则称系统处于不安全状态。

在避免死锁的方法中,允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次资源分配的安全性。本次分配不会导致系统进入不安全状态,才会分配资源给进程

不安全状态不一定会死锁,但安全状态一定不会死锁

  • 银行家算法

c. 检测死锁及解除

不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。

  • 死锁定理

通过资源分配图检测系统是否存在死锁

……

常用的解除死锁方法:

  • 剥夺资源法:剥夺某些死锁进程的资源,再将这些资源分配给其它的进程,使之转为就绪状态

  • 撤消进程法:按照某种顺序逐个地撤消死锁进程,直至死锁状态消除

  • 进程回退法:让一个或多个进程回退到足以回避死锁的地步,然后OS在协调分配,优先将资源分配给某个进程,避免再次进入死锁,进程回退时自愿释放资源而非被剥夺。要求系统设置还原点。

d. 鸵鸟策略

当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。

事实上大多数操作系统,包括 Unix、Linux、Windows 甚至移动端 OS,对于死锁就是采用的都是鸵鸟策略,比如,当我们发现系统卡死时,全屏变灰,点什么按什么都没反应,一般就直接选择强制重启了,等它自己活过来的可能性太低了。

拓展

线程池

服务器响应客户端请求的一种策略,使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,当服务器接收到客户端的请求时,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。数据库连接池采用的也是这种思想。

适用于需要大量的线程来完成任务,且完成任务的时间比较短。 典型的就是WEB服务器完成网页请求这样的任务。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。

浏览器多进程模型与多线程模型的比较

众所周知,宇宙第一浏览器 chrome 采用的是多进程模型,甚至连每一个插件都是一个进程,这么做确实好用,但代价就是吃内存。

  • 占用资源

    毋庸置疑,多线程模型占用的系统资源更少。

  • 响应性

    如果是单个进程,当进程需要执行大量计算或者等待某个IO事件时,它会被阻塞,那这个时候,它就不会响应用户的任何需求。

    如果是多线程的话,那就可以把计算,等待IO事件分出来一个线程去做,不影响主线程响应用户。

  • 稳定性

    进程会更稳定一些,因为各进程之间是相互独立的。

    而对于线程,任何一个线程出问题,会导致整个进程的崩溃。

  • 安全性

    拿浏览器举例,如果浏览器是一个、进程,而每个标签页是隶属于该进程的线程,假设我同时打开了A和B网站,从理论上来说,A网站就有可能窃取到我在B网站上的信息。

    在比如说,在服务器中,假如服务器端采用的是多线程(每来一个客户端请求分配一个线程),那么安全性就不如多进程(每来一个客户端请求建立一个进程)。

    从安全角度看,线程的安全性要比进程的安全性差。当然,这只是理论上的一点猜测,事实上安全是一个非常复杂的问题。

  • 总结

    多进程,多线程,应该不能说孰优孰劣,各自有不同胜任的地方,两者相辅相成。打开任务管理器能看到,一般软件都会开多个进程,每个进程内部又会跑自己的线程。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值