🌈 个人主页:Zfox_
🔥 系列专栏:Linux
目录
- [一:🔥 System V 共享内存](#一:🔥 System V 共享内存)
-
- [🦋 共享内存的原理](#🦋 共享内存的原理)
- [二:🔥 共享内存通信代码](#二:🔥 共享内存通信代码)
-
- [🦋 系统调用接口介绍:](#🦋 系统调用接口介绍:)
- [🦋 使用共享内存通信](#🦋 使用共享内存通信)
- [🦋 共享内存通信优缺点](#🦋 共享内存通信优缺点)
- [三:🔥 System V 消息队列(了解)](#三:🔥 System V 消息队列(了解))
- [四:🔥 System V 信号量(了解)](#四:🔥 System V 信号量(了解))
-
- [🦋 信号量操作(PV操作)](#🦋 信号量操作(PV操作))
- [五:🔥 IPC的理解](#五:🔥 IPC的理解)
-
- [🦋 用户角度](#🦋 用户角度)
- [🦋 内核角度](#🦋 内核角度)
- [六:🔥 共勉](#六:🔥 共勉)
一:🔥 System V 共享内存
共享内存是System V进程通信中的一种方式,是本地通信
🦋 共享内存的原理
\qquad 🦁 进程在进行动态库加载时,动态库会通过页表映射到进程地址空间的共享区中。如果有多个进程要加载同一个动态库,动态库加载到内存后会被这些进程共同使用。
所以根据动态库加载的原理,操作系统可以在内存中创建一个共享内存空间,再通过页表映射到两个进程的共享区中,这样两个进程就可以看到同一份资源了。
操作系统可以为进程通信创造通信条件,但是什么时候通信是取决于进程,所以操作系统必须提供共享内存通信相应的系统调用接口。
- 操作系统中会有多个进程使用共享内存通信,所以会有多个共享内存空间,这就需要操作系统统一管理这些共享内存空间。因此就有了内核数据结构
struct Shm
来管理共享内存通信。
二:🔥 共享内存通信代码
🦋 系统调用接口介绍:
cpp
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
- 用于创建获取共享内存,
key_t key
是一个键值,用于唯一标识共享内存段,由用户自己给值确定,通常使用ftok
函数随机定值;size_t size
是需要分配的共享内存段的大小(以字节为单位);int shmflg
是标志位,用于控制共享内存段的创建和访问权限。
常见标志位:
IPC_CREAT
如果要获取的共享内存不存在则创建,如果存在则获取并返回 (主要用于获取共享内存)IPC_EXCL
单独使用无意义,通常IPC_CREAT | IPC_EXCL
如果获取的共享内存不存在则创建,如果存在则报错 (主要用于创建共享内存)
返回值:
- 成功返回共享内存标识符
(非负整数shmid)
; - 失败返回 -1,并设置errno来指示错误
\qquad 🎯 到这里一直有一个疑问,为什么共享内存的 key 值要用户传递?内核自动生成不香吗?
既然不能让内核生成,那就只能自己创建,并且让这两个进程都能看到。
但是让用户自己设定一个又不好,因为既没有一定的规律,又可能出现大量重复的key,然后导致创建shm失败。
- 为了解决上述问题,系统提供了一个专门用来生成
key
的函数 :ftok
\qquad
cpp
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id)
- 用于根据文件的属性(如inode编号)生成一个唯一的键值,
const char *pathname
是文件路径名,指向系统中的一个现有文件或目录(任意路径,通信进程使用相同的路径即可);int proj_id
是项目标识符,通常为一个字符或整数(任意字符或整数)。即相同路径和项目标识符生成的唯一键值是相同的。
返回值:成功返回 key 唯一键值;失败返回 -1,并设置 errno 来指示错误
\qquad
🦁 系统命令 ipcs -m
查看已存在的共享内存
🦁 系统命令 ipcrm -m 共享内存标识符
删除指定的共享内存,共享内存和文件不同,不会随着进程结束而结束,而是一直在内存中,只能手动删除
区分共享内存唯一键值 key 和标识符 shmid:
- 唯一键值
key
是提供给操作系统用来创建共享内存的,操作系统创建好共享内存后返回的共享内存标识符shmid
是用来给用户管理共享内存的。
\qquad - 删除一个共享内存的系统调用:shmctl
cpp
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
删除指定的共享内存
int shmid
是指定的共享内存标识符,int cmd
是要操作的命令,struct shmid_ds *buf
是这是一个指向 shmid_ds 结构的指针,该结构包含了共享内存段的详细信息。根据 cmd 的值,这个参数可以是输入(用于 IPC_SET)或输出(用于 IPC_STAT)的。
删除 cmd 用到的命令是 IPC_RMID
cpp
void DeleteShm()
{
// 3. 删除共享内存
::shmctl(_shmid, IPC_RMID, nullptr);
}
IPC_RMID
:立即删除共享内存段。这个操作只能由共享内存的创建者或拥有适当权限的进程执行。
\qquad
- 挂接一个共享内存的系统调用:shmat (at:attach)
cpp
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg)
shmat
函数在Linux系统中用于将共享内存挂接到当前进程的地址空间中。
-
int sgmid
是共享内存的标识符; -
const void *shmaddr
是指定共享内存连接到当前进程的地址空间的起始地址,如果这个参数为NULL,系统会自动选择一个合适的地址; -
int shmflg
是指定连接共享内存的权限标志,常用的权限标志有SHM_RDONLY(只读连接),其他情况默认为读写模式(参数传0)。 -
返回值:成功返回共享内存的起始地址;失败返回 -1,并将错误原因存于 errno 中。
\qquad
- 去关联(取消挂接)一个共享内存的系统调用:shmdt (dt:delete attach)
cpp
#include <sys/shm.h>
int shmdt(const void *shmaddr)
shmdt
函数在Linux系统中用于将进程和共享内存断开连接。
const void *shmaddr
是当前进程地址空间中共享内存段的起始地址。
- 返回值:成功时返回 0;失败时返回 -1,并设置相应的 errno。
🦋 使用共享内存通信
要想使用共享内存通信,两个进程,进程1先创建 shm && 使用;进程2 获取shm && 使用。然后一个进程向所挂接的内存中写,另一个读即可完成通信。
因此,可以将共享内存专门抽离作为一个类。
然后创建全局共享内存的对象,以便进程都能看到
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "Time.hpp"
const std::string gpath = "/root/code"; // 必须是存在的路径
int gprojId = 0x6666;
int gshmsize = 4096;
mode_t gmode = 0600;
std::string ToHex(key_t k)
{
char buffer[64];
snprintf(buffer, sizeof(buffer), "0x%x", k);
return buffer;
}
class ShareMemory
{
private:
void CreateShmHelper(int shmflg)
{
// 1. 创建key
_key = ::ftok(gpath.c_str(), gprojId);
if (_key < 0)
{
std::cerr << "ftok error" << std::endl;
return ;
}
// 2. 创建共享内存 && 获取
// 注意:共享内存也有权限!
_shmid = ::shmget(_key, gshmsize, shmflg);
if (_shmid < 0)
{
std::cerr << "shmget error" << std::endl;
return ;
}
std::cout << "shmid : " << _shmid << std::endl;
}
public:
ShareMemory()
:_shmid(-1)
,_key(0)
,_addr(nullptr)
{}
~ShareMemory() {}
void CreateShm()
{
if(_shmid == -1)
CreateShmHelper(IPC_CREAT | IPC_EXCL | gmode);
}
void GetShm()
{
CreateShmHelper(IPC_CREAT);
}
void AttachShm()
{
// 3.共享内存挂接到自己的地址空间中
_addr = ::shmat(_shmid, nullptr, 0);
if ((long long)_addr == -1)
{
std::cout << "attach error" << std::endl;
return ;
}
}
void DetachShm()
{
// detach
if(_addr != nullptr)
::shmdt(_addr);
std::cout << "detach done" << std::endl;
}
void DeleteShm()
{
// 3. 删除共享内存
::shmctl(_shmid, IPC_RMID, nullptr);
}
void *GetAddr()
{
return _addr;
}
void ShmMeta()
{
struct shmid_ds buffer; // 系统提供的数据类型
int n = ::shmctl(_shmid, IPC_STAT, &buffer);
if(n < 0) return ;
std::cout << "############################" << std::endl;
std::cout << buffer.shm_atime << std::endl;
std::cout << buffer.shm_cpid << std::endl;
std::cout << buffer.shm_ctime << std::endl;
std::cout << buffer.shm_nattch << std::endl;
std::cout << buffer.shm_perm.__key << std::endl;
}
private:
int _shmid;
key_t _key;
void *_addr;
};
ShareMemory shm;
struct data
{
char status[32];
char lasttime[48];
char image[4000];
};
🦋 共享内存通信优缺点
缺点:共享内存没有任何保护机制,客户端向共享内存中写数据时,还没有写完,服务端就会从共享内存中读取数据,导致数据不一致问题。如下所示,服务端总是读出相同的数据,其实是客户端只完成了一次写入,服务端就已经读了两次。
优点:访问共享内存时没有任何系统调用,因为共享内存被映射到了进程地址空间的共享区,其是用户级别的,不需要将数据拷贝到管道中再拷贝回进程地址空间中,大大减少了拷贝次数,所以共享内存通信速度最快。
三:🔥 System V 消息队列(了解)
1. 消息队列提供了进程间发送数据块的方法,每个数据块都有一个类型标识。
2. 消息队列基于消息,而管道则基于字节流。
3. 一个或多个进程可以向消息队列写入消息,而一个或多个进程可以从消息队列中读取消息。
认识消息队列相关的方法:
msgget
:获取消息队列
cpp
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflag);
ipcs -q
:查看消息队列的指令ipcrm -q + id
:删除消息队列指令
msgctl
:消息队列删除的系统调用
msgsnd
:发送消息msgrcv
:接收消息
由于消息具有类型,那么在接收的时候就可以接收指定类型的消息了。
🦁 经过上述的学习,我们发现它的接口与共享内存非常的相似,因为它们都遵循System V标准。
四:🔥 System V 信号量(了解)
前提知识:
- 共享资源:可以被多个进程访问的资源
- 临界资源:在系统中被多个进程共享,但在任一时刻只允许一个进程使用的资源。将共享资源保护起来就是临界资源,例如通过互斥访问的方式保护共享资源,其就变成了临界资源
- 临界区/非临界区:代码中有用于访问资源的代码,这些代码就叫做临界区;不访问资源的代码就叫做共享区
信号量:本质是一个对资源进行预订的计数器。
因此信号量必须解决下面两个问题:
信号量必须能被多个进程看到 。
信号量的 - - 与 ++ 操作(PV操作)必须具有原子性(原子性是指一个操作是不可中断的,即该操作要么全部执行成功,要么全部执行失败,不存在执行到中间某个状态的情况。)
-
二元信号量
:计数值只有0和1两个状态(将临界资源作为一个整体访问使用,使用的是二元信号量) -
多元信号量
:计数值大于1(将临界资源划分为多个小资源,供多个进程访问,使用的是多元信号量)
访问临界资源的步骤:1.申请信号量 2.访问临界资源 3.释放信号量
申请信号量的本质就是对临界资源的预定
信号量和共享内存、消息队列一样,需要实现被不同的进程访问,所以信号量本身也是一个共享资源。
🦋 信号量操作(PV操作)
由于信号量也是遵循System V标准的,所以它的常用方法和前面的类似。信号量主要是用于同步和互斥的。
保护的常见方式:
互斥
:任何时刻,只允许一个执行流(进程)访问资源。同步
:多个执行流,访问临界资源的时候,具有一定的顺序性。
因此,我们所写的代码 = 访问临界资源的代码(临界区) + 不访问临界资源的代码(非临界区)。所谓的对共享资源的保护,本质是对访问共享资源的代码进行保护。
(1)创建/获取信号量
cpp
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg)
-
key
:是一个键值,用于唯一标识信号量集 -
nsems
:指定信号量集中信号量的数量,通常这个值至少为1 -
semflg
:是一组标志位,用于指定信号量集的属性 -
常见标志位:IPC_CREAT:如果信号量集存在则获取并返回;如果不存在则创建
IPC_CREAT | IPC_EXCL
:如果信号量集存在则报错;如果不存在则创建
返回值:成功返回非零的信号量标识符;失败返回 -1,并设置 errno 以指示错误原因
(2)删除信号量
cpp
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...)
semid
:是信号量集合的标识符,由 semget 函数返回semnum
:信号量在信号量集合中的索引(从0开始)(如果要删除整个信号量集,则填0)cmd
:指定要执行的控制命令
常见命令:IPC_RMID:删除信号量集合
返回值:成功返回0;失败返回-1并设置 errno
(3)操作信号量
(4 ) 信号量指令:查看信号量
cpp
ipcs -s
(5) 信号量指令:删除信号量
cpp
ipcrm -s semid
五:🔥 IPC的理解
System V 是如何实现IPC的,和管道为什么不同呢?
🦋 用户角度
- 首先我们要知道操作系统是如何管理 IPC 的:先描述,再组织。IPC有哪些属性呢?
根据上面我们可以发现,它们内部都有一个 ipc_perm
的东西。我们可以推测一下,在 OS 层面,IPC 是同类资源。
我们也可以获取IPC对应的属性:
cpp
void ShmMeta()
{
struct shmid_ds buffer; // 系统提供的数据类型
int n = ::shmctl(_shmid, IPC_STAT, &buffer);
if(n < 0) return ;
std::cout << "############################" << std::endl;
std::cout << buffer.shm_atime << std::endl;
std::cout << buffer.shm_cpid << std::endl;
std::cout << buffer.shm_ctime << std::endl;
std::cout << buffer.shm_nattch << std::endl;
std::cout << buffer.shm_perm.__key << std::endl;
}
🦋 内核角度
🦁 我们知道 IPC
资源要被所有进程看到,它一定是全局的。所以IPC资源在内核中一定是一个全局变量
下面我们来看内核源代码:
- 我们发现在消息队列、信号量与共享内存的源码中,结构体开头位置都是 kern_ipc_perm,这点和我们上面从用户层看到的是一样的。
此时,所有的IPC资源都可以直接被柔性数组直接指向。
例如:
p[0] = (struct kern_ipc_perm) &(shmid_kernel)
p[1] = (struct kern_ipc_perm) &(msg_queue)
p[2] = (struct kern_ipc_perm) &(sem_array)
...
那么不就可以使用柔性数组 (类型强转
) ,管理所有的IPC资源了吗?数组下标就是之前的 xxxid,即 xxxget 的返回值!这也就是为什么,之前我们见到的各种 IPC资源的 id 是连续的了。
所以,所有的 IPC 资源,区分 IPC 的唯一性,都是通过 key,各类型的 IPC 资源之间的 key 也可能会冲突。
此时怎么访问IPC资源的其它属性呢?
直接强转,(struct msg_queue*) p[1] ->其它属性
那么一个指针,指向结构体的第一个元素,其实就是指向了整个结构体。访问头部,直接访问;访问其它属性,做强转,这种结构不就是C++中的多态吗?
这时,我们所看到的 kern_ipc_perm
就是 基类
,与之相关的三个就是子类,继承了基类,此时就可以使用基类来管理所有的子类了,这是 C语言实现多态的另一种方式
。
🦁 那具体是怎么识别是哪一种子类的呢?
- 实际在内核中,会定义各种的
ipc_ids
,但是它们的entries
指针都指向同一个kern_ipc_perm
数组。
六:🔥 共勉
以上就是我对 【Linux】IPC进程间通信System V:并发编程实战指南(二)
的理解,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