前言
我们上一期介绍了,用管道实现进程间通信,以及介绍了管道的原理!本期我们来介绍System V系列的进程间通信!本博客主要介绍共享内存,对消息队列和信号量只介绍原理和接口调用!
本期内容介绍
一、System V 共享内存
1、什么是System V 共享内存?
2、System V共享内存的原理
3、System V 共享内存的相关接口
• shmget
• ftok
• shmat
• shmdt
• shmctl
二、System V 消息队列
1、什么是System V 消息队列?
2、System V 消息队列的原理
3、System V 消息队列的相关接口
• msgget
• msgsnd
• msgrcv
• msgctl
指令操作
三、System V 信号量
1、什么是System V的信号量
2、和信号量相关的基本概念
3、理解信号量
4、信号量的相关接口
• semget
• semctl
• semop
指令操作
四、内核对System V 的管理
一、System V 共享内存
我们前面介绍过,进程间通信的本质是不同的进程看到同一份资源!前面介绍的,管道是基于文件系统实现的!这里要介绍的共享内存更直接,直接给你一段内存空间让你通信!
1、什么是System V 共享内存?
共享内存是让不同的进程直接访问同一段物理内存,来实现通信的一种技术!
2、System V共享内存的原理
我们以前在介绍动态库加载的时候,我们介绍过,动态库在物理内存只加载一份!所有依赖该动态库的进程,会将其虚拟内存的共享区各自映射一份!也就是不同的进程都可以看到同一个库!我们介绍过进程间通信的前提是让不同的进程看到同一份资源!动态库加载不就看到了嘛!连库都可以映射,那一段内存应该不是什么问题吧!是的,感谢内存的原理和动态库的加载很像似!
所以,我们可以申请一段内存空间,然后将这块空间挂接到不同进程的虚拟地址空间,此时不同进程就看到了同一块内存空间!就可以通信了!
注意:
1、OS是软硬件资源的管理者,所以上述的开内存空间以及挂接都是OS做的
2、OS不相信任何人,所以必须提供相关的系统调用
3、OS有很多的进程,所以OS要对这些进程进行先描述,再组织的管理
4、共享内存 = 内存空间(数据) + 共享内存的属性
3、System V 共享内存的相关接口
• shmget
作用:创建共享内存
他这个参数有点恶心!下面就来介绍一下他的参数:
参数
• key :这个共享内存的名字(这个是给OS用的)
• size :共享内存的大小(一般是4KB,或4KB的整数倍)
• shmflag : 标记位,和前面介绍文件时的mode一样,也是位图!
这里的key是唯一标记一块共享内存的标记,类似于这块共享内存的名字,这个值是用户提供给内核的,由于用户自己提供冲突的概率有点大,所以,OS为了降低冲突,又提供了一个获取这个key值的接口!所以,我们先来了解这个接口!
• ftok
作用:获取一个创建共享内存的随机key
参数
• pathname : 是一个随便的路径
• proj_id : 是一个随机值
返回值
• 返回生成的key
OK ,现在key有了, size有了!就差shmflag了,他的标记很多,这里我们只介绍两个:
• IPC_CREAT : 如果key对应的共享内存,存在就用;
• IPC_EXCL : 一般不单独使用和IPC_CREAT组合使用
• IPC_CREAT | IPC_EXCL : 如果key对应的共享内存,不存在,就创建;存在,出错返回
• 注意:其实这里的第一个,第三个选项的区别是,第三个一旦成功就一定是全新的,所以一般用它来创建,而用第一个来接收!
shmget的返回值
成功:返回一个正整数,表示标记该共享内存的标记码;(给用来操作的)
失败:返回-1,错误码被设置
OK ,介绍完了,我们来以创建一下:
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string>
#include <cstdio>#define SIZE 4096 //默认4KBkey_t GetCommKey(const std::string &pathname, const int proj_id)
{key_t key = ftok(pathname.c_str(), proj_id);if (key < 0){perror("ftok");}return key;
}//将key转成16进制
std::string ToHeX(key_t key)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", key);return buffer;
}int main()
{const std::string pathname = "/home/cp";const int proj_id = 0x666;key_t key = GetCommKey(pathname, proj_id);int shmi = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);if(shmi < 0)perror("shmget");std::cout << "key: " << ToHeX(key) << std::endl;std::cout << "shmi: " << shmi << std::endl;return 0;
}
这里用来查看共享内存的指令是 ipcs -m
当然我们不想要要了,也可以通过指令删除:
删除共享内存的指令是:ipcrm -m shmi
这里使用shmi删除而不用key删除的原因是,key是内核使用的,而shmi是用户操作的,ipcrm本质是进程,也就是用户,而用户只能使用shmi操作,所以这里用的就是shmi;
• shmat
作用:将共享内存挂载到进程的地址空间
参数
• shmi :表示要将哪个共享内存挂接!
• shmaddr :表示要让共享内存挂接到哪里,一般让OS决定,所以设置为nullptr即可!
• shmflag :表示挂接的权限,我们按照默认权限即设置成0即可!
返回值
成功,返回共享内存的起始地址; 失败, 返回-1, 错误码被设置
注意:挂接时可能会以为权限不显示,所以为了保险起见,我们可以在创建时将权限设置为666/664:
//创建共享内存
int CreaterShm(key_t key)
{int shmi = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);if (shmi < 0)perror("shmget");return shmi;
}//挂接到地址空间
void* AttachShm(int shmi)
{void *addrshm = shmat(shmi, nullptr, 0);if(addrshm == nullptr)perror("shmat");return addrshm;
}
挂接有了如何取消挂接呢?
• shmdt
作用:将共享内存和当前进程的地址空间取消挂接
他的参数就是挂接时的哪个起始地址,很简单不在介绍!
//取消挂接
void DetachShm(void *addrshm)
{int res = shmdt(addrshm);if(res < 0)perror("shmdt");
}
我们此时创建的共享内存是没有办法自己释放的,因为他的创建按之是OS,所以要用户用完之后自己创建,有点挫!有没有办法,在代码中控制呢?当然有!
• shmctl
作用:用于控制共享内存
参数
• shmid:shmget的返回值,表示共享内存
• cmd :控制共享内存的动作
• buf :获取内核描述共享内存的结构体对象的属性
这里的二个参数的选项有:
IPC_STAT用来配合第三个参数,获取内核描述共享内存结构ds对象的;
IPC_RMID删除共享内存段(重要);
OK,下面我们来删除一下共享内存:
//删除共享内存
void DelShm(int shmi)
{shmctl(shmi, IPC_RMID, nullptr);std::cout << "delete shm...." << std::endl;
}
OK,关于关于共享内存的接口就介绍到这里!关于内核描述共享内存的哪个结构体对象,你除非写OS否则你这辈子也不可能用到!而且他也用起来比较简单,这里不在多比比了!
以上我们都是,面向过程式的了解了他的接口,但是并没有通信!下面我们就来用C++的方式封装一下,让他们通信:还是和管道一样,分为client和server端!
shm.hpp
#ifndef __SHM_HPP__
#define __SHM_HPP__#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstdio>
#include <unistd.h>
#include <cstring>const std::string gpathname = "/home/cp/oslearn/linux_learn/shm";
const int gproj_id = 0x66;
#define gCreater 1
#define gUser 2
#define SIZE 4096 // 建议是4096的整数倍class Shm
{
private:// 通过path和pro_id获取key值int GetCommKey(){key_t key = ftok(_pathname.c_str(), _proj_id);if (key < 0){perror("ftok");}return key;}// 获取共享内存int GetShmHeaper(key_t key, size_t size, int flag){int shmi = shmget(key, size, flag);if (shmi < 0){perror("shmget");}return shmi;}// 以创建者的身份获取共享内存bool GetShmForCreate(){if (_who == gCreater){_shmi = GetShmHeaper(_key, SIZE, IPC_CREAT | IPC_EXCL | 0666);std::cout << "shm creater done..." << std::endl;sleep(2);if (_shmi >= 0)return true;}return false;}// 以使用者的身份获取共享内存bool GetShmForUse(){if (_who == gUser){_shmi = GetShmHeaper(_key, SIZE, IPC_CREAT | 0666);sleep(2);std::cout << "shm get done..." << std::endl;if (_shmi >= 0)return true;}return false;}std::string GetCurrUsr(int who){if (who == gCreater)return "gCreater";else if (who == gUser)return "gUser";elsereturn "None";}// 将共享内存挂载到各自的虚拟地址空间、void *AttachShm(){void *addrshm = shmat(_shmi, nullptr, 0);if (addrshm == nullptr){perror("shmat");}std::cout << "who: " << GetCurrUsr(_who) << " attach shm...." << std::endl;return addrshm;}// 取消挂载int DetachShm(void *addrshm){int res = shmdt(addrshm);if (res < 0){perror("shmdt");}std::cout << "who: " << GetCurrUsr(_who) << " detach shm...." << std::endl;return res;}//void *Addrshm(){return _addrshm;}// 发送消息一次void SendMessageOnce(const std::string &msg){void *addrshm = Addrshm();memcpy(addrshm, msg.c_str(), msg.size() + 1);sleep(1);}public:Shm(const std::string &gpathname, int gproj_id, int who): _pathname(gpathname), _proj_id(gproj_id), _who(who){_key = GetCommKey();if (_who == gCreater)GetShmForCreate();elseGetShmForUse();_addrshm = AttachShm();Clear();std::cout << "key: " << _key << std::endl;std::cout << "shmi: " << ToHeX() << std::endl;}~Shm(){if (_addrshm != nullptr)DetachShm(_addrshm);if (_who == gCreater){int res = shmctl(_shmi, IPC_RMID, nullptr);}std::cout << "shm remove done...." << std::endl;}// 将shmi 转为 16 进制std::string ToHeX(){char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", _key);return buffer;}// 将共享内存清空void Clear(){if (_addrshm){memset(_addrshm, 0, SIZE);}}// 发送消息void SendMessage(const std::string &msg, int times = -1){if (times == -1){while (true){SendMessageOnce(msg);}}else{while (times--){SendMessageOnce(msg);}}}// 接收消息void RecerveMessage(){void *addrshm = Addrshm();while (true){std::cout << "msg: " << (char *)addrshm << std::endl;sleep(1);}}private:key_t _key; // key值int _shmi; // shmget的返回值,供用户操作const std::string _pathname; // 路径const int _proj_id; // pro_idint _who; // 使用共享内存的身份void *_addrshm; // 挂载后返回的共享内存的起始地址
};#endif
server.cc
#include "Shm.hpp"int main()
{Shm shm(gpathname, gproj_id, gCreater);shm.RecerveMessage();return 0;
}
client.cc
#include "Shm.hpp"int main()
{Shm shm(gpathname, gproj_id, gUser);shm.SendMessage("hello U, 今天是周六!");return 0;
}
这样写的代码就很优雅了!下面看一下运行结果:
OK,挂载以及通信都没有问题!但是我们发现,上面的客户端还没有写,但是服务端就已经开始读取了!这是共享内存的缺点,就是数据不一致的问题!原因是共享内存不做任何的保护机制!
这个缺点可以可用信号量来解决,我们这里不介绍,后面的线程在介绍!主要原因是共享内存目前处于淘汰化的边缘!后面有更好的解决方案!共享内存的优点就是:他是所有的进程IPC中速度最快的,以为他直接把数据感到了内存,减少了数据的拷贝!
另外,我们上面介绍了,共享内存和管道不一样,管道的生命周期时随进程的,当进程结束管道就被收回了!但是共享内存不一样,我们需手动的释放或者指令释放!这也是共享内存的一个特点:共享内存的生命周期随内核!
二、System V 消息队列
1、什么是System V 消息队列?
System V的消息队列是一种进程间通信的方式!他允许不同进程向他发送和从他获取数据!
其实他本质就是就是一个链表!
2、System V 消息队列的原理
上面介绍什么是System V共享内存的时候,说了,它本质上是一个链表!也就是不同进程发送数据,发送的是特定类型的数据块!另一个接收的进程会根据特定类型获取数据块!在详细一点就是,A进程给B进程发送消息,会先通过内核申请一个特定类型的数据块,然后将你的数据通过接口写到这个特定类型的数据块!然后内核串到链表,B进程根据特定的类型获取时本质也是获取到了哪个数据块!所以这样也就让不同进程看到了同一块资源!符合进程间通信了!
也就是,这里的消息队列就是通过链表实现的一个不同进程交换数据的公共队列!当然OS内部不可能只有两个进程使用他通信!可能有很多的进程都是用他通信,所以OS还是要对他进行先描述,在组织的管理!所以OS内部一定存在管理他的数据结构和描述他的结构体!
3、System V 消息队列的相关接口
上面的共享内存也是System V版本的通信方式,既然都是System V的方式有啥共同点呢?其实他们的共同点在接口方面的体现就是非常的像似!和STL类似!
• msgget
作用:获取消息队列
参数
• key和共享内存的意义一样,获取方式(ftok)也是一样的!
• msgflag : 标记位。也是位图的思想!(这里也和共享内存一样,不在解释)
返回值
成功返回一个和共享内存一样的shmi一样的标识符!否则返回-1,错误码被设置!
• msgsnd
作用:向指定的消息队列发送数据
参数
• msgid:表示向指定的(标识符为msgid)消息队列发
• msgp:发送数据块的起始地址,
• msgsz : 发送数据块的大小
• msgflg :发送数据的选项,这里设置为0即可,让他阻塞式的发!
返回值
• 成功, 返回0; 失败,返回-1, errno被设置
• msgrcv
作用:接收消息队列中的特定类型的数据块中的数据
参数
• msgid:表示从指定的(标识符为msgid)消息队列接收
• msgp:接受数据块的起始地址
• msgsz : 接受数据块的大小
• msgtyp :表示要接收数据块的类型
• msgflg :发送数据的选项,这里设置为0即可,让他阻塞式的接收即可!
返回值
• 成功, 返回读取到(拷贝到进程读取的结构体中的)的字节数; 失败,返回-1, errno被设置
• msgctl
作用:释放消息队列
参数
• msgid :表示要释放的消息队列的标识符
• cmd : 控制消息队列的标记
• buf:用来获取内核中的描述消息队列的结构体对象的ds对象指针
cmd的常用选择享有: IPC_STAT用来配合第三个参数,获取内核描述共享内存结构ds对象的;
IPC_RMID删除消息队列(重要);
指令操作
介绍完System V的接口,我们就发现他们的接口真的很相似!就先指令查看和删除也是:
查看消息队列:ipcs -q
删除消息队列:ipcrm -q msgid
注意:消息队列和共享内存一样也是生命周期随内核的!也就是得手动的删除!
三、System V 信号量
前面在共享内存时,我们发现一个进程在写入时,可能只写了一部分,但是另一个进程已将这部分读走了,导致了双方数据不一致的问题!上面介绍了可以用信号量来解决,什么是信号量?下面我们就来介绍一下!
1、什么是System V的信号量
信号量是进程间通信的一种机制,本质上一个计数器,用来指示某个公共资源的可用性!
2、和信号量相关的基本概念
• 多个执行流(进程)看到的同一份资源叫做公共资源
• 任何时候只有一个执行流(进程)在访问公共资源成为互斥
• 被保护起来的公共资源叫做临界资源
• 访问临界资源的代码叫做临界区
3、理解信号量
上面介绍的时候说了,信号量本质就是一个计数器,用来标识某个公共资源的可用性!这是我告诉你的!我们只是记住了,如何理解呢?我们下面就以一个例子来解释:
我们平时去看定影的时候,需要买电影票,先买票的本质就是对座位资源的预定机制!而一个放映厅你不可能说无限给人家出售票!所以就注定了有一个票数的计数器,这个计数器的初始值就是这场电影的总的票数,每当卖出去一张,票数就--,当票数为0时,就不能在出售了!此时,你要想在买就得等上以上的出来再看!也就是你此时只能在外面等待!但是你此时怕被小朋友打扰你看电影的兴致,你就把当场电影的整个电影放映厅给包了,此时其他人都不能访问!都得等着!
上面的这就和临界资源的是类似的!我们也可以将整个临界资源划分成为,很多的小资源,让多个执行流同时并发的访问,此时只需要保证每个执行流在执行时不妨碍其他的执行流即可!而我们每个执行流,在访问小临界资源前,也是和买票一样需要提前申请的!当申请成功临界资源的计数器就--,当临界资源数目为0时,说明没有临界资源可用了!此时当前的执行流就得阻塞等待!等有资源了在访问!所以把这个记录临界资源数目的计数器叫做信号量!像这种多执行流的计数器,叫做多元信号量!把整体使用整个临界资源的这种只有0和1计数器,成为二元信号量!
其实二元信号量就是你把整个放映厅给包了,在一段时间内只能你用!当你用的时候,就是0,当你用完了就是1!他的本质是一把锁(后面线程介绍)!
所以,我们在访问临界资源时,先要申请信号量资源的,而说回来信号量不也是公共资源吗?他也是让多个进程看到的!所以信号量计数也是要保证安全的!而我们说了成功获取一个信号量的本质就是--操作!,而上层语言的--操作,最后都是要被编译成汇编的,而编译成汇编后一条语句会变成多条语句,在运行时会有岁时被切换走的风险!所以,我们上述的--也不是不安全的!如何保证他的安全性呢?其实让他操作是原子的就行,什么意思呢?简单理解就是:你要么别做,要么就一次性全部做完!具体线程了在介绍!
我们把申请信号量的,本质是将计数器--,的这个操作成为P操作;释放信号量即对计数器++的操作成为V操作!一般把申请和释放信号量的操作成为PV操作!PV操作是原子的!
总结:
• 信号量的本质就是一个计数器,使用PV操作,PV操作时原子的;
• 在访问临界资源前,得先申请信号量,得到信号量后才可以访问临界资源!
• 信号量的值为0和1称为,二元信号量,本质是一把锁!
• 申请信号量的是一种对临界资源的预定机制!
4、信号量的相关接口
• semget
作用:申请信号量
参数
• key : 标识信号量的标识符
• nsems : 表示信号量的数量
• semflag :标记位,和共享内存、消息队列的一样
返回值
成功,返回标识符semid; 失败,返回-1, errno被设置
• semctl
作用:释放信号量
参数
• semid : 标识信号量的标识符
• semnum : 表示要操作的信号量的编号
• cmd : 和共享内存、消息队列一样,IPC_RMID,表示删除信号量(常用)
后面的可变参数是,可以传递一个union的联合吗,但是这个几乎用不到,可以不传!
• semop
作用:PV式操作信号量
参数
• semid : 信号量集的标识符,由
semget
函数返回• sembuf : 指向
sembuf
结构体数组的指针,该数组包含了一组要执行的操作• nspos :
sops
数组中sembuf
结构体的数量,即要执行的操作数量
返回值
成功,返回标识符semid; 失败,返回-1, errno被设置
OK,接口就介绍到这里!后面多线程还会在介绍的!
指令操作
查看信号量:ipcs -q
删除信号量:ipcrm -q semid
注意:信号量和共享内存、消息队列一样也是生命周期随内核的!也就是得手动的删除!
四、内核对System V 的管理
同为system V
系列,共享内存 shm
、消息队列 msg
和信号量 sem
是有共性的,操作系统对这三者进行统一的管理。
Linux
中,描述三者的结构体如下:
其中共享内存 shm
被结构体shmid_kernel
管理,消息队列 msg
被结构体msg_quque
管理,信号量 sem
被结构体sem_array
管理。不过以上结构体中,成员并不是完全的,我只截取了一小部分。
ipc_ids
结构体的entires
成员指向了结构体ipc_id_ary
,ipc_id_ary
的第二个成员是一个柔性数组,该数组是一个指针数组,指向了不同的system V
结构体。此时Linux
对system V
的管理就变成了对数组的增删查改。
那么现在有一个问题就是:为什么一个数组可以指向三种不同类型的结构体变量?我们再回到三个描述system V
的结构体:(其实上面我没说,OS提供了ds获取struct kern_ipc_perm
的部分属性的结构体,可以用xxxctl获取,其成员如下:)
结合上面面介绍的就是这样:
我们内核中它们三个结构体的第一个成员分别是shm_perm
、q_perm
和sem_perm
,这三者其实都是同一个结构体类型struct kern_ipc_perm
,而Linux
就是通过这个struct kern_ipc_perm
来同时管理三种结构体的。
ipc_id_ary中,第二个成员数组的类型是struct kern_ipc_perm*,也就是指向struct kern_ipc_perm指针,这个struct kern_ipc_perm存储了三种system V都具有的属性。struct kern_ipc_perm结构体同时也都是三个system V的结构体的第一个成员,因此在访问具体的某个结构体时,只需要进行一次指针的强制类型转换即可。Linux就是通过这样一种方式,把所有的system V都统一地管理了起来。这里又有一个问题,那ipc_id_ary是如何区分是共享内存、消息队列还是信号量的呢?其实在他的struct kern_ipc_perm内部存在一个标记mode就是标记他的!其实上层获取到的xxxid就是ipc_id_ary的下标!如果你学过面向对象的话你就会发现这和我们面向对象的多态简直是神似!其实这就是C语言版的多态!
OK,本期分享就到这里,我是cp期待与你的下一次相遇!
结束语:一入编程深似海,从此代码夜夜改!