2 进程
这章简单介绍进程的基本概念,这章节是操作系统的重要章节之一。进程的概念,像进程控制,PCB块的理解,尤其要注意进程同步问题。是如何解决进程同步的,算法思想要了解。后面比较重要的内容是处理机的调度问题,如何分配资源,分配内存的算法,最久未使用算法等等等等。还有一个很重要的死锁的概念,以及死锁的解决算法。还有后面的存储器管理,内存控制,算法置换,以及页面算法。
让我们先从进程开始吧!
1、概念
我们先介绍,在操作系统未引入进程时,程序的执行方式以及它的执行特点,分析它的状态。然后再来思考,存在的问题,最后分析,为什么要引入进程,后面再分析引入进程后,程序的执行状态。
1.1 程序的顺序执行
一个程序的执行有几个阶段。假设一个进程有三个语句:
S1: a = x+y;
S2: b = a-5;
S3: c = b+1;
那这个程序执行顺序就是:S1 -> S2 -> S3
1.1.1 特征
这种顺序执行,首先执行是有顺序的。其次,执行是在封闭环境下的,程序运行的时候会占据整机资源,程序运行时不会被其他程序干扰。第三个是可再现的。因为是顺序的,重复执行都会得到一样的结果。
1.2 程序并发执行
1.2.1 执行状态
我们用前趋图来表示程序运行之间的状态描述
通过这个前趋图,我们能清晰的看到进程之间执行的关系。找到他们的前趋关系。
1.2.2 特征
1)间断性:进程的执行不是按顺序的,是间断的。
2)失去封闭性:因为共享资源,所以不封闭了。
3)不可再现:由于失去了封闭性,程序的执行状态无法受到控制,多次执行程序的结果不一定一致。
1.3 进程
为了提高计算机资源利用率,计算机使用并发执行程序,而并发执行程序会有山美国说的很多问题。如何对程序进行管理,引入了进程结构。
1.3.1 特征
1)结构特征:为了管理进程,设置了一个PCB(进程控制块)来管理。进程则由数据、程序、PCB三者组成。
2)动态性:进程是一个程序进程的一次执行过程。所以进程会消亡,它具有动态性,具有生命周期。与程序不同,程序只是一段指令,是静态的。
3)并发性:既然进程是为了让程序并发执行,拥有并发性的特征也很合理吧…
4)异步性:并发即具有异步特征
1.3.2 基本状态
1)就绪态:进程已经分配除了CPU以外的所有资源,就可以准备执行了。这个时候只需要等待CPU空闲,即可进入执行。
2)执行状态:程序正在执行。
3)阻塞态:进程需要申请某些资源例如请求IO等,就会从执行态变成阻塞态,当资源满足后,重新变成就绪态
上诉三种是进程的基本状态,除了基本状态还有引入了一些其他状态
1.3.3 数据结构PCB
PCB是进程的基本数据结构,有了它可以很方便的管理进程。通过PCB,可以知道当前进程的执行状态,断点地址,CPU可以找到进程的数据和程序指令
1)PCB中的信息:
1,进程标识符。标识id,包括内部标识符和外部标识符。内部标识符是进程序号,便于系统使用。外部标识符是给人看的。能描述这个进程的父进程,子进程等关系,还能描述用户标识。
2),处理机状态。当进程在CPU执行的时候,很多信息放在寄存器中,因此在中断重新执行的时候,为了便于从断点执行,需要把寄存器信息保存好。
3),进程调度信息。PCB中也会存放一些关于进程调度的信息。包括进程状态,优先级和进程调度所需要的其他信息这些通常和调度算法相关。
4),进程控制信息。包括程序和数据的地址,同步机制,资源清单,以及连接指针以支持进程所在队列的下一个进程PCB的地址。
2)PCB组织方式:包括链接和索引两种方式。
2、进程控制
要进行进程管理,最首要要能够控制进程。控制进程的创建、终止,控制进程状态等等。这些控制功能是由操作系统内核中的原语实现的,先简单介绍操作系统内核。
2.1 操作系统内核
操作系统被划分成多个层次,由于不同的用户使用需求和水平的区别,一般把处理机分为两个执行状态,用户态和内核态。
用户态是普通用户使用的状态,只具有较低特权的执行状态。只能执行一些规定的指令,只能访问一些规定的寄存器和存储区。防止普通用户对OS的破坏。
内核态相当于root用户了,能访问一切区域,包括寄存器,存储区等等。
内核应该对操作系统有一定的管理功能。包括系统支撑和资源管理。
像中断处理,原语操作等等,像进程管理等等
2.2 进程创建
进程是一种继承树的层次结构。
创建进程的过程:
- 首先是提出请求
- 然后申请PCB
- 然后分配资源
- 然后初始化PCB
- 等待进入就绪队列
2.3 进程阻塞&唤醒
一般来说,当进程在执行过程中,出现资源请求无法获取的情况,进程就会无法继续执行,进入阻塞状态。
2.3.1 出现进程阻塞的事件
1)进程申请共享资源失败。此时没有足够的资源分配给进程,那进程就会进入阻塞状态,等待资源准备好再继续执行。
2)等待操作完成。当该进程操作依赖另一进程的操作,或者依赖IO等,那就需要等待了。
3)新数据尚未到达。对于两个合作的进程而言,进程1需要获取进程2的新数据才能继续执行,这时候就可能出现阻塞。
4)等待新任务。对于某些系统进程来说,当没有任务之后,就会把自己阻塞。
2.3.2 阻塞过程
进程倘若出现了上诉事件,便会进行阻塞。这时候会调用阻塞原语block将自己阻塞。这是一种主动行为,进程block进程后,需要马上停止进程执行,把执行状态修改,把PCB放入阻塞队列,等待调度,并保留当前状态。
2.3.3 唤醒过程
当进程阻塞后,满足继续执行的条件,要重新启动进程,则需要进行唤醒。
唤醒则需要相关进程对该进程调用wakeup源于,把执行状态修改。
要注意的是,block和wakeup原语是成对出现的,否则被阻塞的进程永远无法继续运行。
3. 进程同步
这节是本章的重点,也是难点,如何理解进程间同步,会出现什么问题?如何解决?本节先介绍相关概念,然后介绍一些同步问题和解决方法,最后介绍信号量的解决方法,还有一些经典场景问题。
3.1 基本概念
临界资源:所谓临界资源就是同时只能一个进程访问,像打印机等等。对于这些资源的访问是互斥的
临界区:对于临界资源访问的那段代码,就称为临界区。
3.2 硬件同步机制
使用硬件方法来实现进程同步,最具典型的是关中断方法
关中断:进入临界区执行后,关闭中断响应,计算机不响应中断。
进入一个临界区时,它有一个锁,只有锁开才可进入,锁关则需要等待。进入后,执行完成后需关锁。这个过程可能被中断导致其他进程错误进入。因此关闭中断即可。
### 3.3 信号量机制
信号量机制是最典型最常用的解决同步问题的方法。
3.3.1 整型信号量
设置一个整型量S,表示资源数目,同时设置两个原语操作来访问这个资源。wait(S),和signal(S),这两个操作也称为PV操作。
3.3.2 记录型信号量
记录型信号量则进一步改进。记录型信号量采用了一个记录型的数据结构。由于使用整型信号量的时候,当资源S不足时,进程就会进入忙等,无法继续执行。这个时候创建一个struct,里面存放的S值,还存放了一个链表,里面存放了无法获取资源的进程,将它阻塞。
typedef struct {
int value;
struct process_control_bolck *list;
}semaphore;
wait(semaphore *S) {
S->value--;
if (S->value < 0) block (S->list);
}
signal(semaphore *S) {
S->value++;
if (S->value <= 0) wakeup(S->list);
}
不管有没有资源,只要请 求资源,就把资源数减一,减一后倘若此时并没有资源分配,则把进程阻塞。
当某一个进程释放资源后,S++,倘若此时S的值仍小于0,表明此时有进程被阻塞了,则唤醒进程,把资源分配给它。
3.4 信号量的应用
我们具体看一下信号量的使用场景和情况。
3.4.1 实现进程互斥
访问一个临界资源的时候,设置一个互斥信号量mutex即可,设其初始值为1,进行PV操作的时候都要判断mutex的值。
semaphore mutex = 1;
Pa() {
while (true) {
wait(mutex);
//临界区;
signal(mutex);
}
}
Pb() {
while (true) {
wait(mutex);
//临界区
signal(mutex);
}
}
要注意,PV操作是需要同时出现的,否则会造成混乱和错误。
3.4.2 实现前趋关系
p1() {S1; V(a); V(b)}
p2() {S2; P(a); V(c); V(d)}
p3() {S3; P(b); V(e);}
p4() {S4; P(c); V(f);}
p5() {S5; P(d); V(g);}
p6() {S6; P(e); P(f); P(g)}
main() {
semaphore a,b,c,d,e,f,g = 0;
p1();p2();p3();p4();p5();p6();
}
3.5 经典同步问题
下面分析一些比较经典的进程同步问题
3.5.1 生产者——消费者问题
问题背景是这样的,生产者在工作间不断生产产品,把生产的产品放在外面柜台的缓冲区中;消费者不断从柜台缓冲区中购买产品。要注意的是,没有产品则不能购买!
int in=0, out=0;
item buffer[n];
semaphore mutex=1, empty=n, full=0;
void preceducer() {
do {
// producer an item nextp;
// 生产
P(empty);
P(mutex);
buffer[in]=nextp;
in = (in+1) % n;
V(mutex);
V(full);
} while(TRUE);
}
void consumer() {
do {
P(full);
P(mutex);
nextc = buffer[out];
out = (out+1) % n;
V(mutex);
V(empty);
// consumer the item in netxc;
// 消费
}
}
void main() {
producer();consumer();
}
3.5.2 哲学家进餐问题
一个圆桌有五个哲学家和五个筷子,当他们要吃饭要拿起两只筷子,倘若每个人都拿起了一只筷子,将使得没人能吃饭!
semaphore choptick[5] = {1,1,1,1,1};
do {
P(chopstick[i]);
P(choptick[(i+1)%5]);
//eat
V(chopstick[i]);
V(choptick[(i+1)%5]);
//think
} while(true);
这个如何解决死锁问题呢?好几种解决方案
1):有一个人无法进餐
2):只有左右两双筷子都可用时才可进餐。
等等
3.5.3 读者写者问题
当在读的时候,写入数据将会导致错误,在写的时候同理。
semaphore rmutex=1, wmutex=1;
int readcount = 0;
void reader() {
do {
P(rmutex);
if (readcount==0) P(wmutex);
readcount++;
V(rmutex);
//read
P(rmutex);
readcount--;
if (readcout==0) V(wmutex);
V(rmutex);
} while(true);
}
void writer() {
do {
P(wmutex);
//read
V(wmutex);
}while(true);
}
void main() {
reader();writer();
}
对写不做过多限制,主要限制读方面
当有人读,是不允许进行写操作的,完成读后,再检查一遍还有没有人读,反之仍不允许写。
4. 线程
4.1 线程引入
引入进程的目的是实现并发,而再引入线程的目的则是为了减少进程切换时的开销。由于进程是拥有资源的独立单位,并且为了方便管理,还有引入PCB。因此切换进程时,会造成大量的资源浪费。需要保留当前处理机环境,切换到上一个处理机环境中。因此设置一个进程的子单位,不具有资源的线程,会大大减少开销。
线程是作为调度的基本单位。
void writer() {
do {
P(wmutex);
//read
V(wmutex);
}while(true);
}
void main() {
reader();writer();
}
对写不做过多限制,主要限制读方面
当有人读,是不允许进行写操作的,完成读后,再检查一遍还有没有人读,反之仍不允许写。
4. 进程间通信
进程之间的信息交换,实际上进程同步时就已经有进程通信。进程之间通过信号量机制进行同步,进程间的互斥,同步信息,这本身就是一个通信过程。 但是基于信号量的机制的进程通信它所能传递的信息少,并且用户难以利用该方式进行进程通信。因此应该设计一个更高级的进程通信方式。
主要分三大类:
- 共享存储器系统
- 消息传递系统
- 管道通信系统
4.1 进程通信类型
4.1.1 共享存储器系统
相互通信的进程通过共享某个数据结构或者存储区来进行通信。
共享数据结构:生产者——消费者问题所设置的缓冲区就是通过这种方式实现的。通过一个公用的缓冲区进行进程同步以及通信。
但是这种方式需要程序员来管理数据结构,增加了设计代码量和难度,通信方式也相对低效,只能传递少量数据。
共享存储区:在存储器中划出一块区域,进程可以对这块区域进行读写来实现通信。
4.1.2 消息传递系统
该方法是目前最主要最广泛使用的方式。进程间的通信是使用格式化的信息,在计算机网络中,也称为报文。程序员利用操作系统所提供的通信命令(原语)来进行通信,是一种高级通信方式。其实现方式也分为直接通信和简介通信两种。
4.1.3 管道通信系统
管道是指连接两个进程间的一个共享文件,以实现通信,也称pipe文件。
发送进程以字符流的形式将大量数据送入管道,接收进程则从中接受数据。为了协调双方的通信,管道需要提供以下三个功能:
- 互斥
- 同步
- 建立连接
因为是面向连接的通信,首要确保对方是否存在,不然通信通了个寂寞。
其次,进程在进行读/写操作时,其他进程需要等待。
还有同步,当读进程在休眠时,写进程写完后,需要唤醒读进程。反之亦然。
4.2 消息传递通信的实现方法
利用发送进程将消息发送给目标进程,通常分为直接或间接两种方式。
4.2.1 直接通信方式
发送进程利用OS提供的发送命令(Send),直接把消息发送给目标进程,目标进程使用操作系统接收进程(Receive)进行接收。此时,两个进程都应该显示对方的标识符。
4.2.2 间接通信方式
进程之间的通信需要一个作为共享数据结构的实体。通常称之为信箱,发送进程把消息放入信箱中,接收进程从中取出。
4.3 消息传递系统实现中的若干问题
实际上这里面对的问题和计算机网络中实现双方通信面对的问题大差不差,相关解决办法都是一致的。
4.3.1 消息链路
便于进程双方的通信,需要在两者间建立一条通信链路。
4.3.2 消息缓冲队列通信机制
5. 线程
5.1 线程引入
引入进程的目的是实现并发,而再引入线程的目的则是为了减少进程切换时的开销。由于进程是拥有资源的独立单位,并且为了方便管理,还有引入PCB。因此切换进程时,会造成大量的资源浪费。需要保留当前处理机环境,切换到上一个处理机环境中。因此设置一个进程的子单位,不具有资源的线程,会大大减少开销。
线程是作为调度的基本单位。