1.共享内存的原理
要理解共享内存的原理,首先我们得记起进程间通信的前提:必须让不同的进程看到同一份资源(必须由OS提供)
我们都知道进程都会有自己的进程地址空间,然后都会通过页表与物理内存进行映射,如果要进行进程间通信的话,那肯定不止一个进程,最少也得两个进程,所以我们就可以让这两个进程对应的进程地址空间都通过页表映射到同一块物理内存。如果细分步骤的话,那就需要进行如下几步:
①申请物理内存
②把申请的物理内存的地址填充到页表中,然后把在进程地址空间里面对应这块空间的共享区的起始地址返回给上层(通过我们前面学的动静态库的理解)
此时两个进程都共享同一块物理内存,这里就达到了我们进程间通信的前提条件了。然后就可以进程进程间通信了,通信后。
③如果我们要释放这块共享内存的话,就可以通过清空页表中映射到该处物理内存的内容,也就是清除掉进程与这块物理内存之间的映射关系
④直接释放这块物理内存
可是,系统里不仅仅只有这两个进程,如果有非常多组的进程共享多块不同的共享内存,也就是说操作系统里会有多组不同的进程同时使用相同的一块共享内存,所以操作系统一定会允许系统同时存在多个共享内存,那既然有这么多的共享内存,操作系统要不要管理起来呢?哪些进程在用哪些共享内存,操作系统是不是得知道呢?答案当然是要的,既然要管理起来,那么就需要先描述在组织。所以为了方便操作系统管理,我们的共享内存会有一个struct shm这样的数据结构对象来代表这块共享内存,这个结构体对象里面会有这块共享内存的属性,比如什么时候创建的,已经用了多少内存,还剩多少内存,到底有多少的指针指向这块内存呢等等。如果申请一块共享内存,那就创建一个对应的struct shm结构体对象,如果释放了,就把这个结构体对象删除了,所以对共享内存的管理就转变成了对struct shm的增删查改。
理解:
1.共享内存,也要被操作系统管理
2.如何保证第二个之后的参与通信的进程,看到的就是同一个共享内存呢?要保证这一点那么就需要要求共享内存必须要有唯一的标识,那么如何做到呢?怎么给另一个进程呢?
2.快速认识系统接口
①int shmget(key_t key, size_t size, int shmflg);
这里的这个key是什么意思呢?我们如何理解呢?那我们拿进程来说,就是进程创建了共享内存,另一个进程怎么会知道呢?于是就约定了一个共享数字,这个数字是多少并不重要,这个数字只需要满足唯一性,每一个共享内存对应的唯一的数字即可,那么我们就可以规定好是多少,规定好之后就相当于是共享内存的唯一标识,当我们创建好共享内存的时候,我们就可以把这个数字写到对应的共享内存的内核数据结构对象里面,然后因为我们规定了,所以写代码期间我们通信双方就都知道这个数字,所以我们此时创建的时候就可以让系统帮我们去找共享内存对应的那个数字跟我们传进去的key是一样的就可以了,比如说key是1234,那么就可以保证通信双方看到的是同一块共享内存,但是呢,随意去创建数字是容易产生冲突的,所以不建议去随便用一个数字,所以我们建议用这个函数,key_t ftok(const char *pathname, int proj_id);
这个函数的作用就是将pathname和一个proj_id转化成一个共享内存的key.这只是一个算法函数,并不会对系统做任何操作,比如说通过一些散列方式形成特定的key,所以将来使用的是同一个pathname,同一个proj_id,再使用同样的一套算法,我们就一定能形成同样的key,那么在代码中自然也就能通过形成同样的key找到同一块共享内存。所以为了我们想要判断是不是同一块共享内存,我们就需要判断是不是同一个pathname,同一个proj_id,同一套算法.
下面我们来聊聊shmflag这个参数,这个参数能够支撑我们既然创建又能获取共享内存,shmflag可以传的两个重点选项是IPC_CREAT(代表shm不存在就创建,存在就获取并返回),IPC_EXCL(不单独使用,通常和IPC_CREAT一起使用,代表shm不存在就创建,存在就出错返回,保证创建的共享内存是全新的),很显然是两个宏,我们前面学过可以通过位图来传递多个标志位,除了传上面的参数,还可以传权限,通过传权限就能修改我们需要设置的权限。
而创建共享内存之后,如果我们要删除共享内存就需要使用命令:ipcrm -m shmid 删除指定的共享内存。
共享内存的大小,强烈建议设置成为n*4096.
②void *shmat(int shmid, const void *shmaddr, int shmflg);
③int shmdt(const void *shmaddr);
④int shmctl(int shmid, int cmd, struct shmid_ds *buf);
3.直接编写代码
3.1创建key值
comm.hpp代码:
#pragma once#include<iostream>#include<string>const std::string pathname = "/home/sunwenchao/mylesson/lesson30";
const int proj_id = 0x11223344;
server.cc代码:
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<cstring>
#include"comm.hpp"
int main()
{key_t key = ftok(pathname.c_str(),proj_id);if(key<0){std::cerr<<"errno: "<<errno <<", errstring: "<<strerror(errno)<<std::endl;return 1;}std::cout<<"key: "<<key<<std::endl;return 0;
}
client.cc代码:
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<cstring>
#include"comm.hpp"
int main()
{key_t key = ftok(pathname.c_str(),proj_id);if(key<0){std::cerr<<"errno: "<<errno <<", errstring: "<<strerror(errno)<<std::endl;return 1;}std::cout<<"key: "<<key<<std::endl;return 0;
}
Makefile代码:
.PHONY:all
all:server clientclient:client.ccg++ -o $@ $^ -std=c++11server:server.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f server client .fifo
运行结果:
3.2创建共享内存
int shmget(key_t key, size_t size, int shmflg);接口测试:
comm.hpp代码:
#pragma once#include<iostream>
#include<cstdlib>
#include<string>const std::string pathname = "/home/sunwenchao/mylesson/lesson30";
const int proj_id = 0x11223344;
const int size = 4096;key_t GetKey()
{key_t key = ftok(pathname.c_str(),proj_id);if(key<0){std::cerr<<"errno: "<<errno <<", errstring: "<<strerror(errno)<<std::endl;exit(1);}std::cout<<"key: "<<key<<std::endl;return key;
}
client.cc代码:
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<cstring>
#include"comm.hpp"
int main()
{key_t key = GetKey();int shmid = shmget(key,size,IPC_CREAT|IPC_EXCL);if(shmid<0){std::cerr<<"errno: "<<errno <<", errstring: "<<strerror(errno)<<std::endl;return 1;}std::cout<<"shmid: "<<shmid<<std::endl;return 0;
}
server.cc代码:
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<cstring>
#include"comm.hpp"
int main()
{key_t key = GetKey();int shmid = shmget(key,size,IPC_CREAT|IPC_EXCL);if(shmid<0){std::cerr<<"errno: "<<errno <<", errstring: "<<strerror(errno)<<std::endl;return 1;}std::cout<<"shmid: "<<shmid<<std::endl;return 0;
}
Makefile代码:
.PHONY:all
all:server clientclient:client.ccg++ -o $@ $^ -std=c++11server:server.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f server client .fifo
运行结果:
查看共享内存属性:
此时进程已经退出了,当我们再次运行时:
发现他报错了。说明确实是不存在就创建了,如果存在了就会报错返回。而这里的共享内存跟我们之前讲的文件并不一样,文件是当进程退出了会直接关闭的,但是这里的共享内存想要释放除了重启系统,否则是需要我们用代码手动释放的。
所以我们就可以得出一个结论:共享内存(IPC资源)的生命周期是随内核的!
这些是标准是规定,实现的时候与进程是没有关系的。
3.3进程与共享内存挂接
void *shmat(int shmid, const void *shmaddr, int shmflg);这个接口是为了将shm映射到进程的地址空间当中,让该进程与共享内存挂接。
我们用如下代码来进行测试:
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<cstring>
#include"comm.hpp"
int main()
{key_t key = GetKey();int shmid = shmget(key,size,IPC_CREAT|IPC_EXCL|0644);if(shmid<0){std::cerr<<"errno: "<<errno <<", errstring: "<<strerror(errno)<<std::endl;return 1;}sleep(5);std::cout<<"shmid: "<<shmid<<std::endl;std::cout<<"开始将shm映射到进程的地址空间中 "<<std::endl;char* s = (char*)shmat(shmid,nullptr,0);sleep(5);return 0;
}
测试结果:
与此同时我们在另一个SSH渠道用指令while :; do ipcs -m; sleep 1;done来进行检测发现:
3.4进程与共享内存断开关联
当在我们的server可执行程序运行过程中,从刚开始没有共享内存,然后通过shmget创建共享内存,然后再通过执行shmat函数让共享内存和当前进程产生关联,至此nattch从0变成了1,然后等到进程退出的时候,nattch又从1变成了0,所以我们这里验证了,nattch是一个计数器,用来对与共享内存产生关联的数量进行计数的。
当然除了进程退出的时候可以让nattch进行--,我们也希望可以不让进程退出的时候实现进程与共享内存直接断开关联,所以我们就可以使用函数int shmdt(const void *shmaddr);我们可以把shmat函数的返回值,也就是共享内存的起始地址作为参数传给该接口,也就是用如下代码:
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<cstring>
#include"comm.hpp"
int main()
{key_t key = GetKey();sleep(3);int shmid = shmget(key,size,IPC_CREAT|IPC_EXCL|0644);if(shmid<0){std::cerr<<"errno: "<<errno <<", errstring: "<<strerror(errno)<<std::endl;return 1;}sleep(5);std::cout<<"shmid: "<<shmid<<std::endl;std::cout<<"开始将shm映射到进程的地址空间中 "<<std::endl;char* s = (char*)shmat(shmid,nullptr,0);sleep(5);std::cout<<"开始将shm从进程的地址空间中移除 "<<std::endl;shmdt(s);sleep(10);return 0;
}
运行结果:
我们发现当执行shmdt接口的时候,进程还没退出的时候,nattch从1变成了0.所以我们这里就实现了将shm从进程的地址空间中移除,也就是让进程与共享内存进行断开关联。
3.5从操作系统删除共享内存
那么我们删除如何去删除我们的创建的共享内存呢?因为我们创建之后每次如果不通过命令ipcrm -m shmid来删除共享内存的话就会引起
这样重复创建的报错,所以我们需要将其用完了共享内存之后进行删除,所以我们就需要使用shmctl这个接口来进行删除共享内存:
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<cstring>
#include"comm.hpp"
int main()
{key_t key = GetKey();sleep(3);int shmid = shmget(key,size,IPC_CREAT|IPC_EXCL|0644);if(shmid<0){std::cerr<<"errno: "<<errno <<", errstring: "<<strerror(errno)<<std::endl;return 1;}sleep(5);std::cout<<"shmid: "<<shmid<<std::endl;std::cout<<"开始将shm映射到进程的地址空间中 "<<std::endl;char* s = (char*)shmat(shmid,nullptr,0);sleep(5);std::cout<<"开始将shm从进程的地址空间中移除 "<<std::endl;shmdt(s);sleep(5);shmctl(shmid,IPC_RMID,nullptr);std::cout<<"开始将shm从OS中删除 "<<std::endl;sleep(10);return 0;
}
运行结果:
4.总结4个接口整体完整代码
comm.hpp
#pragma once#include<iostream>
#include<cstdlib>
#include<string>const std::string pathname = "/home/sunwenchao/mylesson/lesson30";
const int proj_id = 0x11223344;
const int size = 4096;key_t GetKey()
{key_t key = ftok(pathname.c_str(),proj_id);if(key<0){std::cerr<<"errno: "<<errno <<", errstring: "<<strerror(errno)<<std::endl;exit(1);}std::cout<<"key: "<<key<<std::endl;return key;
}std::string ToHex(int id)
{char buffer[1024];snprintf(buffer,sizeof(buffer),"0x%x",id);return buffer;
}int CreateShmHelper(key_t key,int flag)
{int shmid = shmget(key,size,flag);if(shmid<0){std::cerr<<"errno: "<<errno <<", errstring: "<<strerror(errno)<<std::endl;exit(2);}return shmid;
}int CreateShm(key_t key)
{return CreateShmHelper(key,IPC_CREAT|IPC_EXCL|0644);
}int GetShm(key_t key)
{return CreateShmHelper(key,IPC_CREAT);}
Makefile
.PHONY:all
all:server clientclient:client.ccg++ -o $@ $^ -std=c++11server:server.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f server client .fifo
client.cc
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<cstring>
#include"comm.hpp"
int main()
{key_t key = GetKey();int shmid = GetShm(key);char *s = (char*)shmat(shmid,nullptr,0);char c = 'a';for(;c<='z';c++){s[c-'a'] = c;std::cout<<"write: "<<c<<"done"<<std::endl;sleep(6);}shmdt(s);std::cout<<"detach shm done"<<std::endl;return 0;
}
server.cc
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<cstring>
#include"comm.hpp"
int main()
{key_t key = GetKey();std::cout<<"key : "<<ToHex(key)<<std::endl;// sleep(3);int shmid = CreateShm(key);// sleep(5);std::cout<<"shmid: "<<shmid<<std::endl;std::cout<<"开始将shm映射到进程的地址空间中 "<<std::endl;char* s = (char*)shmat(shmid,nullptr,0);// sleep(5);while(true){//直接读取std::cout<<"共享内存的内容:"<<s<<std::endl;sleep(1);}std::cout<<"开始将shm从进程的地址空间中移除 "<<std::endl;shmdt(s);// sleep(5);shmctl(shmid,IPC_RMID,nullptr);std::cout<<"开始将shm从OS中删除 "<<std::endl;// sleep(10);return 0;
}
运行结果:
可是我们发现一个进程写一次之后,另一个进行会读很多次,也就是没有看到有同步机制。但是如果我们想要让其有同步机制我们也可以通过管道来进行实现同步机制的。
5.总结共享内存的特点
1.共享内存的通信方式不会提供同步机制,共享内存是直接裸露给所有的使用者的,一定要注意共享内存的使用安全问题
2.共享内存是所有进程间通信速度最快的
3.共享内存可以提供较大的空间