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

🌈 个人主页: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 信号量(了解)

前提知识:

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

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

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

  1. 信号量必须能被多个进程看到 。
  2. 信号量的 - - 与 ++ 操作(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:并发编程实战指南(二) 的理解,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉

相关推荐
运维佬28 分钟前
在 Linux 系统上部署 Apache Solr
linux·apache·solr
编程墨客36 分钟前
第03章 文件编程
linux·运维·服务器
命里有定数40 分钟前
windows工具 -- 使用rustdesk和云服务器自建远程桌面服务, 手机, PC, Mac, Linux远程桌面 (简洁明了)
linux·运维·服务器·windows·ubuntu·远程工作
cleveryuoyuo1 小时前
进程的程序替换exec*函数和shell实现
linux·服务器
运维佬1 小时前
nginx配置负载均衡详解
运维·nginx·负载均衡
微凉的衣柜1 小时前
使用 VS Code 远程连接时解决 OpenSSL 版本不匹配及权限问题
服务器·vscode·ubuntu
hope_wisdom1 小时前
C++网络编程之SSL/TLS加密通信
网络·c++·ssl·tls·加密通信
erxij1 小时前
【游戏引擎之路】登神长阶(十四)——OpenGL教程:士别三日,当刮目相看
c++·经验分享·游戏·3d·游戏引擎
爱编程的小金毛球球1 小时前
-bash: /home/xxx/anaconda3/bin/conda: No such file or directory
linux·conda·bash
林开落L1 小时前
前缀和算法习题篇(上)
c++·算法·leetcode