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

目录

⛳️推荐

一、直接原理

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 进程就会被阻塞住,以此来实现同步与互斥。

🎁结语:

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

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

库库的里昂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值