【Linux系统】进程间通信:共享内存

news2025/3/7 5:30:09




在这里插入图片描述



认识共享内存

通过 一些系统调用,在物理内存中开辟一块空间,然后将该空间的起始地址,通过页表映射到两个进程的虚拟地址空间的共享区中,这样不就共享了一块空间吗!!!

这种技术就是共享内存,如图所示:


在这里插入图片描述


挂接:而两个进程将该共享区的起始地址映射到虚拟内存的共享区的过程就是:挂接到进程的地址空间中!

去关联:若进程不需要使用这块空间,可以通过页表取消掉映射关系,这就是去关联



共享内存的管理

共享内存在任何时刻,可以在OS内部同时存在很多个,即同时存在很多组进程使用共享内存通信,而每个共享内存会各自有自己的状态:有的新建、有的去关联、有的挂接、有的准备销毁….:那么 OS 要不要管理共享内存呢?

答:当然,每组共享内存都有自己的属性,就需要管理起来

先描述,再组织!!



共享内存的组成:

共享内存 = 共享内存数据结构 + 内存块



共享内存的代码部分

先做接口的使用验证,方便理解理论

共享内存部分也要使用两个可执行程序,因为也是通信


注意:这个共享内存一定是操作系统创建的,只不过是进程通过系统调用让系统创建的,而非进程创建的,本质还是系统,只不过是借助进程


而创建共享内存的使用系统调用工作就由 Server 来执行



函数 shmget 的介绍

使用函数 shmget:开辟一块空间作为共享内存

