信号量与管程
互斥只是进程间关系的一个方面,很多时候,进程间还需要进行同步和通信,因此就需要更高级的同步互斥方法。
信号量(抽象数据类型)
- 一个整型sem , 两个原子操作
- P() : sem - 1 , 若 sem < 0 等待,否则继续
- V():sem + 1, 若 sem <= 0 , 唤醒一个等待的P
- 互斥:二进制信号量
mutex = new Semaphore(1);
- 条件同步:计数信号量
fullBuffers = new Semaphore(0);
emptyBuffers = new Semaphore(n);
相比于锁机制,可以实现进程的并行。
实现细节
1 | classSemaphore{ |
存在问题
- 开发代码时容易出错:使用的信号量已经被占用;忘记释放信号量
- 不能处理死锁
管程(模块)
最初是从编程语言的层面提出的,而不是os层面。通过在高级语言中设计管程机制,简化开发中对同步互斥的操作,实现语言的并发机制。
可以把管程理解成一个特殊的空间。
- 一个Lock :指定临界区, 还没有拿到锁的进程挂在entry queue上,等待进入管程
- 0或n个条件变量:已进入管程的进程在并发访问临界区时所受到的约束,等待进程挂在对应条件变量的等待队列(queues associated with x,y….conditions)上
- wait():释放锁进入睡眠
- signal()
实现细节
1 | class Condition{ |
区别于信号量的V操作,signal不一定会做减法。
例:生产者消费者问题
1 | classBoundedBuffer{ |
存在的问题
当一个正在执行的进程A发出signal操作后,将会调度一个就绪的进程B,那么这个时候到底要执行那一个进程呢?
- Hansen方法:让进程A继续执行,完成release操作,这个时候可能存在多个就绪进程去抢占lock
- Hoare方法:让进程B继续执行,此时不会有抢占问题。用if代替while
经典同步问题的实现
读者写者问题
读者优先
已经有读者时,下一个读者也可以进入,只是需要用原子操作来改变count
变量。可能造成写者一直等待
写者
1 | sem_wait(WriteMutex); |
读者
1 | sem_wait(CountMutex); |
写者优先
同理,写者之间不互斥
哲学家就餐问题
思路一:先拿左再拿右。可能同时拿左,造成死锁
思路二:右边不在把左边放回,随机等待一会再重复。可能造成饥饿
思路三:将就餐的的动作视为互斥。每次允许一人吃饭,效率降低,有三个叉子浪费了
思路四:要么不拿要么拿两把,把状态当临界资源,改变状态是一个互斥操作。
我正学到这,老爸走进来,我跟他说了这个问题,他说这还不好办,把筷子从中间一砍啊! 真妙啊…..
1 | void philosopher(int i) |
进程通信(IPC)
为了解决进程间竞争关系(间接制约关系)而引入进程互斥;为了解决进程间松散的协作关系( 直接制约关系)而引入进程同步;
为了解决进程间紧密的协作关系而引入进程通信。互斥是一种特殊的同步,进程同步是一种进程通信。
进程之间互相交换信息的工作称之为进程通信IPC (InterProcess Communication)(主要是指大量数据的交换)。进程同步往往只有传递信号的作用,不能传递数据。
从发送路径上看,可分为:直接通信和间接通信(通过一个消息队列发送和接收消息)
从对是否正确发送消息的处理方式来看,可分为:同步阻塞(直到正确发送才进行下一步)和异步非阻塞
进程间的通信可以分为两种模型,分别为:
- 共享内存(Shared Memory): 直接通信。开辟一段新的贡献资源段,然后两个进程之间进行数据信息等的交互,该过程不受CPU的控制。
- 消息传递(Message Passing):间接通信。将相关的信息传给内核,然后内核将其转发
信号
类似于一种异步打断机制,用软件中断通知事件处理。
具体实现:需要接受信息的应用程序会在OS中注册一个handler
函数,在收到传来的信号时,OS从内核态返回用户态,并把应用程序的堆栈指针设置为handler
函数的入口地址
信号与信号量的区别:
信号(signal):是一种处理异步事件的方式。信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程外,还可以发送信号给进程本身。linux除了支持unix早期的信号语义函数,还支持语义符合posix.1标准的信号函数sigaction。
信号量(Semaphore):进程间通信处理同步互斥的机制。在多线程环境下使用,它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。
信号比较高效但不能传数据。
管道|
可以进行数据交换。一个进程的输出作为另一个的输入,完成重定向。
管道是一种虚文件,实际上是内核的一块缓存区,之所以称之为虚文件,因为对于左边进程而言它就像一个接收文件,对右边进程而言它就像一个数据发送文件。
具体实现:创建管道,创建两个新的进程,由于子进程继承父进程的文件描述符,这时把管道当做一种文件来处理,并分别设置两个子进程的管道写端和管道读端
消息队列
管道的建立依赖于进程间的父子关系,而且传输的数据是字节流,没有结构化关系。消息队列可以解决这两个问题,将消息作为字节序列存储在消息数组中
共享内存
管道和消息队列都是间接通信。共享内存实现的是直接通信,可以快速大量地传输数据,但是会有一致性问题。
SOCKET
更多用在网络进程通信。相关内容以前已经写过。