前言
本篇文章所涉及的代码已同步到博主本人gitee
:
在之前的学习中,了解到了进程间通信——管道,知道了匿名管道和命名管道实现进程间通信的原理以及通过匿名管道实现进程池、通过命名管道实现两个进程之间的通信。
现在来看System V 版本的进程间通信
System V 进程间通信
什么是System V IPC
System V IPC(Inter-Process Communication)是Unix System V操作系统引入的一组进程间通信机制,后来被大多数Unix-like系统所采纳。它包含三种主要的通信方式:
- 消息队列(Message Queues) - 进程可以通过消息队列发送和接收格式化数据块
- 信号量(Semaphores) - 用于进程间的同步控制
- 共享内存(Shared Memory) - 允许多个进程访问同一块内存区域,是最快的IPC方式
System V IPC的特点
- 持久性:IPC资源一旦创建,会持续存在直到被显式删除或系统重启
- 键值标识:通过唯一的key_t类型键值标识IPC对象
- 权限控制:使用类似文件权限的模式位(读/写/执行对应用户/组/其他)
- 系统范围:IPC对象在整个系统范围内可见,不局限于创建它的进程
共享内存
在之前探究动态库的链接和加载时,我们知道动态库在运行时会被加载到内存中,并映射到进程的地址空间中;映射到了进程地址空间的共享代码区(堆栈之间)
这样通过将库加载到内存的物理地址与映射到进程地址空间的虚拟地址进行关联(通过页表),两个进程就看到了同一个库;(所以,动态库也被称为共享库)
那两个进程可以通过虚拟地址和物理地址的映射关系,找到同一个库文件的物理地址,看到同一个库,那可不可以看到同一块内存资源,从而实现进程间通信呢?
共享内存的原理
如上图所示,只要将物理内存地址,映射到进程地址空间中,并填写页表对应关系;
这样多个进程就可以使用虚拟地址,通过页表映射,找到同一块内存资源;这样多个进程就可以看到同一份资源,就具备了通信的条件。
所以,想要让两个/多个进程看到同一份资源,就只需申请物理内存资源、然后再将物理内存映射到进程地址空间,修改进程对应的页表;这样就可以让多个进程看到同一份内存资源。
对于上述中申请物理内存资源、将物理内存映射到进程地址空间中,修改进程页表这些操作,貌似只有操作系统才可以做这些事情啊。
所以,操作系统就会提供相对应的系统调用,我们就可以通过调用系统调用,让操作系统去完成这些事情从而实现进程间通信。
共享内存相关接口
1. 创建共享内存shmget
shmget
系统调用,用来创建一个共享内存
可以看到,shmget
一共存在三个参数:key
、size
、shmflg
参数size
size
参数表示要创建共享内存的大小;通常情况下
size
是4KB
的整数倍(因为在内存中是以4KB
块为单位的)
参数shmflg
对于这个参数,常见选项有两个:IPC_CREAT
和IPC_EXCL
IPC_CREAT
:表示在创建共享内存时,如果共享内存不存在就新建并返回新建共享内存的标识符;如果共享内存存在就返回该共享内存的标识符。IPC_CREAT | IPC_EXCL
:在创建共享内存时,如果共享内存不存在就新建;如果共享内存不存在就报错。
简单来说就是IPC_CREAT
单独使用时,创建共享内存如果不存在就新建,如果存在就返回这个已经存在共享内存。
IPC_EXCL
单独使用没有任何意义,IPC_CREAT | IPC_EXCL
一起使用,返回的共享内存一定是最新的。
这里在创建共享内存时,也要带上共享内存的默认权限(在使用
shmat
时再详细介绍权限问题)
参数key
在上述中,IPC_CREAT
和IPC_CREAT | IPC_EXCL
选项的唯一区别就是当共享内存已经存在时的区别,那如何区分共享内存(如何判断一个共享内存是否存在呢?
现在要实现进程A
和进程B
的通信,那如何保证进程A
和进程B
打开的是同一个共享内存呢?
现在来看shmget
的第一个参数key_t key
参数
key
用来区分共享内存,这个参数由用户使用时传递;这样进程
A
和进程B
打开共享内存时,只要保证key
是相等的,那拿到的就是同一个共享内存。
key
从哪来呢?
参数key
由用户使用时传递,那key
从哪来呢,任意值都可以吗?
本质上来说,key
可以是任意值,但是通常情况下,都会使用ftok
函数来生成key
。
frok
函数的作用就是根据给定的pathname
和proj_id
生成对应的key
。
pathname
:任意字符串都可以,通常情况下传递当前路径proj_id
:任意数都可以
shmget
返回值
对于shmget
函数,如果创建共享内存成功,就返回共享内存的标识符;如果创建失败就返回-1
,且错误码被设置。
了解了
shmget
的参数key
、size
、shmflg
、函数的返回值以及key
值的创建;现在来简单使用shmget
创建一个共享内存
#include <iostream>
#include <cstdio>
#include <sys/shm.h>
#include <sys/ipc.h>
#define PATHNAME "."
const int proj_id = 066;
const int size = 4096;
int main()
{
// 生成key
key_t key = ftok(PATHNAME, proj_id);
printf("key : %d\n", key);
// 创建共享内存
int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL);
printf("shmif : %d\n",shmid);
if(shmid < 0){
perror("shmget : ");
exit(1);
}
return 0;
}
这里我们是以
%d
十进制的方式输出key
,在使用ipcs -m
查看共享内存时查看到的key
是16
进制数。
可以看到共享内存创建成功了,但是我们如何看到这个创建的共享内存呢?
ipcs -m
查看共享内存
我们可以使用ipcs -m
来查看共享内存
可以看到共享内存
key
值,标识符shmid
,创建者owner
等等
2. 删除共享内存
在上述代码中,我们成功创建了共享内存,但是我们再次运行程序会发现程序报错了
这里我们使用
IPC_CREAT | IPC_EXCL
选项,当共享内存存在时就会报错;所以,我们会发现,共享内存的生命周期不是随进程的,当进程退出时,共享内存不会关闭。
(而我们的文件的声明周期是随进程的,当进程退出时,进程打开的文件就会被关闭)
那也就是说,我们不主动关闭共享内存,共享内存是一直存在的(当系统关机/重启时共享内存就被关闭了)
那如何释放共享内存资源呢?
指令ipcrm
我们可以通过指令
ipcrm -m
来释放共享内存。
通过系统调用释放共享内存
当然,我们也可以通过系统调用来释放共享内存;系统调用shmctl
shmctl
系统调用是用来控制/管理共享内存资源的系统调用,不止是用来删除共享内存。
参数shmid
这个参数指要控制共享内存的标识符,在调用
shmget
创建共享内存成功的返回值。
参数op
这个是一个标志位,通过传递特定的选项,来表示要进行的相关操作。
选项有很多,这里只截取一部分;
要想要删除一个共享内存,要传递的标志位就是
IPC_RMID
。
参数struct shmid_ds* buf
我们可以使用shmctl
函数来获取共享内存的内核数据结构,这里我们要删除共享内存,这个参数传递NULL
/nullptr
即可。
所以,我们现在就可以通过调用系统调用来删除共享内存:
#include <iostream>
#include <cstdio>
#include <sys/shm.h>
#include <sys/ipc.h>
#define PATHNAME "."
const int proj_id = 066;
const int size = 4096;
int main()
{
// 生成key
key_t key = ftok(PATHNAME, proj_id);
printf("key : %d\n", key);
// 创建共享内存
int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL);
printf("shmid : %d\n", shmid);
if (shmid < 0)
{
perror("shmget ");
exit(1);
}
// 删除共享内存
int n = shmctl(shmid, IPC_RMID, nullptr);
if (n < 0)
{
perror("shmctl");
exit(1);
}
std::cout << "rmshm success" << std::endl;
return 0;
}
3. 连接共享内存
在上述操作中,我们创建了共享内存,也释放了共享内存;
但是我们将共享内存创建出来之后,进程如何使用共享内存啊,进程也并不知道共享内存的存在啊?
所以,我们就要将共享内存映射到进程的进程地址空间中,要用的系统调用就是shmat
可以看到shmat
函数一共存在三个参数:shmid
、shmaddr
、shmflg
参数shmid
shmid
:要映射的共享内存的标识符shmid
参数shmaddr
这个参数表示我们要映射到进程地址空间的起始虚拟地址(由我们指定映射到进程地址空间的哪个位置)
但是,我们并不清楚进程地址空间的使用情况啊,所以可以传NULL
来啊让操作系统决定映射到进程地址空间的哪个位置。
shmflg
这个参数可以说是一个标志位,它可以指定附加共享内存段到进程地址空间的方式;它有以下几种取值
0
:通常表示以读/写方式附加共享内存段SHM_RDONLY
:指定以只读方式附加共享内存段SHM_REMAP
:如果次标志被设置,且共享内存已经映射到进程地址空间中,则会重新进行映射到新的地址SHM_EXEC
:运行共享内存段作为可执行代码进行映射。
这里我们在映射时无需考虑这个标志位,传0
即可。
返回值
如果shmat
映射共享内存成功,就会返回一个起始虚拟地址,然后我们就可以使用这个起始虚拟地址,根据页表映射,访问共享内存中数据了。
如果映射失败,就返回(void*) -1
,并且错误码被设置。
现在来将创建好的共享内存进行映射:
#include <iostream>
#include <cstdio>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <unistd.h>
#define PATHNAME "."
const int proj_id = 066;
const int size = 4096;
int main()
{
// 生成key
key_t key = ftok(PATHNAME, proj_id);
printf("key : %d\n", key);
// 创建共享内存
int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL);
printf("shmid : %d\n", shmid);
if (shmid < 0)
{
perror("shmget ");
exit(1);
}
//连接共享内存
void* start = shmat(shmid, nullptr, 0);
if((long long) start < 0)
{
perror("shmat");
exit(1);
}
//连接成功
printf("Start Virtual Address : %p\n", start);
//删除共享内存
int n = shmctl(shmid, IPC_RMID, nullptr);
if (n < 0)
{
perror("shmctl");
exit(1);
}
std::cout << "rmshm success" << std::endl;
return 0;
}
权限问题
在上述操作中,共享内存被创建出来了,但是在shmat
时报错,权限被拒绝,权限问题?
这是因为,在创建共享内存时,我们没有指明它的权限,这就导致创建出来的共享内存权限就是
0
;我们的进程既没有
r
权限也没有w
权限,在连接共享内存时,就被拒绝连接。(没有读写权限为什么要链接该共享内存啊)
所以,在创建共享内存时就要指明创建出来的共享内存的权限
int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
这里的权限和文件的权限一样。
4. 去连接共享内存
我们可以将共享内存映射到进程地址空间中,当然我们可以对已经映射到进程地址空间的共享内存进行去连接、
要进行共享内存的去连接操作,要使用系统调用shmdt
如果去连接成功,shmdt
函数就返回0
,去连接失败就返回-1
。
这里就不演示了,这个接口相对比较简单,只需传shmat
连接成功返回的起始虚拟地址即可。
5. 写入和读取
在上述操作中,创建了共享内存,也将共享内存映射到进程地址空间中,得到了起始虚拟地址;还可以对共享内存进程去连接、释放共享内存等操作。
但是,进程之间没有通信啊,我们只是创建共享内存映射到进程地址空间,让不同的进程看到了同一份资源(共享内存);但是没有进行通信啊
那如何利用共享内存进行进程间通信呢?也就是说如何进行读写操作呢?
创建共享内存
shmget
,创建成功返回共享内存标识符shmid
;将共享内存映射到进程地址空间
shmat
,连接共享内存成功,返回起始虚拟地址。
现在也就能够获得共享内存标识符和映射到进程地址空间的起始虚拟地址;
我们想要进行读写操作,那就要根据这个起始虚拟地址来进行读写操作,就像malloc
函数那样。
所以,现在就可以根据起始虚拟地址来进行读写操作。
这里我们可以发现共享内存实现的进程间通信的一些问题:
server
端启动之后,直接就可以读取共享内存中的内容;不会等待client
端启动。client
端写入内容之后,server
端就可以直接读取到client
写入的内容。
6. 获取内核数据结构
进程A
和进程B
要使用共享内存进行通信,就要有对应的共享内存;与此同时,存在非常多的进程要进行通信,那在内存当中就势必会存在非常多的共享内存;这些共享内存有的是刚创建的、有的是即将释放的、还有的是正在使用的。
那内存中存在如此多的共享内存,操作系统是不是也要将这些共享内存管理起来呢,如何管理呢?
先描述、再组织
在内核当中就势必存在描述一个共享内存的结构体,在这个结构体当中存在着该共享内存的
shmid
、size
、key
等等。操作系统就只要将这些内核数据结构对象管理起来,就可以将所有的共享内存管理起来。
那我们可不可以获取共享内存的内核数据结构,查看一下呢?当然是可以的。
shmctl
系统调用是对共享内存进行管理,我们可以通过第二个参数op
来控制进行某种操作。
IPC_STAT
:第二个参数传IPC_STAT
,就可以将内核数据结构struct shmid_ds
的内容拷贝到buf
中,这个buf
就是我们要传递的第三个参数。
那struct shmid_ds
中有什么呢?
可以看到
struct shmid_ds
在存在一个shm_perm
,而在shm_perm
中就存储着__key
,就是我们在创建共享内存时传递的key
值。在其中还存在其他属性,例如
uid
、pid
等等,在shmid_ds
中还存在着shm_atime
、shm_dtime
、shm_ctime
等一系列时。
所以,我们就可以获取一下内核数据结构:
#include <iostream>
#include <cstdio>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <unistd.h>
#define PATHNAME "."
const int proj_id = 066;
const int size = 4096;
int main()
{
// 生成key
key_t key = ftok(PATHNAME, proj_id);
printf("key : %d\n", key);
// 创建共享内存
// int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL);
int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
printf("shmid : %d\n", shmid);
if (shmid < 0)
{
perror("shmget ");
exit(1);
}
//连接共享内存
void* start = shmat(shmid, nullptr, 0);
if((long long) start < 0)
{
perror("shmat");
exit(1);
}
//连接成功
printf("Start Virtual Address : %p\n", start);
//获取内核数据结构
struct shmid_ds ds;
shmctl(shmid, IPC_STAT, &ds);
printf("ds->_key : %d\n",ds.shm_perm.__key);
printf("ds->natch : %ld\n",ds.shm_nattch);
//删除共享内存
int n = shmctl(shmid, IPC_RMID, nullptr);
if (n < 0)
{
perror("shmctl");
exit(1);
}
std::cout << "rmshm success" << std::endl;
return 0;
}
可以看到内核中是确实存在共享内存对应的数据结构对象的,我们通过
shmctl
传IPC_STAT
标志位也成功获取了内核的数据结构对象shmid_ds
。
基于共享内存实现进程间通信
通过以上的描述和对共享内存接口的使用,了解了共享内存的大致操作,现在来基于共享内存来实现进程间的通信;
现在我们要实现客户端client
与服务端server
的通信,由客户端client
发送信息(向共享内存中写入)、由服务端server
接受信息(从共享内存中读取)
这里就面向对象编程,设计出类shm
,客户端client
和服务端server
进程创建类shm
对象,通过调用接口来完成共享内存的创建和连接;从而具备能够进行通信的条件,来完成进程的通信。
shm
类设计
对于shm
类,这里就要提供服务端进程和客户端进程的接口,这里先逻辑出shm
类的成员函数和成员变量:
#pragma once
#include <iostream>
#define PATHNAME "."
#define PROJID 0x66
class shm
{
public:
shm() : _shmid(-1), _key(-1), _size(SIZE), _proj_id(PROJID), _pathname(PATHNAME), _start_addr(nullptr)
{
}
~shm() {}
void _creat() {} // 创建共享内存
void _get() {} // 获取共享内存
void _connect() {} // 连接共享内存
void _delete_connect() {} // 去连接
void _destroy() {} // 释放共享内存
void *_get_addr() {} // 获取起始虚拟地址
private:
int _shmid;
int _key;
int _size;
int _proj_id;
std::string _pathname;
void *_start_addr;
};
在
shm
类中,我们要给服务端提供创建共享内存_creat
、连接共享内存_connect
、释放共享内存_destroy
接口和获取起始虚拟地址_start_addr
接口。给客户端进程提供获取共享内存
_get
、获取起始虚拟地址_get_addr
接口。
1. 创建共享内存
首先,我们这里实现的shm
要提供一个接口_creat
,用来创建共享内存;
这里在shm
类中存在成员变量_pathname
和_proj_id
,就可以利用这两个成员变量来生成key
值,然后使用生成的key
值来创建共享内存。
这里判断系统调用是否出错比较常用,我们就可以设计一个宏
EXIT
,在系统调用出错时使用该宏输出错误信息和终止进程。#define EXIT(x) \ do \ { \ perror(x); \ exit(1); \ } while (0)
void _creat() // 创建共享内存
{
// 生成key值
_key = ftok(_pathname.c_str(), _proj_id);
if (_key < 0)
{
EXIT("ftok");
}
printf("key : %d\n", _key);
_shmid = shmget(_key, _size, IPC_CREAT | IPC_EXCL | 0666);
if (_shmid < 0)
{
EXIT("shmget");
}
std::cout << "shmget success" << std::endl;
}
2. 获取共享内存
服务端进程server
能够创建共享内存,要想实现server
和client
进程之间的通信,那先要让client
获取这个共享内存啊,这样这两个进程才具备通信的条件。
void _get() // 获取共享内存
{
// 生成key值
_key = ftok(_pathname.c_str(), _proj_id);
if (_key < 0)
{
EXIT("ftok");
}
printf("key : %d\n", _key);
// 获取共享内存--共享内存已经存在
_shmid = shmget(_key, _size, IPC_CREAT);
if (_shmid < 0)
{
EXIT("shmget");
}
std::cout << "shmget success" << std::endl;
}
3. 连接共享内存
server
进程创建共享内存,client
进程获取共享内存;进程要使用共享内存,就要将共享内存映射到自己的进程地址空间中。
这里要注意,
shmat
连接成功返回的是起始虚拟地址;连接失败则返回(void*) -1
。
bool _connect() // 连接共享内存
{
if (_shmid < 0)
return false;
_start_addr = shmat(_shmid, nullptr, 0);
if ((long long)_start_addr < 0)
{
EXIT("shmat");
}
std::cout << "connect success" << std::endl;
return true;
}
4. 获取起始虚拟地址
server
端和client
端连接共享内存之后,两个进程就看到了同一份资源,就具备了通信的条件;
但是server
端和client
端要使用起始虚拟地址,才能对共享内存进行写入和读取操作啊,而起始虚拟地址被封装在shm
类中;所以,就要提供一个获取共享内存虚拟地址的方法_get_addr
void *_get_addr() // 获取起始虚拟地址
{
return _start_addr;
}
5. 释放共享内存
共享内存能够被创建,连接;那也可以去连接和释放
void _delete_connect() // 去连接
{
int n = shmdt(_start_addr);
if (n < 0)
{
EXIT("shmdt");
}
std::cout << "shmdt success" << std::endl;
}
void _destroy() // 释放共享内存
{
int n = shmctl(_shmid, IPC_RMID, 0);
if (n < 0)
{
EXIT("shmctl");
}
std::cout << "destroy success" << std::endl;
}
测试
设计并实现shm
类,现在来进行简单测试:
server端
- 首先就是创建
shm
类对象- 调用
_creat
创建共享内存- 调用
_connect
连接共享内存- 进行读取共享内存中数据
- 调用
delete_connect
去连接、_destroy
释放共享内存
#include "shm.hpp"
int main()
{
shm sh;
sh._creat();
sh._connect();
char* addr = (char*)sh._get_addr();
while(true)
{
//一直读取共享内存中的数据
printf("%s\n",addr);
sleep(1);
}
sh._delete_connect();
sh._destroy();
return 0;
}
client端
- 首先创建
shm
类对象- 调用
_get
获取共享内存- 调用
_connect
连接共享内存- 进行读取共享内存中数据
- 调用
delete_connect
去连接
#include "shm.hpp"
int main()
{
shm sh;
sh._get();
sh._connect();
char* addr = (char*) sh._get_addr();
//写入
int index = 0;
for(char ch = 'A'; ch<='Z';ch++,index++)
{
addr[index] = ch;
sleep(1);
}
sh._delete_connect();
return 0;
}
可以看到,client
向共享内存中写入的数据,server
确实读取到了。
但是,我们会发现一些问题:
- 服务端进程不会等待
client
启动;(在命名管道中,server
创建管道文件,在打开管道文件时会阻塞在open
中,等待client
端)- 客户端写入共享内存的数据,
server
立马就可以读取到。- 客户端
client
退出之后,server
不知道,就会一直读取。
优化代码
在上述实现的shm
类中,无论是在server
端还是client
端,都需要自己显示去调用_creat
、_get
、_connect
等接口;
- 但是我们创建
shm
类就是为了通信,那在创建shm
类对象时,创建、获取、连接共享内存都做完,这样创建shm
类对象之后、获取起始虚拟地址就可以开始通信的。- 此外,我们也不想要显示的调用去连接、释放共享内存;这些在
shm
析构函数中做就可以在程序退出时自动调用。- 还有,创建共享内存和获取共享内存只有
shmget
中参数标识符的差别,这是不是也可以在shm
类存在一个身份标识符,来统一实现。- 最后,生成
key
值使用的pathname
和proj_id
能不能由用户提供,这样多个用户传递不同的pathname
和proj_id
就可以使用不同的共享内存了。
#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATHNAME "."
#define PROJID 0x66
#define SIZE 4096
#define EXIT(x) \
do \
{ \
perror(x); \
exit(1); \
} while (0)
#define CREATER 0
#define USER 1
class shm
{
private:
void _get_key()
{
_key = ftok(_pathname.c_str(), _proj_id);
if (_key < 0)
{
EXIT("ftok");
}
}
void _creat(int flg)
{
_shmid = shmget(_key, _size, flg);
if (_shmid < 0)
{
EXIT("shmget");
}
}
void _connect()
{
_start_addr = shmat(_shmid, nullptr, 0);
if ((long long)_start_addr < 0)
{
EXIT("shmat");
}
}
void _delete_connect()
{
if (_start_addr == 0)
return;
shmdt(_start_addr);
_start_addr = nullptr;
}
void _destroy() // 释放共享内存
{
if (_shmid < 0)
return;
shmctl(_shmid, IPC_RMID, 0);
}
public:
shm(const std::string &pathname, int proj, int id)
: _shmid(-1), _key(-1), _size(SIZE), _proj_id(proj), _pathname(pathname), _start_addr(nullptr), _id(id)
{
// 生成key
_get_key();
// 创建/获取共享内存
int flg;
if (_id == USER)
flg = IPC_CREAT;
else if (id == CREATER)
flg = IPC_CREAT | IPC_EXCL | 0666;
_creat(flg);
// 连接共享内存
_connect();
}
~shm()
{
// 去连接
_delete_connect();
// 释放共享内存
if (_id == CREATER)
_destroy();
}
void *_get_addr()
{
return _start_addr;
}
private:
int _shmid;
int _key;
int _size;
int _proj_id;
std::string _pathname;
void *_start_addr;
int _id;
};
这样,在server
端创建shm
类对象只要传入pathname
和proj_id
用来生成key
值,传入CREATER
表明自己要创建共享内存
在client
端创建类对象时只要传入pathname
和proj_id
用来生成key
值,传入USER
表明自己是使用者,使用已经创建好的共享内存即可。
同步问题
通过上述的操作,我们也会发现,共享内存在写端写入数据之后,读端立马就可以读到;这也是共享内存的一个特点;
- 高性能:
进程直接读写内存,无需内核介入或数据拷贝,速度远快于管道、消息队列等IPC方式。- 低延迟:
省去了系统调用和数据序列化的开销,适合频繁通信或大数据量交换的场景。- 灵活性:
共享内存区域可存储任意数据结构(如数组、结构体),方便复杂数据共享。
在共享内存中,系统并没有通过读取和写入的系统调用;我们在链接共享内存之后,获取起始虚拟地址地址;
根据获取到的起始虚拟地址就可以对共享内存进行访问;(像malloc
开辟空间那样)
我们可以按照任意方方式进行写入和读取。
但是共享内存也存在一些缺点:
首先就是同步问题,在
server
启动创建共享内时,并不会等待另一端进程,这就会导致只有一个进程在使用共享内存;在写端写入数据之后,读端立马就可以读取到;这样就可能导致数据的不一致,也可能写端还没有写完,读端就读取了数据。此外,共享内存要我们自己创建、连接等等;我们自己要管理共享内存,稍有遗漏就看导致内存泄露问题。(共享内存的生命周期是随内核的)
最后,当写端退出时,读端不知道;就会导致写端退出,读端一直在读取。
基于命名管道对共享内存进行同步管理
共享内存存在的一个问题就是同步问题,在写端写入时,读端是可以读取的;
可以看到,如果这里
client
每次写两个字符;但是server
读取到的字符可能是链各个、也可能读取到了一个。
这就可能导致数据不一致问题,那以现在的知识储备,有没有办法解决呢?
- 当然是有的,还记得在命名管道部分,在创建管道文件后,打开文件时就会阻塞
open
函数等待另一端。- 命名管道读端在进行时
read
,如果管道文件没有数据就会阻塞在read
函数中,如果写端退出read
函数返回值就为0
;这样就可以很好的解决共享内存的同步以及写端退出,读端不知道的问题。
这里直接使用深入了解linux系统—— 进程间通信之管道-CSDN博客的命名管道文件代码
//client.cc
#include "shm.hpp"
#include "fifo.hpp"
int main()
{
file wf(_FIFO_PATH_, _FIFO_NAME_);
wf._writeopen();
shm sh(PATHNAME, PROJID, USER);
char *msg = (char *)sh._get_addr();
int index = 0;
for (char ch = 'A'; ch <= 'Z'; ch++, index += 2)
{
msg[index] = ch;
msg[index + 1] = ch;
printf("write : %s\n",msg);
wf._write(); // 写入一条数据,唤醒server进程
sleep(1);
}
wf._close();
return 0;
}
//server.cc
#include "shm.hpp"
#include "fifo.hpp"
int main()
{
// 创建共享内存
shm sh(PATHNAME, PROJID, CREATER);
// 创建并打开管道文件
fifofile ff(_FIFO_PATH_, _FIFO_NAME_);
file rf(_FIFO_PATH_, _FIFO_NAME_);
rf._readopen();
char *msg = (char *)sh._get_addr();
msg[0] = 0;
printf("start address : %p\n", msg);
// 读取数据
while (true)
{
int n = rf._read(); // 等待写端写入
if (n == 0)
break;
printf("read : %s\n", msg);
sleep(1);
}
rf._close();
return 0;
}
这样,使用命名管道简单对共享内存进行同步约束;
可以看到
server
读取和client
写入数据一致了。
到这里本篇文章内容就结束了,感谢各位的支持。