圆咕噜咕噜 发表于 2024-7-30 17:53:45

【Linux】进程间通信3——system V共享内存

1.system V进程间通信

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

[*]system V共享内存(重点)
[*]system V消息队列
[*]system V信号量
此中,system V共享内存和system V消息队列是以传送数据为目的的,而system V信号量是为了包管进程间的同步与互斥而计划的,虽然system V信号量和通信好像没有直接关系,但属于通信范畴。
   

[*]system V共享内存和system V消息队列就雷同于手机,用于沟通信息;
[*]system V信号量就雷同于下棋比赛时用的棋钟,用于包管两个棋手之间的同步与互斥。
2.system V共享内存

        共享内存,是一种进程间通信解决方案,并且是全部解决方案中最快的一个,在通信速度上可以做到一骑绝尘
这是 System V 尺度中一个比较成功的通信方式,特点就是非常快
2.1.共享内存的根本原理

   起首我们要明白,共享内存是为了让进程之间进行通信,所以共享内存一定也遵守着 让差异进程看到同一份资源 的原则,而共享内存可以让毫不相干的进程之间进行通信。
共享内存让差异进程看到同一份资源的方式就是:在物理内存中开发一块公共区域,让两个差异的进程的捏造地址同时对此空间创建映射关系,此时两个独立的进程能看到同一块空间,可以直接对此空间进行【写入或读取】,这块公共区域就是 共享内存
https://img-blog.csdnimg.cn/direct/38bc3958d72d46f8b66feb5e8f969fd7.png
接下来来逐一讲解这个过程 



   留意:
        这里所说的开发物理空间、创建映射等操作都是调用系统接口完成的,也就是说这些动作都由操作系统来完成。 
https://img-blog.csdnimg.cn/direct/84d7cac270654c39bfa6cf451d76f792.png
如上图,这是两个普通进程A和B在系统上工作的原理图,如今想让进程A和B之间进行通信,共享内存的方法是怎样做的呢?
        起首由通信的此中一方负责向系统申请共享内存,这里就让进程A负责好了,OS收到哀求后,在物理内存划出一块内存区域,用来这包管了进程A和进程B可以或许看到并利用同一块内存空间,如下图的红色操作
https://img-blog.csdnimg.cn/direct/fe78b8bc077842dda51d21fc743fafcc.png
        共享内存申请好了,但是并不意味着就可以直接用了,由于进程A,B的页表里并没有关于共享内存区域的映射,因此,进程A和B要分别与共享内存区域进行挂接,挂接的过程就是将共享内存区域的物理地址添加到进程的页表映射中,如许进程就能通过页表映射到共享内存区域了,如下图的蓝色操作
https://img-blog.csdnimg.cn/direct/994013fc145e44598c0807a31c7caf82.png
        不过这个挂接可是没那么简单,操作系统起首将物理内存的共享内存区域映射到捏造地址空间的共享区内,获得一些捏造地址空间,然后再通过页表将捏造地址映射到对应的物理内存,这个和动态库的加载是一个原理
https://img-blog.csdnimg.cn/direct/2b39e7e194de4437a77460c9cf886416.png
如许子共享内存就存在与各自的共享区内了 
    关于共享区:共享区作为捏造地址空间中一块缓冲区域,既可作为堆栈生长扩展的区域,也可用来存储各种进程间的公共资源,好比这里的共享内存,以及之前学习的动态库,相关信息都是存储在共享区中
https://img-blog.csdnimg.cn/direct/be8d711b5d5249a8a65baa1ef30d3c3c.png

等到挂接完成后,进程A和B就能看到并利用同一块内存空间了,至此就可以开始通信,等到通信结束之后,通信双方要分别取消掉对共享内存区域的挂接操作,如下图绿色操作
https://img-blog.csdnimg.cn/direct/6059b66d69a24dc3a6795bb1d553675e.png
取消挂接了并不算彻底结束了,由于共享内存的申请是直接在物理内存上进行的,不会随着进程的退出而释放,只有手动释放,或者系统重启的时候才会释放,因此,进程不再通信后,应当由共享内存申请方在进程退出前释放共享内存,如下图黄色操作 
https://img-blog.csdnimg.cn/direct/ac3ccb057e454bef8761761e70ad8f36.png
至此,共享内存的原理已经完成,总共分成了4个步调实现共享内存通信
   总结下来就说下面这些步调
共享内存的创建大致包罗以下两个过程:

[*]在物理内存当中申请共享内存空间。
[*]将申请到的共享内存挂接到地址空间,即创建映射关系。
共享内存的释放大致包罗以下两个过程:

[*]将共享内存与地址空间去关联,即取消映射关系。
[*]释放共享内存空间,即将物理内存归还给系统。
    留意:
        这里所说的开发物理空间、创建映射等操作都是调用系统接口完成的,也就是说这些动作都由操作系统来完成。  
 2.2. 形貌共享内存的布局体

   在正式利用共享内存通信之前,必要先学习一下 共享内存的相关知识,由于这里的共享内存出自System V尺度,所以System V中的消息队列、信号量绝大部门接口的风格也与之差不多
        共享内存不止用于两个进程间通信,所以共享内存必须确保能持续存在,这也就意味着共享内存的生命周期不随进程,而是随操作系统,一旦共享内存被创建,除非被删除,否则将会不停存在,因此 操作系统必要对共享内存的状态加以形貌
        共享内存也不止存在一份,当出现多块共享内存时,操作系统不大概逐一比对进利用用,秉持着高效的原则,操作系统会把已经创建的共享内存组织起来,更好的进行管理
        所以共享内存必要有自己的数据布局,颠末操作系统 先形貌,再组织 后,构成了下面这个数据布局
