通信之道:解锁Linux进程间通信的无限可能(二)

目录

[一:system V进程间通信](#一:system V进程间通信)

[二:system V共享内存](#二:system V共享内存)

1:共享内存的基本原理

2:共享内存数据结构

3:共享内存的建立与释放

4:共享内存的创建

4.1:代码示例1

5:共享内存的释放

5.1:使用命令释放共享内存

5.2:使用程序释放共享内存

5.2.1:代码示例

6:共享内存的关联

6.1:代码示例1

6.2:代码示例2

7:共享内存的去关联

7.1:代码示例1

[8:使用共享内存实现Server &Client通信](#8:使用共享内存实现Server &Client通信)

8.1:SharedMemory.hpp

8.1.1:构造函数的实现

8.1.2:共享内存模块

8.1.2.1:获取共享内存的key值

8.1.2.2:GetSharedMemory函数

8.1.2.3:GetShmUerCreate函数

8.1.2.4:GetShmForUser函数

8.1.2.5:Attach(挂接共享内存)

8.1.2.6:Detach(分离共享内存)

8.1.2.7:Debugshm函数(获取共享内存状态)、

8.1.2.8:ZeroClearing(清空数据)

8.1.3:析构函数的实现

8.1.4:总代码

8.2:NamedPipe.hpp

8.3:Server.cpp

8.4:Client.cpp

9:共享内存与管道的对比

9.1:管道通信

9.2:共享内存通信


一:system V进程间通信

管道通信本质是基于文件 的,也就是说操作系统并没有为此做过多的设计工作,而system V IPC是操作系统特地设计的一种通信方式。但是不管怎么样,它们的本质都是一样的,都是在想尽办法让不同的进程看到同一份由操作系统提供的资源。

system V IPC提供的通信方式有以下三种:

  1. system V共享内存.
  2. system V消息队列.
  3. system V信号量.

其中,system V共享内存和system V消息队列 是以传送数据 为目的,而system V信号量 是为了保证进程间的同步与互斥而设计的,虽然system V信号量和通信好像没有直接关系,但都属于通信范畴.

二:system V共享内存

1:共享内存的基本原理

共享内存让不同进程看到同一份资源的方式就是,在物理内存中申请一块内存空间,然后将这块内存空间分别与各个进程的页表之间建立映射,再在虚拟地址空间中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫共享内存.

  • PS:这里所说的开辟物理空间、建立映射等操作都是调用系统接口完成的,也就是说这些动作都是由操作系统来完成.

2:共享内存数据结构

在系统当中可能会有大量的进程在进行通信,因此系统当中就可能存在大量的共享内存,那么操作系统就要对其进行管理,因此共享内存除了在内存当中真正开辟空间之外,系统还一定要为共享内存维护相关的内核数据结构.

共享内存的数据结构.

cpp 复制代码
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 */
};
  • 当我们申请了一块共享内存后,为了让要实现通信的进程能够看到同一个共享内存,因此每一个共享内存被申请时都有一个key值,这个key值用于标识系统中共享内存的唯一性.
  • 可以看到上面共享内存数据结构的第一个成员是shm_permshm_perm是一个ipc_perm类型的结构体变量.
  • 每个共享内存的key值存储在shm_perm这个结构体变量中,其中ipc_perm结构体的定义如下
cpp 复制代码
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;
};

3:共享内存的建立与释放

共享内存的建立大致包括以下两个过程:

  1. 在物理内存中申请共享内存空间.
  2. 将申请到的共享内存挂接到地址空间,即建立映射关系.

共享内存的释放大致包括以下两个过程:

  1. 将共享内存与地址空间关联,即取消映射关联.
  2. 释放共享内存空间,即将物理内存归还给系统.

4:共享内存的创建

cpp 复制代码
int shmget(key_t key, size_t size, int shmflg);

shmget函数的参数说明

1.:第一个参数key:是用来标识共享内存的唯一性字段(由用户形成)

  • 为什么由用户设置---->因为想让两个进程不用通过通信,就能得到同一个key,然后A进程创建key,B进程来获取key.

2:第二个参数size:表示共享内存的大小.

3:第三个参数shmflg:表示创建共享内存的方式---->可以使用位图的方式传递.

  • IPC_CREAT:传入这个参数表示,如果创建的共享内存不存在,则创建,如果存在,获取该共享内存并返回.
  • IPC_EXCL:单独使用这个参数没有意义.
  • IPC_CREAT | IPC_EXCL:传入这个参数表示,如果创建的共享内存不存在,则创建,如果存在,则出错返回.

按照第一种方式传参总能获取一段共享内存,按照第三种方式传参,如果成功返回了,那么这段shm是全新的.

shmget函数的返回值说明:

  • shmget调用成功,成功返回一个非负整数,即该共享内存段的标识码.
  • 失败则返回-1.

PS:我们将具有标定某种资源能力的东西叫做句柄,而shmget函数的返回值实际上是共享内存的句柄,这个句柄可以在用户层标识共享内存,当共享内存被创建后,在后续使用共享内存的相关接口时,都通过句柄对指定共享内存进行各种操作.
对于传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取

ftok函数的函数原型如下:

此函数的作用是,将已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,key值会被填充进维护共享内存的数据结构中.

PS:pathname所指定的文件必须存在且可存取。

注意:

  1. 使用ftok函数生成key值可能会产生冲突,此时可以对传入的参数进行修改。
  2. 需要进行通信的各个进程,在使用ftok函数获取key值时,都需要采用同样的路径名和整数标识符,进而生成同一种key值,然后才能找到同一段共享内存.

4.1:代码示例1

cpp 复制代码
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>

// 公共路径
#define pathname "/home/ly/111/Test/main.cpp"
// 整数标识符
#define proj_id 0x66
#define size 2048

std::string ToHex(key_t key)
{
    char buffer[128];
    // buffer:格式化结果将被写入的目标数组."0x%x":格式化字符串是十六进制数值常见的前缀,而%x用来将整数 k 格式化为十六进制表示.
    // 需要被格式化的 key_t 类型值。
    snprintf(buffer, sizeof(buffer), "0x%x", key);
    return buffer;
}
int main()
{
    // 获取key值
    key_t key = ftok(pathname, proj_id);
    if (key < 0)
    {
        perror("ftok");
        exit(-1);
    }
    // 创建共享内存
    // IPC_CREAT | IPC_EXCL:传入这个参数表示,如果创建的共享内存不存在,则创建,如果存在,则出错返回.创建的共享内存是全新的
    int shm = shmget(key, size, IPC_CREAT | IPC_EXCL);
    if (shm < 0)
    {
        perror("shmget");
        exit(-2);
    }
    std::cout << "key == " << ToHex(key) << std::endl;
    std::cout << "shm == " << shm << std::endl;
    return 0;
}

单独使用ipcs命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:

  • -q:罗列消息队列相关信息。
  • -m:罗列共享内存相关信息。
  • -s:罗列信号量相关信息。

ipcs命令输出的每列信息的含义如下

|--------|----------------|
| 属性 | 含义 |
| key | 系统区别共享内存的唯一性标识 |
| shmid | 共享内存的用户层id(句柄) |
| owner | 共享内存的拥有者 |
| perms | 共享内存的权限 |
| bytes | 共享内存的大小 |
| nattch | 关联共享内存的进程数 |
| status | 共享内存的状态 |

注意

  1. key:属于用户形成,内核使用的一个字段,用户不能使用key来进行shm的管理.这是内核进行区分shm的唯一性的.
  2. shmid:内核给用户返回的一个标识符,用来进行用户级对共享内存进行管理的id值.
  3. key就类似于文件的struct file *,shmid就类似于fd.

5:共享内存的释放

通过上面创建共享内存的实验可以发现,当进程运行完毕时,申请的共享内存依旧是存在的,并没有被操作系统释放.实际上

  • 管道生命周期是随进程的.
  • 共享内存 ,不随着进程的结束而自动释放,会一直存在,直到系统重启.因此我们只能够手动释放(通过指令或者其他系统调用),其生命周期是随内核.
  • 文件生命周期是随进程的.
  • 这说明如果进程不主动去删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的.

此时我们若是要将创建的共享内存进行释放,有两个方法

  • 第一个方法是:使用命令释放共享内存.
  • 第二个方法是:在进程间通信完毕后调用释放共享内存的函数进行释放.

5.1:使用命令释放共享内存

bash 复制代码
ipcrm -m shmid

PS:指定删除时,使用的是共享内存的用户层id,即列表中的shmid。

5.2:使用程序释放共享内存

控制共享内存需要使用到shmctl函数,该函数的原型如下.

参数说明

  • 第一个参数shmid:表示所控制共享内存的用户级标识符
  • 第二个参数cmd:表示具体的控制动作(有三个可取值).
  • 第三个参数buf:指向一个保存着共享内存的模式状态和访问权限的数据结构.

返回值说明

  • shmctl调用成功,返回0。
  • shmctl调用失败,返回-1。

其中,shmctl函数的第二个参数传入的常用的选项有以下三个

|----------|-------------------------------------------------|
| 命令 | 说明 |
| IPC_STAT | 把shmid_ds结构中的数据设置为共享内存的当前关联值. |
| IPC_SET | 在进程有足够权限的前提下,将共享内存的当前关联值设置为shmid_ds数据结构中的所给出的值. |
| IPC_RMID | 删除共享内存段. |

5.2.1:代码示例
cpp 复制代码
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
// 公共路径
#define pathname "/home/ly/111/Test/main.cpp"
// 整数标识符
#define proj_id 0x66
#define size 2048

std::string ToHex(key_t key)
{
    char buffer[128];
    // buffer:格式化结果将被写入的目标数组."0x%x":格式化字符串是十六进制数值常见的前缀,而%x用来将整数 k 格式化为十六进制表示.
    // 需要被格式化的 key_t 类型值。
    snprintf(buffer, sizeof(buffer), "0x%x", key);
    return buffer;
}
int main()
{
    // 获取key值
    key_t key = ftok(pathname, proj_id);
    if (key < 0)
    {
        perror("ftok");
        exit(-1);
    }
    // 创建共享内存
    // IPC_CREAT | IPC_EXCL:传入这个参数表示,如果创建的共享内存不存在,则创建,如果存在,则出错返回.创建的共享内存是全新的
    int shm = shmget(key, size, IPC_CREAT | IPC_EXCL);
    if (shm < 0)
    {
        perror("shmget");
        exit(-2);
    }
    std::cout << "key == " << ToHex(key) << std::endl;
    std::cout << "shm == " << shm << std::endl;
    sleep(5);
    //释放共享内存
    shmctl(shm,IPC_RMID,NULL);
    return 0;
}

6:共享内存的关联

将共享内存链接到进程的地址空间需要使用到shmat函数,其原型如下

参数说明

  • 第一个参数shmid,表示待关联共享内存的用户级标识符.
  • 第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为nullptr,表示让内核自己决定一个合适的地址位置.
  • 第三个参数shmflg,表示关联共享内存时设置的某些属性.
cpp 复制代码
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
// 公共路径
#define pathname "/home/ly/111/Test/main.cpp"
// 整数标识符
#define proj_id 0x66
#define size 2048

std::string ToHex(key_t key)
{
    char buffer[128];
    // buffer:格式化结果将被写入的目标数组."0x%x":格式化字符串是十六进制数值常见的前缀,而%x用来将整数 k 格式化为十六进制表示.
    // 需要被格式化的 key_t 类型值。
    snprintf(buffer, sizeof(buffer), "0x%x", key);
    return buffer;
}
int main()
{
    // 获取key值
    key_t key = ftok(pathname, proj_id);
    if (key < 0)
    {
        perror("ftok");
        exit(-1);
    }
    // 创建共享内存
    // IPC_CREAT | IPC_EXCL:传入这个参数表示,如果创建的共享内存不存在,则创建,如果存在,则出错返回.创建的共享内存是全新的
    int shm = shmget(key, size, IPC_CREAT | IPC_EXCL);
    if (shm < 0)
    {
        perror("shmget");
        exit(-2);
    }
    std::cout << "key == " << ToHex(key) << std::endl;
    std::cout << "shm == " << shm << std::endl;

    //挂起共享内存
    std::cout << "SharedMemory Attch begin!!!"<<std::endl;
    char * Memory = (char*)shmat(shm,nullptr,0);
    if(Memory == (void*) -1)
    {
        perror("shmat");
        exit(-3);
    }
    std::cout << "SharedMemory Attch end!!!"<<std::endl;
    sleep(5);
    
    //释放共享内存
    shmctl(shm,IPC_RMID,NULL);
    return 0;
}

shmat的返回值说明

  • shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
  • shmat调用失败,返回(void*)-1。

shmat函数的第三个参数传入的常用的选项有以下三个

|------------|-------------------------------------------------------------------|
| 选项 | 作用 |
| SHM_RDONLY | 关联共享内存后只进行读取操作 |
| SHM_RND | 若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA) |
| 0 | 默认读写权限 |

