操作系统概念学习笔记 11 进程同步(一)

操作系统概念学习笔记 11

进程同步(一)



互相协作的进程之间有共享的数据,于是这里就有一个并发情况下,如何确保有序操作这些数据、维护一致性的问题,即进程同步。

从底层到高级应用,同步机制依次有临界区、信号量、管程、原子事务。

多个进程并发访问和操作同一数据且执行结果与访问发生的特定顺序有关,称之为竞争条件(race condition)

临界区(critical section)

每个进程有一个代码段称为临界区(critical section),在该区中进程可能改变共同变量、更新一个表或写一个文件等。这种系统的重要特征是当一个进程进入临界区,没有其他进程可被允许在临界区内执行,即没有两个进程可同时在临界区内执行。

临界区问题(critical-section problem)是设计一个以便进程协作的协议。每个进程必须请求允许进入其临界区。实现请求的代码段称为进入区(entry section)临界区之后可有退出区(exit section),其他代码段成为剩余区(remainder section)

一个典型进程Pi的通用结构:

do{

进入区

临界区

退出区

剩余区

}while(TRUE)

临界区问题的解答必须满足三项要求:

  • (1)互斥(mutual exclusion)
    如果进程Pi在其临界区内执行,那么其他进程都不能在其临界区内执行;
  • (2)前进(progress)
    如果没有进程在其临界区内执行且有进程需进入临界区,那么只有那么不在剩余区内执行的进程可参加选择,以确定谁能下一个进入临界区,且这种选择不能无限推迟;
  • (3)有限等待(bounded waiting)
    从一个进程做出进入临界区的请求,直到该请求允许为止,其他进程允许进入其临界区内的次数有上限。

一个操作系统,在某个时刻,可同时存在有多个处于内核模式的活动进程,因此实现操作系统的内核代码,会存在竞争条件。内核开发人员有必要确保其操作系统不会产生竞争条件。

有两种方法用于处理操作系统内的临界区问题:

抢占内核(preemptive kernel)非抢占内核(nonpreemptive kernel)

抢占内核允许处于内核模式的进程被抢占。

非抢占内核不允许内核模式的进程被抢占。

非抢占内核的内核数据结构从根本上不会导致竞争条件,对于抢占内核需要认真设计以确保其内核数据结构不会导致竞争条件。

但抢占内核更受欢迎,因为抢占内核更适合实时编程,因为它能允许实时进程抢占处于内核模式运行的其他进程。再者,抢占内核的响应更快,因为处于内核模式的进程在释放CPU之前不会运行过久。

Peterson算法

Peterson算法是一种经典的基于软件的临界区问题算法,不能确保正确运行。

Peterson算法适用于两个进程在临界区与剩余区间交替执行,为了方便,当使用Pi时,Pj来标示另一个进程,即j == i - 1。Peterson算法需要在两个进程之间共享两个数据项:

int turn;

boolean flag[2];

变量turn表示哪个进程可以进入其临界区,即如果turn==i,那么进程Pi允许在其临界区内执行。

数组flag表示哪个进程想要进入临界区,如果flag[i]为true,即Pi想进入其临界区。

//进程Pi的Peterson算法
do{

   flag[i]=TRUE;

   turn=j;

   while(flag[j]&&turn==j);

   临界区

   flag[i]=FALSE;

   剩余区

}while(TRUE)

可以证明,满足三项要求。

硬件同步

通过要求临界区用锁来防护,就可以避免竞争条件,即一个进程在进入临界区之前必须得到锁,而其退出临界区时释放锁。

do{

请求锁

临界区

释放锁

剩余区

}while(TRUE)

硬件特性能简化编程任务且提高系统效率。

对于单处理器环境,临界区问题可简单地加以解决:在修改共享变量时要禁止中断出现。这样其他指令不可能执行,所以共享变量也不会被意外修改。这种方法通常为抢占式内核所采用。

在多处理器环境下,这种解决方法是不可行的,低效且影响系统时钟。

特殊硬件指令以允许能原子地(不可中断的)检查和修改字的内容或交换两个字的内容。如TestAndSet(),当两个指令同时执行在不同的CPU上,那么它们会按任意顺序来顺序执行。

TestAndSet指令定义:

boolean TestAndSet(boolean *target){

  boolean rv=*target;

  *target=TRUE;

  return rv;

}

