【Linux修炼】16.共享内存

在这里插入图片描述每一个不曾起舞的日子,都是对生命的辜负。

共享内存

  • 一.共享内存的原理
  • 二.共享内存你的概念
    • 2.1 接口认识
    • 2.2演示生成key的唯一性
    • 2.3 再谈key
  • 三.共享资源的查看
    • 3.1 如何查看IPC资源
    • 3.2 IPC资源的特征
    • 3.3 进程之间通过共享内存进行关联
  • 四.共享内存的特点
  • 五.共享内存的内核结构
  • 六.共享内存函数的总结

共享内存是为通信而诞生的。除了上一节中讲到的公共文件的方案,还有什么其他方案呢?----以共享内存的方式

一.共享内存的原理

在之前学过的进程地址空间的基础上,我们知道,进程之间具有独立性,因为每个进程的内核数据结构的数据以及页表的映射都是独立的。而对于共享内存,我们同样了解,这是为了让进程之间能够进行通信的公共空间,接下来就通过进程地址空间的结构去了解共享空间的位置及原理:

image-20230311104955697

OS为了让两个毫不相关的进程之间进行通信,进行了三个工作:

  1. 在对应的内存当中让用户帮OS申请一块空间(通过指定的调用接口)
  2. 将创建好的内存映射进进程的地址空间(用户就可以通过访问起始地址的方式来进行对申请的这块内存空间的访问)
  3. 未来不想通信:
    • 取消进程和内存的映射关系
    • 释放内存

因此,我们把申请的这块空间称之为共享内存,将映射关系称之为进程和共享内存进行挂接。将取消进程和内存的映射关系称之为去关联,释放内存释放的就是共享内存。

理解:

  • 进程间通信,是专门设计的,用来IPC的,和malloc/new不是一个东西。
  • 共享内存是一种通信方式,所有想通信的进程,都可以用。
  • OS中一定会存在着很多共享内存。

二.共享内存你的概念

通过让不同的进程,看到同一个内存块的方式,叫做共享内存。

2.1 接口认识

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

int shmget(key_t key, size_t size, int shmflg);// size:共享内存的大小

对于shmflg,常见的有这么两种选择:

  • IPC_CREAT:如果不存在共享文件则创建,存在则获取
  • IPC_EXCL
    • 1.无法单独使用,单独使用没有意义,需要结合IPC_CREAT
    • 2.IPC_CREAT|IPC_EXCL:如果不存在,就创建,如果已存在,就出错返回。即在用户的角度,如果创建成功,一定是一个新的shm!

shmget返回值: 记住他是一个标识符就够用了,得到的是共享内存的标识符。(和文件fd没有任何关系)

key: 是什么不重要,最重要的是其具备的唯一性。

而获取key值,则通过一个新的接口:ftok ,ftok通过指定的字符串数据*pathname以及char类型的proj_id数据进行一系列的算法整合返回了具有唯一性的Key:

key_t ftok(char *pathname, char proj_id);

由于创建的key值有可能已经被别人使用了,因此有失败的可能性。创建Key值如果失败,则返回-1。

2.2演示生成key的唯一性

makefile

.PHONY:all
all:shm_client shm_server

shm_client:shm_client.cc
	g++ -o $@ $^ -std=c++11
shm_server:shm_server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f shm_client shm_server

comm.hpp

#ifndef _COMM_HPP_
#define _COMM_HPP_

#include<iostream>
#include<cerrno>
#include<cstdlib>
#include<cstring>
#include<cstdio>
#include<sys/ipc.h>
#include<sys/shm.h>

#define PATHNAME "."//当前路径
#define PROJ_ID 0x66

key_t getKey()
{
    key_t k = ftok(PATHNAME, PROJ_ID);
    if(k < 0)
    {
        // cin, cout, cerr ->stdin, stdout, stderr->0, 1, 2;标准错误stderr向2打印。
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(1);//终止进程
    }
    return k;
}

#endif

shm_server.cc

#include"comm.hpp"

int main()
{
    key_t k = getKey();

    printf("0x%x\n", k);
    return 0;
}

shm_client.cc

#include"comm.hpp"

int main()
{
    key_t k = getKey();

    printf("0x%x\n", k);
    return 0;
}

image-20230313161821844