6.1:代码示例1

cpp 复制代码
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
// 公共路径
#define pathname "/home/ly/111/Test/main.cpp"
// 整数标识符
#define proj_id 0x66
#define size 2048

std::string ToHex(key_t key)
{
    char buffer[128];
    // buffer:格式化结果将被写入的目标数组."0x%x":格式化字符串是十六进制数值常见的前缀,而%x用来将整数 k 格式化为十六进制表示.
    // 需要被格式化的 key_t 类型值。
    snprintf(buffer, sizeof(buffer), "0x%x", key);
    return buffer;
}
int main()
{
    // 获取key值
    key_t key = ftok(pathname, proj_id);
    if (key < 0)
    {
        perror("ftok");
        exit(-1);
    }
    // 创建共享内存
    // IPC_CREAT | IPC_EXCL:传入这个参数表示,如果创建的共享内存不存在,则创建,如果存在,则出错返回.创建的共享内存是全新的
    int shm = shmget(key, size, IPC_CREAT | IPC_EXCL);
    if (shm < 0)
    {
        perror("shmget");
        exit(-2);
    }
    std::cout << "key == " << ToHex(key) << std::endl;
    std::cout << "shm == " << shm << std::endl;

    //挂起共享内存
    std::cout << "SharedMemory Attch begin!!!"<<std::endl;
    char * Memory = (char*)shmat(shm,nullptr,0);
    if(Memory == (void*) -1)
    {
        perror("shmat");
        exit(-3);
    }
    std::cout << "SharedMemory Attch end!!!"<<std::endl;
    sleep(5);
    
    //释放共享内存
    shmctl(shm,IPC_RMID,NULL);
    return 0;
}

