前面我们学习了管进程间通信的一种方式—>管道。 而我们今天将要介绍的共享内存也是进程间通信的方式。首先,我们要清楚,进程间通信的本质是两个进程看到同一份资源!那么共享内存的本质也是如此。只不过共享内存对于两个进程来说看到的是同一份内存资源! 接下来我们就来详细了解了解共享内存吧。
接下来,我们通过图片来看一看共享内存的基本原理是什么:
和管道类似。共享内存对于Linux系统来说也是文件,这可真的是一切皆文件了!我们知道进程间通信的本质是两个进程看到同一份资源。对于管道而言是看到同一份管道文件,而对于共享内存看到的就是同一份内存! 但是这个时候有一个问题:管道是通过打开文件,返回文件fd的方式。这个fd是操作系统内核的数据。但是共享内存能不能采用这种策略呢? 答案当然是可以! 但是有一个问题:进程是具有独立性的!如果使用操作系统分配的数字给共享内存的话,那么就会存在一个问题:这个数据是操作系统内核的,如果每一个进程要用必须要通过系统调用获取,但是这么做显然是太麻烦了。 所以共享内存并没有采取和管道一样的策略。共享内存采用的方式是让用户指定对应的共享内存的编号,既然由用户指定生成,那么想让每一个进程看到也就不是什么麻烦的事情了---->主动权转移到用户手中了!
前面我们铺垫了那么多,就是为了接下来介绍和共享内存相关的一系列接口。接下来我们就来介绍一下和共享内存相关的接口和命令
前面我们讲了,共享内存的编号是由用户指定的。那么这个编号是随心所欲的吗? 理论上来说是的。不过操作系统的设计者也提供了一个相关的接口给我们生成对应的编号。 这个就是我们要讲的ftok接口。我们先来看看手册里面对于这个函数的说明:
这个函数会根据对应的文件名和指定的proj_id生成一个对应的编号。生成编号的算法是通过大量的数学算法的验证,这个底层的算法逻辑我们不必关心。直接使用即可。
#include
#include
//ftok所在的头文件
#include
#include
//定义用于生成共享内存编号的路径
#define PATH "/home/chy/test"
//提供proj_id--->随便给
#define PROJ_ID 0x22
key_t createKey()
{
//使用ftok函数生成
key_t key=ftok(PATH,PROJ_ID);
return key;
}
int main()
{
key_t key=createKey();
log()<<"create key id is "<<key<<" : " <<strerror(errno)<<std::endl;
return 0;
}
可以看到,这个ftok函数确实给我们生成了一个key。而且从这个key的值来看,确实很具有随机性!
有了ftok函数给我们生成的key,接下来我们就可以根据这个key来生成共享内存了。下面正式介绍我们创建共享内存的系统接口:shmget
我们首先先来看手册里面对于shmget函数的介绍:
前面两个参数一个是key,一个是我们需要的内存大小。这些都没有什么特别的地方,而第三个参数才是我们研究的重头戏第三个参数是shmflg,从名字上来看就是一个标志。而这样类似的参数,我们早在文件操作那块就见过了!所以第三个参数的底层也是使用位图实现的! 而系统的设计者也给我们提供了对应的两个参数:
IPC_CREAT:如果共享内存不存在就创建,如果存在就返回已经存在的共享内存的编号
IPC_EXCL::如果共享内存不存在就创建,如果存在就出错返回!
而通常第二个参数都要配合第一个参数进行使用!那么可能有聪明的读者就会问:既然都要配合第一个参数使用,为什么还要多此一举呢? 实际上,这个第二个选项能够保证我们每次创建的共享内存都是最新的!也就是每次返回的内存的编号都是最新的。接下来,我们来使用一下这个接口。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//定义用于生成共享内存编号的路径
#define PATH "/home/chy/test"
//提供proj_id--->随便给
#define PROJ_ID 0x22
#define MEM_SIZE 4096
#include "log.hpp"
/*
* 在生成共享内存之前,需要先生成共享内存的编号
* */
key_t createKey()
{
//使用ftok函数生成
key_t key=ftok(PATH,PROJ_ID);
return key;
}
int main()
{
key_t key=createKey();
int shmid=shmget(key,MEM_SIZE,IPC_CREAT | IPC_EXCL | 0666);
log()<<"shmid is " << shmid << "err_msg: "<<strerror(errno)<<std::endl;
return 0;
}
接下来我们来看一看这个接口的返回值:man手册是这么说明返回值的:
如果成功:返回的是共享内存的编号 奇怪,为什么不直接返回我们先前使用的key呢?原因是因为这个key纯粹是给我们用户使用的!就如同文件名和inode的关系一样,文件名只是为了方便我们用户使用。而操作系统底层识别文件用的是inode 由于共享内存的底层是文件,所以这个返回值从某种层面上来说也可以认为是先前我们学习的文件描述符。
接下来我们介绍有关共享内存操作的命令。主要包括:使用命令查看共享内存的命令,还有使用命令删除共享内存
首先我们先来看第一个命令,查看共享内存的相关命令:
ipcs -m #查看系统中所有的共享内存信息
这里要特别注意:共享内存的生命周期是不随进程的!换句话说,如果不使用系统调用接口或者是命令回收共享内存,共享内存就会一直存在! 也就是真正意义上的"内存泄漏!" 所以,我们在使用共享内存的时候一定要注意内存泄漏的问题。另外,关于共享内存的大小,我们一般推荐使用4kb或者是4kb的整数倍,因为操作系统都是以4kb为基本单位进行分配的!即使你要4097b的空间,操作系统也是给8kb空间 所以为了能够让空间能够最大化利用,所以我们建议都是给4kb或者4kb的整数倍
接下来我们来看一看删除共享内存的相关操作。删除共享内存一般使用以下的命令:
#删除对应的共享内存
ipcrm -m shmid
补充:ipcs其实是一套标准,不仅有共享内存,还有信号量,消息队列!以下是具体的对应的选项参数:
ipcs -m #共享内存
ipcs -s #信号量
ipcs -q #消息队列
感兴趣的读者可以去额外学习信号量和消息队列。我们这里不额外进行说明了,还是继续以共享内存为主继续讲解
接下来,我们创建了共享内存。但是此时我们并不能直接使用共享内存!原因是虽然是你创建了共享内存,但是你没有共享内存的使用权!如果要使用共享内存需要先把你挂接到共享内存上。 然后接下来就和 使用malloc和new向系统申请的内存一样进行使用就可以了。我们首先先来看man手册里面对于shmat的介绍:
我们接下来在看看对于返回值的说明:
第一个参数就是shmget返回的shmid,第二个参数是我们要挂接到共享内存的哪一个地方。对于我们初学者来说,这个参数设置成nullptr即可,让系统自动帮我们挂接到合适的地方就可以了。第三个参数就是读写权限。 我们直接上代码:
#include "log.hpp"
/*
* 在生成共享内存之前,需要先生成共享内存的编号
* */
key_t createKey()
{
//使用ftok函数生成
key_t key=ftok(PATH,PROJ_ID);
return key;
}
int main()
{
key_t key=createKey();
int shmid=shmget(key,MEM_SIZE,IPC_CREAT | IPC_EXCL | 0666);
log()<<"shmid is " << shmid << "err_msg: "<<strerror(errno)<<std::endl;
//挂接共享内存
shmat(shmid,nullptr,0666);
return 0;
}
挂接内存以后,我们就可以正常使用对应的共享内存了,接下来我们就使用共享内存来模拟以以下server和client之间的通信:
#include "log.hpp"
//作为客户端,只要使用共享内存即可
key_t createKey()
{
key_t key=ftok(PATH,PROJ_ID);
return key;
}
int main()
{
key_t key=createKey();
//获取共享内存 ---这时候存在获取即可
int shmid=shmget(key,MEM_SIZE,IPC_CREAT);
//打印日志
log()<<"shmid is :" <<shmid <<"| "<<strerror(errno)<<std::endl;
char* str=(char*)shmat(shmid,nullptr,0);
log()<<"shmat status :" <<strerror(errno)<<std::endl;
//客户端从键盘读取数据发送给服务端
while(true)
{
printf("getmessage from keyboard is :");
fflush(stdout);
ssize_t s=read(0,str,MEM_SIZE);
str[s]='\0';
sleep(1);
}
return 0;
}
//server
#include "log.hpp"
/*
* 在生成共享内存之前,需要先生成共享内存的编号
* */
key_t createKey()
{
//使用ftok函数生成
key_t key=ftok(PATH,PROJ_ID);
return key;
}
int main()
{
key_t key=createKey();
int shmid=shmget(key,MEM_SIZE,IPC_CREAT | IPC_EXCL | 0666);
log()<<"shmid is " << shmid << "err_msg: "<<strerror(errno)<<std::endl;
//挂接共享内存
char* str=(char*)shmat(shmid,nullptr,0);
log()<<"shmat status"<<" | "<< "err_msg: "<<strerror(errno)<<std::endl;
//服务器端从str读取数据并打印到显示器
while(true)
{
std::cout<<"I am server"<<"I am reading for message from client"<<std::endl;
write(0,str,MEM_SIZE);
sleep(1);
}
return 0;
}
我们可以看到:和管道不同,即使客户端不发送消息,服务器端也是不停地在往显示器写入。也就是共享内存这种通信方式是不会自带任何访问控制的!所以共享内存方式的进程通信也是所有进程间通信速度最快的一种通信方式! 而接下来我们所有写入客户端的消息都会立刻被服务器端读取到!
如果想要去除关联的话,使用的接口是shmdt接口,对应的说明如下:
shmdt的参数就是前面shmat返回的共享内存的地址。
而如果想要在共享内存种里面拥有和管道一样的访问控制:我们可以考虑在共享内存中让两个进程使用命名管道进行通信。这样就拥有了访问控制。 感兴趣的读者可以自行尝试添加管道进行访问控制
前面我们讲过,共享内存如果不自己手动删除的话是会有很严重的内存泄露问题的。那么前面我们都是使用命令进行删除。 不仅我们会经常忘记做这件事,而且这件事情使用命令也是十分繁琐。那么Linux系统也给我们提供了一个可以删除共享内存的系统调用---->shmctl。 我们先来看一看手册中是如何对shmctl接口进行介绍的。
shmctl有三个参数:第一个是shmid,第二个参数是我们要执行的命令选项, 第三个我们设置成nullptr即可。 而这个调用的第二个参数非常有讲究,有如下的选项:
而我们要删除共享内存的话只要把cmd参数设置成IPC_RMID就可以了。
#include "log.hpp"
//作为客户端,只要使用共享内存即可
key_t createKey()
{
key_t key=ftok(PATH,PROJ_ID);
return key;
}
int main()
{
key_t key=createKey();
//获取共享内存 ---这时候存在获取即可
int shmid=shmget(key,MEM_SIZE,IPC_CREAT);
//打印日志
log()<<"shmid is :" <<shmid <<"| "<<strerror(errno)<<std::endl;
char* str=(char*)shmat(shmid,nullptr,0);
log()<<"shmat status :" <<strerror(errno)<<std::endl;
//客户端从键盘读取数据发送给服务端,现在设置如果客户端输入quit就要退出了
while(true)
{
printf("getmessage from keyboard is :");
fflush(stdout);
ssize_t s=read(0,str,MEM_SIZE);
//处理多余的'\nn'
str[s-1]='\0';
if(strcasecmp("quit",str)==0)
{
std::cout<<"Client quit!"<<std::endl;
break;
}
sleep(1);
}
//退出以后,客户端去关联即可
shmdt(str);
return 0;
}
#include "log.hpp"
/*
* 在生成共享内存之前,需要先生成共享内存的编号
* */
key_t createKey()
{
//使用ftok函数生成
key_t key=ftok(PATH,PROJ_ID);
return key;
}
int main()
{
key_t key=createKey();
int shmid=shmget(key,MEM_SIZE,IPC_CREAT | IPC_EXCL | 0666);
log()<<"shmid is " << shmid << "err_msg: "<<strerror(errno)<<std::endl;
//挂接共享内存
char* str=(char*)shmat(shmid,nullptr,0);
log()<<"shmat status"<<" | "<< "err_msg: "<<strerror(errno)<<std::endl;
//服务器端从str读取数据并打印到显示器
while(true)
{
std::cout<<"I am server"<<"I am reading for message from client"<<std::endl;
//std::cout<
if(strcasecmp("quit",str)==0)
{
std::cout<<"client quit,i will quit now"<<std::endl;
break;
}
write(0,str,MEM_SIZE);
sleep(1);
}
//服务端退出:去关联+删共享内存
shmdt(str);
int ret=shmctl(shmid,IPC_RMID,nullptr);
log()<<"shdel ! the num is "<<ret<<"| "<<"err_msg: "<<strerror(errno)<<std::endl;
return 0;
}
从运行结果可以看出:确实使用了shmctl接口以后就把对应的共享内存给删除了。这样我们就可以在代码中回收共享内存,做到一劳永逸。 不过注意:如果共享内存还有和对应的进程关联的时候被删除了,并不会马上被删除!而是对应关联着的进程看见的key值变成了0,不过建议要删除共享内存以前还是要把相关联的进程先取消关联才会更好一点。
前面我们知道,ipc是一个设计的系列。既然能够谈上是一个系列,那么必然代表设计的结构类似。接下来我们来看一看内核中的ipc系列的结构:
//shmid的结构
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 */
};
//semid结构
struct semid_ds {
struct ipc_perm sem_perm; /* permissions .. see ipc.h */
__kernel_time_t sem_otime; /* last semop time */
__kernel_time_t sem_ctime; /* last change time */
struct sem *sem_base; /* ptr to first semaphore in array */
struct sem_queue *sem_pending; /* pending operations to be processed */
struct sem_queue **sem_pending_last; /* last pending operation */
struct sem_undo *undo; /* undo requests on this array */
unsigned short sem_nsems; /* no. of semaphores in array */
};
//msqid
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg *msg_first; /* first message on queue,unused */
struct msg *msg_last; /* last message in queue,unused */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* last change time */
unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
unsigned long msg_lqbytes; /* ditto */
unsigned short msg_cbytes; /* current number of bytes on queue */
unsigned short msg_qnum; /* number of messages in queue */
unsigned short msg_qbytes; /* max number of bytes on queue */
__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
};
//ipc_perm
struct ipc_perm
{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
我们看到对应的ipc系列的三个数据结构的第一个字段都是struct ipc_perm,实际上,这里这么设计就是有意而为之,整个内核中的维护关系大致如下图:
内核里面也是通过数组来维护ipc资源。但是内核维护的是struct ipc_perm类型的指针数组。而每一个系列的ipc系列的资源的第一个字段都是struct ipc_perm。又因为结构体第一个字段的地址就是整个结构体的地址,所以只要拿到了第一个字段的地址,那么就相当于拿到了整个结构体的地址。使用的时候就根据具体类型强制类型转化就可以访问其他字段了。 而对于内核来说,内核看到的永远都是struct ipc_perm类型的地址。这个就是面向对象编程中的切片技术。只不过这里使用的是C语言来实现,而面向对象的编程语言天然支持这种切片转换而已。 不得不说,Linux内核的机制设计的是真的非常优秀!
1.共享内存是进程间通信的一种方式
2.共享内存需要程序员显式使用接口或者命令回收,否则会造成内存泄露问题
3.共享内存没有任何访问机制控制,所以在所有进程间通信中共享内存最快。
4.共享内存创建了以后不能马上使用,必须使用shmat接口挂接。
5.删除共享内存之前需要使用shmdt取消所有的关联。
以上就是本文的主要内容,如有不足之处还望指出。希望大家一起共同进步。