关于 System V 标准,一共有三种通信方式,分别为:共享内存、信号量和消息队列三种通信方式。本篇将较为详细的讲解三种通信方式的实现原理,以及介绍在 Linux 系统下调用这三种的通信方式的接口,其中以共享内存为例,较为详细的讲解和用代码实现这种通信方式。
最后我们得出这三种通信方式存在很大的共同点,以及总结了操作系统对这三种通信方式的管理。目录如下:
目录
共享内存
1. 实现原理
2. 代码实现进程的共享内存通信
共享内存的创建
共享内存的释放
共享内存挂接
共享内存间通信代码
3. 共享内存的优缺点
4. 获取共享内存的属性
消息队列
1. 消息队列的原理
2. 消息队列的接口
3. 消息队列的指令操作
信号量
1. 信号量的原理
2. 信号量的操作
3. 信号量的指令
操作系统对三种 System V 的管理
共享内存
1. 实现原理
共享内存为进程间通信方案,则一定会遵守进程间通信的的原则:让不同进程看见同一份资源。对于共享内存的实现原理为:操作系统会在物理内存中专门给需要通信的进程开辟一段物理内存,然后分别映射到不同进程的虚拟地址中,不同的进程就可以看到同一份的内存资源。原理图如下:
如上的步骤1:操作系统在物理内存中开辟出同于共享内存通信的物理内存;
步骤2:操作系统将物理共享内存通过页表映射到对应的虚拟地址中去。
共享内存实现的几个关键点:
1. 关于实现共享内存的所有操作,都是由OS(操作系统)完成的;
2. OS给需要共享内存通信的进程提供步骤1、2的系统调用,让他们通过系统调用来通信。
3. 共享内存在系统中可以同时存在多份,不同对进程之间可以同时进行通信。
4. 操作系统需要对共享内存进行管理,所以会有对应的共享内存数据结构,以及匹配的算法。
5. 共享内存 = 内存空间 + 共享内存的属性。
2. 代码实现进程的共享内存通信
共享内存的创建
关于使用系统调用实现共享内存的一个重要的系统调用,shmget,也就是用来获取在物理内存中的共享内存,使用方法如下:
int shmget(key_t key, size_t size, int shmflg); 系统调用中的参数:key:用于唯一的表示物理内存中存在的共享内存;size:需要开辟的物理内存的大小,通常建议为4096的倍数shmflg:获取共享内存的方式,常用的为IPC_EXCL 和 IPC_CREAT,关于这两个的搭配有:IPC_CREAT:若不存在共享内存则创建,存在则获取对应的共享内存且返回总能获取一个IPC_EXCL:不能单独的使用IPC_CREAT | IPC_EXCL:对应的共享内存不存在则创建,存在则出错返回只能获取新的
关于在 shmget 中的 key 参数,不同的进程想要进程通信,则填入的 key 值则需要相同,对于这个 key 值,我们一般不建议直接由我们自己来填,而是建议使用系统提供的 ftok 函数给我们随机生成一个,如下:
key_t ftok(const char *pathname, int proj_id);
只需要提供同样的 pathname 和同样的 proj_id,我们就可以生成同样的 key 值,也就可以让不同的进程之间实现通信。
共享内存的释放
还有一个需要注意的点:我们使用进程创建出的共享内存,并不会随着进程的结束而释放,进程结束仍然会保留在内存中,因为这是由底层的操作系统创建出的共享内存,所以我们需要在每一次进程结束的时候,将对应的共享内存关闭,防止内存泄漏。(共享内存的生命周期随内核,文件的生命周期随进程)
我们可以使用 ipcs -m 查看当前系统中存在哪些共享内存,然后使用 ipcrm -m shmid 删除对应的共享内存,如下:
共享内存的 key 与 shmid 的比较:
key:属于用户形成,内核使用的一个字段,内核用于区分共享内存的唯一性,用户不能使用 key 来进行共享内存的管理。
shmid:内核给用户返回的一个标识符,用来进行用户级对共享内存进行管理的 id 值。
使用系统调用删除共享内存为 shmctl,如下:
int shmctl(int shmid, int cmd, struct shmid_ds *buf); 该系统调用用于对共享内存的控制,可以为增删查改 但本篇只需要将共享内存释放,所以只需要填入的参数为: 共享内存的shmid 选择模式,删除为 IPC_RMID buf 为 nullptr
共享内存挂接
现在我们既然已经创建出对应的共享内存,只需要将共享内存挂接到对应的共享内存,然后就可以让对应进程之间开始通信了,如下:
void *shmat(int shmid, const void *shmaddr, int shmflg); 用于挂接共享内存 shmaddr表示将挂接共享内存的位置,通常可以设置为nullptr shmflg表示共享内存的访问权限 shmat的返回值:挂接成功为共享内存的起始地址,连接失败为nullptrint shmdt(const void *shmaddr); 用于将挂接上的共享内存给去掉 去挂接成功返回0,失败返回-1
共享内存间通信代码
Shm.hpp
#include <iostream> #include <string> #include <cerrno> #include <cstring> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <unistd.h>// 路径,随便使用一个路径都可以 const std::string SHPathName = "/home/JZhong/code/linux-code/test_7_20"; const int SHProj_Id = 0x8585;#define SHCreater 1 #define SHUser 0 #define SHsize 4096class Shm {std::string ToHex(int n) {char buff[128];int cx = snprintf(buff, sizeof(buff) - 1, "0x%x", n);buff[cx] = '\0';return buff;}int GetShm(int shmflg) {int sh = shmget(_key, SHsize, shmflg);if (sh == -1) {std::cout << "Create shm fail!" << "the reason is " << strerror(errno) << std::endl;}return sh;}std::string WhoamI() {if (_who == SHCreater)return "Creater";else if (_who == SHUser)return "User";elsereturn "None";}void* AttachShm() {void* shmaddr = shmat(_shmid, nullptr, 0);if (shmaddr == nullptr) {perror("shm attach: ");return nullptr;}std::cout << WhoamI() << " attach the shm" << std::endl;return shmaddr;}void DettachShm(void* shmaddr) {if (shmaddr == nullptr)return;shmdt(shmaddr);} public:Shm(const std::string& pathname, int proj_id, int who): _pathname(pathname), _proj_id(proj_id) , _who(who), _shmaddr(nullptr){_key = ftok(pathname.c_str(), proj_id);// 创建共享内存if (who == SHCreater) {// 加入 0666 权限才可以将共享内存挂接上_shmid = this->GetShm(IPC_CREAT | IPC_EXCL | 0666);std::cout << "Creater create the shm, the key: " << ToHex(_key) << " shmid:" << _shmid << std::endl;} else {_shmid = this->GetShm(IPC_CREAT | 0666);std::cout << "User get the shm, the key: " << ToHex(_key) << " shmid:" << _shmid << std::endl;}_shmaddr = this->AttachShm();}void Zero() {if (_shmaddr) memset(_shmaddr, 0, SHsize);}void* GetAddress() {return _shmaddr;}~Shm() {if (_shmaddr != nullptr)DettachShm(_shmaddr);if (_who == SHCreater) {shmctl(_shmid, IPC_RMID, nullptr);}std::cout << "shm remove done..." << std::endl;} private:std::string _pathname;int _proj_id;int _who;int _shmid;key_t _key;void* _shmaddr; };
server.cc
#include "Shm.hpp"int main() {Shm shm(SHPathName, SHProj_Id, SHCreater);char* addr = (char*)shm.GetAddress();while (true) {std::cout << addr << std::endl;sleep(1);}return 0; }
client.cc
#include "Shm.hpp"int main() {Shm shm(SHPathName, SHProj_Id, SHUser);shm.Zero();char* addr = (char*)shm.GetAddress();char ch = 'A';while (ch <= 'Z') {addr[ch - 'A'] = ch;ch++;sleep(2);}return 0; }
以上的代码让服务器端创建出共享内存,然后让客户端写、客户端读,运行如下:
3. 共享内存的优缺点
优点:通过上面关于读出共享内存的代码我们可以发现,我们没有调用任何的系统调用,直接就将数据写入和读出,这是因为虚拟地址和共享内存已经建立连续,所以共享内存通信方式,是所有进程间通信方式中最快的一种。
缺点:如上代码的运行结果显示,当我们客户端写数据每秒写一个数据,服务器端每两秒读一个数据的时候,我们会发现:服务器端每次读的时候会将读过的数据在读一遍。这也说明共享内存不提供对共享内存的数据任何保护机制,这样的机制同时还会导致数据不一致问题,也就是客户端还没写完,服务器端就将数据读出,会导致数据分析存在问题。
假若我们要解决这种数据不一致问题,我们可以在通信双方间在增加一个管道,增加管道可以每次将数据阻塞起来,只要写端每写完或者没写,读端就不可以读出来,如下:
增加的 NamedPipe.hpp
#pragma once #include <iostream> #include <sys/types.h> #include <sys/stat.h> #include <string> #include <cerrno> #include <cstring> #include <unistd.h> #include <fcntl.h>#define NPCreater 0 #define NPUser 1 #define NPMode 0666 #define NPRead O_RDONLY #define NPWrite O_WRONLY #define DEFAULT_FDX -1 #define NPReadSize 1024const std::string ComPath = "./myfifo";class NamedPipe { private:std::string GetName() {if (_who == NPCreater)return "Creater";else if (_who == NPUser)return "User";elsereturn "None";}// 以不同的方式打开对应的文件int OpenNamedPipe(mode_t mode) {int fd = open(_path.c_str(), mode);if (fd == -1) {std::cout << "open file fail" << " the reason is " << strerror(errno) << std::endl;}return fd;} public:NamedPipe(const std::string& path, size_t who): _path(path), _who(who), _fd(DEFAULT_FDX){// 让服务器端读,让客户端写,服务器创建出对应的管道文件if (who == NPCreater) {int n = mkfifo(_path.c_str(), NPMode);if (n < 0) {std::cout << "create named pipe fail!" << " the reason is " << strerror(errno) << std::endl;}_fd = OpenNamedPipe(NPRead);std::cout << GetName() << " create the named pipe" << std::endl;} else {_fd = OpenNamedPipe(NPWrite); }}int SomeoneUseToRead(std::string* out) {char inbuff[NPReadSize];int n = read(_fd, inbuff, sizeof(inbuff) - 1);if (n == -1) {std::cout << "read failed" << " the reason is " << strerror(errno) << std::endl;} inbuff[n] = '\0';*out = inbuff;return n;}void SomeoneUseForWrite(const std::string& info) {int n = write(_fd, info.c_str(), info.size());if (n == -1) {std::cout << "write failed" << " the reason is " << strerror(errno) << std::endl;}}~NamedPipe() {if (_who == NPCreater) {// 让创建者删除对应的管道文件int n = unlink(_path.c_str());if (n < 0) std::cout << "remove the named pipe fail!" << " the reason is " << strerror(errno) << std::endl; }std::cout << GetName() << " unlink the named pipe file " << std::endl;if (_fd != DEFAULT_FDX) {close(_fd);}} private:std::string _path;size_t _who;int _fd; };
client.cc
#include "Shm.hpp" #include "NamedPipe.hpp"int main() {Shm shm(SHPathName, SHProj_Id, SHUser);shm.Zero();char* addr = (char*)shm.GetAddress();NamedPipe fifo(ComPath, NPUser);char ch = 'A';while (ch <= 'Z') {addr[ch - 'A'] = ch;std::cout << "add " << ch << " into shm " << "wakeup reader" << std::endl;ch++;std::string temp("wakeup");fifo.SomeoneUseForWrite(temp);sleep(2);}return 0; }
server.cc
#include "Shm.hpp" #include "NamedPipe.hpp"int main() {Shm shm(SHPathName, SHProj_Id, SHCreater);char* addr = (char*)shm.GetAddress();NamedPipe fifo(ComPath, NPCreater);while (true) {// 每次先阻塞读std::string temp;fifo.SomeoneUseToRead(&temp);std::cout << addr << std::endl;sleep(1);}return 0; }
4. 获取共享内存的属性
当我们想要获取共享内存的属性的时候,我们可以使用 shmctl 系统调用接口,然后创建出一个 struct shmid_ds 变量获取到 共享内存的属性,如下:
struct shmid_ds ds; shmctl(_shmid, IPC_STAT, &ds); 获取共享内存的属性,使用 IPC_STAT 获取,获取出的属性保存在 ds 中struct shmid_ds {struct ipc_perm shm_perm; /* Ownership and permissions */size_t shm_segsz; /* Size of segment (bytes) */time_t shm_atime; /* Last attach time */time_t shm_dtime; /* Last detach time */time_t shm_ctime; /* Last change time */pid_t shm_cpid; /* PID of creator */pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */shmatt_t shm_nattch; /* No. of current attaches */... };struct ipc_perm {key_t __key; /* Key supplied to shmget(2) */uid_t uid; /* Effective UID of owner */gid_t gid; /* Effective GID of owner */uid_t cuid; /* Effective UID of creator */gid_t cgid; /* Effective GID of creator */unsigned short mode; /* Permissions + SHM_DEST and SHM_LOCKED flags */unsigned short __seq; /* Sequence number */ };
消息队列
1. 消息队列的原理
操作系统会现在内存空间中创建出一个消息队列,然后要通信的进程会使用消息队列的系统调用接口往消息队列中放入结点(放入数据块),接着进程通过消息队列中的标识逐一拿出对应的数据块(所以,消息队列是一个进程向另一个进程发送有类型数据块的一种方式),如下:
2. 消息队列的接口
获取消息队列的接口为 msgget,如下:
int msgget(key_t key, int msgflg);
对于参数 key,我们可以使用 fotk 获取,对于 msgflg,我们也可以使用 IPC_CREAT 和 IPC_EXCL 带入。返回值为 msqid。
当我们想要将消息队列删除时,可以使用接口,msgctl,如下:
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
使用方法和 shmctl 一样。
获取消息队列的属性数据结构:
struct msqid_ds {struct ipc_perm msg_perm; /* Ownership and permissions */time_t msg_stime; /* Time of last msgsnd(2) */time_t msg_rtime; /* Time of last msgrcv(2) */time_t msg_ctime; /* Time of last change */unsigned long __msg_cbytes; /* Current number of bytes in queue (nonstandard) */msgqnum_t msg_qnum; /* Current number of messages in queue */msglen_t msg_qbytes; /* Maximum number of bytes allowed in queue */pid_t msg_lspid; /* PID of last msgsnd(2) */pid_t msg_lrpid; /* PID of last msgrcv(2) */ };struct ipc_perm {key_t __key; /* Key supplied to msgget(2) */uid_t uid; /* Effective UID of owner */gid_t gid; /* Effective GID of owner */uid_t cuid; /* Effective UID of creator */gid_t cgid; /* Effective GID of creator */unsigned short mode; /* Permissions */unsigned short __seq; /* Sequence number */ };
向消息队列发送数据块和接收数据块:
我们可以使用接口 msgsnd 和 msgrcv 接口,如下:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
3. 消息队列的指令操作
消息队列和共享内存一样,生命周期也是随内核的。当我们要查看当前内核存在哪些消息队列,我们可以使用 ipcs -q,当我们删除对应的消息队列,使用 ipcrm -q miqid,如果我们想要查看消息队列、共享内存和信号量在内存中是否存在,使用 ipcs 指令,如下:
信号量
在介绍信号量前先介绍一些概念和基础认识:
1. 共享资源:能被多个执行流(进程)看到的一份资源。
2. 临界资源:被保护起来的资源,一般是通过互斥的方式保护共享资源。
3. 互斥和同步:对于互斥来说,如何时刻只能有一个进程访问共享资源(多个进程形成互斥)。同步为,多个进程可以同时访问一个共享资源,比如共享内存,可以写一点读一点.
4. 临界区:访问共享资源的代码。对来代码来说,分为访问共享资源的代码(一般为使用系统调用接口访问共享资源的代码)和不访问共享资源的代码。
5. 对共享资源的保护,本质是对访问共享资源的代码(临界区)进程保护。
信号量也可以被称为信号灯,是用来保护共享资源(临界资源)的。
1. 信号量的原理
在我们使用共享资源的时候,很多情况下都是将共享资源当成一个整体使用,但是当我们需要将共享资源分块,不整体使用的时候,拆分成很多小块,多个执行流(进程)访问不同的共享资源小块,保证共享资源的一定并发度。
所以信号量就是用来将共享资源拆分小块进行管理的一种方式。
信号量本质是一个对临界资源预先申请的计数器。进程在访问共享资源之前需要向共享资源提出预定申请,也就是申请信号量,当信号量充足的时候才可以申请成功,放完共享资源之后,释放信号量。
当我们将共享资源整体使用的时候,就是能访问的共享资源只有一个,所以这种特殊的情况为二元信号量(二元信号量的访问机制为互斥),当由多个共享资源可以访问的时候就为多元信号量。
关于信号量的设计,信号量本质是一个计数器,那我们能不使用一个全局的变量充当信号量呢?答案是不能,因为信号量是共享资源小块的计数器,这个信号量需要让不同的进程看见的,一个全局变量只能在一个进程中看见。所以对于信号量来说,这是一个在操作系统内核中的一个”计数器“。
但是我们可以发现:信号量和共享内存、消息队列进行比较我们可以发现,信号量作为一个计数器,为什么仍然被纳入到了进程间通信呢?
这是因为信号量仍然可以让不同的进程看到同一份资源,同时信号量也属于一种公共资源,因为其他进程都可以看见信号量。
进程申请信号量的的原理如下:
2. 信号量的操作
首先是关于获取信号量的系统调用 semget,如下:
int semget(key_t key, int nsems, int semflg);
信号量的获取操作和共享内存以及消息队列基本一致,不过操作系统允许我们一次申请多个信号量,也就是 nsems。申请几个 nsems 就为几个。
信号量的删除操作,semctl,如下:
int semctl(int semid, int semnum, int cmd, ...);
和共享内存、消息队列仍然差不多,不过由于系统中存在多个信号量,所以可以定位是哪个信号量,只需要填入参数 semnum 即可。假若要删除全部信号量,只需要将 semnum 设置为 0 即可,关于后面的可变参数,可以填入 struct semid_ds 也就是获取更加详细的信号量的属性。
信号量的属性数据结构,如下:
struct semid_ds {struct ipc_perm sem_perm; /* Ownership and permissions */time_t sem_otime; /* Last semop time */time_t sem_ctime; /* Last change time */unsigned long sem_nsems; /* No. of semaphores in set */ };struct ipc_perm {key_t __key; /* Key supplied to semget(2) */uid_t uid; /* Effective UID of owner */gid_t gid; /* Effective GID of owner */uid_t cuid; /* Effective UID of creator */gid_t cgid; /* Effective GID of creator */unsigned short mode; /* Permissions */unsigned short __seq; /* Sequence number */ };
对指定信号量进行操作,如下:
int semop(int semid, struct sembuf *sops, unsigned nsops);
3. 信号量的指令
获取信号量的指令为:ipcs -s,删除信号量的指令为:ipcrm -s semid(和共享内存消息队列相统一)。如下:
操作系统对三种 System V 的管理
通过以上的认识,我们会发现操作系统对于共享内存、消息队列以及信号量三种进程间通信方式的管理方式有着很大的一致性,比如:
1. 获取和删除都是 XXXget 和 XXXctl
2. 他们的属性数据结构都为 XXXid_ds,且数据结构中的第一个元素为 ipc_perm(且三个的 ipc_perm 都相同)。
所以对于这三种通信方式存在很大的共性。
在操作系统中,操作系统会维护一个 struct ipc_id_arr 的柔性数组并且用其中的 struct kern_ipc_perm* p[0] 来实现管理我们的 IPC 进程间通信,如下:
通过如上的方式就将不同的通信方式进行了统一管理,所以我们平时所获取的 shmid msqid semid,其实就是柔性数组的下标。所以当我们要访问不同的内容的时候,我们只需要使用 (struct sem_arry*) p[semid],(struct shmid_kernel*) p[shmid],(struct msg_queue*) p[msqid] 来进行访问不同的通信方式,这样的一种实现,其实就是一种多态。