使用TestAndSet的互斥实现,声明一个Boolean变量lock,初始化为false

  do{

while(TestAndSetLock(&lock))

;//do nothing

//critical section

    lock=FALSE;

      //remainder section

}while(TRUE);

使用TestAndSet的有限等待互斥:任何等待进入临界区的进程只需要等待n-1次。

boolean waiting[i] = TRUE;
boolean lock;
初始化为false

do{

  waiting[i]=TRUE;

  key=TRUE;

  while(waiting[i]&&key)

    key=TestAndSet(&lock);

  waiting[i]=FALSE;

    //critical section

  j=(i+1)%n;

  while((j!=i)&&!waiting[j])

    j=(j+1)%n;

  if(j==i)

    lock=FALSE;

  else

    waiting[j]=FALSE

    //remainder section

}while(TRUE);

Swap指令的定义:

void Swap(boolean *a,boolean *b){

  booleab temp=*a;

  *a=*b;

  *b=temp;

}

使用Swap的互斥实现:key为每个进程局部变量,lock为全局变量,初始化为false

do{

  key=TRUE;

  while(key==TRUE)

    Swap(&lock,&key);

    //critical section

 lock=FALSE;

    //remainder section

}while(TRUE);

然而,对于硬件设计人员,在多处理器上实现原子指令TestAndSet并不简单。

信号量(semaphore)

应用层面解决临界区问题:信号量

信号量S是个整数变量,除了初始化外,它只能通过两个标准原子操作:wait()和signal()来访问。即P和V。

wait()的定义可表示为:

wait(S){

  while(S<=0)

    ;//no-op

  S--;

}

signal()的定义可表示为:

signal(S){

  S++;

}

在wait()和signal()操作中,对信号量整型值的修改必须不可分地执行。

用法:

通常操作系统区分计数信号量和二进制信号量。计数信号量的值域不受限制,而二进制信号量的值只能为0或1,有的系统,。由于二进制信号量是互斥的,因而可以将其应用于处理多进程的临界区问题。

使用信号量的互斥实现:n个进程共享信号量mutex,初始值1

do{

  wait(mutex);

    //critical section

  signal(mutex);

    //remainder section

}while(TRUE);

计数信号量可以用来控制访问具有若干个实例的某种资源。该信号量初始化为可用资源的数量。当每个进程需要使用资源时,需要对该信号量执行wait()操作。当进程释放资源时,需要对该信号执行signal()操作。

可以用信号量来解决各种同步问题。如先执行Pi的S1语句,然后再执行Pj的S2语句,可以通向一个信号量,初始化为0,然后执行S1完,执行signal(),在执行S2前,执行wait()。

实现:

信号量的主要缺点是要求忙等待(busy waiting)。即在进入代码段中连续地循环。忙等待浪费了CPU时钟,这种类型的信号量也称为自旋锁(spinlock),这是因为进程在其等待锁的时还在运行(自旋锁有其优点,进程在等待锁时不进行上下文切换,而上下文切换可能需要花费相当长的时间。因此如果锁占用的时间短,那么锁就有用了,自旋锁常用于多处理器系统中,这样一个线程在一个处理器自旋时,另一线程可在另一个处理器上在其临界区内执行)

为克服这一缺点,修改wait()和signal()的定义,信号量值不为正时,不是忙等而是阻塞自己,阻塞操作将一个进程放入到与信号量相关的等待队列中,并将该进程的状态切换成等待状态,接着,控制转到CPU调度程序,以选择另一个进程来执行。

被阻塞在等待信号S上的进程,可以在其他进程执行signal()的时候操作之后重新被执行,该进程的重新执行是通过wakeup()操作来进行的将进程从等待状态切换到就绪状态。接着进程被放到就绪队列中。

因而将信号量定义为如下:

typedef struct{

    int value;                  //记录了这个信号量的值   

    struct process *list;       //储存正在等待这个信号量的进程(PCB链表指针)

}semaphore;

每个信号量都有一个整型值和一个进程链表,当一个进程必须等待信号量时,就加入到进程链表上,操作signal()会从等待进程链表中取一个进程以唤醒。

wait()实现:

wait(semaphore *S){

  S->value--;

  if(S->value <0){                  //没有资源

  add this process to S->list;      //进入等待队列  

  block();                          //堵塞 

}

}

signal()实现:

