【Linux 16】进程间通信的方式 - 共享内存

🌈 一、共享内存概述

⭐ 1. 什么是共享内存

  • 共享内存是 OS 内部单独设计出来专门用于进程间通信的模块,是最快的进程间通信的形式。
  • 它允许两个或多个进程访问同一块物理内存区域,从而实现数据的快速交换。
  • 共享内存减少了数据拷贝的开销,并且数据的传输速度非常快,因为数据直接在内存中传输,而不需要经过内核的干预。

⭐ 2. 如何实现共享内存

  1. 申请空间:在物理内存当中申请一块内存空间。
  2. 建立映射:将这块内存通过页表映射到想要进行通信的进程的进程地址空间共享区中,然后返回共享区的起始地址即可。
    • 至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存,多个进程就可以通过共享内存间接的进行通信。

在这里插入图片描述

⭐ 3. 操作系统允许存在多个共享内存

  • 不是所有的进程都是使用同一块内存进行通信的,还有其他多组进程可能要使用不同的共享内存进行通信。
  • 如果 OS 不允许存在多个共享内存的话,那么就意味着同一时刻只有一组进程能够通信,这显然不合理。
  • 因此,OS 允许多个共享内存被创建,即允许同时存在多个共享内存,那么 OS 就要管理这些共享内存

⭐ 4. 操作系统如何管理共享内存

  • 只知道在内存中开辟空间然后映射到地址空间可没法管理共享内存。
  • OS 需要知道一块共享内存的使用情况,才能将共享内存管理起来。
    • 如:OS 需要知道共享内存的大小、什么时候开辟的、谁申请开辟的、有多少进程直接和该共享内存关联等信息。
  • 这些 OS 需要知道的关于共享内存的信息都存储在 shmid_ds 结构体当中。

1. 共享内存的数据结构

  • 共享内存除了在内存当中真正开辟空间之外,OS 还要为共享内存维护相关的内核数据结构。
  • 往后对共享内存的管理就变成了对该数据结构对象的管理。
struct shmid_ds
{
    struct ipc_perm shm_perm;    /* operation perms */
    int shm_segsz;               /* size of segment (bytes) */
    __kernel_time_t shm_atime;   /* last attach time */
    __kernel_time_t shm_dtime;   /* last detach time */
    __kernel_time_t shm_ctime;   /* last change time */
    __kernel_ipc_pid_t shm_cpid; /* pid of creator */
    __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
    unsigned short shm_nattch;   /* no. of current attaches */
    unsigned short shm_unused;   /* compatibility */
    void *shm_unused2;           /* ditto - used by DIPC */
    void *shm_unused3;           /* unused */
};

2. 用 key 值标识唯一的共享内存

  • 当申请了一块共享内存后,为了让要实现通信的进程能够看到同一块共享内存,因此每一个共享内存被申请时都有一个 key 值,这个 key 值用于标识系统中唯一的一块共享内存。
  • 每个共享内存的 key 值存储在 ipc_perm 结构体中,其中 ipc_perm 结构体的定义如下:
struct ipc_perm
{
    __kernel_key_t key;
    __kernel_uid_t uid;
    __kernel_gid_t gid;
    __kernel_uid_t cuid;
    __kernel_gid_t cgid;
    __kernel_mode_t mode;
    unsigned short seq;
};

⭐ 5. 获取共享内存的唯一标识符 key

  • 理论上 key 这玩意可以随便写一个,但这样容易和系统中已有的发生冲突。
  • 因此就需要使用系统提供的 ftok 算法函数来形成唯一的 key 值
    • 使用 ftok 函数形成的 key 值大概率不会发生冲突,但依旧有可能,此时再执行一次 ftok 即可 (更换参数)。

1. ftok 函数原型

#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);
  • 功能 : 将提供的 pathname 和 proj_id 参数通过函数内部的算法转换成 key。
  • 参数 : pathname - 路径名;proj_id - 项目 id,这玩意可以随便写。
  • 返回 : 如果转换成功则返回转换后的唯一的 key 值,转换失败则返回 -1。

2. ftok 函数用例

  • 如果给 ftok 提供的参数相同,那么通过同一个算法获取到的 key 肯定相同。
  • 多个进程只要提交同一份参数给 ftok,那么即可根据获取的同一个 key 值看到同一个共享内存空间。
    • 当前有 server.cpp 和 client.cpp 两个文件,让这两个文件提供给 ftok 函数的参数一致,看看所形成的 key 是否相同。
