【Linux进程间通信二】System V 共享内存和消息队列
【Linux进程间通信二】System V 共享内存和消息队列
- 1.共享内存原理
- 2.创建共享内存
- (1)形成key
- (2)shmflg
- 3.关联进程
- (1)关联
- (2)取消关联
- 4.释放共享内存
- 5.共享内存应用 — server&client通信
- 6.共享内存的优缺点
- (1)优点:共享内存是所有进程间通信(IPC)方式中速度最快的
- (2)缺点:共享内存不提供进程间协同的任何机制
- 7.消息队列
- (1)创建或获取消息队列——msgget函数
- (2)发送或接受数据
- (3)释放消息队列
- 8.System V中IPC资源在内核中的设计
- (1)IPC资源的管理
- (2)多态实现
- (3)资源的标识和访问
1.共享内存原理
进程间通信的本质就是先让不同的进程看到同一份资源。以前学的管道都是基于文件的,那么我们还有其它方案进行进程间通信吗?我们下面学习的共享内存就是由操作系统帮我们在地址空间中进行通信。
system V作为一种通信标准,在此标准下有一套共享内存机制,即通过系统接口得到一块共享内存,这块内存就对应着通信前提中的同一份资源,让不同的进程都“看到”这块资源后,不同进程就可以进行通信,与管道一样,共享内存也是一种IPC技术。
每一个进程都有自己的 task_struct,也就是有自己的地址空,而共享内存就是调用系统接口,向系统申请的一块物理内存资源,因为是操作系统,所以它也有资格修改进程的页表、地址空间等。然后使进程虚拟空间的共享区与物理空间的共享内存之间建立映射关系,最后给应用层返回这个起始的虚拟地址,同理,其他进程通过页表也能映射到这块物理空间,这样多个进程就看到了同一份资源,如下图:

2.创建共享内存
我们得在系统里创建一个共享内存,在 Linux 中创建一个共享内存的系统接口为:shmget(),手册如下:


- key:用户对共享内存的唯一标识符
- size:以字节为单位,表示申请空间的大小
- shmflg:申请空间时的申请方式
- 返回值:成功,返回共享内存的标识符,是一个整数;否则返回 -1,错误码被设置
(1)形成key
系统怎么知道这个共享内存是否存在呢?怎么保证让不同的进程看到同一个共享内存呢?这个key就是操作系统用来识别共享内存块的唯一符号,无论是创建共享内存还是获取共享内存,我们必须要拿到同一个 key,因为拿到同一个 key 才能保证访问的是同一个共享内存,所以 key 是一个数字,它是多少不重要,关键在于它必须在内核中具有唯一性。
对于一个已经创建好的共享内存而言key 在共享内存的描述对象中。
第一次创建的时候,怎样确保这个 key 具有唯一性呢?我们知道,路径天然就具有唯一性,所以我们就可以根据路径这样具有唯一性的属性形成对应的 key,那么在系统中有一个接口ftok()可以帮助我们形成一个 key:


返回值就是 key。
第一个参数就是路径,第二参数是项目id,这两个参数我们都可以随意传,只要保证可以创建出具有唯一性的 key 即可,如果创建失败,我们只需要修改这两个参数即可。
其实 ftok() 就是一套算法,它会把我们的路径和项目 id 进行数值计算,转化为一个数字。
这个 key 为什么要我们用户自己形成呢?如果key是由内核设定,进程之间不知道对方创建共享内存的key值,因为进程具有独立性。
(2)shmflg
有进程申请空间,就会有进程使用,那么创建共享内存只需要创建一次就够了,其它进程想在这个共享内存中通信的时候,不需要创建了,只需要获取这个共享内存就行了,肯定需要通过某种方式去表示如何创建、如何获取这样的概念,那么 shmflg 就是可以表示这些内容,其中有如下选项:

以上两个选项,它们就是宏,而且它们每一个比特位都是不重叠的,用来传标记给系统调用。
其中 IPC_CREAT 表示创建一个共享内存,如果不存在就直接创建,存在就直接获取并返回。如果这个选项单独使用就是以上效果
IPC_CREAT | IPC_EXCL 表示创建一个共享内存,如果不存在就直接创建,存在就出错返回。那么这两个选项组合使用,就能确保我们申请的共享内存一定是一个新的
IPC_CREAT | IPC_EXCL | 0666 可以用来指定新的共享内存的权限
而 IPC_EXCL 不单独使用。
3.关联进程
(1)关联
已经有了共享内存,接下来就要进行对共享内存和进程进行挂接了。那么使用到的系统接口是:shmat(),手册如下:


