学习系统编程No.21【进程间通信之共享内存】

简介: 学习系统编程No.21【进程间通信之共享内存】

引言:

北京时间:2023/4/16/21:53,刚刚把新文章发出去,开完班会回来,本来上篇博客在昨天就能发的,昨天下午打了一下午的羽毛球之后,饭都没吃,躺在床上,准备睡觉,定了一个19:30的闹钟,打算起来将博客剩余的内容写完,但是突然收到了一个不好的消息,导致我不能很好的调整自己的心态,但是在目前看来,一切还在可控范围之内,不然我就不会在这码字了,感慨……,导致昨天晚上摆烂了一晚上,好的是今天补了一些,所以一切都还在正轨,So,keep going!这篇博客,我们就真正正式地学习一下有关共享内存的知识


image.png


回顾命名管道

上篇博客,我们学习了有关命名管道和子进程回收等相关知识,知道了进程间通信是不限于"血缘关系"的进程之间的,就算是两个毫不相关的进程之间也具备通信的场景,并且我们知道,我们上篇博客学习的有关命名管道的知识,就是一种用来构建两个毫不相关的进程之间通信的方法,使用命名管道相关的知识,我们就可以让两个没有联系的进程也可以具备通信能力啦!这就是匿名管道和命名管道本质上最大的区别,匿名管道只能让具有"血缘关系"的进程完成进程间通信,而命名管道却不仅可以让具有血缘关系的进程间通信,而且还可以让完全没有联系的进程间进行通信,但匿名管道和命名管道此时除了这个最本质的区别之外,在使用方式上,也还有一定的差别,匿名管道由pipe函数创建并直接打开,而命名管道由mkfifo函数创建后,打开需要使用open系统接口,所以fifo(命名管道)与pipe(匿名管道)除了上述的本质区别之外,最大的区别就是上述使用方式的区别,区分了这两点之后,匿名管道和命名管道在本质上是一样,都只是一份"共享资源"摆了,一份用来存储临时数据的内存级文件而已


共享内存

明白了上述的知识,此时我们就知道了,没有联系的进程之间在使用了命名管道构建了相应环境之后,此时也是可以进行进程间通信的, 本质就是要让两个进程看到同一份共享的资源而已,所以接下来,我们就学习一下,实现两个没有任何联系的进程间完成通信的另一种方法:共享内存


基本原理

如下图所示:


image.png


代码编写

明白了上述的原理之后,此时我们就可以进入第二个阶段,也就是代码编写阶段,自己实现一下使用共享内存完成进程间通信,当然如果想要自己写代码来实现这一功能,那么就必须要使用系统调用接口,因为只有系统调用接口才可以帮助我们在内存中创建共享内存和找到对应创建出来的共享内存,具体接口如下所示:


13.png


从上图的使用说明可以看出,该接口头文件:#include <sys/ipc.h> #include <sys/shm.h>,具体使用方式int shmget(key_t key, size_t size, int shmflg);此时通过使用方式可以发现,该接口的第三个参数是一个shmflg,表明,这个接口使用的是标志位的传参方式,通过宏定义的方式,实现各种不同的行为,然后通过条件判断和按位与判断是否匹配, 例如上图中的 IPC_CREAT和IPC_EXCL 两个宏,此时这两个宏代与共享内存的创建有关,其中IPC_CREAT允许单独使用,具体的行为就是查看共享内存,如果共享内存不存在,就创建一个共享内存,如果共享内存存在,则获取已经存在的共享内存地址,然后返回,而IPC_EXCL则不允许单独使用,必须和IPC_CREAT配合使用(IPC_CREAT | IPC_EXCL),具体的行为是查看共享内存,如果共享内存不存在,就创建一个共享内存,如果已经存在则立马出错返回,所以得出结论,如果单独使用IPC_CREAT,那么这个共享内存可能是别人正在使用的,是一个旧的共享内存,而如果让IPC_EXCL配合IPC_CREAT,那么此时创建的共享内存一定是一个全新的共享内存,没有被使用的共享内存


