[Linux]进程间通信-共享内存与消息队列

目录

一、共享内存

1.共享内存的原理

2.共享内存的接口

命令行

创建共享内存

共享内存的挂接

去掉挂接

共享内存的控制

3.共享内存的使用代码

Comm.hpp--封装了操作接口

客户端--写入端

服务器--读取端

4.管道实现共享内存的同步机制

二、消息队列

1.底层原理

2.使用接口

创建消息队列

发送消息

接收消息

消息队列的控制


一、共享内存

1.共享内存的原理

共享内存是进程间通信中最快的一个形式了,对于管道来说是复用了文件系统的代码,而共享内存(System V)是操作系统单独设计的一个模块用来实现进程间通信。

对于进程来说通信的前提条件就是不同的进程要看到同一份资源空间,共享内存的实现则是操作系统在内存中单独使用了一块空间用于进程之间的通信。因为是在物理内存中的空间,所以如果进程想要使用的话,还是要通过页表的映射到虚拟地址空间当中,映射的位置在栈区和堆区之间的共享区内。

一个计算机中,不可能只有两个进程进行通信,也不可能之有两个进程使用共享内存进行通信,那么对于多个共享内存,每个共享内存是哪些进程在使用,权限是什么等等都需要操作系统直到,所以还是先组织在描述的方式,使用结构体将共享内存描述起来,并使用链表等数据结构把多个共享内存管理起来。使用引用计数计数,当没有进程使用共享内存的时候在释放共享内存。

那么当一个进程创建了共享内存之后,如何保证其他想要通信的进程能够找到该共享区呢?就需要提供一个唯一的标识符key标志一个共享内存区域。两个进程在创建共享区的时候,会约定一个key,一个进程用接口创建好共享内存之后,把这个key放入操作系统管理共享内存的结构体中,另一个进程通过key向操作系统索要该共享内存的起始地址,操作系统通过遍历共享内存链表找到该key值的共享内存,然后将起始地址返回给进程。这样两个进程都获取到了该共享内存的起始地址,就访问到了一份资源,就实现了进程间的通信了。

2.共享内存的接口

命令行

ipcs -m 查看系统中的System V共享内存

ipcrm -m XX(shmid) 用于释放共享内存

创建共享内存

头文件 <sys/ipc.h> <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

该函数用来创建或获取共享内存的标识符。这里的key值就用于表示共享内存段,size则是该共享内存的大小,shmflg是用于设置共享内存段的权限和其他属性。成功返回标识符,错误返回-1同时错误码被设置。

key字段:这个可以是用户自定义的非零整数,但是这样的话,很容易重复,我们最好是让系统帮我们生成一个唯一的不重复是最好的选择。系统为我们提供了ftok函数,会根据传递的路径名和项目标识符来生成一个唯一的键值key。

头文件 <sys/type.h> <sys/ipc.h>

key_t ftok(const char* pathname, int proj_id);

size字段:共享内存的大小尽量设置为4096的整数倍,因为底层是以4096byte为单位进行共享内存空间的申请,如果传递的是4097byte会为该共享内存申请两个4096byte大小的空间的。

shmfig字段:当为IPC_CREAT时,表示如果共享内存段不存在就创建,如果存在就获取这个共享内存段的标识符,当为IPC_EXCL时不单独使用,通常配合IPC_CREAT,表示如果不存在就创建一个共享内存,该内存段已经存在了就返回-1,用于确保创建一个新的,唯一的共享内存段。而且可以设置共享内存的操作权限,和设置文件的权限类似,可以采用8进制传参。

共享内存的挂接

void *shmat(int shmid, const void* shmaddr, int shmflg);

该函数时用于将共享内存段连接到进程的地址空间当中,使进程可以访问当这块区域。成功会返回一个指向共享内存段的指针,用于访问共享内存,但是返回的是void*类型,和malloc一样需要强转后再使用。错误返回(void*)-1,同时错误码被设置,如果错误码是EACCES表示权限不足,EINVAL表示参数无效。

shmid是shmget函数返回的共享内存段标识符,用于确定要访问哪一个共享内存段。shmaddr指定共享内存连接到进程虚拟地址空间的起始地址,通常设置为nullptr让系统帮我们确定该起始地址,方位我们设置的地址有冲突。shmflg表示用于控制内存段的连接方式,可以设置读、写等权限,但是需要本身该共享内存就要有这种权限才可以,否则会出错,通常设置为0即可。

