前言
之前我们学习了管道,管道的本质就是文件,通过文件描述符进行数据的传输,只不过只是内存级别的,不往磁盘中写入。
管道很有用,但速度不算很快,因为数据需要在内核空间和用户空间之间进行来回拷贝,且缓冲区大小有限,可能会限制数据量的传输。因此Linux系统设计者还设计了专门用户通信的System V共享内存,进程可以通过将共享内存段映射到它们的地址空间中来实现共享数据。
一、System V共享内存
共享内存区是最快的IPC(InterProcess Communication)形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到 内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
进程间通信的前提:让不同进程看到同一份由操作系统提供的资源。
共享内存就是先在物理内存中开辟一段空间,然后在进程共享区中也找一段空间,通过页表映射,那么我们进程就会获取到映射的起始地址。那么就可以通过该地址进行访问,往里面写入或者读取数据。
另一个进程不需要再创建了,只需要找到开辟的空间,同样映射到自己的共享区中,这样便达到了进程间通信的前提,可以通过共享内存进行通信。
当进程通讯完毕,我们想要取消通信,让进程去干其他事情时,可以取消页表的映射关系。在进程地址空间开辟内存的本质,其实就是建立虚拟地址到物理地址的映射,通过是否有映射关系来判断该地址是否被使用,当页表映射去除,并不需要清理虚拟地址空间数据,只需要后面开辟的时候在写入覆盖就好。
而我们所开辟在物理内存的空间,他并不会因为进程的退出而关闭,而是长期保存,就算没有一个进程在使用该空间,也不会关闭,需要手动将共享内存释放掉。共享内存的声明周期是随内核的!
既然系统支持进程使用共享内存的方式进行通信,那么系统中不止有两个进程,可能存在很多进程,建立很多共享内存,在进行通信,那么操作系统一定要对共享内存进行管理,必须清楚有那些进程在使用该共享内促,又有那些进程在使用领一个共享内存。而管理的本质——先描述,再组织,因此操作系统中肯定有共享内存的结构体。这样就可以把对共享内存的管理,转变为对数据结构的增删查改。
二、共享内存的系统接口
1.shmget()
shmget()
- 作用:可以创建或获取共享内存
- 参数1:key,做一个标识,让需要通过共享内存通信的进程都可以通过这个值来找到共享内存。
- 参数2:size,创建共享内存的大小,建议设置size为4096的整数倍。
- 参数3:shmflg,定义了宏,并可以添加权限。重要的选项如下
IPC_CREAT,shm不存在就会创建,存在就获取并返回
IPC_EXCL,代表存在,一般不单独使用,IPC_EXCL | IPC_CREAT shm不存在就创建,存在就报错并返回。(保证创建的共享内存是全新的)
- 返回值:成功返回共享内存标识符shmid,失败返回-1,并设置错误码。
2.shmat()
shmat() at->attach
- 作用:将共享内存挂载到进程的虚拟地址空间共享区中。
- 参数1:shmid,共享内存标识符,代表你使用的那个共享内存
- 参数2:shmaddr,代表手动设置共享内存挂载到的虚拟内存地址,传入nullptr,代表让操作系统帮我选择地址
- 参数3:shmflg,表示按照某个方式(读写)挂载到虚拟内存上,传入0就代表使用shmget的默认权限
- 返回值:挂接成功的共享内存在虚拟地址空间中的起始地址,出错返回-1。
3.shmdt()
shmdt() dt->detach
- 作用:移除挂载到进程地址空间的共享内存
- 参数:挂载时候的地址。
4.shmctl()
shmctl() ctl->ctrl
- 参数1:shmid,共享内存标识符,代表你使用的那个共享内存
- 参数2:cmd,命令选项(IPC_STAT、IPC_SET、IPC_RMID:移除共享内存)
- 参数3:buf,共享内存属性的数据结构,不需要设置为nullptr。
三、共享内存代码实现
首先,我们需要先使用shmget创建共享内存,其Key理论上我们是可以随便设置的,只要不发生冲突就好,但是如果让系统函数帮我们处理,比如说利用哈希的思想,就能让冲突概率足够低,因此可以选择使用 ftok 函数生成key。
我们只需要传递 pathname 和 proj_id就可以,这两个参数也是可以随便设置的,但建议是pathname设置为当前路径,proj_id设置一个略微复杂的数就可以。
服务端使用如下代码进行创建共享内存
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/ipc.h> //Inter-Process Communication
#include <sys/shm.h>
#include <unistd.h>
using namespace std;
const string pathname = "/home/kky/centos_test/109/240320_systemV_sharedMemory";
const int proj_id = 0x12345678;
key_t GetKey()
{
key_t key = ftok(pathname.c_str(), proj_id);
if (key < 0)
{
cerr << "错误码:" << errno << ",错误信息:" << strerror(errno) << endl;
exit(1);
}
return key;
}
int main()
{
key_t key = GetKey();
cout << "key:" << key << endl;
int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0664);
if (shmid < 0)
{
cerr << "错误码:" << errno << ",错误信息:" << strerror(errno) << endl;
return 2;
}
cout << "shmid:" << shmid << endl;
}
输入如下指令可以查看系统中的共享内存
ipcs -m
我们进程已经结束,但共享内存仍然看得到。
并且我们再执行程序,会直接报错,告诉我们文件存在,也就是共享内存存在。这也证明了共享内存声明周期是随内核的。
那么我们创建共享内存,挂接共享内存,然后通信通信,再从操作系统进程地址空间移除共享内存,从操作系统删除共享内存,代码都放在一起如下所示。
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/ipc.h> //Inter-Process Communication
#include <sys/shm.h>
#include <unistd.h>
using namespace std;
const string pathname = "/home/kky/centos_test/109/240320_systemV_sharedMemory";
const int proj_id = 0x12345678;
key_t GetKey()
{
key_t key = ftok(pathname.c_str(), proj_id);
if (key < 0)
{
cerr << "错误码:" << errno << ",错误信息:" << strerror(errno) << endl;
exit(1);
}
return key;
}
int main()
{
//获取key
key_t key = GetKey();
cout << "key:" << key << endl;
//创建共享内存
int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0664);
if (shmid < 0)
{
cerr << "错误码:" << errno << ",错误信息:" << strerror(errno) << endl;
return 2;
}
cout << "shmid:" << shmid << endl;
//将共享内存挂载到虚拟内存中间
char* s = (char*)shmat(shmid,nullptr,0);
cout<<"shm挂载"<<endl;
//这里通信
//将shm从进程地址空间移除
sleep(5);
shmdt(s);
cout<<"将shm从进程地址空间移除"<<endl;
//从操作系统移除shm
sleep(5);
shmctl(shmid,IPC_RMID,nullptr);
cout<<"从操作系统移除shm"<<endl;
sleep(10);
}
上面是服务端的,那么我们客户端代码就很简单,只需要打开共享内存,挂载,通信完毕,取消挂载即可。
那么现在,当我们将共享内存创建好,挂载完成,现在就需要开始通信了,共享内存的通信方式很简单,直接对挂载的返回值——s ,做写入和读取就可以了。
server进行读取
client进行写入
如下是运行结果,因为我们设置sleep的原因,写入比较慢,读取比较快,这里我们看到共享内存并不和管道一样,读取会同步,共享内存的通讯读取并不同步。
但是这样有可能读取数据不完整,人家可能还没发完消息,你这边读了一半就直接走了,这样肯定不好,我们可以使用管道的同步机制,server通过管道给client发送消息,说你可以读取了再让client进行读取。
运行结果发现,成功读写同步,虽然sleep(3)和sleep(1),但在管道的作用下,依然同步了
为了观看方面,最后我做了一下封装, 代码如下
comm.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/ipc.h>//Inter-Process Communication
#include <sys/shm.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
const int size = 4096;
const string pathname = "/home/kky/centos_test/109/240320_systemV_sharedMemory";
const int proj_id = 0x12345678;
key_t GetKey()
{
key_t key = ftok(pathname.c_str(),proj_id);
if(key<0)
{
cerr<<"错误码:"<<errno<<",错误信息:"<< strerror(errno)<<endl;
exit(1);
}
return key;
}
int _CreateShm(key_t key,int flag)
{
int shmid = shmget(key, size, flag);
if (shmid < 0)
{
cerr << "错误码:" << errno << ",错误信息:" << strerror(errno) << endl;
return 2;
}
}
int CreateShm(key_t key)
{
return _CreateShm(key, IPC_CREAT|IPC_EXCL|0664);
}
int GetShm(key_t key)
{
return _CreateShm(key, IPC_CREAT);
}
string ToHex(int id)
{
char buff[1024];
snprintf(buff,sizeof(buff),"0x%x",id);
return buff;
}
int Creatpipe()
{
//创建管道
int n = mkfifo("fifo",0666);
if(n<0)
{
cerr << "错误码:" << errno << ",错误信息:" << strerror(errno) << endl;
exit(3);
}
}
server.cc
#include "comm.hpp"
using namespace std;
void Communitation(char* s,int fd)
{
while(true)
{
int code = 0;
ssize_t r = read(fd,&code,sizeof(code));
if(s>0)
{
cout<<"共享内存中的内容"<<s<<endl;
sleep(1);
}
else if(s==0)
{
break;
}
}
}
int main()
{
Creatpipe();
//获取key
key_t key = GetKey();
cout << "key:" << ToHex(key) << endl;
//创建共享内存
int shmid = CreateShm(key);
cout<<"shmid:"<<shmid<<endl;
//将共享内存挂载到虚拟内存中间
char* s = (char*)shmat(shmid,nullptr,0);
cout<<"shm挂载"<<endl;
int fd = open("fifo",O_RDONLY);
//这里通信
Communitation(s,fd);
//将shm从进程地址空间移除
//sleep(5);
shmdt(s);
cout<<"将shm从进程地址空间移除"<<endl;
//从操作系统移除shm
//sleep(5);
shmctl(shmid,IPC_RMID,nullptr);
cout<<"从操作系统移除shm"<<endl;
}
client.cc
#include "comm.hpp"
void Communication(char* s,int fd)
{
// 通信
char c = 'a';
while (c < 'z')
{
s[c - 'a'] = c;
cout << "写入了:" << c << endl;
c++;
sleep(3);
int code = 1;
write(fd,&code,sizeof(code));
}
}
int main()
{
key_t key = GetKey();
int shmid = GetShm(key);
char *s = (char *)shmat(shmid, nullptr, 0);
int fd = open("fifo",O_WRONLY);
// 通信
Communication(s,fd);
//关闭链接
shmdt(s);
}
Makefile
.PHONY:all
all:client server
server:server.cc
g++ -o $@ $^ -std=c++11
client:client.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f server client fifo