[操作系统概念]第四部分——同步

进程同步

一些概念:
* 竞争条件: 多个进程并发访问和操作公共变量,并且执行结果和访问数据有关的情况。
* 临界区:这个区间进程的代码会改变公共变量.临界区有一个特征:一个时间只有一个进程可以进入临界区,进而不会导致公共变量混乱。进入临界区的进程必须要先请求进入临界区以便协调。
* 实现请求进入临界区的代码段称为进入区。

  • 处理临界区问题,算法一定要满足三个条件:互斥(只有一个进程可以进入临界区)、前进(多个请求进入临界区的进程一定有一个可以进入临界区,并且这个决策不能无线推迟)、有限等待(当一个进程请求进入临界区,直到这个请求被允许,其他进程进入临界区的次数有上限)

  • 操作系统的编写,需要处理内核的临界区问题,对于这个问题,非抢占内核不存在竞争条件,所以没有临界区问题,而抢占内核存在临界区问题,而且在多处理器问题更甚。

几种解决临界区问题的方法

  1. Peterson算法:基于软件的经典的临界区算法
//假设有两个进程,这是其中之一,另一个将i与j更换位置即可
do{
    flag[i]=true;
    turn=j;
    while(flag[j]&&trun==j);//没有循环体
    //临界区代码
    flag[i]=false;
    //剩余代码
}while(true);
  1. 基于硬件的锁
    如果硬件提供原子性的检测临界区条件并且进入临界区的操作,那么代码可以简单地使用基于硬件的命令

  2. 基于软件的信号量
    信号量是一个整数,除了初始化之外,还有wait和signal两个原子性的函数可以操作这个整数,wait的用途是将信号量(整数)自减,如果信号量不大于0,则等待(循环忙等),signal用途是将信号量自增。将信号量作为系统的某一种资源,使用wait和signal操作,即可实现申请该资源,如果资源富裕则使用,空缺则等待,使用完则归还资源的目的。
    演示代码如下

//可能的wait和signal函数的实现
void wait(int s){
    while(s<=0);//忙等
    s--;
}//需要注意,这个函数中   操作都需要是原子性的
void signal(int s){
    s++;
}
//信号量使用实例
do{
    wait(mutex);//原子性操作,检测信号量mutex是否为正,为正则减1,否则在此处阻塞等待
    //临界区代码
    signal(mutex);//原子性操作,将信号量mutex加1
}while(true);

另外,信号量的两个操作以前也叫做P、V原语
* 信号量分为两种:计数信号量(信号量值域不受限,可正可负),二进制信号量(信号量值域只有0、1,也成为互斥锁)。
* 信号量的wait函数的设计中,如果信号量不大于0,则会阻塞在循环中忙等,这样的设计又被成为自旋锁,自旋锁有缺点,它浪费了CPU的时间,但也有优点,它避免了上下文的切换。如果上下文切换很耗时,那么使用自旋锁是一个处理的方法,自旋锁常用于多处理器的场景。
* 一个克服忙等的方法是,如果wait函数中,发现信号量不大于0,则让本进程等待,让出CPU,知道这个信号量在其他进程中被执行signal操作,这个时候唤醒等待的信号量进程。这样的设计,需要有一个队列,储存等待信号量的进程PCB,然后在signal函数中检测,如果信号量大于0,则唤醒一个队列中的进程继续执行。

几种经典的同步问题

有限缓冲问题(生产者消费者为题)

说明:有一个缓冲区(里面存放若干缓冲项),一个进程生产资源(生产缓冲项),另一个进程消费资源(消耗缓冲项),当缓冲项数目有限时,为有限缓冲。这里需要考虑的是,对于缓冲区的操作应当是互斥操作,生产消耗缓冲项也可以当作一个计数信号量处理。所以可以有以下代码:

/* 首先定义三个信号量:mutex、full、empty
 * mutex代表进入临界区的二进制信号量,初始化为1
 * full代表生产缓冲项的信号量,初始化为0
 * empty代表消耗缓冲项的信号量,初始化为有限缓冲的上限n
 * 这里的一个点是,对缓冲项的生产、消费,实现是操作了两个实际不相关的信号量(程序不出错的情况下二者有full+empty=n的关系)
*/
//生产者代码
do{
    //生产一个缓冲项
    wait(mutex);
    wait(empty);
    //将缓冲项加入缓冲区
    signal(full);
    signal(mutex);
}while(true);

