最近,回头又看了下关于操作系统的知识,发现进程通信的知识还是比较有迷惑性和重要性的。所以,通过自己的学习来讲这部分的知识进行部分的总结。下面这些都是操作系统中提到的进程通信的经典问题,然后延伸到Java中的并发和同步问题。所以,对于这里面的原理还是要好好的分析和掌握。
进程通信IPC的含义和方法:
进程通信就是指进程间的信息交换,交换信息可以使一个状态,也可以是很多的byte。进程间同步互斥也存在信息的交换,因此也属于是一种IPC,属于是低级通信。该低级通信存在的问题:1)通信的数据量太少;2)通信对用户不透明(数据的传递或者同步互斥都需要程序员实现)
高级通信机制(高级通信的通信细节被OS隐藏,因此使用起来增加方便而且可以传送大量的数据,尤其是管道通信):
1. 共享内存(最快的方式):相互通信的进程共享某些数据结构或者是存储区,进程之间可以通过这些共享空间进行通信。分为:1)基于共享数据结构的通信,如生产者消费者系统中的有界缓冲区;2)基于共享存储区的通信,可以传输大量数据,通信的进程之间可以像读写普通存储器一样读写共享存储区
2. 消息传递系统:进程间通信采用的是格式化的消息,可以直接使用OS提供的消息发送或者接受原语进行通信。由于隐藏了通信细节,所以简化了通信程序的复杂性
3. 管道通信:管道是连接两个一个读进程和一个写进程之间用于实现数据交换的一个共享文件。为了协调管道通信双方,需要管道机制实现如下功能:1)互斥:统一时刻只能有一个进程对管道进行读写;2)同步:当读端发现管道为空的时候需要睡眠等待,直到有数据时候被唤醒,相应的写端也是在管道已满的时候等待直到被唤醒;3)确定对方的存在性:只有同时有读端和写端,管道才有存在意义
4:信号量:进程间通信处理同步互斥的机制。信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。(关键就是通过PV操作)
我建议大家阅读这篇文章的时候,先对PV操作有点了解,这样能够更好的理解下面的内容。
可以阅读下我的另外一篇文章:https://blog.csdn.net/cs_hnu_scw/article/details/80204038
一:生产者和消费者
#define N 100 //缓冲区中的数目
typedef int semaphore; //定义信号量的类型
semaphore mutex = 1 ; //临界区的访问数目
semaphore full = 0 ; //缓冲区中满槽的数目
semaphore empty = N ; //缓冲区中空槽的数目
//生产者
void producer(void){
int item;
while(true){
item = produce_item(); //模拟生产者生产出的数据
P(&empty) ; //进行P操作,将空槽的数目-1
P(&mutex) ; //进入临界区
insert_item(item) ; //将产生的数据放入到缓冲区中,便于消费者消费
V(&mutex) ; //退出临界区
V(&full) ; //满槽的数目+1,因为产生了一个数据
}
}
//消费者
void consumer(void){
int item;
while(true){
P(&full) ; //进行P操作,将满槽的数目-1
P(&mutex) ; //进入临界区
item = getProduce_item(item) ; //获取缓冲区中的数据
V(&mutex) ; //退出临界区
V(&empty) ; //空槽的数目+1,因为取出了一个数据
consum_item(item); //消费者进行消费
}
}
思路:
(1)生产者,首先需要判断缓冲区中的空槽数目是否为0,如果为0,那么表示无法进行生产,而只能阻塞,直到消费者进行消费之后,才能继续生产。
(2)生产者,如果判断当前的空槽数目大于0,那么表示可以进行生产数据到缓冲区。并且生产之后让满槽的数目+1。
(3)针对消费者来说,需要判断满操作的数目是否为0,如果为0,表示缓冲区中没有数据,所以必须阻塞,而等待生产者进行生产数据之后再进行消费。
(4)消费者如果发现满缓冲区中的数目是大于0的,那么直接可以进行消费。并且消费之后,让空槽的数目+1。
特别注意:对于生产者代码中的两个P操作交换一下次序,将使得mutex的值在empty之前而不是之后减1,。如果此时缓冲区完全满了,生产者将被阻塞,而此时mutex又为0。这样的话,当消费者下次试图进行消费缓冲区的时候,它将对mutex执行一个P操作,而此时mutex为0,而消费者也被阻塞,这样就会导致死锁。
二:哲学家进餐问题
#define N 5 //哲学家的数目
#define LEFT (I+N-1)%N //i的左邻居编号
#define RIGHT (I+1)%N //i的右邻居编号
#define THINKING 0 //哲学家在思考
#define HUNGRY 1 //哲学家试图拿起叉子
#define EATING 2 //哲学家进餐
typedef int semaphore; //定义信号量的类型
int state[N] ; //数据用来跟踪记录每位哲学家的状态
semaphore mutex = 1 ; //临界区的访问数目
semaphore s[N] //每位哲学家一个信号量
//哲学家,其中参数i表示:哲学家编号,从0到N-1
void philosopher(int i){
while(true){
think(); //哲学家在思考
take_forks(i); //哲学家想得到两把叉子,要么成功要么阻塞
eat(); //哲学家进行用餐
put_forks(i); //哲学家把叉子放回去
}
}
//尝试是否能够获取两把叉子
void take_forks(int i){
P(&mutex); //进入临界区
state[i] = HUNGRY; //哲学家处于饥饿状态,想获取两把叉子
test(i); //进行尝试获取两把叉子
V(&mutex); //离开临界区
P(&s[i]); //如果得不到叉子则阻塞
}
//尝试获取两把叉子
void test(int i ){
if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT]!= EATING){
stati[i] = EATING;
V(&s[i]);
}
}
//将叉子放回
void put_forks(int i ){
P(&mutex); //进入临界区
state[i] = THINGK; //因为该哲学家就餐完了,所以对应的信号量位置处于“思考”状态
test(LEFT); //检查其哲学家的左边哲学家是否可以就餐
test(RIGHT); //检查其哲学家的右边哲学家是否可以就餐
V(&mutex); //退出临界区
}
思路:
(1)开始的时候,哲学家都是在进行思考操作。当某个哲学家需要进餐的时候,首先,他需要判断是否能够获取到身边的两把叉子。如果不能获取,那么就进行阻塞。
(2)如果此时当前哲学家成功获取了两把叉子,即当前的哲学家处于饥饿状态,并且其左右两边的哲学家的状态都不是处于就餐状态,那么表示周围的哲学家并没有在使用叉子,所以,能够进行就餐操作。
(3)当当前哲学家进行就餐完成之后,就需要把获取到的叉子进行放入,来供给其他的哲学家进行后续的就餐。
(4)当哲学家进行放回叉子的时候,就判断其左右哲学家是否能够进行就餐,如果可以,那么就让其两把的哲学家进行就餐。
三:读者–写者问题
typedef int semaphore; //定义信号量的类型
semaphore db ; //控制对数据库的访问
semaphore mutex = 1 ; //临界区的访问数目
int readerNumber = 0 ; //正在读或者即将读的数目
//读者
void reader(void){
while(true){
P(&mutex); //进入临界区
readerNumber = readerNumber +1; //当前读者的数目+1
if(readerNumber == 1){ //如果当前读者就只有一个,那么就要对数据库进行控制
P(&db); //得到数据库的控制,因为免得让写者进行修改
}
V(&mutex); //退出临界区
read_data_base; //进行阅读
P(&mutex); //进入临界区
readerNumber = readerNumber - 1; //读者的数目-1
if(readerNumber == 0){ //如果当前读者的数目为0,那么就要放弃数据库的控制权,这样写者才可以进行操作
V(&db); //放弃数据库控制权
}
V(&mutex); //退出临界区
use_date_read(); //使用读到的数据
}
}
//写者
void write(void){
while(true){
think_up_data(); //非临界区的操作
P(&db); //获取数据库控制权,从而进行写操作
write_data_todb(); //进行更新数据库
V(&db); //释放数据库的控制,这样读者才可以进行读操作
}
思路:
(1)在一个读者到达且没有写者的时候,那么判断是否是第一个读者,如果是的话,那么就需要得到数据库的控制权,以免写者到达的时候进行修改数据库。
(2)如果连续的读者到来,那么由于读者是可以同时访问的,所以,读者没有影响。
(3)当写者到来的时候,首先判断数据库权利是否可获得,如果不可获得,表示还有读者来进行读操作,只能等所有的读者读完之后,才能进行写操作,并且同时要获取数据库权利,以免读者进行读修改的内容。
(4)如果写者到达时候,获得了数据库控制权,那么表示没有读者,那么写者进行写操作,并获得数据库控制权。
四:睡觉中的理发师问题
伪代码:
#define CHAIRS 5 //定义空的椅子数目,也就是最大的顾客数目
typedef int semaphore; //定义信号量的类型
semaphore customer = 0 ; //当前的顾客数目
semaphore barbers = 0 ; //等待顾客的理发师数目
semaphore mutex = 1 ; //临界区的访问数目
int waiting = 0 ; //顾客在等待而不是理发
//理发师
void barber(void){
while(true){
P(&customer); //如果等待理发的顾客数目为0,则理发师进入休眠
P(&mutex); //进入临界区
wating = waitng - 1; //等待理发的顾客数目-1
V(&barbers); //当前理发师准备好进行理发
V(&mutex); //退出临界区
cut_hair(); //理发
}
}
//顾客
void customer(void){
P(&mutex); //进入临界区
if(wating < CHAIRS){ //如果当前等待理发的顾客数目小于总的椅子数目
waiting = waiting + 1; //等待理发的数目+1
V(&customers); //如果需要,就唤醒理发师
V(&mutex); //退出临界区
P(&barbers); //如果空闲的理发师为0,那么就进行睡眠状态
get_haircut(); //顾客进行理发
}else{ //表示没有多余的椅子供顾客坐了,那么顾客直接离开理发店
V(&mutex);
}
}
思路:
(1)当理发师开始工作时,他执行barber,这将导致理发师阻塞在信号量customer上,因为该信号量的初值为0,理发师就去睡觉,直到有顾客来。
(2)当一个顾客来之后,他执行过程customer,首先获取信号量mutex以进入临界区。如果不久之后又有一位顾客来,则这个顾客只能等待第一个顾客释放mutex后才能做别的事。该顾客随后检查是否等候的顾客少于椅子数,如果不是,他就释放mutex并离开。
(3)如果有一把椅子可坐,则这个顾客递增整型变量wating,随后对信号量customer执行V操作,唤醒理发师。此时顾客和理发师都处于清醒状态。当顾客释放mutex时,理发师获得mutex,他进行一些准备后开始理发。
(4)当理发完成后,顾客退出该过程,并离开理发店。与前边的例子不同,这里顾客不执行循环,因为每个顾客只需要一次理发,但理发师必须执行循环以服务下一位顾客。如果有顾客,则为顾客理发。否则去睡觉。
总结:当最初看这个的时候,其实不太明白里面的原因,但是当多看了几次之后,并且加上自己的理解和生活体验,慢慢的对于这些经典问题就有了眉目。对于这些问题,不仅仅是操作系统提到的,而在Java的开发中是经常有碰到的,就比如并发操作里面的购买票的问题,其实就是一个生产者–消费者问题。如果,把原理弄清楚了,再结合Java本身的语法知识,那么很快就能够对这个问题进行求解。所以,知识其实是相通的,要擅于理解并且进行发散性思维,因为很多时候,一个问题会迁出很多个问题,那么我们就又需要对后续的问题进行求解,这样的话,对于我们自身的知识面就会不断的进行扩展,而这也正是BAT这些公司在面试所看重的能力。。
上面也是自己的一些理解,如果有什么不对的地方,欢迎大家批评指正,共同学习~
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/12402.html