代码运行后,我们可以发现关联失败了,因为我们在使用shmget函数时,没有对创建的共享内存设置权限,所以创建出来的共享内存的默认权限为0,即什么权限都没有,因此进程没有权限关联该共享内存.

因此在使用shmget函数创建共享内存时,在其第三个参数处设置共享内存创建后的权限,权限的设置规则与设置文件权限的规则相同。

6.2:代码示例2

cpp 复制代码
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
// 公共路径
#define pathname "/home/ly/111/Test/main.cpp"
// 整数标识符
#define proj_id 0x66
#define size 2048

std::string ToHex(key_t key)
{
    char buffer[128];
    // buffer:格式化结果将被写入的目标数组."0x%x":格式化字符串是十六进制数值常见的前缀,而%x用来将整数 k 格式化为十六进制表示.
    // 需要被格式化的 key_t 类型值。
    snprintf(buffer, sizeof(buffer), "0x%x", key);
    return buffer;
}
int main()
{
    // 获取key值
    key_t key = ftok(pathname, proj_id);
    if (key < 0)
    {
        perror("ftok");
        exit(-1);
    }
    // 创建共享内存
    // IPC_CREAT | IPC_EXCL:传入这个参数表示,如果创建的共享内存不存在,则创建,如果存在,则出错返回.创建的共享内存是全新的
    int shm = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
    if (shm < 0)
    {
        perror("shmget");
        exit(-2);
    }
    std::cout << "key == " << ToHex(key) << std::endl;
    std::cout << "shm == " << shm << std::endl;

    //挂起共享内存
    std::cout << "SharedMemory Attch begin!!!"<<std::endl;
    char * Memory = (char*)shmat(shm,nullptr,0);
    if(Memory == (void*) -1)
    {
        perror("shmat");
        exit(-3);
    }
    std::cout << "SharedMemory Attch end!!!"<<std::endl;
    sleep(5);

    //释放共享内存
    shmctl(shm,IPC_RMID,NULL);
    return 0;
}