去掉挂接

int shmdt(const void* shmaddr);

用于将共享内存段和共享区之间的映射断开,传递的参数就是只需要是共享内存的起始地址就可以,因为系统直到共享内存的大小等各种字段,所以也就会根据大小,删除页表部分映射关系就实现了去掉映射的操作。

共享内存的控制

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

这个函数就像是共享内存段的 "控制面板",可以对共享内存段进行各种管理操作,如获取共享内存段的状态信息、修改其属性或者标记它为删除等。shmid还是共享内存唯一标识字段,cmd是一个命令码,用于指定对共享内存段要执行的操作。buf则是根据cmd的不同而设置不同的值。

cmd与buf字段:当cmd为IPC_STAT时,表示获取共享内存段的当前状态信息,此时buf就必须传递一个struct shmid_ds结构体用来接收;当cmd为IPC_SET时用于设置共享内存段的属性,所以要将修改的属性设置到buf中,传递给系统,让操作系统去修改共享内存的属性;而当cmd为IPC_RMID的时候,则表示删除该共享内存,不是解除映射,而是真正的删除物理内存中的共享内存,所以不用传递buf了,设置为nullptr即可。而且共享内存的声明周期是跟随内核的,所以我们必须手动释放才可以。

3.共享内存的使用代码

Comm.hpp--封装了操作接口
cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

//项目路径和工程id
const std::string pathname = "./Comm.hpp";
const int proj_id = 0x11223344;
//共享内存大小
const int size = 4096;
//管道文件名称
const std::string filename = "fifo_file";

//创建key值
key_t GetKey()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    if(key < 0)
    {
        std::cout << "ftok craet key fail" << std::endl;
        exit(1);
    }
    return key;
}

int ShmgetHelper(key_t key, int flag)
{
    int shmid = shmget(key, size, flag);
    if(shmid < 0)
    {
        std::cout << "shmget create system v shared memory fail" << std::endl;
        exit(2);
    }
    return shmid;
}

//创建共享内存
int CreateShm(key_t key)
{
    return ShmgetHelper(key, IPC_CREAT | IPC_EXCL | 0666);
}
//获取共享内存
int GetShm(key_t key)
{
    return ShmgetHelper(key, IPC_CREAT);
}
//创建管道文件
void Makefile()
{
    int n = mkfifo(filename.c_str(), 0666);
    if(n < 0)
    {
        //已经存在了
        if(errno == EEXIST)
            return;
        //真的出错了
        std::cout << "mkfifo create pipe file fail" << std::endl;
        exit(3);
    }
}
客户端--写入端
cpp 复制代码
#include "Comm.hpp"

int main()
{
    //获取key值
    key_t key = GetKey();
    //获取共享内存的标识
    int shmid = GetShm(key);
    //和共享内存建立映射
    char* ptr = (char*)shmat(shmid, nullptr, 0);
    //以写的方式打开命名管道
    int fd = open(filename.c_str(), O_WRONLY);

    //写入数据
    for(char ch = 'a'; ch <= 'z'; ch++)
    {
        ptr[ch -'a'] = ch;
        //该字段没有任何作用,只是用来唤醒服务端进行读取操作
        int code = 1;
        ssize_t n = write(fd, &code, sizeof(int));
        if(n< 0)
        {
            std::cout << "write fail" << std::endl;
            break;
        }
        sleep(1);
    }
    //关闭共享内存的映射以及管道文件
    shmdt(ptr);
    close(fd);
    return 0;
}
服务器--读取端
cpp 复制代码
#include "Comm.hpp"

int main()
{
    //创建命名管道
    Makefile();
    //获取key值
    key_t key = GetKey();
    //创建共享内存
    int shmid = CreateShm(key);
    //建立映射
    char* ptr = (char*)shmat(shmid, nullptr, 0);
    //以读的方式打开文件
    int fd = open(filename.c_str(), O_RDONLY);

    //进行通信
    while(true)
    {
        int code = 0;
        ssize_t n = read(fd, &code, sizeof(code));
        if(n < 0)
        {
            std::cout << "read fail" << std::endl;
            break;
        }
        else if(n == 0)
        {
            std::cout << "client quit" << std::endl;
            break;
        }
        else
        {
            std::cout << ptr << std::endl;
        }
    }
    //关闭文件
    close(fd);
    //取消映射
    shmdt(ptr);
    //释放共享内存
    shmctl(shmid, IPC_RMID, nullptr);

    return 0;
}

