在上篇博文我们了解了通过管道完成进程间通信,我们了解匿名管道和命名管道,并且通过编码模拟实现使用了匿名管道和命名管道。我们知道要让进程间完成通信必须让这两个进程首先看到同一份资源,因此给予这个前提,本篇博文我们了解另外一种可以进程间通信的方式 -- 共享内存。
1.system V共享内存
system V是一套标准,这是系统级别的接口。我们知道进程间通信的前提是:先让不同的进程看到同一份资源!
1.1共享内存原理的理解
首先我们来了解一下共享内存的原理:
我们在之前知道了一个进程由自己的tast_struct和进程的地址空间,那么其中对我们来讲tast_struct有指向自己的地址空间。我们知道进程地址空间内有栈区,堆区,数据区等等.....我们在动静态库中也知道我们可以把磁盘中的库加在到内存中通过页表映射到我们进程的进程地址空间中的共享区。其中共享库就在这里。而进程间通信一定至少有两个进程(如下图),那么这两个进程是毫无关系的,每一个进程都有自己的页表,也可以通过页表映射到自己的地址空间。假设今天有一个特性的接口,首先能够在物理内存中创建一份空间,然后各进程调用这个接口,将这份空间通过页表映射到自己进程的地址空间上。那么进程就可以通过自己的地址空间看到物理内存中的这份空间。因此各进程都可以看到物理内存中的同一份资源。此时对我们来讲这种机制就是共享内存。
共享内存示意图
1.2 共享内存编码
1.2.1 共享内存函数
- shmget函数
功能:shmget是用来创建共享内存的
参数:
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
IPC_CREAT:这个参数就和命名管道一样,如果存在就不用再创建了就获取,如果不存在就创建之。
IPC_EXCL : 要和IPC_CREAT一起使用(按位或 | ),如果不存在指定的共享内存,就创建之;如果存在指定的共享内存就出错返回。能够保证如果shmget函数调用成功一定是一个全新的share memory共享内存!
这个共享内存是存在内核中的 内核会给我们维护共享内存的结构!共享内存也要被内核管理起来,先描述再组织。下面就是共享内促的内核数据结构:
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 shmid_ds{} -> struct ipc_perm -> key(shmget) 共享内存的唯一值!而通过这个key我们就能表示这个共享内存。因此如果一个共享内存存在,就有一个key值,我们只要拿到这个key值,是不是就相当于拿到了共享内存。因此为了更好的进行控制,这个key值由我们的用户来提供。我们可以通过key值来标定这个共享内存,在内核中让不同的进程看到同一份共享内存做法是让他们拥有同一个key值即可。
这个key我们一般要使用ftok来生成。
我们使用一下来看看效果:
comm.hpp
#pragma once#include <iostream>#include <cstdio>#include <cerrno>#include <cstdlib>#include <cstring>#include <sys/types.h>#include <sys/ipc.h>#include <sys/shm.h>#include <unistd.h>using namespace std;#define PATH_NAME "/home/Lxy/code"#define PROJ_ID 0x14#define MEM_SIZE 4096key_t CreatKey(){ key_t key = ftok(PATH_NAME,PROJ_ID); if(key < 0 ) { cerr<< "ftok:" << strerror(errno) <<endl; exit(1); } return key;}IpcShmCli.cc
#include "comm.hpp"#include "Log.hpp"int main(){ key_t key = CreatKey(); cout<<"key :" <<key << endl; return 0;}IpcShmSer.cc
#include "comm.hpp"#include "Log.hpp"//充当创建共享内存的角色int main(){ key_t key = CreatKey(); Log() << "key " << key <<endl; //创建一个全新的共享内存 int shmid = shmget(key,MEM_SIZE,IPC_CREAT|IPC_EXCL); if(shmid < 0) { Log()<<"shmget:" << strerror(errno) << endl; return 2; } Log() << "creat shm success ,shmid: " << shmid << endl; return 0;}Log.hpp
#pragma once#include <iostream>#include <ctime>std::ostream &Log(){ std::cout << "For Debug | " << "timestamp: " << (uint64_t)time(nullptr) << " "; return std::cout;}makefile
.PHONY:allall: ipcShmCli ipcShmSeripcShmCli:IpcShmCli.cc g++ -Wall -o $@ $^ -std=c++11ipcShmSer:IpcShmSer.cc g++ -Wall -o $@ $^ -std=c++11 .PHONY:cleanclean: rm -f ipcShmCli ipcShmSer当我们第二次运行ipcShmSer时,此时共享内存已经存在了,因此IPC_EXCL会失败返回,我们查看退出码果然是2,因此system V下的共享内存生命周期是随内核的!而且共享内存如果不显示删除的话,只有等OS重启才能删除。因此接下来,我们要学习一个 共享内存的删除接口
1.2.2 删除共享内存
1.用指令来删除
要删除共享内存,首先我们要查看共享内存是否存在,我们可以在命令行输入
ipcs -m
当我们查看到我们当前系统所有的共享内存后我们在命令行输入下面这条指令就可以删除指定shmid的共享内存
ipcrm -m shmid
此时我们将共享内存删除后,我们再运行ipcShmSer就可以再次创建一个新的共享内存了
2.用系统接口来删除
- shmctl
功能: 用于控制共享内存
参数:
返回值: 成功返回 0 ; 失败返回 -1
话不多说 我们直接把用在我们刚刚所写的ipcShmSer.cc中
#include "comm.hpp"#include "Log.hpp"//充当创建共享内存的角色int main(){ key_t key = CreatKey(); Log() << "key " << key <<endl; Log() << "create share memory begin! \n"; sleep(5); //创建一个全新的共享内存 int shmid = shmget(key,MEM_SIZE,IPC_CREAT|IPC_EXCL); if(shmid < 0) { Log()<<"shmget:" << strerror(errno) << endl; return 2; } Log() << "creat shm success ,shmid: " << shmid << endl; //使用 sleep(5); //删除 shmctl(shmid,IPC_RMID,nullptr); Log()<<"delete shm : " << shmid << "sucess \n"; sleep(5); return 0;}为了能够主观感受到共享内存从创建到删除的全过程,我们写一个监控脚本来每隔1秒查看一下
while :; do ipcs -m; sleep 1; done
1.2.3 使用共享内存完成进程间通信
在上述的准备工作中,我们把共享内存的原理,已经共享内存的创建,删除都已经能够完成了,接下来到了最重要的地方,就是不同进程间要使用共享内存完成进程间通信.此时共享内存在内存中创建完成了,我们需要将物理内存中的共享内存挂接到进程的地址空间内。这里就需要使用另外一个函数
- shmat
功能:将共享内存连接到进程地址空间
参数:
返回值: 成功返回一个指针,指向共享内存的第一个节;失败返回-1
那么我们直接在代码中使用:
//使用 //1.将共享内存挂接到进程地址空间内 char * str = (char*)shmat(shmid,nullptr,0); Log()<< " attach shm : " << shmid << "sucess !\n";那么如果我们挂接之后不想用了我们要去关联,我们可以使用下面这个函数
- shmdt函数
功能:将共享内存段与当前进程脱离
参数:
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
我们直接在代码中体现
//使用 //1.将共享内存挂接到进程地址空间内 char * str = (char*)shmat(shmid,nullptr,0); Log()<< " attach shm : " << shmid << "sucess !\n"; //2.不想用就去关联 shmdt(str); Log()<< " detach shm : " << shmid << "sucess !\n"; sleep(5);现在我们再来写ipcShmCli.cc
#include "comm.hpp"#include "Log.hpp"int main(){ //创建相同的Key值 key_t key = CreatKey(); Log() <<"key :" <<key << endl; //获取 shmid int shmid = shmget(key,MEM_SIZE,IPC_CREAT); if(shmid < 0) { Log()<<"shmget:" << strerror(errno) << endl; return 2; } //获取成功之后挂接 char* str = (char*)shmat(shmid,nullptr,0); //用它 sleep(5); //去关联 shmdt(str); //不用删除共享内存 共享内存的创建和销毁全权由Ser管理 return 0;}当我们同时将Cli和Ser同时挂接到了一个共享内存上时,此时保证了两个进程已经同时看到了同一份资源,接下来我们来写Cli的操作。这里需要注意的是,我们把共享内存是映射到了我们进程地址空间上了(堆栈之间),因此对于每一个进程来说,挂接到自己的上下文中的共享内存是属于自己的空间的。类似于堆空间或者栈空间,因此可以被用户直接使用,不需要调用任何的系统接口!
1.2.4 Cli写字符 Ser获取
当我们建立好共享内存并且同时挂接到两个进程之后,我们可以进行进程间通信了,我们首先可以完成一个较为简单的通信。我们让Cli往共享内存里面写26个英文字符,每个一秒写一个,让Ser每隔一秒读取一次共享内存里面的内容。这样当我们同时启动程序的时候,我们应该看到的现象是Ser每隔一秒在屏幕打印出英文字符(从A开始到Z),每隔一秒多一个字符并换行。我们运行起来看看结果:
//用它 //没有使用任何的系统调用接口 int cnt = 0; while(cnt <= 26) { str[cnt] = 'A' + cnt; ++cnt; str[cnt] = '\0'; sleep(1); }//2.使用 while(true) { printf("%s\n",str); sleep(1); }运行结果:
因此共享内存因为自身的特性,没有任何访问控制!共享内存被进程双方直接看到,可以直接通信,属于用户双方,但是不安全!因此共享内存挂接到两个进程的时候,进程1向共享内存写入内容是,进程2立马就可以看到。因此共享内存是所有进程间通信中速度最快的!
完成上面的功能后,我们知道了在Cli中写入字符,Ser可以立马获取。因此我们只需要稍稍修改Cli的代码就可以传输我们自定义的信息了。我们一起来看看代码的实现。
#include "comm.hpp"#include "Log.hpp"int main(){ //创建相同的Key值 key_t key = CreatKey(); Log() <<"key :" <<key << endl; //获取 shmid int shmid = shmget(key,MEM_SIZE,IPC_CREAT); if(shmid < 0) { Log()<<"shmget:" << strerror(errno) << endl; return 2; } //获取成功之后挂接 char* str = (char*)shmat(shmid,nullptr,0); //自定义输入 while(true) { printf("Please Enter# "); fflush(stdout); ssize_t s = read(0,str,MEM_SIZE); if(s > 0) { str[s] = '\0'; } } //去关联 shmdt(str); //不用删除共享内存 共享内存的创建和销毁全权由Ser管理 return 0;}这种通信方式就是共享内存!
附录:
ipcShmCli.cc
#include "comm.hpp"#include "Log.hpp"int main(){ //创建相同的Key值 key_t key = CreatKey(); Log() <<"key :" <<key << endl; //获取 shmid int shmid = shmget(key,MEM_SIZE,IPC_CREAT); if(shmid < 0) { Log()<<"shmget:" << strerror(errno) << endl; return 2; } //获取成功之后挂接 char* str = (char*)shmat(shmid,nullptr,0); while(true) { printf("Please Enter# "); fflush(stdout); ssize_t s = read(0,str,MEM_SIZE); if(s > 0) { str[s] = '\0'; } } //用它 //没有使用任何的系统调用接口 // int cnt = 0; // while(cnt <= 26) // { // str[cnt] = 'A' + cnt; // ++cnt; // str[cnt] = '\0'; // sleep(1); // } //去关联 shmdt(str); //不用删除共享内存 共享内存的创建和销毁全权由Ser管理 return 0;}ipcShmSer.cc
#include "comm.hpp"#include "Log.hpp"//充当创建共享内存的角色int main(){ key_t key = CreatKey(); Log() << "key " << key <<endl; Log() << "create share memory begin! \n"; //sleep(5); //创建一个全新的共享内存 int shmid = shmget(key,MEM_SIZE,IPC_CREAT|IPC_EXCL | 0666); if(shmid < 0) { Log()<<"shmget:" << strerror(errno) << endl; return 2; } Log() << "creat shm success ,shmid: " << shmid << endl; sleep(2); //使用 //1.将共享内存挂接到进程地址空间内 char * str = (char*)shmat(shmid,nullptr,0); Log()<< " attach shm : " << shmid << "sucess !\n"; //sleep(2); //2.使用 while(true) { //有数据了再读 printf("%s\n",str); //sleep(1); } //2.不想用就去关联 shmdt(str); Log()<< " detach shm : " << shmid << "sucess !\n"; sleep(2); //删除 shmctl(shmid,IPC_RMID,nullptr); Log()<<"delete shm : " << shmid << "sucess \n"; //sleep(5); return 0;}comm.hpp
#pragma once#include <iostream>#include <cstdio>#include <cerrno>#include <cstdlib>#include <cstring>#include <sys/types.h>#include <sys/ipc.h>#include <sys/shm.h>#include <unistd.h>using namespace std;#define PATH_NAME "/home/Lxy/code"#define PROJ_ID 0x14#define MEM_SIZE 4096key_t CreatKey(){ key_t key = ftok(PATH_NAME,PROJ_ID); if(key < 0 ) { cerr<< "ftok:" << strerror(errno) <<endl; exit(1); } return key;}Log.hpp
#pragma once#include <iostream>#include <ctime>std::ostream &Log(){ std::cout << "For Debug | " << "timestamp: " << (uint64_t)time(nullptr) << " "; return std::cout;}makefile
.PHONY:allall: ipcShmCli ipcShmSeripcShmCli:IpcShmCli.cc g++ -Wall -o $@ $^ -std=c++11ipcShmSer:IpcShmSer.cc g++ -Wall -o $@ $^ -std=c++11 .PHONY:cleanclean: rm -f ipcShmCli ipcShmSer(本篇完)