此时再运行程序,即可发现关联该共享内存的进程数由0变成了1,而共享内存的权限显示也不再是0,而是我们设置的666权限

7:共享内存的去关联

取消共享内存与进程地址空间之间的关联需要使用到shmdt函数,其函数原型如下

参数说明

  • shmaddr: 待去关联共享内存的起始地址,即 由 shmat 所返回的指针

返回值说明

  • shmdt调用成功,返回0
  • shmdt调用失败,返回-1

注意:将共享内存段与当前进程脱离不等于删除共享内存段

7.1:代码示例1

cpp 复制代码
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
// 公共路径
#define pathname "/home/ly/111/Test/main.cpp"
// 整数标识符
#define proj_id 0x66
#define size 2048

std::string ToHex(key_t key)
{
    char buffer[128];
    // buffer:格式化结果将被写入的目标数组."0x%x":格式化字符串是十六进制数值常见的前缀,而%x用来将整数 k 格式化为十六进制表示.
    // 需要被格式化的 key_t 类型值。
    snprintf(buffer, sizeof(buffer), "0x%x", key);
    return buffer;
}
int main()
{
    // 获取key值
    key_t key = ftok(pathname, proj_id);
    if (key < 0)
    {
        perror("ftok");
        exit(-1);
    }
    // 创建共享内存
    // IPC_CREAT | IPC_EXCL:传入这个参数表示,如果创建的共享内存不存在,则创建,如果存在,则出错返回.创建的共享内存是全新的
    int shm = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
    if (shm < 0)
    {
        perror("shmget");
        exit(-2);
    }
    std::cout << "key == " << ToHex(key) << std::endl;
    std::cout << "shm == " << shm << std::endl;

    //挂起共享内存
    std::cout << "SharedMemory Attch begin!!!"<<std::endl;
    char * Memory = (char*)shmat(shm,nullptr,0);
    if(Memory == (void*) -1)
    {
        perror("shmat");
        exit(-3);
    }
    std::cout << "SharedMemory Attch end!!!"<<std::endl;
    sleep(2);
    std::cout << "Detach Begin!!!"<<std::endl;
    //共享内存去关联
    shmdt(Memory);
    sleep(2);
    std::cout << "Detach End!!!"<<std::endl;

    sleep(2);

    //释放共享内存
    shmctl(shm,IPC_RMID,NULL);
    return 0;
}

运行程序,通过监控即可发现该共享内存的关联数由1变0的过程,即取消了共享内存与该进程之间的关联.

8:使用共享内存实现Server &Client通信

  • 在知道了共享内存的创建、关联、去关联以及释放后,现在可以尝试让两个进程通过共享内存进行通信了,既然服务端与客户端是通过共享内存来进行通信的,那么我们可以将其封装到一个类里面.
  • Server端:创建共享内存、创建命名管道,然后阻塞等待通知.
  • Client端:连接共享内存、打开命名管道写端,然后不断写入数据并通知 Server 读取.

8.1:SharedMemory.hpp

cpp 复制代码
#ifndef __SHaredMemory_HPP__
#define __SHaredMemory_HPP__

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <cstring>
#include <stdlib.h>
const int GCreater = 1;
const int GUser = 2;
const int GShmSize = 4096;

