【Linux修行路】进程通信——共享内存

news2024/12/23 13:56:22

目录

⛳️推荐

一、直接原理

1.1 共享内存的的申请

1.2 共享内存的释放

二、代码演示

2.1 shmget

2.1.1 详谈key——ftok

2.2 创建共享内存样例代码

2.3 获取共享内存——进一步封装

2.4 共享内存挂接——shmat

2.5 共享内存去关联——shmdt

2.6 释放共享内存——shmctl

2.7 开始通信

2.7.1 processb 基础代码编写

2.7.2 通信代码编写

三、共享内存的特点

3.1 共享内存 VS 管道

四、拓展内容

4.1 查看共享内存的属性

4.2 借助管道实现共享内存的同步与互斥


⛳️推荐

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站【Linux修行路】动静态库详解点击跳转到网站

一、直接原理

1.1 共享内存的的申请

共享的原理和动态库的共享原理一致,共享内存的申请主要分为以下三步:

1.2 共享内存的释放

去关联,释放共享内存。申请、挂接、去关联、释放这些动作都是由操作系统来做的,进程不能自己去做,进程中可以通过 malloc 去申请空间,但是因为进程独立性的存在,一个进程自己 malloc 申请的空间,只属于当前进程,不能由多个进程共享。

  • 操作系统在物理内存上申请一块空间。

  • 将申请到的空间,通过页表挂接到进程地址空间的共享区。

  • 返回起始虚拟地址,供程序中使用。

系统中可能同时有多组进程 都需要通信,因此系统中可能存在多个共享内存,所以操作系统要把这多个共享内存管理起来。先描述,再组织,操作系统中一定有一个内核结构体是用来描述共享内存的。

二、代码演示

2.1 shmget

shmget 函数用来申请一块共享内存。

image-20240306100932350

  • key:一个数字,是几不重要,关键在于它必须在内核中具有唯一性,能够让不同的共享内存具有唯一性标识。
  • size:创建共享内存的大小,单位是字节。一般建议是4096的整数倍,如果传的是4097,操作系统实际上申请的空间大小是 4096*2,虽然操作系统多申请了,但是多的部分用户不能使用,用了会被判定为越界。
  • shmflg:标记位,常用选项有 IPC_CREAT :如果申请的共享内存不存在,就创建,存在,就获取并返回。IPC_EXCL :如果申请的共享内存存在,就出错返回。IPC_CREAT | IPC_EXCL :如果申请的共享内存不存在,就创建,存在就出错返回。这俩选项一起使用保证了,如果我们申请成功了一个共享内存,这个共享内存一定是一个新的。IPC_EXCL 不单独使用。其次,共享内存的权限也通过这个标志位进行传递。
  • 返回值:创建成功,返回共享内存标识符;创建失败,返回-1。

2.1.1 详谈key——ftok

无论是创建共享内存还是使用共享内存,都需要调用该函数。第一个进程可以通过 key 创建共享内存,第二个之后的进程,只需要拿着同一个 key 就可以和第一个进程看到同一个共享内存。key 在共享内存的描述对象中,第一次创建共享内存的时候,就必须有一个 key 了。使用 ftok 函数生成一个 key

image-20240306105116360

  • pathname:路径名。
  • proj_id:项目 ID。
  • 返回值:生成成功 key 被返回;生成失败 -1 被返回(路径名如果不存在的话是有可能生成失败的)。

只要这两个参数一样,那么两个进程就可以得到同一个 key 值。

**key值为什么是通过用户传参来生成的,而不是操作系统直接生成的?**因为操作系统不知道哪两个进程需要通信,假设 A 进程和 B 进程进行通信,在 A 进程中操作系统随机生成了一个 key 给 A 进程,此时 B 进程也需要知道 key 值,但是操作系统是不知道 A 进程要和 B 进程进行通信,所以操作系统没办法将这个 key 值交给 B 进程,只有程序员(写代码的人)才知道 A、B 进程之间需要通信。其实 ftok 函数相当于是两个通信进程之间的一种约定,只要它们约定好同一个 pathname 和 proj_id,那么这两个进程就能得到同一个 key