通过make后执行发现,两个程序的k值是一样的,这就证明了ftok指定参数的返回值是唯一的。(k实际上就是32位的一个整数)

2.3 再谈key

OS中一定存在多个共享内存,因为彼此之间可能都需要通信,因此也就都需要申请一块空间。而OS申请的共享空间,也一定和进程一样需要被管理,既然需要管理,那么一定也是先描述再组织的方式,即共享内存 = 物理内存块+共享内存的相关属性

之前谈到过,key是什么不重要,能进行唯一性的标识最重要,因此创建共享内存的时候,是如何保证共享内存在系统中是唯一的呢?当然是通过key来确定的,只要一个进程也看到了同一个key,就能够访问这个共享内存。那么key在哪里,实际上这就和PCB一样,key就在内核中的属性集合里,即:

struct shm{
    key_t key;
    //...
}

即:key是通过shmget这样的系统调用,设置进入共享内存属性中,用来表示该共享内存在内核中的唯一性!

shmid和key就好比fd和inode。为什么有了key还需要shmid呢?通过key和shmid的区分,能够面向系统层面和用户层面,这样能够更好的进行解耦,以免内核中的变化影响到用户级。

三.共享资源的查看

共享(IPC)

3.1 如何查看IPC资源

ipcs -m/q/s查看:image-20230313194544812

3.2 IPC资源的特征

image-20230313195548826

我们发现,当第一次执行成功之后,再次调用不会成功,这是因为共享内存并不像管道一样进程结束之后自动释放内存,共享内存的声明周期是随着OS的,不是随着进程的!因此这就需要我们主动的去释放这块空间,即通过指令:ipcrm -m 加上这块共享内存shmid的值,虽然key也是唯一的,但key是系统层面的,shmid才是对于我们用户层面的。

对于释放共享内存,除了上述的手动命令,其也有自己的接口能够进行共享内存物理空间的释放,即:

#include<sys/ipc.h>
#include<sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);//cmd代表控制的种类,即内置的有多种选择。
//返回值:失败则返回-1
void delShm(int shmid)
{
    if(shmctl(shmid, IPC_RMID, nullptr) == -1)
    {
        std::cerr << errno << " : " << strerror(errno) << std::endl;
    }
}

以下代码复制SSH渠道:执行while :; do ipcs -m; sleep 1; echo"--------------------";done,并在左侧执行shm_server,观察现象:

#ifndef _COMM_HPP_
#define _COMM_HPP_

#include<iostream>
#include<cerrno>
#include<cstdlib>
#include<cstring>
#include<cstdio>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>

#define PATHNAME "."//当前路径
#define PROJ_ID 0x66
#define MAX_SIZE 4096 //单位是字节
key_t getKey()
{
    key_t k = ftok(PATHNAME, PROJ_ID);//可以获取同样的key
    if(k < 0)
    {
        // cin, cout, cerr ->stdin, stdout, stderr->0, 1, 2;标准错误stderr向2打印。
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(1);//终止进程
    }
    return k;
}

int getShmHelper(key_t k, int flags)
{
    //k是要shmget,设置进入共享内存属性中的!用来表示
    //该共享内存,在内核中的唯一性!!
    //shmid VS key
    //fd    VS inode
    int shmid = shmget(k, MAX_SIZE, flags);
    if(shmid < 0)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        //失败就终止,没有共享内存了
        exit(2);
    }
    return shmid;
}
int getShm(key_t k)
{
    return getShmHelper(k, IPC_CREAT);
}

int createShm(key_t k)
{
    return getShmHelper(k, IPC_CREAT | IPC_EXCL);
}

void delShm(int shmid)
{
    if(shmctl(shmid, IPC_RMID, nullptr) == -1)
    {
        std::cerr << errno << " : " << strerror(errno) << std::endl;
    }

}


#endif
#include"comm.hpp"

int main()
{
    key_t k = getKey();
    printf("key: 0x%x\n", k); // key
    int shmid = createShm(k);
    printf("shmid: %d\n", shmid); // shmid
    sleep(5);//5s之后就会被删除
    delShm(shmid);
    
    return 0;
}

共享内存1

发现,进程执行之前没有对应的信息,执行之后信息出现5s,最终被释放。再次执行也不会出错。

进程之间进行关联

