之前已经讲了通过管道来进行进程间通信,匿名管道是通过子进程继承父进程的文件描述符表来使两个进程看到同一份匿名管道文件实现的,有名管道是通过文件名作为唯一标识来使两个毫不相干的进程看到同一份资源。管道通信是基于文件系统的通信方式。而System V是操作系统提供的聚焦于本地通信的通信方式,本文介绍System V主要是介绍共享内存这种通信方式。
共享内存
什么是共享内存(物理内存块+属性)
共享内存是操作系统开辟的一块内存块,开辟成功后会将内存块的地址通过页表映射到进程的进程地址空间中去,只要将这个内存块通过页表映射到两个不同的进程地址空间,就可以让两个进程实现通信(因为它们有一个相同的内存块)。
1.如果是申请内存块的话,malloc/new也可以做到,但是malloc和new申请的内存块只能让本进程看见,因为进程间具有独立性。
2.共享内存是操作系统设置的一种进程间通信的方式,所以操作系统内可能同时存在多组进程使用共享内存进行通信,也就是说操作系统要对共享内存做管理。
3.使用malloc时我们需要传大小,但是free时却不用传任何参数,这是因为malloc开辟的空间比我们预计要申请的空间要大,多出来的这一部分存放的是malloc出来的那块空间的属性。同理共享内存不只是一个物理的内存块,还有它的属性。
4.将共享内存通过页表和进程建立联系也叫挂接,使用完毕以后将联系销毁(不是删除共享内存只是将页表映射关系取消)又叫去关联
共享内存的接口认识
查看共享内存
ipcs -m/-q/-s //共享内存/消息队列/信号量
删除共享内存
ipcrm -m +shmid
删除共享内存使用shmid而不是key,因为key是共享内存的内核级的标识,而我们使用指令是在用户层,所以要用用户层标识,也就是shmid
也可以通过函数来删除共享内存
#include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf); //cm参数使用IPC_RMID就是删除,这个函数有很多用途,但主要用于删除
这个函数在后面的代码部分有使用
共享内存的创建(ftok和shmget)
要确保两个进程使用的是同一个共享内存块,那么必须要有能唯一标识一个共享内存块的方法,共享内存的唯一标识就是key,所以在创建共享内存块之前需要获取key。
#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id);
要让两个进程看到同一个共享内存块,就要让两个进程拿到同一个key,只要两个进程对ftok传同样的参数就可以获得同样的key值
有了key值以后就可以申请共享内存块了
#include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg); //最后一个参数是标志位,可以传0(相当于IPC_CREAT) //IPC_CREAT:如果不存在就创建,存在就获取 //IPC_CREAT|IPC_EXCL:不存在就创建,存在就获取(IPC_EXCL不能单独使用)
挂接和去关联(shmat和shmdt)
#include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg); //挂接就是建立这个内存块和进程之间的联系,此后该进程就可以使用这个内存块了,其中第二个参数表示可以将这个内存块映射到指定的地址,但是也可以传空,第三个参数可以传0,表示默认行为(默认就可以读写) int shmdt(void *shmaddr); //参数直接传空就行
这里有个问题,就是最开始的时候,我们使用key来创建共享内存,但是后面的函数接口使用的都是shmid。
key和shmid都是共享内存的唯一标识,它们的关系就像fd和inode的关系,key是共享内存在内核中的标识,shmid是用户层的标识。搞两套标识符主要是为了让用户层和内核层解耦,使得内核层的改变不会影响用户层。
利用共享内存通信(简单的代码演示)
两个进程,一个server负责创建共享内存块和删除(所以该进程要先运行),并从共享内存中读取数据,一个client进程用于获取共享内存并向其中写入数据。公共的代码放到一个公用的头文件中:
comm.hpp
#include <iostream> #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <errno.h> #include <cstring> #include<cstdio> using namespace std; #define PATHNAME "." #define PROJ_ID 0x66 #define MAX_SIZE 4096 // 内存是分块按页管理的,所以建议大小是4096的整数倍 // 创建共享内存:1.获取唯一标识:ftok 2.根据唯一标识申请空间 key_t getkey() // 本来要传两个参数,但是这里采用宏定义,就不用传参数了 { key_t key = ftok(PATHNAME, PROJ_ID); if (key < 0) { cerr << errno << ":" << strerror(errno) << endl; exit(-1); } return key; } //拿到唯一标识以后,可以通过这个唯一标识创建或者获取共享内存 int getShmHelper(key_t key,int flags) { int shmid=shmget(key,MAX_SIZE,flags); if(shmid<0) { cerr << "getShmHelper"<<errno << ":" << strerror(errno) << endl; exit(-2); } } int getShm(key_t key) { return getShmHelper(key,IPC_CREAT); } int createShm(key_t key) { return getShmHelper(key,IPC_CREAT|IPC_EXCL|0600);//创建的时候要将共享内存的权限设置好 } //创建完毕,可以将进程与共享内存开始挂接 void*attachShm(int shmid) { void*start = shmat(shmid,nullptr,0); if((long long)start==-1L)//linux是64位系统,所以指针大小是八字节,强转为long long,-1L,代表该数的类型是long long { cerr <<"attachShm"<< errno << ":" << strerror(errno) << endl; exit(-3); } } //使用完毕以后,要去关联和删除 void*detShm(void*start) { if(shmdt(start)==-1) { cerr<<"detShm" << errno << ":" << strerror(errno) << endl; exit(-4); } } int delShm(int shmid) { if(shmctl(shmid,IPC_RMID,nullptr)==-1) { cerr<<"delShm" << errno << ":" << strerror(errno) << endl; exit(-5); } }
server.cc
#include"comm.hpp" int main() { //服务端建立共享内存,给客户端使用,所有有关共享内存的维护工作都由服务端来进行 key_t key=getkey(); //cout<<key<<endl; //int flags=IPC_CREAT|IPC_EXCL; int shmid=createShm(key); printf("创建成功,尝试挂接\n"); //共享内存本身就是一块空间,不用再开辟缓冲区了 char*start=(char*)attachShm(shmid); printf("挂接成功,开始读取\n"); while(true) { printf("client say:%s\n",start); sleep(1); } detShm(start); delShm(shmid); return 0; }
client.cc
#include"comm.hpp" int main() { //获取共享内存,挂接共享内存,向共享内存中写入数据 key_t key=getkey(); int shmid=getShm(key); printf("获取成功,尝试挂接\n"); //直接将共享内存作为缓冲区 char *start_client=(char*)attachShm(shmid); printf("挂接成功,开始输入\n"); const char*str="hello server,I am cilent,I want to say:你最近过的还好吗"; int cnt=1; while(true) { //向共享内存中写入数据 snprintf(start_client,MAX_SIZE,"%s[pid:%d]:消息编号:%d",str,getpid(),cnt++); sleep(1); if(cnt==10) break; } //使用完去关联 detShm(start_client); return 0; }
总结共享内存
共享内存的优缺点
优点:
共享内存是最快的通信方式,因为拷贝次数相比其他的通信方式要更少
缺点
共享内存作为最快的通信方式,但是使用的却很少,这主要是因为:
1.它的下标与文件系统完全不兼容,而Linux下一切皆文件
2.它没有同步和互斥,没有对数据进行保护
共享内存的特点
1.共享内存的声明周期不随进程,而是随操作系统(这也是所有IPC资源的特点包括消息队列和信号量),也就是说某个进程建立了一个共享内存,即使这个进程结束了,这个共享内存也不会被回收,除非操作系统关闭或重启。
2.共享内存的大小建议是4KB的整数倍,因为操作系统管理内存是分块管理的,而一个内存块的大小就是4KB,如果你申请4097字节的大小,虽然你只能使用4097个字节的空间,但其实操作系统给你划分了2个4096字节,而多出来的空间就被平白浪费了。
消息队列(了解)
消息队列是操作系统提供的一个内核级的队列,它两端都可以读写,为了保证不读到自己写的数据,因此它的每一个成员都可以被认为是一个结构体,该结构体中包含两个成员:数据类型和数据块缓冲区;比如一端的类型设为1,另一端的类型设为0,那么在读取的时候一端只会读取类型为0的数据,另一端只会读取类型为1的数据。
接口介绍
1、获取消息队列(msgget)
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgget(key_t key, int msgflg); //这里的key也需要使用ftok函数生成,这个msgflg也与共享内存的参数一样 //如果成功,返回值将是消息队列标识符(非负整数) ,否则为 -1。用 errno 指示错误。
2.删除消息队列
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgctl(int msqid, int cmd, struct msqid_ds *buf); //msqid(与共享内存的shmid一样,是用户层对消息队列的唯一标识),其他的与共享内存一样
信号量(了解)
预备概念
信号量本质是一个计数器,用来表示公共资源中,资源多少的问题,信号量主要是用与同步和互斥和原子性的(这些后面会讲)。
这里有两点需要注意:
1.公共资源就是可以被多个进程访问的资源,但是如果这个公共资源是没有被保护的,那么就会导致数据不一致的问题(很可能一个进程还没写完,另一个进程已经开始读了),我们将被保护的公共资源称之为临界资源,访问临界资源的代码部分称为临界区。
2.既然公共资源是可以被多个进程访问,所以信号量不可能是一个全局变量,因为全局变量只是在一个进程内有效
关于同步和互斥,这些后面都会讲,这里先将互斥的概念放出来:所谓的互斥就是指当两个进程想访问同一个公共资源时,不能同时一起访问,必须要等一个进程访问完了以后另一个进程才能访问。
那么什么是原子性:要么不做,要么做完。比如说你转账,要么不转,要转就一定要转成功,也就是说要么我转账成功然后扣款,要么我转账失败,但是我的钱不能少。
理解信号量
今天我去看电影,我买了一张票,于是我在该电影院的这场电影的观影厅中有了一个座位,即使我看电影迟到了,也不会发生我的座位被别人坐了这种事,因为这个座位的电影票被我买了以后,别人是不能再买到的。假设这个观影厅中有一百个座位,我买了一张以后就只剩下99个座位可售了,电影院无法卖出101张票,因为电影院没有站票。
信号量其实是一种资源预定机制,一旦某个进程预定了某个公共资源以后,信号量就要--
,表示当前可用的公共资源减少一个(也叫P操作),一旦信号量减到0就表示当前的公共资源已消耗殆尽了,同理,如果信号量++
就表示有进程释放资源了(也叫V操作),可用的公共资源又增多一个。此外我买到票了,电影院就一定让我进,我没买到票,电影院一定不会让我进,所以信号量也是一种保护机制。
此外公共资源还可以大致分为两类:
1.必须作为整体使用的公共资源:如管道
2.可以划分成一个个的子资源来使用(比如我买了一包湿巾,我不可能一次用一整包,而是一片一片的使用)
既然申请公共资源的时候要通过信号量,也就是说必须要保证信号量的正确性,信号量的正确性保障关键在于,对于信号量的操作无论是--
还是++
都是原子性的。
IPC资源的管理方式
与一切皆文件那块类似,还是使用了一个结构体指针数组,通过指针来指向不同的实现方式,这也是C语言模拟实现多态的方法(更准确的来说C++的多态就是因此而生)。