操作系统概念(第六章) 进程同步(一)

进程同步

背景

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

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

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

临界区(critical section)

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

临界资源(Critical resource)每次只能被一个进程访问。而临界区则是能够访问临界资源的代码片段。

临界区问题(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=i1 。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)

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

Peterson算法实际上是一种谦让的过程,即:
Pi :我已经准备好了,但是我让这次一次的turn=j,看看 Pj 是否要运行,如果是的话,我就让 Pj 先运行。
Pj 也是这样的情况。

硬件同步

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

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);

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的算法,该算法满足所有的临界区的三个要求。
公用的数据结构如下
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);

为了满足有限等待,当一个进程退出其临界区时,它会循环地扫描数组waiting[i]并根据这一顺序而指派第一个等待进程作为下一个进入临界区的进程。因此,任何等待进入临界区的进程只需要等待 n1 次。
然而,对于硬件设计人员,在多处理器上实现原子指令TestAndSet并不简单。

信号量(semaphore)

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

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

wait()就是等待资源的过程,定义可表示为:

wait(S)
{
  while(S<=0)
    ;//no-op
  S--;
}

signal()就是释放资源的过程,定义可表示为:

signal(S)
{
  S++;
}

在wait()和signal()操作中,对信号量整型值的修改必须不可分地执行。即当一个进程修改信号量值时,不能有其他进程同时修改同一信号量的值。另外,对于wait(S),对于S的整数值测试( S0 )和对其可能的修改(S–),也必须不被中断地执行。

用法

通常操作系统区分计数信号量和二进制信号量。计数信号量的值域不受限制,而二进制信号量的值只能为0或1,有的系统,将二进制信号量成为互斥锁
由于二进制信号量是互斥的,因而可以将其应用于处理多进程的临界区问题:这 n 个进程共享信号量mutex,初始值 1 。结构如下

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

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

可以用信号量来解决各种同步问题。如先执行Pi S1 语句,然后再执行 Pj S2 语句,可以通向一个信号量,初始化为0。
进程 P1 中插入语句:

S1;
signal(synch);

在进程 P2 中插入语句:

wait(synch);
S2;

因为初始化synch为0, P2 只有在 P1 调用signal(synch),即( S1 )之后,才会执行 S2

实现

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

为克服这一缺点,修改wait()和signal()的定义,信号量值不为正时,不是忙等而是阻塞自己,阻塞操作将一个进程放入到与信号量相关的等待队列中,并将该进程的状态切换成等待状态,接着,控制转到CPU调度程序,以选择另一个进程来执行,从而使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 组成的系统,每个都访问共享的信号量 初值均为

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 <script type="math/tex" id="MathJax-Element-50">0</script>。

生产者进程结构:

do
{
  …
  //produce an item in next pwait(empty);
wait(mutex);
…
//add next p to buffersignal(mutex);
signal(full);
}while(TRUE);

消费者进程结构:

do
{
wait(full);
wait(mutex);
…
//remove an item from buffer to next csignal(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个哲学家同时坐在桌子上;
②只有两只筷子都可用时才允许一个哲学家拿起它们(他必须在临界区内拿起两只筷子);
③使用非对称解决方法,即技术哲学家先拿起左边的筷子,接着拿起右边的筷子,而偶数哲学家先拿起右边的筷子,接着拿起左边的筷子。

  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《操作系统概念:第九版》是由Abraham Silberschatz、Peter B. Galvin和Greg Gagne合著的一本计算机科学领域的经典教材。本书系统地介绍了操作系统的基本概念、原理和设计方法,深入讨论了多道程序、处理器调度、内存管理、文件系统等重要主题。 该书可以分为七个主要部分。第一部分介绍了操作系统的概述,包括操作系统的作用、功能和发展历史。第二部分探讨了进程管理,包括进程的创建、调度、同步和通信。第三部分涵盖了内存管理,包括分区管理、页面置换算法和虚拟内存等内容。 第四部分介绍了文件系统,包括文件和目录的组织、磁盘调度以及文件系统的实现。第五部分讨论了输入/输出系统,包括驱动程序、设备管理和磁盘存储等。第六部分介绍了分布式系统和并行操作系统,包括网络和分布式文件系统。最后一部分讨论了安全性和保护机制,包括访问控制和安全策略等。 该书采用了清晰的语言和生动的例子,结合了最新的研究成果和实际案例,使读者更容易理解和应用操作系统概念。此外,书中还包含了大量的习题和实践项目,帮助读者加深理解,并将所学知识应用到实际问题中。 《操作系统概念:第九版》是一本全面且权威的操作系统教材。它不仅适用于计算机科学专业的学生,也适用于从业人员和对操作系统感兴趣的读者。无论是在理论研究还是在实践应用方面,这本教材都是值得一读的重要参考书。 ### 回答2: 《操作系统概念(abraham silberschatz pdf)第九版》是一本面向计算机科学专业学生和专业人士的经典教材。本书对操作系统概念和原理进行了详细而全面的介绍。 首先,本书从操作系统的角度定义了计算机系统,并解释了它的组成部分。书中详细讨论了进程管理、内存管理、文件系统、输入输出管理等关键概念,并提供了相应的算法和实例。此外,本书还介绍了多处理器系统、并行计算和分布式系统等现代操作系统的新兴技术。 此外,本书还对操作系统的设计与实现进行了探讨。作者详细解释了操作系统的内核设计和模块化,提供了实践项目和实验手册,帮助读者深入理解操作系统的实际运作。并且,本书还对操作系统的安全性和保护机制进行了介绍,让读者了解如何保护计算机系统免受恶意软件和非法访问的侵害。 此外,本书在自愿顺序和并发性方面提供了许多实例和案例研究。它详细介绍了进程同步、进程互斥、死锁的原因和解决方法,让读者有系统地了解并理解操作系统的关键概念。 综上所述,《操作系统概念(abraham silberschatz pdf)第九版》是一本经典的操作系统教材,对操作系统概念和原理进行了全面而深入的介绍。无论是对于计算机科学专业学生还是专业人士,都是一本值得阅读和参考的书籍。 ### 回答3: 《操作系统概念(第九版)》是Abraham Silberschatz等人合著的经典教材。本书详细介绍了操作系统概念、原理、设计和实现等方面的知识。 首先,操作系统是计算机系统中最基础的软件之一,它负责管理计算机的硬件和软件资源,为用户和应用程序提供一个友好且高效的运行环境。本书从操作系统的基本概念入手,探讨了各种操作系统的常见功能和特性,例如进程管理、内存管理、文件系统等。 其次,在介绍操作系统的各个组成部分时,本书还深入讨论了各种操作系统的设计和实现原理,包括进程调度算法、内存管理策略、磁盘调度算法等等。读者可以通过学习这些原理,了解操作系统是如何高效地管理计算机资源的。 此外,本书还涵盖了一些当前热门的操作系统相关技术和概念,如多核处理器、虚拟化技术、云计算等。这些内容让读者能够紧跟操作系统领域的最新发展,并理解这些技术对操作系统设计和实现带来的挑战与机遇。 《操作系统概念(第九版)》以其全面、系统的内容、清晰易懂的语言和详细的例子,深受师生们的喜爱。无论是作为本科生的教材还是研究生的参考书,它都是一本不可或缺的操作系统经典教材。通过学习这本书,读者可以全面掌握操作系统的基本概念和原理,提升计算机科学领域的专业技能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值