// 文件: server.cpp client.cpp
// 下面的代码除了打印部分不一样,其余部分两个文件都一致
#include <string>
#include <cassert>
#include <iostream>
#include <sys/ipc.h>
#include <sys/types.h>

using std::cout;
using std::endl;
using std::string;

const string pathname = "/home/yxc/Linux/2024_04_05_进程间通信/3.共享内存";
const int proj_id = 0x11223344;

int main()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    assert(key >= 0);
    
    cout << "server.cpp key : " << key << endl;// 这行代码只存在 server.cpp 文件中
	cout << "client.cpp key : " << key << endl;// 这行代码只存在 client.cpp 文件中
	
    return 0;
}

在这里插入图片描述

⭐ 6. 为什么要由用户提供 key

  • OS 明明就可以自己生成一个随机数作为唯一的 key 值,然后作为共享内存结构体中的 key 值,为什么还要由用户自己提供 key 呢?

为了让通信双方都能看到唯一的 key 值

  • 在系统层面上,key 值只有 OS 和创建共享内存的进程知道。
  • 如果 key 是由操作系统形成的,此时创建共享内存的进程还能通过 shmid 知道具体是哪个共享内存。但是其他使用共享内存的进程不知道 key 值,就没办法知道 shmid 值,就无法访和目标进程获取同一块共享内存了。

🌈 二、查看共享内存

⭐ 1. 使用 ipcs -m 查看

  • 可在命令行中输入 ipcs -m 指令查看当前的共享内存信息。

在这里插入图片描述

参数说明
key共享内存段的键值 key
shmid共享内存段的应用层标识符 id
owner该贡献内存的拥有者
perms共享内存所持有的权限
bytes共享内存段的大小,以字节为单位
nattch当前与该共享内存关联的进程数
status共享内存的状态

⭐ 2. key 与 shmid 的区别

  • key:不在应用层使用,只用来在内核中标识共享内存的唯一性。在创建共享内存的时候会被设置进共享内存的数据结构中。
  • shmid:使用 shmid 来操作共享内存,是方便我们进行编程而存在的。通过这个标识符,进程可以访问和操作共享内存段。

🌈 三、共享内存函数

  • 前提知识:共享内存 (Shared Memory) 的简写是 shm,以下所有的共享内存函数都是基于 shm 命名的。

⭐ 1. shmget 创建共享内存

1. 函数原型

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
  • 功能
    • 创建一块共享内存并将 key 填充进共享内存的结构体中。
  • 参数
    • key:用来唯一标识准备开辟的一块共享内存;
    • size:想开辟的共享内存的大小;
    • shmflg:创建共享内存的方式。
  • 返回
    • 如果共享内存创建成功,返回一个共享内存标识符;失败则返回 - 1,并设置错误码。

2. shmflg 参数的可选项

选项说明
IPC_CREAT如果共享内存不存在,就创建共享内存;如果存在,则获取共享内存。主要用于获取共享内存。
IPC_CREAT | IPC_EXCL如果共享内存不存在则创建,如果存在则出错返回。主要用于创建全新的共享内存。

3. 函数用例

// 文件: server.cpp
#include <string>
#include <cstring>
#include <cassert>
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>

using std::cout;
using std::endl;
using std::string;

const string pathname = "/home/yxc/Linux/2024_04_05_进程间通信/3.共享内存";
const int proj_id = 0x11223344;
const int size = 4096;

// 将创建 key 和判断是否创建成功封装在一起
key_t get_key()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    assert(key >= 0);
    return key;
}

int main()
{
	// 创建共享内存的键值 key
    key_t key = get_key();
	// 创建共享内存      
    int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
    assert(shmid >= 0);
    
    cout << "shmid:" << shmid << endl;

    return 0;
}

在这里插入图片描述

⭐ 2. shmat 将共享内存段连接到进程地址空间

1. 函数原型

#include <sys/shm.h>
#include <sys/types.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • 功能
    • 哪个进程调用该函数,就会将指定的共享内存链接到该进程的地址空间。
  • 参数
    • shmid:共享内存标识符;
    • shmaddr:将共享内存连接到进程地址空间中指定的一个起始地址,通常设置为空 (NULL / nullptr),表示让 OS 自己决定一个合适的地址位置;
    • shmflg:进程连接内存时的可选参数,通常设置为 0。
  • 返回
    • 成功则返回共享内存映射到进程地址空间中的起始地址;失败则返回 (void*)-1 并设置错误码。

2. shmflg 参数的可选项

