【Linux进程间通信】共享内存
目录
作者:爱写代码的刚子
时间:2024.3.3
前言:本篇博客将介绍system V共享内存,进程通信的本质就是让不同的进程先看到同一份资源。
system V共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
共享内存示意图
共享内存让不同进程看到同一份资源的方式就是,通过一种接口在物理内存当中申请一块内存空间,然后通过此接口将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。
注意:
这里所说的开辟物理空间、建立映射等操作都是调用系统接口完成的,也就是说这些动作都由操作系统来完成。所以操作系统需要提供具有如下功能的接口:
- 创建共享内存 —— 删除共享内存(OS内部帮我们做)
- 关联共享内存 —— 去关联共享内存(进程做,实际也是OS做)
这个操作不能由进程来执行,而由操作系统来执行,因为进程是需求方,操作系统是执行方。
(操作系统需要管理共享内存,先描述(多大,谁申请的,有多少进程关联,使用了多少,引用计数),再组织!),所以存在struct结构体,对共享内存的管理变成了对数据结构的增删查改。
共享内存的数据结构
在系统当中可能会有大量的进程在进行通信,因此系统当中就可能存在大量的共享内存,那么操作系统必然要对其进行管理,所以共享内存除了在内存当中真正开辟空间之外,系统一定还要为共享内存维护相关的内核数据结构。共享内存的数据结构如下:
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 */
};
共享内存函数
- shmget函数
-
功能:用来创建共享内存
-
原型:int shmget(key_t key, size_t size, int shmflg);
-
参数:
- key:表示待创建共享内存在系统中的唯一标识
- size:表示带创建共享内存的大小(单位是字节)(建议设置为页[4KB]的整数倍)
- shmflg:表示创建共享内存的方式,由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
IPC_CREAT(单独):如果你申请的共享内存不存在,就创建,存在,就获取并返回
IPC_CREAT | IPC_EXCL :如果你申请的共享内存不存在,就创建,存在,就出错返回。这样确保我们成功申请的这个共享内存一定是新的!
IPC_EXCL不单独使用
- 返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1;
【问题】:如何保证让不同的进程看见同一个共享内存呢?/ 如何知道这个共享内存存在还是不存在?
- 第一个参数key(共享内存标识符)可以保证,key是几并不重要。关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识。
- 第一个进程可以通过key创建共享内存,第二个之后的进程,只要拿着同一个key就可以和第一个进程看到同一个共享内存了。
- 对于一个已经创建好的共享内存,key在哪?key在共享内存的描述对象中!
- 第一次创建的时候,必须要有key。
- key——类似——路径——唯一
- ftok函数
ftok函数并没有在内核里做任何事情,只是一套算法,将pathname和proj_id进行数值计算,形成冲突概率比较小的key即可!理论上pathname和proj_id可以由用户指定(用户约定的)。
举一个例子:
common.hpp:
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include "log.hpp"
using namespace std;
const string pathname = "/home/Harvey/ceshi/共享内存";
const int proj_id = 0x5555;
const int size = 4096;
Log mylog;
key_t GetKey()
{
key_t k = ftok(pathname.c_str(),proj_id);
if(k<0)
{
mylog(Fatal,"ftok error:%s",strerror(errno));
exit(1);
}
mylog(Info,"ftok success!key is %d",k);
return k;
}
int GetShareMem()
{
key_t k =GetKey();
int shmid = shmget(k,size,IPC_CREAT | IPC_EXCL);
if(shmid<0)
{
mylog(Fatal,"create share memory error:%s",strerror(errno));
exit(2);
}
mylog(Info,"create share memory success,shmid:%d",shmid);
return shmid;
}
#endif
log.hpp:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <stdarg.h>
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define SIZE 1024
#define LogFile "log.txt"
class Log{
public:
Log():printMethod(Screen),path("./log/")
{}
~Log()
{
}
void Enable(int method)
{
printMethod = method;
}
std::string levelToString(int level)
{
switch(level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level,const std::string &logtxt)
{
switch(printMethod)
{
case Screen:
std::cout<<logtxt<<std::endl;
break;
case Onefile:
printOneFile(LogFile,logtxt);
break;
case Classfile:
printClassFile(level,logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname,const std::string &logtxt)
{
std::string _logname = path+logname;
int fd = open(_logname.c_str(),O_WRONLY | O_CREAT | O_APPEND, 0666);
if(fd<0)return;
write(fd,logtxt.c_str(),logtxt.size());
close(fd);
}
void printClassFile(int level,const std::string &logtxt)
{
std::string filename =LogFile;
//为文件名添加后缀
filename+=".";
filename+=levelToString(level);
printOneFile(filename,logtxt);
}
void operator()(int level,const char *format,...)
{
time_t t = time(nullptr);
struct tm *ctime =localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%d-%d-%d %d:%d:%d]",levelToString(level).c_str(),
ctime->tm_year + 1900,ctime->tm_mon+1,ctime->tm_mday,
ctime->tm_hour,ctime->tm_min,ctime->tm_sec);
//提取可变参数
va_list s;
va_start(s,format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer,sizeof(rightbuffer),format,s);
va_end(s);
//格式:默认部分——自定义部分
char logtxt[SIZE*2];
snprintf(logtxt,sizeof(logtxt),"%s %s\n",leftbuffer,rightbuffer);
printLog(level,logtxt);
}
private:
int printMethod;
std::string path;
};
makefile:
.PHONY:all
all:processa processb
processa:processa.cc
g++ -o $@ $^ -g -std=c++11
processb:processb.cc
g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
rm -f processa processb
processa.cc:
#include "common.hpp"
using namespace std;
extern Log mylog;
int main()
{
int shmid= GetShareMem();
mylog(Debug,"shmid:%d",shmid);
sleep(20);
return 0;
}
- 运行一下processa:
- key vs shmid:
key用于操作系统内标定唯一性
shmid只在自己的进程内,用来标识资源的唯一性
- 再运行一下processa:
我们发现了报错,进程退出了,但是资源还在。
因为ipc资源是操作系统申请的,由操作系统进行管理。
- ipcs -m查看所有的IPC资源(1.共享内存2.信号量3.消息队列)
共享内存的生命周期是随内核的!用户不主动关闭,共享内存会一直存在。除非内核重启,或者用户释放
- ipcrm -m +shmid使用shmid释放共享内存,为什么不用key,因为key是在内核态的,shmid是在用户层的。
【问题】:如何设置共享内存的权限?
- **int shmid = shmget(k,size,IPC_CREAT | IPC_EXCL | 0666);**将选项带上权限即可
再次注意!!共享内存的大小一般建议是4096字节(4kb)的整数倍。如果创建4097字节,实际上操作系统给出的是4096*2的大小
将共享内存挂接到对应的进程
- shmat函数
- char shmaddr = (char)shmat(shmid,nullptr,0);**将共享内存进行挂接
将共享内存取消挂接
- shmdt函数
- shmdt(shmaddr);
小知识,为什么释放共享内存时只需要传入起始地址?因为这个块空间还存了一些这块空间的属性(我们叫做cookie)
shmdt返回值:成功返回0,失败返回-1
释放共享内存
- shmctl函数
- **shmctl(shmid,IPC_RMID,nullptr);**销毁共享内存
Common.hpp:
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include "log.hpp"
using namespace std;
const string pathname = "/home/Harvey/ceshi/共享内存";
const int proj_id = 0x5555;
const int size = 4096;
Log mylog;
key_t GetKey()
{
key_t k = ftok(pathname.c_str(),proj_id);
if(k<0)
{
mylog(Fatal,"ftok error:%s",strerror(errno));
exit(1);
}
mylog(Info,"ftok success!key is 0x%x",k);
return k;
}
int GetShareMemHelper(int flag)
{
key_t k =GetKey();
int shmid = shmget(k,size,flag);
if(shmid<0)
{
mylog(Fatal,"create share memory error:%s",strerror(errno));
exit(2);
}
mylog(Info,"create share memory success,shmid:%d",shmid);
return shmid;
}
int CreateShareMem()
{
return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
int GetShareMem()
{
return GetShareMemHelper(IPC_CREAT);
}
#endif
processa.cc:
#include "common.hpp"
using namespace std;
extern Log mylog;
int main()
{
int shmid= CreateShareMem();
mylog(Debug,"shmid:%d",shmid);
char* shmaddr = (char*)shmat(shmid,nullptr,0);
mylog(Debug,"attach shm down,shmaddr:0x%x",shmaddr);
sleep(5);
shmdt(shmaddr);
mylog(Debug,"detach shm down,shmaddr:0x%x",shmaddr);
sleep(5);
shmctl(shmid,IPC_RMID,nullptr);
mylog(Debug,"destory shm down,shmaddr:0x%x",shmaddr);
return 0;
}
processb.cc:
#include "common.hpp"
using namespace std;
extern Log mylog;
int main()
{
int shmid= GetShareMem();
mylog(Debug,"shmid:%d",shmid);
char* shmaddr = (char*)shmat(shmid,nullptr,0);
mylog(Debug,"attach shm down,shmaddr:0x%x",shmaddr);
sleep(5);
shmdt(shmaddr);
mylog(Debug,"detach shm down,shmaddr:0x%x",shmaddr);
sleep(5);
// shmctl(shmid,IPC_RMID,nullptr);
// mylog(Debug,"destory shm down,shmaddr:0x%x",shmaddr);
return 0;
}
- 但是目前为止进程a和进程b并没有进行通信
共享内存创建出来后是给用户去使用的,当我们已经将它转化为字符串后我们可以将它当作字符串进行使用。(直接访问)
processa:
#include "common.hpp"
using namespace std;
extern Log mylog;
int main()
{
int shmid= CreateShareMem();
char* shmaddr = (char*)shmat(shmid,nullptr,0);
//IPC code
//一旦有人把数据写入到共享内存,其实我们立马能看到了
//不需要经过系统调用,直接就能看到数据了
while(true)
{
cout<<"client say@"<<shmaddr<<endl;//直接访问共享内存
}
shmdt(shmaddr);
shmctl(shmid,IPC_RMID,nullptr);
return 0;
}
processb:
#include "common.hpp"
using namespace std;
extern Log mylog;
int main()
{
//一旦有了共享空间,挂接到自己的地址空间中,直接把它当成你的内存空间来用即可
//不需要调用系统接口
int shmid= GetShareMem();
char* shmaddr = (char*)shmat(shmid,nullptr,0);
//IPC code
while(true)
{
//char buffer[1024];
cout<<"Please Enter@";
//fgets(buffer,sizeof(buffer),stdin);
fgets(shmaddr,4096,stdin);
//memcpy(shmaddr,buffer,strlen(buffer)+1);
}
shmdt(shmaddr);
return 0;
}
共享内存的特性
- 共享内存没有同步互斥之类的保护机制(所以需要被保护,可以结合管道进行使用)
- 共享内存是所有的进程间通信中,速度最快的(拷贝少,采用地址空间映射的方法)
- 共享内存内部的数据,由用户自己维护
共享内存扩展
- man shmctl通过shmctl函数查看共享内存的属性
修改成这一串代码可以查看共享内存具体的属性
struct shmid_ds shmds;
while(true)
{
cout<<"client say@"<<shmaddr<<endl;//直接访问共享内存
sleep(1);
shmctl(shmid,IPC_STAT,&shmds);
cout<<"shm size: "<<shmds.shm_segsz<<endl;
cout<<"shm nattch: "<<shmds.shm_nattch<<endl;
//cout<<"shm __key: " << shmds.shm_perm.__key<<endl;
printf("shm __key:0x%x\n",shmds.shm_perm.__key);
cout<<"shm mode: "<<shmds.shm_perm.mode<<endl;
}
共享内存配合管道进行使用
common.h:
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include "log.hpp"
using namespace std;
const string pathname = "/home/Harvey/ceshi/共享内存";
const int proj_id = 0x5555;
const int size = 4096;
Log mylog;
key_t GetKey()
{
key_t k = ftok(pathname.c_str(),proj_id);
if(k<0)
{
mylog(Fatal,"ftok error:%s",strerror(errno));
exit(1);
}
mylog(Info,"ftok success!key is 0x%x",k);
return k;
}
int GetShareMemHelper(int flag)
{
key_t k =GetKey();
int shmid = shmget(k,size,flag);
if(shmid<0)
{
mylog(Fatal,"create share memory error:%s",strerror(errno));
exit(2);
}
mylog(Info,"create share memory success,shmid:%d",shmid);
return shmid;
}
int CreateShareMem()
{
return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}
int GetShareMem()
{
return GetShareMemHelper(IPC_CREAT);
}
#define FIFO_FILE "./myfifo"
#define MODE 0664
enum
{
FIFO_CREATE_ERR = 1,
FIFO_DELETE_ERR,
FIFO_OPEN_ERR
};
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
processa:
#include "common.hpp"
extern Log mylog;
int main()
{
Init init;
int shmid = CreateShareMem();
char *shmaddr = (char*)shmat(shmid, nullptr, 0);
// ipc code 在这里!!
// 一旦有人把数据写入到共享内存,其实我们立马能看到了!!
// 不需要经过系统调用,直接就能看到数据了!
int fd = open(FIFO_FILE, O_RDONLY); // 等待写入方打开之后,自己才会打开文件,向后执行, open 阻塞了!
if (fd < 0)
{
mylog(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
exit(FIFO_OPEN_ERR);
}
struct shmid_ds shmds;
while(true)
{
char c;
ssize_t s = read(fd, &c, 1);
if(s == 0) break;
else if(s < 0) break;
cout << "client say@ " << 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;
}
processb:
#include "common.hpp"
extern Log mylog;
int main()
{
int shmid = GetShareMem();
char *shmaddr = (char*)shmat(shmid, nullptr, 0);
int fd = open(FIFO_FILE, O_WRONLY); // 等待写入方打开之后,自己才会打开文件,向后执行, open 阻塞了!
if (fd < 0)
{
mylog(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
exit(FIFO_OPEN_ERR);
}
// 一旦有了共享内存,挂接到自己的地址空间中,你直接把他当成你的内存空间来用即可!
// 不需要调用系统调用
// ipc code
while(true)
{
cout << "Please Enter@ ";
fgets(shmaddr, 4096, stdin);
write(fd, "c", 1); // 通知对方
}
shmdt(shmaddr);
close(fd);
return 0;
}
运行结果: