进程间通信 System V系列: 共享内存,初识信号量
- 一.共享内存
- 1.引入
- 2.原理
- 3.系统调用接口
- 1.shmget
- 2.shmat和shmdt
- 3.shmctl
- 4.边写代码边了解共享内存的特性
- 1.ftok形成key,shmget创建与获取共享内存
- 2.shm相关指令
- 3.shmat和shmdt挂接和取消挂接
- 4.shmctl获取共享内存信息,释放共享内存
- 5.开始通信
- 5.利用管道实现共享内存的协同机制
- 1.Sync(同步类)
- 2.读写端的修改
- 3.动图演示
- 6.共享内存的优缺点
- 二.消息队列
- 1.概念
- 2.接口,数据结构等等
- 三.信号量理论
- 1.信号量的原理
- 2.信号量的理论
- 1.从生活中的例子理解信号量
- 2.进程角度的信号量
- 3.信号量的细节
- 1.信号量必须要由OS提供并维护
- 2.信号量的基本操作
- 3.信号量的接口
- 1.semget
- 2.semctl
- 3.semop
- 四.System V系列的进程间通信的小总结
- 五.利用信号量实现共享内存的协同机制
- 1.思路
- 2.Server创建并获取信号量,Client获取信号量 -> ftok和semget
- 1.ftok
- 2.shmget
- 3.Server阻塞申请信号量资源 - semop
- 4.Client初始化信号量资源 - semctl
- 5.Server释放信号量资源 - semctl
- 6.完整代码
- 1.Common.hpp
- 2.sem.hpp
- 3.ShmServer.cpp
- 4.ShmClient.cpp
- 7.演示
我们不是都有管道了吗?为什么还要有其他的进程间通信方式呢?
当时的年代,通信技术是一个非常火的点,就像现在人工智能和各种大模型一样,类似于百家争鸣的样子,所以有很多进程间通信的方式
因为共享内存跟我们学的进程地址空间有密切联系,所以我们重点学习
而信号量我们就先认识一下,学习一下理论即可
一.共享内存
1.引入
管道方便是方便,直接复用文件接口即可,但是想要使用管道是需要访问内核的,而且管道的内核级缓冲区也是在内核当中的,因此会导致效率不是特别好(因为访问内核本身就是一个比较大的消耗)
那么有没有什么办法能够让两个进程无需访问内核就能进行进程间通信呢?
2.原理
跟命名管道一样,共享内存也是允许完全无关的两个进程商量一下一起使用同一份资源,从而实现进程间通信的
看似很好懂,但是有几个值得关注的点:
3.系统调用接口
shm就是shared_memory:共享内存
ipc: InterProcess Communication:进程间通信
1.shmget
第三个参数为什么要这么设计呢?
我们一起来分析一下
我们刚才还没有说返回值
2.shmat和shmdt
分配完一个共享内存了,下面要做的事情就是把共享内存映射到进程的进程地址空间当中,并用页表建立该映射
shmat:shmattach是负责建立映射的,也就是将共享内存和进程挂接起来
shmdt:shmdetach(detach是分离,拆卸的意思),也就是取消该共享内存跟进程的挂接关系
shmdt直接传入shmat的返回值即可
shmat:如果挂接失败,返回(void*)-1
3.shmctl
4.边写代码边了解共享内存的特性
1.ftok形成key,shmget创建与获取共享内存
下面我们应该是要使用shmat和shmdt了,不过在此之前,我们还要介绍几个指令
2.shm相关指令
如何释放呢?
可以通过shmctl系统调用接口来释放,也可以通过指令来释放
我们先介绍指令释放
这里的key显示的是16进制,我们刚才打印的是10进制
因此我们改一下代码,让它以16进制打印
3.shmat和shmdt挂接和取消挂接
while :;do ipcs -m;sleep 1;done
这里反过滤掉了root创建的共享内存
while :;do ipcs -m | grep -v root;sleep 1;done
我们看到了挂接和取消挂接的全过程
4.shmctl获取共享内存信息,释放共享内存
我们实现了共享内存的创建/获取,挂接,取消挂接和释放
下面是时候开始让这两个进程开始通信了
在挂接之后,取消挂接之前开始通信
5.开始通信
我们刚才获取信息只是为了告诉大家这个函数有这么个功能而已,因此我们就不调用这个获取信息的函数了哦
通信成功
那么没有协同机制怎么办?
一个很好的方法是借助信号量来解决这一问题,但是因为信号量的接口太麻烦(比共享内存的这些接口还要麻烦很多),因此我们以后详细介绍信号量的时候再去使用信号量的接口
要不然是像我刚才那样通信双方约定好一个暗号,读端读到暗号时意味着通信结束
而是那样只能解决一部分情况下保证读端读取完所有的写端数据时才退出
还是无法解决写端还没写入你读端就开始读了啊
我们可以利用天然具有协同机制的管道啊!!
又因为我们这两个进程是没有血缘关系的,因此我们用一下命名管道吧
这里直接把我们之前写的管理命名管道的代码拿过来
#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cerrno>
#include <cstring>
#include <string>
using namespace std;
const char* path="./namedpipe";
#define MODE 0666
class Fifo
{
public:
Fifo(const char* path):_path(path)//用成员变量保存路径
{
int ret=mkfifo(_path.c_str(),MODE);
if(ret==-1)//说明创建失败
{
cerr<<"create namedpipe fail, errno: "<<errno<<" , strerror: "<<strerror(errno)<<endl;
}
else
{
cout<<"create namedpipe succeed"<<endl;
}
}
~Fifo()
{
unlink(_path.c_str());
}
private:
string _path;
};
5.利用管道实现共享内存的协同机制
1.Sync(同步类)
2.读写端的修改
3.动图演示
成功解决了写端没写数据,读端还读的问题
6.共享内存的优缺点
下面我们趁热打铁快速了解一下消息队列
二.消息队列
1.概念
消息队列的生命周期也是随内核的,跟共享内存一样
2.接口,数据结构等等
三.信号量理论
1.信号量的原理
这里介绍信号量的原理,
一方面是为了让我们更好地理解信号量,一方面是先提出一些多线程当中的概念
2.信号量的理论
1.从生活中的例子理解信号量
2.进程角度的信号量
3.信号量的细节
1.信号量必须要由OS提供并维护
2.信号量的基本操作
3.信号量的接口
1.semget
2.semctl
3.semop
关于信号量的更多话题我们等到多线程的时候还会再说明的
四.System V系列的进程间通信的小总结
共享内存,消息队列和信号量的很多接口都是相同的,它们的内核数据结构当中也都有一个一样的结构体:ipc_perm,
它们都是主要应用于本地通信的,因此在目前的网络时代当中并不常用(用的更多的还是网络通信)
它们都属于System V系列的进程间通信,OS为了管理它们搞了一个
ipc_ids结构体,ipc_id_ary结构体,kern_ipc_perm结构体实现了ipc资源的动态申请和释放,并将对ipc资源的管理转换为了对kern_ipc_perm的增删查改和对ipc_id_ary的动态扩容
不过因为System V系列的进程间通信的结构和数据结构都是独立设计的,跟文件是完全解耦的,因此不符合Linux一切皆文件的设计思想,这也是System V系列的进程间通信并不热门的原因
如果OS能够在struct file当中封装一个ipc_perm的指针,把kern_ipc_perm关联起来,并利用文件接口封装ipc资源使用的接口,就能让System V系列的进程间通信符合一切皆文件
那样的话使用起来肯定也就更容易,肯定就能热门了
五.利用信号量实现共享内存的协同机制
本来想写完前4点就结束吧,不过心血来潮想用一下信号量,下面我们一起来用一下信号量吧
1.思路
依旧是回归我们之前需要利用管道实现共享内存的协同机制的时候
我们的目的是让读端一开始阻塞等待,等到写端准备要进行写入的时候告诉读端: 我要开始写啦,你也开始读吧
此时就能够保证读端不会在一开始的时候做无意义的读取操作
大致流程分为:
- 读端(Server)创建并获取信号量
- Server阻塞申请信号量资源,此时读端就是阻塞等待写端进行写入
- Client获取读端创建好的信号量
- 写端(Client)准备写入时初始化信号量资源
- Server成功申请信号量资源,开始进程间通信
- 最后Server释放信号量资源
流程很清晰,
(那为什么我们一开始不用信号量呢? 因为信号量接口太麻烦了…,而且我用管道和信号量来解决这一共享内存的同步机制是为了学习熟悉这些接口)
之前有一些点我没有注意到,写代码的时候屡屡碰壁,最后才搞过来了
2.Server创建并获取信号量,Client获取信号量 -> ftok和semget
1.ftok
- ftok里面传入的路径必须是我们Linux系统中的确存在的路径!!!
- 我们申请的共享内存和信号量各自的key是不可以相同的(大家也能够很好的理解,因为key才是ipc资源的唯一标识嘛)
我们就用这个产生sem的key
用这个产生shm的key
2.shmget
我们共享内存的资源就是一个整体,因此nsems传入1
然后跟共享内存一样,Server传IPC_CREAT | IPC_EXCL | 0666, Client传入IPC_CREAT即可
Server:
Client:
3.Server阻塞申请信号量资源 - semop
当sem_op<0即需要申请信号量时,如果信号量==0,那么该进程就会阻塞,等待信号量>0
而信号量在还没有被进程设置之前默认值是0,因此我们可以这样来玩
(注意:
- semget时传入的nsems时你申请的这个信号量集当中的信号量数目,而不是信号量的初始值!!
- 初始值需要进程显式传入,而且默认值是0[我就是因为这点屡屡碰壁]
- sembuf是本来就有的,不需要我们显式提供[我就是因为这点屡屡碰壁]
)
Server不设置信号量,在读取之前申请信号量资源阻塞等待写端进行写入(我们起名为lock函数)
Client即将进行写入之前初始化该信号量为1(我们起名为Unlock函数),此时Server等待成功,退出阻塞状态,开始进行读取操作
4.Client初始化信号量资源 - semctl
5.Server释放信号量资源 - semctl
6.完整代码
1.Common.hpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
using namespace std;
const char* shm_path="/home/wzs/ubuntucode/process_ipc/semaphore";
const int shm_id=0x5678;
const int agreeSize=4096;
key_t GetKey(const char* k_path,int proj_id)
{
key_t ret=ftok(k_path,proj_id);
if(ret==-1)
{
cout<<"ftok fail"<<endl;
exit(1);
}
return ret;
}
class Shm
{
public:
int GetShmid(int key,int size,int shmflg)
{
int shmid=shmget(key,size,shmflg);
if(shmid==-1)
{
cout<<"shmget fail"<<endl;
exit(1);
}
return shmid;
}
void* Attach(int shmid)
{
void* addr=shmat(shmid,nullptr,0);
if(addr==(void*)-1)
{
cout<<"shmat fail"<<endl;
exit(1);
}
return addr;
}
void Detach(void* addr)
{
shmdt(addr);
}
void DelShm(int shmid)
{
shmctl(shmid,IPC_RMID,nullptr);
}
};
2.sem.hpp
#pragma once
#include <sys/sem.h>
const char* sem_path="/home/wzs/ubuntucode/process_ipc/pipe/namepipe/name_pipepool";
const int sem_id=0x3f45289;
union semun {
int val; /* 用于SETVAL */
struct semid_ds *buf; /* 用于IPC_STAT和IPC_SET */
unsigned short *array; /* 用于GETALL和SETALL */
};
void ChangeCount(sembuf* buf,int val)
{
buf->sem_num=0;
buf->sem_op=val;
buf->sem_flg=SEM_UNDO;
}
class Sem
{
public:
int GetSemid(key_t key,int nsems,int semflg)
{
int semid=semget(key,nsems,semflg);
if(semid==-1)
{
cout<<"semget fail"<<endl;
exit(1);
}
return semid;
}
void DelSem(int semid)
{
semctl(semid,0,IPC_RMID);
}
void Change(int semid,sembuf* sops)
{
cout<<"wait sem resource..."<<endl;
semop(semid,sops,1);
cout<<"wait success!"<<endl;
}
void GetInfo(int semid)
{
int val=semctl(semid,0,GETVAL);
cout<<val<<endl;
}
void Init(int semid,semun un)
{
semctl(semid,0,SETVAL,un);
}
void Unlock(int semid)
{
union semun sem_union;
sem_union.val=1;//将信号量的初始值设置为1,此时相当于开锁,读端可以拿到信号量,开始读取
Init(semid,sem_union);
}
void lock(int semid)
{
sembuf buf;
ChangeCount(&buf,-1);
Change(semid,&buf);//我想申请信号量,但是信号量默认是0,我需要阻塞等待
}
};
3.ShmServer.cpp
#include "Common.hpp"
#include "sem.hpp"
int main()
{
Shm shm;
key_t sem_key=GetKey(shm_path,shm_id);//获取Key
int shmid=shm.GetShmid(sem_key,agreeSize,IPC_CREAT | IPC_EXCL | 0666);//申请shm
char* addr=(char*)shm.Attach(shmid);//挂接shm
//利用二元信号量(锁)
Sem sem;
key_t k=GetKey(sem_path,sem_id);
int semid=sem.GetSemid(k,1,IPC_CREAT | IPC_EXCL | 0666);
//等待获取锁
sem.lock(semid);
cout<<"receive message begin########################################"<<endl;
//开始读取
while(true)
{
if(addr[0]=='q') break;
cout<<"this is message:"<<addr<<"。"<<endl;
sleep(1);
}
cout<<"receive message over#########################################"<<endl;
cout<<"Server will detach shm now..."<<endl;
shm.Detach(addr);//解除挂接
shm.DelShm(shmid);//删除shm
sem.DelSem(semid);//删除sem
return 0;
}
4.ShmClient.cpp
#include "Common.hpp"
#include "sem.hpp"
int main()
{
Shm shm;
key_t sem_key=GetKey(shm_path,shm_id);
int shmid=shm.GetShmid(sem_key,agreeSize,IPC_CREAT);
Sem sem;
key_t k=GetKey(sem_path,sem_id);
int semid=sem.GetSemid(k,1,IPC_CREAT);
char* addr=(char*)shm.Attach(shmid);
memset(addr,0,agreeSize);
cout<<"send message begin########################################"<<endl;
//开锁
sem.Unlock(semid);
for(char c='A';c<='Z';c++)
{
addr[c-'A']=c;
sleep(1);
}
addr[0]='q';
cout<<"send message over########################################"<<endl;
cout<<"Client will detach shm now..."<<endl;
shm.Detach(addr);
return 0;
}
7.演示
以上就是Linux 进程间通信 System V系列: 共享内存,信号量,简单介绍消息队列的全部内容,希望能对大家有所帮助!!