4.管道实现共享内存的同步机制

共享内存是没有任何的同步机制的,所以说读取端不知道写入端什么时候写入了数据,也不知道共享内存中有没有数据,所以为读写两端提供了命名管道的机制,当写入端向共享内存写入的同时会向管道文件写入一个无意义的数字,表示告诉服务器我写入了数据;服务器会一直阻塞式的读取管道文件,如果说客户端写入了,那么管道文件会有数据,read可以读取后,会表明有数据了,服务端就会去访问共享内存了。

二、消息队列

1.底层原理

(System V)消息队列的底层原理和共享内存基本一致,只是消息队列是在物理内存中维护了一个队列这样的数据结构,而共享内存只是一块空间而已。

因为是队列的数据结构,写入的数据会以数据块的形式作为队列中的一个一个的元素,而共享内存在读取的时候没办法将共享区中的数据进行分割,只能我们在应用层去分割数据。而且采用这种结构的话,是对数据的分割,就可以记录每个数据块是哪个进程发过来的了。

2.使用接口

接口的使用和形式上基本上也没有什么太大的区别,这样的话也方便了使用者去记忆。

创建消息队列

消息队列没有挂载的单独接口,当消息队列被创建出来之后,就会映射到虚拟地址空间当中,所以说该接口也是做了两个工作。

int msgget(key_t key, int msgflg);

发送消息

因为写入的是结构化的数据,所以要提供接口去写入。

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

msqid用来标识向哪个消息队列写入;msgp通常是用户自定义的一个结构体,结构体内部的成员为一个long类型的字段表示消息的类型和一个任意字段表示存放发送的消息;msgsz是传递消息的长度;msflg用于设置发送操作的属性,通常是0,也可以设置为IPC_NOWAIT就表示非阻塞式的发送数据,如果队列满了,会返回-1,同时错误码被设置为EAGAIN。

接收消息

int msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

msgtyp字段:用于指定接收消息的类型,当msgtyp大于0的时候,表示接收的消息类型为msgtyp的消息;msgtyp等于0的时候,表示接收消息队列中的第一个消息,不考虑类型;msgtyp小于0的时候则表示,接收的消息类型为小于或等于msgtyp绝对值的消息中类型值最小的消息。所以说发送消息传递的结构体中的类型并非是标志着int,double,而是用户在功能上自己定义区分了发送的消息。

消息队列的控制

消息队列没有取消挂载(映射)的概念,只有释放的概念,和共享内存一样cmd为IPC_RMID的时候就是释放消息队列的操作,但是这里并不是真正的时候,而是引用计数减少,只有当最后一个使用该消息队列的进程释放了消息队列的时候,引用计数减少到0,才会真的释放该消息队列。

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

相关推荐
猫咪-952735 分钟前
mv指令详解
linux·指令
叶 落42 分钟前
Ubuntu 下载安装 kibana8.7.1
服务器·ubuntu·kibana·es
大霞上仙43 分钟前
jenkins入门7 --发送邮件1
运维·jenkins
鲁子狄1 小时前
[笔记] Jenkins 安装与配置全攻略:Ubuntu 从零开始搭建持续集成环境
java·linux·运维·笔记·ubuntu·ci/cd·jenkins
endswel1 小时前
Jenkins pipeline 发送邮件及包含附件
运维·jenkins·邮件
坐忘3GQ1 小时前
119.使用AI Agent解决问题:Jenkins build Pipeline时,提示npm ERR! errno FETCH_ERROR
运维·npm·jenkins·nodejs·jenkinsfile·文心快码·fetch_error
蚊子爱喝水1 小时前
Centos7使用yum工具出现 Could not resolve host: mirrorlist.centos.org
linux·运维·centos
☆凡尘清心☆1 小时前
CentOS Stream 9上安装配置NFS
linux·运维·centos
JaneZJW2 小时前
嵌入式岗位面试八股文(篇三 操作系统(下))
linux·stm32·面试·嵌入式·c
网络安全-杰克2 小时前
网络安全概论——入侵检测系统IDS
服务器·安全·web安全