const std::string pathname = "/home/ly/111/InterProcessCommunication/SharedMemory";
int proj_id = 0x66;
class SharedMemory
{
private:
    key_t GetCommKey();
    int GetSharedMemory(key_t key, int size, int flag);
public:
    SharedMemory(const std::string &pathname, int proj_id, int who);
    ~SharedMemory();


    std::string ToHex(key_t k);

    //服务端创建
    bool GetShmUerCreate();

    //客户端使用
    bool GetShmForUse();


    //挂接共享内存
    void * Attach();

    //分离共享内存
    void Detach(void * SharedMemoryAddress);

    void * GetSharedMemoryAddress();

    void Debugshm();
    
    void ZeroClearing();
private:
    key_t _key;
    int _shmid;
    std::string _pathname;
    int _proj_id;
    int _who;
    void * _SharedMemoryAddress;
};


#endif

我们使用SharedMemory这个类来封装共享内存,用于创建、获取、挂接、分离、删除、清空共享内存的操作 ,这样子的话,我们可以像操作对象一样进行去对共享内存进行相应的操作.

  • GCreator:标识共享内存的创建者.
  • GUser:标识共享内存的使用者.
  • GShmSize:共享内存的大小.
  • pathname:全局的已存在的路径名
  • proj_id:全局的整数标识符
  • _key:是用来标识共享内存的唯一性字段(由用户形成)
  • _shmid:内核给用户返回的一个标识符,用来进行用户级对共享内存进行管理的id值.
  • _pathname + _proj_id:用于获取key值
  • _who:表示身份
  • _SharedMemoryAddress:共享内存的地址
8.1.1:构造函数的实现
cpp 复制代码
    SharedMemory(const std::string &pathname, int proj_id, int who)
    :_pathname(pathname)
    ,_proj_id(proj_id)
    ,_who(who)
    ,_SharedMemoryAddress(nullptr)
    {
        //获取Key值
        _key = GetCommKey();
        //创建共享内存
        //创建者
        if(_who == GCreater)
            GetShmUerCreate();
        //使用者
        else if(_who == GUser)
            GetShmForUse();
        //无论是创建者还是使用者在创建的时候直接挂接即可~
        _SharedMemoryAddress = Attach();

        std::cout << "_shmid:>" << _shmid << std::endl;
        std::cout << "_key:>" << _key << std::endl;

    }
  1. 生成对应的key值.
  2. 根据角色决定"创建共享内存"还是获取"共享内存".
  3. 自动挂接共享内存.
8.1.2:共享内存模块
8.1.2.1:获取共享内存的key值
cpp 复制代码
    key_t GetCommKey()
    {
        // 第一个参数是:一个字符串参数,表示用于生成键值的文件路径.通常是一个存在的文件路径,第二个参数是:一个整数值,作为项目的标识符,用来生成唯一的键值.通常是一个小整数
        key_t k = ftok(pathname.c_str(),proj_id);
        if(k < 0)
            perror("ftok");
        return k;
    }
  • 根据路径和项目id生成System V IPC key.
  • 这是多个进程找到同一块共享内存的前提.
8.1.2.2:GetSharedMemory函数
cpp 复制代码
    int GetSharedMemory(key_t key, int size, int flag)
    {
        int Shmid = shmget(key,size,flag);
        if(Shmid < 0)
        {
            perror("Shmget");
        }
        return Shmid;
    }
  • shmget() 的一层封装.
  • 创建共享内存段.
8.1.2.3:GetShmUerCreate函数
cs 复制代码
    //服务端创建
    bool GetShmUerCreate()
    {
        //判断用户是否是创建者
        if(_who == GCreater)
        {
            // IPC_CREAT与IPC_EXCL,传入这个参数表示,如果创建的共享内存不存在,则创建,如果存在则出错返回
            _shmid = GetSharedMemory(_key,GShmSize,IPC_CREAT | IPC_EXCL | 0666);
            if(_shmid >= 0)
            {
                std::cout << "Shm Create Done...." << std::endl;
                return true;
            }
        }
        return false;
    }
  • 服务端专用
  • 若共享内存不存在则进行创建,若存在则报错
8.1.2.4:GetShmForUser函数
cpp 复制代码
    //客户端使用
    bool GetShmForUse()
    {
        if(_who == GUser)
        {
             //获取共享内存
            _shmid == GetSharedMemory(_key,GShmSize,0666);
            if(_shmid >= 0)
            {
                std::cout << "Shm Get Done...."<<std::endl;
                return true;
            }
        }
        return false;
    }
  • 使用者获取共享内存
8.1.2.5:Attach(挂接共享内存)
cpp 复制代码
    void * Attach()
    {
        //确保每次挂接共享内存时都不会与先前的挂接发生冲突
        if(_SharedMemoryAddress != nullptr)
        {
            Detach(_SharedMemoryAddress);
        }
        //第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为nullptr,表示让内核自己决定一个合适的地址位置.
        void * SharedMemoryAddress = shmat(_shmid,nullptr,0);
        if(SharedMemoryAddress == (void *) -1)
        {
            perror("shmat fail");
        }
        std::cout << "Attach Success" << std::endl;
        return SharedMemoryAddress;
    }
  1. 确保每次挂接共享内存不会与先前的挂接发生冲突.
  2. 将共享内存映射到当前进程地址空间.
