操作系统复习笔记(三)——Process synchronization

本贴最后更新于 2245 天前,其中的信息可能已经时过境迁

Process Synchronization 1

  • IPC(Interprocess communication)
    协作的进程需要 IPC

三种 IPC 模型

进程通信可以通过

  • shared memory 共享内存
  • pipe 管道
  • message passing 消息传递

Message passing

  • 建立一个 communicate link(physical/logical)
  • 通过 send/receive 交换

直接通信,对称寻址 Direct communication-symmetric addressing

  • 进程必须互相知道对方名字
    • send(P,message)
    • receive(Q,message)
  • 自动建立了 link 两个进程间只有一条,一条链路只有两个进程

直接通信,非对称寻址

  • 发送时指明了名字,接收只有 id
    • send(P,message)
    • receive(id,message)
      例如:共享打印机

直接通信的缺点:必须知道对方的名字,局限了模块化

间接通信

  • 消息从中介取,往中介发 mailbox or ports
    • send(A,message)
    • receive(A,message)

每个 mailbox 都有独立 id
一条链路连接多个进程

IPC synchronization

  • Blocking send :直到上一个消息被接收时才会发下一个消息
  • Non-blocking send :随时发
  • Blocking receive :一个消息来的时候 receiver 才工作
  • Non-blocking receive :随时取

Non-blocking 和 blocking receive 是最常见的

IPC buffering

link 的消息队列容量

  • Zero capacity 队列长度=0
  • Bounded capacity 队列长度=n
  • Unbounded capacity 队列长度=

Critical-Section Problem 临界区问题

对于共享数据的并行访问可能导致数据的不一致

Race condition 竞争条件

多个进程同时访问操作文件,最终结果取决于最后执行的指令
为了避免竞争条件,并行的进程必须是 mutual exclusion 互斥的

互斥

  • 如果一个进程正在使用一个共享文件或变量,其他进程不能做同样的事

The critical-section problem

每个进程拥有的操作共享数据的代码称为 critical section 临界区。要保证当一个进程执行时它的临界区代码时,其他进程不能执行临界区代码

解决方案:
在执行之前加点条件,判断能否执行,执行完毕后解锁