2.2 创建共享内存样例代码

#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string>
#include "log.hpp"
#include <string.h>
#include <errno.h>
using namespace std;

const int size = 4096;
const string path_name = "/home/wcy";
const int proj_id = 0x6666;
Log log;

key_t GetKey() // 获取 key
{
    key_t k = ftok(path_name.c_str(), proj_id);
    if(k < 0)
    {
        // 获取 key 失败
        log(Fatal, "ftok error: %s", strerror(errno));
        exit(1);
    }
    log(Info, "ftok sucess, key is: %d", k);

    return k;
}

int GetShareMem() // 创建共享内存
{
    key_t key = GetKey();
    int shmid = shmget(key, size, IPC_CREAT|IPC_EXCL);
    if(shmid < 0)
    {
        log(Fatal, "creat share memeory error: %s", strerror(errno));
        exit(2);
    }

    log(Info, "creat share memory success, shmid is: %d", shmid);

    return shmid;
}

#endif

key 和 shmid:

key 是在操作系统内部来唯一标识一块共享内存,是给操作系统来使用的;shmid 是给进程使用的,用来表示资源的唯一性。虽然共享内存属于文件系统,但是 shmid 和文件描述符的兼容性做的并不好,共享内存给自己单独设置了一个类似于文件描述符表的东西。

**共享内存的生命周期是随内核的,用户不主动关闭,共享内存会一致存在。**只有内核重启或者用户主动释放,共享内存才会被释放。

查看操作系统中所有的共享内存:ipcs -m

image-20240306113648742

  • perms:权限位
  • nattch:和当前共享内存关联的进程个数。

命令行中删除共享内存:ipcrm -m shmid。指令是由用户输入的的,在用户层统一使用 shmid

2.3 获取共享内存——进一步封装

int GetShareMemHelper(int flag)
{
    key_t key = GetKey();
    int shmid = shmget(key, size, flag);
    if(shmid < 0)
    {
        log(Fatal, "creat share memeory error: %s", strerror(errno));
        exit(2);
    }

    log(Info, "creat share memory success, shmid is: %d", shmid);

    return shmid;
}

// 创建共享内存
int CreatMem()
{
    return GetShareMemHelper(IPC_CREAT|IPC_EXCL|0666);
}

// 获取共享内存
int GetMem()
{
    return GetShareMemHelper(IPC_CREAT); // 这里也可以传0
} 

2.4 共享内存挂接——shmat

shmat:将某个共享内存挂接到当前进程的地址空间中。

image-20240306130457459

  • shmid:共享内存的标识符。
  • shmaddr:指向挂接在地址空间的什么位置,因为我们也不知道挂接在什么位置,所以一般设为 nullptr 即可。
  • shmflg:设置当前进程对该共享内存的权限,一般设置成0,表示采用共享内存自身的权限。
  • **返回值:**共享内存挂接在地址空间中的地址。
// processa
#include "comm.hpp"
#include <unistd.h>

int main()
{
    // 创建共享内存
    int shmid = CreatMem();
    log(Debug, "creat sharemem done...");

    sleep(5);
    // 挂接共享内存
    char *sharmem = (char*)shmat(shmid, nullptr, 0);
    log(Debug, "%d attch success", shmid);

    sleep(5);
    log(Debug, "processa quit...");
    return 0;
}

2.5 共享内存去关联——shmdt

shmdt:去掉某个共享内存与当前进程的关联。

image-20240306132053723

  • shmaddr:就是 shmat 函数返回的那个地址。
// processa
#include "comm.hpp"
#include <unistd.h>

int main()
{
    // 创建共享内存
    int shmid = CreatMem();
    log(Debug, "creat sharemem done...");

    sleep(5);
    // 挂接共享内存
    char *shamem = (char*)shmat(shmid, nullptr, 0);
    log(Debug, "%d attch done, to 0x%x", shmid, shamem);

    sleep(5);

    // 去关联
    shmdt(shamem);
    log(Debug, "%d deattch done", shmid);

    log(Debug, "processa quit...");
    return 0;
}

