共享内存的概念
共享内存字面理解就是进程间共同享有的存储空间,不同于管道通信,共享内存就像是进程自己的空间一样,不像管道文件还得使用文件描述符去访问文件,通过文件交流信息。共享内存则是实时信息交流,几乎不存在信息的中间转换。那么共享内存在哪里呢?就真的是在每个进程中都保留一份么?也不是,开辟的共享内存在整个内存空间中独一份,但是可以通过页表映射到不同的进程中去,让各个进程都能够看到这份资源,实现通信。下面是结构图式的理解:
这里要注意区别于父子进程的写时拷贝现象,与其相反,共享内存内的数据在被一个进程修改时,其余进程所看到的资源都会是被修改过的。只有这样,进程间的通信才成为可能。
🙋♂️:为什么父子进程不像共享内存那样处理呢,舍弃写时拷贝,这样父子进程间不就可以直接通过一些变量进行通信了嘛?
👨🏫:理论上在设计时可以这么处理,但是有些场景下我们可能并不需要父子进程间的变量强相关,这会增加使用某些变量的风险。假如一个变量在父进程中作为计数器,在子进程中作为判断条件,那么就会造成严重的逻辑BUG,处理不好整个程序直接玩完。因此为了降低风险,就有了写时拷贝并设计了共享内存这样的结构,只有在需要的时候,用户自己去使用共享内存,就会安全不少。
共享内存使用须知
根据共享内存的概念特性,我们不难发现,共享内存的使用就像是堆上申请的空间一样,可以直接进行访问和修改,那么也就意味着空间的使用不受控制,用户想怎么来就怎么来,进程间容易出现空间操作混乱的情况,因此使用时需要控制一下空间的使用时序。
除了使用要受到控制这个特点之外,貌似也没有什么需要特别注意的地方,整个流程如下:
创建共享内存
创建共享内存需要使用shmget函数,在内存中开辟空间。这里的参数有点含金量,下面阐述一下各个参数的含义。
key:共享内存的唯一标识。共享内存有存在多个的可能情况,因此得各自区分开来,为创建的与创建好的也得区分开,因此唯一标识符的存在就很好理解了。这个唯一标识符并不是系统自动生成的,而是用户自己提供的,为了方便多个进程想准确快速的使用同一个共享内存,又给我们提供了另一个函数ftok函数:
根据已经存在的文件名pathname(目录也是文件!),再加上一个非零的id值(随意,看用户自己创建不同共享内存的区别规则),系统会根据这两个参数生成一个唯一确定的key值。只要pathname和proj_id相同,生成的key值就会相同。这也就保证了不同进程可以根据同一个路径名和id值,访问到同一个共享内存。
size:共享内存的大小,单位是字节数。这也是共享内存的特点之一,内存空间是按字节数来创建与访问的。
shmflg:共享内存开辟时的属性设置,共有三个参数可以传:
IPC_CREAT:创建一块共享内存,如果已经存在了,就获取它,如果不存在,就黄建一块新的共享内存,并获取它。
IPC_EXCL:与IPC_CREAT搭配使用,根据key值,如果该共享内存存在了,就会出错。不存在的话正常创建并获取它,这也就保证了如果创建内存成功,该共享内存一定是一个全新的。
mode_flags:开辟的空间的权限,与open函数中的mode参数是一个意思,对应 所有者、所属组、其它 三个组的权限。例如:0x666就是所有使用者都能进行读和写的操作。
返回值:返回一个整型数字,类似于文件描述符的东西,是提供给我们用户使用的共享内存的标识,注意区别key这个系统调用接口的标识符,虽然是一个意思,但数值不同,面向的对象也不同。
共享内存的映射与链接
一个进程无论创建不创建共享内存,要想使用共享内存,就必须得将要使用的共享内存映射到自己的共享区上,保证在使用的时候可以找到该共享内存的首地址。
映射与链接的话需要使用shmat函数。
参数:
shmid:就是shmget的返回值,也就是用户级的共享内存标识符,这个参数意义就是链接对应的共享内存。
shmaddr:指针参数,如果shmaddr为NULL,系统将选择一个合适的(未使用的)地址来附加段。一般我们在不涉及到特殊情况下默认传空指针就行。
shmflg:默认传0(还没涉及到复杂的场景,这里就先这样用着)。
返回值:共享内存的首地址,类型是void* ,用户可以自己转换成需要的类型。如果出错的话会返回void* ( -1)。
拿到返回值就意味着可以正常使用该共享内存了。
共享内存的映射取消
如果某个进程不想使用某个共享内存了,就可以将其在共享区的映射给删掉。
参数:
shmaddr:shmat的返回值。
返回值:成功时返回0;错误时返回-1,并设置errno以指示错误的原因。
共享内存的删除
共享内存删除意味着所有的进程都不能使用该内存了,注意区分于映射的取消。
参数:
shmid:共享内存的用户级标识符
cmd:一般传IPC_RMID,当最后一个进程取消映射后,直接删除掉共享内存。
buf:默认我们传空指针。
共享内存实现进程通信
这里我们使用客户端于服务端之间的通信来进行测试。
准备工作:两个可执行程序(客户端与服务端)、两个程序共同看到的头文件(方便看见同一个共享内存)。
注意事项:通信的时候需要进行访问控制,这里我使用的是管道的阻塞式读来控制。
头文件Common.hpp:
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
#define PATH_NAME "/home/jia/learning_code-college-age"//创建共享内存所使用的文件名
#define PROJ_ID 0x46 //创建共享内存的id值
#define SHM_SIZE 4096 //共享内存的大小
#define FIFO_FILE ".fifo"//管道文件的名字
#define READER O_RDONLY//管道文件的打开方式
#define WRITER O_WRONLY//管道文件的打开方式
key_t CreateKey()//创建key值
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 0)
{
cerr << "ftok: " << strerror(errno) << endl;
exit(1);
}
return key;
}
void CreateFifo(const string fifofile)//创建管道文件
{
umask(0);
if(mkfifo(fifofile.c_str(),0666)<0)
{
cerr<<"mkfifo: "<<strerror(errno)<<endl;
exit(2);
}
}
int Open(const string fifofile, int mode)//以某种方式打开管道文件
{
return open(fifofile.c_str(),mode);
}
int Wait(int fd)//阻塞式等待
{
uint32_t values=0;
return read(fd,&values,sizeof(values));
}
void Signal(int fd)//向文件中写入信息,使得阻塞式等待解除
{
uint32_t values=1;
write(fd,&values,sizeof(values));
}
int Close(int fd,const string fifofile)//关闭管道,并删除管道文件
{
close(fd);
unlink(fifofile.c_str());
}
服务端IpcShmSer.cc:
#include "Common.hpp"
int main()
{
cout<<"Ser Begin"<<endl;
CreateFifo(FIFO_FILE);//创建管道文件
key_t key = CreateKey();
cout << "key: " << key << endl;
int shmid=shmget(key,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);//创建共享内存
if(shmid<0)
{
cerr<<"shmget: "<<strerror(errno)<<endl;
return 2;
}
cout<<"shmget success, shmid: "<<shmid<<endl;
int fd=Open(FIFO_FILE,READER);//服务端以读的方式打开管道文件
cout <<"Open Success,fd: "<<fd<<endl;
char* str=(char*)shmat(shmid,nullptr,0);//建立映射关系,并获取共享内存首地址
while(true)
{
if(Wait(fd)<=0) break;//阻塞式等待,只要客户端没有向管道写入内容,就一直卡在此处,不进行输出操作。
printf("%s",str);//使用共享内存,这里是直接输出
}
shmdt(str);//取消映射
shmctl(shmid,IPC_RMID,nullptr);//服务端退出的话要删除共享内存
Close(fd,FIFO_FILE);//关闭管道文件并删除管道文件
return 0;
}
客户端IpcShmCli.cc:
#include "Common.hpp"
int main()
{
cout << "Cli Begin" << endl;
key_t key = CreateKey();
cout << "key: " << key << endl;
int shmid = shmget(key, SHM_SIZE, IPC_CREAT);//获取客户端已经创建好的共享内存
if (shmid < 0)
{
cerr << "shmget: " << strerror(errno) << endl;
return 2;
}
cout << "shmget success, shmid: " << shmid << endl;
int fd = Open(FIFO_FILE, WRITER);//以写的方式打开管道文件
cout << "Open Success,fd: " << fd << endl;
char *str = (char *)shmat(shmid, nullptr, 0);//与共享内存建立映射
while (true)
{
printf("Please Enter# ");
fflush(stdout);
ssize_t s = read(0, str, SHM_SIZE);//从标准输入流中拿取数据放到str中,也就是向共享内存写入数据
if (s > 0 && s < SHM_SIZE)
{
str[s] = '\0';
}
Signal(fd);//给服务端发信号,表示可以使用共享内存了
}
shmdt(str);//结束的话取消映射,客户端并不负责共享内存与管道文件的处理。
return 0;
}
运行结果:
总结
共享内存的使用其实并不是重点,重点在于如何理解共享内存的概念与特性。