目录
一.SystemV版本的进程间通信
我们之前学习的管道通信的本质是基于文件的那么也就是说OS没有做太多的设计工作,而SystemV
IPC是操作系统特定设置的一种通信的方式。但是他们的本质是一样的都是让不同的进程看到同一份资源。共享内存,顾名思义就是允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
注意:共享内存没有提供同步机制那么也就是说当一个进程在对共享内存进行写操作时并不会阻止另外一个进程对共享内存进行读取。所以一般需要采用其他机制来同步对共享内存的访问比如说信号量。
systemV IPC提供的通信方式有以下三种:
1.system V 共享内存
2.system V 消息队列
3.system V 信号量
其中共享内存和消息队列主要是以传送数据为目的,而信号量是为了保证进程间的同步互斥。看起来好像信号量和通信好像没有直接关系但是它也属于通信范畴
二.共享内存的通信原理
共享内存通信的原理就是让不同的进程看到同一份资源。OS在物理内存中开辟一块空间,我们知道在Linux下每个进程都有自己的进程控制块(PCB),虚拟地址空间并且都有一个与之对应的页表。而页表负责将虚拟地址转换为物理地址。通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。这样两个进程就看到同一块物理内存而这块物理内存我们称为共享内存。
三.为什么共享内存的速度最快
从上面这张图中我们可以看出来:
Proc A 进程给内存中写数据, Proc B 进程从内存中读取数据,在此期间一共发生了两次复制
(1)Proc A 到共享内存 (2)共享内存到 Proc B
因为直接在内存上操作,所以共享内存的速度也就提高了。
四.共享内存的创建删除及其指令
1.共享内存的创建
创建共享内存我们使用的函数是shmget函数,shmget函数的原型如下:
int shmget (key_t key,size_t size,int shmflg);
shmget参数说明:
1.第一个参数key用来在系统中唯一标识一块共享内存和进程PID类似,都是会设置到管理共享内存的数据结构中。
2.第二个参数size表示共享内存的大小,一般建议是4KB的整数倍。Linux 会以页为单位管理内存,无论是将磁盘中的数据加载到内存中,还是将内存中的数据写回磁盘,操作系统都会以页面为单位进行操作,哪怕我们只向磁盘中写入一个字节的数据,我们也需要将整个页面中的全部数据刷入磁盘中。绝大多数处理器上的内存页的默认大小都是 4KB或者是4KB的整数倍所以建议是4KB的整数倍
3.第三个参数为标记位,这个和我们之前学习基础IO时的标志位非常的类似它就是一个宏并且在32个比特位中只有一位为1,并且各不相同。
4.返回值:如果调用成功会返回一个标识符用来唯一标识一个有效的共享内存(这个标识符是在用户层),失败返回-1.
可能有老铁就会说了shmget的第一个参数不就是用来标识唯一块共享内存吗?注意这个key是在系统中唯一标识不是在用户层,我们对应共享内存进行操作都是在用户层对齐进行操作所以我们要使用用户层的ID。那在系统中唯一标识一块共享内存的id如何生成了下面我们来看一个函数ftok
ftok函数原型如下:
key_t ftok(const char*pathname,int proj_id);
ftok函数是将一个以存在的路径名(该路径一定要存在)和一个整数转换为一个key值(这个key是独一无二它不用和其他共享内存的key相同)。这个key是会被设置进维护共享内存的数据结构当中。
注意:
1.我们调用ftok函数可能会失败,生成的key冲突了此时我们需要更改路径名或者是proj_id。
2.使用共享内存进行通信的进程在使用ftok函数获取key时必须保证填入的参数一样,这样才能保证生成的key值是一样的。进而才能找到同一个共享内存。
shmget函数的第三个参数shmflg常用的组合方式主要有以下两种。
组合方式 | 作用 |
IPC_CREAT或者直接写0 | 如果内核当中没有于key值对应的共享内存则创建一个共享内存并返回标识该共享内存的id(用户层id),如果已经有了那么直接返回标识该共享内存的id即可 |
IPC_CREAT | IPC_EXCL | 如果内核当中没有于key值对应的共享内存则创建一个共享内存并返回标识该共享内存的id(用户层id),如果已经有了则出错返回也就是返回-1.也就是说用于这个组合得到的共享内存一定是全新的。 |
下面我们来使用ftok和shmget函数来创建共享内存并将id打印出来。在这里为了方便将一些头文件放到一个comm.h当中
comm.h:
#pragma once #define PATH_NAME "./" #define ID 0x22222222 #define SIZE 4096 #include<iostream> #include<sys/ipc.h> #include<sys/shm.h> #include<unistd.h> using namespace std;
test.cpp
#include"comm.h" #include<cstdio> int main() { //获取一个关键字用来唯一标识系统中的共享内存 key_t key=ftok(PATH_NAME,ID); //这个key值会设置进管理共享内存的数据结构中 if(key<0){ cerr<<"ftok fail"<<endl; return 1; } int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666); //IPC_EXCL设置之后保证创建出来的共享内存一定是全新的 if(shmid<0){ cerr<<"shmget fail"<<endl; return 2; } printf("%x\n",key); printf("%d\n",shmid); return 0; }
运行结果:
我们发现在用户层和系统层用来标识共享内存的id是不一样的。
在linux我们可以使用ipcs指令查看有关进程间通信的信息:
如果我们单独使用ipcs那么会列出消息队列,共享内存,信号量的相关信息。如果我们只想查看其他的一个信号我们可以使用选项:
ipcs - q//列出消息队列的相关信息 ipcs - m//列出共享内存的相关信息 ipcs - s//列出信号量的相关信息
例如使用ipcs -m 查看共享内存的相关信息:
此时我们发现查看的结果有很多的信息,下面我们一起来看看这些信息是什么:
key:在系统中唯一标识一块共享内存即系统层id
shmid:共享内存在用户层的id.
owner:共享内存的拥有者
perms:共享内存的权限
bytes:共享内存的大小
nattch:有多少个进程和这块共享内存关联
status:共享内存的状态。
2.共享内存的释放:
通过上面的实验我们发现当我们的进程结束之后共享内存依然存在,并没有被OS释放。通过之前对管道的学习我们知道管道的生命周期是随进程的,而共享内存的生命周期是随内核的。也就是说进程退出了但是它创建的共享内存不会随着进程的退出而被释放掉,如果我们不手动的删除共享内存那么它会一直存在。删除共享内存有两种方式一种是通过命令一种是通过函数调用进行释放:
1.通过命令释放共享内存
我们可以使用ipcrm -m shmid(shmid是用户层标识共享内存的id):
当然与之对应删除消息队列和信号量的命令为:
ipcrm -q +id//删除消息队列 ipcrm -s +id //删除信号量
2.通过shmctl函数释放共享内存,shmctl的函数原型如下:
int shmctl(int shmid,int cmd,struct shmid_ds*buf);
参数说明:
第一个参数shmid表示在用户层标识共享内存的id
第二个参数标识具体控制动作
第三个参数buf用于获取或者设置所控制共享内存的数据结构。
返回值:成功返回0失败返回-1.
其他shmctl的第三个参数选项主要有以下三个:
IPC_SET:获取共享内存的当前关联值,此时buf为输出型参数
IPC_SET:当进程有权限的情况下将共享内存的当前关联值设置为buf所值的数据结构当中的值
IPC_RMID:删除共享内存
注意:
对于一个共享内存,实现采用的是引用计数的原理,当进程脱离共享存储区后,计数器减一,挂架成功时,计数器加一,只有当计数器变为零时,才能被删除。当进程终止时,它所附加的共享存储区都会自动脱离。
下面我们通过一段代码进行演示:
#include"comm.h" #include<cstdio> int main() { //获取一个关键字用来唯一标识系统中的共享内存 key_t key=ftok(PATH_NAME,ID); //这个key值会设置进管理共享内存的数据结构中 if(key<0){ cerr<<"ftok fail"<<endl; return 1; } int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666); //IPC_EXCL设置之后保证创建出来的共享内存一定是全新的 if(shmid<0){ cerr<<"shmget fail"<<endl; return 2; } printf("%x\n",key); printf("%d\n",shmid); sleep(5); shmctl(shmid,IPC_RMID,NULL); cout<<"共享内存删除完成"<<endl; sleep(5); return 0; }
我们通过一段shell脚本进行监控:
while :; do ipcs -m sleep 2; echo "###################";done
五.共享内存的关联和去关联
1.共享内存的关联
将共享内存挂接到进程的地址空间我们需要使用shmat函数,shmat函数原型如下:
void*shmat(int shmid,const void*shmaddr,int shmflg);
参数说明:
第一个参数shmid是表示待关联共享内存的用户级id.
第二个参数shmaddr指定共享内存映射到进程地址空间的某一地址通常我们设置为NULL,让内核自己设置一个合适的位置。
第三个参数shmflg表示设置关联共享内存时设置某些属性。
返回值:
- shmat调用成功返回共享内存映射到进程地址空间中的起始地址。
- 调用失败返回返回(void*)-1.
其中shmat的第三个参数shmflg常用选项主要有以下三个:
选项 | 作用 |
SHM_RDONLY | 关联之后的共享内存只能进行读取操作 |
SHM_RND | 若shmaddr不为空,则关联地址自动向下调整为SHMLBA的整数倍。 |
0 | 默认为读写权限 |
下面我我们尝试使用shmat函数对共享内存进行关联:
#include"comm.h" #include<cstdio> int main() { //获取一个关键字用来唯一标识系统中的共享内存 key_t key=ftok(PATH_NAME,ID); //这个key值会设置进管理共享内存的数据结构中 if(key<0){ cerr<<"ftok fail"<<endl; return 1; } int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666); //IPC_EXCL设置之后保证创建出来的共享内存一定是全新的 if(shmid<0){ cerr<<"shmget fail"<<endl; return 2; } printf("attach begin\n"); char*memory=(char*)shmat(shmid,NULL,0); if(memory==nullptr){ cerr<<"shmat fail"<<endl; return 3; } printf("attach end\n"); sleep(10); shmctl(shmid,IPC_RMID,NULL); cout<<"共享内存删除完成"<<endl; return 0; }
同样的我们可以使用上面那个脚本进行检测在这里就不再演示。
2.共享内存的去关联
取消共享内存的关联我们使用的是shmdt,函数原型如下:
int shmdt(const void*shmaddr);
参数说明:
- 要去关联共享内存的起始地址即调用shmat返回得到的地址。
返回值:
调用成功返回0失败返回-1.
六.使用共享内存实现sever&client之间的通信
我们再知道共享内存的创建,关联和去关联以及释放之后现在我们可以尝试让两个进程之间进行通信。其中服务端负责创建共享内存创建好之后进行关联然后进入死循环。
服务端代码如下:
#include"comm.h" #include<cstdio> int main() { //获取一个关键字用来唯一标识系统中的共享内存 key_t key=ftok(PATH_NAME,ID); //这个key值会设置进管理共享内存的数据结构中 if(key<0){ cerr<<"ftok fail"<<endl; return 1; } int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666); //IPC_EXCL设置之后保证创建出来的共享内存一定是全新的 if(shmid<0){ cerr<<"shmget fail"<<endl; return 2; } char*memory=(char*)shmat(shmid,NULL,0); if(memory==NULL){ cerr<<"shmat fail"<<endl; return 3; } while(true){ sleep(1); printf("%s\n",memory); } shmctl(shmid,IPC_RMID,NULL); cout<<"共享内存删除完成"<<endl; return 0; }
客户端只需要和服务端创建的共享内存进行关联即可,然后向共享内存中写入数据。
对应代码如下:
#include"comm.h" #include<cstdio> int main() { key_t key=ftok(PATH_NAME,ID); int shmid=shmget(key,SIZE,0); if(shmid<0){ perror("shmget:"); return 1; } char*mem=(char*)shmat(shmid,NULL,0); char c='A'; while(c<='Z'){ mem[c-'A']=c; c++; mem[c-'A']=0; sleep(2); } shmdt(mem); //注意不需要删除 return 0; }
为了让服务端和客户端再使用ftok函数获取key值时能够得到同一个key值,那么客户端和服务端传入ftok的参数必须相同这样才能生成同一个key值。再这里我们将客户端和服务端都要使用的信息放入到一个公共的头文件当中。
共有头文件comm.h
#pragma once #define PATH_NAME "./" #define ID 0x22222222 #define SIZE 4096 #include<iostream> #include<sys/ipc.h> #include<sys/shm.h> #include<unistd.h> using namespace std;
对应Makfile如下:
.PHONY:all all:client server client:client.cpp g++ -o $@ $^ server:server.cpp g++ -o $@ $^ .PHONY:clean clean: rm -f client server
总结:
(1)优点:我们可以看到使用共享内存进行进程之间的通信是非常方便的,而且函数的接口也比较简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,加快了程序的效率。
(2)缺点:共享内存没有提供同步机制,这使得我们在使用共享内存进行进程之间的通信时,往往需要借助其他手段来保证进程之间的同步工作。