当前位置: 首页 > article >正文

【Linux】IPC进程间通信System V:并发编程实战指南(二)

🌈 个人主页:Zfox_
🔥 系列专栏:Linux

目录

  • 一:🔥 System V 共享内存
    • 🦋 共享内存的原理
  • 二:🔥 共享内存通信代码
    • 🦋 系统调用接口介绍:
    • 🦋 使用共享内存通信
    • 🦋 共享内存通信优缺点
  • 三:🔥 System V 消息队列(了解)
  • 四:🔥 System V 信号量(了解)
    • 🦋 信号量操作(PV操作)
  • 五:🔥 IPC的理解
    • 🦋 用户角度
    • 🦋 内核角度
  • 六:🔥 共勉

一:🔥 System V 共享内存

共享内存是System V进程通信中的一种方式,是本地通信

🦋 共享内存的原理

\qquad 🦁 进程在进行动态库加载时,动态库会通过页表映射到进程地址空间的共享区中。如果有多个进程要加载同一个动态库,动态库加载到内存后会被这些进程共同使用。

所以根据动态库加载的原理,操作系统可以在内存中创建一个共享内存空间,再通过页表映射到两个进程的共享区中,这样两个进程就可以看到同一份资源了。
在这里插入图片描述

操作系统可以为进程通信创造通信条件,但是什么时候通信是取决于进程,所以操作系统必须提供共享内存通信相应的系统调用接口。

  • 操作系统中会有多个进程使用共享内存通信,所以会有多个共享内存空间,这就需要操作系统统一管理这些共享内存空间。因此就有了内核数据结构 struct Shm 来管理共享内存通信。

二:🔥 共享内存通信代码

🦋 系统调用接口介绍:

#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
#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
#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

void DeleteShm()
{
    // 3. 删除共享内存
    ::shmctl(_shmid, IPC_RMID, nullptr);
}
  • IPC_RMID:立即删除共享内存段。这个操作只能由共享内存的创建者或拥有适当权限的进程执行。

\qquad

  • 挂接一个共享内存的系统调用:shmat (at:attach)
#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)
#include <sys/shm.h>

int shmdt(const void *shmaddr) 

shmdt 函数在Linux系统中用于将进程和共享内存断开连接。
const void *shmaddr 是当前进程地址空间中共享内存段的起始地址。

  • 返回值:成功时返回 0;失败时返回 -1,并设置相应的 errno。

🦋 使用共享内存通信

要想使用共享内存通信,两个进程,进程1先创建 shm && 使用;进程2 获取shm && 使用。然后一个进程向所挂接的内存中写,另一个读即可完成通信。

因此,可以将共享内存专门抽离作为一个类。
然后创建全局共享内存的对象,以便进程都能看到

#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获取消息队列
#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 信号量(了解)

前提知识:

  1. 共享资源:可以被多个进程访问的资源
  2. 临界资源:在系统中被多个进程共享,但在任一时刻只允许一个进程使用的资源。将共享资源保护起来就是临界资源,例如通过互斥访问的方式保护共享资源,其就变成了临界资源
  3. 临界区/非临界区:代码中有用于访问资源的代码,这些代码就叫做临界区;不访问资源的代码就叫做共享区

信号量:本质是一个对资源进行预订的计数器。

因此信号量必须解决下面两个问题:

  1. 信号量必须能被多个进程看到 。
  2. 信号量的 - - 与 ++ 操作(PV操作)必须具有原子性(原子性是指一个操作是不可中断的,即该操作要么全部执行成功,要么全部执行失败,不存在执行到中间某个状态的情况。)
  • 二元信号量:计数值只有0和1两个状态(将临界资源作为一个整体访问使用,使用的是二元信号量)

  • 多元信号量:计数值大于1(将临界资源划分为多个小资源,供多个进程访问,使用的是多元信号量)

访问临界资源的步骤:1.申请信号量 2.访问临界资源 3.释放信号量

申请信号量的本质就是对临界资源的预定

信号量和共享内存、消息队列一样,需要实现被不同的进程访问,所以信号量本身也是一个共享资源。

🦋 信号量操作(PV操作)

由于信号量也是遵循System V标准的,所以它的常用方法和前面的类似。信号量主要是用于同步和互斥的。

保护的常见方式:

  • 互斥任何时刻,只允许一个执行流(进程)访问资源。
  • 同步多个执行流,访问临界资源的时候,具有一定的顺序性。

因此,我们所写的代码 = 访问临界资源的代码(临界区) + 不访问临界资源的代码(非临界区)。所谓的对共享资源的保护,本质是对访问共享资源的代码进行保护。

(1)创建/获取信号量

#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)删除信号量

#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 ) 信号量指令:查看信号量

ipcs -s

(5) 信号量指令:删除信号量

ipcrm -s semid

五:🔥 IPC的理解

System V 是如何实现IPC的,和管道为什么不同呢?

🦋 用户角度

  • 首先我们要知道操作系统是如何管理 IPC 的:先描述,再组织。IPC有哪些属性呢?

在这里插入图片描述

根据上面我们可以发现,它们内部都有一个 ipc_perm 的东西。我们可以推测一下,在 OS 层面,IPC 是同类资源。

我们也可以获取IPC对应的属性:

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:并发编程实战指南(二) 的理解,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉
在这里插入图片描述


http://www.kler.cn/a/381578.html

相关文章:

  • QML —— QML调用C++两种方法(附完整测试源码)
  • python-读写Excel:openpyxl-(4)下拉选项设置
  • Rockchip SoC AI 与视觉处理器路线图:赋能未来的 AI 驱动设备
  • 分布式事务的几种方式 2PC,3PC,Distributed Lock,TCC
  • 小白直接冲!BiTCN-BiLSTM-Attention双向时间卷积双向长短期记忆神经网络融合注意力机制多变量回归预测
  • 【测试工具】Fastbot 客户端稳定性测试
  • xcode更新完最新版本无法运行调试
  • Postman断言与依赖接口测试详解
  • 人工智能AI 产品经理与传统产品经理工作到底有什么不同?非常详细收藏我这一篇就够了
  • kubernetes部署rancher无法查看pod日志及通过execute shell进入pod解决办法
  • 【Android Wi-Fi 操作命令指南】
  • pdf添加目录标签python(手动配置)
  • 【大数据学习 | kafka】producer之拦截器,序列化器与分区器
  • 数论——约数(完整版)
  • 动态避障-图扑自动寻路 3D 可视化
  • 使用Python简单实现客户端界面
  • 数据结构(8.7_2)——败者树
  • 苹果iOS 18.4将允许欧盟地区的iPhone用户设置默认地图和翻译应用
  • Excel 个人时间管理工具
  • 一文带您了解SonarScanner的原理和使用方法(包括maven构建和命令行执行)
  • 面试题:Vue生命周期
  • 【python】OpenCV—Connected Components
  • sheng的学习笔记-tidb框架原理
  • angular实现dialog弹窗
  • CentOS—OpenEulerOS系统联网指南
  • 大学城水电管理:Spring Boot应用案例