2.6 释放共享内存——shmctl

shmctl:用来释放一个共享内存。

image-20240306133608142

  • cmd:操作选项。IPC_STAT:将内核中共享内存的属性拷贝到 buf 里面。IPC_RMID:删除共享内存。
  • struct shmid_ds *buf:语言层面用来描述一个共享内存的结构体,里面保存了共享内存的部分属性。
  • **返回值:**如果操作是 IPC_RMID,那么删除成功返回0,失败返回-1。
// processa
#include "comm.hpp"
#include <unistd.h>

int main()
{
    // 创建共享内存
    int shmid = CreatMem();
    log(Debug, "creat sharemem done...");

    sleep(5);
    // 挂接共享内存
    char *shamem = (char*)shmat(shmid, nullptr, 0);
    log(Debug, "sharemem %d attch done, to 0x%x", shmid, shamem);

    sleep(5);

    // 去关联
    shmdt(shamem);
    log(Debug, "sharemem %d deattch done", shmid);

    sleep(5);
    // 释放共享内存
    int ret = shmctl(shmid, IPC_RMID, NULL);
    if(ret < 0)
        log(Debug, "sharemem delete error: %s", strerror(errno));
    else
        log(Debug, "sharemem delete success...");

    sleep(5);

    log(Debug, "processa quit...");
    return 0;
}

2.7 开始通信

2.7.1 processb 基础代码编写
#include "comm.hpp"
#include <unistd.h>

int main()
{
    // 获取共享内存
    int shmid = GetMem();
    log(Debug, "Get sharemem done...");
    sleep(5);

    // 挂接
    char *shmaddr = (char*)shmat(shmid, nullptr, 0);
    log(Debug, "sharemem %d attch done, to 0x%x", shmid, shmaddr);
    sleep(5);

    // 去关联
    shmdt(shmaddr);
    log(Debug, "sharemem %d deattch done", shmid);


    return 0;
}
2.7.2 通信代码编写

processa:

#include "comm.hpp"
#include <unistd.h>

int main()
{
    // 创建共享内存
    int shmid = CreatMem();
  
    // 挂接共享内存
    char *shamem = (char*)shmat(shmid, nullptr, 0);

    // ipc-cod 通信代码
    while(true)
    {
        cout << "client asy@ " << shamem << endl; // 直接访问共享内存
        sleep(1);
    }

    // 去关联
    shmdt(shamem);
    
    // 释放共享内存
    int ret = shmctl(shmid, IPC_RMID, NULL);
    return 0;
}

processb:

#include "comm.hpp"
#include <unistd.h>

int main()
{
    // 获取共享内存
    int shmid = GetMem();

    // 挂接
    char *shmaddr = (char*)shmat(shmid, nullptr, 0);

    // ipc-code 通信代码
    while(true)
    {
        cout << "Please enter:";
        fgets(shmaddr, size, stdin);
    }

    // 去关联
    shmdt(shmaddr);

    return 0;
}

一旦有了共享内存,并且挂接到当前进程的地址空间上了,在程序中就把它当做该进程自己的内存空间来使用即可,无需再调用系统调用。一旦有人把数据写入到共享内存,其实我们立马就能看到,不需要经过系统调用,就能直接看到数据。可以把共享内存就当做用户自己 malloc 出来的一块空间。

三、共享内存的特点

  • 共享内存没有同步与互斥之类的保护机制,即写端没有向共享内存中写入,读端可以正常读取。

  • 共享内存是所有的进程间通信中,速度最快的。原因在于数据拷贝次数少。

  • 共享内存中的数据,完全是由用户自己维护,操作系统不会帮我们做清空工作。

3.1 共享内存 VS 管道