(所谓 shmget :就是 get a shared memory

在这里插入图片描述

这个函数需要理解的东西比较多,下面一一展开讲解各个参数:

  • key :这个后面讲解
  • size_t size 是创建共享内存的大小,是用户自定义大小(后续会将这个自定义的 size 放置在头文件 Comm.hpp 中,自定义大小为 4096 字节,即4KB)
  • int shmflg 这是一个标记位参数

这些标记位:IPC_CREATIPC_EXCL 其实就是只有一个比特位为 1 的整型的宏

通过或运算,将这些标记为计算成参数 int shmflg ,以位图形式传参,这个之前讲过



这两个宏 IPC_CREATIPC_EXCL 的意义:

  • IPC_CREAT 用于创建共享内存段,如果共享内存段已经存在,则会获取该共享内存段。
  • IPC_CREAT | IPC_EXCL 组合使用时,如果共享内存段不存在,则创建它;如果共享内存段已经存在,则返回错误。
  • IPC_EXCL 单独使用没有意义,通常与 IPC_CREAT 结合使用。

IPC_CREAT | IPC_EXCL 组合使用,查询共享内存 shm,如果不存在就创建,如果存在就报错返回(不要这个存在的),这句话的背后含义就是:只要成功,一定是新的共享内存!我不拿老的共享内存!



函数 shmget的参数 key


1、这是一个系统调用参数,代表肯定是需要用户输入的

2、 key 是用于表示共享内存的唯一性的标识符(类似于 进程 PID 的作用:进程唯一标识符)

例如描述共享内存的结构

struct shm
{
	int key;  // 共享内存标识符
	//....
}


key 值的共享


现在产生了两个问题:

问题一: 你怎么保证不同进程看到的是同一个共享内存?

问题二: 这个key,为什么要用户传入?操作系统内核自己生成不可以吗?



思考过程:

1、如果这个 key 是系统内核生成给进程的:

进程 A 使用系统调用创建共享内存,系统返回该共享内存的 key 给进程 A,进程 A 此时就拿到了该共享内存的 key 值,

但是,需要通信另一方进程 B 怎么获取这个共享内存的 key 呢?????

进程 B 是拿不到的!!

2、有人说进程 A 通过某种方式把该共享内存的 key 给进程 B:

既然可以直接给,不就已经可以通信了吗,还要什么共享内存!

这不就是先有鸡还是先有蛋的问题嘛!



回答问题

那如何保证不同进程看到的是同一个共享内存?

其实可以参考上节课学的命名管道中,另外定义的头文件 Comm.hpp 中定义的命名管道文件名路径

只要 ServerClient 都包含这个头文件,我们两端都能看到这个 命名管道文件名路径

我们的 key 也可以这样,设置到一个头文件中,两端都包含该头文件就可以获取同一个 key

有了 key 不就可以获取到同一个 共享内存了吗!~



key 的设置


前面说过,key 值是需要用户输入给系统调用 shmget,因此 key 值需要用户设计。

理论上可以设置任何数值,如果重复了就会冲突(因为需要保证唯一性),就需要用户重新设置一个,大白话就是这个 key 是碰出来的,需要你自己找一个值保证唯一性(一个一个试出来)。

而为了降低这种冲突率,我们通常会通过函数 ftok,传入 “当前进程的路径+一个 id 值”(路径实际上也可以随便写(最好设置成当前路径),id 值随便设置)

ftok 函数会通过某种算法,生成一个 key值,这个 key 值已经尽可能的趋于绝对唯一了
( 这个有点像我们之前学的,生成一个键值减少哈希冲突的方法)




我们通信双方的两个进程都设置一个公共的路径+公共 id 值,两个进程都能通过 ftok 函数获取到同一个 key



在这里插入图片描述




Linux是如何在表面的应用层面,保证不同进程看到同一份资源的?

本质上还是:同一个份路径+同一个份项目ID

(至于通过函数转换为 key值,这些都是进一步的操作,本质上还是不同进程先看到同一份路径+项目ID)


而命名管道通信,也是不同进程看到同一个命名管道文件路径,最终达到看到同一份资源的目的


命名管道通信 和 本课的共享内存

都是要通过先在一个头文件中设置同一个固定的 路径(还有ID),然后不同进程的程序中包含该头文件,最终获取到同一份固定的
路径(还有ID),得出找到 同一份资源的道路



验证两个进程会看到同一个 key

代码:

我们将共享路径和共享ID放到 Comm.hpp 中,两个程序 Server.cc / Cilent.cc 都引入该头文件,则都能获取到共享路径和共享ID,生成同一 key 值.


在这里插入图片描述




运行结果如下:如图,不同程序进程最后都得到同一个 key 值

在这里插入图片描述



加一点优化:把 10 进制的 key 值转换为 16 进制(字符串),为了好看一些

std::string ToHex(key_t k) {
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%x", k);
    return buffer;
}

在这里插入图片描述



共享内存的创建:代码实现


这个是 Server 做的事

代码如下:

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "Comm.hpp"
using namespace std;

int main()
{
    // 1、创建key
    // key_t ftok(const char *pathname, int proj_id);
    key_t key = ftok(g_path.c_str(), g_proId);
    cout << ToHex(key) << '\n';

    // 2、创建共享内存
    // int shmget(key_t key, size_t size, int shmflg);
    int shmid = shmget(key, g_shmsize, IPC_CREAT | IPC_EXCL);
    if(shmid < 0)
    {
        std::cerr << "error: shmget fail!" << '\n';
        return 1;
    }
    cout << "shmid: " << shmid << '\n';
    return 0;
}


运行结果如下:

返回值 shmid = 0 表示创建共享内存成功,这个创建共享内存的返回值 shmid 其实是:共享内存标识符

在这里插入图片描述

可是第二轮连续几轮 ./server 为什么 创建共享内存直接失败???

共享内存标识符

函数 shmget 会返回一个 a valid shared memory identifier 共享内存标识符

在这里插入图片描述



问题:为什么连续创建共享内存失败?

这个和共享内存本身有关

对于动态开辟的堆空间: 平时我们在堆上 malloc/new 申请的内存空间,如果用户不主动通过 free/delete 释放,当程序退出后,系统也会自己将对应的空间释放,其本质是这些申请的空间是属于一个进程的!当进程退出时,当然也就无需使用这些资源,就会被系统自动释放,这些空间资源生命周期随进程

对于共享内存来说: 虽然共享内存是进程自己通过系统调用创建的,但是本质上共享内存是系统借进程之手创建的,还是属于系统而非进程,因此进程退出,这块属于系统的共享内存不用关心进程的“死活”,共享内存的生命周期:随内核!


释放共享内存的方式:

  • 用户必须让OS释放

  • OS重启!

如果共享内存不释放,就会占用内存资源,这就是内存泄漏!!!



查看当前系统内存中的共享内存块

命令 :ipc -m

在这里插入图片描述

当前系统内部还存在这块共享内存,该共享内存的 key 就是我们之前 两个进程看到的同一个 key 值!

这个共享内存的shmid 就是第一次运行 ./server 创建共享内存的返回值!

也表示着该共享内存没有被删除,没有被系统回收



我们应该如何主动删除共享内存:

删除共享内存有两种方法:指令、和代码函数



共享内存的删除:指令删除

命令: ipcrm -m 共享内存的key? or id?

自己试试可以发现,直接使用 key 值是不能删除一个共享内存的,用户不能拿着 key 指定删除共享内存。

在这里插入图片描述



因此,我们需要通过指定 shmid 来删除该共享内存!

在这里插入图片描述


这里得出一个结论: 在用户层我们用于唯一指定一个共享内存的方式是该共享内存块的 shmid ,而并非 key 值!


shmid vs key

用户层唯一指定一个共享内存的是 shmid

系统层唯一指定一个共享内存的是 key

shmid :只给用户用的一个标识shm的标识符

key :只作为内核中,区分shm唯一性的标识符,不作为用户管理shm 的id 值

这个有点像:文件描述符对应一个文件的物理地址

用户层指定一个打开文件的是:文件描述符

系统层指定一个打开文件的是:物理地址


因此,之后我们讲解共享内存的管理,其实就是通过 shmid 管理的!



同时, shmid 的值是递增分配的,和进程的 pid 的分配方式相似

在这里插入图片描述



共享内存的删除:代码删除


函数 shmctl :获取共享内存的相关属性,同时可以通过传入一个命令参数 cmd 对这个共享内存进行某些操作

在这里插入图片描述


参数 cmd

在这里插入图片描述



第一个 cmd 选项:IPC_STAT

这个就是获取该共享内存的一些状态信息,它会将底层的数据结构 struct shmid_ds获取给你,你再通过指定来获取需要的属性

**简单来说:**你看到该函数的第三个参数是 struct shmid_ds *buf 了吗?你可以传入一个 struct shmid_ds
指针类型的数据给该函数,当你传入 cmd = IPC_STAT 时,该函数会给你传回一个 struct shmid_ds
结构,该结构记录着共享内存的相关消息

// int shmctl(int shmid, int cmd, struct shmid_ds *buf);
struct shmid_ds buf;
shmctl(shmid, IPC_STAT, &buf);
cout << "shm_cpid: " << buf.shm_cpid << '\n';
cout << "shm_atime: " << buf.shm_atime << '\n';
cout << "shm_segsz: " << buf.shm_segsz << '\n';

在这里插入图片描述



第三个 cmd 选项:IPC_RMID

该选项会将该共享内存标记成待删除,通过这个选项达到删除一个共享内存的目的


我通过该代码删除共享内存:

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "Comm.hpp"
using namespace std;

int main()
{
    // 1、创建key
    // ....
    
    // 2、创建共享内存
    // ....



    // 3、释放共享内存
    // int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    int ret = shmctl(shmid, IPC_RMID, nullptr);  // 不获取属性就传递nullptr
    if(ret < 0)
    {
        std::cerr << "error: remove shared memory fail!" << '\n';
        return 1;
    }
    cout << "共享内存删除成功!" << '\n';
    return 0;
}



代码运行结果如下:

我们同时开启了两个终端窗口,上面这个窗口用于运行 ./server,下面这个窗口用于每隔一秒钟执行一次 ipcs -m 命令来显示系统中的共享内存信息

可以发现,但./server运行启动,ipcs -m 查询到有个共享内存生成,几秒后运行到删除代码则删除该共享内存

在这里插入图片描述

实际上,我们上面的的程序仅仅是创建一个共享内存与获取他的 shmid ,而并没有建立好通信!即还未使用该共享内存

真正开始使用之前,必须将共享内存和进程建立联系,即将共享内存通过页表映射:即挂接到进程中

需要操作页表,一般就需要使用到系统调用



共享内存的挂接


函数 shmat()at 是指 attach

在这里插入图片描述



  • 参数 const void* shmaddr :用户指定挂接到你的进程的什么虚拟地址位置,目前我们用不到,直接定义成 NULL

  • 参数shmflg:设置选项参数,就是指定设置一些功能(如只读、只写,因为共享内存不仅仅可以用来进程间通信,而我们本节课主要用来进程间通信,因此直接设置成 0 即可)

  • 返回值void* 类型指针,是共享内存挂接到进程中的实际的虚拟地址位置

在这里插入图片描述

如果挂接成功就返回挂接的地址:如果该地址是你指定的,就返回你指定的那个地址;否则,按实际情况返回地址



代码如下:

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "Comm.hpp"
using namespace std;

int main()
{
    // 1、创建key
    // ....

    // 2、创建共享内存
    // ....


    // 3、共享内存挂接到进程虚拟地址空间
    // void *shmat(int shmid, const void *shmaddr, int shmflg);
    void* shmatRet = shmat(shmid, NULL, 0);

    // (long long)shmatRet
    // 将void*强转成整型:挂接成功则放回地址,失败返回-1
    // 为什么不能用 int 强转:因为int在64位机器上是4字节,而指针类型是8字节,因此直接转换编译器不允许(精度丢失)
    if((long long)shmatRet == -1)
    {
        cout << "shmatRet: " << (long long)shmatRet << '\n';
    }
    else cout << "共享内存 shmat 挂接成功, shmatRet: " << (long long)shmatRet << '\n';






    // 4、释放共享内存
    // ....

    return 0;
}



运行结果如下:

返回值 shmatRet == -1 :说明挂接失败

在这里插入图片描述


查询共享内存:nattch 表示挂接该共享内存的进程数

在这里插入图片描述
nattch 为零,也说明本次操作挂接失败,为什么?

这是因为当前没有权限挂接,解释如下:



共享内存权限 perms

观察上张图片中的 permsperms 是共享内存的权限!

perms 即为 permission

你当前的共享内存块的权限 perms = 0 表示你没有任何权限!当然也就不能挂接


因此我们需要先手动设置权限

方式:在创建共享内存的函数 shmget 的第三个参数(那个位图),按位或一个类型 mode_t 的共享内存权限值 mode



设置共享内存权限

1、先设置全局量:

mode_t g_mode = 0600; 

2、在函数 shmget 设置权限

int shmid = shmget(key, g_shmsize, IPC_CREAT | IPC_EXCL | g_mode);

运行结果如下:

在这里插入图片描述



挂接该共享内存,若使用完可不能直接释放,还要去关联:



共享内存的去关联


函数 shmdtshm(shared memory,共享内存)和 dt(detach,分离)的组合,全称是 “shared memory detach”。这个函数用于将共享内存段从调用进程的地址空间中分离出来。

该函数的参数是:一个地址,即之前使用函数 shmat 挂接共享内存的返回值(共享内存的实际挂接虚拟地址)

在这里插入图片描述



代码如下:

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "Comm.hpp"
using namespace std;

int main()
{
    // 1、创建key
    // ....

    // 2、创建共享内存
    // ....


    // 3、共享内存挂接到进程虚拟地址空间
    // ....


    // 4、去关联
    // int shmdt(const void *shmaddr);
    int shmdtRet = shmdt(shmatRet);
    if(shmdtRet != 0) 
    {
        cout << "error: shmdt 去关联 fail!" << '\n';
    }
    else cout << "共享内存 shmat 去关联成功" << '\n';


    sleep(3);   // 用于观察


    // 5、释放共享内存
    // ....

    return 0;
}

运行结果如下:共享内存 shmat 去关联成功

在这里插入图片描述



若共享内存被释放,而不去关联?若共享内存去关联,而不释放?

  1. 仅释放共享内存段
    • 如果你仅调用了 shmctl(shmid, IPC_RMID, NULL) 来释放共享内存段,而没有调用 shmdt 去关联,那么共享内存段会被标记为销毁,但进程仍然会持有对该共享内存段的引用
    • 进程的地址空间中仍然会保留指向已销毁共享内存段的指针,这会导致潜在的悬空指针问题和未定义行为
    • 相当于释放一个 malloc 的内存块,但是你的指针没有置为 NULL
  2. 仅去关联共享内存段
    • 如果你仅调用了 shmdt 去关联共享内存段,而没有调用 shmctl 释放,那么共享内存段仍然存在于系统中,只是当前进程不再访问它,相当于内存泄漏。
    • 其他进程仍然可以访问该共享内存段,直到最后一个进程去关联并且调用 shmctl 释放


【重要】中场总结


到现在我们还没有实现真正的通信,实际上 ServerClient 需要做的事基本一致,区别点都使用星号 ⭐ 标记出来了,如下:

对于 Server

1、获取key值

2、创建共享内存 ⭐

3、挂接共享内存

4、使用共享内存

5、去关联共享内存

6、删除共享内存 ⭐

对于 Client

1、获取key值

2、获取shmid ⭐

3、挂接共享内存

4、使用共享内存

5、去关联共享内存

对于 Client 来说, Client 无需 新建共享内存和删除共享内存 ,只需要通过 shmget 函数获取已存在的共享内存的 shmid 并使用该共享内存即可!



接下来,我们会将操作共享内存的相关方法封装到 共享内存类 class SharedMemory 中,并设计服务器类 class Server 和 客户端类 class Client,在服务器运行程序 Server.cc 和 客户端运行程序 Client.cc 中调用 共享内存类 中的各种方式操作该共享内存:

  • 创建共享内存 / 获取shmid
  • 挂接共享内存
  • 去关联共享内存
  • 删除共享内存


实现通信

  • 对于 Client :只需从键盘获取用户输入,并将内容 strcpy 拷贝到挂载的 共享内存 中,就实现了数据发送
  • 对于 Server :只需从挂载共享内存的指定地址中读取数据,就实现了数据的获取,要注意的是,循环获取数据时,当共享内存为空,我们可以人为控制其阻塞等待,直到有数据输入(因为共享内存没有互斥机制,需要人为控制)
  • Client 发送 quit 时,表示 Client 退出;当 Server 读取到 quit 时,表示本次通信结束, Server 也主动退出

Client 的业务逻辑

// 使用共享内存: Client 的业务逻辑
void UseShm(char* shmaddr)
{
    // 向共享内存的挂接地址写入数据
    std::string message;
    while(1)
    {
        std::cout << "Please Enter# ";
        std::getline(std::cin, message);
        strcpy(shmaddr, message.c_str());
        if(strcmp(message.c_str(), "quit") == 0)
        {
            std::cout << "Client quit!" << '\n';
            break;
        }
    }
}

Server 的业务逻辑

// 使用共享内存: Server的业务逻辑
void UseShm(char *shmaddr)
{
    // 向共享内存的挂接地址获取数据
    std::cout << "Client say# ";
    fflush(stdout);
    while (1)
    {
        if (shmaddr[0] == '\0')
        {
            // 共享内存没有数据,继续等待,手动阻塞等待
            continue;
        }
        else
        {
            std::cout << shmaddr << std::endl;


            if (strcmp(shmaddr, "quit") == 0)
            {
                std::cout << "Server quit!" << '\n';
                break;
            }

            std::cout << "Client say# ";
            fflush(stdout);
            shmaddr[0] = '\0'; // 清空共享内存
        }

        sleep(1);
    }
}


运行效果演示

在这里插入图片描述


共享内存通信的完整代码


共享内存头文件:SharedMemory.hpp


#pragma once
#include <iostream>
#include <string.h> // 用于字符串操作函数如memset等
#include <unistd.h> // 提供POSIX操作系统API,如sleep
#include <sys/types.h> // 定义数据类型如key_t
#include <sys/ipc.h> // 提供System V IPC函数原型、宏定义和数据结构
#include <sys/shm.h> // 提供共享内存的函数原型、宏定义和数据结构

// 用于key值的共享路径和共享ID
const std::string g_path = "/home/mine/linux-learning/_2025_01_31_pipe_04_Shared_Memory_IPC";
const int g_id = 111;

// 共享内存的大小
const int g_shm_size = 4097;

// 共享内存的创建和获取shmid的标志位
const int g_shm_create_flag = IPC_CREAT | IPC_EXCL;
const int g_shm_get_id_flag = IPC_CREAT;

// 共享内存的权限
const mode_t g_shm_mode = 0666;

// 共享内存挂载计数器
int g_shm_cnt = 0;



// 共享内存类:共享内存操作相关函数
class SharedMemory
{
public:
    SharedMemory() = default;
    ~SharedMemory() = default;

    // 1、获取key值
    key_t GetKey()
    {
        key_t key = ftok(g_path.c_str(), g_id); // 根据指定路径和项目ID生成唯一的IPC键 key
        if (key == -1) // 如果ftok失败
        {
            std::cerr << "ftok failed" << std::endl;
            return -1;
        }

        std::cout << "getKey success!" << std::endl;
        return key;
    }

    // 2、创建共享内存
    int CreateShm(key_t key, int shmflg)
    {
        int shmid = shmget(key, g_shm_size, shmflg);

        if (shmid < 0) // 如果shmget失败
        {
            // 如果是获取 shmid 失败
            if (shmflg == g_shm_get_id_flag) 
                std::cerr << "get shmid failed" << std::endl;
            // 如果是创建共享内存失败
            else
                std::cerr << "shmget failed" << std::endl;
            return -1;
        }

        std::cout << "shmget success!" << std::endl;
        return shmid;
    }

    // 3、挂接共享内存
    char *AttachShm(int shmid)
    {
        char *shmaddr = (char *)shmat(shmid, nullptr, 0);
        if (shmaddr == (char *)-1) // 如果shmat失败
        {
            std::cerr << "shmat failed" << std::endl;
            return nullptr;
        }

        g_shm_cnt++; // 增加挂接计数
        std::cout << "shmat success!" << std::endl;
        return shmaddr;
    }

    // 4、去关联共享内存
    int DetachShm(char *shmaddr)
    {
        int ret = shmdt(shmaddr);
        if (ret < 0)
        {
            std::cerr << "shmdt failed" << std::endl;
            return -1;
        }

        g_shm_cnt--; // 减少挂接计数
        std::cout << "DetachShm success!" << std::endl;
        return 0;
    }

    // 5、删除共享内存
    int DeleteShm(int shmid)
    {   
        int num = 10; // 设置尝试删除共享内存的最大次数
        while(g_shm_cnt > 0){ // 当还有进程挂载该共享内存时循环
            // 输出提示信息表示共享内存正在被使用
            std::cerr << "shm is using" << std::endl;
            
            // 等待一秒后重试
            sleep(1);
            if(--num == 0){ // 如果尝试次数耗尽仍无法删除,则返回失败
                std::cerr << "多次重试, 仍有进程挂载, delete failed!" << std::endl;
                return -1;
            }
        }

        int ret = shmctl(shmid, IPC_RMID, nullptr);
        if (ret < 0)
        {
            std::cerr << "shmctl failed" << std::endl;
            return -1;
        }

        std::cout << "DeleteShm success!" << std::endl;
        return 0;
    }
};


// 实例化一个SharedMemory对象,便于在程序中使用
SharedMemory sharedmemory;


服务端头文件: Server.hpp


#pragma once

#include <iostream>
#include "SharedMemory.hpp"

/*
1、获取key值
2、创建共享内存
3、挂接共享内存
4、使用共享内存
5、去关联共享内存
6、删除共享内存
*/

class Server
{
public:
    Server() = default;
    ~Server() = default;

    // 使用共享内存: Server的业务逻辑
    void UseShm(char *shmaddr)
    {
        // 向共享内存的挂接地址获取数据
        std::cout << "Client say# ";
        fflush(stdout);
        while (1)
        {
            if (shmaddr[0] == '\0')
            {
                // 共享内存没有数据,继续等待,手动阻塞等待
                continue;
            }
            else
            {
                std::cout << shmaddr << std::endl;
                

                if (strcmp(shmaddr, "quit") == 0)
                {
                    std::cout << "Server quit!" << '\n';
                    break;
                }

                std::cout << "Client say# ";
                fflush(stdout);
                shmaddr[0] = '\0'; // 清空共享内存
            }

            sleep(1);
        }
    }
};



客户端头文件: Client.hpp


#pragma once


#include <iostream>
#include <string>
#include <sys/shm.h>
#include <sys/shm.h>
#include "SharedMemory.hpp"


// Client 负责向共享内存写入

/*
1、获取key值
2、获取shmid
3、挂接共享内存
4、使用共享内存
5、去关联共享内存
*/

class Client
{
public:
    Client() = default;
    ~Client() = default;

    // 使用共享内存: Client 的业务逻辑
    void UseShm(char* shmaddr)
    {
        // 向共享内存的挂接地址写入数据
        std::string message;
        while(1)
        {
            std::cout << "Please Enter# ";
            std::getline(std::cin, message);
            strcpy(shmaddr, message.c_str());
            if(strcmp(message.c_str(), "quit") == 0)
            {
                std::cout << "Client quit!" << '\n';
                break;
            }
        }
    }
};


服务端通信程序: Server.cc


#include "Comm.hpp"
#include "Server.hpp"

int main()
{
    Server server;

    // 1、获取key值
    key_t key = sharedmemory.GetKey();
    std::cout << "key: " << key << std::endl;

    // 2、创建共享内存
    int shmid = sharedmemory.CreateShm(key, g_shm_create_flag | g_shm_mode);

    // 3、挂接共享内存
    char *shmaddr = sharedmemory.AttachShm(shmid);

    // 4、使用共享内存
    server.UseShm(shmaddr);

    // 5、去关联共享内存
    sharedmemory.DetachShm(shmaddr);

    // 6、删除共享内存
    sharedmemory.DeleteShm(shmid);

    return 0;
}


客户端通信程序: Client.cc


#include "Comm.hpp"
#include "Client.hpp"

int main()
{
    Client client;

    // 1、获取key值
    key_t key = sharedmemory.GetKey();
    std::cout << "key: " << key << std::endl;

    // 2、创建共享内存
    int shmid = sharedmemory.CreateShm(key, g_shm_get_id_flag);

    // 3、挂接共享内存
    char *shmaddr = sharedmemory.AttachShm(shmid);

    // 4、使用共享内存
    client.UseShm(shmaddr);

    // 5、去关联共享内存
    sharedmemory.DetachShm(shmaddr);

    return 0;
}


问题:在操作共享内存时,为什么可以直接操作而无需使用系统调用?

对于管道: 管道一旦建好了,不管命名还是匿名,我们都要用read和 write来进行调用把数据从用户拷贝到我们的文件缓存区里,然后从文件缓冲区域再通过 write通过read拷贝到我自己的用户缓冲区,是需要要用系统调用


**对于共享内存:**为什么不用系统调用,

我们的地址空间当中含有代码区、数据区、代码区、字符常量区以初始化数据区、未初始化数据区、堆区栈区

堆栈之间是共亨区

地址空间当中包括共亨区在内的这部分区域:叫用户空间

共享内存一旦被我们挂接到地址空间,我们用户可以直接使用,不需要使用系统调用

就如同这个 malloc 的内存一样, malloc出来的空间是在堆上的,它也是在我们的用户空间



共享内存通信的特点


(1)共享内存通信是最快的,没有之一!

因为共享内存只需两次拷贝,就能完成读写:

写入:只需要将数据从外设缓冲区直接拷贝到物理内存中,

读取:只需要将数据从物理内存直接拷贝到外设缓冲区中,


而管道通信中间借助文件,需要四次拷贝才能完成读写


(2)共享内存没有相应的保护机制

例如管道通信,会有互斥等保护机制,共享内存没有

既然没有保护机制,我们要自己解决,我们可以通过使用命名管道解决该问题,为我们的共享内存通信添加一个管道通信,相当于加上一层管道通信



共享内存通信的优化:借助命名管道的保护机制


通过命名管道(FIFO)为共享内存通信添加额外的同步层。具体方法如下:


机制概述

创建两个管道:一号管道 和 二号管道

通过标记位传递的方式告知对方我的工作结束了,你的工作可以开始了,具体如下:


对于 Client 进程(即 shm 的写端)

1、读取一号管道:用作等待 Server 进程(即 shm 的读端) 的 共享内存读取完成

意思是 Client 进程 必须拿到 Server 进程 的结束工作信号, Client 进程 才能开始自己的 写共享内存 的操作

2、写数据,到共享内存

3、写入二号管道:用作写入结束信号,表示自己写入结束,反馈信号给 Server 进程(即 shm 的读端)



对于 Server 进程(即 shm 的读端)

1、读取二号管道:用作等待 Client 进程(即 shm 的写端) 的 共享内存写入完成

2、从共享内存读取数据

3、写入一号管道:用作读取结束信号,表示自己读取结束,反馈信号给 Client 进程(即 shm 的写端)



图示如下


在这里插入图片描述




死循环与解决策略

由于双方都需要对方的操作结束信号才能进行下一步操作,可能会导致死循环。为此,需预先向管道内写入初始信号,使写端能先开始操作。同时,利用命名管道对读写端的自然限制(如阻塞等待数据),可有效避免数据竞争。

比如这个函数:先提前向一号管道写入标志位,告知 Client 端可以写入数据,以此作为整个流程的开始

// 一号管道初始化
void InitIPC()
{
    // 提前给一号管道写入标志位
    int fd = OpenPipe(g_open_for_write, FIFO_NAME1);
    WritePipe(fd);
    close(fd);
}


整个运行流程


  1. 手动向一号管道写入启动信号。
  2. 写端读取到该信号后开始写入共享内存,并在完成后向二号管道发送完成信号。
  3. 读端监听到二号管道的信号后,开始从共享内存中读取数据,并在读取完成后向一号管道写入完成信号。
  4. 进程以此模式持续循环。

这种设计确保了共享内存访问的安全性与效率,同时也解决了潜在的死锁问题。



三个问题


问题一:不如直接用管道通信?

在管道里写的,它的数据量不会大都是一个字节的

所以内核到用户用户到内核拷贝的数据量比较小,

我们可以使用共享内存来传递大块数据,而管道只传递小量数据

就能在拷贝少量数据的情况下,实现两者快速通信。



问题三:为什么需要两个管道?

我们如果使用一个管道,在管道中写入一个 1 作为标记,

对于 Server 端来说:

Server端(即为写shm端)读管道获取该标记,表示允许开始写 shm:这一层 Server端 是 该管道的读端

Server端写完 shm 后,向管道重新写入一个 1,表示我已经写完shm,你可以开始读了:此时 Server端是该管道的 写端

对于 Client 端来说:

我从管道获取到标志位 1,表示我可以开始读取 shm,此时我是管道的 读端

我读取完shm后,向管道写入标志位 1,表示我读取 shm 结束,此时我是管道的 写端


对于通信双方,各方都需要对管道读和写,而我们之前学习管道时讨论过,管道通信时单向的,是半双工通信,对于同一个管道来说,通信的任一方不能同时拥有对该管道的读和写的权力

这意味着数据只能在一个方向上传输:要么从一个进程流向另一个进程,要么反方向流动,但对于同一个管道实例而言,不能同时进行双向的数据传输。因此,在使用管道进行进程间通信时,通常需要创建一对管道来实现全双工通信,即允许两个进程同时互相发送和接收消息。



问题三:那为啥不直接在共享内存里加标记位,而需要通过管道传递?


1、数据原子性问题:

标志位不一定是原子的:

如果该标志位只占一个字节:其实是可以用于标记是否可以读写的,一个字节的修改在汇编层面只有一条 move 指令,具有原子性,即用即修改

如果该标志位需要++这类的操作:在汇编层面有三条指令(需要取数据,在CPU运行,更新数据),则不具有原子性,当写端在写操作进行标记位修改,而读端需要检测标记位的状态(以便进入读操作),此时因为写端对标记位修改有三条指令,就有可能出现,读端获取的标记位的状态时旧数据,而写端还没来及修改,读端就将数据拿走了


2、不停检测导致资源浪费问题:

读写端需要对标记位进行不停检测,才知道是否可以进行读操作或写操作嘛

这样不断的检测会导致资源浪费


小结

学习这个解决办法也是为了学习如何对一个共享资源保护问题,

这些操作也仅仅是对共享内存通信的略加保护,要实现真正保护还需用到 加锁



完整实现代码


管道保护者类的设计 PipeProtecter


1、设计初始化通信函数 InitIPC

提前给一号管道写入标志位(目的是破开双管道读写循环)



2、创建管道

  • 创建管道 CreateFIFO :输入指定管道名字创建管道
  • 创建两个不同管道 CreateDoublePipe :因为程序需要一对管道,命名不同,因此需要调用两次 创建管道函数 CreateFIFO
  • 两个管道名 与 创建权限 设置为全局变量:
// 创建管道的命名和权限
const std::string FIFO_NAME1 = "./myfifo1";
const std::string FIFO_NAME2 = "./myfifo2";
const mode_t g_mode = 0666;


3、打开管道和关闭管道函数

  • 打开管道 OpenPipe
  • 关闭管道 ClosePipe
  • 关闭两个管道 CloseDoublePipe :调用两次

打开管道用于读 和 用于写 需要的标志位不同,设置为全局量易于修改和增加可读性

// 打开管道用于读 和 用于写
const int g_open_for_read = O_RDONLY;
const int g_open_for_write = O_WRONLY;


4、写管道和读管道

读管道 ReadPipe :从管道中读取一个 标志位大小的数据

写管道 WritePipe :往管道中写入一个 标志位大小的数据

// 管道标记位
const int FLAG = 111;


5、私有成员 和 Set 函数

两个文件描述符 fdint fd1, fd2;

通信双方实际上使用的不是同一个 class PipeProtecter ,因为存在写时拷贝

各自打开两个管道文件的 fd 不一定相同,因此可以使用函数 SetFd 存入本方的两个 fd

#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

// 创建管道的命名和权限
const std::string FIFO_NAME1 = "./myfifo1";
const std::string FIFO_NAME2 = "./myfifo2";
const mode_t g_mode = 0666;

// 打开管道用于读 和 用于写
const int g_open_for_read = O_RDONLY;
const int g_open_for_write = O_WRONLY;

// 管道标记位
const int FLAG = 111;

class PipeProtecter
{
public:
    PipeProtecter() = default;
    ~PipeProtecter() = default;

    // 创建一对管道
    void CreateDoublePipe()
    {
        CreateFIFO(FIFO_NAME1);
        CreateFIFO(FIFO_NAME2);
    }

    // 创建管道
    bool CreateFIFO(const std::string &fifo_name)
    {
        int ret = mkfifo(fifo_name.c_str(), g_mode);
        if (ret < 0)
        {
            std::cerr << "mkfifo failed!" << '\n';
            return false;
        }

        std::cout << "mkfifo \"" + fifo_name + "\" success!" << '\n';
        return true;
    }

    // 打开管道
    int OpenPipe(int open_flag, const std::string &fifo_name)
    {
        int fd = open(fifo_name.c_str(), open_flag);
        if (fd < 0)
        {
            std::cerr << "open \"" + fifo_name + "\" failed!" << '\n';
            return -1;
        }

        std::cout << "open \"" + fifo_name + "\" success!" << '\n';
        return fd;
    }


    // 关闭两个管道
    void CloseDoublePipe()
    {
        ClosePipe(fd1);
        ClosePipe(fd2);
    }

    // 关闭管道
    void ClosePipe(int fd)
    {
        close(fd);
    }

    // 读取管道
    void ReadPipe(int fd)
    {
        int flag = -1;
        int ret = read(fd, &flag, sizeof(FLAG));
        if (ret < 0)
        {
            std::cerr << "read failed!" << '\n';
            return;
        }
    }

    // 写入管道
    void WritePipe(int fd)
    {
        int ret = write(fd, &FLAG, sizeof(FLAG));
        if (ret < 0)
        {
            std::cerr << "write failed!" << '\n';
            return;
        }
    }


    // 设置两个管道的 fd
    void SetFd(int _fd1, int _fd2)
    {
        fd1 = _fd1;
        fd2 = _fd2;
    }


    // 一号管道初始化
    void InitIPC()
    {
        // 提前给一号管道写入标志位
        int fd = OpenPipe(g_open_for_write, FIFO_NAME1);
        WritePipe(fd);
        close(fd);
    }

private:
    int fd1, fd2;
};


PipeProtecter pipeProtecter;


Server 的优化


对于 Server 程序 Server.cc

添加三个与管道相关的操作

  • 创建一对管道 CreateDoublePipe
  • 初始化管道 InitIPC
  • 删除管道 CloseDoublePipe

#include "Server.hpp"
#include "PipeProtecter.hpp"

int main()
{
    Server server;

    // 1、获取key值
	// ...
    
    // 2、创建共享内存
	// ...
    
    // 3、挂接共享内存
	// ...

    // 4、创建一对管道 + 初始化管道
    pipeProtecter.CreateDoublePipe();
    pipeProtecter.InitIPC();

    // 5、使用共享内存
	// ...

    // 6、去关联共享内存
	// ...

    // 7、删除共享内存
	// ...

    // 8、删除管道
    pipeProtecter.CloseDoublePipe();

    return 0;
}


对于 Server.hppUsePipe 函数

核心就是三个步骤:

  • 打开二号管道进行读
  • 向共享内存写入数据
  • 打开一号管道进行写

在这之前,先打开两个管道,并设置两个 fd

相比于上一个版本的 UseShm 函数,本版本可以直接写入管道,而无需向上个版本那样需要手动阻塞


// 使用共享内存2.0: Server的业务逻辑
void UseShm(char *shmaddr)
{
    // 1、打开二号管道进行读
    // 2、向共享内存写入数据
    // 3、打开一号管道进行写

    // 打开二号管道
    int fd2 = pipeProtecter.OpenPipe(g_open_for_read, FIFO_NAME2);
    // 打开一号管道
    int fd1 = pipeProtecter.OpenPipe(g_open_for_write, FIFO_NAME1);
    // 将两个管道的 fd 记录在 pipeProtecter 中
    pipeProtecter.SetFd(fd1, fd2);

    while (1)
    {
        std::cout << "Client say# ";

        // 读取 二号管道: 读取不到就会阻塞
        pipeProtecter.ReadPipe(fd2);

        // 读取共享内存(加上管道互斥保护)
        std::cout << shmaddr << std::endl;

        if (strcmp(shmaddr, "quit") == 0)
        {
            std::cout << "Server quit!" << '\n';
            break;
        }

        // 写入 一号管道
        pipeProtecter.WritePipe(fd1);
    }
}


Client 的优化


对于 Client 程序 Client.cc

添加一个与管道相关的操作

  • 删除管道 CloseDoublePipe

#include "Client.hpp"
#include "PipeProtecter.hpp"


int main()
{
    Client client;

    // 1、获取key值
    // ....

    // 2、创建共享内存
    // ....

    // 3、挂接共享内存
    // ....

    // 4、使用共享内存
    // ....

    // 5、去关联共享内存
    // ....
    
    // 6、删除管道
    pipeProtecter.CloseDoublePipe();

    return 0;
}


对于 Client.hppUsePipe 函数

核心就是三个步骤:

  • 打开一号管道进行读
  • 向共享内存写入数据
  • 打开二号管道进行写

在这之前,先打开两个管道,并设置两个 fd

// 使用共享内存: Client 的业务逻辑
void UseShm(char *shmaddr)
{
    // 1、打开一号管道进行读
    // 2、向共享内存写入数据
    // 3、打开二号管道进行写


    // 打开一号管道
    int fd1 = pipeProtecter.OpenPipe(g_open_for_read, FIFO_NAME1);
    // 打开二号管道
    int fd2 = pipeProtecter.OpenPipe(g_open_for_write, FIFO_NAME2);
    // 将两个管道的 fd 记录在 pipeProtecter 中
    pipeProtecter.SetFd(fd1, fd2);

    std::string message;
    while (1)
    {
        std::cout << "Please Enter# ";
        std::getline(std::cin, message);

        // 读取 一号管道: 读取不到就会阻塞
        pipeProtecter.ReadPipe(fd1);

        // 将数据写入共享内存(加上管道互斥保护)
        strcpy(shmaddr, message.c_str());
        if (strcmp(message.c_str(), "quit") == 0)
        {
            std::cout << "Client quit!" << '\n';
            break;
        }

        // 写入 二号管道
        pipeProtecter.WritePipe(fd2);
    }
}


运行效果演示

请添加图片描述



写程序过程中的注意事项

这是我写本文的程序过程中,发现几点容易犯错或不注意的点:

注意事项:



1、Server 端需要主动调用删除shm函数,用于删除shm


2、打印语句,若打印不出,要主动刷新

std::cout << "Client say# ";
fflush(stdout);



3、获取 key 值的 ftok 函数需要 路径 和 ID 是有限制的!不能随便设置

  • 路径:该路径必须真实存在!
  • ID:范围通常是 0 到 255 之间的整数值
  • 错误处理:ftok函数可能会失败。如果指定的路径不存在或无法访问,或者proj_id参数超出范围(通常是0到255之间的整数值),ftok会返回-1。



4、共享内存的相关操作函数设计,调用系统调用和库函数,建议都加上创建成功或失败的提示,增加可读性,如:

key_t key = ftok(g_path.c_str(), g_id);
if (key == -1)
{
    std::cerr << "ftok failed" << std::endl;
    return -1;
}

std::cout << "getKey success!" << std::endl;



5、Client 写入共享内存中,需要拷贝进去!

std::getline(std::cin, message);
strcpy(shmaddr, message.c_str());



6、要控制 server 等待所有进程去关联后,再删掉共享内存

可以设计一个全局的计数器,记录挂载共享内存的数量,每次去关联都减一,每次挂载都加一

删除函数中,若计数器不为零,则重试等待,多次重试挂载数仍不为 0 ,则退出删除失败

// 5、删除共享内存
int DeleteShm(int shmid)
{   
    int num = 10;
    while(g_shm_cnt > 0){
        // 共享内存正在使用:表示仍有进程挂载该共享内存
        std::cerr << "shm is using" << std::endl;

        // 循环不断重试删除, 一秒重复一次
        sleep(1);
        if(--num == 0){
            std::cerr << "多次重试, 仍有进程挂载, delete failed!" << std::endl;
            return -1;
        }
    }

    // ...
}



7、若你启动程序时,发现 共享内存 创建失败,有可能是当前内存中已存在该共享内存,之前可能没删除

ipcs -m 查看一下

ipcrm -m "shmid" :删除指定 shmid 的共享内存



8、函数 shmget 既可以创建共享内存,也可以获取已创建共享内存的 shmid

这只需要传递不同的 flag 就可以实现

const int g_shm_create_flag = IPC_CREAT | IPC_EXCL;// 创建共享内存
const int g_shm_get_id_flag = IPC_CREAT;		   // 获取已创建共享内存的 shmid



9、写时拷贝问题

需要注意的是:虽然我们 serverclient 包含的是同一个头文件 SharedMemory.hpp ,但是如果 serverclient 各自会对该头文件内的某些数据做修改,如修改 class SharedMemory 的成员变量,他们两个之间会有 写时拷贝,意思是两者的修改互不影响,

另一方面来说,就不能将 class SharedMemory 的成员变量用作他们的共享资源,一方修改了是不能通过头文件共享给对方的,而是会发送写时拷贝

理解好这点,我们应该做的:保持原来的路径+项目ID是不能修改的常量(保证一定是共享的!),而 ”后天“ 计算的 shmid 等值可以作为 class SharedMemory 的成员变量



10、两者挂接到自己虚拟地址空间中的地址是不一定一样的!



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

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

相关文章

渗透测试之WAF组合条件绕过方式手法详解以及SQL注入参数污染绕过

目录 组合绕过waf ​先看一些语句 绕过方式 我给出的注入语句是&#xff1a; 这里要注意的几点是&#xff1a; 组合绕过方式 完整过狗注入语句集合 http请求分块传输方法 其它方式绕过 http参数污染绕过waf 面试题:如何参数污染绕过waf 可以通过http参数污染绕过wa…

oracl:多表查询>>表连接[内连接,外连接,交叉连接,自连接,自然连接,等值连接和不等值连接]

SQL&#xff08;Structured Query Language&#xff0c;结构化查询语言&#xff09;是一种用于管理和操作关系数据库的标准编程语言。 sql分类: 数据查询语言&#xff08;DQL - Data Query Language&#xff09; 查询的关键词 select 多表查询>>表连接 表连接: 把2个…

ARIMA详细介绍

ARIMA&#xff08;AutoRegressive Integrated Moving Average&#xff0c;自回归积分滑动平均模型&#xff09;是一种用于时间序列分析和预测的统计模型。它结合了自回归&#xff08;AR&#xff09;、差分&#xff08;I&#xff09;和移动平均&#xff08;MA&#xff09;三种方…

飞致云开源社区月度动态报告(2025年1月)

自2023年6月起&#xff0c;中国领先的开源软件公司飞致云以月度为单位发布《飞致云开源社区月度动态报告》&#xff0c;旨在向广大社区用户同步飞致云旗下系列开源软件的发展情况&#xff0c;以及当月主要的产品新版本发布、社区运营成果等相关信息。 飞致云开源运营数据概览&…

【搜索回溯算法篇】:拓宽算法视野--BFS如何解决拓扑排序问题

✨感谢您阅读本篇文章&#xff0c;文章内容是个人学习笔记的整理&#xff0c;如果哪里有误的话还请您指正噢✨ ✨ 个人主页&#xff1a;余辉zmh–CSDN博客 ✨ 文章所属专栏&#xff1a;搜索回溯算法篇–CSDN博客 文章目录 一.广度优先搜索&#xff08;BFS&#xff09;解决拓扑排…

WPS怎么使用latex公式?

1、下载并安装mathtype https://blog.csdn.net/weixin_43135178/article/details/125143654?sharetypeblogdetail&sharerId125143654&sharereferPC&sharesourceweixin_43135178&spm1011.2480.3001.8118 2、将mathtype嵌入在WPS MathType面板嵌入器,免费工具…

简单的爱心跳动表白网页(附源码)

一&#xff1a;准备工作 在开始之前&#xff0c;确保已经具备基础的 HTML、CSS 和 JavaScript 知识。同时&#xff0c;也要准备好一个代码编辑器&#xff0c;比如 VS Code 或 Sublime Text。接下来&#xff0c;我们需要创建三个文件&#xff1a;index.html、styles.css 和 scr…

【AI】DeepSeek 概念/影响/使用/部署

在大年三十那天&#xff0c;不知道你是否留意到&#xff0c;“deepseek”这个词出现在了各大热搜榜单上。这引起了我的关注&#xff0c;出于学习的兴趣&#xff0c;我深入研究了一番&#xff0c;才有了这篇文章的诞生。 概念 那么&#xff0c;什么是DeepSeek&#xff1f;首先百…

【4Day创客实践入门教程】Day3 实战演练——桌面迷你番茄钟

Day3 实战演练——桌面迷你番茄钟 目录 Day3 实战演练——桌面迷你番茄钟1. 选择、准备元件、收集资料2. 硬件搭建3.编写代码 Day0 创想启程——课程与项目预览Day1 工具箱构建——开发环境的构建Day2 探秘微控制器——单片机与MicroPython初步Day3 实战演练——桌面迷你番茄钟…

AndroidCompose Navigation导航精通1-基本页面导航与ViewPager

文章目录 前言基本页面导航库依赖导航核心部件简单NavHost实现ViewPagerPager切换逻辑图阐述Pager导航实战前言 在当今的移动应用开发中,导航是用户与应用交互的核心环节。随着 Android Compose 的兴起,它为开发者提供了一种全新的、声明式的方式来构建用户界面,同时也带来…

Node.js——body-parser、防盗链、路由模块化、express-generator应用生成器

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…

C语言指针专题四 -- 多级指针

目录 1. 多级指针的核心原理 1. 多级指针的定义 2. 内存结构示意图 3. 多级指针的用途 2. 编程实例 实例1&#xff1a;二级指针操作&#xff08;修改一级指针的值&#xff09; 实例2&#xff1a;动态二维数组&#xff08;二级指针&#xff09; 实例3&#xff1a;三级指…

深度学习的应用

目录 一、机器视觉 1.1 应用场景 1.2 常见的计算机视觉任务 1.2.1 图像分类 1.2.2 目标检测 1.2.3 图像分割 二、自然语言处理 三、推荐系统 3.1 常用的推荐系统算法实现方案 四、图像分类实验补充 4.1 CIFAR-100 数据集实验 实验代码 4.2 CIFAR-10 实验代码 深…

RabbitMQ 多种安装模式

文章目录 前言一、Windows 安装 RabbitMq1、版本关系2、Erlang2.1、下载安装 Erlang 23.12.2、配置 Erlang 环境变量 3、RabbitMQ3.1、下载安装 RabbitMQ 3.8.93.2、环境变量3.3、启动RabbitMQ 管理插件3.3、RabbitMQ3.4、注意事项 二、安装docker1、更新系统包&#xff1a;2、…

吴恩达深度学习——有效运作神经网络

内容来自https://www.bilibili.com/video/BV1FT4y1E74V&#xff0c;仅为本人学习所用。 文章目录 训练集、验证集、测试集偏差、方差正则化正则化参数为什么正则化可以减少过拟合Dropout正则化Inverted Dropout其他的正则化方法数据增广Early stopping 归一化梯度消失与梯度爆…

DDD - 微服务架构模型_领域驱动设计(DDD)分层架构 vs 整洁架构(洋葱架构) vs 六边形架构(端口-适配器架构)

文章目录 引言1. 概述2. 领域驱动设计&#xff08;DDD&#xff09;分层架构模型2.1 DDD的核心概念2.2 DDD架构分层解析 3. 整洁架构&#xff1a;洋葱架构与依赖倒置3.1 整洁架构的核心思想3.2 整洁架构的层次结构 4. 六边形架构&#xff1a;解耦核心业务与外部系统4.1 六边形架…

数据结构与算法之二叉树: LeetCode LCP 10. 二叉树任务调度 (Ts版)

二叉树任务调度 https://leetcode.cn/problems/er-cha-shu-ren-wu-diao-du/description/ 描述 任务调度优化是计算机性能优化的关键任务之一。在任务众多时&#xff0c;不同的调度策略可能会得到不同的总体执行时间&#xff0c;因此寻求一个最优的调度方案是非常有必要的 通…

玩转大语言模型——配置图数据库Neo4j(含apoc插件)并导入GraphRAG生成的知识图谱

系列文章目录 玩转大语言模型——使用langchain和Ollama本地部署大语言模型 玩转大语言模型——ollama导入huggingface下载的模型 玩转大语言模型——langchain调用ollama视觉多模态语言模型 玩转大语言模型——使用GraphRAGOllama构建知识图谱 玩转大语言模型——完美解决Gra…

计算机毕业设计Python+CNN卷积神经网络考研院校推荐系统 考研分数线预测 考研推荐系统 考研爬虫 考研大数据 Hadoop 大数据毕设 机器学习

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

OpenCV:闭运算

目录 1. 简述 2. 用膨胀和腐蚀实现闭运算 2.1 代码示例 2.2 运行结果 3. 闭运算接口 3.1 参数详解 3.2 代码示例 3.3 运行结果 4. 闭运算的应用场景 5. 注意事项 相关阅读 OpenCV&#xff1a;图像的腐蚀与膨胀-CSDN博客 OpenCV&#xff1a;开运算-CSDN博客 1. 简述…