8.1.2.6:Detach(分离共享内存)
cpp 复制代码
    //分离共享内存
    void Detach(void * SharedMemoryAddress)
    {
        if(SharedMemoryAddress == (void *) -1)
            return;
        shmdt(SharedMemoryAddress);
        std::cout << "Detach Success" << std::endl;
    }
  • 分离共享内存
8.1.2.7:Debugshm函数(获取共享内存状态)、
cpp 复制代码
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • shmid:由shmget返回的共享内存标识码
  • cmd:将要采取的动作(有三个可取值)
  • buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
  • 返回值:成功返回0;失败返回-1
  • 功能:用于控制共享内存
cpp 复制代码
    void Debugshm()
    {
        struct shmid_ds ds;
        //获取共享内存段的状态信息,并将其存储在 shmid_ds 结构中。
        int n = shmctl(_shmid,IPC_STAT,&ds);
        if(n < 0)
            return;
        std::cout << "ds.shm_perm.__key:> "<< ds.shm_perm.__key << std::endl;
        std::cout << "ds.shm_nattch:> "<< ds.shm_nattch << std::endl; 
    }
  • 查看共享内存的状态
8.1.2.8:ZeroClearing(清空数据)
cpp 复制代码
    void ZeroClearing()
    {
        if(_SharedMemoryAddress != nullptr)
        {
            //将内存中的值以字节为单位设置对应的内容
            memset(_SharedMemoryAddress,0,4096);
        }
    }
8.1.3:析构函数的实现
cpp 复制代码
    ~SharedMemory()
    {
        if(_who == GCreater)
        {
            int res = shmctl(_shmid,IPC_RMID,nullptr);
            std::cout << "Shm Remove Done...."<<std::endl;
        }
    }
  • 删除共享内存(仅创建者)
8.1.4:总代码
cpp 复制代码
#ifndef __SHaredMemory_HPP__
#define __SHaredMemory_HPP__

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <cstring>
#include <stdlib.h>
const int GCreater = 1;
const int GUser = 2;
const int GShmSize = 4096;

const std::string pathname = "/home/ly/111/SharedMemory";
int proj_id = 0x66;
class SharedMemory
{
private:
    key_t GetCommKey()
    {
        // 第一个参数是:一个字符串参数,表示用于生成键值的文件路径.通常是一个存在的文件路径,第二个参数是:一个整数值,作为项目的标识符,用来生成唯一的键值.通常是一个小整数
        key_t k = ftok(pathname.c_str(),proj_id);
        if(k < 0)
            perror("ftok");
        return k;
    }
    int GetSharedMemory(key_t key, int size, int flag)
    {
        int Shmid = shmget(key,size,flag);
        if(Shmid < 0)
        {
            perror("Shmget");
        }
        return Shmid;
    }
public:
    SharedMemory(const std::string &pathname, int proj_id, int who)
    :_pathname(pathname)
    ,_proj_id(proj_id)
    ,_who(who)
    ,_SharedMemoryAddress(nullptr)
    {
        //获取Key值
        _key = GetCommKey();
        //创建共享内存
        //创建者
        if(_who == GCreater)
            GetShmUerCreate();
        //使用者
        else if(_who == GUser)
            GetShmForUse();
        //无论是创建者还是使用者在创建的时候直接挂接即可~
        _SharedMemoryAddress = Attach();

    }
    ~SharedMemory()
    {
        if(_who == GCreater)
        {
            int res = shmctl(_shmid,IPC_RMID,nullptr);
            std::cout << "Shm Remove Done...."<<std::endl;
        }
    }


    std::string ToHex(key_t k);

    //服务端创建
    bool GetShmUerCreate()
    {
        //判断用户是否是创建者
        if(_who == GCreater)
        {
            // IPC_CREAT与IPC_EXCL,传入这个参数表示,如果创建的共享内存不存在,则创建,如果存在则出错返回
            _shmid = GetSharedMemory(_key,GShmSize,IPC_CREAT | IPC_EXCL | 0666);
            if(_shmid >= 0)
            {
                std::cout << "Shm Create Done...." << std::endl;
                return true;
            }
        }
        return false;
    }

    //客户端使用
    bool GetShmForUse()
    {
        if(_who == GUser)
        {
             //获取共享内存
            _shmid = GetSharedMemory(_key,GShmSize, 0666);
            if(_shmid >= 0)
            {
                std::cout << "Shm Get Done...."<<std::endl;
                return true;
            }
        }
        return false;
    }


    //挂接共享内存
    void * Attach()
    {
        //确保每次挂接共享内存时都不会与先前的挂接发生冲突
        if(_SharedMemoryAddress != nullptr)
        {
            Detach(_SharedMemoryAddress);
        }
        //第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为nullptr,表示让内核自己决定一个合适的地址位置.
        void * SharedMemoryAddress = shmat(_shmid,nullptr,0);
        if(SharedMemoryAddress == (void *) -1)
        {
            perror("shmat fail");
        }
        std::cout << "Attach Success" << std::endl;
        return SharedMemoryAddress;
    }

