在上篇博文我们了解了通过管道完成进程间通信,我们了解匿名管道和命名管道,并且通过编码模拟实现使用了匿名管道和命名管道。我们知道要让进程间完成通信必须让这两个进程首先看到同一份资源,因此给予这个前提,本篇博文我们了解另外一种可以进程间通信的方式 -- 共享内存。

1.system V共享内存

system V是一套标准,这是系统级别的接口。我们知道进程间通信的前提是:先让不同的进程看到同一份资源!

1.1共享内存原理的理解

首先我们来了解一下共享内存的原理:

我们在之前知道了一个进程由自己的tast_struct和进程的地址空间,那么其中对我们来讲tast_struct有指向自己的地址空间。我们知道进程地址空间内有栈区,堆区,数据区等等.....我们在动静态库中也知道我们可以把磁盘中的库加在到内存中通过页表映射到我们进程的进程地址空间中的共享区。其中共享库就在这里。而进程间通信一定至少有两个进程(如下图),那么这两个进程是毫无关系的,每一个进程都有自己的页表,也可以通过页表映射到自己的地址空间。假设今天有一个特性的接口,首先能够在物理内存中创建一份空间,然后各进程调用这个接口,将这份空间通过页表映射到自己进程的地址空间上。那么进程就可以通过自己的地址空间看到物理内存中的这份空间。因此各进程都可以看到物理内存中的同一份资源。此时对我们来讲这种机制就是共享内存。

[ Linux ] 进程间通信之共享内存_#include

共享内存示意图

[ Linux ] 进程间通信之共享内存_#include_02

1.2 共享内存编码

1.2.1 共享内存函数

  • shmget函数

功能:shmget是用来创建共享内存的

参数:

  1. key : 这个共享内存段的名字
  2. size : 共享内存的大小 建议设置成页(4KB)的整数倍
  3. shmflg :由9个权限标志构成,他们的用法和创建文件时使用的mode模式表示是一样的

返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

[ Linux ] 进程间通信之共享内存_#include_03

[ Linux ] 进程间通信之共享内存_#include_04

[ Linux ] 进程间通信之共享内存_共享内存_05

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来生成。

[ Linux ] 进程间通信之共享内存_进程间通信_06

[ Linux ] 进程间通信之共享内存_进程间通信_07

我们使用一下来看看效果:

[ Linux ] 进程间通信之共享内存_#include_08

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 4096



key_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:all
all: ipcShmCli ipcShmSer

ipcShmCli:IpcShmCli.cc
g++ -Wall -o $@ $^ -std=c++11

ipcShmSer:IpcShmSer.cc
g++ -Wall -o $@ $^ -std=c++11

.PHONY:clean
clean:
rm -f ipcShmCli ipcShmSer


当我们第二次运行ipcShmSer时,此时共享内存已经存在了,因此IPC_EXCL会失败返回,我们查看退出码果然是2,因此system V下的共享内存生命周期是随内核的!而且共享内存如果不显示删除的话,只有等OS重启才能删除。因此接下来,我们要学习一个 共享内存的删除接口

[ Linux ] 进程间通信之共享内存_共享内存_09

[ Linux ] 进程间通信之共享内存_#include_10

[ Linux ] 进程间通信之共享内存_#include_11

1.2.2 删除共享内存

1.用指令来删除

要删除共享内存,首先我们要查看共享内存是否存在,我们可以在命令行输入

ipcs -m

[ Linux ] 进程间通信之共享内存_进程间通信_12

当我们查看到我们当前系统所有的共享内存后我们在命令行输入下面这条指令就可以删除指定shmid的共享内存

ipcrm -m shmid

[ Linux ] 进程间通信之共享内存_进程间通信_13

此时我们将共享内存删除后,我们再运行ipcShmSer就可以再次创建一个新的共享内存了

[ Linux ] 进程间通信之共享内存_共享内存_14


2.用系统接口来删除

  • shmctl

功能: 用于控制共享内存

参数:

  1. shmid : 由shmget返回的共享内存标识码
  2. cmd:将要采取的动作(有三个可取值) 其中IPC_RMID 为删除共享内存

[ Linux ] 进程间通信之共享内存_共享内存_15

  1. buf : 指向一个保存着共享内存的模式状态和访问权限的数据结构

返回值: 成功返回 0 ; 失败返回 -1

[ Linux ] 进程间通信之共享内存_进程间通信_16

[ Linux ] 进程间通信之共享内存_#include_17

话不多说 我们直接把用在我们刚刚所写的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

[ Linux ] 进程间通信之共享内存_进程间通信_18

[ Linux ] 进程间通信之共享内存_共享内存_19


1.2.3 使用共享内存完成进程间通信

在上述的准备工作中,我们把共享内存的原理,已经共享内存的创建,删除都已经能够完成了,接下来到了最重要的地方,就是不同进程间要使用共享内存完成进程间通信.此时共享内存在内存中创建完成了,我们需要将物理内存中的共享内存挂接到进程的地址空间内。这里就需要使用另外一个函数

  • shmat

功能:将共享内存连接到进程地址空间

参数:

  1. shmid : 共享内存标识符
  2. shmaddr :指定连接的地址
  3. shmflg : 它的两个可能取值是SHM_RND 和 SHM_RDONLY

返回值: 成功返回一个指针,指向共享内存的第一个节;失败返回-1

[ Linux ] 进程间通信之共享内存_#include_20

[ Linux ] 进程间通信之共享内存_#include_21

那么我们直接在代码中使用:

//使用
//1.将共享内存挂接到进程地址空间内
char * str = (char*)shmat(shmid,nullptr,0);
Log()<< " attach shm : " << shmid << "sucess !\n";

那么如果我们挂接之后不想用了我们要去关联,我们可以使用下面这个函数

  • shmdt函数

功能:将共享内存段与当前进程脱离

参数:

  1. shmaddr: 由shmat所返回的指针

返回值:成功返回0;失败返回-1

注意:将共享内存段与当前进程脱离不等于删除共享内存段

[ Linux ] 进程间通信之共享内存_共享内存_22

我们直接在代码中体现

//使用
//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);

[ Linux ] 进程间通信之共享内存_#include_23

[ Linux ] 进程间通信之共享内存_#include_24


现在我们再来写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;
}

[ Linux ] 进程间通信之共享内存_进程间通信_25

当我们同时将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);
}

[ Linux ] 进程间通信之共享内存_共享内存_26

//2.使用
while(true)
{
printf("%s\n",str);
sleep(1);
}

[ Linux ] 进程间通信之共享内存_#include_27

运行结果:

[ Linux ] 进程间通信之共享内存_进程间通信_28

因此共享内存因为自身的特性,没有任何访问控制!共享内存被进程双方直接看到,可以直接通信,属于用户双方,但是不安全!因此共享内存挂接到两个进程的时候,进程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;
}

[ Linux ] 进程间通信之共享内存_进程间通信_29

这种通信方式就是共享内存!



附录:

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 4096



key_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:all
all: ipcShmCli ipcShmSer

ipcShmCli:IpcShmCli.cc
g++ -Wall -o $@ $^ -std=c++11

ipcShmSer:IpcShmSer.cc
g++ -Wall -o $@ $^ -std=c++11

.PHONY:clean
clean:
rm -f ipcShmCli ipcShmSer


(本篇完)