共享内存原理
看上面这张图,其实只要是进程间通信都离不开让他们看到同一块资源(内存),其实共享内存这里和动态库那里一样,都是要加载到共享区,共享内存提供者,是操作系统,操作系统要不要管理共享内存,当然要的,先描述再组织,重新理解,共享内存=共享内存块+对应的共享内存的内核数据结构。
共享内存有关函数介绍
代码创建共享内存,接下来介绍一下这个函数
返回值:共享内存的用户层标识符,类似曾经的fd,失败返回-1
key:要通信的对方进程,怎么保证,对方能看到,并且看到的就是我创建的共享内存呢,通过key,数据是几,不重要只要能够在系统唯一即可,server && client ,使用同一个key 只要key值相同,就是看到了同一个共享内存,这里就需要用到一个函数ftok来生成,下面再仔细说。
size:这个就是你要创建的共享内存多大,共享内存的大小,最好是页(PAGE: 4096)的整数倍。
shmflg:有这两个参数IPC_CREAT and IPC_EXCL,单独IPC_CREAT,如果创建共享内存,如果底层已经存在,获取之,并且返回,如果不存在,创建之,并返回,单独使用IPC_EXCL,没有意义 ,IPC_CREAT和IPC_EXCL一起,如果底层不存在,创建之,并返回,如果底层存在,出错返回,返回成功一定是一个全新的shm。
返回一个key值,失败返回-1
pathname:这个参数,是传一个地址,最好传一个有权限访问的地址
proj_id:这个随便传一个数即可。
这个函数的作用是删除共享内存,这个第一个参数就是之前创建共享内存时返回的那个值,cmd就以什么方式删除(IPC_RMID),最后那个参数是操作系统管理共享内存的数据结构,可以传nullptr
perms是权限,nattch有多少个是与我有关联的,owner是所有者,bytes是大小。
shmat将指定的共享内存挂接到自己的地址空间,返回的就是这个共享内存的起始地址,第一个参数就不说了,前面有,第二个传nullprtr即可,第三个传0,shmdt去除关联,传前面那个函数的返回值即可。
共享内存使用
接下来代码演示:
//Log.hpp
#ifndef _LOG_H_
#define _LOG_H_
#include <iostream>
#include <ctime>
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
const std::string msg[] = {
"Debug",
"Notice",
"Warning",
"Error"
};
std::ostream &Log(std::string message, int level)
{
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
return std::cout;
}
#endif
//Makefile
.PHONY:all
all:shmClient shmServer
shmClient:shmClient.cc
g++ -o $@ $^ -std=c++11
shmServer:shmServer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f shmClient shmServer
//comm.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cassert>
#include "Log.hpp"
using namespace std;
#define PATH_NAME "/home/chx"
#define PROJ_ID 0x66
#define SHM_SIZE 4096
//shmClient.cc
#include "comm.hpp"
int main()
{
key_t k = ftok(PATH_NAME, PROJ_ID);
if (k < 0)
{
Log("create key failed", Error) << " client key : " << k << endl;
exit(1);
}
Log("create key done", Debug) << " client key : " << k << endl;
// 获取共享内存
int shmid = shmget(k, SHM_SIZE, 0);
if (shmid < 0)
{
Log("create shm failed", Error) << " client key : " << k << endl;
exit(2);
}
Log("create shm success", Error) << " client key : " << k << endl;
sleep(10);
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
if (shmaddr == nullptr)
{
Log("attach shm failed", Error) << " client key : " << k << endl;
exit(3);
}
Log("attach shm success", Error) << " client key : " << k << endl;
sleep(10);
// 使用
// 去关联
int n = shmdt(shmaddr);
assert(n != -1);
Log("detach shm success", Error) << " client key : " << k << endl;
sleep(10);
// client 要不要chmctl删除呢?不需要!!
return 0;
}
//shmServer.cc
#include "comm.hpp"
int main()
{
// 1. 创建公共的Key值
key_t k = ftok(PATH_NAME, PROJ_ID);
assert(k != -1);
Log("create key done", Debug) << " server key : " << k << endl;
// 2. 创建共享内存 -- 建议要创建一个全新的共享内存 -- 通信的发起者
int shmid = shmget(k, SHM_SIZE, IPC_CREAT | 0666 | IPC_EXCL);
if (shmid == -1)
{
perror("shmget");
exit(1);
}
Log("create shm done", Debug) << " shmid : " << shmid << endl;
// 3. 将指定的共享内存,挂接到自己的地址空间
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
Log("attach shm done", Debug) << " shmid : " << shmid << endl;
sleep(10);
// 4. 将指定的共享内存,从自己的地址空间中去关联
int n = shmdt(shmaddr);
assert(n != -1);
(void)n;
Log("detach shm done", Debug) << " shmid : " << shmid << endl;
sleep(10);
// 5. 删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
(void)n;
Log("delete shm done", Debug) << " shmid : " << shmid << endl;
return 0;
}
先运行Server端创建共享内存,然后让client去关联申请的共享内存,即可。
共享内存总结
堆栈相对而生,其中这块区域就是可以存放共享内存、内存映射和共享库,如果双方进程想进行通信,可以直接进行访问,也就是内存级的读和写。之前在文件操作中,我们使用系统调用read、write这些操作,这些属于内核级操作。而共享内存的使用是直接在进程内部进行操作的,所以效率比管道高,其不用经过内核处理。
首先我们要明白一点就是,共享内存是在内核空间的,还是用户空间,其实肯定是用户空间,不用经过系统调用,直接可以访问,双方进程如果要通信,直接进行内存级的读和写即可,我们之前讲的pipe,fifo都要通过read、write来进行通信,这是为什么呢,因为管道是文件级别的操作,而文件是操作系统负责的,处于3-4G内核空间的,所以需要调用系统调用接口,所以与之相比较他就减少了很多拷贝,所以也就快了。
但是共享内存缺乏访问控制,会带来并发问题。比如写方还没有将数据写完,读端就将数据读取了。
共享内存实现访问控制
首先我们使用 Init类,当创建对象时,就创建了fifo管道,当进程结束时fifo自动删除。
然后我们定义以下4个接口,OpenFIFO、Wait、Signal、Closefifo本质对应的是open打开fifo文件;Wait对应read等待数据的写入,无数据则阻塞;Signal对应Client端进行write写入数据;Closefifo调用close关闭文件。
class Init
{
public:
Init()
{
umask(0);
int n = mkfifo("./fifo", 0666);
assert(n == 0);
(void)n;
Log("create fifo success", Notice) << endl;
}
~Init()
{
unlink("./fifo");
Log("create fifo success", Notice) << endl;
}
};
//创建管道
Init init;
#define READ O_RDONLY
#define WRITE O_WRONLY
int OpenFIFO(string pathname, int flags)
{
Log("等待中……", Notice) << endl;
int fd = open(pathname.c_str(), flags);
assert(fd >= 0);
return fd;
}
void Wait(int fd)
{
uint32_t temp = 0;
ssize_t s = read(fd, &temp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
Log("唤醒中……", Notice) << endl;
}
void Signal(int fd)
{
uint32_t temp = 1;
ssize_t s = write(fd, &temp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
(void)s;
}
void Closefifo(int fd)
{
close(fd);
}
//Server端改动:
int fd = OpenFIFO("./fifo", READ);
for (;;)
{
Wait(fd);
printf("%s\n", shmaddr);
sleep(1);
if (strcmp(shmaddr, "quit") == 0)
{
printf("quit\n");
break;
}
}
Closefifo(fd);
//Client端改动:
int fd = OpenFIFO("./fifo", WRITE);
while (true)
{
ssize_t s = read(0, shmaddr, SHM_SIZE - 1);
Signal(fd);
if (strcmp(shmaddr, "quit") == 0)
break;
}
Closefifo(fd);
这样,server端就具有了访问控制,如果没有数据,则阻塞等待数据。