操作系统(8)---进程的同步与互斥以及信号量机制(万字总结~)(2)

简介: 操作系统(8)---进程的同步与互斥以及信号量机制(万字总结~)


操作系统(7)----调度相关知识点(万字总结~)(1):https://developer.aliyun.com/article/1511020?spm=a2c6h.13148508.setting.27.54e54f0eH5yHaK

补充:

互斥锁

解决临界区最简单的工具就是互斥锁(mutex lock)。一个进程在进入临界区时应获得锁;在退出临界区时释放锁。函数acquire()获得锁,而函数release()释放锁。


每个互斥锁有一个布尔变量 available,表示锁是否可用。如果锁是可用的,调用 acqiure()会成功,且锁不再可用。当一个进程试图获取不可用的锁时,会被阻塞,直到锁被释放。


acquire()或release()的执行必须是原子操作,因此互斥锁通常采用硬件机制来实现。

互斥锁的主要缺点是忙等待,当有一个进程在临界区中,任何其他进程在进入临界区时必须连续循环调用 acquire()。当多个进程共享同一CPU时,就浪费了CPU周期。因此,互斥锁通常用于多处理器系统,一个线程可以在一个处理器上等待,不影响其他线程的执行。

需要连续循环忙等的互斥锁,都可称为自旋锁(spinlock),如TSL指令、swap指令、单标志法。


特性:

1.需忙等,进程时间片用完才下处理机,违反“让权等待”


2.优点:等待期间不用切换进程上下文,多处理器系统中,若上锁的时间短,则等待代价很低


3.常用于多处理器系统,一个核忙等,其他核照常工作,并快速释放临界区

4.不太适用于单处理机系统,忙等的过程中不可能解锁,只有该进程时间片用完,另一个进程上处理机(上锁),并且使用完临界资源,退出临界区,该进程才能再次获得锁。

排号自旋锁

自旋锁中有一种排号自旋锁:

排号锁就是要给用锁的线程进行排号,然后锁沿着这个号进行传递,因此可以说锁的竞争就变成了一个先进先出的等待队列。

struct lock
{
  volatile int owner;
  volatile int next;
};
 
void lock_init(struct lock *lock)
{
  //初始化排号锁
  lock->owner = 0;
  lock->next = 0;
}
 
void lock(struct lock*lock)
{
  //拿取自己的序号
  volatile int my_ticket = atomic_FAA(&lock->next,1);
  while(lock->owner != my_ticket)
  ; //循环忙等
}
 
void unlock(struct lock*lock)
{
  //传递给下一位竞争者
  lock->owner++;
}

owner表示当前的锁持有序号,next表示下一个需要分发的序号

第17行是拿取自己的序号,并累加,这样就不会拿取相同的序号

第18行是看当前锁的持有者是不是自己,不是就循环等待

第25行是释放锁,然后锁持有者向后传递。

条件变量

在生产者和消费者模型中,无剩余空位时,生产者会陷入循环等待,他可以不用循环等待的,这会浪费cpu资源,因此需要一种挂起/唤醒机制,条件变量就是为这个机制而设计的。

通过条件变量的接口,一个线程可以停止使用CPU并将自己挂起,当等待的条件满足时,其他线程会唤醒该挂起的线程让其继续执行。


int empty_slot = 5;
int filled_slot = 0;
struct cond empty_cond;
struct lock empty_cnt_lock;
struct cond filled_cond;
struct lock filled_cnt_lock;
 
void producer(void)
{
  int new_msg;
  while(TRUE)
  {
    new_msg = produce_new();
    lock(&empty_cnt_lock);
    while(empty_slot == 0)
    {
      cond_wait(&empty_cond, &empty_cnt_lock);
    }
    empty_slot --;
    unlock(&empty_cnt_lock);
    
    buffer_add_safe(new_msg);
    
    lock(&filled_cnt_lock);
    filled_slot ++;
    cond_signal(&filled_cond);
    unlock(&filled_cnt_lock);
  }
}

empty_cnt_lock和filled_cnt_lock是来保护对共享计数器empty_slot与filled_slot的修改的锁,这个锁设计的目的是在使用条件变量时,必须要搭配互斥锁一起使用。


这里设置了两个条件,empty_cond 缓冲区无空位和filled_cond 缓冲区无数据。


