操作系统(7)---进程通信、进程互斥、进程同步与信号量
一、进程通信
进程通信是进程之间的信息交换。主要有三种方式:
- 管道通信
e.g ps -aux | grep exp (左边只能写,右边只能读,半双工管道,管道在内核)
管道的实质是一个用于连接读写进程的一个共享文件,固定大小的缓冲区。数据一旦被读出,则从管道中丢弃。没写满不读,没读空不写。
- 消息传递(可以实现多进程通信)
- 直接通信方式
消息直接挂到接收进程的消息缓冲队列上
消息=消息头(发送进程ID、接受进程ID、消息类型、消息长度等)+消息体
- 间接通信方式
消息先发送至中间实体(信箱)
- 共享存储(共享空间互斥访问,也叫做临界区,内容称为临界资源)
- 基于数据结构的共享
通信时指定数据结构,低级通信方式
- 基于存储区的共享
指定内存中的一块存储区域直接进行读写,高级通信方式
二、进程互斥
涉及共享存储的时候,进程互斥可以解决并发进程对临界区的使用问题(临界区资源竞争)。当某一进程在访问临界区的时候,其他进程不可读出或者修改该存储区域的内容。
一般通过锁机制来实现。通过设置标识来表明临界区是否可用。可以实现多个进程互斥访问。
三、进程同步与信号量
同步是互相合作的并发进程在一些关键点上互相等待与互通消息,从而实现协调进行。实质是各合作进程的行为保持某种一致性和不变关系。解决了异步环境下进程合作协调推进的问题。
以司机和售票员为例,司机启动车辆需要等待关门信号,售票员开门需要等待司机停车信号。在这种情况下,锁机制失效,因为司机和售票员两个进程不存在资源竞争且两次的信号信息不同。
引入信号量进制来实现同步。信号用来实现P操作(sleep)和V操作(wakeup)
用信号量解决司机售票员问题来进一步理解:
有两个信号量s1、s2。s1代表关门,value初值为0,queue指向司机进程;s2代表停车,value初值为0,queue指向售票员。
司机进程的步骤为:P(s1)——>运行——>V(s2)。先判断是否受到关门信号,来决定发车(继续运行)还是等待关门信号(司机进程sleep)。运行结束,给出停车信号唤醒售票员进程。
售票员进程的步骤为:V(s1)——>坐车——>P(s2)。先给出关门信号唤醒司机进程发车。坐车结束,先判断是否受到停车信号,来决定开门(继续运行)还是等待停车信号(售票员进程sleep)。
整个步骤为:1.司机进程准备运行,P(s1)操作使s1.value--为-1,司机进程休眠执行售票员进程。
2.售票员进程开始执行,V(s1)操作使s1.value++为0,唤醒司机进程,两个进程并发执行。
3.售票员进程准备门,P(s2)操作使s2.value--为-1,售票员进程休眠。
4.司机进程中的V(s2)操作使s2.value++为0,唤醒售票员进程,司机进程结束。
信号量也可解决进程互斥问题:
上述例子中,一共有两台打印机,一次只能一个进程使用一台打印机:
若设置sem.value的初值为1,queue指向这个进程队列,那么进程1中sem.value--为0不会休眠,可以使用打印机,进程2开始value<0,从而进程234都会休眠必须等待进程1使用完打印机,通过V操作唤醒,两个打印机一次只能用一个,没有充分利用。
若设置sem.value的初值为2,queue指向这个进程队列,那么进程1中sem.value--为1不会休眠,可以使用1号打印机,进程2中sem.value--为0不会休眠,可以使用2号打印机,进程34都会休眠等待进程12使用完打印机,通过V操作唤醒,两个打印机都能充分利用,提高了效率。
信号量解决进程同步和进程互斥的区别:
初值不同,同步的信号量初值一般设为0;互斥的信号量初值设为非0正数,和临界资源个数有关,若为负数,则代表休眠进程的个数或者说休眠队列的长度。
Linux信号量实现:
有名信号量:一个特殊文件,存于/dev/shm路径下,可以被任何知道其名字的进程使用,适用于多进程合作,进程退出后需要删除并释放资源。
常用函数:1.sem_t *sem_open ( const char *name , int oflag , mode_t mode , unsigned int value ) ; 打开信号量文件,以name命名,设初值为value。
2.int sem_wait ( sem_t *sem ) ; P操作
3.int sem_post ( sem_t *sem ) ; V操作
4.int sem_close ( sem_t *sem ) ; 关闭文件
5.int sem_unlink ( const char *name ) ; 删除,45常合用,先关闭文件后删除
无名信号量:只存于内存中,适用于一个进程的多线程合作,用完也要删除并释放资源。
常用函数:1.int sem_init ( sem_t *sem , int pshared , unsigned int value ) ; 设置无名信号量
2.int sem_wait ( sem_t *sem ) ; P操作
3.int sem_post ( sem_t *sem ) ; V操作
4.int sem_destroy ( sem_t *sem ) ; 删除
实例(有名信号量在多进程中的应用):
#include<stdio.h> #include<stdlib.h> #include<semaphore.h> #include<unistd.h> #include<fcntl.h> #include<sys/wait.h> int main(int argc,char **argv) { int pid; sem_t *sem; const char sem_name[]="mysqm"; pid=fork(); if(pid<0){ printf("Error in the fork\n"); } else if(pid==0){ sem=sem_open(sem_name,O_CREAT,0644,1); if(sem==SEM_FAILED){ printf("unable to create semaphore\n"); sem_unlink(sem_name); exit(-1); } sem_wait(sem); for(int i=0;i<3;++i){ printf("child process run:%d\n",i); sleep(1); } sem_post(sem); } else{ sem=sem_open(sem_name,O_CREAT,0644,1); if(sem==SEM_FAILED){ printf("unable to create semaphore\n"); sem_unlink(sem_name); exit(-1);} sem_wait(sem); for(int i=0;i<3;++i) { printf("parent process run: %d\n",i); sleep(1); } sem_post(sem); wait(NULL); sem_close(sem); sem_unlink(sem_name); } return 0; }
结果:
用信号量解决生产者和消费者问题:
问题描述:若仓库已满,生产者不能再生产,必须等待消费;若仓库是空的,消费者不能再消费,必须等待生产;若库存充足,生产者和消费者不能同时对仓库库存进行修改。
问题分析:该问题同时包含了进程互斥和进程同步,需要三个信号量s1、s2、s3,s1代表仓库是否已满,s2代表仓库是否已空,s3代表仓库库存是否正在被修改。进程互斥的value初值取决于临界资源的个数,这里只有一个仓库,故s3.value设置是为1。
生产者的步骤为:p(s1)判断是否可以生产——>p(s3)占用临界区——>生产——>v(s3)释放临界区——>v(s2)提醒消费者可以消费了。
消费者的步骤为:p(s2)判断是否可以消费——>p(s3)占用临界区——>消费——>v(s3)释放临界区——>v(s2)提醒生产者有仓库有空位可以生产了。
关于s1和s2初值value设定:按照上文所说,若都设置为0,生产者和消费者一起休眠,且s1s2同时为0,表示仓库又满又空,这是矛盾的。所以,s1和s2的初值不能同时为0,s1.value代表仓库还有多少空位,s2.value代表仓库还有多少库存。
注意:同时有进程同步和进程互斥时,要先同步后互斥,先确定是否有继续执行的条件再给予临界区控制权,否则会出现僵持(例如满仓时,若生产者先执行,占用了临界区,p(s1)操作使生产者休眠,转而执行消费者,然而生产者未释放临界区资源,消费者也不可用,两个进程出现僵持)。
问题模拟代码:
#include <semaphore.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <sys/types.h> #include <sys/wait.h> #include <fcntl.h> #include <sys/ipc.h> #define NBUFF 10 int nitems=2; struct { int count; sem_t *mutex,*empty,*full; }shared; void *produce(void *arg){ int i; for(i=0;i<nitems;i++){ sem_wait(shared.full); sem_wait(shared.mutex); shared.count++; printf("produce successfully! now:%d\n",shared.count); sleep(1); sem_post(shared.mutex); sem_post(shared.empty); } return NULL; }
void *consume(void *arg){ int i; for(i=0;i<nitems;i++){ sem_wait(shared.empty); sem_wait(shared.mutex); shared.count--; printf("consume successfully! now:%d\n",shared.count); sleep(1); sem_post(shared.mutex); sem_post(shared.full); } return NULL; } int main(int argc,char **argv){ pthread_t tp,tc; const char s1[]="full"; const char s2[]="empty"; const char s3[]="mutex"; shared.full=sem_open(s1,O_CREAT,0644,NBUFF); shared.empty=sem_open(s2,O_CREAT,0644,0); shared.mutex=sem_open(s3,O_CREAT,0644,1); pthread_create(&tp,NULL,produce,NULL); pthread_create(&tc,NULL,consume,NULL); pthread_join(tp,NULL); pthread_join(tc,NULL); sem_unlink(s1); sem_unlink(s2); sem_unlink(s3); return 0; }
结果:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理