第一个参数 shmid 就是上面shmget()的返回值。
第二个参数 shmaddr 就是我们想让当前的共享内存挂接到共享区的哪个位置,但是一般让系统决定挂接到哪里,所以设置为 nullptr 即可。
三个参数 shmflg 就是有关挂接的权限,我们按照共享内存默认的权限即可,设置为0即可。
返回值:最终挂接到的虚拟地址会以返回值的形式返回给我们。
(2)取消关联
可以使用系统接口取消关联,对应接口为:shmdt(),手册如下:



只有一个参数 shmaddr,这个参数就是 shmat() 的返回值。
只需要传入起始地址就可以了吗?怎么知道这个空间有多大呢?共享内存实际上被申请的时候,它有自己的管理属性,那么它自己会记录共享内存有多大,共享内存也必须是连续的,所以在进行地址空间映射的时候,从连续空间加上大小,我们就知道它的范围了,我们只需要知道从哪开始就行了。
4.释放共享内存
共享内存的生命周期是随内核的,所以每次进程退出后 IPC 资源还是存在的,那么我们也可以使用指令直接把它释放,但是我们还有对应的系统接口释放共享内存,其接口为shmctl(),手册如下:

第一个参数就是共享内存的 id。
第三个参数,struct shmid_ds 就是类似于内核当中的管理共享内存所对应的 struct 结构体。

也就是说,shmctl()一定能让我们获取到共享内存的属性,那么我们要查看共享内存的属性做什么?
所以就有了第二个参数 cmd,表明我们要做什么操作,那么 cmd 的选项有如下:

其中我们需要的是 IPC_RMID,它的作用是标记共享内存被删除。我们删除就不关注共享内存的属性了,所以第三个参数设为 nullptr 即可。那么返回值成功也是返回0,失败返回-1
5.共享内存应用 — server&client通信
comm.hpp
#pragma once#include <iostream>
#include <cstdlib>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>using namespace std;const string PATHNAME = "/home/lhc/lesson/Shm";
const int proj_id = 0x11223344;
const int size = 4096;//共享内存的大小永远是4096的倍数,建议设置为n*4096
const string FILENAME = "fifo";key_t GetKey()
{key_t key = ftok(PATHNAME.c_str(),proj_id);if(key < 0){cerr << "errno:" << errno << ",errstring:" << strerror(errno) << endl;exit(1);}return key;
}int ShmHelper(key_t key ,int flag)
{int shmid = shmget(key,size,flag);if(shmid < 0){cerr << "errno:" << errno << ",errstring:" << strerror(errno) << endl;exit(2);}return shmid;
}int Creatshm(key_t key)
{return ShmHelper(key,IPC_CREAT | IPC_EXCL | 0644);
}
int Getshm(key_t key)
{return ShmHelper(key,IPC_CREAT);
}
server.cpp
#include <iostream>
#include <cstring>
#include <sys/ipc.h> //Inter-Process Communication
#include <sys/shm.h>
#include <unistd.h>
#include "comm.hpp"using namespace std;int main()
{bool r = mkfifo(FILENAME.c_str(),0666);//创建管道key_t key = GetKey();int shmid = Creatshm(key);char* s = (char*)shmat(shmid,nullptr,0);cout << "开始将shm映射到进程的地址空间中" << endl;//int fd = open(FILENAME.c_str(), O_RDONLY);while (true){//waitint code = 0;ssize_t n = read(fd,&code,sizeof(code));if (n > 0){// 直接读取cout << "共享内存的内容: " << s << endl;sleep(1);}else if (n == 0){break;}}
//shmdt(s);cout << "开始将shm从进程的地址空间中移除" << endl;shmctl(shmid,IPC_RMID,nullptr);cout << "开始将shm从OS中删除" << std::endl;return 0;
}
client.cpp
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h> //Inter-Process Communication
#include <sys/shm.h>
#include "comm.hpp"using namespace std;int main()
{key_t key = GetKey();int shmid = Getshm(key);char* s = (char*)shmat(shmid,nullptr,0);cout << "attach shm done" << endl;//int fd = open(FILENAME.c_str(),O_WRONLY);char c = 'a';for(;c <= 'z';c++){s[c - 'a'] = c;cout << "write : " << c << " done" << endl;sleep(1);//通知服务端int code = 1;write(fd,&code,sizeof(4));}
//shmdt(s);cout << "开始将shm从进程的地址空间中移除" << endl;shmctl(shmid,IPC_RMID,nullptr);cout << "开始将shm从OS中删除" << std::endl;return 0;
}
Makefile
.PHONY:all
all:server clientserver:server.cppg++ -o $@ $^ -std=c++11
client:client.cppg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f server client fifo
6.共享内存的优缺点
(1)优点:共享内存是所有进程间通信(IPC)方式中速度最快的
- 减少拷贝次数:在管道通信中,数据通常要经过至少两次拷贝,数据从一个进程的缓冲区写入管道,然后从管道中读取到另一个进程的缓冲区中,这涉及两次数据在不同内存区域之间的复制操作。在共享内存中允许多个进程直接访问同一块内存区域,当一个进程将数据写入到共享内存中,其他进程可以立即看到,最多只会经历一次从进程的用户空间到共享内存的拷贝。
- 直接访问:进程可以直接对共享内存进行读写操作,无需通过OS进行数据中转,这大大减少了内核参与数据传输的开销,提高了通信效率。
(2)缺点:共享内存不提供进程间协同的任何机制
这会导致多个进程同时访问共享内存区域时,出现数据不一致和数据竞争等问题。因为没有内置的同步和互斥手段,不同进程可能在不可预测的时间点对共享内存进行读写操作,从而破坏数据的完整性。例如,一个进程正在写入数据时,另一个进程可能同时在读取,可能会读取到不完整的数据;或者两个进程同时写入,可能会导致数据覆盖混乱。所以需要额外的机制(管道、信号量等)来保证数据的完整性和一致性。
7.消息队列
一种进程间通信(IPC)的机制,允许多个进程通过发送和接收带有类型的数据块(消息)进行通信,这些消息在队列中按照先进先出(FIFO)的顺序存储
发送进程将消息添加到队列的末尾,接收进程从队列的头部读取信息
假设进程A将数据块入队列,进程B也将数据块入队列,那么进程A就可以从队列中读取到进程B的数据块。那么进程A和进程B怎么区分这些数据块呢?到底是自己的数据块还是对方的数据块?所以,它们必须区分开来,区分方式就是向内核发送的数据块是带类型的!这个类型就是区分是自己的数据块还是对方的数据块
操作系统内部肯定不止一个消息队列,会有非常多的进程进行通信,所以操作系统还要管理消息队列,所以需要先描述,再组织
(1)创建或获取消息队列——msgget函数