搞定了我们之前学习过的有关位图知识的第三个参数,此时我们再来看看其它两个参数,第二个参数显然就是一个表示创建共享内存大小的参数,这里不多做讲解,我们重点看看第一个参数,第一个参数是决定两个进程间能不能找到同一共享内存的关键,表示的是一个关键数字,这个关键数字就是用来让两个进程寻找到同一共享内存的钥匙,类似于就是该共享内存的编号一般!那么这个key值我们应该怎么获取呢?此时就涉及到了第二个系统调用接口,如下:


第二个接口,给共享内存设置关键字(ftok):


14.png


创建一个关键字的系统调用接口,ftok(),具体使用方法如上,头文件:#include<sys/types.h> #include<sys/ipc.h>,调用方式key_t ftok(const char *pathname, int proj_id);具体功能:通过具体参数的传递,创建出一个唯一的 IPC(进程间通信)标识符,为创建的共享内存提供唯一该标识符,进而让两个不同的进程通过该标识符找到对应的共享内存,知道了ftok()接口的主要作用和使用方式,此时要 注意:在操作系统内部,进行进程间通信的不可能只有一对进程,而是有很多很多对进程,所以此时操作系统无论是在效率方面(快速定位对应的共享内存)还是管理方面(不能杂乱无章)都必须对创建出的共享内存进行 管理(先描述,再组织),最终变成对一个一个的struct shm结构体的增删查改,所以此时这个结构体struct shm中存放的就是某一共享内存的全部属性(创建大小、创建时间、对应的key值等),所以通过类比和类推,结合以前的知识,一个文件=内容+属性或者是一个进程=进程对应的内核数据结构(struct task)+ 对应的代码和数据,此时就可以知道在操作系统内部,共享内存=共享内存的内核数据结构(struct shm)+ 在内存上开辟的空间


并且要明白在使用共享内存构建进程间通信场景的时候,和使用命名管道构建进程间通信是类似的,只要一个进程创建了命名管道(也就是打开了某一个文件),此时另一个进程此时就不需要再打开该文件,而是可以直接通过文件名和对应的文件路径,找到对应的文件,然后向该文件中读取或者写入,所以同理,创建共享内存,只要其中一个进程使用shmget()接口创建了共享内存,那么此时另一个进程就不需要再创建了,而只要根据ftok()接口的返回值,找到对应的共享内存(依次比对),然后向其中写入或者读取数据就行了,所以这也就是为什么要使用ftok()接口生成一个key值的主要原因,具体原理如下图所示:


15.png


通过上图和上述文字的描绘,此时我们就明白了使用共享内存进行进程间通信中shmget()接口和ftok()的基本使用原理,shmget()接口就像是盖一个房子,ftok()接口就像是打开该房子某一个房间的钥匙,所以进程间通信的本质还是在构建进程间通信的场景,也就是如何让这两个进程看到同一份"资源"


具体代码如下:

common.hpp文件:

16.png


server.cpp文件:

17.png

client.cpp文件:

18.png


如上图代码所示,在common.hpp文件中对shmget()接口进行封装,一个表示使用该接口创建共享内存,一个表示使用该接口获取共享内存(具体和上述有关key值和IPC_CREAT、IPC_EXCL有关),运行结果如下图所示:

19.png


如上图所示,此时发现当我们使用ftok()接口创建了一个key值时,两个进程由于PATHNAME和PROJ_ID是一样的,所以最终调用ftok()接口生成的返回值是相同的,此时其中一个进程就可以拿着这个key值去创建一个新的共享内存出来,并且另一个进程也可以根据这个key值去寻找对应的共享内存,并且此时还发现,当我们此时先执行client,再执行server,此时的server告诉我们的是该共享内存已经存在,而如果是先执行server,再执行client,此时如下图所示:


20.png


原因很简单,本质上就是IPC_CREAT和IPC_EXCL的区别,如果先执行client,那此时表示的就是先执行IPC_CREAT,而单独使用IPC_CREAT,如果有对应key值的共享内存,那么就返回该共享内存,如果没有就创建一个新的共享内存,而IPC_CREAT | IPC_EXCL一起使用的时候,如果没有对应key值的共享内存,那么它创建一个新的,如果有对应key值的共享内存,那么由于此时它必须创建一个新的共享内存,所以此时直接就报错,说该共享内存已经存在,同理另一种情况,先执行IPC_CREAT | IPC_EXCL,再执行IPC_CREAT,那么由于使用IPC_CREAT无论是对应key值的共享内存是存在,还是不存在,它都会返回对应key值的共享内存,所以此时可以正常运行