当生产者要写数据时发现没有空位,则通过cond_wait函数挂起,条件是empty_cond无空位,搭配的互斥锁是empty_cnt_lock,后边那个cond_signal是唤醒线程,由于写入数据则缓冲区存在数据了,可以唤醒由于缓冲区无数据而挂起的消费者线程。


struct cond{
  struct thread *wait_list;
};
 
void cond_wait(struct cond *cond, struct lock *mutex)
{
  list_append(cond->wait_list, thread_self());//将线程加入等待队列
  atomic_block_unlock(mutex); //原子挂起并放锁
    //这里为一个原子操作,它将当前线程从条件变量的等待队列中移除,并在同一原子操作中释放了互斥锁 mutex
  lock(mutex);  //重新获得互斥锁(被唤醒后)
}
 
void cond_signal(struct cond *cond)
{
  if(!list_empty(cond->wait_list))//看是否有线程等待在条件变量上
    wakeup(list_remove(cond->wait_list)); //操作系统提供的唤醒
}
 
void cond_broadcast(struct cond *cond)  //广播操作,用于唤醒所有等待在条件变量上的线程
{
  while(!list_empty(cond->wait_list))
    wakeup(list_remove(cond->wait_list));
}


二.信号量机制

由于上述的进程互斥实现方法存在以下问题:


1.在双标志先检查法中,进入区的“检查”,“上锁”操作无法一气呵成,从而导致了两个进程有可能同时进入临界区的问题;


2.所有的解决方案都无法实现“让权等待”。

为解决上述问题,提出了信号量机制

用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现了进程互斥、进程同步。

原语是一种特殊的程序段,其执行只能一气呵成,不可被中断。原语是由关中断/开中断指令实现的。软件解决方案的主要问题是由“进入区的各种操作无法一气呵成”,因此如果能把进入区、退出区的操作都用“原语”实现,使这些操作能“一气呵成”就能避免问题。


一对原语:wait(S)原语和 signal(S)原语,可以把原语理解为我们自己写的函数,函数名分别为 wait和 signal,括号里的信号量S其实就是函数调用时传入的一个参数。wait、signal 原语常简称为P、V操作。因此,做题的时候常把wait(S)、signal(s)两个操作分别写为 P(S)、V(S)

信号量其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初值为1的信号量。


1.信号量的类别

(1)整型信号量

用一个整数型的变量作为信号量,用来表示系统中某种资源的数量。


与普通整数变量的区别:对信号的操作只有三种:初始化,P操作,V操作。

例如:某计算机系统中有一台打印机

while(S<=0);

S=S-1;


这两个句子与双标志先检查法的原理是相同的,先检查资源是否足够,如果资源够占用一个资源,但是这里是用原语实现,所以就避免了双标志先检查法中两个进程同时进入临界区的问题。


同时这里的while循环也会导致不满足“让权等待”原则,会发生"忙等"


实质上,这里也不是忙等,忙等的情况下,若该进程时间片完,会发生时间片中断,切换到其他进程,但这里是原子操作,进程是不可被中断的。


这里的代码只是演示,不是简单的while循环,而是先给信号量上一个自旋锁,判断如果资源不够,那么会先释放自旋锁,紧跟goto语句回到获取自旋锁的那一步,那么下次回到这个进程时会再次对信号量上自旋锁。


这里也是看了很多资料总结了一下,若佬们有其他见解,请在评论区指教指教,谢谢佬们

(2)记录型信号量

整型信号量的缺陷是存在“忙等”问题,因此人们又提出了“记录型信号量”,即用记录型数据结构表

示的信号量。

如果剩余资源数不够使用block语使进程从运行态进入阻塞态,并把挂到信号量S的等待队列(即阻塞队列)中

S.value<=0,表示释放资源后,若还有别的进程在等待这种资源,则使用wakeup原语唤醒等待队列中的一个进程,该进程从阻塞态变为就绪态。

例如:某计算机系统中有2台打印机,则可在初始化信号量S时将S.value的值设为2,队列S.L设置为空

1.当分配给P0进程CPU时,剩余资源数会减1

2.接下来CPU分配给P1进程,剩余的一个资源分配给P1进程

3.接下来CPU分配给P2进程,剩余资源减1,当前剩余资源为-1,value的值在减1之后小于0,说明此时系统当中没有多余资源分配给进程了。因此这个进程会在wait()原语中,主动执行block原语,即把自己阻塞的原语,因此P2进程会被放到打印资源的等待队列中。

S.value=0,资源恰好分配完S.value=-1,有1个进程在等待

