【Linux取经路】进程通信——共享内存

news2025/5/21 10:47:07

文章目录

  • 一、直接原理
    • 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 借助管道实现共享内存的同步与互斥
  • 五、结语

在这里插入图片描述

一、直接原理

1.1 共享内存的的申请

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

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

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

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

1.2 共享内存的释放

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

image-20240306092506867

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

二、代码演示

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 函数相当于是两个通信进程之间的一种约定,只要它们约定好同一个 pathnameproj_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/1683695.html

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

相关文章

安全攻防基础

一、安全是什么&#xff1f;就是三个基础原则 安全就是保护数据 1. 机密性 对未授权的主体不可见 开发人员不能拥有敏感数据的访问权限 密钥要复杂 显示器伤的数据被别有用心的人窥探 2. 完整性 没授权的人不可修改数据 3. 可用性 被授权的主体可读 二、如何解决安全问题…

Rust面试宝典第14题:旋转数组

题目 给定一个数组&#xff0c;将数组中的元素向右移动k个位置&#xff0c;其中k是非负数。要求如下&#xff1a; &#xff08;1&#xff09;尽可能想出更多的解决方案&#xff0c;至少有三种不同的方法可以解决这个问题。 &#xff08;2&#xff09;使用时间复杂度为O(n)和空间…

微服务远程调用 RestTemplate

Spring给我们提供了一个RestTemplate的API&#xff0c;可以方便的实现Http请求的发送。 同步客户端执行HTTP请求&#xff0c;在底层HTTP客户端库(如JDK HttpURLConnection、Apache HttpComponents等)上公开一个简单的模板方法API。RestTemplate通过HTTP方法为常见场景提供了模…

VisualStudio2022的使用

Visual Studio 2022 的安装、卸载和使用方法详解 一、安装Visual Studio 2022 1. 下载Visual Studio 2022 要安装Visual Studio 2022&#xff0c;需要先下载安装程序。可以从微软的官方网站&#xff08;Visual Studio下载页面&#xff09;下载免费的社区版&#xff08;Commun…

非平稳信号的傅里叶变换与短时傅里叶变换

一、仿真一个非平稳的时间序列。 N 10000; t 0:N-1; z1 4.2*sin(2*pi/20.*t5); z2 2.2*sin(2*pi/100.*(10.001*t).*t8); w1 randn(length(t),1); yz1z2w1; figure;plot(y,LineWidth,1.5);grid on; ylabel(Signal); xlabel(Time); 二、傅里叶变换&#xff08;FFT&#xff…

网创教程:WordPress插件网创自动采集并发布

网创教程&#xff1a;WordPress插件网创自动采集并发布 使用插件注意事项&#xff1a; 如果遇到404错误&#xff0c;请先检查并调整网站的伪静态设置&#xff0c;这是最常见的问题。需要定制化服务&#xff0c;请随时联系我。 本次更新内容 我们进行了多项更新和优化&#x…

保护共享资源的方法(互斥锁)

我最近开了几个专栏&#xff0c;诚信互三&#xff01; > |||《算法专栏》&#xff1a;&#xff1a;刷题教程来自网站《代码随想录》。||| > |||《C专栏》&#xff1a;&#xff1a;记录我学习C的经历&#xff0c;看完你一定会有收获。||| > |||《Linux专栏》&#xff1…

一个基于vue、nuxt.js的网盘搜索项目,且持续开源和维护;目的实现人人都可以拥有自己的网盘搜索网站;

一个基于vue、nuxt.js的网盘搜索项目&#xff0c;且持续开源和维护&#xff1b;目的实现人人都可以拥有自己的网盘搜索网站&#xff1b; &#x1f310;Github地址 https://github.com/unilei/aipan-netdisk-search &#x1f310;在线体验 https://so.aicompasspro.com/

我在去哪儿薅到了5块钱火车票代金券,速薅

哈哈&#xff0c;亲爱的薅羊毛小伙伴们&#xff01; 刚刚在去哪儿大佬那儿发现了一个超级薅羊毛福利&#xff01;我只花了短短两分钟&#xff0c;就搞到了一张5块钱火车票代金券&#xff0c;简直是天上掉馅饼的节奏啊&#xff01; 话不多说&#xff0c;薅羊毛的姿势给你们摆好…