参数和返回值都是和共享内存类似,形成key的方式和共享内存一样
(2)发送或接受数据

发送数据:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msqid 为向指定的消息队列发;msgp 为数据块的起始地址;msgsz 为数据块的大小;msgflg 设为0,阻塞式发就可以了
接收数据:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
前三个参数和上面的一样;msgtyp 是数据块的类型;最后一个参数也是和上面一样。其中我们可以看一看数据块的缓冲区,里面有数据块的类型和大小:

(3)释放消息队列

这三个参数也是和共享内存类似
我们还可以用指令查看操作系统中的消息队列,例如 ipcs -q,如下:

8.System V中IPC资源在内核中的设计
(1)IPC资源的管理
- 在OS中,通过维护一个数组来管理不同的IPC资源,如:共享内存、消息队列、信号量
- 每种IPC资源都有其特定的数据结构(如:shmid_ds、msqid_ds、semid_ds等),这些数据结构的第一个成员都是以ipc_perm结构体作为开头

(2)多态实现
- ipc_perm包含了IPC资源共有的属性,内核层面上称为kern_ipc_perm、用户层面上称为ipc_perm,所以可以将ipc_perm视为基类,不同的IPC资源数据结构(如:shmid_ds、msqid_ds等)作为ipc_perm的子类,扩展了额外的特定于该资源的属性
- 通过将数组的元素类型定为ipc_perm*,OS就可以实现对不同IPC资源的统一管理和访问。当需要访问特定资源的额外属性时,可以通过强制类型转化来实现,如:(shmid_ds*)array[i]->额外属性
(3)资源的标识和访问
- 每个IPC资源在创建时都会分配唯一的标识符(如:shmid、msgid等),这个标识符实际上就是数组的下标
- 标识符的增长是线性递增的,即使资源被释放,新的资源也会获得比上一个资源更大的标识符,为了避免数组越界,OS提供了回绕机制