管道通信中:数据要拷贝两次。主要原因在于,管道本质上是文件,我们不能通过键盘直接往文件里面进行写入,要想让键盘输入的内容写入的文件中,首先需要在程序中定义一个字符数组(或者 string 对象),暂时存储键盘的输入,然后在将这个数组中的内容写入到文件,这就涉及一次拷贝(将数组中的内容,拷贝到文件缓冲区),其次另一端在进行读取的时候,所有的文件读取操作,都要求定义一段空间,将读取到的内容存储起来,这个过程又会涉及一次拷贝,总体算下来,完成一次管道通信,需要进行两次拷贝。

共享内存通信:程序中可以把共享内存当做自己的内存空间来使用,因此对于写端,可以直接从键盘读取数据存储到共享内存中,无需创建字符数组(或者 string 对象)来暂时存储输入的内容;对于读端,可以直接从共享内存中进行读取,然后打印,在没有特殊要求的情况可以不用将共享内存中的数据存储起来。

四、拓展内容

4.1 查看共享内存的属性

通过 shmctl 函数区获取共享内存的属性。struct shmid_ds 结构体就是用户层面去描述一个共享内存的结构体。

image-20240306151122027

int main()
{
    // 创建共享内存
    int shmid = CreatMem();

    struct shmid_ds shmds; // 用来存储共享内存的属性

    // 挂接共享内存
    char *shamem = (char*)shmat(shmid, nullptr, 0);

    // ipc-cod 通信代码
    while(true)
    {
        cout << "client asy@ " << shamem << endl; // 直接访问共享内存
        
        // 打印共享内存的属性
        shmctl(shmid, IPC_STAT, &shmds);
        // cout << "__key: " << shmds.shm_perm.__key << endl;
        printf("0x%x\n", shmds.shm_perm.__key);
        cout << "shm_atime: " << shmds.shm_atime << endl;
        cout << "shm_cpid: " << shmds.shm_cpid << endl;
        cout << "shm_nattch: " << shmds.shm_nattch << endl;
        sleep(1);
    }

    // 去关联
    shmdt(shamem);

    // 释放共享内存
    int ret = shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

4.2 借助管道实现共享内存的同步与互斥

processa:

#include "comm.hpp"
#include <unistd.h>

int main()
{
    // 创建共享内存
    int shmid = CreatMem();

    Init init; // 创建有名管道

    // 打开管道
    int fd = open(FIFO_FILE, O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }

    struct shmid_ds shmds;

    // 挂接共享内存
    char *shamem = (char *)shmat(shmid, nullptr, 0);

    // ipc-cod 通信代码
    while (true)
    {
        char ch;
        int n = read(fd, &ch, sizeof(ch));
        if (n == 0)
            break;
        else if (n > 0)
        {
            cout << "client asy@ " << shamem; //<< endl; // 直接访问共享内存
        }
        else
        {
            log(Fatal, "read error: %s\n", strerror(errno));
            exit(FIFO_READ_ERR);
        }
    }

    // 去关联
    shmdt(shamem);

    // 释放共享内存
    int ret = shmctl(shmid, IPC_RMID, NULL);

    // 关闭管道
    close(fd);
    return 0;
}

processb:

#include "comm.hpp"
#include <unistd.h>

int main()
{
    // 获取共享内存
    int shmid = GetMem();

    // 打开管道
    int fd = open(FIFO_FILE, O_WRONLY);
    if(fd < 0)
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }

    // 挂接
    char *shmaddr = (char*)shmat(shmid, nullptr, 0);

    // ipc-code 通信代码
    while(true)
    {
        cout << "Please enter:";
        fgets(shmaddr, size, stdin);
        write(fd, "c", 1);
    }

    // 去关联
    shmdt(shmaddr);

    // 关闭管道
    close(fd);
    return 0;
}

在没有管道的情况下,processa 进程一直在不间断的读取共享内存中的数据,现在创建一个管道,在 processa 进程读取共享内存之前,想让它从管道中读取,只有读到了特定的信号,才能去共享内存中进行读取。processb 进程在向共享内存中写入数据之后,向管道中写入一个字符,以此为信号,通知 processa 进程,现在共享内存中有数据了,你可以去读取了。在 processb 进程没有向共享内存中写入数据的时候,此时管道为空,读端就会阻塞,也就是 processa 进程就会被阻塞住,以此来实现同步与互斥。