4.同理,CPU为P3进程服务,没有多余的资源,所以P3也会被放入等待队列的队尾中

S.value=-2,有2个进程在等待

5.P0在使用完打印机后会进行signal操作,首先会让value值做加1的操作,即剩余资源数加1,此时S.value的值会从-2变为-1,若S.value依然是小于等于0(注意这里的0,因为是减完后才等于0,所以等于0,也有进程在等待队列中)的,说明等待队列中依然有进程等待,signal(S)操作中,会主动执行wakeup原语用于唤醒等待队列中队头的进程,即P2,并且将P0释放的打印机资源分配给P2,P2从等待队列中移除,若此时P2进程得到CPU,P2就能使用打印机资源了。

6.P2使用完打印机后也会进行signal操作,首先将value减1,接着使用wqakeup原语唤醒等待队列中对头的进程P3,并且将打印机资源分配给P3,P3会从等待队列中移除,此时等待队列为空。若此时P3被分配CPU,就可以使用打印机资源。

7.若此时P1使用完进程释放资源,剩余资源加1,即从0变为1,此时剩余资源数已经大于0了,说明没有进程在等待队列中,所以执行signal操作时,并不需要执行wakeup原语,唤醒进程。



8.最后P3使用完打印机资源后,会对打印机资源释放,系统回收打印机资源,剩余资源数从1变为2,也不需要执行wakeup原语。


总结:

wait(S)、signal(s)也可以记为 P(S)、V(S),这对原语可用于实现系统资源的“申请”和“释放”


对信号量S的一次P操作意味着进程请求一个单位的该类资源,因此需要执行 S.value--,表示资源数减1,当S.value<0时表示该类资源已分配完毕,因此进程应调用 block 原语进行自我阻塞(当前运行的进程从运行态--->阻塞态),主动放弃处理机,并插入该类资源的等待队列S.L中。可见,该机制遵循了“让权等待”原则,不会出现“忙等”现象。


对信号量S的一次V操作意味着进程释放一个单位的该类资源,因此需要执行S.value++,表示资源数加1,若加1后仍是S.value<=0,表示依然有进程在等待该类资源,因此应调用 wakeup 原语唤醒等待队列中的第1个进程(被唤醒进程从阻塞态--->就绪态)

2.信号量机制实现进程互斥

实现进程互斥操作的步骤如下:

1.分析并发进程的关键活动,划定临界区(如:对临界资源打印机的访问就应放在临界区)

2.设置互斥信号量 mutex,初值为 1

3.在进入区 P(mutex)--申请资源

4.在退出区 V(mutex)--释放资源

此时P1进程申请进入临界区,由于此时互斥信号量为1,即临界资源为1,所以此时P1可以进入临界区,若P2也想进入临界区,就会被阻塞在P操作,等到P1进行V操作,释放资源后,P2进程才能被唤醒。

注:


1.队不同临界资源需要设置不同的互斥信号量,例如打印机资源设置为mutex1,摄像头资源设置为mutex2。


2.P、V操作必须成对出现。缺少P(mutex)就不能保证临界资源的互斥访问。缺少V(mutex)会导致资源永不被释放,等待进程永不被唤醒。


3.信号量机制实现进程同步

进程同步就是要让各并发进程按要求有序地推进。例如,下图中P1、P2并发执行,由于存在异步性,因此二者交替推进的次序是不确定的。

若 P2的“代码4”要基于 P1的“代码1”和“代码2”的运行结果才能执行,那么我们就必须保证“代码4”一定是在“代码2”之后才会执行。这就是进程同步问题,让本来异步并发的进程互相配合,有序推进。


实现步骤如下:

1.分析什么地方需要实现“同步关系”,即必须保证“一前一后”执行的两个操作(或两句代码)

2.设置同步信号量S,初始为0

3.在“前操作”之后执行 V(S)

4.在“后操作”之前执行 P(S)

注意这里是前V后P,由于刚开始信号量为0,需要先V(s),释放资源,后P(S)才能申请到该资源。

假设刚开始P1上处理机运行,先执行到 V(S)操作,则 S++后 S=1。之后当执行到 P(S)操作时,由于 S=1,表示有可用资源,会执行S--,S的值变回 0,P2进程不会执行 block 原语(即P2进程不会被阻塞),而是继续往下执行代码4。