注:shm 表示共享内存
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值用于标识系统中共享内存的唯一性。
        这里必要解释一下key,共享内存是用来进程间通信的,那么系统中那么多进程,肯定会存在很多的共享内存,那么系统要管理这些共享内存就要给这些共享内存标号,标明它的唯一性,这个key值就是这段共享内存在系统中的唯一性编号,通过这个唯一性编号,以及你要申请的共享内存的大小,系统就可以帮你申请一块共享内存了。
        key_t 实际就是对 int 进行了封装,表示一个数字,用来标识差异的共享内存块,可以理解为共享内存的 inode。
        你怎么包管让差异的进程看到同一个共享内存呢?你怎么知道这个共享内存存在还是不存在呢?都要借助key。只要第一个进程通过key创建共享内存,第二个之后的进程只要拿着同一个key就可以和第一个进程看到同一块共享内存了
        可以看到上面共享内存数据布局的第一个成员是shm_perm,shm_perm是一个ipc_perm范例的布局体变量,每个共享内存的key值存储在shm_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;
};
        共享内存虽然属于文件系统,但它的布局是颠末特别计划的,与文件系统中的 inode 那一套布局逻辑不一样
   共享内存的数据布局shmid_ds和ipc_perm布局体分别在/usr/include/linux/shm.h和/usr/include/linux/ipc.h中定义。
https://img-blog.csdnimg.cn/direct/e8cbceb36b9543b08556a0f03771eb55.png
https://img-blog.csdnimg.cn/direct/7b2848656b1849dea5fb3c5004bbb931.png

再者,操作系统怎么知道有多少个进程在利用这个共享内存呢?那么很简单,肯定有一个雷同引用计数的东西  
 3.利用共享内存

3.1.共享内存的创建

创建共享内存我们必要用shmget函数,shmget函数的函数原型如下:
https://img-blog.csdnimg.cn/direct/09e792a6221a448d84e56e5cb95deea3.png
 shmget函数的参数分析:


[*]第一个参数key,表示待创建共享内存在系统当中的唯一标识。
[*]第二个参数size,表示待创建共享内存的大小。
[*]第三个参数shmflg,表示创建共享内存的方式。
shmget函数的返回值分析:


[*]shmget调用成功,返回一个有用的共享内存标识符(用户层标识符)。
[*]shmget调用失败,返回-1。
   留意: 我们把具有标定某种资源本领的东西叫做句柄,而这里shmget函数的返回值实际上就是共享内存的句柄,这个句柄可以在用户层标识共享内存,当共享内存被创建后,我们在后续利用共享内存的相关接口时,都是必要通过这个句柄对指定共享内存进行各种操作。  
    返回值
        由于共享内存拥有自己的数据布局,所以 返回值 int 实际就是 shmid,雷同于文件系统中的 fd,用来对差异的共享内存块进行操作,
     传入shmget函数的第一个参数key,必要我们利用ftok函数进行获取

      这里必要解释一下key,共享内存是用来进程间通信的,那么系统中那么多进程,肯定会存在很多的共享内存,那么系统要管理这些共享内存就要给这些共享内存标号,标明它的唯一性,这个key值就是这段共享内存在系统中的唯一性编号,通过这个唯一性编号,以及你要申请的共享内存的大小,系统就可以帮你申请一块共享内存了。
        key_t 实际就是对 int 进行了封装,表示一个数字,用来标识差异的共享内存块,可以理解为 inode
        你怎么包管让差异的进程看到同一个共享内存呢?你怎么知道这个共享内存存在还是不存在呢?都要借助key。只要第一个进程通过key创建共享内存,第二个之后的进程只要拿着同一个key就可以和第一个进程看到同一块共享内存了。
        所以第一次创建的时候,必须有一个key。
 


[*]我们在命名管道怎么确定是同一个命名管道的?
是通过同一路径下面+同一文件名

  传入shmget函数的第一个参数key,必要我们利用ftok函数进行获取
https://i-blog.csdnimg.cn/direct/43eda0d2a66240cc8c988178853791c8.png
        ftok函数的作用就是,将一个已存在的路径名pathname和一个项目的识符转换成一个key值,称为IPC键值,在利用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据布局当中。
        ftok函数可以理解为一套算法,像加法一样,传数据进去,它只是搞盘算的,除了天生一个效果,别的东西它不干, 将pathname 和 proj_id当成数据通过算法天生了一个序号id

        简单来说就是给一个文件路径名和一个int值,那么ftok函数就会天生一个能唯一标识共享内存的key值,也就是shmget()函数中第一个参数的key值       
https://img-blog.csdnimg.cn/direct/3c42135e3d0d474399d1d06c4c61e336.png
必要留意的是,pathname所指定的文件必须存在且可存取。

留意:


[*]我们说ftok只是一套算法,利用ftok函数天生key值大概是一样的,就像1+8,2+7和3+6的效果都是9,同样,我们传入差异的pathname和proj_id也大概会如许子,算出的key值雷同。
[*]我们上面说只要第一个进程通过key创建共享内存,第二个之后的进程只要拿着同一个key就可以和第一个进程看到同一块共享内存了。
[*]所以必要进行通信的各个进程,在利用ftok函数获取key值时,只要我们传入雷同的pathname和proj_id就能得到雷同的key值,然后才气找到同一个共享资源。
   

