System V 共享内存
- 共享内存是什么
- 如何使用共享内存
- ftok
- shmget
- shmat
- shmdt
- shmctl
- 共享内存的原理
- 共享内存实现两个进程间通信
- 共享内存的特点
- 共享内存与管道配合使用
- 两个进程间通信
- 多个进程间通信
共享内存是什么
🚀共享内存是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间的数据传递不在涉及到内核,换句话说是进程不在通过执行进入内核的系统调用来传递彼此的数据。
🚀共享内存是物理内存的一块区域,通过页表的映射挂接到共享它的进程的地址空间上,再返回给用户这段空间的起始地址,这样用户就能使用这一块内存了。
🚀共享内存与管道不同,管道创建好后,仍需使用系统调用接口来从管道中读取或写入数据,并且管道只允许一端读一端写,而共享内存是直接使用这块内存空间。
如何使用共享内存
ftok
- 参数: 第一个参数是文件名,并且是已经存在的文件,第二个参数是项目的id(可以随便填写)
- 返回值: 返回一个整型的key值,这个key值起到唯一标识的作用。如果失败返回-1。
- 用途: 这个函数是通过所传入的一个字符串和一个整形数,根据它内部的一套算法尽可能的产生一个唯一标示的key值,而这个key值会在创建共享内存的时候使用。
shmget
-
参数: 第一个参数就是通过ftok产生的key值,第二个参数是所要创建的共享内存的大小,第三个参数是shmflag,通常使用IPC_CREAT和IPC_EXCL。
IPC_CREAT : 创建一个共享内存,如果已经存在那么就是用已经存在的共享内存。
IPC_EXCL : 这个选项无法单独使用通常是配合IPC_CREAT使用,其作用是创建一个共享内存,如果此共享内存已经存在就报错返回,其目的就是创建一个全新的共享内存。 -
返回值: 如果创建成功,那么返回共享内存的表示符shmid,如果创建失败就返回-1。
🚀key值的作用:
1.确保进行进程间通信的进程可以看到同一块共享内存。
2.确保创建出来的共享内存与其他已经存在的共享内存冲突。
共享内存使几个进程间得到通信,但是系统中存在大量的进程,可能会创建出大量的共享内存,OS势必会对这些共享内存进行管理操作,也就是说内核中肯定会存在描述共享内存的结构体,里面存储了关于共享内存的许多字段。而这些一个个的结构体对象肯定也会通过某种数据结构组织起来。而key值就会被存储在结构体中,在创建共享内存的时候,会先遍历已存在的共享内存的结构体对象,查看当前的key值是否已经在之前出现过,如果使用的是IPC_CREAT选项,若key值已经出现过那么就返回这块共享内存的标识符(类似于数组下标的东西),如果加上了IPC_EXCL选项,key值已经存在的话就会报错返回。
实际上不一定是以链表这种数据结构来存储的。key值实际上是在shmid_ds结构体对象的第一个成员shm_perm(也是个结构体)中存储的。
shmat
- 参数: 第一个参数是共享内存的表示符,第二个参数是选择挂接的地址,通常我们设置为nullptr,意思是交给OS自动处理,第三个参数通常默认为0。
- 返回值: 返回void的指针,与malloc的返回值十分类似,通常我们将其强转为char 类型的指针使用。如果挂接失败则返回(void*)-1。
前面提到,用户创建完共享内存后,要将共享内存挂接到共享它的进程的地址空间上,就是通过shmat函数来完成的,与动态库类似,动态库首先被加载到物理内存,通过页表的映射作用,映射到进程地址空间的共享区。
shmdt
- 参数: 共享内存的首地址。
- 返回值: 去关联成功的话返回0,否则返回-1。
进程在使用共享内存前首先要与共享内存关联,使用完毕后要与共享内存去关联。
shmctl
- 参数: 第一个参数是共享内存标识符,第二个参数是命令选项如果是删除共享内存的话就选择IPC_RMID,相应的第三个参数设置为nullptr,如果第二个参数选择的是IPC_SET是将内核数据结构的信息提取出来,那么第三个参数就为缓冲区的地址。
- 返回值: 成功的话返回0,否则返回-1。
🚀试着获取内核数据结构:
#define SERVER 0
#define CLIENT 1
class Init
{
public:Init(int n): type(n){int key = get_key();if (type == SERVER)shmid = create_shm(key, gsize);elseshmid = get_shm(key, gsize);start = attch_shm(shmid);}char *getstart(){return start;}int getshmid(){return shmid;}~Init(){detach_shm(start);if (type == SERVER)del_shm(shmid);}private:char *start;int type;int shmid;
};
int main()
{Init init(SERVER);struct shmid_ds p;shmctl(init.getshmid(), IPC_SET, &p);cout << "key : " << p.shm_perm.__key << endl;return 0;
}
共享内存的原理
🚀共享内存是物理内存的一段,通过页表映射到共享它的进程的地址空间上,并且将这块空间的首地址返回给用户。这样两个进程就可以看到同一份资源,可以进行进程间通信了。
🚀系统同存在许多共享内存,OS为了管理这些共享内存,会为其创建结构体对象例如struct shmid_ds,里面存储了关于共享内存的属性。key值被存储在struct shmid_ds内部的第一个成员struct shm_perm中。
🚀对于多个这样像shmid_ds这样的结构体,在内核中会议某种数据结构将它们组织起来。所谓的shmid共享内存标识符就类似于数组的下标。
🚀key值是在内核中使用的,shmid是在用户层使用的。
System V标准的进程间通信的方式,除了共享内存外还有消息队列和信号量。
描述这三个东西的结构体中都有 struct ipc_perm这个结构体,在内核中其实是将这个结构体的指针组织了起来。无论是共享内存,还是消息队列,还是信号量,它们都有这个结构体,它们三个组织在同一数据结构中的。由于perm这个结构体是第一个成员,所以perm的地址与整个结构体的地址是相同的,所以如果想得到整个结构体,可以通过指针强转的方式得到。
共享内存实现两个进程间通信
由于两个进程使用共享内存进行通信时,所用的系统调用接口大致相似,所以我们把这些接口进行简单的封装放在头文件中。即使这样两个进程在调用这些接口的代码也十分相似,所以可以封装成一个类,这个类的构造函数中进行共享内存的创建和挂接,析构函数中进行去关联和删除共享内存,这样就简化了代码,并且使代码更美观。
🚀使用共享内存的一般步骤:
- 创建共享内存
- 共享内存与进程关联
- 共享内存与进程去关联
- 释放共享内存空间
🚀comm.hpp
#pragma once
#include <iostream>
using namespace std;
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <cstdio>
#include <sys/shm.h>#define PATHNAME "."
#define PROJID 1234const int gsize = 4096;
key_t get_key()
{key_t n = ftok(PATHNAME, PROJID);if (n == -1){perror("ftok");exit(1);}return n;
}int create_shm_helper(int key, size_t size, int shmflag)
{int shm_id = shmget(key, size, shmflag);if (shm_id == -1){perror("shmget");exit(2);}return shm_id;
}int create_shm(int key, size_t size)
{return create_shm_helper(key, size, IPC_CREAT | IPC_EXCL | 0664);
}int get_shm(int key, size_t size)
{return create_shm_helper(key, size, IPC_CREAT);
}char *attch_shm(int shm_id)
{char *start = (char *)shmat(shm_id, nullptr, 0);if ((void *)start == (void *)-1){perror("shmat");exit(3);}return start;
}
void detach_shm(char *start)
{int n = shmdt(start);if (n == -1){perror("shmdt");exit(4);}
}
void del_shm(int shmid)
{int n = shmctl(shmid, IPC_RMID, nullptr);if (n == -1){perror("shmctr");exit(5);}
}
#define SERVER 0
#define CLIENT 1
class Init
{
public:Init(int n): type(n){int key = get_key();if (type == SERVER)shmid = create_shm(key, gsize);elseshmid = get_shm(key, gsize);start = attch_shm(shmid);}char *getstart(){return start;}int getshmid(){return shmid;}~Init(){detach_shm(start);if (type == SERVER)del_shm(shmid);}private:char *start;int type;int shmid;
};
🚀server.cpp
#include "comm.hpp"int main()
{Init init(SERVER);// struct shmid_ds p;// shmctl(init.getshmid(), IPC_SET, &p);// cout << "key : " << p.shm_perm.__key << endl;char *start = init.getstart();int n = 0;while (n < 30){cout << start << endl;sleep(1);n++;}return 0;
}
🚀client.cpp
#include "comm.hpp"
int main()
{Init init(CLIENT);char *start = init.getstart();char ch = 'A';while (ch <= 'Z'){start[ch - 'A'] = ch;ch++;start[ch - 'A'] = '\0';sleep(1);}return 0;
}
这份小的代码就是让两个进程实现通信,客户端向共享内存中写入,服务端从共享内存中读取并打印到显示器。
🚀运行效果
共享内存的特点
🚀共享内存是进程间通信最快的方式,创建好共享内存并与进程关联后,就可以直接使用这块空间,与管道相比,管道实现通信的方式通过read,write接口来访问管道资源的,这样就会比共享内存多了两次数据在缓冲区之间拷贝的操作,这也就是为什么管道的速度比共享内存慢了。
🚀共享内存没有同步与互斥的机制做保护,管道通信的话,写端在向管道中写数据的时候,读端会阻塞直到写端把数据写完读端才能读取数据,而共享内存不存在这种保护机制,数据的写入和读取可以同时进行,这回造成许多问题。
🚀共享内存的声明周期是随操作系统的,而管道的声明周期是随进程的。
🚀共享内存的大小是向上对其到页大小的整数倍的。
操作系统中空间的大小是以页为单位的,每页的大小是4096字节。通过ipcs -m 指令可以查看共享内存的属性。
可以看到这里显示共享内存的大小是4097字节,但其实OS为其开辟了8KB的空间,只是允许用户使用4097大小的空间。
🚀使用ipcrm -m shmid 删除共享内存
🚀共享内存也是有权限的,在使用shmget系统调用创建共享内存的时候,其三个参数shmflag可以加上共享内存的权限,IPC_CREAT | IPC_EXCL | 0666,这样传参。
🚀nattch表示有几个进程与共享内存进行关联。
共享内存与管道配合使用
两个进程间通信
🚀上面提到共享内存没有同步与互斥的保护机制,通常是与其他方式配合使用的。我们可以通过共享内存与命名管道相配合的方式来完成进程间通信。在客户端与服务端之间创建两个命名管道,这两个管道的作用是,写端向共享内存写完数据后向读端发送一个信号使其开始在共享内存中读取数据,通向读取完成后给它再给写端发送信号,告诉写端现在可以向共享内存中写入数据了,这样可以达到写端写完数据读端才能读取,读端读取完毕后写端才能继续写入数据。
comm.hpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <cerrno>
#include <fcntl.h>
#include <sys/ipc.h>
#include <cstdlib>
#include <sys/shm.h>
#include <cstring>
using namespace std;#define S_TO_C "pipe1"
#define C_TO_S "pipe2"
const int gsize = 4096;
#define PATHNAME "."
#define PROJ_ID 1234key_t getKey()
{key_t k = ftok(PATHNAME, PROJ_ID);if (k == -1){perror("ftok");exit(5);}return k;
}int createShmHelper(key_t key, int flag)
{int shmid = shmget(key, gsize, flag);if (shmid == -1){perror("shmget");exit(6);}return shmid;
}int createShm(key_t key)
{return createShmHelper(key, IPC_CREAT | IPC_EXCL | 0664);
}int getShm(key_t key)
{return createShmHelper(key, IPC_CREAT);
}char *attchShm(int shmid)
{char *start = (char *)shmat(shmid, nullptr, 0);if ((void *)start == (void *)-1){perror("shmat");exit(7);}return start;
}void detachShm(char *start)
{int n = shmdt(start);if (n == -1){perror("shmdt");exit(9);}
}void delShm(int shmid)
{int n = shmctl(shmid, IPC_RMID, nullptr);if (n == -1){perror("shmctl");exit(8);}
}#define SERVER 0
#define CLIENT 1
class Init
{
public:Init(int n): type(n){key_t key = getKey();if (type == SERVER)shmid = createShm(key);elseshmid = getShm(key);start = attchShm(shmid);}char *getstart(){return start;}~Init(){detachShm(start);if (type == SERVER)delShm(shmid);}private:char *start;int type;int shmid;
};
server.cpp
#include "comm.hpp"
int main()
{// 1.创建两个命名管道int n = mkfifo(S_TO_C, 0664);if (n == -1){cerr << errno << endl;return 1;}int m = mkfifo(C_TO_S, 0664);if (m == -1){cerr << errno << endl;return 2;}// 2.服务端以读的方式打开pipe2,写的方式打开pipe1cout << "----------------" << endl;int wfd = open(S_TO_C, O_WRONLY);cout << "----------------" << endl;if (wfd == -1){cerr << errno << endl;return 3;}int rfd = open(C_TO_S, O_RDONLY);if (rfd == -1){cerr << errno << endl;return 4;}// 3.共享内存相关工作Init init(SERVER);char *start = init.getstart();// 通信int send = 1;write(wfd, &send, sizeof(send));while (true){int receive = 0;// 客户端写完数据,发送信号,服务端接收到信号后再读取read(rfd, &receive, sizeof(receive));if (receive){cout << "收到来自客户端允许读取数据的信号" << endl;cout << start << endl;}else{break;}// 读取完给客户端发送信号,让其继续写入数据write(wfd, &send, sizeof(send));sleep(1);}// 4.关闭管道close(wfd);close(rfd);unlink(S_TO_C);unlink(C_TO_S);return 0;
}
client.cpp
#include "comm.hpp"int main()
{// 2.服务端以读的方式打开pipe1,写的方式打开pipe2int rfd = open(S_TO_C, O_RDONLY);if (rfd == -1){cerr << errno << endl;return 4;}int wfd = open(C_TO_S, O_WRONLY);if (wfd == -1){cerr << errno << endl;return 3;}sleep(1);// 3.共享内存相关工作Init init(CLIENT);char *start = init.getstart();// 通信int cnt = 1;while (true){int receive = 0;read(rfd, &receive, sizeof(receive));if (receive){cout << "收到服务端信号,继续写入" << endl;memset(start, '\0', gsize);snprintf(start, 4096, "%d->%s", cnt++, "hello shm_pipe");// strcpy(start, "hello shm_pipe\n");}int send = 1;write(wfd, &send, sizeof(send));sleep(1);}// 4.关闭管道close(wfd);close(rfd);return 0;
}
🚀程序的运行效果:
多个进程间通信
🚀一块共享内存可以被多个进程关联,那么它可以有多个读端和多个写端,这里实现一个只有一个写端有多个读端的例子,首先创建若干个管道,管道的写端都是ctrlProcess进程,读端分别是process1,process2 … 当ctrlProcess进程向共享内存写入数据完成后,可以选择让哪个进程来读取共享内存中的数据。
comm.hpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <cerrno>
#include <fcntl.h>
#include <sys/ipc.h>
#include <cstdlib>
#include <sys/shm.h>
#include <cstring>
#include <string>
#include <vector>
#include <cstdio>
using namespace std;// 管道名称
const int N = 100;
struct Pipe
{static string pipe_name[N];static int _size;Pipe(){}public:void Add(const char *str){pipe_name[_size] = str;_size++;}
};
int Pipe::_size = 3;
string Pipe::pipe_name[N] = {"pipe1","pipe2","pipe3"};
const int gsize = 4096;
#define PATHNAME "."
#define PROJ_ID 1234key_t getKey()
{key_t k = ftok(PATHNAME, PROJ_ID);if (k == -1){perror("ftok");exit(5);}return k;
}int createShmHelper(key_t key, int flag)
{int shmid = shmget(key, gsize, flag);if (shmid == -1){perror("shmget");exit(6);}return shmid;
}int createShm(key_t key)
{return createShmHelper(key, IPC_CREAT | IPC_EXCL | 0664);
}int getShm(key_t key)
{return createShmHelper(key, IPC_CREAT);
}char *attchShm(int shmid)
{char *start = (char *)shmat(shmid, nullptr, 0);if ((void *)start == (void *)-1){perror("shmat");exit(7);}return start;
}void detachShm(char *start)
{int n = shmdt(start);if (n == -1){perror("shmdt");exit(9);}
}void delShm(int shmid)
{int n = shmctl(shmid, IPC_RMID, nullptr);if (n == -1){perror("shmctl");exit(8);}
}#define SERVER 0
#define CLIENT 1
class Init
{
public:Init(int n): type(n){key_t key = getKey();if (type == SERVER)shmid = createShm(key);elseshmid = getShm(key);start = attchShm(shmid);}char *getstart(){return start;}~Init(){detachShm(start);if (type == SERVER)delShm(shmid);}private:char *start;int type;int shmid;
};
ctrlProcess.cpp
#include "comm.hpp"class Endpoint
{
public:Endpoint(const string &str, int wfd): _pipe_name(str), _wfd(wfd){}public:string _pipe_name;int _wfd;
};int select_process()
{int select = 0;cout << "#####################" << endl;cout << "######0.process1#####" << endl;cout << "######1.process2#####" << endl;cout << "######2.process3#####" << endl;cout << "#### 3.exit ####" << endl;cout << "Please select# ";cin >> select;getchar();return select;
}
int main()
{// 创建共享内存Init init(SERVER);// 创建命名管道文件for (int i = 0; i < Pipe::_size; i++){int n = mkfifo(Pipe::pipe_name[i].c_str(), 0664);if (n == -1){perror("mkfifo");exit(-1);}}// 以写的方式打开这些命名管道vector<Endpoint> end_points;for (int i = 0; i < Pipe::_size; i++){int wfd = open(Pipe::pipe_name[i].c_str(), O_WRONLY);if (wfd == -1){perror("open");exit(-2);}end_points.push_back(Endpoint(Pipe::pipe_name[i], wfd));}// 通信char *start = init.getstart();while (true){int select = select_process();if (select == 3)break;if (select < 0 || select > 3)continue;memset(start, '\0', 4096);cout << "向共享内存写入信息# ";fgets(start, 4000, stdin);// cin >> start;// 通知相应进程int command = 1;write(end_points[select]._wfd, &command, sizeof(int));sleep(2);cout << endl;}// 关闭pipefor (int i = 0; i < Pipe::_size; i++){close(end_points[i]._wfd);}// unlink pipefor (int i = 0; i < Pipe::_size; i++){unlink(end_points[i]._pipe_name.c_str());}return 0;
}
process1.cpp
#include "comm.hpp"
int main()
{// 打开相应管道int rfd = open(Pipe::pipe_name[0].c_str(), O_RDONLY);if (rfd == -1){perror("open");exit(-3);}// 挂接共享内存Init init(CLIENT);char *start = init.getstart();// 接受来自ctrl的命令while (true){int command = 0;int n = read(rfd, &command, sizeof(int));if (n == 4){if (command == 1){cout << "process1收到从共享内存读取数据的命令" << endl;cout << start;}}else if (n == 0){cout << "写端关闭" << endl;break;}elsebreak;}// 关闭pipeclose(rfd);return 0;
}
process2.cpp
#include "comm.hpp"int main()
{// 打开相应管道int rfd = open(Pipe::pipe_name[1].c_str(), O_RDONLY);if (rfd == -1){perror("open");exit(-3);}// 挂接共享内存Init init(CLIENT);char *start = init.getstart();// 接受来自ctrl的命令while (true){int command = 0;int n = read(rfd, &command, sizeof(int));if (n == 4){if (command == 1){cout << "process2收到从共享内存读取数据的命令" << endl;cout << start;}}else if (n == 0){cout << "写端关闭" << endl;break;}elsebreak;}close(rfd);return 0;
}
process3.cpp
#include "comm.hpp"
int main()
{// 打开相应管道int rfd = open(Pipe::pipe_name[2].c_str(), O_RDONLY);if (rfd == -1){perror("open");exit(-3);}// 挂接共享内存Init init(CLIENT);char *start = init.getstart();// 接受来自ctrl的命令while (true){int command = 0;int n = read(rfd, &command, sizeof(int));if (n == 4){if (command == 1){cout << "process3收到从共享内存读取数据的命令" << endl;cout << start;}}else if (n == 0){cout << "写端关闭" << endl;break;}elsebreak;}close(rfd);return 0;
}
🚀运行效果: