文章目录
- 1 信号量机制
- 1.1 整形信号量
- 1.2 记录形信号量
- 1.3 信号量机制小结
- 2 用信号量机制实现进程互斥、同 步、前驱关系
- 2.1 信号量机制实现进程互斥
- 2.2 信号量机制实现进程同步
- 2.3 信号量机制实现前驱关系
- 2.4 信号量机制实现进程互斥、同 步、前驱关系小结
1 信号量机制
- 用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现了进程互斥、进程同步。
- 信号量其实就是一个变量 ,可以用一个信号量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初值为1的信号量。
- 原语是一种特殊的程序段,其执行只能一气呵成,不可被中断。原语是由关中断/开中断指令实现的。
- 软件解决方案实现临界区的互斥主要问题是由“进入区的各种操作无法一气呵成”,因此如果能把进入区、退出区的操作都用“原语”实现,使这些操作能“一气呵成”就能避免问题。
一对原语:wait(S)
原语和signal(S)
原语,可以把原语理解为我们自己写的函数,函数名分别为wait
和signal
,括号里的信号量S其实就是函数调用时传入的一个参数。
wait、signal
原语常简称为P、V
操作。因此常把 wait(S)、signal(S)
两个操作分别写为P(S)、V(S)
1.1 整形信号量
用一个整数型的变量作为信号量,用来表示系统中某种资源的数量。
int S = 1; //初始化整形信号量s,表示当前系统中,某种可用资源数
wait(S){ //wait原语,相当于“进入区”while(S<=0); //若资源数不够用,则一直循环等待S=S-1; //若资源够用,则占用一个资源}
signals(S){ //signals原语,相当于“退出区”S=S+1; //使用完资源后,在退出区释放资源}
整形信号量:
- “检查”和“上锁”一气呵成, 避免了并发、异步导致的问题
- 存在的问题:不满足“让权等待” 原则,会发生“忙等”
1.2 记录形信号量
整型信号量的缺陷是存在“忙等”问题,因此人们又提出了“记录型信号量”,即用记录型数据结构表示的信号量。
/*记录型信号量的定义*/
typedef struct {int value; //剩余资源数struct process *L; //等待队列
} semaphore
若某个进程需要使用资源时,通过wait
原语申请:
void wait( semaphore S){ //相当于申请资源S.value--;if(S.value<0){bolck(S.L) //如果剩余资源数不够,使用block原语使进程从运行态进入阻塞态,并把挂到信号量S的等待队列(即阻塞队列)中}
}
进程使用完资源后,通过signal
原语释放:
void signal ( semaphore S ){ //相当于释放资源s.value++;if(S.value<=0){wakeup(S.L);//释放资源后,若还有别的进程在等待资源,则使用wakeup原语唤醒等待队列中的一个进程,该进程从阻塞态变为就绪态}
}
说明:
S.value
的初值表示系统中某种资源的数目。- 对信号量S的一次P操作意味着进程请求一个单位的该类资源,因此需要执行
S.value--
,表示资源数减1,当S.value<0
时表示该类资源已分配完毕,因此进程应调用block原语进行自我阻塞(当前运行的进程从运行态变成阻塞态),主动放弃处理机,并插入该类资源的等待队列S.L
中。可见,该机制遵循了“让权等待”原则, 不会出现“忙等”现象。- 对信号量S的一次V操作意味着进程释放一个单位的该类资源,因此需要执行
S.value++
,表示资源数加1, 若加1后仍是S.value<=0
,表示依然有进程在等待该类资源,因此应调用wakeup
原语唤醒等待队列中的第一个进程(被唤醒进程从阻塞态变成就绪态)。
1.3 信号量机制小结
信号量的值=这种资源的剩余数量(信号量的值如果小于0,说明此时有进程在等待这种资源)
P(S)——申请一个资源S,如果资源不够就阻塞等待
V(S)——释放一个资源S,如果有进程在等待该资源,则唤醒一个进程
2 用信号量机制实现进程互斥、同 步、前驱关系
2.1 信号量机制实现进程互斥
思想步骤:
- 分析并发进程的关键活动,划定临界区(如:对临界资源打印机的访问就应放在临界区)
- 设置互斥信号量
mutex
,初值为1- 在进入区
P(mutex)
——申请资源- 在退出区
V(mutex)
——释放资源
代码实现:
/*记录型信号量定义*/
typedef struct {int value; //剩余资源数struct process *L; //等待队列
} semaphore;/*信号量机制实现互斥*/semaphore mutex=1; //初始化信号量P1(){...P(mutex); //使用临界资源前需要加锁临界区代码段...V(mutex); //使用临界资源后需要解锁...
}P2(){...P(mutex); //使用临界资源前需要加锁临界区代码段...V(mutex); //使用临界资源后需要解锁...
}
注意问题:
- 要会自己定义记录型信号量,但 如果题目中没特别说明,可以把信号量的声明简写成“
semaphore mutex=1;
”这种形式- 对不同的临界资源需要设置不同的互斥信号量。
P、V
操作必须成对出现。缺少P(mutex)
就不能保证临界资源的互斥访问。缺少V(mutex)
会导致资源永不被释放,等待进程永不被唤醒。
2.2 信号量机制实现进程同步
进程同步:要让各并发进程按要求有序地推进。
比如,P1、P2并发执行,由于存在异步性,因此二者交替推进的次序是不确定的。
若P2的“代码4”要基于P1的“代码1”和“代码2”的运行结果才能执行,那么我们就必须保证“代码4”一定是在“代码2”之后才会执行。
这就是进程同步问题,让本来异步并发的进程互相配合,有序推进。
思想步骤:
- 分析什么地方需要实现“同步关系”,即必须保证“一前一后”执行的两个操作(或两句代码)
- 设置同步信号量S,初始为0
- 在“前操作”之后执行
V(S)
- 在“后操作”之前执行
P(S)
技巧口诀:前V后P
代码实现:
以下代码保证了P2
进程中代码4一定是在P1
进程中代码2之后执行:
/*信号量机制实现同步*/
semaphore S=0; //初始化同步信号量,初始值为0P1(){代码1;代码2;V(S);代码3;
}
P2(){P(S);代码4;代码5;代码6;
}
注意问题:
semaphore S=0
理解:信号量S代表“某种资源”,刚开始是没有这种资源的。P2
需要使用这种资源, 而又只能由P1
产生这种资源- 若先执行到
V(S)
操作,则S++
后S=1
。之后当执行到P(S)
操作 时,由于S=1
,表示有可用资源,会执行S--
,S的值变回0,P2
进程不会执行block
原语,而是继续往下执行代码4- 若先执行到
P(S)
操作,由于S=0
,S--
后S=-1
,表示此时没有 可用资源,因此P操作中会执行block
原语,主动请求阻塞。 之后当执行完代码2,继而执行V(S)
操作,S++
,使S变回0, 由于此时有进程在该信号量对应的阻塞队列中,因此会在V 操作中执行wakeup
原语,唤醒P2
进程。这样P2就可以继续执行代码4了
2.3 信号量机制实现前驱关系
思想步骤:
其实每一对前驱关系都是一个进程同步问题(需要保证一前一后的操作) :
- 要为每一对前驱关系各设置一个同步信号量
- 在“前操作”之后对相应的同步信号量执行V操作
- 在“后操作”之前对相应的同步信号量执行P操作
进程
P1
中有句代码S1
,P2
中有句代码S2
,P3
中有句代码S3
……P6
中有句代码S6
。这些代码要求按如下前驱图所示的顺序来执行:
- 要为每一对前驱关系各设置一个同步信号量
- 在“前操作”之后对相应的同步信号量执行V操作,在“后操作”之前对相应的同步信号量执行P操作
前V后P V→P
2.4 信号量机制实现进程互斥、同 步、前驱关系小结