🎁结语:

        今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是我前进的动力!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2092133.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

gitee版本控制

前置要求&#xff1a; 安装Git git下载地址&#xff1a;https://git-scm.com/download/win 注册gitee gitee官网&#xff1a;Gitee - 基于 Git 的代码托管和研发协作平台 创建普通项目 目录 git推送远程仓库基本操作 克隆仓库到本地 项目上传 版本管理 分支管理版本…

紧急 浮毛正在挑战免疫系统?推荐榜TOP3浮毛空气净化器使用体验

作为一名多猫家庭的铲屎官&#xff0c;出门路人必知道我养猫&#xff0c;不是把铲屎官三个字大大的打在我脑门上了。而是衣服、裤子上无处不在的猫毛&#xff0c;以前我就靠着人力与各种工具与猫毛斗争&#xff0c;但效果总是差强人意。直到有一天&#xff0c;我因忽视浮毛而患…

Linux 用户缓冲区

1. 文件描述符的分配规则 我们知道Linux进程默认情况下会有3个缺省打开的文件描述符&#xff0c;分别是标准输入stdin--0&#xff0c; 标准输出stdout--1&#xff0c; 标准错误stderr--2。0,1,2对应的物理设备一般是&#xff1a;键盘&#xff0c;显示器&#xff0c;显示器.接下…

字符串原始字面量

简介&#xff1a;C11中添加定义了原始字符串的字面量&#xff0c;定义方式为&#xff1a;R"xxx(原始字符串)xxx",其中&#xff08;&#xff09;两边的字符串可以省略。原始字面量R可以直接表示字符串的实际含义&#xff0c;而不需要额外对字符串做转译或链接等操作 …

ddpm和ddim小记

前面分析了DDPM和DDIM&#xff0c;但是仍然感觉对其理解不是和透彻&#xff0c;最近又学习了下&#xff0c;简单记录一下进一步的理解。为了方便理解&#xff0c;这里直接以两个像素的灰度图像为例。前面讲过无论是DDPM还是DDIM&#xff0c;他们的训练过程都是一样的&#xff0…

一套采用JAVA语言开发的数字化产科管理平台源码,自主知识产权,三甲综合医院应用案例,系统稳定运行,全套源码交付。

一套采用JAVA语言开发的一套数字化产科管理平台源码&#xff0c;自主知识产权&#xff0c;三甲综合医院应用案例&#xff0c;系统稳定运行。全套源码交付。 数字化产科管理平台源码技术栈&#xff1a; 技术架构&#xff1a;前后端分离 开发语言&#xff1a;Java 开发工具…

深度学习基础--梯度下降与初始化

在神经网络的背景下&#xff0c;它们用于寻找能够最小化损失函数的参数&#xff0c;使模型能够根据输入准确预测训练数据的输出。基本方法是随机选择一组初始参数&#xff0c;然后逐步进行微小调整&#xff0c;平均而言这些调整会降低损失。每一次的调整都是基于当前参数位置对…

ERROR: Cannot uninstall numpy 1.24.2, RECORD file not found.

目录 1.问题描述&#xff1a;2.解决方法&#xff1a;2.1流程2.2结果 小结&#xff1a; 1.问题描述&#xff1a; 卸载 numpy 时报错&#xff1a; ERROR: Cannot uninstall numpy 1.24.2, RECORD file not found. You might be able to recover from this via: pip install --f…

HTB-sequal(mysql)

前言 各位师傅大家好&#xff0c;我是qmx_07&#xff0c;今天给大家讲解sequal这台靶机 渗透过程 信息搜集 服务器开放了3306mysql端口思路&#xff1a;mysql爆破-sC参数会执行 相关的默认脚本 连接mysql数据库 通过空密码连接道mysql数据库flag&#xff1a;7b4bec00d1a39…

【GD32】从零开始学GD32单片机 | USB通用串行总线接口+HID键盘例程(GD32F470ZGT6)