//消费者代码
do{
    wait(mutex);
    wait(full);
    //从缓冲区消费一个缓冲项
    signal(empty);
    signal(mutex);
}while(true);

读者写者问题

说明:在公用一个资源,比如数据库、文件、数据的时候,查看读取并不会影响其他的进程,修改会影响其他的进程,所以提出了读者和写者的区别,一般而言,要允许多个读者同时读,但同一时间只有一个写者可以写。
读者写者问题有诸多变种,都与优先级有关,比如第一读者写者问题(读者优先,新的读者不会因为有写者的等待请求而不使用公用资源,也就是说写者可能一直等不到资源),第二读者写者问题(写者优先,如果有写者等待,那么新的读者只会等待写者操作资源完成后再读)
下面是一个读者优先的读者写者问题的解决示例:

/* 首先定义两个信号量:mutex和wrt,以及一个int型变量readCount
 * mutex代表更新readCounter的互斥信号量,初始化为1
 * wrt代表读和写的互斥信号量,初始为1
 * readCounter代表读者的个数,更新它的操作是临界区操作,需要使用mutex信号量,初始化为0
*/
//写者进程
do{
    wait(wrt);
    //写者操作代码
    signal(wrt);
}while(true);

//读者代码
do{
    wait(mutex);
    readCount++;
    if(readCount==1){
        wait(wrt);
    }
    signal(mutex);
    //读者操作代码
    wait(mutex);
    readCount--;
    if(readCount==0){
        signal(wrt);
    }
    signal(mutex);
}

哲学家进餐问题

说明:哲学家进餐说的是一种情况:在圆桌上有5个人,他们用筷子吃饭,但是每一个人面前只有一只筷子,这样的话,5个人就不能一块儿吃饭,只能一部分人得到两只筷子之后才能吃饭,此外还要保证5个人都有机会得到两只筷子,不会一直等待直到饿死或者死锁。那么如何管理5个人拿筷子就是一个问题了。

管程

管程是一种高级的同步构造,信号量使用不正确会导致不容易发现的错误,为了克服信号量的问题,才提出了这样的语言构造。

原子事务

临界区问题的解决保证了临界区的执行是原子的,如果两个临界区操作并发的执行,其结果是二者按照某一次序顺序执行。
数据库系统很关注原子执行的问题。执行单个逻辑功能的一系列指令或者操作成为 事务 ,事务的执行要求保持原子性,一起成功或者一起失败。为了保证这一点,引入了 提交 (执行成功的事务)和 撤销 (执行失败的事务)。撤销的事务需要回滚,如何回滚,回滚到什么时候呢?一种方法是使用基于日志的恢复。
在每一次操作之前,首先要记录日志,记录下事务的所有操作和新值旧值。如果事务执行失败,则从日志中查找出没有标记结束的日志,恢复到旧值( undo 操作),或者重新执行,更新数据到新值( redo 操作),至于到底是哪一个操作,需要看事务失败的类型。
因为检测日志耗时耗力,因此引入了检查点,失败的事务只需要从最近时间的检查点开始恢复就行。

并发原子操作

若干个原子操作并发的执行,其结果是顺序执行,称为 串行化 。然而,并不是非串行化的操作一定产生错误,只有特定持续的执行序列才会导致并发操作产生错误,这里产生错误的操作序列成为 冲突操作 (如果两个事务共同访问一个数据项并且至少有一个write操作,则两个事务有冲突操作)。冲突操作的解决需要 冲突可串行化 ,即如果两个事务的顺序执行可以通过一系列非冲突操作的交换得到另一个执行顺序,那么这个另一个顺序称为冲突可串行化,这样的执行序列不会产生错误。
串行化能力
为了确保串行化能力,需要一些方法,这里提出两种协议: 两段加锁协议 (增长阶段只能获得锁,收缩阶段只能释放锁,这种协议可确保冲突串行化,但是不能避免死锁), 基于时间戳的协议 (这种协议避免了死锁,且确保了可串行化)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值