1. 进程的调度算法
① 先来先服务(FCFS)
- 先来先服务(
first-come first-served
,FCFS):是最简单的调度算法,按照请求的先后顺序进行调度。 - 特点:
- 非抢占式的调度算法,易于实现,但性能不好
- 有利于长作业,不利于短作业。 因为短作业必须等待前面的长作业执行完毕才能执行,会造成短作业的等待时间过长。
- 上表中的进程执行顺序:
A(3) --> B(6) --> D(4) --> C(5) --> E(2)
② 短作业优先(SJF)
- 短作业优先(
shortest job first
,SJF):按估计运行时间最短的顺序进行调度。 - 特点:
- 非抢占式的调度算法,优先照顾短作业,具有很好的性能,降低平均等待时间,提高吞吐量。
- 不利于长作业,长作业可能一直处于等待状态,造成长作业饿死。
- 没有考虑作业的优先紧迫程度,不能用于实时系统。
- 上表中的进程执行顺序:
③ 最短剩余时间优先(SRTN)
- 最短剩余时间优先 (
shortest remaining time next
, SRTN):
- 最短剩余时间优先是短作业优先的抢占式版本,按剩余运行时间最短的剩余进行调度。
- 新的进程到来时,将新进程的运行时间与当前进程的剩余运行时间进行比较。
- 如果新进程的运行时间更短,则挂起当前进程,运行新进程;否则新进程等待。
- 特点: 确保一旦新的短进程进入系统,可以尽快处理。
- 上表中的进程执行顺序:
④ 时间片轮转
- 时间片轮转(
round robin
,RR):
- 使用队列,按照
FCFS
的原则将就绪进程排序,后来的进程插入到队列末尾。 - 选择队列中的首进程,为其分配CPU时间片。时间片用完,计时器发出时钟中断,停止该进程的运行,将其送回队列末尾。
- 重复步骤1和2,直到完成进程调度。
- 特点:
- 时间片轮转的效率与时间片的大小有很大关系。 时间片太小,进程切换频繁反而使CPU利用率变低;时间片太大,系统的实时性不能得到保证。
- 时间片轮转算法,大多用于分时系统。
- 上表的进程执行顺序(时间片为1):
⑤ 优先级调度
- 优先级调度:为每个进程分配一个优先级,优先级高的先调度,优先级低后调度。
- 根据当前进程执行时,遇到较高优先级进程的处理方式,分为抢占式优先级调度、非抢占式优先级调度。
- 抢占式优先级调度:进程在执行期间,具有更高优先级的进程到来,则中断当前进程,执行具有更高优先级的进程。
- 非抢占式优先级调度:进程在执行期间,具有更高优先级的进程到来,仍然继续执行直到完成。
- 为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
⑥ 多级反馈队列
- 多级反馈队列:
- 设置多个队列,每个队列的时间片大小不同,例如
1, 2, 4, 8 ...
。 - 进程就绪时首先进入一级队列,被调度执行后,如果还需执行,则进入二级队列,依次类推。如果到了最后一级队列还未执行完毕,则仍然进入该队列。
- 每一级队列中的进程按照
FCFS
进行调度,只有当上一级队列中没有进程等待执行时,才能调度当前队列中的进程。
- 多级反馈队列是为连续执行多个时间片的进程考虑的,通过更改每级队列的时间片大小,减少该进程的调度次数。
2. 进程间通信IPC
- 进程间通信(IPC): 在进程间进行信息传输。
- 进程同步: 让多个进程按照一定的顺序执行。
- IPC是手段,进程同步是目的。即为了实现进程同步,我们可以在进程间传递一些进程同步所需要的信息。
① 管道(pipe)
- 管道(pipe): 一种半双工通信方式,数据流只能单向交替传输;只能在具有亲缘关系的进程间通信,如父子进程、兄弟进程。
- 管道可以通过调用
pipe()
函数创建,fd[0]
用于读,fd[1]
用于写。
#include <unistd.h>
int pipe(int fd[2]);
- 优点: 简单方便
- 缺点:
- 半双工通信,数据流单向交替传输
- 只能在具有亲缘关系的进程间通信
- 缓冲区有限
② 命名管道(FIFO)
- 命名管道(FIFO): 一种半双工通信方式,数据流只能单向交替传输;允许在不具有亲缘关系的进程间通信。
FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。 - 命名管道可以通过调用
mkfifo()
函数或者mkfifoat()
函数进行创建。
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);
- 优点:
- 可以用于任意关系的进程间通信。
FIFO
常用于C/S应用程序中,作为汇聚点,在客户进程和服务器进程之间传递数据。
- 缺点:
- 长期存于系统中,使用不当容易出错
- 缓冲区有限
③ 信号量(Semophore)
- 信号量(semophore):
- 是一个计数器,用于控制多个进程对共享资源的访问。
- 主要作为进程间以及同一进程内不同线程之间的同步手段。
- 优点: 同步进程或者同一进程内的不同线程
- 缺点: 信号量有限
- PV操作:
- P操作(down/wait): 当
S > 0
时,S = S - 1
;当S = 0
,进程睡眠,等待S > 0
。 - V操作(up/singal): 当
S = 上限
时,进程睡眠,等待S < 上限
;否则,S = S + 1
。
- 如果信号量只有0和1两种取值,可以成为互斥量(
Mutex
)。0
表示临界区已经加锁,1
表示临界区解锁。 - 典型应用: 生产者-消费者问题。
- 需要使用Mutex实现对缓冲区的互斥访问,初始值为1;当生产者向其中添加产品时,消费者不能访问;当消费者获取产品时,生产者不能访问。
- 信号量
empty
用于记录缓冲区中可放入的产品数,初始值为N
;empty
为0,表示缓冲区已满,生产者不能再添加产品。 - 信号量
full
用于记录缓冲区中已经放入的产品数,初始值为0
;full
不为0,表示缓冲区中有产品,消费者可以获取产品。
#define N 10
typedef int semophore
semophore mutex = 1; // 缓冲区处于可访问状态
semophore empty = N; // 初始时,可以放入N个产品
semophore full = 0; // 初始时,缓冲区没有产品
void producer(){
while(true){
int item = produce_item();
down(&empty); // 空出来的位置-1
down(&mutex); // 缓冲区加锁
insert_product(item);
up(&mutex); // 缓冲区解锁
up(&full); // 可消费的产品+1
}
}
void consumer(){
while(true){
down(&full); // 可消费的产品-1
down(&mutex); // 缓冲区加锁
int item = remove_item();
consume_product(item);
up(&muutex); // 缓冲区解锁
up(&empty); // 空出来的位置+1
}
}
④ 信号(Signal)
- 信号: 一种比较复杂的通信方式,用于通知接收进程某个事件已经发生
⑤ 消息队列(Message Queue)
- 消息队列(Message Queue): 是消息的链表,存放在内核中并由消息队列标识符标识。
- 相比于 FIFO,消息队列具有以下优点:
- 消息队列可以独立于读写进程存在,从而避免了FIFO中同步管道的打开和关闭时可能产生的困难;
- 避免了FIFO中同步阻塞问题,不需要进程自己提供同步方法。
- 读进程可以根据消息类型有选择的接收消息,而不是像FIFO那样默认接收。
- 缺点: 信息的复制需要消耗额外的CPU时间,不适合信息量大或者操作频繁的场合。
⑥ 共享内存(Shared Memory)
- 共享内存:
- 映射一段能被其他进程访问的内存,这段内存由一个进程创建,但多个进程都可以访问。
- 由于不需要在进程之间拷贝共享数据,是一种最快的IPC方式。
- 共享内存往往需要与其他IPC方式配合使用,如信号量,同步对共享内存的访问。
- 缺点:
- 需要自己实现进程间读写操作的同步,如通过信号量
- 内存实体存在于计算机中,只能在一计算机的进程之间共享,不能用于网络通信。
⑦ 套接字(socket)
- 套接字: 可用于不同机器间的进程通信,一般通过socket实现客户端和服务器的
TCP
或UDP
通信 - 优点:
- 适合客户和服务器之间信息的实时交互
- 可以加密,数据安全性强
- 缺点: 接收到的字节流需要进行解析,转化成应用层数据。
- 客户和服务器建立TCP连接,进行socket通信的流程:
- 服务器和客户端均调用
socket()
函数创建自己的socket。其中socket()
函数返回一个socket描述符,它唯一标识一个socket。 -
socket()
函数返回的socket中无具体的地址。为了避免在listen()
和connect()
函数调用时被系统随机分配端口,服务器需要主动调用bind()
函数为其绑定ip和port。 - 服务器调用
listen()
函数,把socket设置为被动方式,以便随时接受客户的连接请求。 - 客户调用
connect()
函数向服务器发起连接请求,要求connect()
函数中ip和port必须与bind()
函数绑定的一致。 - 服务器通过
accpet()
函数,从监听队列中获取已经完成三次握手的连接。有就返回,没有就阻塞等待。 - 这时客户和服务器已经建立了socket连接,可以通过
send()
和recv()
函数发送和接收消息,或者通过read()
和write()
函数实现读写操作。 - 一旦客户或服务器结束使用socket,就可以调用
close()
函数关闭相应的socket描述符。
- 如果使用socket实现UDP通信,则服务器不需要调用
listen()
和accept()
方法,客户端直直接通过connect()
建立与服务器的socket连接。
-
listen()
函数是让socket处于被动方式,以便监听客户的连接请求。UDP是无连接的,因此无需监听连接请求。 -
accept()
函数用于从监听队列中获取完成三次握手的连接,listen()
函数都不需要使用,它就更不需要了。