假设刚开始P2上处理机运行,先执行到 P(S)操作,由于S=0,S--后 S=-1,表示此时没有可用资源,因此P操作中会执行 block 原语,主动请求阻塞。之后当执行完代码2,继而执行V(S)操作,S++,使S变回 0,由于此时有进程在该信号量对应的阻塞队列中,因此会在V操作中执行 wakeup 原语,唤醒 P2进程。这样 P2 就可以继续执行 代码4 了。


总的来说,保证了代码4一定在代码2之后执行。

操作系统(8)---进程的同步与互斥以及信号量机制(万字总结~)(3):https://developer.aliyun.com/article/1511049?spm=a2c6h.13148508.setting.25.54e54f0eH5yHaK

目录
相关文章
|
5天前
|
算法 调度 UED
深入理解操作系统:进程调度与优先级队列
【10月更文挑战第31天】在计算机科学的广阔天地中,操作系统扮演着枢纽的角色,它不仅管理着硬件资源,还为应用程序提供了运行的环境。本文将深入浅出地探讨操作系统的核心概念之一——进程调度,以及如何通过优先级队列来优化资源分配。我们将从基础理论出发,逐步过渡到实际应用,最终以代码示例巩固知识点,旨在为读者揭开操作系统高效管理的神秘面纱。
|
2天前
|
算法 调度 UED
深入理解操作系统:进程管理与调度策略
【10月更文挑战第34天】本文旨在探讨操作系统中至关重要的一环——进程管理及其调度策略。我们将从基础概念入手,逐步揭示进程的生命周期、状态转换以及调度算法的核心原理。文章将通过浅显易懂的语言和具体实例,引导读者理解操作系统如何高效地管理和调度进程,保证系统资源的合理分配和利用。无论你是初学者还是有一定经验的开发者,这篇文章都能为你提供新的视角和深入的理解。
13 3
|
4天前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
5天前
|
算法 调度 UED
深入理解操作系统的进程调度机制
本文旨在探讨操作系统中至关重要的组成部分之一——进程调度机制。通过详细解析进程调度的概念、目的、类型以及实现方式,本文为读者提供了一个全面了解操作系统如何高效管理进程资源的视角。此外,文章还简要介绍了几种常见的进程调度算法,并分析了它们的优缺点,旨在帮助读者更好地理解操作系统内部的复杂性及其对系统性能的影响。
|
6天前
|
消息中间件 算法 Linux
深入理解操作系统之进程管理
【10月更文挑战第30天】在数字时代的浪潮中,操作系统作为计算机系统的核心,扮演着至关重要的角色。本文将深入浅出地探讨操作系统中的进程管理机制,从进程的概念入手,逐步解析进程的创建、调度、同步与通信等关键过程,并通过实际代码示例,揭示这些理论在Linux系统中的应用。文章旨在为读者提供一扇窥探操作系统深层工作机制的窗口,同时激发对计算科学深层次理解的兴趣和思考。
|
3天前
|
消息中间件 算法 调度
深入理解操作系统:进程管理的艺术
【10月更文挑战第33天】本文旨在揭示操作系统中进程管理的神秘面纱,带领读者从理论到实践,探索进程调度、同步以及通信的精妙之处。通过深入浅出的解释和直观的代码示例,我们将一起踏上这场技术之旅,解锁进程管理的秘密。
8 0
|
5天前
|
算法 Linux 调度
深入理解操作系统之进程调度
【10月更文挑战第31天】在操作系统的心脏跳动中,进程调度扮演着关键角色。本文将深入浅出地探讨进程调度的机制和策略,通过比喻和实例让读者轻松理解这一复杂主题。我们将一起探索不同类型的调度算法,并了解它们如何影响系统性能和用户体验。无论你是初学者还是资深开发者,这篇文章都将为你打开一扇理解操作系统深层工作机制的大门。
13 0
|
6天前
|
安全 Linux 数据安全/隐私保护
Vanilla OS:下一代安全 Linux 发行版
【10月更文挑战第30天】
21 0
Vanilla OS:下一代安全 Linux 发行版
|
9天前
|
人工智能 安全 Linux
|
29天前
|
Unix 物联网 大数据
操作系统的演化与比较:从Unix到Linux
本文将探讨操作系统的历史发展,重点关注Unix和Linux两个主要的操作系统分支。通过分析它们的起源、设计哲学、技术特点以及在现代计算中的影响,我们可以更好地理解操作系统在计算机科学中的核心地位及其未来发展趋势。
下一篇
无影云桌面