do{ entry section //执行前判断 crical section exit section //执行完毕后解锁 reminder section }

解决临界区问题需要满足

  • Mutual Exclusion 互斥
  • Progress 前进原则
    如果没有临界区代码在执行,有一个进程想进入临界区是,应当允许
  • Bounded wait 有限等待
    想进入临界区的在有限时间内总能进入

例 火车问题

前提

有两列相向而行的火车需要在某个地点占用同一段铁轨,如何保证两车进入不会相撞,且两车都能正常运行

算法 1 交替进入
int turn; turn=i //Pi可以进入

Pi

do { while(turn!=i); /* critical section */ turn=j; /* remainder section */ } while(1);

满足互斥,但不满足前进原则(只有一辆车运行的情况下)

算法 2 flag each process 给每个进程都标记
boolean flag[2]; flag[i]=true;

Pi

do { flag[i]=true; while(flag[j]); //空循环等待j /* critical sectioon */ flag[i]=false; /* remainder */ } while(1);

如果两个同时刻到,那么都进不去,不满足 Bounded waiting

算法 3 1 和 2 的组合
do { flag[i]=true; turn=j; //先把机会让给另外一个 while(flag[j] && turn==j); //如果另外一个也到了,就空循环等待 /* critical section */ flag[i]=false; /* remainder section */ } while(1);

三个要求都满足

Process Synchronization 2

Synchronization tools for IPC

  • Synchronization hardware
  • Semaphores
  • Critical Regions
  • Monitors

Synchronization hardware 硬件同步

很多系统提供了硬件对于临界区代码的支持

  • 单处理器可以禁止中断,使当前运行代码不会被抢占
  • 在多处理器上上述方案效率太低

现代计算机提供了特别的 原子的(atomic) 硬件指令

  • 检查和修改字的内容
  • 或交换两个字的内容

TestAndSet Instruction 检查和修改指令 自旋锁

方法:

boolean TestAndSet(boolean *target) { boolean rv=*target; *target=true; return rv; }

初始化:

boolean lock=false;

执行时:

do{ while(TestAndSet(&lock)); //critical section lock=false; //remainder section }

Swap Instruction

操作两个数据,与指令 TestAndSet 一样原子执行

void Swap(boolean *a,boolean *b) { boolean temp=*a; *a=*b; *b=temp; }

初始化:

boolean lock=false;

执行时:

do { key=true; while(key==true) Swap(&lock, &key); //critical section lock=false; //remainder }

Semaphores 信号量

Dijkstra 提出
信号量 一个整形变量,只能通过两个标准原子操作:

  • wait()
  • signal()

wait(s):

while(s<=0) ; //no-operation s--;

signal(s):

s++

在 wait()和 signal()操作中,对信号量的修改必须是原子的,即当一个进程修改信号量值时,不能有其他进程同时修改同一信号的值

初始化:

int matex=1;

Critical section of n processes

do { wait(mutex); //critical section signal(mutex); //remainder section } while(1);

两种类型的信号量

  • 计数信号量的值域不受限制,用来表示可用资源的数量
  • 二进制信号量的值只能为 0 或 1

信号量实现的分析

主要缺点是 忙等待(busy waiting)

忙等待

当一个进程位于其临界区内时,其他想进入临界区的进程必须连续循环等待,浪费了 CPU 时钟。也称为自旋锁。

解决方法

为了克服忙等待,可以使用阻塞并放入等待队列的操作。通过 wakeup()来重新执行

定义一个新结构

typedef struct { int value; struct process *List; } semaphore;
  • Block operation: block()
  • Wakeup operation: wakeup()

新的信号量操作定义

wait(semaphore *S){ S.value--; if(S.value<0) { block(); //add this process to S.List } } signal(semaphore *S) { S.value++; if(S.value<=0) { wakeup(P); //remove a process P from S.List } }

Bounded-Buffer Problem 可用计数信号量来解决

Deadlock and Starvation 死锁与饥饿

  • Deadlock: 两个或多个进程无限地等待一个事件
  • Starvation: 无限期阻塞

Critical Regions 临界域

一种高层同步结构

Monitor 管程

另一种高层同步结构

比信号量更高的抽象。或者说并没有这种实体存在于系统或编程语言中,更多的是一种机制,一种解决方法,但是编程语言和操作系统都提供了实现管程的重要部件条件变量

管程就是管理一组共享变量的程序。主要有一下几部分组成

mutex // 一个互斥锁,任何线程访问都必须先获得mutex condition //一个或者多个,每个条件变量都包含一个等待队列 balabala //共享变量及其访问方法

特点

  • 采用面向对象方法,简化线程同步
  • 同一时刻仅有一个线程在管程中工作
  • 可临时放弃管程的访问权,叫醒一个在等待队列中的线程,这是其他方法都没有的(原子锁和信号量一旦进入临界区就必须执行完临界区代码才能退出),而实现这一点采用的就是条件变量

类型

  • Hansan 管程
  • Hoare 管程

实现

管程类型提供了一组由程序员定义的、在管程内互斥的操作

monitor monitor-name { shared variable declarations procedure body P1() { } procedure body Pn() { } {initialization code} }

同一时刻管程内只有一个进程在活动,因此不需显式地编写同步代码

条件变量

为了让进程在管程内等待,必须声明一些 condition 类型的变量,即条件变量

条件变量两种操作

  • x.wait() 进程挂起
  • x.signal() 被挂起的进程重新活动

condition

注意

这里的 wait 和 P 操作是不一样的,最大的不同就是他一定会将自己阻塞同时释放锁。而 signal 如果没有等待线程相当于一个空操作。而且 numWaiting 不可能为负数

Process Synchronization 3

一些经典的同步问题

1. 读写者问题

前提条件

  1. 写者、读者互斥访问文件资源。
  2. 多个读者可以同时访问文件资源。
  3. 只允许一个写者访问文件资源。

两种情况

1. 读者优先
  • 没有读者会因为有一个写者在等待而去等待其他读者的完成
  • 写者执行写操作前,应该让所有读者和写者退出
  • 除非有一个写者在访问临界区,其他情况下,读者不应该等待
2. 写者优先

如果一个写者等待访问对象,那么不会有新读者开始读操作

读者优先的实现

初始化变量

BINARY_SEMAPHORE wrt=1; BINARY_SEMAPHORE mutex=1; int readcount=0;

Reader:

do { wait(mutex); //Allow 1 reader in entry readcount=readcount+1; if(readcount==1) wait(wrt); //1st reader locks writer signal(mutex); //reading operation wait(mutex); readcount=readcount-1; if(readcount==0) signal(wrt); //last reader frees writer signal(mutex); }

Writer:

do { wait(wrt); //writing operation signal(wrt); }

写者优先的实现

初始化变量

BINARY_SEMAPHORE read=1; //使有写者进行操作时读者等待 BINARY_SEMAPHORE file=1; //使文件操作互斥 BINARY_SEMAPHORE mutex1=1; //使改变readcount的方法互斥 BINARY_SEMAPHORE mutex2=1; //使改变writecount的方法互斥 int readcount=0; int writecount=0;

Reader:

do { wait(read); //等待直至读者队列没有阻塞,即全部写者都退出,同时自身进入后再次上锁,使后来的读者等待,因为接下来要进行一系列互斥的操作 wait(mutex1); readcount++; if(readcount==1) //第一个读者进入 wait(file); //后来的写者无法操作文件,但对后来的读者的文件操作不造成影响 signal(mutex1); signal(read); //释放 //read operation wait(mutex1); readcount--; if(readcount==0) signal(file); signal(mutex1); }

Writer:

do { wait(mutex2); writecount++; if(writecount==1) //第一个写者进入 wait(read); //阻塞读者队列 signal(mutex2); wait(file); //write operation signal(file); wait(mutex2); writecount--; if(writecount==0) //最后一个写者退出 signal(read); //释放读者队列 signal(mutex2); }

哲学家进餐问题

前提条件

5 个哲学家围着桌子吃饭,筷子只有 5 枝,分别在每个哲学家的左手边和右手边。哲学家必须得到两只筷子才能吃饭,要使每个哲学家都能吃到饭,该如何调度

解决方法

只有左右两只筷子都可用时,才允许一个哲学家拿起它们

管程实现

void philosopher(int i) { //主方法 while(true) { thinking(); dp.pickup(i); eating(); dp.putdown(i); } } monitor dp { enum{thinking,hungry,eating} state[5]; condition self[5]; void pickup(int i) { state[i]=hungry; //设置本人为饥饿状态 test[i]; //测试左右两个人是否在吃 if(state[i]!=eating) //如果需要等待 self[i].wait(); //进入等待,在pickup方法处阻塞 } void putdown(int i) { state[i]=thinking; test((i+4)%5); test((i+1)%5); //本人放下筷子后,状态发生了变化,测试左右两个是否等待吃,如有,则给他们开始吃的机会(但也不一定能马上开始吃) } void test(int i) { if((state[(i+4)%5]!=eating) && (state[i]==hungry) && (state[(i+1)%5]!=eating)) { //如果左右两个人并没有在吃,而且自己处于饥饿状态 state[i]=eating; //设置状态为eating self[i].signal(); //释放pickup方法,在philosopher方法中下一步将执行eating操作 } } initializationCode() { for(int i=0;i<5;i++) state[i]=thinking; } }

生产者消费者问题

前提条件

一个生产者,一个消费者,库存有限,如何能持续生产

解决思路

对于生产者,如果缓存是满的就去睡觉。消费者从缓存中取走数据后就叫醒生产者,让它再次将缓存填满。若消费者发现缓存是空的,就去睡觉了。下一轮中生产者将数据写入后就叫醒消费者。

不完善的解决方案会造成“死锁”,即两个进程都在“睡觉”等着对方来“唤醒”。

伪代码

初始化变量:

semaphore mutex=1; //临界区互斥信号量 semaphore empty=n; //空闲库存空间 semaphore full=0; //库存初始化为空

生产者进程:

producer () { while(1) { produce an item in nextp; //生产数据 P(empty); //获取空闲库存单元 P(mutex); //进入临界区. add nextp to buffer; //将数据放入缓冲区 V(mutex); //离开临界区,释放互斥信号量 V(full); //库存数加1 } }

消费者进程:

consumer () { while(1) { P(full); //获取库存数单元 P(mutex); // 进入临界区 remove an item from buffer; //从库存中取出数据 V (mutex); //离开临界区,释放互斥信号量 V (empty) ; //空闲库存数加1 consume the item; //消费数据 } }

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...