选项说明
SHM_RDONLY关联共享内存后只进行读取操作
SHM_RND若 shmaddr 不为空,则连接的地址会自动向下调整为 SHMLBA 的整数倍。
0默认为读写权限

3. 函数用例

#include <string>
#include <cassert>
#include <iostream>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>

using std::cout;
using std::endl;
using std::string;

const string pathname = "/home/yxc/Linux/2024_04_05_进程间通信/3.共享内存";
const int proj_id = 0x11223344;
const int size = 4096;

// 将创建 key 和判断是否创建成功封装在一起
key_t get_key()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    assert(key >= 0);
    return key;
}

// 将创建共享内存和判断是否创建成功封装在一起
int create_shm(key_t key)
{
    int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
    assert(shmid >= 0);
    return shmid;
}

int main()
{
    key_t key = get_key();                      // 算出共享内存的唯一标识符
    int shmid = create_shm(key);                // 创建共享内存
    
    cout << "连接开始" << endl;
    char *s = (char *)shmat(shmid, nullptr, 0); // 将共享内存与当前进程连接
    sleep(5);
    cout << "连接结束" << endl;
	
    return 0;
}
  • nattch 表示当前有多少个进程与该共享内存关联,观察进程在启动和退出时 nattch 的变化。
    • 注:在进程终止时,会自动将共享内存与当前进程脱离。

在这里插入图片描述

⭐ 3. shmdt 将共享内存段与当前进程脱离

1. 函数原型

#include <sys/shm.h>
#include <sys/types.h>
       
int shmdt(const void *shmaddr);
  • 功能:手动取消共享内存与进程的虚拟地址空间的映射关系。
  • 参数:shmaddr 关联共享内存时获得的共享内存映射到地址空间的起始地址。
  • 返回:脱离成功则返回 0,失败则返回 -1 并设置错误码。

2. 函数用例

#include <string>
#include <cassert>
#include <iostream>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>

using std::cout;
using std::endl;
using std::string;

const string pathname = "/home/yxc/Linux/2024_04_05_进程间通信/3.共享内存";
const int proj_id = 0x11223344;
const int size = 4096;

// 将创建 key 和判断是否创建成功封装在一起
key_t get_key()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    assert(key >= 0);
    return key;
}

// 将创建共享内存和判断是否创建成功封装在一起
int create_shm(key_t key)
{
    int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
    assert(shmid >= 0);
    return shmid;
}

int main()
{
    key_t key = get_key();                      // 算出共享内存的唯一标识符
    int shmid = create_shm(key);                // 创建共享内存
    cout << "连接开始" << endl;
    char *s = (char *)shmat(shmid, nullptr, 0); // 将共享内存与当前进程连接

    sleep(2);
    shmdt(s);                                   // 取消关联
    cout << "连接结束" << endl;
    sleep(5);

    return 0;
}

在这里插入图片描述

⭐ 4. shmctl 控制共享内存

1. 函数原型

#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • 功能
    • 实现对共享内存的删改查。
  • 参数
    • shmid:由 shmget 函数返回的共享内存标识符,指定控制的是哪个共享内存。
    • cmd:准备采取什么方式控制共享内存。
    • buf:指向一个保存着共享内存的模式状态和访问权限的数据结构,如果没需求可设置为空。
  • 返回
    • 控制成功则返回 0,失败则返回 -1 并设置错误码。

2. cmd 参数的可选项

选项说明
查:IPC_STAT获取共享内存的当前关联值,此时参数 buf 作为输出型参数
改:IPC_SET在进程有足够权限的前提下,将共享内存的当前关联值设置为 buf 所指的数据结构中的值
删:IPC_RMID删除共享内存

3. 函数用例

  • 这里只演示删除 IPC_RMID 操作。
#include <string>
#include <cassert>
#include <iostream>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>

using std::cout;
using std::endl;
using std::string;

const string pathname = "/home/yxc/Linux/2024_04_05_进程间通信/3.共享内存";
const int proj_id = 0x11223344;
const int size = 4096;

// 将创建 key 和判断是否创建成功封装在一起
key_t get_key()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    assert(key >= 0);
    return key;
}

// 将创建共享内存和判断是否创建成功封装在一起
int create_shm(key_t key)
{
    int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
    assert(shmid >= 0);
    return shmid;
}

