一、共享内存(Shared Memory)的认识
共享内存(Shared Memory)是多进程间共享的一部分物理内存。它允许多个进程访问同一块内存空间,从而在不同进程之间共享和传递数据。这种方式常常用于加速进程间的通信,因为数据不需要在不同的进程间进行拷贝。
在操作系统中,共享内存通常是通过映射一段能被其他进程所访问的内存实现的。一个进程可以创建一个共享内存段,并将该段连接到其地址空间中。其他进程也可以将这段共享内存连接到它们的地址空间中。这样,所有进程都可以访问同一段内存,实现数据的共享。
在内核中共享内存可以存在很多个,操作系统必须先创建描述共享内存的结构体,再把这些结构体组织起来管理。为了保证两个或者是多个进程看到同一个共享内存,就要给每一个共享内存提供唯一性的标识。
二、创建共享内存的方法
创建共享内存的方法为shmget,其中第一个参数为key,key就是共享内存在内核中的唯一标识。size是要设置的共享内存的大小(在内核中,共享内存是以4kb为基本单位的,我们在给共享内存分配大小的时候最好也是分配4kb的整数倍的大小。)。还有一个参数shmflg,shmflg可以有很多选项,但最常见的有两个:
IPC_CREAT:如果共享内存不存在, 就创建之, 如果共享内存已经存在, 直接获取它。IPC_EXCL:不能单独使用, 没意义。IPC_CREAT | IPC_EXCL:如果共享内存不存在, 就创建之, 如果共享内存已经存在,就出错返回!!如果共享内存创建是成功的, 则一定是一个新的共享内存!
如果shmget成功获取或创建了共享内存段,它会返回一个非负整数,这个整数是共享内存段的标识符(也称为共享内存段的ID)。这个标识符在后续的共享内存操作中(如shmat和shmdt)会被使用。
2.1、key的获取
这里的pathname是一串文件路径,proj_id是一个整数,这两个参数由用户随意指定,操作系统底层通过特定的算法帮我们形成一个key值,如果形成失败-1被返回。如果成功这个key值就会被设置进描述共享内存的结构体中用来标识这块共享内存的唯一性。通过给两个进程或者是多个进程传入同样的pathname和proj_id就能让它们看到同一块共享内存。
三、查看共享内存的方法
采用ipcs指令可以查看系统中指定用户创建的共享内存,消息队列和信号量。
ipcs -m:查看系统中指定用户创建的共享内存
ipcs -q:查看系统中指定用户创建的消息队列
ipcs -s:查看系统中指定用户创建的信号量
四、指令删除共享内存的方法
ipcrm -m shmid(共享内存id):删除用户指定的共享内存。
五、代码实现共享内存通信
5.1、获取key值
其实获取key可以封装成函数也可以不封装,这里我是将其封装成函数了。
key_t get_key(const char* pathname, int proj_id){ key_t key = ftok(pathname, proj_id); //成功返回key值,失败返回-1 if(key == -1) { cout << "获取key值失败,原因是:" << strerror(errno) << endl; exit(1); } return key;}
5.2、创建共享内存
共享内存是为了实现两方或是多方通信的,这里我就设置成为两方通信。所以一定是一方创建共享内存,另一方获取共享内存。要注意的是,共享内存也是有权限的,所以创建的一方需要指明创建的共享内存的权限。
int get_or_create_shared_memory(key_t key, int size, int flag){ int shmid = shmget(key, size, flag); //成功返回共享内存标识符,失败返回-1 if(shmid == -1) { cout << "共享内存创建失败,原因是:" << strerror(errno) << endl; exit(2); } return shmid;}int create_shared_memory(key_t key, int size){ return get_or_create_shared_memory(key, size, IPC_CREAT | IPC_EXCL | 0666);}int get_shared_memory(key_t key, int size){ return get_or_create_shared_memory(key, size, IPC_CREAT);}
5.3、挂接共享内存/去挂接共享内存
shmid表示要挂接的共享内存的shmid,shmaddr表示要将该共享内存挂接到进程地址空间的什么位置,其实这个我们不用管,操作系统会自行帮我们挂接,可以直接设置为nullptr,shmflg表示可以对该共享内存做什么操作,设置为0默认是可读可写。 如果挂接成功,返回挂接到进程地址空间的地址,如果挂接失败,返回-1。
5.4、同步操作
如果读写共享内存的进程间没有进行同步操作,可能就会发生脏读,即写入的数据和读到的数据不一致。所以要进行进程同步操作。这里我借助了管道来进行同步操作,即写方写完了再唤醒读方来读。
#include <iostream>#include <cstring>#include <unistd.h>#include <sys/stat.h>#include <sys/types.h>#include <fcntl.h>#include <cstdio>using namespace std;#define MODE 0666 //权限#define NAME "./fifo.txt"//定义命名管道结构体class Fifo{private: string _name; // 文件路径加文件名public: Fifo(const string &name) : _name(name) { int n = mkfifo(_name.c_str(), MODE); if (n == 0) cout << "创建管道成功!" << endl; else cout << "创建管道失败!原因是:" << strerror(errno) << endl; }; ~Fifo() { int n = unlink(_name.c_str()); if (n == 0) cout << "删除管道成功!" << endl; else cout << "删除管道失败!原因是:" << strerror(errno) << endl; };};//同步结构体class Sync{private: int rfd; int wfd;public: void open_read() { rfd = open(NAME, O_RDONLY); if (rfd == -1) { cout << "读打开管道失败!" << endl; exit(1); } } void open_write() { wfd = open(NAME, O_WRONLY); if (wfd == -1) { cout << "写打开管道失败!" << endl; exit(1); } } int wait() { int ret = 0; int n = read(rfd, &ret, sizeof(int)); return n; } void wake_up() { int ret = 0; int n = write(wfd, &ret, sizeof(int)); }};
读写方分别创立一个sync对象,在读写的时候分别调用wait和wake_up方法进行同步。
5.5、删除共享内存
进程创建的共享内存如果在进程结束时没有释放,则共享内存会一直存在。也就是说,共享内存的声明周期是随内核的,如果我们没有主动去释放共享内存,除非重启系统,否则共享内存一直存在。所以在写端当你已经不写了时要将共享内存删掉。
shmctl系统调用加上IPC_RMID选项可以删除共享内存。
void shm_del(int shmid){ int ret = shmctl(shmid, IPC_RMID, nullptr); if (ret == -1) cerr << "删除共享内存失败" << endl; else cout << "删除共享内存成功" << endl;}
shmctl的第三个选项可以传入一个描述共享内存的对象的地址来获取该共享内存的属性,如果只是删除共享内存,直接设置为nullptr即可。
六、总结
共享内存不提供进程间协同的任何机制。但是共享内存是所有进程间通信机制中速度最快的。因为共享内存是通过页表直接与进程地址空间中的地址产生关联的,写方只需要将数据拷贝到共享内存中,读方直接通过地址就能访问内容,无需进行数据的拷贝,直接就提高了访问数据的速度。也就是说共享内存进行进程间通信只需要一次数据的拷贝,而我们之前提到的管道通信,都是读方调用write函数将数据写入内存(进行了一次拷贝),读方再调用read函数将数据拷贝到用户层,要进行两次数据的拷贝。
七、说明
因为实现共享内存的文件数较多,所以以上并不是全部代码,如果想获取全部实现代码,请移步到本人码云:C++代码: C++代码保存的地方 - Gitee.com