明白了上述知识之后,此时还要明白一点,当我们在某一个程序中使用shmget()接口创建了共享内存之后,当该程序结束,也就是该进程退出,此时共享内存的生命周期并不会像匿名管道和命名管道一样,随着进程的结束而释放,由上图就可以看出,当client进程退出之后,server进程再使用key值匹配共享内存时,显示的是该共享内存已存在,所以得出结论:当创建了一个共享内存之后,如果没有对该共享内存进行删除处理,那么该共享内存就会一直存在,除非电脑被重启


删除共享内存

所以为了解决上述的问题(当进程退出,也就是通信完成,共享内存不会被删除),此时有两个方法,如下:

1.使用指令手动删除

功能 指令
显示所有的IPC设施 ipcs -a
显示所有的消息队列Message Queue ipcs -q
显示所有的信号量 ipcs -s
显示所有的共享内存 ipcs -m
显示IPC设施的详细信息 ipcs -q -i id
删除一个共享内存 ipcrm -m shmid


感兴趣的同学可以参考该链接:IPC指令详解

2.使用系统调用接口

接口:shmctl(控制共享内存的状态)

具体使用方式,如下图所示:


22.png


头文件:#include<sys/ipc.h>、#include<sys/shm.h>,具体调用方式: int shmctl(int shmid, int cmd, struct shmid_ds *buf);其中第一个参数shmid表示的就是具体你想要控制的共享内存的shmid;第二个参数cmd表示指定要执行的操作(命令)如:删除或更改共享内存区域的属性,例如:IPC_STAT:获取共享内存的状态信息,并将其存储在由buf指定的shmid_ds结构体中,IPC_SET:设置共享内存区域的状态信息,这些信息包含在buf所指向的shmid_ds结构体中,IPC_RMID:从系统中删除共享内存区域;第三个参数buf:用于传递或接收共享内存的信息,本质就是一个结构体指针,因为共享内存=内核数据结构(struct shmid_ds)+ 开辟的内存,所以此时的buf就是一个指向struct shmid_ds结构体的指针,具体使用方式如下代码所示:


23.png


上述代码表示的就是在程序内部使用系统调用接口,直接删除对应的shmid共享内存

如何给共享内存设置权限:

指令:ipcs -m显示所有所有的共享内存和对应的信息,如下图所示:

24.png


如上图所示,此时我们可以知道,一个共享内存是存在一定的权限的(默认无任何权限),但是如果我们想要让它拥有一定的权限是可以手动添加的,如下代码所示:

25.png


如何关联共享内存

搞定了上述的知识,创建共享内存和释放共享内存,此时我们距离构建出进程间通信就差最后一步了,也就是如何让进程和共享内存可以关联和取消关联,也就是表明,虽然我们自己创建了共享内存,但是这个共享内存并不一定是给我们自己使用的,想要使用该共享内存,就一定要先让该进程和对应的共享内存关联起来,此时就面临一个问题,那应该如何让进程和共享内存关联起来呢?此时就又涉及到了一个系统调用接口:shmat() 具体使用方式如下图所示:


26.png

shmat() 用于将共享内存关联到进程地址空间的函数,其参数具体含义如下:


shmid:需要被关联到进程虚拟地址空间中的共享内存的标识符

shmaddr:指定共享内存被关联到虚拟地址空间的地址,如果为 NULL ,则表示由系统自动分配一个地址

shmflg:指定某进程可以对虚拟地址空间中的共享内存的访问模式、操作标志和权限(如读写、信号量等)

总:shmat() 函数的作用是将 shmid 所对应的共享内存关联到当前进程的地址空间,关联后,该共享内存可以通过 shmaddr 所指定的地址直接访问或者被更新,如果 shmaddr 参数为 NULL ,系统将根据进程地址空间的可用空间自动选择一个合适的地址来映射共享内存,在 shmflg 中可以设置访问权限、创建共享内存等选项,最后,当成功调用 shmat() 函数后,将返回一个指向共享内存区域附加地址的指针,如果发生错误,则返回 -1


注意: 因为共享内存的大小我是知道的,所以此时只要知道共享内存在虚拟地址空间中的起始地址,此时根据起始地址和偏移量(大小),此时就可以获取到整个映射在虚拟地址空间上的共享内存


