✨个人主页: 北 海
🎉所属专栏: Linux学习之旅
🎃操作环境: CentOS 7.6 阿里云远程服务器
文章目录
- 🌇前言
- 🏙️正文
- 1、什么是共享内存?
- 2、共享内存的相关知识
- 2.1、共享内存的数据结构
- 2.2、创建 shmget
- 2.2.1、关于 key 的获取
- 2.3、释放共享内存
- 2.3.1、通过指令释放
- 2.3.2、通过共享内存控制函数释放
- 2.4、进程关联 shmat
- 2.5、进程去关联 shmdt
- 2.6、共享内存控制 shmctl
- 3、共享内存简单使用
- 4、共享内存的补充知识
- 4.1、共享内存的大小
- 4.2、共享内存 “快” 的原因
- 4.3、共享内存的缺点
- 5、共享内存实操--配合命名管道完成通信
- 5.1、逻辑设计
- 5.3、效果演示
- 5.4、注意事项
- 5.5、完整源码
- 🌆总结
🌇前言
共享内存出自 System V
标准,是众多 IPC
解决方案中最快的一种,使用共享内存进行通信时,不需要借助函数进入内核传递数据,而是直接对同一块空间进行数据访问,至于共享内存是如何使用的、通信原理是怎么实现的、以及共享内存+命名管道的组合通信程序该如何实现,都将在本文中解答
天下武功,唯快不破
🏙️正文
1、什么是共享内存?
共享内存 全称 System V
共享内存,是一种进程间通信解决方案,并且是所有解决方案中最快的一个,在通信速度上可以做到一骑绝尘
这是 System V
标准中一个比较成功的通信方式,特点就是非常快,除此之外,System V
标准中还有另外两种通信方式:
- 消息队列
- 信号量
这两种通信方式现在已经比较少见了,因为 存在更好的、更实用的通信方式(比如 POSIX
中提供的通信方式)
话不多说,先来看看 System V
共享内存的工作原理:在物理内存中开辟一块公共区域,让两个不同的进程的虚拟地址同时对此空间建立映射关系,此时两个独立的进程能看到同一块空间,可以直接对此空间进行【写入或读取】,这块公共区域就是 共享内存
显然,共享内存的目的也是 让不同的进程看到同一份资源
关于共享区:共享区作为虚拟地址空间中一块缓冲区域,既可作为堆栈生长扩展的区域,也可用来存储各种进程间的公共资源,比如这里的共享内存,以及之前学习的动态库,相关信息都是存储在共享区中
注意: 共享内存块的创建、进程间建立映射都是由 OS
实际执行的
2、共享内存的相关知识
在正式使用共享内存通信之前,需要先学习一下 共享内存的相关知识,因为这里的共享内存出自 System V
标准,所以 System V
中的消息队列、信号量绝大部分接口的风格也与之差不多
2.1、共享内存的数据结构
共享内存不止用于两个进程间通信,所以共享内存必须确保能持续存在,这也就意味着共享内存的生命周期不随进程,而是随操作系统,一旦共享内存被创建,除非被删除,否则将会一直存在,因此 操作系统需要对共享内存的状态加以描述
共享内存也不止存在一份,当出现多块共享内存时,操作系统不可能一一比对进行使用,秉持着高效的原则,操作系统会把已经创建的共享内存组织起来,更好的进行管理
所以共享内存需要有自己的数据结构,经过操作系统 先描述,再组织 后,构成了下面这个数据结构
注:shm
表示共享内存
struct shmid_ds
{
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
其中 struct ipc_perm
中存储了 共享内存中的基本信息,具体包含内容如下:
struct ipc_perm
{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
共享内存虽然属于文件系统,但它的结构是经过特殊设计的,与文件系统中的 inode
那一套结构逻辑不一样
2.2、创建 shmget
创建共享内存时,需要借助 shmget
这个函数
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
关于 shmget
函数
组成部分 | 含义 |
---|---|
返回值 int | 创建成功返回共享内存的 shmid ,失败返回 -1 |
参数1 key_t key | 创建共享内存时的唯一 key 值,通过函数计算获取 |
参数2 size_t size | 创建共享内存的大小,一般为 4096 |
参数3 int shmflg | 位图,可以设置共享内存的创建方式及创建权限 |
因为共享内存拥有自己的数据结构,所以 返回值 int
实际就是 shmid
,类似于文件系统中的 fd
,用来对不同的共享内存块进行操作
参数2为创建共享内存的大小,单位是字节,一般设为 4096
字节(4kb
),与一个 PAGE
页大小相同,有利于提高 IO
效率
参数3是位图结构,类似于 open
函数中的参数3(文件打开方式),常用的选项有以下几个:
IPC_CREAT
创建共享内存,如果存在,则使用已经存在的IPC_EXCL
避免使用已存在的共享内存,不能单独使用,需要配合IPC_CREAT
使用,作用是当创建共享内存时,如果共享内存已经存在,则创建失败权限
因为共享内存也是文件,所以权限可设为文件的起始权限0666
而参数1比较特殊,key_t
实际就是对 int
进行了封装,表示一个数字,用来标识不同的共享内存块,可以理解为 inode
,因为是标识值,所以必须确保 唯一性,需要使用函数 ftok
根据不同的 项目路径 + 项目编号 + 特殊的算法,生成一个碰撞率低的标识值,供操作系统对共享内存进行区分和调用
2.2.1、关于 key 的获取
使用函数 ftok
生成 key
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
关于 ftok
函数
组成部分 | 含义 |
---|---|
返回值 key_t | 返回生成的标识值,等价于 int 类型 |
参数1 const char *pathname | 项目路径,可使用 绝对 或 相对 路径 |
参数2 int proj_id | 项目编号,可以根据实际情况编写 |
注意: 只有先让操作系统根据同一个 key
创建/打开 同一个共享内存,不同的进程才能看到同一份资源
下面是创建 共享内存 的代码
common.h
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
using namespace std;
#define PATHNAME "." // 项目名
#define PROJID 0x29C // 项目编号
const int gsize = 4096;
const mode_t mode = 0666;
//将十进制数转为十六进制数
string toHEX(int x)
{
char buffer[64];
snprintf(buffer, sizeof buffer, "0x%x", x);
return buffer;
}
// 获取key
key_t getKey()
{
key_t key = ftok(PATHNAME, PROJID);
if (key == -1)
{
// 失败,终止进程
cerr << "ftok fail! "
<< "errno: " << errno << " | " << strerror(errno) << endl;
exit(1);
}
return key;
}
// 共享内存助手
int shmHelper(key_t key, size_t size, int flags)
{
int shmid = shmget(key, size, flags);
if (shmid == -1)
{
// 失败,终止进程
cerr << "shmget fail! "
<< "errno: " << errno << " | " << strerror(errno) << endl;
exit(2);
}
return shmid;
}
// 创建共享内存
int createShm(key_t key, size_t size)
{
return shmHelper(key, size, IPC_CREAT | IPC_EXCL | mode);
}
// 获取共享内存
int getShm(key_t key, size_t size)
{
return shmHelper(key, size, IPC_CREAT);
}
server.cc
#include <iostream>
#include "common.h"
using namespace std;
int main()
{
// 服务端创建共享内存
key_t key = getKey();
int shmid = createShm(key, gsize);
cout << "server key: " << toHEX(key) << endl;
cout << "server shmid: " << shmid << endl;
return 0;
}
client.cc
#include <iostream>
#include "common.h"
using namespace std;
int main()
{
// 客户端打开共享内存
key_t key = getKey();
int shmid = getShm(key, gsize);
cout << "client key: " << toHEX(key) << endl;
cout << "client shmid: " << shmid << endl;
return 0;
}
运行结果如下:
通过 shmget
和 ftok
函数获得唯一的 key
和 shmid
创建出来的共享内存可以通过 ipcs -m
查看
ipcs -m
共享内存 301465
就是通过上述代码生成的
注意: 因为共享内存每次都是随机生成的,所以每次生成的 key
和 shmid
都不一样
2.3、释放共享内存
当我们再次运行程序时,会出现下面这种情况:
服务端运行失败,原因是 shmget
创建共享内存失败,这是因为服务端创建共享内存时,传递的参数为 IPC_CREAT | IPC_EXCL
,其中 IPC_EXCL
注定了当共享内存存在时,创建失败
而客户端只是单纯的获取共享内存,同时也只传递了 IPC_CREAT
参数,所以运行才会成功
综上所述,服务端运行失败的根本原因是 待创建的共享内存已存在,如果想要成功运行,需要先将原共享内存释放
共享内存的释放方式主要有以下两种:
2.3.1、通过指令释放
可以直接在命令行中通过指令,根据 shmid
释放指定共享内存
ipcrm -m shmid
共享内存已被释放
2.3.2、通过共享内存控制函数释放
这里先提前使用一下函数 shmctl
,在服务端中加入删除共享内存的函数,当服务端运行结束时,自动删除共享内存
shmctl(shmid, IPC_RMID, NULL);
server.cc
#include <iostream>
#include "common.h"
using namespace std;
int main()
{
// 服务端创建共享内存
key_t key = getKey();
int shmid = createShm(key, gsize);
cout << "server key: " << toHEX(key) << endl;
cout << "server shmid: " << shmid << endl;
int n = 5;
while(n)
{
//运行五秒后删除共享内存
cout << n-- << endl;
sleep(1);
}
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
共享内存也被成功释放了,实际中会使用函数进行自动释放,因为手动释放比较麻烦
2.4、进程关联 shmat
共享内存在被成功创建后,进程还不 “认识” 它,只有让待通信进程都 “认识” 同一个共享内存后,才能进行正常通信,让进程 “认识” 共享内存这一操作称为 关联
当进程与共享内存关联后,共享内存才会 通过页表映射至进程的虚拟地址空间中的共享区中
需要使用 shmat
函数进行关联
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
关于 shmat
函数
组成部分 | 含义 |
---|---|
返回值 void* | 如同 malloc 一样,返回的是 void* 指针,可以根据需求进行强转 |
参数1 int shmid | 待关联的共享内存 id |
参数2 const void *shmaddr | 共享内存关联至进程共享区的地址,可以不用管 |
参数3 int shmflg | 关联后,进程对共享内存的读写属性 |
当进程与共享内存关联后,返回的就是共享内存映射至共享区的起始地址
- 关联成功返回起始地址
- 关联失败返回
(void*) -1
共享内存映射至共享区时,我们可以指定映射位置(即传递参数2),但我们一般不知道具体地址,所以 可以传递 NULL
,让编译器自动选择位置进行映射
关于参数3,一般直接设为 0
,表示关联后,共享内存属性为 默认读写权限,更多选项如下所示:
SHM_RDONLY
关联共享内存后只进行读取操作SHM_RND
若shmaddr
不为NULL
,则关联地址自动向下调整为SHMLBA
的整数倍,SHMLBA
的值为PAGE_SIZE
,具体调整公式:shmaddr - (shmaddr % SHMLBA)
一般通信数据为字符,所以可以将 shmat
的返回值强转为 char*
下面是 服务端 和 客户端 关联共享内存 的代码
服务端睡五秒后结束,客户端睡三秒后就结束,监视窗口每隔2秒更新一次
server.cc
#include <iostream>
#include "common.h"
using namespace std;
int main()
{
// 服务端创建共享内存
key_t key = getKey();
int shmid = createShm(key, gsize);
cout << "server key: " << toHEX(key) << endl;
cout << "server shmid: " << shmid << endl;
char *start = (char*)shmat(shmid, NULL, 0);
if ((void*)start == (void*)-1)
{
cerr << "shmat fail!"
<< "errno: " << errno << " | " << strerror(errno) << endl;
shmctl(shmid, IPC_RMID, NULL); //即使异常了,也要把共享内存释放
exit(1);
}
//挂接成功后,睡五秒再释放
printf("start: %p\n", start);
sleep(5);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
client.cc
#include <iostream>
#include "common.h"
using namespace std;
int main()
{
// 客户端打开共享内存
key_t key = getKey();
int shmid = getShm(key, gsize);
cout << "client key: " << toHEX(key) << endl;
cout << "client shmid: " << shmid << endl;
char *start = (char *)shmat(shmid, NULL, 0);
if ((void *)start == (void *)-1)
{
cerr << "shmat fail!"
<< "errno: " << errno << " | " << strerror(errno) << endl;
exit(1);
}
// 挂接成功后,睡三秒就结束
printf("start: %p\n", start);
sleep(3);
return 0;
}
共享内存信息中的 nattch
表示当前共享内存中的进程关联数
注意: 程序运行结束后,会自动取消关联状态
2.5、进程去关联 shmdt
可以手动去关联,即使用函数 shmdt
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
这个函数使用非常简单,将已关联的共享内存地址传递进行去关联即可
返回值:去关联成功返回 0
,失败返回 -1
,并将错误码设置
如同关闭 FILE*
、fd
、free
等一些列操作一样,当我们关联共享内存,使用结束后,需要进行去关联,否则会造成内存泄漏(指针指向共享内存,访问数据)
所以需要在上面的代码结尾加上 shmdt(start)
去关联
注意:
-
共享内存在被删除后,已成功挂接的进程仍然可以进行正常通信,不过此时无法再挂接其他进程
-
共享内存被提前删除后,状态
status
变为 销毁dest
2.6、共享内存控制 shmctl
System V
标准中还为共享内存提供了一个控制函数 shmctl
,其原型如下图所示:
关于 shmctl
函数
组成部分 | 含义 |
---|---|
返回值 int | 成功返回 0 ,失败返回 -1 |
参数1 int shmid | 待控制的共享内存 id |
参数2 int cmd | 控制共享内存的具体动作,同样是位图 |
参数3 struct shmid_ds *buf | 用于获取或设置所控制共享内存的数据结构 |
之前在释放共享内存时,我们就已经使用过了 shmctl
,给参数2传入的是 IPC_RMID
,表示删除共享内存,除此之外,还可以给参数2传递以下动作:
IPC_STAT
用于获取或设置所控制共享内存的数据结构IPC_SET
在进程有足够权限的前提下,将共享内存的当前关联值设置为buf
数据结构中的值
buf
就是共享内存的数据结构,可以使用 IPC_STAT
获取,也可以使用 IPC_SET
设置
当参数2为 IPC_RMID
时,参数3可以不用传递;其他两种情况都需传递 struct shmid_ds *buf
演示代码:通过 shmctl
获取共享内存的数据结构,并从中获取 pid
、key
#include <iostream>
#include "common.h"
using namespace std;
int main()
{
// 服务端创建共享内存
key_t key = getKey();
int shmid = createShm(key, gsize);
cout << "getpid(): " << getpid() << endl;
cout << "server key: " << toHEX(key) << endl;
char *start = (char*)shmat(shmid, NULL, 0); //去关联
if ((void*)start == (void*)-1)
{
cerr << "shmat fail!"
<< "errno: " << errno << " | " << strerror(errno) << endl;
shmctl(shmid, IPC_RMID, NULL); //即使异常了,也要把共享内存释放
exit(1);
}
struct shmid_ds buf;
int n = shmctl(shmid, IPC_STAT, &buf);
if (n == -1)
{
cerr << "shmctl fail!"
<< "errno: " << errno << " | " << strerror(errno) << endl;
shmctl(shmid, IPC_RMID, NULL); //即使异常了,也要把共享内存释放
exit(1);
}
cout << "==================" << endl;
cout << "buf.shm_cpid: " << buf.shm_cpid << endl;
cout << "buf.shm_perm.__key: " << toHEX(buf.shm_perm.__key) << endl;
shmdt(start); //去关联
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
通过程序证明了 共享内存确实有自己的数据结构
结论: 共享内存 = 共享内存的内核数据结构(struct shmid_ds
) + 真正开辟的空间
3、共享内存简单使用
当两个进程与同一块共享内存成功关联后,可以直接对该区域进行读写操作,就像 父子进程读取同一个数据一样,不过不能进行写入,因为会发生 写时拷贝 机制,拷贝共享数据
但共享内存就不一样了,真·共享,不会发生 写时拷贝
简单使用共享内存流程如下:
- 创建、关联共享内存
- 客户端向服务端写入数据(字符串)
- 服务端每隔十秒读取一次
为了使操作更加简洁,可以将 common.h
中的代码封装为一个类,创建、关联、去关联等操作一气呵成
common.hpp
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
using namespace std;
#define PATHNAME "." // 项目名
#define PROJID 0x29C // 项目编号
enum
{
SERVER = 0,
CLIENT = 1
};
class shm
{
public:
shm(int id)
: _id(id)
{
_key = getKey(); //获取 key
// 根据不同的身份,创建 / 打开 共享内存
if (_id == SERVER)
_shmid = shmHelper(_key, gsize, IPC_CREAT | IPC_EXCL | mode);
else
_shmid = shmHelper(_key, gsize, IPC_CREAT);
// 关联共享内存
_start = shmat(_shmid, NULL, 0); // 关联
if (_start == (void *)-1)
{
cerr << "shmat fail!"
<< "errno: " << errno << " | " << strerror(errno) << endl;
exit(1);
}
}
~shm()
{
// 去关联
int n = shmdt(_start);
if (n == -1)
{
cerr << "shmdt fail!"
<< "errno: " << errno << " | " << strerror(errno) << endl;
exit(1);
}
// 根据不同的身份,判断是否需要删除共享内存
if(_id == SERVER)
shmctl(_shmid, IPC_RMID, NULL);
}
key_t getKey() const
{
key_t key = ftok(PATHNAME, PROJID);
if (key == -1)
{
// 失败,终止进程
cerr << "ftok fail! "
<< "errno: " << errno << " | " << strerror(errno) << endl;
exit(1);
}
return key;
}
int getShmID() const
{
return _shmid;
}
void *getStart() const
{
return _start;
}
protected:
static const int gsize = 4096;
static const mode_t mode = 0666;
// 将十进制数转为十六进制数
string toHEX(int x)
{
char buffer[64];
snprintf(buffer, sizeof buffer, "0x%x", x);
return buffer;
}
// 共享内存助手
int shmHelper(key_t key, size_t size, int flags)
{
int shmid = shmget(key, size, flags);
if (shmid == -1)
{
// 失败,终止进程
cerr << "shmget fail! "
<< "errno: " << errno << " | " << strerror(errno) << endl;
exit(2);
}
return shmid;
}
private:
key_t _key;
int _shmid = 0;
void *_start;
int _id; // 身份标识符,用来区分服务端与客户端
};
server.cc
#include <iostream>
#include <cstring>
#include <unistd.h>
#include "common.hpp"
using namespace std;
int main()
{
// 服务端
shm s(SERVER);
// 获取共享内存起始地址
char *start = (char *)s.getStart();
// 开始通信
while (true)
{
cout << "server get: " << start << endl;
//读取到26个字母后,关闭服务端
if (strlen(start) == 26)
break;
sleep(1);
}
return 0;
}
client.cc
#include <iostream>
#include "common.hpp"
using namespace std;
int main()
{
// 客户端
shm c(CLIENT);
// 获取共享内存起始地址
char *start = (char *)c.getStart();
// 开始通信
int n = 0;
printf("client sent: ");
fflush(stdout); //细节:强制刷新缓冲区
//写入26个字母后,终止客户端
while (n < 26)
{
start[n] = ('A' + n);
printf("%c", start[n]);
fflush(stdout); //细节:强制刷新缓冲区
n++;
start[n] = '\0';
sleep(1);
}
cout << endl;
return 0;
}
运行效果如下:
注意:
- 如果想实现
client
不按回车打印数据,需要使用fflush
手动刷新printf
的缓冲区 - 需要先启动服务端,才启动客户端;如果先启动了客户端,会导致客户端创建共享内存后,无法释放,程序也无法运行
- 因为共享内存不区分读端与写端,只要关联了,两者都可以进行读写
4、共享内存的补充知识
关于共享内存,还需要知道以下几个特点
4.1、共享内存的大小
在上面的代码中,我们将共享内存的大小设为 4096
字节,即一个 PAGE
页的大小(4kb
);如果申请 4097
字节大小的共享内存,操作系统实际上会分配 8192
字节(8kb
的空间),但供共享内存使用的只有 4097
字节
为什么会出现这种现象?
- 因为操作系统为了避免因非法操作导致出现越界访问问题,所以会开辟
PAGE
页的整数倍大小空间,多开辟的空间不会给共享内存时,主要是用来检测是否出现了越界访问
4.2、共享内存 “快” 的原因
共享内存 IPC
快的秘籍在于 减少数据拷贝(IO
),IO
是很慢、很影响效率的
比如在使用管道通信时,需要经过以下几个步骤:
- 从进程
A
中读取数据(IO
) - 打开管道,然后通过系统调用将数据写入管道(
IO
) - 通过系统调用从管道读取数据(
IO
) - 将读取到的数据输出至进程
B
(IO
)
也就说,使用管道通信至少需要经过 4
次 IO
但共享内存就不一样,直接访问同一块区域进行数据读写
在使用共享内存通信时,只需要经过以下两步:
- 进程
A
直接将数据写入共享内存中 - 进程
B
直接从共享内存中读取数据
显然,使用共享内存只需要经过 2
次 IO
所以共享内存的秘籍是 减少拷贝(IO
)次数
- 得益于共享内存的这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程通信中,速度最快的
4.3、共享内存的缺点
共享内存这么快,为什么不直接只使用共享内存呢?
因为快是要付出代价的,因为 “快” 导致共享内存有以下缺点:
- 多个进程无限制地访问同一块内存区域,导致共享内存中的数据无法确保安全
- 即 共享内存 没有同步和互斥机制,某个进程可能数据还没写完,就被别人读走了,或者被别人覆盖了
总的来说,不加规则限制的共享内存是不推荐使用的
就像 《唐伯虎点秋香》 中船夫一样
(船上)
唐伯虎: 哎,兄弟啊,给我追一下华府的官船。
船夫: 好!公子,小心小心啊!
船夫: 公子,你还真识货,这么多船,你偏偏挑中了我这条船,我可是出了名的快啊。
唐伯虎: 是吗?
船夫: 当然了。
唐伯虎: 哎~~~你的船在下沉哎!
船夫: 我不是说了,沉也沉得快嘛。
当然可以利用其他通信方式,控制共享内存的写入与读取规则
- 比如使用命名管道,进程
A
写完数据后,才通知进程B
读取 - 进程
B
读取后,才通知进程A
写入
假如是多端写入、多端读取的场景,则 可以引入生产者消费者模型,加入互斥锁和条件变量等待工具,控制内存块的读写
5、共享内存实操–配合命名管道完成通信
共享内存如果不加以控制的话,很难实现管道般的通信,所以我们要对它进行改造
5.1、逻辑设计
共享内存的特点是 无读写规则限制,进程即可读也可写,容易造成冲突,因此我们可以对其加以限制,所使用的工具正是上文中学习的 命名管道
场景:两个独立进程使用共享内存实现通信
所需要资源:一块共享内存,两条命名管道
- 一条管道负责 服务端写,客户端读,另一条管道则负责 服务端读,客户端写,间接实现 双向通知
可能有的人想问:为什么不直接使用共享内存通知?答案很简单,我们加入命名管道的目的就是为了实现进程间使用共享内存通信,当然不能使用 共享内存 -> 辅助实现共享内存通信,这不合理
所以我们这个程序的逻辑设计流程如下:
- 创建共享内存,将服务端、客户端进程关联
- 创建两条管道,分别让服务端、客户端以不同方式打开
- 进行通信
因为大部分操作之前都已经学过了,所以这里直接先演示效果,然后说明一下注意事项,想提前看看源码的同学可以跳转至最后一个部分
5.3、效果演示
这里模拟实现的是 客户端写,服务端读,如果想反转,更改读写逻辑即可,因为共享内存支持双向通信
5.4、注意事项
在这份代码中,我们需要注意 谁先启动的问题,因为是两条命名管道,刚开始都在等对方写入数据,所以必须由一方先出击,打破这种 无限等待 的破局,建议谁读取,谁就先通知,即在执行通信代码前,通知 写入方 可以写入数据了
关于其他值得 注意 的点:
- 打开命名管道文件时,需要特别注意,别打开错了
- 在通信结束后,需要删除命名管道文件
5.5、完整源码
将 共享内存 和 命名管道 的前置准备工作进行封装,代码极其优雅
common.h
#include <iostream>
#include <cerrno>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
#define PATHNAME "." // 项目名
#define PROJID 0x29C // 项目编号
// 两条管道名
const char *fifo_name1 = "fifo1";
const char *fifo_name2 = "fifo2";
enum
{
SERVER = 0,
CLIENT = 1
};
class shm
{
public:
shm(int id)
: _id(id)
{
_key = getKey(); // 获取 key
// 根据不同的身份:
// 创建 / 打开 共享内存
// 创建 / 打开 命名管道
if (_id == SERVER)
{
_shmid = shmHelper(_key, gsize, IPC_CREAT | IPC_EXCL | mode);
int n = mkfifo(fifo_name1, mode);
assert(n != -1);
(void)n;
n = mkfifo(fifo_name2, mode);
assert(n != -1);
(void)n;
// 服务端以写打开命名管道1,以读打开命名管道2
_wfd = open(fifo_name1, O_WRONLY);
_rfd = open(fifo_name2, O_RDONLY);
}
else
{
_shmid = shmHelper(_key, gsize, IPC_CREAT);
// 客户端以读打开命名管道1,以写打开命名管道2
_rfd = open(fifo_name1, O_RDONLY);
_wfd = open(fifo_name2, O_WRONLY);
}
// 关联共享内存
_start = shmat(_shmid, NULL, 0); // 关联
if (_start == (void *)-1)
{
cerr << "shmat fail!"
<< "errno: " << errno << " | " << strerror(errno) << endl;
exit(1);
}
}
~shm()
{
// 关闭fd
close(_wfd);
close(_rfd);
// 去关联
int n = shmdt(_start);
if (n == -1)
{
cerr << "shmdt fail!"
<< "errno: " << errno << " | " << strerror(errno) << endl;
exit(1);
}
// 根据不同的身份:
// 判断是否需要删除管道文件
// 判断是否需要删除共享内存
if (_id == SERVER)
{
unlink(fifo_name1);
unlink(fifo_name2);
shmctl(_shmid, IPC_RMID, NULL);
}
}
key_t getKey() const
{
key_t key = ftok(PATHNAME, PROJID);
if (key == -1)
{
// 失败,终止进程
cerr << "ftok fail! "
<< "errno: " << errno << " | " << strerror(errno) << endl;
exit(1);
}
return key;
}
int getShmID() const
{
return _shmid;
}
void *getStart() const
{
return _start;
}
int getWFD() const
{
return _wfd;
}
int getRFD() const
{
return _rfd;
}
protected:
static const int gsize = 4096;
static const mode_t mode = 0666;
// 共享内存助手
int shmHelper(key_t key, size_t size, int flags)
{
int shmid = shmget(key, size, flags);
if (shmid == -1)
{
// 失败,终止进程
cerr << "shmget fail! "
<< "errno: " << errno << " | " << strerror(errno) << endl;
exit(2);
}
return shmid;
}
private:
key_t _key;
int _shmid = 0;
void *_start;
int _wfd; // 写端 与 读端 fd
int _rfd;
int _id; // 身份标识符,用来区分服务端与客户端
};
server.cc
#include <iostream>
#include <cstring>
#include <unistd.h>
#include "common.hpp"
using namespace std;
int main()
{
// 服务端:读取
shm s(SERVER);
char *start = (char *)s.getStart();
int wfd = s.getWFD();
int rfd = s.getRFD();
const char *str = "yes";
// 因为是服务端先启动,所以直接先向管道中发出 yes 的指令
write(wfd, str, strlen(str));
char buff[64];
while (true)
{
// 等待客户端发出操作命令
int n = read(rfd, buff, sizeof(buff) - 1);
buff[n] = 0;
if (n > 0)
{
if (strcasecmp(str, buff) == 0)
{
// 客户端允许服务端进行读取
int i = 0;
while (start[i])
{
buff[i] = start[i];
i++;
}
buff[i] = 0;
printf("server read# %s\n", buff);
if (strcasecmp("exit", buff) == 0)
break;
// 读取完成,通知客户端写入
write(wfd, str, strlen(str));
}
}
else if (n == 0)
{
cerr << "客户端未从管道中读取到数据" << endl;
}
else
{
cerr << "读取异常" << endl;
break;
}
}
return 0;
}
client.cc
#include <iostream>
#include <cstring>
#include <unistd.h>
#include "common.hpp"
using namespace std;
int main()
{
// 客户端:写入
shm c(CLIENT);
char *start = (char *)c.getStart();
int wfd = c.getWFD();
int rfd = c.getRFD();
const char *str = "yes";
srand((size_t)time(NULL));
char buff[64];
while (true)
{
// 等待服务端发出操作命令
int n = read(rfd, buff, sizeof(buff) - 1);
buff[n] = 0;
if (n > 0)
{
if (strcasecmp(str, buff) == 0)
{
printf("client write# ");
fflush(stdout);
fgets(buff, sizeof buff, stdin);
int i = 0;
while(buff[i] != '\n')
{
start[i] = buff[i];
i++;
}
buff[i] = start[i] = 0;
// 写入完成,通知服务端读取
write(wfd, str, strlen(str));
if (strcasecmp("exit", buff) == 0)
break;
}
}
else if (n == 0)
{
cerr << "客户端未从管道中读取到数据" << endl;
}
else
{
cerr << "读取异常" << endl;
break;
}
}
return 0;
}
本文中涉及的所有代码均在此仓库中:《共享内存博客仓库》
🌆总结
以上就是本次关于 Linux
进程间通信之 共享内存
的全部内容了,共享内存
是所有 IPC
中最快的一种,因为它省去了很多不必要的 IO
操作,进程直接对话进程,效率极高,不过在狂飙的后果就是不安全,因此在实现 共享内存
实现进程间通信时,需要借助其他 IPC
方式控制共享内存,这样才能合理发挥 共享内存
的实力
相关文章推荐
Linux进程间通信【命名管道】
Linux进程间通信【匿名管道】
Linux基础IO【软硬链接与动静态库】
Linux基础IO【深入理解文件系统】
Linux【模拟实现C语言文件流】
Linux基础IO【重定向及缓冲区理解】
Linux基础IO【文件理解与操作】