signal(semaphore *S){

  S->value++;

  if(S->value<=0){                  //上面++后,S仍然还<=0,说明资源供不应求,等待者还有很多,于是唤醒等待队列中的一个

  remove a process P from S->list;

  wakeup(P);                        //切换到就绪状态  

}

}

操作block()挂起调用他的进程。

操作wakeup(P)重新启动阻塞进程P的执行。

这两个操作都是由操作系统作为基本系统调用来提供的。

在具有忙等的信号量经典定义下,信号量的值绝对不能为负数,但是本实现可能造成信号量为负值。如果信号量为负值,那么其绝对值就是等待该信号量的进程的个数。

等待进程的链表可以利用进程控制块PCB中的一个链接域来加以轻松实现。即每个信号量包括一个整型值和一个PCB链表的指针。

信号量的关键之处是它们原子的执行。必须确保没有两个进程能同时对一个信号量进行操作,在单处理器情况下,可以在执行wait()和signal()的时候简单的关闭中断,保证只有当前进程进行。

多处理器下,若禁止所有CPU的中断,则会严重影响性能,SMP系统必须提供其他加锁技术(如自旋锁),以确保wait()与signal()可原子地执行。

死锁与饥饿:

具有等待队列的信号量的实现可能会导致这样的情况:

两个或多个进程无限地等待一个事件,而该事件只能由这些等待进程之一来产生。这里的事件是signal()操作的执行。当出现这样的状态时,这些进程就称为死锁(deadlocked)

例如,一个由P1 P2 组成的系统,每个都访问共享的信号量S和Q,SQ初值均为1:

P0:

wait(S);

wait(Q);

//......

signal(S);

signal(Q);

P1:

wait(Q);

wait(S);

//......

signal(Q);

signal(S);

假设,P0执行wait(S),接着P1执行wait(Q),P0再执行wait(Q)时,必须等待,直到P1执行signal(Q),而此时P1也在等待P0执行signal(S),两个操作都不能进行,P0和P1就死锁了。

与死锁相关的另一个问题是无限期阻塞(indefinite blocking)或饥饿(starvation):即进程在信号量内无限期等待。

举个例子来理解死锁饥饿的区别:

  • 死锁(deadlock)

指的是两个或者两个以上的进程相互竞争系统资源,导致进程永久阻塞。

例如:

1、桌子上有慢慢一桌子的美食,但是只有一双筷子。
2、甲拿了一根,然后在找另一根。
3、乙拿了一根,然后也在找另一根。
4、因为他们都掌握了对方必需的资源,导致最后他们俩谁都吃不到美食。

  • 饥饿(starvation)

指的是等待时间已经影响到进程运行,此时成为饥饿现象。如果等待时间过长,导致进程使命已经没有意义时,称之为“饿死”。

例如:

1、小明要告诉妈妈明天开家长会。
2、小明妈妈因为工作太忙,在公司加班,没有回家。
3、于是第二天,小明的妈妈就错过了家长会。(“饿死”)
4、其实小明的妈妈没有出现“死锁”。只是小明的优先级过低,不如工作重要。

经典同步问题

有限缓存问题—生产者消费问题:

假设缓冲池有n个缓冲项,每个缓冲项能存在一个数据项。信号量mutex提供了对缓冲池访问的互斥要求,并初始化为1。信号量empty和full分别用来表示空缓冲项和满缓冲项的个数,信号量empty初始化为n;而信号量full初始化为0。

生产者进程结构:

do{

  …

  //produce an item in next p

…

wait(empty);

wait(mutex);

…

//add next p to buffer

…

signal(mutex);

signal(full);

}while(TRUE);

消费者进程结构:

do{

wait(full);

wait(mutex);

…

//remove an item from buffer to next c

…

signal(mutex);

signal(empty);

…

//consume the item in next c

…

}while(TRUE);

读者-写者问题:

只读数据库的进程称为读者;更新(读和写)数据库的称为写者

第一读者-写者问题:要求没有读者需要保持等待除非已有一个写者已获得允许已使用共享数据库。换句话说,没有读者会因为一个写者在等待而会等待其他读者的完成。

第二读者-写者问题:要求一旦写者就绪,那么写者会尽可能快得执行其写操作。换句话说,如果一个写者等待访问对象,那么不会有新读者开始读操作。