代码实现如下:


27.png

所以此时只要让两个不同的进程都调用该函数,此时就可以让两个不同的进程都和对应的共享内存关联起来了,此时我们就完成了我们的目的:让两个不同的进程看到同一份"资源",这样我们就将进程间通信的环境给构建出来了,最后,就可以进行进程间通信了,但是,此时要 注意:就是当两个进程之间完成了通信之后,此时要让对应的进程从对应的共享内存中脱离出来(也就是取消关联),此时就又涉及到了一个系统调用接口:shmdt,具体使用方式和shmat上相同,只是参数的传递不同而已,具体调用方式:int shmdt(const void *shmaddr);所以想要让一个进程脱离某个共享内存,此时只需要改变对应进程虚拟地址空间中的共享内存和物理内存上的共享内存的页表映射关系就行,总之,shmdt() 函数可以将对共享内存的访问从当前进程的地址空间中分离出来,并使该共享内存可以被其他进程访问或删除


具体代码如下

具体通信还没开始,但是进程间通信的环境已经搭建完成

客户端:

#include <iostream>
#include "common.hpp"
using namespace std;
int main()
{
    // 1.获取相同的key值
    key_t k = get_key();
    cout << "server key:" << to_hex(k) << endl;
    // 2.创建共享内存(一个创建,一个获取)
    int shmid = creat_shm(k, shmsize);
    cout << "server shmid:" << shmid << endl;
    // 3.将自己和共享内存关联起来(也就是将共享内存关联到进程的虚拟地址空间中)
    char* start = attch_shm(shmid);
    //具体通信内容
    sleep(5);
    // 4.将自己和共享内存取消关联
    detattch_shm(start);
    // struct shmid_ds ds;
    // int n = shmctl(shmid,IPC_STAT,&ds);
    // if(n == -1)
    // {
    //    cout << "key:" << to_hex(ds.shm_perm.__key) << endl;
    //    cout << "creater pid:" << ds.shm_cpid << " : " << getpid() << endl;
    // }
    // 要明白shmget()接口的返回值代表的就是对应共享内存的标识符
    // 4.删除共享内存(删一次就够了)
    del_shm(shmid);//使用shmctl共享内存控制接口,删除一个共享内存
    return 0;
}

服务端:

#include <iostream>
#include "common.hpp"
using namespace std;
int main()
{
    // 1.获取到相同的key值
    key_t k = get_key(); // 这个写法不好,写成获取16进制的方法更好
    cout << "client key:" << to_hex(k) << endl;
    // 2.获取共享内存
    int shmid = get_shm(k, shmsize);
    cout << "client shmid:" << shmid << endl;
    // 3.将自己和共享内存关联起来
    char* start = attch_shm(shmid);
    //具体通信内容
    // 4.将自己和共享内存取消关联
    detattch_shm(start);
    return 0;
}

共享代码:

