目录
1.1 先来先服务 first-come first-serverd(FCFS)
1.2 短作业优先 shortest job first(SJF)
1.3 最短剩余时间优先 shortest remaining time next(SRTN)
1.4 最高相应比优先 highest response ratio next(HRRN)
进程与线程
1. 进程
进程是具有独立功能的程序在某个数据集合上的一次运行活动,是系统进行资源分配的独立单位。
- 进程是资源分配的基本单位。
- 进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。
- 进程控制表:所有进程的 PCB 集合,上限即为操作系统的并发度。
多道程序设计:允许多个程序同时进入内存并运行,其目的是为了提高系统效率
下图显示了 4 个程序创建了 4 个进程,这 4 个进程可以并发地执行。
程序的一次执行过程
是正在运行程序的抽象
将一个CPU变成多个虚拟的CPU
系统资源以进程为单位分配,如内存、文件、 ……每个具有独立的地址空间
操作系统将CPU调度给需要的进程
2. 线程
线程是进程中的一个运行实体,是 CPU 独立调度的基本单位。
一个进程中可以有多个线程,它们共享进程资源。
Word 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。
Word 中同时运行 I/O 线程(保持输入)、排版线程(输入后进行相应的排版)和保存线程(实现每隔一段时间自动保存)。
- 线程之间相互通信无需调用内核(同一进程内的线程共享内存和文件)。
- 同一个进程的线程之间共享所在进程的地址空间和其他资源
- 同进程一样,线程也可以创建与撤销另一个线程。
3. 区别
Ⅰ 拥有资源
进程是资源分配的基本单位,线程不拥有资源,但可以访问隶属进程的资源。
Ⅱ 调度
线程是 CPU 独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
Ⅲ 系统开销
由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
Ⅳ 通信方面
线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。
进程状态的切换
- 就绪状态(ready):已具备运行条件,但由于没有空闲 CPU 而暂时不能运行,等待被调度
- 运行状态(running): 占有 CPU,并在 CPU 上运行
- 阻塞状态(waiting):等待某一事件(如等待读盘结果)而暂时不能运行,等待资源
应该注意以下内容:
- 只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。
- 阻塞状态是缺少需要的资源或需要等待某一事件的结果从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。
- 状态的改变就是PCB从一个进程队列进入另一个进程队列的过程
其他状态:
- 创建态:已创建进程并分配了PID、PCB,但因资源有限而未执行
- 终止态:数据记录,资源回收
- 挂起态:过载时需进行负载调节,会将内存空间收回映像暂时写入磁盘
进程调度算法
- CPU 调度:控制、协调多个进程对 CPU 的竞争,按一定的调度算法从就绪队列中选择一个进程。
- 系统场景:N 个进程、M 个 CPU
- 衡量指标:吞吐量、周转时间、响应时间、CPU 利用率、等待时间
不同环境的调度算法目标不同,因此需要针对不同环境来讨论调度算法。
1. 批处理系统
批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。
1.1 先来先服务 first-come first-serverd(FCFS)
按照进程就绪的先后顺序进行调度,非抢占,公平,实现简单。
有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长,即平均周转时间过长。
1.2 短作业优先 shortest job first(SJF)
按估计运行时间最短的顺序进行调度,具有最短完成时间的进程优先执行,非抢占式。
长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。
1.3 最短剩余时间优先 shortest remaining time next(SRTN)
SJF 抢占式版本,当一个新就绪的进程比当前运行进程具有更短的完成时间时,系统选择新进程执行。
即按估计剩余时间最短的顺序进行调度。
- 优点:最短的平均周转时间(所有进程可同时运行的前提下)
- 缺点:不公平性(源源不断的短作业会使先到达的长作业一直等待)
1.4 最高相应比优先 highest response ratio next(HRRN)
折中权衡,计算每个进程的响应比 R,之后总是选择 R 最高的进程执行。
响应比 = 周转时间 / 处理时间 = (处理时间+等待时间) / 处理时间
2. 交互式系统
交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。
2.1 时间片轮转 (RR)
将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。
时间片轮转算法的效率和时间片的大小有很大关系:
- 抢占式,时间片用完时
- 因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。
- 而如果时间片过长,那么实时性就不能得到保证。
2.2 优先级调度
为每个进程分配一个优先级,按优先级进行调度。
为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
2.3 多级反馈队列
一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。
多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。
每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。
可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。
抢占式,时间片用完时。
2.4 最短进程优先
思想与 短作业优先算法 相同。
3. 实时系统
实时系统要求一个请求在一个确定时间内得到响应。
分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。
进程同步机制
进程互斥:由于各进程要求使用共享资源,而这些资源需要排他性使用,所以造成各进程之间竞争使用这些资源的现象。
临界资源:系统中某些资源一次只允许一个进程使用,称为 临界资源 或 互斥资源。
优先级反转:一个低优先级的进程使用了临界资源(进入了临界区),则造成更高优先级的进程就绪时希望抢占 CPU ,但是得不到临界资源而造成阻塞。
1. 临界区
系统中某些资源一次只允许一个进程使用,称为 临界资源 或 互斥资源。
对临界资源进行访问的那段代码称为临界区。
为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。
// entry section
// critical section;
// exit section
2. 同步与互斥
同步:指系统中多个进程中发生的事件存在某种时序关系,需要相互合作,共同完成一项任务。(例:某个进程运行到某一点,要求另一个伙伴进程为它提供消息,未获得消息之前,该进程进入阻塞态。)
互斥:多个进程在同一时刻只有一个进程能进入临界区。
进程互斥的解决方案:开关中断指令、测试并加锁指令、交换指令。
3. 信号量
信号量(Semaphore)是一个整型变量,用于进程间信息传递
可以对其执行初始化、 down 和 up 操作,也就是常见的 P 和 V 操作。
- down : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0;
- up :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。
down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。
如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。
typedef int semaphore;
semaphore mutex = 1;
void P1() {
down(&mutex);
// 临界区
up(&mutex);
}
void P2() {
down(&mutex);
// 临界区
up(&mutex);
}
如果信号量的取值推广到多值,成为了计数信号量,可用于解决同步问题。
使用信号量实现生产者-消费者问题
问题描述:
- 使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。
- 因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。
- 为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。
注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行 up(empty) 操作,empty 永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。
V 操作的顺序可以调整,但是会扩大临界区使用的范围,使在临界区的时间变长。
#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;
void producer() {
while(TRUE) {
int item = produce_item();
down(&empty);
down(&mutex);
insert_item(item);
up(&mutex);
up(&full);
}
}
void consumer() {
while(TRUE) {
down(&full);
down(&mutex);
int item = remove_item();
up(&mutex);
up(&empty);
consume_item(item);
}
}
使用信号量实现读者-写者问题
- 允许多个进程同时对数据进行读操作
- 但是不允许读和写以及写和写操作同时发生
一个整型变量 count 记录在对数据进行读操作的进程数量,一个互斥量 count_mutex 用于对 count 加锁,一个互斥量 data_mutex 用于对读写的数据加锁。
typedef int semaphore;
semaphore count_mutex = 1;
semaphore data_mutex = 1;
int count = 0;
void reader() {
while(TRUE) {
down(&count_mutex); // 此时 count 作为临界资源要用互斥量和 PV 操作保护起来
count++; // count 表示当前读者数
if(count == 1) down(&data_mutex); // 第一个读者需要对数据进行加锁,防止写进程访问
up(&count_mutex);
read();
down(&count_mutex);
count--;
if(count == 0) up(&data_mutex); // 最后一个读者需要对数据进行解锁
up(&count_mutex);
}
}
void writer() {
while(TRUE) {
down(&data_mutex);
write();
up(&data_mutex);
}
}
4. 管程
概念:实现进程间通信机制的一种方式,由关于共享的数据结构与在其上的一组操作组成。
原因:使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,易出错,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。
c 语言不支持管程,下面的示例代码使用了类 Pascal 语言来描述管程。示例代码的管程提供了 insert() 和 remove() 方法,客户端代码通过调用这两个方法来解决生产者-消费者问题。
monitor ProducerConsumer //管程有一个特定的名字
integer i;
condition c;
procedure insert(); //由共享的数据结构与在其上的一组操作组成
begin
// ...
end;
procedure remove();
begin
// ...
end;
end monitor;
互斥:管程有一个互斥特性:在一个时刻只能有一个进程使用管程——为了保证数据结构的完整性。进程在无法继续执行的时候不能一直占用管程,否者其它进程永远不能使用管程。
同步:管程引入了 条件变量 以及相关的操作:wait() 和 signal() 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。
条件变量:(例如名称为c)是管程内的一种数据结构,且只有在管程中才能被访问,它对管程内的所有过程是全局的,只能通过两个原语操作来控制它。
c.wait( )-调用进程阻塞并移入与条件变量c相关的队列中,并释放管程,直到另一个进程在该条件变量c上执行signal( )唤醒等待进程并将其移出条件变量c队列。
c.signal( )-如果存在其他进程由于对条件变量c执行wait( )而被阻塞,便释放之;如果没有进程在等待,那么,信号被丢弃。
条件变量与P、V操作中信号量的区别:
条件变量是一种信号量,但不是P、V操作中纯粹的计数信号量,没有与条件变量关联的值,不能像信号量那样积累供以后使用,仅仅起到维护等待进程队列的作用。因此在使用条件变量x时,通常需要定义一个与之配套使用的整型变量x-count用于记录条件变量x所维护等待队列中的进程数。
进程与管程的关系:
进程只能通过调用管程中的过程来间接的访问管程中的数据结构,以实现进程间的通信。
使用管程实现生产者-消费者问题
// 管程
monitor ProducerConsumer
condition full, empty;
integer count := 0; //共享的数据与操作部分
condition c;
procedure insert(item: integer);
begin
if count = N then wait(full);
insert_item(item);
count := count + 1;
if count = 1 then signal(empty);
end;
function remove: 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.insert(item); //只有生产与放入缓存区的操作
end
end;
// 消费者客户端
procedure consumer
begin
while true do
begin
item = ProducerConsumer.remove; //只有消费与移除缓存区的操作
consume_item(item);
end
end;
进程通信
进程同步与进程通信很容易混淆,它们的区别在于:
- 进程同步:控制多个进程按一定顺序执行;
- 进程通信:进程间传输信息。
进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。
1. 管道
利用内核在两个进程之间建立通道,仅仅在管道的一端只读,另一端只写,利用读写的方式在进程之间传递数据。
利用一个缓冲介质——内存或者文件连接两个相互通信的进程
管道是通过调用 pipe 函数创建的,fd[0] 用于读,fd[1] 用于写。
#include <unistd.h>
int pipe(int fd[2]);
它具有以下限制:
- 先进先出顺序
- 管道通信机制也要有协调 互斥 同步 读写进程的能力
- 只支持半双工通信(双向交替传输);
- 只能在父子进程中使用。
2. FIFO
也称为命名管道,去除了管道只能在父子进程中使用的限制。
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);
FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。
3. 消息队列
在内核中建立一个链表,发送方按照一定的标识将数据发送到内核中,内核将其放入链表后,等待接收方的请求。接收方发送请求后,内核按照消息的标识,从内核中将消息从链表中摘下,传递给接收方。消息是一种完全的异步操作方式。
相比于 FIFO,消息队列具有以下优点:
- 消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难;
- 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法;
- 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。
4. 信号量
它是一个计数器,用于为多个进程提供对共享数据对象的访问。
5. 共享存储
允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种 IPC。
需要使用信号量来同步对共享存储的访问。
多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。另外 XSI 共享内存不是使用文件,而是使用使用内存的匿名段。
6. 套接字
与其它通信机制不同的是,它可用于不同机器间的进程通信。