信号量临界区保护
什么是信号量?
通过对这个量的访问和修改,让大家有序推进。
为啥需要保护信号量?
既然大家都要修改它,那它肯定有并发问题(逼格高点👍:竞态条件),因此,需要保护信号量
不保护会出现的问题?
由于CPU时间片的调度,共享数据(信号量),将出现语义错误。以生产者-消费者为例,假设有两个生产者p1,p2,信号量sem=n
若在改变p1时,时间片发生调度,将可能产生以下次序:
p1.register = sem
p1.register = p1.register - 1
p2.register = sem
p2.register = p2.register - 1
sem = p2.register
sem = p1.register
此时sem=-1,信号量的语义将出现问题🤦♂️
如何解决这种问题
分析问题:这个问题源于有多个线程去竞争修改信号量,由于修改过程不具备原子性,因此出现了修改一半,时间片就结束,最终不合时宜的运行顺序导致sem的语义出现错误
解决想法:🤷♀️它不体面,就帮他体面,让信号量修改时具备原子性,我们抽象出一个🔒,它锁住的区域叫做临界区,该区域内的代码片段运行具备原子性
| p1 | p2 |
|---|---|
| 🔒 | |
| p1.register = sem | |
| p1.register = p1.register - 1 | |
| 检查🔒,发现没有,等待把 | |
| sem = p1.register | |
| 解锁 | |
| p2.register = sem p2.register = p2.register - 1 sem = p2.register |
临界区
临界区是指一次只允许一个进程进入的该进程的那一段代码,而🔒保护的是临界区在被使用时不被侵犯,一般设置锁之前需要确定临界区的范围
临界代码的保护原则
基本原则:
- 互斥进入:如果一个进程在临界区等待,则其他进程不允许进入
- 有空让进:若干个进程需要进入临界区时,应尽快使一进程进入临界区
- 有限等待:从进程发出进入请求到允许进入,不能无限等待(避免饥饿)
实现方式
一、轮转法
# P0
while(turn == 0); #空转
# 临界区
turn = 1
# 剩余区
# P1
while(turn == 1);
# 临界区
turn = 0
# 剩余区
轮转法,也作值日法,就是一替一次。但是有个问题,假设p0运行完毕,此时临界区不存在任何进程,时间片又再一次分配给P0,该进程此时将无法再次进入,不满足有空等待这一基本原则
二、标记法
# P0
flag[0] = true
while(flag[1] == true);
flag[0] = false
# p1
flag[1] = true
while(flag[0] == true);
flag[1] = false
标记法,不当的时间片切换,将出现死锁的情况
三、Peterson算法
结合标记和轮转两种思想,在标记的基础上增加轮转值(非对称标记),使得无论如何都不会产生死锁的情况
# P0
flag[0] = true
while(flag[1] == true && turn == 1);
# ===
turn = 1
flag[1] = true
# p1
flag[1] = true
while(flag[0] == true && turn == 0);
# ===
turn = 0
flag[0] = true
验证:
- 满足互斥进入:如果两个进程都进入临界区,此时
flag[0]=flag[1]=true turn=0=1,不成立 - 满足有空让进:如果p1不在临界区,此时
flag[0]=true or turn = 0,p0都有机会进入 - 满足有限等待:p0要求进入时,p1不会一直进入,因为p1执行一次就将
turn=0,因此p0不会饥饿
多进程临界区保护算法—面包店算法
仍然是标记与轮转结合:
- 轮转:每个进程获得一个序号,序号最小的进入
- 标记:进程离开时标记为
num[i]=0
正如面包店一样:进店买单、取号,买单是指得到标记,取号表示加入轮转
# pi 共有n个进程
# 取号
choosing[i] = true
num[i] = max(num[0],num[n-1]) + 1
# 加入轮转
choosing[i] = false
for(j=0;j<n;j++){
while(choosing[j]);# 有人在选号停一停
while((num[j]!=0) && (num[j]<num[i]));
}
# 临界区
nums[i] = 0
正确性分析:
- 互斥进入:
pi在临界区,pk要进入,此时一定有num[i]<num[k]),pk循环等待 - 有空让进:如果进程没在临界区,最小序号的进程一定能进入
- 有限等待:刚离开临界区的进程,将再次排到最后,所以任意一个想再次进入临界区的进程,至多等待n个进程
临界保护的另一类解法—硬件下场
临界区被破坏一个重要的原因就是被调度,而被调度一个主要因素就是时钟中断,那就靠硬件阻止中断,也就能阻止调度。
硬件给了我们一个很好的指令
cli()
# 临界区
sti()
该指令,其实将控制CPU中断的寄存器INTR关闭,可以直接关闭单CPU的时钟中断
弊端:多CPU不好使
临界区保护的硬件原子指令
主要思想:上锁—修改信号量—开锁
利用原子指令,原子执行修改🔒变量的代码(需要硬件的支持)
TestAndSet(boolean &x){
# 一次执行
boolean rv = x;
x = true;
return rv;
# 不中断
}
while(TestAndSet(&lock));
# 临界区
lock = false
信号量的代码实现
// 概念程序
Producer(item){
p(empty);
...
v(full)
}
// 真实程序 producer.c
main(){
sd = sem_open("empty") // 申请信号量,需要进入内核态,因为要申请PCB队列
for(i=1 to 5)
sem_wait(sd) //判断是否有空闲缓冲区
write(fd,&i,4)//在文件写出这5个数,每个数四个字节
}
// sem_open sem.c
typedef struct {
char name[20];
int value;
task_struct *queue;
} semtable[20] // 定义全局数组
sys_sem_open(char *name){
//在semtable中寻找name对上的;
//没找到就创建;
//返回对应下标
}
// sem_wait
sys_sem_wait(int sd){
cli(); //单CPU
if(semtable[sd].value -- < 0){
//设置自己为阻塞 cur.state = '阻塞'
//将自己加入semtable[sd].queue
//schedule()
}
sti();
}
操作系统内部存在的同步代码
读磁盘块
bread(int dev, int block){
struct buffer_head *bh;//申请一段空闲缓冲区
l1_rw_block(READ,bh);//启动读命令
wait_on_buffer(bh);//阻塞
}
启动磁盘读命令后睡眠,等待磁盘读完之后,由磁盘中断将其唤醒
lock_buffer(buffer_head *bh){
cli();
while(bh -> b_lock != 1) // 特色:没有负数,while检测
sleep_on(&bh->b_wait);
bh->b_lock = 1;
sti();
}
void sleep_on(struct task_struct **p){
struct task_struct *tmp;
temp = *p;
*p = current;
current->state = TASK_UNINTERRUPTIBLE;
schedule();
//tmp 下一个进程信息
if(tmp)
tmp->state = 0;
}
利用栈存储局部变量temp,最终找到下一个task_struct
唤醒过程
磁盘中断,将队首唤醒,队员再按次序依次唤醒后边的进程
- while 是将阻塞队列全部唤醒的机制,不需要有负数,因为它不需要记录等待队列有多少的进程需要被唤醒(全部唤醒)
- if 是将阻塞队列中第一个唤醒的机制
所有进程全部唤醒的原因:有时队列后方进程的优先级更高,需要优先执行
static void read_intr(void){
...
end_request(1);
}
end_request(int uptodate){
...
unlock_buffer(CURRENT->br);
}
unlock_buffer(struct buffer_head *bh){
bh->b_lock = 0;
wake_up(&bh->b_wait);
}
wake_up(struct task_struct **p){
if(p && *p){
(**p).state=0;
// 出队
*p = NULL;
}
}

浙公网安备 33010602011771号