OS复习-进程同步

一些概念:

临界区 | 互斥量 | 信号量(信号灯) | 事件


使用有限缓冲方案的的消费者-生产者模型,不能正确的并发执行,保证不了进程间同步,比如counter++/counter--语句:

/* producer */
while(1)
{
	while(counter == BUFFER_SIZE)
		; // 缓冲区满,等待
	buffer[in] = nextProduced;
	in = (in + 1) % BUFFER_SIZE;
	counter++;
}
/* consumer */
while(1)
{
	while(counter == 0)
		; //缓冲区空,等待
	nextConsumed = buffer[out];
	out = (out + 1) % BUFFER_SIZE;
	counter--;
}

临界区:(可以理解为必须保证原子性,即实现进程间同步的代码部分)每个进程有一个代码段,在该区域可以修改共同变量、更新共享的文件等。每一时刻只能允许一个进程进入(满足这一要求的代码称为进入区,entry section),之后还可有退出区 exit section,其他代码为剩余区 remainder section。临界区必须满足的三项要求:互斥有空让进(即前进原则:临界区空,那些不在剩余区的进程参与决策,不能无限延迟)、有限等待

实现同步功能的几种方式:纯软件实现会造成忙等待busy waiting,即等待进程都在无限循环,浪费了CPU时钟,也叫自旋锁spinlock,自旋锁的一点优点是可以避免上下文切换,这在等待时间较短时比较有用,可以保证内核不被多个进程抢占,可以参考这里这里这里这里)、硬件原子指令信号量

两进程的进入区和退出区代码(依赖软件实现):

// initial section
// ... ...
boolean flag[2];
int turn;
flag[0] = flag[1] = false;

do{
    // entry section
    flag[i] = true;
    turn = j; //因为先申请进入临界区进程的turn值会被后申请的改写,这样反写可以使得先申请进入临界区的进程有可以首先进入。保证了“有限等待”原则
    while(flag[j] == true && turn == j) ;//陷入循环等待

    // critical section
    // ... ...

    // exit section
    flag[i] = false;

    // remainder section
    // ... ...
}while(1); 
多进程的进入区和退出区代码(又叫做 面包店算法,思想是 排队号码+进程id 来决定谁进入):
// initial,其中N+1表示进程id,共N个进程(没有0号进程)
// entering[i]为真,表示进程i正在获取它的排队号码
boolean entering[N+1];
// number[i]表示进程i的排队号码,为0时表示未参加排队,既不想获取共享资源。该值没有上限
int number[N+1];
for(int i = 0; i <= N; i++)
{
      entering[i] = false;
      number[i] = 0;
} 

/*假设不使用entering数组,那么就可能会出现这种情况:设进程i的优先级高于进程j(即i<j),
两个进程获得了相同的排队登记号(number数组的元素值相等)。进程i在写number[i]之前,
被优先级低的进程j抢先获得了CPU时间片,这时进程j读取到的number[i]为0,因此进程j进入了临界区. 
随后进程i又获得CPU时间片,它读取到的number[i]与number[j]相等,且i<j,因此进程i也进入了临界区。
这样,两个进程同时在临界区内访问,可能会导致数据腐烂(data corruption)。
算法使用了entering数组变量,使得修改number数组的元素值变得“原子化”,解决了上述问题。
*/

lock(integer i)
{
      // entering实现原子操作(纯软件实现)
      entering[i] = true;
      number[i] = 1 + max(number, N); //每次获取排队号码都是原来的最大值加1
      entering[i] = false;
      for(int j = 1; j <= N; j++)
      {
            while(entering[j]); //等待正在获取排队号码的进程
            while((number[j]!=0) && (number[j],j)<(number[i],i));//等待排队号码比当前进程的小,或者相同号码进程优先权高(id小)的进程进入临界区
      }
}

unlock(integer i)
{
      number[i] = 0;
}

Thread(integer i)
{
      while(true)
      {
            lock(i);//entry section
            
            //critial section
            // ... ...

            unlock(i);//exit section

            //remainder section
            // ... ...
      }
}

同步硬件:许多系统提供特殊硬件指令以允许原子的(如同不可中断一样)检查和修改或交换某个地址的内容,可以借助这些指令实现互斥。

信号量:信号量S是个整数变量,除了初始化外,只能通过两个标准原子操作wait(P,表示测试)和signal(V,表示增加)来访问。一种克服忙等待的wait和signal的实现方式:当进程执行wait操作时发现信号量非正时必须等待,但该进程不是忙等而是阻塞自己。阻塞操作将一个进程放入到与信号量相关的等待队列中,该进程的状态被切换为“等待”。该进程在其他进程执行signal操作后被wakeup重新执行。具体如下:

typedef struct
{
      int value;
      struct process *L;
}semaphore; //每个信号都有一个整数值和一个进程链表

void wait(semaphore S)
{
      S.value --;
      if(S.value < 0) //不是忙等待,而是阻塞
            add this process to S.L;
            block(); //阻塞
} 

void signal(semaphore S)
{
      S.value ++;
      if(S.value <= 0) //如果value为负数,那么其绝对值就是等待该信号量的进程数
            remove a process P from S.L;
            wakeup(P);
}

实现wait和signal原子操作的方式,单CPU可以简单的禁止中断,多CPU必须由硬件提供特殊的原子指令。实际操作系统可能会使用二进制信号量(只有0和1),因为更容易实现。

另外,信号量的使用很不灵活,假如不正确使用的话很容易出现计数错误和各种类型的错误。为了解决这些问题,有一些高级语言构造,比如:临界区域管程

死锁:多个进程无限等待一个事件(signal),而该事件只能由这些等待进程之一来产生。产生死锁的四个条件:1)互斥条件;2)请求和保持条件;3)不剥夺条件;4)环路等待条件。

饥饿(无限期阻塞):进程在信号量内无穷等待,比如进程链表按照LIFO顺序来添加和删除进程。

经典同步问题:1)生产者-消费者的有限缓冲模型;2)读者-作者问题(只读和只写,书上的例子是一对多问题,容易造成作者饥饿);3)哲学家吃饭问题(书上的例子容易造成死锁)



Ref: 《操作系统概念》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值