上述都没有提到进程进行关联的问题,有几个进程能够进行关联在上述动图右侧nattch可以看到,明显看到上面的nattch值为0,那么下面就来进行挂接一下:

此时就又有一个新的接口:

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

//参数1:指定的共享内存,参数二:地址空间,一般设置为nullptr,参数三:读写权限,一般设置为0就可以了,默认就可以读写。

//返回值:共享内存空间的起始地址,就等价于malloc的返回值
void *shmat(int shmid, const void* shmaddr, int shmflg);

因此就可以在comm.hpp中加上这个功能:

void* attchShm(int shmid)
{
    void* mem = shmat(shmid, nullptr, 0);//64系统:指针占8字节
    if((long long)mem == -1L)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(3);
    }
    return mem;
}

image-20230313211728082

但此时挂接就需要进行读写,因此还需要在createshm中添加选项:image-20230313212309740

接下来看看运行结果:

共享内存2

可以发现的是,由于我们新增了0600即拥有者的读写权限,perm也就显示了600,此外nattch的链接数量也变成了1,这说明有一个进程和这个共享内存关联起来了,而我们所演示的就是我自己的进程与共享内存进行了关联。运行之后同样可以释放。但上面直接释放过于粗暴,因为我们之前将进程和共享内存进行了关联,所以我们需要在释放之前将这个关联去掉,否则就有可能出问题。去关联并不是删掉共享内存,而是回收对应的页表。为了去关联,就又引出了一个接口:

// 参数就是在shmat时设定的返回值,对于返回值:成功就是0,失败就是-1.
int shmdt(const void* shmaddr);

image-20230313213653870

image-20230313213721589

因此,添加了这段代码后,就比上述现象在结束之前多了一个nattch变为0的过程。


3.3 进程之间通过共享内存进行关联

上述我们已经实现了shm_server与共享内存的关联,如果想让两个进程之间进行通信,那就需要另一个shm_client也与同一个共享内存进行关联:image-20230314101239560

但对于这段代码,不需要释放共享内存,因为在shm_server.cc中已经实现。这段接入之后,nattch的关联数就会变成2。


在之前的学习中,我们通过管道采用char buffer[1024]缓冲区的方式进行通信,现在有了共享内存就可以通过共享内存将两个进程连接起来。

shm_server.cc

#include"comm.hpp"

int main()
{
    key_t k = getKey();
    printf("key: 0x%x\n", k); // key
    int shmid = createShm(k);
    printf("shmid: %d\n", shmid); // shmid
    sleep(5);//5s之后就会被删除

    char* start = (char*)attchShm(shmid);//挂接成功
    printf("attach success, addresss start: %p\n", start);
    //使用
    while(true)
    {
        //char buffer[]; read(pipefd, buffer)
        printf("client say: %s\n", start);
        sleep(1);
    }
    //去关联:让进程和共享内存丧失关联性
    detachShm(start);
    sleep(5);
    delShm(shmid);
    return 0;
}

shm_client.cc

#include"comm.hpp"

int main()
{
    key_t k = getKey();
    printf("key: 0x%x\n", k);
    int shmid = getShm(k);
    printf("shmid: %d\n", shmid);
    sleep(5);
    //与共享内存产生关联
    char* start = (char*)attchShm(shmid);
    printf("attach success, address start: %p\n", start);
    
    const char* message = "hello server,我是另一个进程,正在和你通信";
    pid_t id = getpid();
    int cnt = 1;
    char buffer[1024];
    while(true)
    {
        sleep(1);
        snprintf(start, MAX_SIZE, "%s[pid:%d][消息编号:%d]", message, id, cnt++);
        // snprintf(buffer, sizeof(buffer), "%s[pid:%d][消息编号:%d]", message, id, cnt);
        // memcpy(start, buffer, strlen(buffer)+1);
        // //pid, count, message
    }
    //去关联:让进程和共享内存丧失关联性
    detachShm(start);
    return 0;
}

执行:(记得手动ipcrm -m)

共享内存3

四.共享内存的特点

共享内存的优点:

所有进程间通信,速度是最快的!因为其共享内存在进程地址空间能够大大减少数据的拷贝次数。(即本来用buffer,现在没有必要)

综合考虑管道和共享内存,考虑键盘输入和显示器输入,共享内存共有几次数据拷贝,即同一段代码,通过管道和共享内存,分别进行了几次拷贝?

