操作系统(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

目录
相关文章
|
16天前
|
存储 消息中间件 资源调度
「offer来了」进程线程有啥关系?10个知识点带你巩固操作系统基础知识
该文章总结了操作系统基础知识中的十个关键知识点,涵盖了进程与线程的概念及区别、进程间通信方式、线程同步机制、死锁现象及其预防方法、进程状态等内容,并通过具体实例帮助理解这些概念。
「offer来了」进程线程有啥关系?10个知识点带你巩固操作系统基础知识
|
8天前
|
消息中间件 存储 网络协议
操作系统的心脏:深入理解进程间通信(IPC)机制
在现代计算机系统中,操作系统扮演着至关重要的角色,而进程间通信(IPC)作为操作系统的核心功能之一,极大地影响着系统的性能和稳定性。本文将通过浅显易懂的语言,详细探讨进程间通信的基本原理、主要类型及其实际应用,旨在为读者提供一个清晰且全面的理解和认识。 ##
|
19天前
|
人工智能 Kubernetes 算法
探究操作系统的心脏——进程管理机制
本文深入探讨了操作系统核心组件之一——进程管理机制。进程管理作为操作系统的基础功能,负责协调和控制计算机系统内运行的所有进程,确保系统资源的有效分配与利用。通过详细介绍进程的定义、状态转换、调度算法以及多线程技术等关键概念,本文揭示了进程管理如何支撑起整个操作系统的运行框架,并保障用户任务的顺利执行。同时,文章还讨论了现代操作系统在进程管理方面的创新与挑战,为读者提供了一个全面而深入的理解视角。
31 1
|
26天前
|
算法 调度 UED
探索操作系统的心脏——进程管理机制
本文将深入探讨操作系统中至关重要的部分——进程管理机制。我们将从基本概念入手,逐步解析进程的定义、状态及其在操作系统中的角色。随后,我们会详细讨论进程调度算法,包括先来先服务、短作业优先、时间片轮转和优先级调度等,分析它们的优势与应用情景。最后,通过实例展示这些算法在实际系统运作中的运用,帮助读者更好地理解进程管理的核心原理。
|
26天前
|
算法 调度 Python
探索操作系统的内核——一个简单的进程调度示例
【9月更文挑战第17天】在这篇文章中,我们将深入探讨操作系统的核心组件之一——进程调度。通过一个简化版的代码示例,我们将了解进程调度的基本概念、目的和实现方式。无论你是初学者还是有一定基础的学习者,这篇文章都将帮助你更好地理解操作系统中进程调度的原理和实践。
|
1月前
|
消息中间件 安全 Kafka
Python IPC机制全攻略:让进程间通信变得像呼吸一样自然
【9月更文挑战第12天】在编程领域,进程间通信(IPC)是连接独立执行单元的关键技术。Python凭借简洁的语法和丰富的库支持,提供了多种IPC方案。本文将对比探讨Python的IPC机制,包括管道与消息队列、套接字与共享内存。管道适用于简单场景,而消息队列更灵活,适合高并发环境。套接字广泛用于网络通信,共享内存则在本地高效传输数据。通过示例代码展示`multiprocessing.Queue`的使用,帮助读者理解IPC的实际应用。希望本文能让你更熟练地选择和运用IPC机制。
41 10
|
26天前
|
消息中间件 Python
深入理解操作系统的进程间通信(IPC)机制
本文将探讨操作系统中的核心概念——进程间通信(IPC),揭示其在系统运作中的重要性及实现方式。通过分析不同类型的IPC手段,如管道、信号、共享内存等,帮助读者更好地理解操作系统的内部工作原理及其在实际应用中的表现。
36 1
|
27天前
|
消息中间件 存储 大数据
深入理解操作系统中的进程间通信(IPC)机制
本文旨在探讨操作系统中进程间通信(IPC)的核心机制与其重要性。通过对不同IPC手段如管道、信号、消息队列及共享内存等的详细解析,揭示它们如何高效地促进进程间的信息交换与同步。文章不仅阐述各种IPC技术的实现原理,还探讨了它们在实际系统应用中的场景与优化策略,为系统开发者提供全面而深入的理解。
|
27天前
|
消息中间件 程序员 数据处理
探究操作系统中的进程间通信(IPC)机制及其在现代软件开发中的应用
本文深入探讨了操作系统中的核心概念——进程间通信(IPC),揭示了其在现代软件开发中的关键作用。通过对各种IPC机制如管道、消息队列、共享内存等的详细分析,本文旨在为读者提供一个清晰的理解框架,帮助他们掌握如何在实际应用中有效利用这些技术以实现进程间的协同工作。此外,文章还将探讨IPC在高并发环境下的性能优化策略,以及如何避免常见的IPC编程错误。通过结合理论与实践,本文不仅适合希望深入了解操作系统原理的技术人员阅读,也对那些致力于提升软件质量和开发效率的程序员具有重要参考价值。
25 0
|
1月前
|
存储 监控 安全
探究Linux操作系统的进程管理机制及其优化策略
本文旨在深入探讨Linux操作系统中的进程管理机制,包括进程调度、内存管理以及I/O管理等核心内容。通过对这些关键组件的分析,我们将揭示它们如何共同工作以提供稳定、高效的计算环境,并讨论可能的优化策略。
30 0