#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <sys/ipc.h> //这个是ftok接口的头文件
#include <sys/shm.h>
#include <sys/types.h>
#include <cerrno>
#include <string.h>
#include <cstdio>
#include <stdlib.h>
#include <unistd.h>
#include <cassert>
#include <sys/stat.h>
#include <sys/shm.h>
using namespace std;
// 并且注意:此时的这个头文件的一个公共头文件,让两个进程文件都可以使用的文件
#define PATHNAME "."  // 此时这个表示的就是定义一个路径,待会供给shmget接口使用(自己指定,只要两个进程相同就行)
#define PROJ_ID 0x666 // 这个也就是自己给的,也是可以随便写的
// 此时只要把这两个参数传递给ftok()接口,此时该接口根据特定的算法就会生成一个唯一的key值
// 这样定义的好处就是可以直接让两个进程使用同一个路径和id
//-----------------------------------------------------------------------------------
const int shmsize = 4096; // 这个表示的就是开辟共享内存的大小(单位:字节)
key_t get_key() // 这个key_t本质就是一个int而已,大佬就是喜欢typedef
{
    key_t k = ftok(PATHNAME, PROJ_ID);
    if (k == -1)
    {
        cout << errno << " : " << strerror(errno) << endl;
        exit(1);
    }
    return k; // 代码走到这里表示的就是ftok()函数生成key值成功,此时就可以把这个key值传给别的进程中的shmget()接口使用了
}
// 将key值搞成16进程(用处不大),但是自己要会写
string to_hex(int x)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%x", x);
    return buffer;
}
//此时下面的写法显得代码冗余,所以改进如下写法
int creat_shm_helper(key_t k,int shmsize,int flag)
{
    int shmid = shmget(k,shmsize,flag);
    if(shmid == -1)
    {
        cerr << errno << " : " << strerror(errno) << endl;
        exit(2);//创建失败就退出
    }
    return shmid;
}
int creat_shm(key_t k, int shmsize) // 此时只是将shmget()接口再进行一次封装而已,这个一定要懂
{
    // int IPC = shmget(k,shmsize,IPC_CREAT | IPC_EXCL);//注意:一定要创建一个全新的出来,因为老的可能别的进程正在使用中
    // if(IPC == -1)
    // {
    //     cerr << errno << " : " << strerror(errno) << endl;
    //     exit(2);//创建失败就退出
    // }
    // return IPC;//程序来到这里表示的就是成功,返回对应的内存标识符就行(这样进程就可以根据标识符,找到同一共享内存啦!)
    umask(0);
    return creat_shm_helper(k,shmsize,IPC_CREAT | IPC_EXCL | 0666);
}
int get_shm(key_t k, int shmsize)
{
    // int IPC = shmget(k,shmsize,IPC_CREAT);//此时表示的是获取一个共享内存,只要把对应的key值给给shmget()接口,此时该接口就会依次去和已经存在的共享内存比较,最终把对应key的共享内存放回给给IPC
    // if(IPC == -1)
    // {
    //     cerr << errno << " : " << strerror(errno) << endl;
    //     exit(2);//创建失败就退出
    // }
    // return IPC;
    return creat_shm_helper(k,shmsize,IPC_CREAT);
}
char* attch_shm(int shmid)
{
    char* start = (char*)shmat(shmid,NULL,0);//这个接口的返回值是一个void*,和malloc是一样的,malloc的返回值也是一个void*,本质就是放回一个地址给你
    return start;
}
void detattch_shm(char* start)
{
    int n = shmdt(start);
    assert(n != -1);
}
void del_shm(int shmid)
{
    int n = shmctl(shmid,IPC_RMID,0);
    assert(n != -1);
}
#endif

北京时间:2023/4/20/0:01

image.png

总结:进程间通信之共享内存的知识算是学完了,发现,不管是那种通信方式,本质上还是让两个进程看到同一份"资源"而已!


相关文章
|
2月前
|
存储 缓存 监控
|
2月前
麒麟系统mate-indicators进程占用内存过高问题解决
【10月更文挑战第7天】麒麟系统mate-indicators进程占用内存过高问题解决
234 2
|
21天前
|
监控 Java Android开发
深入探讨Android系统的内存管理机制
本文将深入分析Android系统的内存管理机制,包括其内存分配、回收策略以及常见的内存泄漏问题。通过对这些方面的详细讨论,读者可以更好地理解Android系统如何高效地管理内存资源,从而提高应用程序的性能和稳定性。
54 16
|
21天前
|
网络协议 Linux 虚拟化
如何在 Linux 系统中查看进程的详细信息?
如何在 Linux 系统中查看进程的详细信息?
39 1
|
2月前
|
运维 JavaScript Linux
容器内的Nodejs应用如何获取宿主机的基础信息-系统、内存、cpu、启动时间,以及一个df -h的坑
本文介绍了如何在Docker容器内的Node.js应用中获取宿主机的基础信息,包括系统信息、内存使用情况、磁盘空间和启动时间等。核心思路是将宿主机的根目录挂载到容器,但需注意权限和安全问题。文章还提到了使用`df -P`替代`df -h`以获得一致性输出,避免解析错误。
|
2月前
麒麟系统mate-indicators进程占用内存过高问题解决
【10月更文挑战第5天】麒麟系统mate-indicators进程占用内存过高问题解决
169 0
|
3月前
|
监控 API
【原创】用Delphi编写系统进程监控程序
【原创】用Delphi编写系统进程监控程序
|
4月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
400 0
|
2月前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
80 1
|
2月前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。