对于管道来说,通过的是如下步骤:image-20230314143948484

将键盘输入的数据放到自己指定的缓冲区buffer中为第一次,将buffer中的数据拷贝到管道中是第二次,将管道中的数据拷贝到另一个进程的缓冲区中为第三次,将缓冲区的数据打印在显示器中为第四次,此外还有输入输出流stdin和stdout,所以为4+2次。

对于共享内存来说,没有中间的buffer,因此也就是2+2次。

共享内存的缺点:

共享内存不会进行同步和互斥的操作,没有对数据做任何的保护。也就是说,共享内存并不像管道一样,管道是当读写都打开时,如果不读,写满就会不写了,如果写端不写,读端就不会继续读了并且阻塞在那里,而共享内存没有做这样的保护。那么如何保护?今后将会学到信号量和互斥锁的方式对管道进行保护。

五.共享内存的内核结构

共享内存数据结构:

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 */
};

struct ipc_perm{
    key_t          __key;    /* Key supplied to shmget(2) */
    uid_t          uid;      /* Effective UID of owner */
    gid_t          gid;      /* Effective GID of owner */
    uid_t          cuid;     /* Effective UID of creator */
    gid_t          cgid;     /* Effective GID of creator */
    unsigned short mode;     /* Permissions + SHM_DEST and SHM_LOCKED flags */
    unsigned short __seq;    /* Sequence number */
}

共享内存的大小

共享内存的大小,一般建议是4KB的整数倍,因为系统分配共享内存是以4KB为单位的! — 内存划分内存块的基本单位。

否则内核会给你向上取整。但我们能够使用的仍是我们指定的大小。

六.共享内存函数的总结

上面在演示的时候,已经逐步的介绍了有关共享内存函数的功能,我们在这里总结一下:

shmget函数

功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
    key:这个共享内存段名字
    size:共享内存大小
    shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

shmat函数

功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
    shmid: 共享内存标识
    shmaddr:指定连接的地址
	shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
  • 说明:

    shmaddr为NULL,核心自动选择一个地址
    shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
    shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -
    (shmaddr % SHMLBA)
    shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
    

shmdt函数

功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
参数
	shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离,不等于删除共享内存段

shmctl函数

功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
	shmid:由shmget返回的共享内存标识码
	cmd:将要采取的动作(有三个可取值),如下
	buf:指向一个保存着共享内存的模式状态和访问权限的数据结构,一般设定为nullptr即可
返回值:成功返回0;失败返回-1

image-20230314152338242

此外,关于最终的代码展示在如下链接:共享内存代码

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

10个最频繁用于解释机器学习模型的 Python 库

文章目录什么是XAI&#xff1f;可解释性实践的步骤技术交流1、SHAP2、LIME3、Eli54、Shapash5、Anchors6、BreakDown7、Interpret-Text8、aix360 (AI Explainability 360)9、OmniXAI10、XAI (eXplainable AI)XAI的目标是为模型的行为和决定提供有意义的解释&#xff0c;本文整理…

C++基础算法①——高精度加减法计算

高精度算法1.导论2.高精度低精度3.高精度高精度4.高精度减法1.导论 当我们利用计算机进行数值计算&#xff0c;有时候会遇到这样的问题&#xff1a; n&#xff01;的精确结果是多少&#xff1f; 当n小于30的时候&#xff0c;我们当然可以通过电脑自带的计算器计算出来。但是当…

「Vue面试题」动态给vue的data添加一个新的属性时会发生什么?怎样去解决的?

一、直接添加属性的问题 我们从一个例子开始 定义一个p标签&#xff0c;通过v-for指令进行遍历 然后给botton标签绑定点击事件&#xff0c;我们预期点击按钮时&#xff0c;数据新增一个属性&#xff0c;界面也 新增一行 <p v-for"(value,key) in item" :key&q…

学习 Python 之 Pygame 开发坦克大战(一)

学习 Python 之 Pygame 开发坦克大战&#xff08;一&#xff09;Pygame什么是Pygame?初识pygame1. 使用pygame创建窗口2. 设置窗口背景颜色3. 获取窗口中的事件4. 在窗口中展示图片(1). pygame中的直角坐标系(2). 展示图片(3). 给部分区域设置颜色5. 在窗口中显示文字6. 播放音…