[*]1.为什么要让我们来设置key值?明明让用户干会有冲突啊!
        重要的是,假如操作系统为你这个进程天生了一个key,那么你怎么把这个key通报给另一个进程呢?操作系统怎么知道你要和谁通信?
        与其说是用户来设定的,不如说是用户来约定的,如许子用户可以自由指定哪些用户来通信!
   

[*]2.当shmget()三个参数齐全,创建共享内存成功的时候,就会返回一个共享内存的标识码shmid,大概你会惊讶,刚才用ftok已经天生了标识共享内存的码,怎么这里又返回了一个?key和shmid是一样的吗?
其实这两个码都可以用来标识共享内存,shmid与key的关系就雷同于文件系统中的fd和inode,

[*]key就像inode一样,是给系统看的,操作系统通过key来标识这个共享内存
[*]shmid和fd雷同,是给应用层的进程利用的,只在进程里存在,是进程用来控制这个共享内存的途径
我们刚开始得通过key获得shmid,之后进程就不停利用shmid来管理共享内存了
   

[*]3.为何要多此一举呢?
        这是为了系统层和应用层之间的解耦,避免因应用层的shmid出现错误而影响了系统层的正常工作
            参数2为创建共享内存的大小,单元是字节,一样平常设为 4096 字节(4kb),与一个 PAGE 页大小雷同,有利于进步 IO 效率

        假如size设置成4097 ,在OS底层给你分配了2页(按页对齐),但是你要4097字节那么我就只让你看到4097个字节的空间,绝对不少给你但也不多给你,少给了大概会出问题,多给了也大概出问题,用户要我怎么办我就怎么办,严格按照用户来 ; 所以最好设置4096的整数倍。
https://img-blog.csdnimg.cn/direct/6c83825fd87e4e1b8b7209708e993f1c.png
    参数3是位图布局,雷同于 open 函数中的参数3(文件打开方式),常用的选项有以下几个:

[*]IPC_CREAT 创建共享内存,假如存在,则利用已经存在的
[*]IPC_EXCL 避免利用已存在的共享内存,不能单独利用,必要共同 IPC_CREAT 利用,作用是当创建共享内存时,假如共享内存已经存在,则创建失败
[*]权限 由于共享内存也是文件,所以权限可设为文件的起始权限 0666
    参数3常用的组合方式:


[*]IPC_CREAT    假如不存在键值与key相称的共享内存,则新建一个共享内存并返回该共享内存的句柄;假如存在如许的共享内存,则直接返回该共享内存的句柄
[*]IPC_CREAT | IPC_EXCL    假如不存在键值与key相称的共享内存,则新建一个共享内存并返回该共享内存的句柄;假如存在如许的共享内存,则堕落返回
换句话说:


[*]利用组合IPC_CREAT,一定会获得一个共享内存的句柄,但无法确认该共享内存是否是新建的共享内存。
[*]利用组合IPC_CREAT | IPC_EXCL,只有shmget函数调用成功时才会获得共享内存的句柄,并且该共享内存一定是新建的共享内存。
我们就可以写出下面这些接口了
#pragma once

#include<iostream>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string>
#include <fcntl.h>
#include <cerrno>
#include<cstdlib>
using namespace std;

const string pathname="/home/zs_108/A";//我的processa所在地
const int proj_id=0x8088;//随便取的
const int SIZE=4096;//4kb

key_t Getkey()//获取key值
{
    key_t key=ftok(pathname.c_str(),proj_id);
    if(key<0)
    {
      perror("Getkey failed");
      exit(1);//没key玩个屁啊
    }
    printf("key: %x\n", key); //打印key值
    return key;
}