对于这两个问题的解答可能导致饥饿问题。对第一种情况,写者可能饥饿;对第二种情况,读者可能饥饿。

对于第一读者-写者问题的解决:

读者进程共享以下数据结构:

semaphore mutex, wrt;

int readcount;

信号量mutex和wrt初始化为1,readcount初始化为0,信号量wrt为读者和写者进程所共有。信号量mutex用于确保在更新变量readcount时的互斥。变量readcount用来跟踪有多少进程正在读对象。信号量wrt供写者作为互斥信号量,它为第一个进入临界区和最后一个离开临界区的读者所使用,而不被其他读者所使用。

写者进程结构:

do{

  wait(wrt);

  …;

  //writing is performed

  …;

  signal(wrt);

}while(TRUE);

读者进程结构:

do{

  wait(mutex);

  readcount++;

  if(readcount==1)

  wait(wrt);

signal(mutex);

…;

//reading is performed

…;

wait(mutex);

readcount--;

if(readcount==0)

  signal(wrt);

signal(mutex);

}while(TRUE);

推广为读写锁。

在以下情况下最为有用:

一是,当可以区分哪些进程只需要读共享数据,哪些进程只需要写共享数据;

二是,当读者进程数比写进程多时。

哲学家进餐问题:

拿起与他相近的两只筷子,一个哲学家一次只能拿起一只筷子,同时有两只筷子时,就能吃,吃完,会放下两只筷子。

一种简单的方法,每只筷子都用一个信号量来表示。一个哲学家通过执行wait()操作试图获取相应的筷子,他会通过执行signal()操作以释放相应的筷子。

共享数据为:semaphore chopstick[5];其中所有chopstick的元素初始化为1。

哲学家i的结构:

do{

  wait(chopstick[i]);

  wait(chopstick[(i+1)%5]);

  …;

  //eat

  …;

  signal(chopstick[i]);

  signal(chopstick[(i+1)%5]);

  …;

  //think

  …;

}while(TRUE);

但这种方法会发生死锁,例如,所有哲学家同时饥饿,且同时拿起左边的筷子。

多种可以解决死锁的方法:
①最多只允许4个哲学家同时坐在桌子上;
②只有两只筷子都可用时才允许一个哲学家拿起它们(他必须在临界区内拿起两只筷子);
③使用非对称解决方法,即技术哲学家先拿起左边的筷子,接着拿起右边的筷子,而偶数哲学家先拿起右边的筷子,接着拿起左边的筷子。

时间: 2024-12-06 09:20:39

操作系统概念学习笔记 11 进程同步(一)的相关文章

操作系统概念学习笔记 12 进程同步(二)管程

操作系统概念学习笔记 12 进程同步(二) 管程 基本的.高级的同步构造,即管程(monitor)类型. 使用: 管程类型提供了一组由程序员定义的.在管程内互斥的操作.管程类型的表示包括一组变量的声明(这些变量的值定义了一个类型实例的状态)和对这些变量操作的子程序和函数的实现.管程的类型表示不能直接为各个进程所使用.因此,在管程内定义的子程序只能访问位于管程内那些局部声明的变量和形式参数.类似的,管程的局部变量能被局部子程序访问. 管程结构确保一次只有一个进程能在管程内活动.不需要显示的编写同步

操作系统概念学习笔记 5 操作系统管理简述

操作系统概念学习笔记 5 操作系统管理简述 进程管理 处于执行中的程序被称作进程. 进程需要一定的资源(包括cpu时间.内存.文件.I/O设备)来完成任务.这些资源可以在进程创建时分配给进程,也可以在执行时分配给进程.除了在创建时得到各种物理和逻辑资源外,进程还可以接受传输过来的各种初始化数据. 程序本身并不是进程,程序是被动的实体.而进程是活动的实体.进程是系统工作的单元. 单线程进程具有一个程序计数器来明确下一个执行的指令,直到进程终止. 在任何时候,最多只有一个指令代表进程被执行.因此,尽

操作系统概念学习笔记 10 CPU调度

操作系统概念学习笔记 10 CPU调度 多道程序操作系统的基础.通过在进程之间切换CPU,操作系统可以提高计算机的吞吐率. 对于单处理器系统,每次只允许一个进程运行:任何其他进程必须等待,直到CPU空闲能被调度为止. 多道程序的目标是在任何时候都有某些进程在运行,以使CPU的使用率最大化.多道程序的思想较为简单,当一个进程必须等待时,操作系统会从该进程拿走CPU的使用权,而将CPU交给其他进程. CPU-I/O 区间周期 CPU的成功调度依赖于进程的如下属性: 进程执行由CPU执行周期和I/O等