如何在CSDN中使用ChatGPT

本篇文章致力于帮助大家理解和使用ChatGPT&#xff08;现在CSDN改成”C知道“了&#xff09;。简介ChatGPT是OpenAI公司开发的一种大型语言模型。它是一种基于Transformer架构的深度学习模型&#xff0c;可以对语言进行建模和生成。它可以处理问答、对话生成、文本生成等多种任…

1.认识网络爬虫

1.认识网络爬虫网络爬虫爬虫的合法性HTTP协议请求与响应(重点)网络爬虫 爬虫的全名叫网络爬虫&#xff0c;简称爬虫。他还有其他的名字&#xff0c;比如网络机器人&#xff0c;网络蜘蛛等等。爬虫就好像一个探测机器&#xff0c;它的基本操作就是模拟人的行为去各个网站溜达&am…

TCP/IP协议

✏️作者&#xff1a;银河罐头 &#x1f4cb;系列专栏&#xff1a;JavaEE &#x1f332;“种一棵树最好的时间是十年前&#xff0c;其次是现在” 目录TCP/IP协议应用层协议自定义应用层协议DNS传输层协议端口号UDP协议UDP协议端格式TCP协议TCP协议段格式TCP工作机制确认应答(安…

单片机怎么实现真正的多线程?

所谓多线程都是模拟的&#xff0c;本质都是单线程&#xff0c;因为cpu同一时刻只能执行一段代码。模拟的多线程就是任务之间快速切换&#xff0c;看起来像同时执行的样子。据说最近有多核的单片机&#xff0c;不过成本应该会高很多。对于模拟的多线程&#xff0c;我知道的有两种…

html实现浪漫的爱情日记(附源码)

文章目录1.设计来源1.1 主界面1.2 遇见1.3 相熟1.4 相知1.5 相念2.效果和源码2.1 动态效果2.2 源代码2.3 代码结构源码下载更多爱情表白源码作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/details/129264757 html实现浪漫的爱情…

【C++】红黑树

文章目录红黑树的概念红黑树的性质特征红黑树结点的定义红黑树的插入操作情况1情况2情况3特殊情况代码实现红黑树的验证红黑树的删除红黑树和AVL树的比较红黑树的应用红黑树的概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但是每一个结点都增加一个存储位表示结点的颜…

把C#代码上传到NuGet,大佬竟是我自己!!!

背景 刚发表完一篇博客总结自己写标准化C#代码的心历路程&#xff0c;立马就产生一个问题&#xff0c;就是我写好标准化代码后&#xff0c;一直存放磁盘的话&#xff0c;随着年月增加&#xff0c;代码越来越多&#xff0c;项目和版本的管理就会成为一个令我十分头疼的难题&…

可路由计算引擎实现前置数据库

很多大机构都会有个中央数据仓库负责向应用提供数据服务。随着业务的发展&#xff0c;中央数据仓库的负载在持续增加。一方面&#xff0c;数仓是前端应用的数据后台&#xff0c;而前端应用不断增多&#xff0c;用户访问的并发数也不断增长。另一方面&#xff0c;数仓还要承担原…

ChatGPT在工业领域的用法

在工业数字化时代&#xff0c;我们需要怎么样的ChatGPT&#xff1f; 近日&#xff0c;ChatGPT热度高居不下&#xff0c;强大的人机交互能力令人咋舌&#xff0c;在国内更是掀起一股讨论热潮。一时间&#xff0c;这场由ChatGPT引起的科技飓风&#xff0c;使得全球最顶尖科技力量…

C++回顾(一)——从C到C++

前言 在学习了C语言的基础上&#xff0c;C到底和C有什么区别呢&#xff1f; 1.1 第一个C程序 #include <iostream>// 使用名为std的命名空间 using namespace std;int main() {// printf ("hello world\n");// cout 标准输出 往屏幕打印内容 相当于C语言的…

【AI作画】使用stable-diffusion-webui搭建AI作画平台

一、安装配置Anaconda 进入官网下载安装包https://www.anaconda.com/并安装&#xff0c;然后将Anaconda配置到环境变量中。 打开命令行&#xff0c;依次通过如下命令创建Python运行虚拟环境。 conda env create novelai python3.10.6E:\workspace\02_Python\novalai>conda…