    //分离共享内存
    void Detach(void * SharedMemoryAddress)
    {
        if(SharedMemoryAddress == nullptr)
            return;
        shmdt(SharedMemoryAddress);
        std::cout << "Detach Success" << std::endl;
    }

    void * GetSharedMemoryAddress()
    {
        return _SharedMemoryAddress;
    }

    void Debugshm()
    {
        struct shmid_ds ds;
        //获取共享内存段的状态信息,并将其存储在 shmid_ds 结构中。
        int n = shmctl(_shmid,IPC_STAT,&ds);
        if(n < 0)
            return;
        std::cout << "ds.shm_perm.__key:> "<< ds.shm_perm.__key << std::endl;
        std::cout << "ds.shm_nattch:> "<< ds.shm_nattch << std::endl; 
    }

    void ZeroClearing()
    {
        if(_SharedMemoryAddress != nullptr)
        {
            //将内存中的值以字节为单位设置对应的内容
            memset(_SharedMemoryAddress,0,4096);
        }
    }
private:
    key_t _key;
    int _shmid;
    std::string _pathname;
    int _proj_id;
    int _who;
    void * _SharedMemoryAddress;
};


#endif

8.2:NamedPipe.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <string>
#include <cerrno>
#include <cstdio>
#include <unistd.h>
#include <fcntl.h>

// 创建的公共路径
const std::string Comm_Path = "./myfifo";
#define Creator 1
#define User 2
#define Write O_WRONLY
#define Read O_RDONLY
#define DefaultFd -1
#define BaseSize 4096

class NamePiped
{

private:
    // 打开管道
    bool OpenNamedPipe(int mode)
    {
        _fd = open(_FiFo_Path.c_str(), mode);
        if (_fd < 0)
            return false;
        return true;
    }

public:
    NamePiped(const std::string &path, int who)
        : _FiFo_Path(path), _id(who), _fd(DefaultFd)
    {
        // 为创建者则创建管道
        if (_id == Creator)
        {
            {
                // 第一个参数为指定命名管道的路径,第二个参数mode表示设置管道的权限
                int Res = mkfifo(_FiFo_Path.c_str(), 0666);
                if (Res != 0)
                    perror("mkfifo");
            }
            std::cout << "Creater Create Named Pipe" << std::endl;
        }
    }

    /* const & : const std::string 输入型参数
    // *  :      std::string *  输出型参数
       &  :      std::string & 输入输出型参数
    */
    //读取管道数据
    int ReadNamedPipe(std::string * out)
    {
        char Buffer[BaseSize];
        int n = read(_fd,Buffer,sizeof(Buffer));
        if(n > 0)
        {
            Buffer[n] = 0;
            *out = Buffer;
        }
        return n;
    }
    //写入管道数据
    int WriteNamedPipe(const std::string & In)
    {
        return write(_fd,In.c_str(),In.size());
    }

    //  读端
    bool OpenForRead()
    {
        return OpenNamedPipe(Read);
    }
    // 写端
    bool OpenForWrite()
    {
        return OpenNamedPipe(Write);
    }


    ~NamePiped()
    {
        if (_id == Creator)
        {
            int Res = unlink(_FiFo_Path.c_str());
            if (Res != 0)
                perror("unlink");
            std::cout << "Creater Free Named Pipe" << std::endl;
        }
        if(_fd != DefaultFd)
            close(_fd);
    }

private:
    const std::string _FiFo_Path;
    int _id;
    int _fd;
};

这里博主还需要使用管道,有的uu会有些好奇,已经使用了共享内存了来实现两个进程间的通信了,为什么还需要使用共享内存呢?因为共享内存有以下特点

  • 直接访问内存,访问速度很快.
  • 没有通知机制------->不知道"什么时候有数据,写了数据对方不知道".

我们来做一个假设,假如说去掉管道,只使用共享内存的话,会产生什么情况

  • Server端怎么知道有新数据,那么针对Server端只能这样子做
cpp 复制代码
while (true) {
    if (共享内存变了) {
        处理数据;
    }
}

但是这样子就会导致以下三个问题

  • CPU 被浪费
  • 不优雅,没有事件驱动,全靠"猜测"
  • 可能读到脏数据

那么加入了管道以后,就可以解决上面的三个问题,因为管道提供了

1.阻塞等待机制

  • 没有数据时------>睡眠(不占用CPU)
  • 有数据时----->自动唤醒

2.事件通知

  • 发送信号:告知共享内存有新数据了

3.解耦数据与控制

  • 共享内存负责传输数据.
  • 管道负责发送信号.
  • 这样子做到了数据通道 与 控制通道的分离设计

8.3:Server.cpp