操作系统概念学习笔记 13 死锁(一)

操作系统概念学习笔记 13 死锁(一) 所有申请的资源都被其他等待进程占有,那么该等待进程有可能在无法改变其状态,这种情况称为死锁(deadlock). 系统模型 进程在使用资源之前必须先申请资源,在使用资源之后要释放资源.进程所申请的资源数量不能超过系统所有资源的总量. 在正常操作模式下,进程只能按如下顺序使用资源: ①申请:如果申请不能立即被允许,那么申请进程必须等待,直到它获得该资源为止. ②使用:进程对资源进行操作. ③释放:进程释放资源 资源的申请与释放为系统调用.其他资源的申请与释放

操作系统概念学习笔记 8 进程

操作系统概念学习笔记 8 进程 概念 进程 进程是执行中的程序,这只是非正式的说法.进程不只是程序代码,程序代码称为文本段(代码段),还包括当前活动,通过程序计数器的值和处理器寄存器的内容来表示.此外,进程还包括进程堆栈段(临时数据.函数参数.局部变量.地址)和数据段(包括全全局变量.还可能包括堆(leap),是在进程运行期间动态分配内存. 程序是被动实体,如存储在磁盘上包含一系列指令的文件内容(可执行文件),而进程是一个活动实体,他有一个程序计数器来表示下一个要执行的命令和相关资源集合. 虽然

操作系统概念学习笔记 7 操作系统结构

操作系统概念学习笔记 7 操作系统结构 系统设计 设计目标 系统设计的第一个问题是定义系统的目标与规格.在最高层,系统设计受到硬件选择和系统类型的影响. 需求可以分为两个基本类:用户目标和系统目标 策略和机制 操作系统设计的重要原理是策略(policy)和机制(mechanism)的区别.机制决定如何做,策略决定做什么.策略可能会随着时间或位置而有所改变,每次改变都可能需要底层机制的改变.系统更需要通用机制.这样策略的改变只需要重定义一些系统参数. 操作系统重要功能的改善可能是由于更好的数据结构

操作系统概念学习笔记 1 加电引导过程

操作系统概念学习笔记 1 加电引导过程 加电-引导程序(bootstrap program) 引导程序通常位于ROM或EEPROM中,引导程序必须定位操作系统内核并把它装入内存,接着操作系统开始执行第一个进程如init并等待事件的发生. 简单来说即:1,电自检程序.2,自举装入程序.3,引导程序.4,操作系统 流程图: linux系统为例: 1.加电并且启动BIOS 加电:把电源按钮按下去,主板通电后会启动BIOS. 2.BIOS到要引导的存储设备 BIOS启动之后会先进行POST(short

操作系统概念学习笔记 16 内存管理(二) 段页

操作系统概念学习笔记 16 内存管理 (二) 分页(paging) 分页(paging)内存管理方案允许进程的物理地址空间可以使非连续的.分页避免了将不同大小的内存块匹配到交换空间上(前面叙述的内存管理方案都有这个问题,当位于内存中的代码和数据需要换出时,必须现在备份存储上找到空间,这是问题就产生了.备份存储也有前面所述的与内存相关的碎片问题,只不过访问更慢). 传统上,分页支持一直是由硬件来处理的.最近的设计是通过将硬件和操作系统相配合来实现分页. 基本方法 实现分页的基本方法设计将物理内存分

操作系统概念学习笔记 15 内存管理(一)

操作系统概念学习笔记 15 内存管理(一) 背景 内存是现代计算机运行的中心.内存有很大一组字或字节组成,每个字或字节都有它们自己的地址.CPU根据程序计数器(PC)的值从内存中提取指令,这些指令可能会引起进一步对特定内存地址的读取和写入. 一个典型指令执行周期,首先从内存中读取指令.接着该指令被解码,且可能需要从内存中读取操作数.在指令对操作数执行后,其结果可能被存回到内存.内存单元只看到地址流,而并不直到这些地址是如何产生的(由指令计数器.索引.间接寻址.实地址等)或它们是什么地址(指令或数