1. 简介 USB&#xff0c;全称通用串行总线&#xff0c;相信大家都非常熟悉了&#xff0c;日常生活只要用到手机电脑都离不开这个接口&#xff0c;像鼠标键盘U盘都需要使用这个接口进行数据传输&#xff0c;下面简单介绍一下。 1.1 版本标准 USB的标准总体可以分为低速、全速和…

04:布局规划

1.切换到丝印层 2.用2D线对模块区域划分

keil5烧录后不自动复位和Flash Download failed - “Cotex-M3“报错解决

目录 项目场景&#xff1a; 复位问题描述 复位原因分析&#xff1a; 复位解决方案&#xff1a; 下载错误问题描述 下载错误原因分析&#xff1a; 下载错误解决方案&#xff1a; 总结 项目场景&#xff1a; keil5编译stm32例程在烧录时候遇到的一些问题 复位问题描述 1. 使…

全面解读LSC局域网屏幕监控软件:功能、优势与应用场景一网打尽!

在信息化高速发展的今天&#xff0c;企业管理的效率和精准度成为决定竞争力的关键因素。 LSC局域网屏幕监控软件&#xff08;LAN Screen Capture&#xff09;&#xff0c;作为安企神推出的一款专为现代企业量身打造的超级局域网监控管理软件&#xff0c;以其强大的功能和灵活的…

MAC环境导出项目的目录结构

一、安装Homebrew包管理工具 /bin/bash -c "$(curl -fsSL https://gitee.com/ineo6/homebrew-install/raw/master/install.sh)" 官网网址&#xff1a;https://brew.idayer.com/ 二、用brew包管理工具安装tree brew install tree 三、打开终端&#xff0c;导出项目…

怎么用AI做视频总结?

利用AI工具批量生成影视短剧推广https://docs.qq.com/doc/DYnl6d0FLdHp0V2ll 搞个插件就可以了。 我只能说AI的终极目的就是为了视频服务的&#xff0c;语音&#xff08;配音&#xff09;、视频脚本&#xff08;文案&#xff09;、绘图&#xff08;画面&#xff09;、设计&…

数据结构:单链表逆置的相关问题

1.思路&#xff1a;这里主要是用到头插法的思想进行单链表的一个逆置 2.知识点回顾&#xff1a; &#xff08;1&#xff09;头插法 &#xff3b;1&#xff3d;什么叫头插法&#xff1a;新增节点在头节点后面&#xff08;下图为单链表结构&#xff09; &#xff3b;2&#xf…

Leetcode 257-二叉树的所有路径

给你一个二叉树的根节点 root &#xff0c;按 任意顺序 &#xff0c;返回所有从根节点到叶子节点的路径。 叶子节点 是指没有子节点的节点。 题解 递归回溯 遇到叶节点返回 每层的做法,list加上当前节点的string值 本题解将res作为全局变量&#xff0c;作为局部变量写法也…

图像边缘检测Canny

一、Canny边缘检测原理 边缘检测是图像处理和计算机视觉中的基本问题&#xff0c;边缘检测的目的是标识数字图像中亮度变化明显的点。 Canny边缘检测算法是由4步构成&#xff1a;噪声去除、计算图像梯度、非极大值抑制、滞后阈值 1、噪声去除&#xff1a;由于边缘检测很容易受到…

Vulnhub靶场 | DC系列 - DC6

文章目录 DC-6环境搭建渗透测试 DC-6 环境搭建 靶机镜像下载地址&#xff1a;https://vulnhub.com/entry/dc-6,315/需要将靶机和 kali 攻击机放在同一个局域网里&#xff1b;本实验kali 的 IP 地址&#xff1a;192.168.10.146。 渗透测试 使用 nmap 扫描 192.168.10.0/24 网…

【报错已解决】`ValueError: Expected 2D array, got 1D array instead`的

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 引言&#xff1a; 在使用机器学习库&#xff08;如scikit-learn&#xff09;时&#xff0c;开发者可能会遇到ValueError: Expec…