202473读书笔记|《但愿呼我的名为旅人:松尾芭蕉俳句300》——围炉夜话,身顿心安,愿每个人都能在爱里自由驰骋

202473读书笔记|《但愿呼我的名为旅人&#xff1a;松尾芭蕉俳句300》——围炉夜话&#xff0c;身顿心安&#xff0c;愿每个人都能在爱里自由驰骋 &#x1f60d;&#x1f60d;&#x1f929;&#x1f929; 译者序正文二正文三正文四正文五正文六正文七 《但愿呼我的名为旅人&…

【动手学强化学习】第 6 章 Dyna-Q 算法知识点总结

【动手学强化学习】第 6 章 Dyna-Q 算法知识点总结 本章知识点基于模型的强化学习与无模型的强化学习方法简介无模型的强化学习方法基于模型的强化学习方法 强化学习算法的评价指标Dyna-Q算法Dyna-Q 算法的具体流程Dyna-Q 代码实践 本章知识点 基于模型的强化学习与无模型的强…

前端 CSS 经典:好看的标题动画

前言&#xff1a;好看的标题动画实现。 效果&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><…

syncthing文件夹同步与版本管理

1 前言 syncthing可以用来同步文件夹里的所有文件&#xff0c;并且有不错的版本管理&#xff0c;基本每次更改文件&#xff0c;20-40秒就被扫描到了&#xff0c;非常丝滑&#xff1b;这次以此来同步obsidian的插件和文件&#xff0c;达到多端同步&#xff1b; 我家里有一台台…

自定义横向思维导图,横向组织架构图,横向树图。可以自定义节点颜色,样式,还可以导出为图片

最近公司设计要求根据目录结构&#xff0c;横向展示。所以做了一个横向的思维导图&#xff0c;横向的树结构&#xff0c;横向的组织架构图&#xff0c;可以自定义节点颜色&#xff0c;样式&#xff0c;还可以导出为图片 话不多说&#xff0c;直接上图片&#xff0c;这个就是一…

Django自定义命令

Django自定义命令 我们知道&#xff0c;Django内部内置了很多命令&#xff0c;例如 python manage.py runserver python manage.py makemigrations python manage.py migrate我们可以在python控制台中查看所有命令 我们也可以自定义命令&#xff0c;让python manage.py执行…

如何使用甘特图来做任务管理?zz-plan甘特图的实践指南

在项目管理和任务调度中&#xff0c;甘特图是一种非常实用的工具&#xff0c;它可以帮助团队成员清晰地规划、执行和跟踪项目进度。然而&#xff0c;如何有效利用甘特图进行任务管理&#xff0c;对于许多团队来说仍然是一个挑战。本文将结合 zz-plan https://zz-plan.com/ 甘特…

彻底搞懂JavaScript原型和原型链

基于原型编程 在面向对象的编程语言中&#xff0c;类和对象的关系是铸模和铸件的关系&#xff0c;对象总是从类创建而来&#xff0c;比如Java中&#xff0c;必须先创建类再基于类实例化对象。 而在基于原型编程的思想中&#xff0c;类并不是必须的&#xff0c;对象都是通过克隆…

Unity数据持久化2——XML

简介&#xff1a; 基础知识 XML文件格式 XML基本语法 XML属性 练习&#xff1a; C#读取存储XML XML文件存放位置 读取XML文件 练习&#xff1a; 存储修改XML文件 练习&#xff1a; 总结 实践小项目 必备知识点 必备知识点——C#中XML序列化 必备知识点——C#中XML反序列化 必备…

ubuntu设置root开机登录,允许root用户ssh远程登录

ubuntu与centos系统不同&#xff0c;默认root开机不能登录。 1、输入一下命令创建root密码&#xff0c;根据提示输入新密码 sudo passwd root 2、打开gdm-autologin文件&#xff0c;将auth required pam_succeed_if.so user ! root quiet_success这行注释掉&#xff0c;这行就…

【python】python社交交友平台系统设计与实现(源码+数据库)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…