目录
[system V 共享内存](#system V 共享内存)
[system V 消息队列(了解)](#system V 消息队列(了解))
[system V 信号量](#system V 信号量)
上篇:Linux进程间通信(IPC)------管道-CSDN博客
https://blog.csdn.net/suimingtao/article/details/156070951?
system机制的进程间通信有三种:共享内存、消息队列、信号量。它们是由内核提供的通信方法
system V 共享内存
通过让不同进程看到同一个内存块的方式,就称为共享内存
原理
通常来讲,因为进程中的地址都是虚拟地址 (进程地址空间),因此A进程申请的空间并不会和B进程申请的空间造成冲突,也就是平常所说的进程独立性

但如果先让操作系统在物理内存申请一块空间 ,再将该空间通过页表映射到进程的地址空间(关联),不同的进程就能看到同一份资源了,映射了这块内存的进程之间就可以进行通信了。当通信结束后,再取消进程和这块内存的映射关系(去关联)并释放内存

这块由操作系统在物理内存申请的空间就是共享内存,将共享内存映射到进程的地址空间的过程称为进程和共享内存关联,当不通信时,通过去关联来解除关系
共享内存是专门设计出来用于进程间通信(IPC)的,它不能通过例如malloc、new创建出来,因为malloc、new出来的地址是虚拟地址 ,无法让其他进程在物理内存中找到
共享内存是一种通信方式,例如有两个进程在通过共享内存的方式进行通信,同时其他进程也可以通过共享内存的方式进行通信,所以OS中肯定会存在很多的共享内存
shmget系统调用
创建和获取共享内存都通过shmget系统调用完成

shmflg:类似于open的flag参数,可选的参数用位图表示 ,可以同时使用多个参数 ,该位置可以用IPC_CREAT 和IPC_EXCL两个参数
- IPC_CREAT表示如果不存在就创建,如果存在就获取
- IPC_EXCL 不能单独使用,如果要使用就必须和I PC_CREAT一起使用,即IPC_CREAT | IPC_EXCL,表示如果不存在就创建,如果存在就出错返回,也就是说如果创建成功,一定是一个新的共享内存
- 除此之外,若是创建共享内存,还需要加上该共享内存段的权限 ,例如IPC_CREAT | IPC_EXCL | 0666
除此之外,若该位置为0,即为默认行为,和 IPC_CREAT 行为一致
size:指定创建的共享内存段的大小,该位置最好写4096的整数倍 。因为共享内存的单位是page,一般来说,一个page为4096字节 ,若写的其他值,例如5000,OS也会为该共享内存分配两个page,即8192个字节的空间,即会向上取整到page的整数倍。
此时若用 ipcs -m 查看,虽然size部分显示的是4097,但OS不会限制我们使用超过4097的数据,我们可以使用全部的8192个字节的空间
在C语言阶段的malloc和free中,我们申请了特定字节数的空间后,free的传参只有该空间的起始地址,是怎么知道我们要释放多少字节的空间的?
当调用malloc(size)时,实际分配的空间比请求的size要大 ,这多出来的空间用于存储分配的大小、权限等信息,这块空间存储的数据叫做cookie数据.
同时,因为malloc的底层依旧是操作系统申请的空间 ,所以它也遵循先描述,再组织,因此这块多出的空间实际就是为创建结构体所分配的空间
cpptypedef struct { size_t size; // 存储分配的大小 // 其他元数据(如用于完整性检查的magic值) } header_t;共享内存最终也是操作系统申请的空间,所以它也需要被管理,因此根据先描述,再组织,创建共享内存时也需要多申请一些空间用于创建结构体,该结构体存的就是该块共享内存的容量、权限等等信息。
所以:共享内存 = 物理内存块 + 共享内存的相关属性
返回值:若成功,返回该共享内存的标识符,若失败,返回-1。未来想对共享内存做操作时,就用该标识符进行操作。该返回值和open返回的fd一样,也是数组下标,只不过它在不同的操作系统中起始位置不一样
key:要想实现进程间通信,就需要保证两个进程看到同一份资源。 ++匿名管道通过继承的方式保证,命名管道通过唯一路径的方式保证++ ,而在共享内存中,则是通过唯一标识key来保证 ,它的作用只是保证唯一性,因此值是多少并不重要。 要想知道key,需要用到ftok函数

该函数通过传一个路径和一个数字,生成一个唯一的key
只要我们两个进程传进的 pathname 、 proj_id 同样,那么返回的key也一样,一个进程用该key创建共享内存,另一个进程用key获取共享内存,就可以看到同一份资源了
现在我们知道,共享内存靠key来保证在系统中的唯一性,只要另一个进程也看到key,就可以找到这段唯一的共享内存
那么,key在哪呢?当通过shmget创建共享内存时,就是将参数key存到了创建共享内存时创建的结构体中,当shmget获取共享内存时,遍历共享内存所对应属性的key和带来的key是否相等
也就是说,key是通过shmget设置进共享内存属性中的,用来标识该共享内存在内核的唯一性
shmid和key都是用于标识唯一性的,那它们两个之间有什么区别呢?
shmid vs key 就类似于 fd vs inode ,两者都用于标识唯一性,本质目的是为了解耦,这样即使C++的底层代码发生变动(key和inode改变),也不会影响到上层的shmid和fd
代码实现
和命名管道的代码实现一样,我们用server.cpp作为创建共享内存的一端,client作为获取共享内存的一端,两端通用的头文件、宏、函数就放在command.hpp中
command.hpp:
cpp
#ifndef __COMM_HPP__//防止重复包含头文件(第一次执行时,没有定义该宏,会定义该宏,第二次因为定义了该宏,就不会执行了)
#define __COMM_HPP__
#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#define PATHNAME "."//这里就举例为设置ftok的路径为当前路径
#define PROJ_ID 0x114//ftok的项目ID这里也是随便设的
#define MAX_SIZE 4096//指定创建的共享内存段的大小
using namespace std;
key_t GetKey()//获取key
{
key_t key = ftok(PATHNAME,PROJ_ID);
if(key == -1)//若Key为-1,代表获取key失败,则终止进程
{
cerr << "ftok错误 --> " << errno << ":" << strerror(errno) << endl;//cerr为标准错误,它不需要经过缓冲区,会直接显示在屏幕上,更快
exit(1);
}
return key;
}
int ShmHelper(key_t key,int shmflg)//通用的创建/获取共享内存
{
int shmid = shmget(key,MAX_SIZE,shmflg);
if(shmid == -1)//若返回-1,则创建/获取失败,则终止进程
{
cerr << "shmget错误 --> " << errno << ":" << strerror(errno) << endl;
exit(2);
}
return shmid;
}
int CreateShm(key_t key)//创建共享内存(若成功,必是一个全新的共享内存)
{
return ShmHelper(key,IPC_CREAT | IPC_EXCL | 0600);//设为只有拥有者可以读写
}
int GetShm(key_t key)//获取共享内存
{
return ShmHelper(key,IPC_CREAT);
}
#endif
server.cpp:
cpp
#include "command.hpp"
int main()
{
key_t key = GetKey();//获取key
int shmid = CreateShm(key);//创建全新的共享内存段
cout << "key:" << key << "\nshmid:" << shmid << endl;
return 0;
}
client.cpp:
cpp
#include "command.hpp"
int main()
{
key_t key = GetKey();//获取key
int shmid = GetShm(key);//获取共享内存段
cout << "key:" << key << "\nshmid:" << shmid << endl;
return 0;
}
运行会发现,它们两进程确实看到了同一份共享内存,但当我们再次重复运行server端时,会报错文件已存在

在管道中,只要进程退出,它的文件描述符会自动释放,即管道文件的生命周期是随进程的。
但共享内存的生命周期并不是随进程的,而是随操作系统的(不止共享内存,只要是system V版本的进程间通信,例如消息队列和信号量,它们的生命周期也都是随操作系统的)
要想查看ststem V版本的进程间通信资源状态,可以用ipcs命令

从上往下,依次是消息队列,共享内存段,信号量
若只想查看其中某个,按照从上往下的顺序可以分别用**-q,-m,-s**选项,这里就以查看共享内存为例

可以看到,即使进程关闭,创建出来的共享内存依然存在,++owner为创建出该共享内存段的用户,perms为该共享内存段的权限,bytes为该共享内存段的大小,nattch为与该共享内存段连接的进程个数++
删除共享内存也有对应的接口,但此时进程已经关闭,需要先从命令行删除该共享内存
在命令行中删除ipc资源可以用ipcrm 命令,若想删共享内存,那就是ipcrm -m ,消息队列和信号量同理。此时要删的是共享内存,因此在后面加上要删除的共享内存的shmid (为什么不是用key呢?key是在内核中标识唯一性的 ,shmid是在应用层标识唯一性的,既然这指令,那一定是在应用层)

shmctl系统调用
要想在进程中删除共享内存,就要用到shmctl系统调用

- shmid即为想操作的共享内存ID
- cmd为操作的类型,可选参数有IPC_STAT , IPC_SET , IPC_RMID ,IPC_INFO , SHM_INFO , SHM_STAT,SHM_LOCK , SHM_UNLOCK 。要想删除共享内存段,就需要用到IPC_RMID(rm为删除的意思),将共享内存立即移除。除此之外,IPC_STAT可以获取制定共享内存段的信息,将信息传到buf中
- buf可以当作一个输出型参数,若我们的cmd是IPC_STAT,就可以在buf中传一个struct shmid_ds类型的结构体,将指定共享内存段的信息都写入进去。该结构体是操作系统暴露给用户的共享内存的一部分数据结构,操作系统管理共享内存的结构比它复杂得多
cpp
struct shmid_ds
{
struct ipc_perm shm_perm; /* 所有权和权限 */
size_t shm_segsz; /* 段的大小(字节) */
time_t shm_atime; /* 最后附加时间(进程调用 shmat 的时间) */
time_t shm_dtime; /* 最后分离时间(进程调用 shmdt 的时间) */
time_t shm_ctime; /* 最后修改时间(如权限或大小变更) */
pid_t shm_cpid; /* 创建者的进程 ID */
pid_t shm_lpid; /* 最后操作的进程 ID(shmat/shmdt 调用者) */
shmatt_t shm_nattch; /* 当前附加进程数 */
...
};
struct ipc_perm
{
key_t __key; /* 传入 shmget(2) 的键值 */
uid_t uid; /* 所有者的有效用户 ID */
gid_t gid; /* 所有者的有效组 ID */
uid_t cuid; /* 创建者的有效用户 ID */
gid_t cgid; /* 创建者的有效组 ID */
unsigned short mode; /* 权限位(如 0666) + SHM_DEST 和 SHM_LOCKED 标志 */
unsigned short __seq; /* 序列号(用于 IPC 内部管理) */
};
但如果我们的cmd是IPC_RMID,则不需要传buf,设为nullptr即可
该函数若失败,返回值为-1
cpp
void DelShm(int shmid)//删除共享内存
{
if(shmctl(shmid,IPC_RMID,nullptr) == -1)
cerr << "shmctl错误 --> " << errno << ":" << strerror(errno) << endl;
}
现在我们将server端加上删除共享内存的部分,并在删除前sleep5秒代表使用,就可以看到共享内存在ipcs -m中的变化
cpp
#include "command.hpp"
int main()
{
key_t key = GetKey();//获取key
int shmid = CreateShm(key);//创建全新的共享内存段
cout << "key:" << key << "\nshmid:" << shmid << endl;
sleep(5);
DelShm(shmid);
return 0;
}
运行的前5秒:

进程结束后:

shmat系统调用
现在我们只是将共享内存创建出来了,还不能使用。在此之前,需要现将共享内存与进程关联起来,就需要用到shmat(at为attach)系统调用,它可以将指定的共享内存段与该进程的进程地址空间连接起来

- shmid即要连接的共享内存id
- shmaddr为想要映射到的地址空间,一般来说不需要设定,即传nullptr,除非你真的有需要指定映射地址的需求
- shmflg的设置跟读写权限有关,一般设置为0,表示读写权限都有
它的返回值就是该共享内存映射到进程地址空间的起始地址,若连接失败,返回-1
cpp
void* AttachShm(int shmid)//连接共享内存
{
void* addr = shmat(shmid,nullptr,0);
if((long long)addr == -1LL)
{
cerr << "shmat错误 --> " << errno << ":" << strerror(errno) << endl;
DelShm(shmid);
exit(4);
}
return addr;
}
再在server端和client端加入连接共享内存的代码
server.cpp:
cpp
#include "command.hpp"
int main()
{
key_t key = GetKey();//获取key
int shmid = CreateShm(key);//创建全新的共享内存段
cout << "key:" << key << "\nshmid:" << shmid << endl;
sleep(5);
void* addr = AttachShm(shmid);//连接共享内存
cout << "共享内存起始地址:" << addr << endl;
sleep(5);
DelShm(shmid);//删除共享内存
return 0;
}
client.cpp:
cpp
#include "command.hpp"
int main()
{
key_t key = GetKey();//获取key
int shmid = GetShm(key);//获取共享内存段
cout << "key:" << key << "\nshmid:" << shmid << endl;
sleep(5);
void* addr = AttachShm(shmid);//连接共享内存
cout << "共享内存起始地址:" << addr << endl;
sleep(5);
return 0;
}
都运行起来后,该共享内存的连接数先是0

再是1

再是2

再是1,此时是client端结束运行了

再删除共享内存

shmdt系统调用
但是在共享内存与进程关联时直接删除不太好,可以先去关联,即修改页表,回收进程地址空间中的虚拟地址,当nattch为0时再删除
去关联共享内存就需要用到shmdt系统调用(dt为detach的意思)

参数为要去关联的共享内存的起始地址,即shmat的返回值。成功返回0,失败返回-1
cpp
void DetachShm(void* addr)//去关联共享内存
{
if(shmdt(addr) == -1)
cerr << "shmdt错误: " << errno << strerror(errno) << endl;
}
完善server和client:
cpp
//server.cpp:
#include "command.hpp"
int main()
{
key_t key = GetKey();//获取key
int shmid = CreateShm(key);//创建全新的共享内存段
void* addr = AttachShm(shmid);//关联共享内存
//通信
DetachShm(addr);//去关联共享内存
DelShm(shmid);//删除共享内存
return 0;
}
//client.cpp:
#include "command.hpp"
int main()
{
key_t key = GetKey();//获取key
int shmid = GetShm(key);//获取共享内存段
void* addr = AttachShm(shmid);//关联共享内存
//通信
DetachShm(addr);//去关联
return 0;
}
到现在为止,就完成了通信前的所有准备工作,可以正式开始通信了
共享内存没有专门通信的接口,也不需要,它可以直接当作char*字符串使用
完整代码:
cpp
//server.cpp:
#include "command.hpp"
int main()
{
key_t key = GetKey();//获取key
int shmid = CreateShm(key);//创建全新的共享内存段
void* addr = AttachShm(shmid);//关联共享内存
//通信
while(true)
{
//共享内存可以当作char*字符串使用,因此可以直接用%s输出
printf("client: %s\n",addr);
sleep(1);
}
DetachShm(addr);//去关联共享内存
DelShm(shmid);//删除共享内存
return 0;
}
//client.cpp:
#include "command.hpp"
int main()
{
key_t key = GetKey();//获取key
int shmid = GetShm(key);//获取共享内存段
char* addr = (char*)AttachShm(shmid);//关联共享内存,强转为char*方便后面用snprintf通信
//通信过程
int pid = getpid();
int cnt = 1;
while(true)
{
//共享内存可以直接当作char*类型的字符串使用,因此这里可以直接用snprintf写入
snprintf(addr,MAX_SIZE,"你好server,我是client,这是我发的第%d条消息[mypid:%d]",cnt++,pid);
sleep(1);
}
DetachShm(addr);//去关联
return 0;
}
command.cpp:
cpp
#ifndef __COMM_HPP__//防止重复包含头文件(第一次执行时,没有定义该宏,会定义该宏,第二次因为定义了该宏,就不会执行了)
#define __COMM_HPP__
#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#define PATHNAME "."//这里就举例为设置ftok的路径为当前路径
#define PROJ_ID 0x114//ftok的项目ID这里也是随便设的
#define MAX_SIZE 4096//指定创建的共享内存段的大小
using namespace std;
key_t GetKey()//获取key
{
key_t key = ftok(PATHNAME,PROJ_ID);
if(key == -1)//若Key为-1,代表获取key失败,则终止进程
{
cerr << "ftok错误 --> " << errno << ":" << strerror(errno) << endl;//cerr为标准错误,它不需要经过缓冲区,会直接显示在屏幕上,更快
exit(1);
}
return key;
}
int ShmHelper(key_t key,int shmflg)//通用的创建/获取共享内存
{
int shmid = shmget(key,MAX_SIZE,shmflg);
if(shmid == -1)//若返回-1,则创建/获取失败,则终止进程
{
cerr << "shmget错误 --> " << errno << ":" << strerror(errno) << endl;
exit(2);
}
return shmid;
}
int CreateShm(key_t key)//创建共享内存(若成功,必是一个全新的共享内存)
{
return ShmHelper(key,IPC_CREAT | IPC_EXCL | 0600);//设为只有拥有者可以读写
}
int GetShm(key_t key)//获取共享内存
{
return ShmHelper(key,IPC_CREAT);
}
void DelShm(int shmid)//删除共享内存
{
if(shmctl(shmid,IPC_RMID,nullptr) == -1)
cerr << "shmctl错误 --> " << errno << ":" << strerror(errno) << endl;
}
void* AttachShm(int shmid)//关联共享内存
{
void* addr = shmat(shmid,nullptr,0);
if((long long)addr == -1LL)
{
cerr << "shmat错误 --> " << errno << ":" << strerror(errno) << endl;
DelShm(shmid);
exit(4);
}
return addr;
}
void DetachShm(void* addr)//去关联共享内存
{
if(shmdt(addr) == -1)
cerr << "shmdt错误: " << errno << strerror(errno) << endl;
}
#endif

前三行为空是因为client还没启动
共享内存特点(优缺点):
共享内存的生命周期随OS,这一点上面已经说过了,就不赘述了
优点:共享内存是最快的IPC方式。它能大大减少数据拷贝的次数
在用别的IPC方式时,例如管道,写端需要先将数据拷贝到buffer[]中 ,再将buffer拷贝到管道中 ,读端再将管道中的数据拷贝到buffer中 ,再将buffer中的数据拷贝到屏幕上,共4次拷贝

但在共享内存中,写端可以将数据直接拷贝到共享内存中 ,读端再将共享内存的数据拷贝到显示器上,共2次拷贝

但键盘输入的数据会先存到输入缓冲区 中,再传给buffer[]/共享内存;同理,输出到显示器的数据也会先存到输出缓冲区,再给显示器。若把这两次拷贝加上,管道共4+2=6次拷贝,共享内存共2+2=4次拷贝
缺点:不会进行同步与互斥操作,没有对数据做任何保护
当前程序是写端每隔一秒发送一次,读端每隔一秒接收一次,若此时将写端改为每隔5秒发送一次

会发现每5秒读端读取的都是一条消息,若是管道,当写端没有写入时,读端会阻塞等待。
并且当我们只启动了server端时,也会一直输出,不会像管道一样等待写端启动
因此可以再将代码完善一下,实现只有当写端写入时,通知读端读取,否则让server阻塞等待
这里可以利用管道的同步与互斥。写端每次向共享内存写入后,就向管道中发送一条消息;读端只有当从管道中读到消息后,再去共享内存中读取

完善同步与互斥的共享内存通信(管道实现):
cpp
//server.cpp:
#include "command.hpp"
int main()
{
bool flg = CreatePipe();//创建命名管道
assert(flg);
(void)flg;
int fd = open(PIPE_PATH,O_RDONLY);//只读打开命名管道
if(fd == -1)
{
cerr << "open错误 --> " << errno << ":" << strerror(errno) << endl;
exit(6);
}
key_t key = GetKey();//获取key
int shmid = CreateShm(key);//创建全新的共享内存段
void* addr = AttachShm(shmid);//关联共享内存
//通信
while(true)
{
//管道
if(PipeRead(fd,shmid) == false)//只有当写端向管道发送信息后,才允许server从共享内存中读取
break;//若写端退出,则结束server端
//共享内存可以当作char*字符串使用,因此可以直接用%s输出
printf("client: %s\n",addr);
sleep(1);
}
RemovePipe();
DetachShm(addr);//去关联共享内存
DelShm(shmid);//删除共享内存
return 0;
}
//client.cpp:
#include "command.hpp"
int main()
{
int fd = open(PIPE_PATH,O_WRONLY);//只写打开管道文件
if(fd == -1)
{
cerr << "open错误 --> " << errno << ":" << strerror(errno) << endl;
exit(5);
}
sleep(1);//这里是为了让server先运行,将共享内存创建工作做完,否则因为管道有同步功能,只有双方都打开管道文件时才会继续运行,会导致client端试图创建共享内存
key_t key = GetKey();//获取key
int shmid = GetShm(key);//获取共享内存段
char* addr = (char*)AttachShm(shmid);//关联共享内存,强转为char*方便后面用snprintf通信
//通信过程
int pid = getpid();
int cnt = 1;
while(true)
{
//共享内存可以直接当作char*类型的字符串使用,因此这里可以直接用snprintf写入
snprintf(addr,MAX_SIZE,"你好server,我是client,这是我发的第%d条消息[mypid:%d]",cnt++,pid);
//管道
write(fd,"1",sizeof(1));//每次向共享内存中写入,都会向管道发送一条消息
sleep(5);//写端每5秒写一次,读端每1秒读一次
}
DetachShm(addr);//去关联
return 0;
}
comand.hpp:
cpp
#ifndef __COMM_HPP__//防止重复包含头文件(第一次执行时,没有定义该宏,会定义该宏,第二次因为定义了该宏,就不会执行了)
#define __COMM_HPP__
#include <iostream>
#include <cstring>
#include <cassert>
#include <cerrno>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define PATHNAME "."//这里就举例为设置ftok的路径为当前路径
#define PIPE_PATH "/tmp/mypipe"
#define PROJ_ID 0x114//ftok的项目ID这里也是随便设的
#define MAX_SIZE 4096//指定创建的共享内存段的大小
using namespace std;
key_t GetKey()//获取key
{
key_t key = ftok(PATHNAME,PROJ_ID);
if(key == -1)//若Key为-1,代表获取key失败,则终止进程
{
cerr << "ftok错误 --> " << errno << ":" << strerror(errno) << endl;//cerr为标准错误,它不需要经过缓冲区,会直接显示在屏幕上,更快
exit(1);
}
return key;
}
int ShmHelper(key_t key,int shmflg)//通用的创建/获取共享内存
{
int shmid = shmget(key,MAX_SIZE,shmflg);
if(shmid == -1)//若返回-1,则创建/获取失败,则终止进程
{
cerr << "shmget错误 --> " << errno << ":" << strerror(errno) << endl;
exit(2);
}
return shmid;
}
int CreateShm(key_t key)//创建共享内存(若成功,必是一个全新的共享内存)
{
return ShmHelper(key,IPC_CREAT | 0600);//设为只有拥有者可以读写
}
int GetShm(key_t key)//获取共享内存
{
return ShmHelper(key,IPC_CREAT);
}
void DelShm(int shmid)//删除共享内存
{
if(shmctl(shmid,IPC_RMID,nullptr) == -1)
cerr << "shmctl错误 --> " << errno << ":" << strerror(errno) << endl;
}
void* AttachShm(int shmid)//关联共享内存
{
void* addr = shmat(shmid,nullptr,0);
if((long long)addr == -1LL)
{
cerr << "shmat错误 --> " << errno << ":" << strerror(errno) << endl;
DelShm(shmid);
exit(4);
}
return addr;
}
void DetachShm(void* addr)//去关联共享内存
{
if(shmdt(addr) == -1)
cerr << "shmdt错误: " << errno << strerror(errno) << endl;
}
bool CreatePipe()//创建命名管道
{
int fd = mkfifo(PIPE_PATH,0600);
if(fd == -1)
{
cerr << "mkfifo错误 --> " << errno << ":" << strerror(errno) << endl;
return true;
}
return true;
}
void RemovePipe()//删除命名管道
{
int n = unlink(PIPE_PATH);
assert(n == 0);
(void)n;
}
bool PipeRead(int fd,int shmid)//读端的管道验证
{
char buf[4];
int n = read(fd,buf,sizeof(buf));
if(n == 0)
{
cout << "写端退出" << endl;
return false;//若写端退出,则返回false,让写端一起退出
}
if(n < 0)
{
cerr << "read错误 --> " << errno << ":" << strerror(errno) << endl;
RemovePipe();
DelShm(shmid);
exit(7);
}
return true;
}
#endif
再运行时,只有当读写全开时,才会开始读取,并且即使写端每5秒写一次,读端每1秒读一次,也不会读取陈旧信息,且写端关闭时读端会自动退出

共享内存的内核结构
在介绍shmctl系统调用时,简单说了一下它的第三个参数shmid_ds结构体
cpp
struct shmid_ds
{
struct ipc_perm shm_perm; /* 所有权和权限 */
size_t shm_segsz; /* 段的大小(字节) */
time_t shm_atime; /* 最后附加时间(进程调用 shmat 的时间) */
time_t shm_dtime; /* 最后分离时间(进程调用 shmdt 的时间) */
time_t shm_ctime; /* 最后修改时间(如权限或大小变更) */
pid_t shm_cpid; /* 创建者的进程 ID */
pid_t shm_lpid; /* 最后操作的进程 ID(shmat/shmdt 调用者) */
shmatt_t shm_nattch; /* 当前附加进程数 */
...
};
struct ipc_perm
{
key_t __key; /* 传入 shmget(2) 的键值 */
uid_t uid; /* 所有者的有效用户 ID */
gid_t gid; /* 所有者的有效组 ID */
uid_t cuid; /* 创建者的有效用户 ID */
gid_t cgid; /* 创建者的有效组 ID */
unsigned short mode; /* 权限位(如 0666) + SHM_DEST 和 SHM_LOCKED 标志 */
unsigned short __seq; /* 序列号(用于 IPC 内部管理) */
};
现在我们就试着读取一下共享内存的各种属性
cpp
#include "command.hpp"
int main()
{
key_t key = GetKey();//获取key
int shmid = CreateShm(key);//创建全新的共享内存段
void* addr = AttachShm(shmid);//关联共享内存
struct shmid_ds buffer;//输出型参数
shmctl(shmid,IPC_STAT,&buffer);
cout << "段大小:" << buffer.shm_segsz << endl << "key值:" << buffer.shm_perm.__key << endl;
DetachShm(addr);//去关联共享内存
DelShm(shmid);//删除共享内存
return 0;
}

system V 消息队列(了解)
消息队列是操作系统提供的内核级队列 ,它允许两端同时进行读写
消息队列中的数据是一个个的节点,每个节点是一个结构体,但我们只需要关注该节点的两个字段:
cpp
struct
{
//....
int type;//节点类型
char buffer[];//发送的数据
//....
}
通过type字段,可以知道该节点是谁发送的 ,例如一端发送的节点type为1,另一端发送的节点type为0,那么就可以知道哪些节点是某一端应该接收的

系统调用
想要创建或获取消息队列,需要用到 msgget系统调用

- 第一个参数key相信大家并不陌生,并且作用也和大家想的一样
- 第二个参数msgflg也有些眼熟,因为共享内存有shmflg,并且它们的用法也一致,都只有IPC_CREAT 和 IPC_EXCL 两个参数
返回值即返回类似于shmid的msqid
想要删除消息队列,需要用到 msgctl系统调用

- msqid即为消息队列的编号,和shmid作用一样
- cmd的参数为想对消息队列做的操作,最常用的也是 IPC_RMID (删除),除此之外,若设置IPC_STAT ,也可以通过给第三个参数buf传一个msqid_ds结构体当作输出型参数获取消息队列的信息
cpp
struct msqid_ds {
struct ipc_perm msg_perm; /* 所有权和权限 */
time_t msg_stime; /* 最后一次 msgsnd(2) 的时间 */
time_t msg_rtime; /* 最后一次 msgrcv(2) 的时间 */
time_t msg_ctime; /* 最后一次修改的时间 */
unsigned long __msg_cbytes; /* 队列中当前字节数(非标准扩展) */
msgqnum_t msg_qnum; /* 队列中当前消息数 */
msglen_t msg_qbytes; /* 队列允许的最大字节数 */
pid_t msg_lspid; /* 最后一次 msgsnd(2) 的进程 PID */
pid_t msg_lrpid; /* 最后一次 msgrcv(2) 的进程 PID */
};
struct ipc_perm {
key_t __key; /* 提供给 msgget(2) 的键值 */
uid_t uid; /* 所有者的有效用户 ID (UID) */
gid_t gid; /* 所有者的有效组 ID (GID) */
uid_t cuid; /* 创建者的有效用户 ID (UID) */
gid_t cgid; /* 创建者的有效组 ID (GID) */
unsigned short mode; /* 访问权限(如读/写权限标志) */
unsigned short __seq; /* 内部序列号 */
};
消息队列发送数据(写入)的接口为 msgsnd 系统调用(snd为send的意思)

- msqid即消息队列编号
- msqp即为发送的数据块,该数据块是一个结构体,里面包含了数据块的类型和数据
cpp
struct msgbuf
{
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
msgsz为数据块的大小,msgflg一般设为0
与发送数据相对应的就是接收数据(读取),为msgrcv系统调用

只是比发送数据的接口多了一个数据块类型参数msgtyp,可以指定接收什么类型的数据
system V 信号量
信号量本质上是一个计数器,用来表示公共资源中,资源数量的多少(公共资源即为可以被多个进程同时访问的资源)
在之前的共享内存中,它没有对数据的保护 ,就可能会造成数据不一致(同一份数据在不同地方的存储或表示不一致,导致不同用户或系统看到的结果不同,例如多个用户同时修改一份数据)
被保护起来的资源被称为临界资源 ,访问临界资源的代码部分被称为临界区 ,其他代码部分被称为非临界区
要想将公共资源保护起来,就需要同步或互斥 ,而信号量就可以实现。互斥:当有两个进程想访问同一份公共资源时,只允许一个人去访问,只有当一个进程访问结束后,才允许另一个进程访问
若一个操作只有两态,这种情况称为原子性。例如转账系统,要么不转,要转就必须成功,如果转到一半,已经将自己的钱扣出去了,但因为网络问题对方没有收到钱,就必须将钱再加回来,
而信号量就可以让多进程实现原子性的同步与互斥。
虽然信号量本质是一个计数器,但并不是随便一个计数器都能被多个进程所看到。要想让多个进程看到同一个计数器,就需要先让多个进程看到同一份资源,因此信号量也被划分到了进程间通信的范畴
例如,在电影院中,并不是只要坐在座位上,这个座位就是你的,而是需要通过买票,票上有自己的座位号,当你买票成功的那一刻,这个座位就已经属于你了,即使你不去看这场电影,这个座位也不会有人坐。因此电影买票的本质是对电影院中的座位的预订机制
并且,若一个电影院只有100个座位,对应的有100张票,那么绝对不能售出第101张票
因此可以定义一个计数器int count = 100; 每次有人买票成功后,将count--; 当count == 0 时,就不能再售票了,
共享资源分为两种情况:1.作为一个整体去使用 2.划分为一个一个的资源子部分
例如管道就是作为整体去使用的例子,当有进程在访问管道时,别的进程就不能访问,只有当该进程访问结束后才可以访问
但有时需要多个进程并行访问,A访问该共享资源的1部分,B访问该共享资源的2部分,这时就是将公共资源划分为资源子部分去使用,电影院就是很好的例子,将整个放映厅想象成一块共享资源,每个座位就是一个子部分,人相当于进程,每个进程想要访问资源子部分都需要预订,预订就需要通过信号量来完成
cpp
int sem = 40;
//所有进程想访问公共资源,必须先将信号量-- P操作
sem--;
//当访问结束后,必须将信号量++ V操作
sem++;
进程访问信号量让它--的操作叫做P操作
访问结束后让信号量++的操作叫做V操作
由于信号量也是公共资源,它也需要保证自己的安全,因此它的++、--操作也是原子性的
++若一个信号量的初始值是1,就代表这块资源是被整体使用,否则就是被划分为子部分使用++
创建/获取信号量,需要用到semget系统调用

- key的作用和共享内存、消息队列中的key一样
- nsems为想要创建的信号量的个数,该系统调用允许一次性创建多个信号量,用数组下标的方式表示
- semflg,和前面介绍的一样,都是只有IPC_CREAT 和IPC_EXCL两个选项
返回值是semid,信号量编号
如果想删除信号量,就要用到semctl系统调用

- semid即为信号量编号
- semnum,如果在创建信号量时创建了多个信号量,该参数就是用于填数组下标的,如果只创建了一个,就写0
- cmd即为想对信号量做的操作,和共享内存、消息队列同样,IPC_RMID 为删除。如果是IPC_STAT,即为获取信号量的信息,需要填第四个参数
第四个参数是可变参数,它需要传一个semnu联合体的实例 ,注意这里和前面的共享内存和消息队列就有些许不一样了,这里传的是一个实例,不是指针 ,而这个semnu联合体里,有一个指针变量,类似于共享内存、消息队列的结构体指针参数
cpp
// 联合体semun用于semctl系统调用的参数传递
union semun {
int val; /* 用于SETVAL命令设置信号量值 */
struct semid_ds *buf; /* 用于IPC_STAT/IPC_SET命令的缓冲区指针 */
unsigned short *array; /* 用于GETALL/SETALL命令的数组指针 */
struct seminfo *__buf; /* 用于IPC_INFO命令的缓冲区指针
(Linux特有功能) */
};
// 信号量集合状态结构体
struct semid_ds {
struct ipc_perm sem_perm; /* 信号量集的权限和所有权信息 */
time_t sem_otime; /* 最后一次semop操作的时间戳 */
time_t sem_ctime; /* 最后一次状态修改的时间戳 */
unsigned long sem_nsems; /* 信号量集中包含的信号量数量 */
};
// IPC对象权限结构体
struct ipc_perm {
key_t __key; /* 通过semget(2)传入的键值 */
uid_t uid; /* 所有者的有效用户ID */
gid_t gid; /* 所有者的有效组ID */
uid_t cuid; /* 创建者的有效用户ID */
gid_t cgid; /* 创建者的有效组ID */
unsigned short mode; /* 权限标志位(低9位有效) */
unsigned short __seq; /* 序列号(用于内部管理) */
};
若想获取信息,可以如下定义:
cpp
union semnu s;
struct semid_ds sd;
s.buf = &sd;//创建一个semnu变量,指向一个semid_ds变量
semctl(semid,0, IPC_STAT,s);
cout << s.buf->sem_nsems;//查看信息
要想对信号量进行PV操作(++、--),就要用到semop系统调用(op为option的意思)

- semid即消息量编号
- sops是一个sembuf类型的结构体指针:
cpp
unsigned short sem_num; /* 信号量编号 /
short sem_op; / 信号量操作 /
short sem_flg; / 操作标志 */
- sem_num,若一次创建了多个信号量,该位置就是操作信号量的下标,若只创建了一个信号量,则为0
- sem_op,一般有两个值,-1代表--操作,即P操作,1代表++操作,即V操作
- sem_flg一般设为0
- nsops为信号量的个数,该系统调用允许同时对多个信号量进行PV操作,如果是多个信号量,sops就是一个数组,每个元素说明了每个信号量要进行的操作
IPC资源的组织方式
现在再回顾共享内存、消息队列、信号量这三个IPC方式,会发现它们三者的接口都高度相似 ,并且它们暴露给用户的内核结构体也高度相似,它们的结构体中的第一个元素都是struct ipc_perm类型

实际上,操作系统会维护一个指针数组struct ipc_perm *perms[],当要创建共享内存/消息队列/信号量时,就会把它的struct ipc_perm类型字段的地址存入该数组

并且,在C语言中,结构体第一个成员的地址和结构体对象本身的地址,在值上来说是相等的(意义不一样)
那么如果将来需要访问myshm中的资源,就可以直接将该数组的值强转成对应类型,就可以访问该资源了
cpp
(struct shmid_ds*)perms[0] ->【字段】;
这种思路和C++的多态很像,当然,是爸爸像儿子: )