int main()
{
    key_t key = get_key();                      // 算出共享内存的唯一标识符
    int shmid = create_shm(key);                // 创建共享内存
    cout << "连接开始" << endl;
    char *s = (char *)shmat(shmid, nullptr, 0); // 将共享内存与当前进程连接
    sleep(2);
    shmdt(s);                                   // 取消关联
    cout << "连接结束" << endl;
    sleep(5);
    shmctl(shmid, IPC_RMID, nullptr);       	// 删除共享内存
    cout << "共享内存已被删除" << endl;

    return 0;
}

在这里插入图片描述

🌈 四、使用共享内存

  • 实现客户端向共享内存中依次写入 a ~ z 的 26 个字母,然后服务端每次都读取共享内存中的所有数据。

1. 代码展示

  1. command.hpp 公共文件:包含客户端和服务端两个进程所需要的公共代码。
#pragma once

#include <string>
#include <cassert>
#include <unistd.h>
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>

using std::cout;
using std::endl;
using std::string;

const string pathname = "/home/yxc/Linux/2024_04_05_进程间通信/3.共享内存";
const int proj_id = 0x11223344;
const int size = 4096; // 准备创建的共享内存的大小

// 获取唯一的 key
key_t get_key()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    assert(key >= 0);
    return key;
}

int create_shm_helper(key_t key, int flag)
{
    int shmid = shmget(key, size, flag);
    assert(shmid >= 0);
    return shmid;
}

// 创建共享内存
int create_shm(key_t key)
{
    return create_shm_helper(key, IPC_CREAT | IPC_EXCL | 0644);
}

// 获取共享内存
int get_shm(key_t key)
{
    return create_shm_helper(key, IPC_CREAT);
}
  1. server.cpp 服务端文件:用来创建共享内存,从客户端获取数据并打印。
#include "command.hpp"

int main()
{
    cout << "开始获取键值 key" << endl;
    key_t key = get_key();                      // 获取键值 key
    cout << "key: " << key << endl;

    cout << "开始创建共享内存" <<endl;
    int shmid = create_shm(key);                // 创建共享内存
    cout << "shmid: " << shmid << endl;

    cout << "开始将共享内存映射到进程地址空间" << endl;
    char* s = (char*)shmat(shmid, nullptr, 0);  // 关联共享内存
    
    // 通信代码
    while (true)
    {
        // 直接读取
        cout << "共享内存的内容: " << s << endl;
        sleep(1);
    }

    cout << "开始将共享内存从进程地址空间分离" << endl;
    shmdt(s);                                   // 脱离共享内存

    cout << "开始删除共享内存" << endl;
    shmctl(shmid, IPC_RMID, nullptr);           // 释放共享内存

    return 0;
}
  1. client.cpp 客户端文件:用来获取共享内存,往共享内存输入数据。
#include "command.hpp"

int main()
{
    key_t key = get_key();                      // 获取键值 key
    int shmid = get_shm(key);                   // 获取共享内存
    char *s = (char *)shmat(shmid, nullptr, 0); // 关联共享内存
    cout << "关联共享内存成功" << endl;
 
    // 通信代码
    for (char ch = 'a'; ch <= 'z'; ch++)
    {
        s[ch - 'a'] = ch;                       // 往共享内存写入
        cout << "write: " << ch << " done" << endl;
        sleep(1);                               // 每隔一秒向共享内存写入一次
    }

    shmdt(s);                                   // 分离共享内存
    cout << "分离共享内存成功" << endl;

    return 0;
}

2. 结果展示

在这里插入图片描述

🌈 五、释放共享内存

1. 释放共享内存的步骤

  1. 取消映射:取消进程的虚拟地址空间和物理内存的映射关系,本质就是清空页表的问题。
  2. 释放内存:共享内存的生命周期是随内核的,曾经创建的共享内存不会随着进程的退出而释放,需要手动释放共享内存。

2. 释放共享内存的方法

  1. 在命令行使用指令释放共享内存:使用 ipcrm -m shmid 指令删除指定 shmid 的共享内存。

在这里插入图片描述

  1. 在代码中使用函数释放共享内存:可以使用 shmctl 函数释放共享内存,前面已经实现过,就不过多赘述。

🌈 六、共享内存特点

  1. 共享内存不提供同步机制,共享内存是直接裸露给所有的使用者的,需要注意共享内存的使用安全问题。
  2. 共享内存是所有进程间通信方式中最快的一种。
    • A / B 进程写进共享内存中的内容另一个进程立马就能看到,可有效减少拷贝 (至少减少了 2 次,写进程不用将数据拷贝到缓冲区,读进程不用从缓冲区拷贝)。
  3. 共享内存可以提供较大的空间。
  • 45
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值