我们知道进程间通信的本质是:先让不同的进程,看到同一份资源
如下图示,这些我们都还是知道的,即有两个进程,分别有其对应的task_struct和对应的进程地址空间,通过页表可以转换到物理内存上
如下所示,如果我们在物理内存上创建一个共享区,然后通过页表映射虚拟地址,然后返回虚拟地址的起始地址
同理右侧的进程也让它这样做,我们就可以实现让不同的进程看到同一份资源了
这就是共享内存
我们这个具体可以分为三步
- 申请内存
- 将这块内存挂接到进程地址空间
- 返回首地址
那么如果我们要释放这个共享内存呢?
我们首先需要去掉关联
然后释放共享内存
那么上面的操作都是进程直接去做的吗?
当然不是的,必须直接由操作系统来做。
这个过程是由需求方给执行方提供一个系统调用
也就是操作系统要给我们用户提供一个系统调用来做的
操作系统要不要管理所有的共享内存呢???
当然要!
先描述,在组织
要用内核结构体去描述共享内存
我们首先要申请一块共享内存
所用到的系统调用接口是这个
#include
#include
int shmget(key_t key, size_t size, int shmflg);
关于这个系统调用,下面是参数的介绍
对于第二个参数size
他是创建共享内存的大小 (问题一)
单位是字节
对于它的返回值
如果成功,他会返回一个共享内存的标识,如果失败,返回-1 (问题二)
对于第三个参数shmflg
我们知道,我们这个共享内存创建出来了以后,就不需要再次创建了,只需要获取即可
所以就注定了我们需要如何创建,如何获取
如果需要关注的选项就是下面两个
IPC_CREAT(单独使用)的意思是创建一个共享内存,如果这个共享内存存在,直接获取,如果不存在就创建并返回他
IPC_CREAT | IPC_EXCL(两个一起使用):如果申请的共享额你存不存在,就创建,如果存在就出错返回。它可以确保如果我们申请成功了,这个共享内存一定是一个新的
IPC_EXCL不单独使用
(在这里我们还涉及一个权限问题三,稍后提出)
那么我们这里还存在一个问题,我们如何知道这个共享内存存在还是不存在呢?怎么保证让不同的进程看到同一个共享内存呢?(问题零)这就与key相关了
谈谈key:
这个key是一个数字,这个数字几,不重要,关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识
第一个进程可以通过key创建共享内存,第二个之后的进程,只要拿着同一个key,就可以和第一个进程看到同一个共享内存了!
对于一个已经创建好的共享内存,那么key在哪?
key在共享内存的描述对象中!第一次创建的时候,必须有一个key了,这个key怎么有呢?
key --类似于 --路径 ,具有唯一性
先来回答key的第四个问题
有一个接口
#include
#include
key_t ftok(const char *pathname, int proj_id);
这个接口中它其实就是一套算法,由pathname,和proj_id进行了数值计算
而这两个参数也是由用户自由指定的
最后的返回值就是一个冲突概率比较低的key
如果冲突了也就是失败了,我们需要调整一下两个参数
那么为什么这个接口要由操作系统来提供,并且还有可能冲突呢?
因为同一个函数中,使用同一个参数可以产生同一个key。我们这就可以确保两个进程具有一样的key了
#include "comm.hpp"
int main()
{
int shimid = GetShareMem();
sleep(20);
log(Info, "process quit...");
return 0;
}
comm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include
#include
#include
#include
#include
#include
#include
#include "log.hpp"
using namespace std;
const string pathname = "/home/jby_1";
const int proj_id = 0x6666;
const int size = 4096;
Log log;
key_t GetKey()
{
key_t key = ftok(pathname.c_str(), proj_id);
if(key < 0)
{
log(Fatal, "ftok error: %s", strerror(errno));
exit(1);
}
log(Info, "ftok success, key is : %d", key);
return key;
}
int GetShareMem()
{
key_t key = GetKey();
int shmid = shmget(key, size, IPC_CREAT|IPC_EXCL);
if(shmid < 0)
{
log(Fatal, "creat share memory error : %s", strerror(errno));
exit(2);
}
log(Info, "creat share memory success, shimid : %d", shmid);
return shmid;
}
#endif
如上代码的运行结果为
那么这个key和shmid有什么区别呢?
这个key是操作系统内标定唯一性的
而shmid是只在进程内,用来表示资源唯一性的
即一个在用户层,一个在内核层
当我们再次运行这个进程的时候
我们会发现他会说,文件已经存在了。
我们可以用下面这个指令去查看系统当中的共享内存
ipcs -m
这个shmid和key都是与我们前面进程所运行的结果一致的,这就说明这个共享内存就是由这个进程所创建的
而我们此时进程早已退出,这就说明如果我们不主动的关闭掉这个共享内存,那么它是不会被关的
即:共享内存的生命周期是随内核的 !
用户不主动关闭,共享内存会一直存在。除非内核重启/用户释放。
如果我们要释放掉这个共享内存,那么用下面的指令
ipcrm -m shmid值
运行结果为
这是因为key用于内核层,而shmid是用户层的。只有在内核当中进行对比的时候,才会使用key。在用户层统一使用shmid
我们删掉这块内存,之后重新运行就可以了
这里这个perms是权限,它其实也是一个文件,我们可以为他设置对应的权限
nattch是关联的意思,意思是当前有几个进程与这块共享内存是关联的
关于这个共享内存的权限问题,我们可以在这里进行设置
如下就是运行结果了
总之。上面我们以及该创建好了共享内存,不过我们还需要的是获取
现在我们前面的代码已经被改为了如下
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include
#include
#include
#include
#include
#include
#include
#include "log.hpp"
using namespace std;
const string pathname = "/home/jby_1";
const int proj_id = 0x6666;
const int size = 4097;
Log log;
key_t GetKey()
{
key_t key = ftok(pathname.c_str(), proj_id);
if(key < 0)
{
log(Fatal, "ftok error: %s", strerror(errno));
exit(1);
}
log(Info, "ftok success, key is : 0x%x", key);
return key;
}
int GetShareMemHelper(int flag)
{
key_t key = GetKey();
int shmid = shmget(key, size, flag);
if(shmid < 0)
{
log(Fatal, "creat share memory error : %s", strerror(errno));
exit(2);
}
log(Info, "creat share memory success, shimid : %d", shmid);
return shmid;
}
int CreateShm()
{
return GetShareMemHelper(IPC_CREAT|IPC_EXCL|0666);
}
int GetShm()
{
return GetShareMemHelper(IPC_CREAT);
}
#endif
如下所示,我们创建了一个4097字节大小的共享内存
但是其实我们更加建议创建一个4KB倍数的共享内存大小。
因为虽然我们按4097申请成功了,但是实际上操作系统给的是4096*2的大小,多余的部分不仅给了,还没法用。相当于浪费掉了。
这里有一个shmat接口
#include
#include
void *shmat(int shmid, const void *shmaddr, int shmflg);
第一个参数好说,我们知道就是我们刚刚的共享内存的shmid
第二个参数shmaddr,代表的含义是我们想将共享内存挂接到哪个位置。即共享区的哪个位置,我们一般可以设置为nullptr,让系统去决定挂到哪里去了
第三个参数就是该共享内存的权限,比如只读等等,当然我们也可以设置为0,就是按照默认的来
这个返回值void*比较像我们以前的malloc,malloc得到是一块虚拟地址,只有当读写的时候才发生缺页中断,才申请内存
我们现在用如下代码进行测试
#include "comm.hpp"
int main()
{
int shmid = CreateShm();
log(Info, "Create share success");
sleep(5);
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
log(Info, "attach shm success");
sleep(5);
return 0;
}
如下所示,前五秒连接数为0,随后五秒为1,随后变为0。但是共享内存一直存在
我们前面已经有关联的接口了,现在我们不想等进程退出,就可以进行去关联,这个也是有对应的接口的
#include
#include
int shmdt(const void *shmaddr);
这里这个参数其实就是我们曾经使用shmat关联函数所得到的虚拟地址
这个接口是比较类似于之前free接口的
那么我们这个是怎么知道我们要释放多少呢?
所以这里一定隐藏着我们没有看到的数据。malloc也是一样的
malloc其实会比我们要申请的内存多一点点的。用于存储一些我们看不到的数据。所以malloc其实更适合用于申请大空间,因为这样影响比较小。
我们一般把这些多申请的空间叫做cookie
这个接口的返回值是成功为0,失败为-1
我们用如下代码,运行结果为如下:
#include "comm.hpp"
int main()
{
int shmid = CreateShm();
log(Info, "Create share success");
sleep(5);
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
log(Info, "attach shm success, shmaddr: 0x%x", shmaddr);
sleep(5);
shmdt(shmaddr);
log(Info, "detach shm success, shmaddr: 0x%x", shmaddr);
sleep(5);
return 0;
}
我们前面所用的都是指令去删除掉共享内存,但是其实系统调用也是有删除共享内存的接口的
#include
#include
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
第一个参数我们还是比较清楚的就是共享内存的id
第三个参数就是这样一个结构体
这些就是一些属性。
这个结构体里面的属性是我们内核当中管理共享内存的一个子集。也就是可以获取共享内存里面的属性
第二个参数中
我们只关心这个
它的作用是将共享内存标记为被删除的。
而我们删除的时候是不关心它的属性的,所以属性直接设置为nullptr
对于返回值
成功为0,失败为-1
我们代码如下
#include "comm.hpp"
int main()
{
int shmid = CreateShm();
log(Info, "Create share success");
sleep(5);
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
log(Info, "attach shm success, shmaddr: 0x%x", shmaddr);
sleep(5);
shmdt(shmaddr);
log(Info, "detach shm success, shmaddr: 0x%x", shmaddr);
sleep(5);
shmctl(shmid, IPC_RMID, nullptr);
log(Info, "destory shm success, shmaddr: 0x%x", shmaddr);
return 0;
}
运行结果为
我们让进程b的代码为如下
#include "comm.hpp"
int main()
{
int shmid = GetShm();
log(Info, "Create share success");
sleep(3);
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
log(Info, "attach shm success, shmaddr: 0x%x", shmaddr);
sleep(3);
shmdt(shmaddr);
log(Info, "detach shm success, shmaddr: 0x%x", shmaddr);
sleep(3);
return 0;
}
进程a如下
#include "comm.hpp"
int main()
{
int shmid = CreateShm();
log(Info, "Create share success");
sleep(3);
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
log(Info, "attach shm success, shmaddr: 0x%x", shmaddr);
sleep(3);
shmdt(shmaddr);
log(Info, "detach shm success, shmaddr: 0x%x", shmaddr);
sleep(3);
shmctl(shmid, IPC_RMID, nullptr);
log(Info, "destory shm success, shmaddr: 0x%x", shmaddr);
return 0;
}
最终运行结果为如下
截止到现在,我们前面还没有开始通信
我们前面所做的一切都是让不同的进程看到同一份资源
我们现在可以实现一个最简单的通信
下面是进程a的代码
#include "comm.hpp"
int main()
{
int shmid = CreateShm();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
//ipc code
while(true)
{
cout << "process assess a message: " << shmaddr << endl;
sleep(1);
}
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, nullptr);
return 0;
}
下面是进程b的代码
#include "comm.hpp"
int main()
{
int shmid = GetShm();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
//ipc code
while(true)
{
char buffer[1024];
cout << "Please Enter@";
fgets(buffer, sizeof(buffer), stdin);
memcpy(shmaddr, buffer, strlen(buffer) + 1);
}
shmdt(shmaddr);
return 0;
}
运行结果为
当然上面的代码我们对进程b还加了一个缓冲区,其实我们连缓冲区都不需要
#include "comm.hpp"
int main()
{
int shmid = GetShm();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
//ipc code
//一旦有了共享内存,挂接到字节的地址空间中,我们直接把它当成我们的内存空间来用即可!
//不需要系统调用
while(true)
{
cout << "Please Enter@";
fgets(shmaddr, 4096, stdin);
}
shmdt(shmaddr);
return 0;
}
即
对于进程b
一旦有了共享内存,挂接到字节的地址空间中,我们直接把它当成我们的内存空间来用即可!
不需要系统调用对于进程a
一旦有人把数据写入到共享内存,其实我们立马就能看到了
不需要经过系统调用,就能看到数据了
共享内存没有同步互斥之类的保护机制
共享内存是所有进程间通信中,速度最快的!
因为拷贝少。而管道的方式,是要先将数据写入到管道中,然后再从管道读出来。它这种方式直接写入到内存当中了,直接用即可
- 共享内存内部的数据,由用户自己维护!
如下所示,是我们前面所提及的共享内存的一些属性
在第一个结构体中,最重要的字段是第一个shm_perm,这个还是一个结构体,也就是下面的这个。其他的字段后面都有解释
在这个第二个结构体中,第一个就是key。所以我们应用层中生成的key,最后一定会被写入到内核中。里面的这个mode就是共享内存的权限问题
我们可以直接去看看这些属性
#include "comm.hpp"
int main()
{
int shmid = CreateShm();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
struct shmid_ds shmds;
//ipc code
while(true)
{
cout << "process assess a message: " << shmaddr << endl;;
sleep(1);
shmctl(shmid, IPC_STAT, &shmds);
cout << "shm size: " << shmds.shm_segsz <<endl;
cout << "shm nattch: " << shmds.shm_nattch <<endl;
printf("shm __key: 0x%x \n" ,shmds.shm_perm.__key);
cout << "shm mode: " << shmds.shm_perm.mode <<endl;
}
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, nullptr);
return 0;
}
运行结果为
我们的代码没有同步机制。但是管道有。
我们可以用管道的方式。来进行同步
进程a代码
#include "comm.hpp"
int main()
{
Init i;
int shmid = CreateShm();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
int fd = open(FIFO_FILE, O_RDONLY);
struct shmid_ds shmds;
//ipc code
while(true)
{
char c;
ssize_t n = read(fd, &c, 1);
if(n == 0) break;
else if(n < 0) break;
cout << "process assess a message: " << shmaddr << endl;;
sleep(1);
shmctl(shmid, IPC_STAT, &shmds);
cout << "shm size: " << shmds.shm_segsz <<endl;
cout << "shm nattch: " << shmds.shm_nattch <<endl;
printf("shm __key: 0x%x \n" ,shmds.shm_perm.__key);
cout << "shm mode: " << shmds.shm_perm.mode <<endl;
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, nullptr);
close(fd);
return 0;
}
进程b代码
#include "comm.hpp"
int main()
{
int shmid = GetShm();
char* shmaddr = (char*)shmat(shmid, nullptr, 0);
int fd = open(FIFO_FILE, O_WRONLY);
//ipc code
//一旦有了共享内存,挂接到字节的地址空间中,我们直接把它当成我们的内存空间来用即可!
//不需要系统调用
while(true)
{
cout << "Please Enter@";
fgets(shmaddr, 4096, stdin);
write(fd,"c", 1);//通知对方
}
shmdt(shmaddr);
close(fd);
return 0;
}
comm.hpp代码
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include
#include
#include
#include
#include
#include
#include
#include "log.hpp"
using namespace std;
const string pathname = "/home/jby_1";
const int proj_id = 0x6666;
const int size = 4097;
Log log;
#define FIFO_FILE "./myfifo"
#define MODE 0664
enum
{
FIFO_CREATE_ERR = 1,
FIFO_DELETE_ERR,
FIFO_OPEN_ERR
};
key_t GetKey()
{
key_t key = ftok(pathname.c_str(), proj_id);
if(key < 0)
{
log(Fatal, "ftok error: %s", strerror(errno));
exit(1);
}
log(Info, "ftok success, key is : 0x%x", key);
return key;
}
int GetShareMemHelper(int flag)
{
key_t key = GetKey();
int shmid = shmget(key, size, flag);
if(shmid < 0)
{
log(Fatal, "creat share memory error : %s", strerror(errno));
exit(2);
}
log(Info, "creat share memory success, shimid : %d", shmid);
return shmid;
}
int CreateShm()
{
return GetShareMemHelper(IPC_CREAT|IPC_EXCL|0666);
}
int GetShm()
{
return GetShareMemHelper(IPC_CREAT);
}
class Init
{
public:
Init()
{
//创建一个管道
int n = mkfifo(FIFO_FILE, MODE);
if(n == -1)
{
perror("mkfifo");
exit(FIFO_CREATE_ERR);
}
}
~Init()
{
//关闭信道
int m = unlink(FIFO_FILE);
if(m == -1)
{
perror("unlink:");
exit(FIFO_DELETE_ERR);
}
}
};
#endif
运行结果为,如下所示,我们实现了同步