int GetShareMem()//获取共享内存
{
    int shmid=shmget(Getkey(),SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存
    if(shmid<0)
    {
      perror("Creat failed");
      exit(2);//共享内存都创建不出来,玩什么呢?
    }
    printf("shm: %d\n", shmid); //打印句柄
    return shmid;
}
   我们补充一个小知识
Linux当中,我们可以利用ipcs下令查看有关进程间通信设施的信息。
https://img-blog.csdnimg.cn/direct/de7d298d4bb34ced93949712159fd693.png
单独利用ipcs下令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:


[*]-q:列出消息队列相关信息。
[*]-m:列出共享内存相关信息。
[*]-s:列出信号量相关信息。
例如,携带-m选项查看共享内存相关信息:
https://img-blog.csdnimg.cn/direct/bcd01807ca7446508610070c890e9371.png
 此时,根据ipcs下令的查看效果和我们的输出效果可以确认,共享内存已经创建成功了。
ipcs下令输出的每列信息的含义如下:
https://img-blog.csdnimg.cn/direct/8e1e57a2f67549a6914efcf39cd46ade.png
 留意: key是在内核层面上包管共享内存唯一性的方式,而shmid是在用户层面上包管共享内存的唯一性,key和shmid之间的关系雷同于inode和fd之间的的关系。
 我们先拿个东西测试一下
processa.cpp
#include"common.h"

int main()
{
    int shmid=GetShareMem();//创建共享内存
    sleep(20);
} https://i-blog.csdnimg.cn/direct/684eebf834be49f681084a28756a225c.png
我们再运行一下
https://i-blog.csdnimg.cn/direct/7dd1de4905bb4937a4f31429302af412.png
报错了,这是共享内存在进程退出后还存在!!!!

   3.2.共享内存的释放

        通过上面创建共享内存的实验可以发现,当我们的进程运行完毕后,申请的共享内存仍旧存在,并没有被操作系统释放。
        实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。
        这分析,进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放,假如进程不自动删除创建的共享内存,那么共享内存就会不停存在,直到关机重启(system V IPC都是云云),同时也分析了IPC资源是由内核提供并维护的。
此时我们若是要将创建的共享内存释放,有两个方法,


[*]一就是利用下令释放共享内存,
[*]二就是在进程通信完毕后调用释放共享内存的函数进行释放。
   

[*]利用下令释放共享内存资源
我们可以利用ipcrm -m shmid下令释放指定id的共享内存资源。
https://img-blog.csdnimg.cn/direct/a8807e4025d647ec9647ebb8d28134cf.png
留意: 指定删除时利用的是共享内存的用户层id,即列表当中的shmid。 


[*]为什么不是key呢?
你是用户,用户层同一利用shmid,只有操作系统能用key啊!!!!
我们可以利用这个来解决上面谁人问题
https://i-blog.csdnimg.cn/direct/ce0cd6266c55408bb3d3335d4ab4f765.png
   

[*] 利用程序释放共享内存资源
还有一个系统调用接口可以用来释放共享内存资源
https://img-blog.csdnimg.cn/direct/1fd72e889e4b49bfb8a1547954544d92.png
参数:


[*]第一个参数shmid,表示所控制共享内存的用户级标识符(shmid——shmget的返回值)。
[*]第二个参数cmd,表示具体的控制动作。
[*]第三个参数buf,用于获取或设置所控制共享内存的数据布局。
返回值:


[*]shmctl调用成功,返回0。
[*]shmctl调用失败,返回-1。
shmctl函数的第二个参数传入的常用的选项:
https://img-blog.csdnimg.cn/direct/d57aed42fab9465c851362e3e6f61ce6.png

 例如,在以下代码当中,共享内存被创建,两秒后程序自动移除共享内存,再过两秒程序就会自动退出。

processa.cpp
#include"common.h"

int main()
{
    int shmid=GetShareMem();//创建共享内存
    sleep(2);
   shmctl(shmid, IPC_RMID, NULL); //释放共享内存
        sleep(2);
}
我们可以在程序运行时,利用以下监控脚本时刻关注共享内存的资源分配情况:
while :; do ipcs -m;echo "###################################";sleep 1;done
通过监控脚本可以确定共享内存确实创建两秒后成功释放了。
https://i-blog.csdnimg.cn/direct/8bc1f473d8ce4f1d87f3d226572e4286.png

 3.2.共享内存的关联

        共享内存在被成功创建后,进程还不 “认识” 它,只有让待通信进程都 “认识” 同一个共享内存后,才气进行正常通信,让进程 “认识” 共享内存这一操作称为 关联
        当进程与共享内存关联后,共享内存才会 通过页表映射至进程的捏造地址空间中的共享区中
将共享内存连接到进程地址空间我们必要用shmat函数,shmat函数的函数原型如下:
https://img-blog.csdnimg.cn/direct/12584397384f4773b07a893e2e13fec1.png
shmat函数的参数分析:


[*]第一个参数shmid,表示待关联共享内存的用户级标识符(shmget的返回值)。
[*]第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。
[*]第三个参数shmflg,表示关联共享内存时设置的某些属性。
           共享内存映射至共享区时,我们可以指定映射位置(即通报参数2),但我们一样平常不知道具体地址,所以 可以通报 NULL,让编译器自动选择位置进行映射 
 第三个参数shmflg传入的常用的选项:
 选项   作用    SHM_RDONLY 关联共享内存后只进行读取操作    SHM_RND 若shmaddr不为NULL,则关联地址自动向下调解为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA) 0  默认为读写权限 
shmat函数的返回值分析:


[*]shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
[*]shmat调用失败,返回(void*)-1。
这时我们可以尝试利用shmat函数对共享内存进行关联。
#include"common.h"

int main()
{
    int shmid=GetShareMem();//创建共享内存

    printf("attach begin!\n");
        sleep(2);
        char* mem = (char*)shmat(shmid, NULL, 0); //关联共享内存
        if (mem == (void*)-1){
                perror("shmat");
                return 1;
        }
        printf("attach end!\n");
        sleep(2);

}
         代码运行后发现关联失败
https://i-blog.csdnimg.cn/direct/5d2f2c8a57c94d2c820fad0df606c0cb.png
主要缘故原由是我们利用shmget函数创建共享内存时,并没有对创建的共享内存设置权限,所以创建出来的共享内存的默认权限为0,即什么权限都没有,因此server进程没有权限关联该共享内存。
https://i-blog.csdnimg.cn/direct/27d38d479d734171b54cd4268cf5708d.png

   

[*]那么怎么设置权限呢?
      shmget函数创建共享内存时,在其第三个参数处设置共享内存创建后的权限,权限的设置规则与设置文件权限的规则雷同。