cpp 复制代码
#include "SharedMemory.hpp"
#include "NamedPipe.hpp"
int main()
{
    //1:创建共享内存
    SharedMemory shm(pathname, proj_id, GCreater);
    char * SharedMemoryAddress = (char *)shm.GetSharedMemoryAddress();
    shm.Debugshm();
    //2:创建管道
    NamePiped fifo(Comm_Path,Creator);
    fifo.OpenForRead();
    while (true)
    {
        std::string Data;
        fifo.ReadNamedPipe(&Data);
        std::cout << "SharedMemoryAddress Conent:>" << SharedMemoryAddress << std::endl;
    }
    
}
  1. 创建共享内存
  2. 创建FIFO
  3. 打开读端
  4. 阻塞等待
  5. 读取共享内存

8.4:Client.cpp

cpp 复制代码
#include "SharedMemory.hpp"
#include "NamedPipe.hpp"
int main()
{
    // 1:创建共享内存
    SharedMemory shm(pathname, proj_id, GUser);
    shm.ZeroClearing();
    char *SharedMemoryAddress = (char *)shm.GetSharedMemoryAddress();
    sleep(3);

    char ch = 'A';
    // 2:创建管道
    NamePiped fifo(Comm_Path, User);
    fifo.OpenForWrite();
    while (ch <= 'Z')
    {
        SharedMemoryAddress[ch - 'A'] = ch;
        std::string Data = "Wake up";
        std::cout << "Add " << ch << " into Shm," << "Wake up Reader" << std::endl;
        fifo.WriteNamedPipe(Data);
        sleep(2);
        ch++;
    }
    return 0;
}
  1. 创建共享内存(用户)
  2. 将管道里面的数据清零
  3. 获取共享内存的地址
  4. 打开管道的写端
  5. 循环将数据写入到共享内存
  6. 管道负责通知

9:共享内存与管道的对比

共享内存创建好后就不再需要调用系统接口进行通信了,而管道创建好后依旧需要使用read、write等系统调用接口进行通信。实际上,共享内存是所有进程间通信方式中最快的一种方式.

9.1:管道通信

从这张图可以看出,使用管道通信的方式,将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作

  1. 服务端将信息从输入文件复制到服务器的临时缓冲区中.
  2. 服务端临时缓冲区信息复制到管道中.
  3. 客户端 将信息从管道复制到客户端的缓冲区中.
  4. 客户端临时缓冲区的信息复制到输出文件中.

9.2:共享内存通信

从这张图可以看出,使用共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作:

  1. 从输入文件到共享内存
  2. 从共享内存到输出文件

|----------|----------------------------------------------------------|-----------------------------|
| 对比维度 | 管道(Pipe/FIFO) | 共享内存(Shared Memory) |
| 通信速度 | 较慢(涉及到了两次数据拷贝) | 最快(直接访问物理内存,零拷贝) |
| 同步机制 | 自带同步机制(内核管理:空时读阻塞,满时写阻塞) | 无自带同步(需借助信号量、互斥锁等自行控制) |
| 数据格式 | 字节流(无格式,先进先出顺序读取) | 自定义(普通内存块,由程序自行定义结构) |
| 通信方向 | 半双工(单向传输,若需双向通常需要重建两个管道) | 全双工(任意进程均可随时读写同一块内存) |
| 系统开销 | 传输时开销较大(每次读写都需要系统调用) | 映射时有一定开销,传输时几乎无开销 |
| 容量限制 | 容量较小(通常受限于内核缓冲区,如 Linux 默认 64KB) | 容量大(可申请 MB 或 GB 级别,受限于物理内存) |
| 适用范围 | 匿名管道:仅限有血缘关系的进程(父子/兄弟) 命名管道:用于同一主机下的进程 | 可用于同一主机上的任意进程 |
| 生命周期 | 匿名管道:随进程结束而销毁 命名管道:随文件系统,但无进程打开时释放资源 | 随内核(除非显式删除或系统重启,否则一直存在) |

相关推荐
唐墨1232 小时前
linux kernel之设备树
linux·运维·服务器
huanmieyaoseng10032 小时前
centos 配置国内yum源2026新
linux·运维·centos
草莓熊Lotso2 小时前
Linux 线程同步与互斥(一):彻底搞懂线程互斥原理、互斥量底层实现与 RAII 封装
linux·运维·服务器·开发语言·数据库·c++
feng_you_ying_li2 小时前
linux之进程概念
linux
j_xxx404_2 小时前
深入理解Linux底层存储:从物理磁盘架构到文件系统(inode/Block)原理
linux·运维·服务器·后端
嵌入式×边缘AI:打怪升级日志3 小时前
深度剖析Linux按键驱动四种访问方式:从查询到异步通知
linux·运维·服务器
凉、介3 小时前
从设备树到驱动源码:揭秘嵌入式 Linux 中 MMC 子系统的统一与差异
linux·驱动开发·笔记·学习·嵌入式·sd·emmc
Full Stack Developme3 小时前
Linux 软连接与硬连接比较
linux·运维·服务器
云边有个稻草人3 小时前
【Linux系统】第九节—进程状态续集+进程优先级+进程切换
linux·进程状态·进程优先级·linux进程调度算法·linux进程切换·死循环进程如何运行·pri and ni