int shmid = shmget(Getkey, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建权限为0666的共享内存
         而共享内存的权限(perms)显示也不再是0,而是我们设置的666权限。
我们看例子
processa.cpp
#include"common.h"

int main()
{
    int shmid=GetShareMem();//创建共享内存
    sleep(2);

}https://i-blog.csdnimg.cn/direct/c47f13fb97e14957bc598ba4459312fb.png
如许子我们的共享内存就有权限了!!!!!就能被别人访问了!!!!
为了优化我们的利用,我们对GetShareMemHelper函数进行优化
#pragma once

#include<iostream>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string>
#include <fcntl.h>
#include <cerrno>
#include<cstdlib>
using namespace std;

const string pathname="/home/zs_108/A";//我的processa所在地
const int proj_id=0x7777;//随便取的
const int SIZE=4096;//4kb

key_t Getkey()//获取key值
{
    key_t key=ftok(pathname.c_str(),proj_id);
    if(key<0)
    {
      perror("Getkey failed");
      exit(1);//没key玩个屁啊
    }
    printf("key: %x\n", key); //打印key值
    return key;
}

int GetShareMemHelper(int flag)//获取共享内存
{
    int shmid=shmget(Getkey(),SIZE, flag); //创建新的共享内存
    if(shmid<0)
    {
      perror("Creat failed");
      exit(2);//共享内存都创建不出来,玩什么呢?
    }
    printf("shm: %d\n", shmid); //打印句柄
    return shmid;
}

int CreatShm()//负责创建共享内存
{
    return GetShareMemHelper(IPC_CREAT | IPC_EXCL|0666);
}

int GetShm()//单纯的使用已经存在的共享内存
{
    return GetShareMemHelper(IPC_CREAT);
} 我们将这个创建和利用分开来
https://i-blog.csdnimg.cn/direct/265dcb2682004dee89eb05b013d8b825.png

我们如今来看看
processa.cpp
#include"common.h"

int main()
{
    int shmid=CreatShm();//创建共享内存

    printf("attach begin!\n");
        sleep(2);
        char* mem = (char*)shmat(shmid, NULL, 0); //关联共享内存
        if (mem == (void*)-1){
                perror("shmat");
                return 1;
        }
        printf("attach end!\n");
        sleep(2);

}  运行看看
https://i-blog.csdnimg.cn/direct/59912fe85b85447ea84cff922bf8cad5.png
https://i-blog.csdnimg.cn/direct/6a6ebd026e364e0cab93a5e81bcefdf1.png
很好 
   留意: 程序运行结束后,会自动取消关联状态 
 3.4.共享内存的去关联

   如同关闭FILE*、fd、free等一些列操作一样,当我们关联共享内存,利用结束后,必要进行去关联,否则会造成内存走漏(指针指向共享内存,访问数据)
取消共享内存与进程地址空间之间的关联我们必要用shmdt函数,shmdt函数的函数原型如下:
https://i-blog.csdnimg.cn/direct/029145e2d9fe48c39563bb5f4f1309ef.png
这个函数利用非常简单,将已关联的共享内存地址通报进行去关联即可 
shmdt函数的参数分析:


[*]待去关联共享内存的起始地址,即调用shmat函数时得到的起始地址。也就是直接传shmat的返回值进去即可
shmdt函数的返回值分析:


[*]shmdt调用成功,返回0。
[*]shmdt调用失败,返回-1。
   留意:


[*] 共享内存在被删除后,已成功挂接的进程仍旧可以进行正常通信,不过此时无法再挂接其他进程
[*] 共享内存被提前删除后,状态 status 变为 销毁 dest
如今我们就可以或许取消共享内存与进程之间的关联了。 
#include"common.h"

int main()
{
    int shmid=CreatShm();//创建共享内存

    printf("attach begin!\n");
        sleep(2);
        char* mem = (char*)shmat(shmid, NULL, 0); //关联共享内存
        if (mem == (void*)-1){
                perror("shmat");
                return 1;
        }
        printf("attach end!\n");
        sleep(2);

    printf("detach begin!\n");
        shmdt(mem); //共享内存去关联
        printf("detach end!\n");
        sleep(2);
} 运行程序,通过监控即可发现该共享内存的关联数由1变为0的过程,即取消了共享内存与该进程之间的关联。
https://i-blog.csdnimg.cn/direct/5efea31313e94816989ddb8413e9c8ae.png
留意: 将共享内存段与当前进程脱离不便是删除共享内存,只是取消了当前进程与该共享内存之间的联系。
4.用共享内存实现通信

先来复习一下makefile
https://i-blog.csdnimg.cn/direct/8e6d9970a0ae47d5b0864382779fad0a.png
        为了让procesa和processb在利用ftok函数获取key值时,可以或许得到同一种key值,那么procesa和processb传入ftok函数的路径名和和整数标识符必须雷同,如许才气天生同一种key值,进而找到同一个共享资源进行挂接。这里我们可以将这些必要共用的信息放入一个头文件当中,procesa和processb共用这个头文件即可。 
common.h
#pragma once

#include<iostream>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string>
#include <fcntl.h>
#include <cerrno>
#include<cstdlib>
#include<cstring>
using namespace std;

const string pathname="/home/zs_108/A";//我的processa所在地
const int proj_id=0x7777;//随便取的
const int SIZE=4096;//4kb

key_t Getkey()//获取key值
{
    key_t key=ftok(pathname.c_str(),proj_id);
    if(key<0)
    {
      perror("Getkey failed");
      exit(1);//没key玩个屁啊
    }
    printf("key: %x\n", key); //打印key值
    return key;
}

int GetShareMemHelper(int flag)//获取共享内存
{
    int shmid=shmget(Getkey(),SIZE, flag); //创建新的共享内存
    if(shmid<0)
    {
      perror("Creat failed");
      exit(2);//共享内存都创建不出来,玩什么呢?
    }
    printf("shm: %d\n", shmid); //打印句柄
    return shmid;
}

int CreatShm()//负责创建共享内存
{
    return GetShareMemHelper(IPC_CREAT | IPC_EXCL|0666);
}

int GetShm()//单纯的使用已经存在的共享内存
{
    return GetShareMemHelper(IPC_CREAT);
}         在知道了共享内存的创建、关联、去关联以及释放后,如今可以尝试让两个进程通过共享内存进行通信了。在让两个进程进行通信之前,我们可以先测试一下这两个进程能否成功挂接到同一个共享内存上。
processa负责创建共享内存,创建好后将共享内存和processa进行关联,之后进入死循环,便于观察processa是否挂接成功。
processa代码如下:
#include"common.h"

int main()
{
    int shmid=CreatShm();//创建共享内存

        char* shmaddr = (char*)shmat(shmid, NULL, 0); //关联共享内存
        if (shmaddr == (void*)-1){
                perror("shmat failed");
                return 1;
        }

        while(true)//通信部分
        {
   ;
        }
       
       
        shmdt(shmaddr); //共享内存去关联
        shmctl(shmid,IPC_RMID,nullptr);//删除共享内存
}
我们可以运行一下看看有没有连接成功
while :; do ipcs -m;echo "###################################";sleep 1;done
https://i-blog.csdnimg.cn/direct/a8f522033fc64ffe8e9a3df6d06aa88a.png 挂接成功了啊
processb只必要直接和processa创建的共享内存进行关联即可,之后也进入死循环,便于观察processb是否挂接成功。
processb.cpp代码如下:
#include"common.h"

int main()
{
    int shmid=GetShm();//获取共享内存

        char* shmaddr = (char*)shmat(shmid, NULL, 0); //关联共享内存
        if (shmaddr == (void*)-1){
                perror("shmat failed");
                return 1;
        }

        while(true)//通信部分
        {
      ;
        }
       
       
        shmdt(shmaddr); //共享内存去关联
} 先后运行processa和processb后,通过监控脚本可以看到processa和processb所关联的是同一个共享内存,共享内存关联的进程数也是2,表示processa和processb挂接共享内存成功。
https://i-blog.csdnimg.cn/direct/2b2c94a29662448293907deaf381b467.png
此时我们就可以让processa和processb进行通信了,这里以简单的发送字符串为例。
进程要怎么读取/写入?调用系统调用接口read/write?共享内存已经在我们的进程的进程地址空间里了,就是让我们利用捏造地址来访问即可!!!这个和利用堆的内存是一样的,我们可以把这个shmat和malloc进行类比,shmat获取共享内存的首地址,malloc获得分配的堆内存的首地址
processb.cpp
include"common.h"

int main()
{
    int shmid=GetShm();//获取共享内存

        char* shmaddr = (char*)shmat(shmid, NULL, 0); //关联共享内存
        if (shmaddr == (void*)-1){
                perror("shmat failed");
                return 1;
        }

        while(true)//通信部分
        {
      cout<<"Please Enter@";
      fgets(shmaddr,4096,stdin);
        }
       
       
       
        shmdt(shmaddr); //共享内存去关联
} processa.cpp
#include"common.h"

int main()
{
    int shmid=CreatShm();//创建共享内存

        char* shmaddr = (char*)shmat(shmid, NULL, 0); //关联共享内存
        if (shmaddr == (void*)-1){
                perror("shmat failed");
                return 1;
        }

        while(true)//通信部分
        {
   cout<<"processb say@"<<shmaddr<<endl;//读取共享内存的内容
       sleep(1);
        }
       
       
        shmdt(shmaddr); //共享内存去关联
        shmctl(shmid,IPC_RMID,nullptr);//删除共享内存
}
我们先运行一下啊
https://i-blog.csdnimg.cn/direct/83ce0ca145cd42d19b806b898c3973f8.png 
我们processb还没有启动嘞!!!processa就不停运行!!!!
接下来我们运行一下processb,来运行一下
https://i-blog.csdnimg.cn/direct/df8a76c0fc51426895a623a56f9af5c3.png
我们发现确实能通信,但是我们也发现一些端倪,我输入的慢一点,左边的怎么还会重复打印我上一次输入的内容?,这个是共享内存的特点——内存!!!可以存东西的,可不是管道那样不能存数据的
5.共享内存的特点



[*](1)、共享内存无同步,无互斥之类的掩护机制
[*](2)、共享内存是全部进程间通信速度最快的。
[*](3)、共享内存的生命周期随内核。
6.共享内存与管道进行对比

6.1.通信速度比较

当共享内存创建好后就不再必要调用系统接口进行通信了(直接对地址空间进行操作),而管道创建好后仍必要read、write等系统接口进行通信。实际上,共享内存是全部进程间通信方式中最快的一种通信方式 
6.2.数据拷贝过程

read是把数据从内核缓冲区复制到进程缓冲区 , write是把进程缓冲区复制到内核缓冲区
当共享内存创建好后就不再必要调用系统接口进行通信了,而管道创建好后仍必要read、write等系统接口进行通信。
    我们先来看看管道通信:
https://img-blog.csdnimg.cn/direct/cba63fa7b9bb4e8699563a59c8619638.jpeg
从这张图可以看出,利用管道通信的方式,将一个文件从一个进程传输到另一个进程必要进行四次拷贝操作:

[*]服务端将信息从输入文件复制到服务端的临时缓冲区中。
[*]将服务端临时缓冲区的信息复制到管道中。
[*]客户端将信息从管道复制到客户端的缓冲区中。
[*]将客户端临时缓冲区的信息复制到输出文件中。
     我们再来看看共享内存通信:
https://img-blog.csdnimg.cn/direct/1d382eb60b1647cb9f2a3997c78f8cbe.jpeg
从这张图可以看出,利用共享内存进行通信,将一个文件从一个进程传输到另一个进程只必要进行两次拷贝操作:

[*]从输入文件到共享内存。
[*]从共享内存到输出文件。


7.共享内存的补充知识

7.1、共享内存的大小

在上面的代码中,我们将共享内存的大小设为 4096 字节,即一个 PAGE 页的大小(4kb);假如申请 4097 字节大小的共享内存,操作系统实际上会分配 8192 字节(8kb 的空间),但供共享内存利用的只有 4097 字节
   为什么会出现这种征象?


[*]由于操作系统为了避免因非法操作导致出现越界访问问题,所以会开发 PAGE 页的整数倍大小空间,多开发的空间不会给共享内存时,主要是用来检测是否出现了越界访问
https://img-blog.csdnimg.cn/direct/5c11f4c501454880a81bd4f1f6f9d69d.png
7.2.为什么共享内存是速度最快的IPC方法?



[*]① 共享内存的拷贝次数少
[*]② 在利用共享内存时不涉及系统调用接口(也就是不会有内核态到用户态之间的转化,由于都是在用户层进行操作的)
[*]③ 不提供任何掩护机制(没有同步与互斥) 
7.3、共享内存的缺点

共享内存这么快,为什么不直接只利用共享内存呢?
由于快是要付出代价的,由于 “快” 导致共享内存有以下缺点:
           多个进程无穷定地访问同一块内存区域,导致共享内存中的数据无法确保安全
即 共享内存 没有同步和互斥机制,某个进程大概数据还没写完,就被别人读走了,或者被别人覆盖了
总的来说,不加规则限定的共享内存是不推荐利用的

7.5.、获取共享内存的数据布局

System V尺度中还为共享内存提供了一个控制函数shmctl,其原型如下图所示:
https://img-blog.csdnimg.cn/direct/f174d3762ee649438601913cb401a0fb.png
https://img-blog.csdnimg.cn/direct/ed44600c85db416c9fe595064d1fc10c.png
我们可以看看第3个参数
https://i-blog.csdnimg.cn/direct/827bffc1b96340229b0f1772897f1299.png要控制共享内存,就得知道它的全部属性 
https://i-blog.csdnimg.cn/direct/74e8ec23a40b4eb182121a2d07cf0da8.png
之前在释放共享内存时,我们就已经利用过了 shmctl,给参数2传入的是 IPC_RMID,表示删除共享内存,除此之外,还可以给参数2通报以下动作:


[*]IPC_STAT 用于获取或设置所控制共享内存的数据布局
[*]IPC_SET 在进程有充足权限的前提下,将共享内存的当前关联值设置为 buf 数据布局中的值
buf 就是共享内存的数据布局,可以利用 IPC_STAT 获取,也可以利用 IPC_SET 设置
当参数2为 IPC_RMID 时,参数3可以不用通报;其他两种情况都需通报 struct shmid_ds *buf
演示代码:通过 shmctl 获取共享内存的数据布局,并从中获取 pid、key
common.h
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>

using namespace std;

#define PATHNAME "." // 项目名
#define PROJID 0x29C         // 项目编号

const int gsize = 4096;
const mode_t mode = 0666;

//将十进制数转为十六进制数
string toHEX(int x)
{
    char buffer;
    snprintf(buffer, sizeof buffer, "0x%x", x);
    return buffer;
}


// 获取key
key_t getKey()
{
    key_t key = ftok(PATHNAME, PROJID);
    if (key == -1)
    {
      // 失败,终止进程
      cerr << "ftok fail!"
             << "errno: " << errno << " | " << strerror(errno) << endl;
      exit(1);
    }

    return key;
}

// 共享内存助手
int shmHelper(key_t key, size_t size, int flags)
{
    int shmid = shmget(key, size, flags);
    if (shmid == -1)
    {
      // 失败,终止进程
      cerr << "shmget fail!"
             << "errno: " << errno << " | " << strerror(errno) << endl;
      exit(2);
    }

    return shmid;
}

// 创建共享内存
int createShm(key_t key, size_t size)
{
    return shmHelper(key, size, IPC_CREAT | IPC_EXCL | mode);
}

// 获取共享内存
int getShm(key_t key, size_t size)
{
    return shmHelper(key, size, IPC_CREAT);
}
 processa.cpp 
#include <iostream>
#include "common.h"

using namespace std;

int main()
{
    // 服务端创建共享内存
    key_t key = getKey();
    int shmid = createShm(key, gsize);

    cout << "getpid(): " << getpid() << endl;
    cout << "server key: " << toHEX(key) << endl;

    char *start = (char*)shmat(shmid, NULL, 0); //去关联
    if ((void*)start == (void*)-1)
    {
      cerr << "shmat fail!"
             << "errno: " << errno << " | " << strerror(errno) << endl;
      shmctl(shmid, IPC_RMID, NULL);//即使异常了,也要把共享内存释放
      exit(1);
    }

    struct shmid_ds buf;
    int n = shmctl(shmid, IPC_STAT, &buf);
    if (n == -1)
    {
      cerr << "shmctl fail!"
             << "errno: " << errno << " | " << strerror(errno) << endl;
      shmctl(shmid, IPC_RMID, NULL);//即使异常了,也要把共享内存释放
      exit(1);
    }

    cout << "==================" << endl;
    cout << "buf.shm_cpid: " << buf.shm_cpid << endl;
    cout << "buf.shm_perm.__key: " << toHEX(buf.shm_perm.__key) << endl;


    shmdt(start);   //去关联
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
} https://img-blog.csdnimg.cn/direct/024954b4b9ad46efb61233c01395e09d.png
通过程序证明了 共享内存确实有自己的数据布局
结论: 共享内存 = 共享内存的内核数据布局(struct shmid_ds) + 真正开发的空间
7.5.用管道解决共享内存没有同步机制的问题

共享内存是没有同步机制的,我们可以借助命名管道来实现同步机制
common.h
#pragma once

#include<iostream>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string>
#include <fcntl.h>
#include <cerrno>
#include<cstdlib>
#include<cstring>
#include <sys/types.h>
#include <sys/stat.h>
using namespace std;

const string pathname="/home/zs_108/A";//我的processa所在地
const int proj_id=0x7777;//随便取的
const int SIZE=4096;//4kb

key_t Getkey()//获取key值
{
    key_t key=ftok(pathname.c_str(),proj_id);
    if(key<0)
    {
      perror("Getkey failed");
      exit(1);//没key玩个屁啊
    }
    printf("key: %x\n", key); //打印key值
    return key;
}

int GetShareMemHelper(int flag)//获取共享内存
{
    int shmid=shmget(Getkey(),SIZE, flag); //创建新的共享内存
    if(shmid<0)
    {
      perror("Creat failed");
      exit(2);//共享内存都创建不出来,玩什么呢?
    }
    printf("shm: %d\n", shmid); //打印句柄
    return shmid;
}

int CreatShm()//负责创建共享内存
{
    return GetShareMemHelper(IPC_CREAT | IPC_EXCL|0666);
}

int GetShm()//单纯的使用已经存在的共享内存
{
    return GetShareMemHelper(IPC_CREAT);
}

#define FIFO_FILE "./myfifo"
#define MODE 0664

enum{
    FIFO_CREAT_ERR=1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR
}; processa.cpp 
#include"common.h"

int main()
{
       
    int shmid=CreatShm();//创建共享内存

        char* shmaddr = (char*)shmat(shmid, NULL, 0); //关联共享内存
        if (shmaddr == (void*)-1){
                perror("shmat failed");
                return 1;
        }


        int n=mkfifo(FIFO_FILE,MODE);//创建管道
    if(n==-1)
    {
      perror("mkfifo failed:");
      exit(FIFO_CREAT_ERR);
    }

        //打开管道,等待写入方打开之后,自己才会打开文件,向后执行,open阻塞
    int fd=open(FIFO_FILE,O_RDONLY);//读方式
    if(fd<0)
    {
      perror("open:");
      exit(FIFO_OPEN_ERR);
    }

        while(true)//通信部分
        {
                char c;
                ssize_t s=read(fd,&c,1);//管道里面读1个字符,要是没读到就一直break,直到读到一个字符才会执行后面的命令
                if(s==0) break;
                else if(s<0) break;

   cout<<"processb say@"<<shmaddr<<endl;//直接读取共享内存的内容
       sleep(1);
        }
       
       
        shmdt(shmaddr); //共享内存去关联
        shmctl(shmid,IPC_RMID,nullptr);//删除共享内存

       
        close(fd);//关闭管道

        int m=unlink(FIFO_FILE);//删除管道
      if(m==-1)
      {
            perror("unlink :");
            exit(FIFO_DELETE_ERR);
      }
}  processb.cpp
#include"common.h"

int main()
{
    int shmid=GetShm();//获取共享内存

        char* shmaddr = (char*)shmat(shmid, NULL, 0); //关联共享内存
        if (shmaddr == (void*)-1){
                perror("shmat failed");
                return 1;
        }


    //打开管道,等待写入方打开之后,自己才会打开文件,向后执行,open阻塞
    int fd=open(FIFO_FILE,O_WRONLY);//写方式
    if(fd<0)
    {
      perror("open:");
      exit(FIFO_OPEN_ERR);
    }

        while(true)//通信部分
        {
      cout<<"Please Enter@";
      fgets(shmaddr,4096,stdin);

      write(fd,"r",1);//通知对方去读
        }
       
       
        shmdt(shmaddr); //共享内存去关联
   
    close(fd);//关闭管道
} https://i-blog.csdnimg.cn/direct/510fe7874eb34a34b15c67b3af1b2889.png
完善啦!!!!
我们这里只是分析共享内存可以实现同步机制,并没有别的意思!!!! 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【Linux】进程间通信3——system V共享内存