【Linux】共享内存(shm)代码实现

news2024/12/25 9:24:33

文章目录

  • 共享内存介绍
    • 最快的IPC形式
    • 共享内存示意图
    • 共享内存数据结构
    • 共享内存函数
      • shmget函数
        • shmfig
      • shmat函数
        • 说明:
      • shmdt函数
      • shmctl函数
    • 共享内存的原理
      • 小结
    • 共享内存的特点
      • 生命周期
      • 共享内存的大小
      • 共享内存为什么快
      • 共享内存没有任何的保护机制即同步互斥
      • 扩展内容
  • 代码编写(重点)
    • ftok介绍
      • 原型及参数介绍
      • 为什么要用
      • 与shmid的区别
    • makefile书写
    • comm.cpp(重点)
      • 条件编译以及头文件
      • toHex函数
      • getkey函数
      • creatShm以及getShm
      • attachShm,detachShm和delShm
      • 定义类进行封装
    • server.cc
    • client.cc
  • 代码总结

共享内存介绍

最快的IPC形式

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到
内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据

共享内存示意图

在这里插入图片描述

共享内存数据结构

shmid_ds中的内容可以通过系统函数获得(后面会讲):

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

共享内存函数

shmget函数

功能:用来创建共享内存
原型:int shmget(key_t key, size_t size, int shmflg);
参数:

key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的

返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

shmfig

IPC_CREAT and IPC_EXCL
单独使用IPC_CREAT: 创建一个共享内存,如果共享内存不存在,就创建之,如果已经存在,获取已经存在的共享内存并返回
IPC_EXCL不能单独使用,一般都要配合IPC_CREAT
IPC_CREAT | IPC_EXCL: 创建一个共享内存,如果共享内存不存在,就创建之, 如果已经存在,则立马出错返回 – 如果创建成功,对应的shm,一定是最新的!

shmat函数

功能:将共享内存段连接到进程地址空间
原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:

shmid: 共享内存标识
shmaddr:指定连接的地址->设为nullptr即为随即指定
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:指向一个保存着共享内存的模式状态和访问权限的数据结构

返回值:成功返回0;失败返回-1

在这里插入图片描述

共享内存的原理

先创建出一块共享的物理内存,然后通过页表各自映射到两个进程。->让不同的进程看到同一份资源! 内存块->共享内存
(之前我们是通过看到同一个文件来进行通信的,这是通过看到同一个内存块进行通信的)
如何取消呢?:
进程AB分别修改页表,去掉映射关系,释放共享内存块即可

小结

  1. 创建
  2. 关联进程和取消关联
  3. 释放共享内存块

共享内存的特点

生命周期

共享内存的生命周期不随进程,而是随整个操作系统
我们只能通过指令删除或者使用系统调用接口进行删除

共享内存的大小

共享内存的大小是以PAGE页(4kb)为单位的
![[Pasted image 20230418185622.png]]
OS分给你8kb,但是我只能用4097->可以防止访问越界

共享内存为什么快

我们在通信的时候,没有使用任何的接口,一旦共享内存映射到进程的地址空间,该共享内存就直接被所有的进程直接看到了
因为共享内存的这种特性,可以让进程通信的时候,减少拷贝的次数,因此是最快的

共享内存没有任何的保护机制即同步互斥

因为管道是通过系统接口通信的(里面可能会有保护机制),而共享内存直接通信

扩展内容

试着改一下代码:(将在之后的博客中书写)

  1. 让我client写完后,才通知server读取.刚开始的时候一定先让client先运行 一个管道
  2. 将命名管道带进来
  3. client写完后,才通知server读取,读取完了,才让client进行写入 两个管道

代码编写(重点)

ftok介绍

原型及参数介绍

函数原型:key_t ftok(const char* pathname,int proj_id);
其中,pathname是路径字符串,proj_id是项目id
这两个参数都可以随便书写,没有任何意义,只不过要保证不同的shm空间,这两个参数一定是不完全相同的.

为什么要用

我们要保证每一个共享内存都有唯一的一个编号,且server端和client端都可以接收得到同一个key值来访问同一块共享内存(即实现我们所谓的"不同的进程看到同一份资源"),这个编号便可由ftok函数产生.

与shmid的区别

在我们使用shmget成功后,会返回一个标识码,我们把它称之为"shmid",那么shmid不也是唯一的吗?和我们的key有什么区别呢?

其实他们俩的区别和inode编号与文件fd的区别差不多
对于shm的未来的所有操作,在用户层阶段,都是使用的 shmid
只有在系统内核,才是key值–>key本质是内核中使用的
所以我们一般用户只需要关注shmid即可

makefile书写

我们这里需要写两个文件:client.ccserver.cc
所以需要用all来依赖这两个文件,到时候只需要make all便可以同时完成两个文件的编译

.PHONY:all
all:shmclient shmserver

shmclient:client.cc
	g++ -o $@ $^ -std=c++11
shmserver:server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f shmserver shmclient
//make all/make 都完成编译(make默认的是代码的开头)
//make clean会删除这两个文件

comm.cpp(重点)

条件编译以及头文件

#ifndef __COMM_HPP__ //如果没有定义这个宏,则才执行以下的编译,防止重复编译
#define __COMM_HPP__
#include <iostream>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <cassert>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>

#define PATHNAME "."//当前路径
#define PROJID 0x6666//随便写的一个数

using namespace std;

const int gsize = 4096; //暂时

//函数调用接口,接下来一个个地写

#endif

toHex函数

为了方便调试,我们写一个函数将十进制数字才换成十六进制:

string toHex(int x)
{
	char buffer[64];
	snprintf(buffer,sizeof(buffer),"0x%x",x);
	//snprintf函数可以将字符串格式化输出到buffer中
	//格式为0x
}

getkey函数

上文我们提到过,需要使用ftok函数来得到一个唯一的key值,作为shmget的参数

key_t getKey()
{
	key_t k = ftok(PATHNAME,PROJID);
	if(k == -1)//差错判断
	{
		cerr << "error: " << errno << ":" << strerror(errno) << endl;
		//errno会存储一个错误编号,可以痛过strerror解码出来
		exit(1);
	}
	return k;
}

creatShm以及getShm

//使用这个辅助函数的话,我们就不需要为client和server分别书写一份相同的代码了
//只需要让他们俩传入不同的flag即可
static int createShmHelper(key_t k,int size,int flag)
{
	int shmid = shmget(k,size,flag);
	if(shmid == -1)
	{
		cerr << "error: " << errno << ":" << strerror(errno) << endl;
		exit(2);
	}
	return shmid;
}

int creatShm(key_t k,int size)
{
	umask(0)//将该进程的掩码设置为0
	return createShmHelper(k,size,IPC_CREAT | IPC_EXCL | 0666);
}
int getShm(key_t k,int size)
{
	return createShmHelper(k,size,IPC_CREAT);
}

attachShm,detachShm和delShm

我们已经创建出了共享内存这块空间了,需要我们将它写入我们的链表中,即attach

char* attachShm(int shmid)
{
	char* start = (char*)shmat(shmid,nullptr,0);
	return start;
}
void detachShm(char* start)
{
	int n = shmdt(start);
	assert(n!=-1);
	(void)n;
}
void delShm(int shmid)//使用shmctl接口
{
    int n = shmctl(shmid, IPC_RMID, nullptr);
    assert(n != -1);
    (void)n;
}

定义类进行封装

#define SERVER 1
#define CLIENT 0

class Init
{
public:
	Init(int t):type(t)
	{
		key_k k = get_key();
		if(type == SERVER)
		{
			shmid = createShm(k,gsize);
		}
		else
		{
			shmid = getShm(k,gsize);
		}
		start = attach(shmid);
	}
	char *getStart(){ return start; }
    ~Init()
    {
        detachShm(start);
        if(type == SERVER) delShm(shmid);
    }
	
private:
	char* start;
	int type;//server or client
	int shmid;
};

server.cc

#include "comm.hpp"
#include <unistd.h>

int main()
{
    Init init(SERVER);
    char *start = init.getStart();

    int n = 0;
    while(n <= 30)
    {
        cout <<"client -> server# "<< start << endl;
        sleep(1);
        n++;
    }
    return 0;
}

client.cc

#include "comm.hpp"
#include <unistd.h>

int main()
{
    Init init(CLIENT);
    char *start = init.getStart();
    char c = 'A';
    
    while(c <= 'Z')
    {
        start[c-'A'] = c;
        c++;
        start[c] = '\0';
        sleep(1);
    }
    return 0;
}

代码总结

makefile

.PHONY:all
all:shmclient shmserver

shmclient:client.cc
	g++ -o $@ $^ -std=c++11
shmserver:server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f shmserver shmclient

comm.cpp

#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <iostream>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <cassert>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>

using namespace std;

//  IPC_CREAT and IPC_EXCL
// 单独使用IPC_CREAT: 创建一个共享内存,如果共享内存不存在,就创建之,如果已经存在,获取已经存在的共享内存并返回
// IPC_EXCL不能单独使用,一般都要配合IPC_CREAT
// IPC_CREAT | IPC_EXCL: 创建一个共享内存,如果共享内存不存在,就创建之, 如果已经存在,则立马出错返回 -- 如果创建成功,对应的shm,一定是最新的!

#define PATHNAME "."
#define PROJID 0x6666

// 共享内存的大小是以PAGE页(4KB)为单位的
const int gsize = 4096; //暂时

key_t getKey()
{
    key_t k = ftok(PATHNAME, PROJID);
    if(k == -1)
    {
        cerr << "error: " << errno << " : " << strerror(errno) << endl;
        exit(1);
    }
    return k;
}

string toHex(int x)
{
    char buffer[64];
    snprintf(buffer, sizeof buffer, "0x%x", x);
    return buffer;
}

static int createShmHelper(key_t k, int size, int flag)
{
    int shmid = shmget(k, gsize, flag);
    if(shmid == -1)
    {
        cerr << "error: " << errno << " : " << strerror(errno) << endl;
        exit(2);
    }
    return shmid;
}

int createShm(key_t k, int size)
{
    umask(0);
    return createShmHelper(k, size, IPC_CREAT | IPC_EXCL | 0666);
}

int getShm(key_t k, int size)
{
    return createShmHelper(k, size, IPC_CREAT);
}

char* attachShm(int shmid)
{
    char *start = (char*)shmat(shmid, nullptr, 0);
    return start;
}

void detachShm(char *start)
{
    int n = shmdt(start);
    assert(n != -1);
    (void)n;
}

void delShm(int shmid)
{
    int n = shmctl(shmid, IPC_RMID, nullptr);
    assert(n != -1);
    (void)n;
}

#define SERVER 1
#define CLIENT 0

class Init
{
public:
    Init(int t):type(t)
    {
        key_t k = getKey();
        if(type == SERVER) shmid = createShm(k, gsize);
        else shmid = getShm(k, gsize);
        start = attachShm(shmid);
    }
    char *getStart(){ return start; }
    ~Init()
    {
        detachShm(start);
        if(type == SERVER) delShm(shmid);
    }
private:
    char *start;
    int type; //server or client
    int shmid;
};


#endif

client.cc

#include "comm.hpp"
#include <unistd.h>

int main()
{
    Init init(CLIENT);
    // start 就已经执行了共享内存的起始空间
    char *start = init.getStart();
    char c = 'A';

    while(c <= 'Z')
    {
        start[c - 'A'] = c;
        c++;
        start[c - 'A'] = '\0';
        sleep(1);
    }


    // key_t k = getKey();
    // cout << "client key: " << toHex(k) << endl;

    // int shmid = getShm(k, gsize);
    // cout << "client shmid: " << shmid << endl;

    // //3. 将自己和共享内存关联起来
    // char* start = attachShm(shmid);

    // sleep(15);
    
    // // 4.  将自己和共享内存去关联
    // detachShm(start);


    return 0;
}

server.cc

#include "comm.hpp"
#include <unistd.h>

int main()
{
    Init init(SERVER);

    // start 就已经执行了共享内存的起始空间
    char *start = init.getStart();

    int n = 0;
    // 我们在通信的时候,没有使用任何接口?一旦共享内存映射到进程的地址空间,该共享内存就直接被所有的进程 直接看到了!
    // 因为共享内存的这种特性,可以让进程通信的时候,减少拷贝次数,所以共享内存是所有进程间通信,速度最快的
    // 共享内存没有任何的保护机制(同步互斥) -- 为什么?管道通过系统接口通信,共享内存直接通信
    while(n <= 30)
    {
        cout <<"client -> server# "<< start << endl;
        sleep(1);
        n++;
    }

    // 扩展内容:
    // 1. client写完了,才通知让server读取。刚开始,一定先让client运行 一个管道
    // 2. 命名管道带进来 
    // 3. client写完了,才通知让server读取.读取完了,才让client进行写入 两个管道


    // //1. 创建key
    // key_t k = getKey();
    // cout << "server key: " << toHex(k) << endl;

    // //2. 创建共享内存
    // int shmid = createShm(k, gsize);
    // cout << "server shmid: " << shmid << endl;

    // sleep(3);

    // //3. 将自己和共享内存关联起来
    // char* start = attachShm(shmid);

    // sleep(20);

    // // 通信代码在这里!

    // // 4.  将自己和共享内存去关联
    // detachShm(start);

    // sleep(3);

    // struct shmid_ds ds;
    // int n = shmctl(shmid, IPC_STAT, &ds);
    // if(n != -1)
    // {
    //     cout << "perm: " << toHex(ds.shm_perm.__key) << endl;
    //     cout << "creater pid: " << ds.shm_cpid  << " : " << getpid() << endl;
    // }

    // ?. 删除共享内存
    //delShm(shmid);
    return 0;
}

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

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

相关文章

《软件开发本质论》读书笔记

目录 第一部分——价值的循环开发取向频繁交付小的&#xff0c;价值大&#xff0c;代价小的特性 测试同时要有业务测试和开发测试开发测试应在代码开发的同时完成甚至先于代码完成 重构第一部分总结 第二部分——说明与论述团队目的自主专精 五卡法预测软件计划控制好自己所参与…

二叉树一定是完全二叉树

一、树的概念及其结构 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。 1、树的特点 ①有一个特殊的结点&…

ARM Coresight 及 DS-5 介绍 5 - ARM Cortex-M DS-5 加载 ELF 文件运行

文章目录 1.1.1 DS-5 工程创建1.1.2 DS-5 加载 ELF 脚本创建1.1.3 DS-5 脚本读写 Memory1.1.4 DS-5 扫描脚本 1.1.1 DS-5 工程创建 在使用ARM DS-5 连接 board(或者PFGA)之前首先需要能够扫描到相应的硬件信息&#xff0c;比如对应的cpu的相关信息&#xff1a;coresight 相关组…

【Zblog搭建博客网站】windows环境搭建属于自己的博客并发布上线 - cpolar内网穿透

文章目录 1. 前言2. Z-blog网站搭建2.1 XAMPP环境设置2.2 Z-blog安装2.3 Z-blog网页测试2.4 Cpolar安装和注册 3. 本地网页发布3.1. Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 转载自远程内网穿透的文章&#xff1a;【Zblog建站】搭建属于自己的博客网站&#…

【C++】初始化列表,explicit 关键字,静态成员

初始化列表 构造函数内可以对于成员的赋值&#xff0c;故不能称为是初始化&#xff0c;初始化只能初始化依次&#xff0c;C中类对象的初始化可由初始化列表完成 内置类型成员可以通过设置缺省值实现初始化&#xff0c;但对于const修饰的成员、引用成员、没有默认构造函数的自…

hive解析json

目录 一、背景 二、hive 解析 json 数据函数 1、get_json_object 2、json_tuple 3、使用嵌套子查询(exploderegexp_replacesplitjson_tuple)解析json数组 4、使用 lateral view 解析json数组 一、背景 我们进行ETL(Extract-Transfer-Load) 过程中,经常会遇到从不同数据…

编码拓展:链接库

一.认识链接库 1.1库 计算机中&#xff0c;有些文件专门用于存储可以重复使用的代码块&#xff0c;例如功能实用的函数或者类&#xff0c;我们通常将它们称为库文件&#xff0c;简称“库”&#xff08;Library&#xff09;。 以 C 语言为例&#xff0c;如下为大家展示的就是…

JUC高级十-并发加锁原理之AbstractQueuedSynchronizer(AQS)

1. 前置知识 公平锁和非公平锁可重入锁自旋锁LockSupport数据结构之双向链表设计模式之模板设计模式 AQS重要性 JAVA ------>JVM AQS ------>AQS 2. AQS入门级别理论知识 2.1 是什么? 2.1.1 字面意思 Abstract Queued Synchronizer----抽象的队列同步器 源码位置: …

【Microsoft Edge】如何彻底卸载 Edge

文章目录 一、问题描述二、卸载 Edge2.1 卸载正式版 Edge2.2 卸载非正式版 Edge2.2.1 卸载通用的 WebView22.2.2 卸载 Canary 版 Edge2.2.3 卸载其他版本2.3 卸载 Edge Update 2.4 卸载 Edge 的 Appx 额外安装残留2.5 删除日志文件2.6 我就是想全把 Edge 都删了 一、问题描述 …

预测模型的局部评价?

预测模型的局部评价 为什么要进行局部评价&#xff1f; 首先是临床决策曲线分析通常会给预测模型的使用规定一个阈值范围&#xff0c;相应地预测模型的评价也应该局限在这个范围之内才是合理的&#xff1b; 其次&#xff0c;全局性地评价往往不够敏感&#xff0c;即好的模型和坏…

本地搭建属于自己的ChatGPT:基于Python+ChatGLM-6b+Streamlit+QDrant+DuckDuckGo

本地部署chatglm及缓解时效性问题的思路&#xff1a; 模型使用chatglm-6b 4bit&#xff0c;推理使用hugging face&#xff0c;前端应用使用streamlit或者gradio。 微调对显存要求较高&#xff0c;还没试验。可以结合LoRA进行微调。 缓解时效性问题&#xff1a;通过本地数据库…

C语言实现银行家算法

一.银行家算法 1.由来 银行家算法最初是由荷兰计算机科学家艾兹赫尔迪杰斯特拉&#xff08;Edsger W. Dijkstra&#xff09;于1965年提出的。当时他正致力于解决多道程序设计中产生的死锁问题。在多道程序设计中&#xff0c;由于不同进程之间共享有限的系统资源&#xff0c;如…

【JavaEE初阶】多线程(一)认识线程 线程的创建 Thread的用法

摄影分享&#xff01; 文章目录 认识线程&#xff08;Thread&#xff09;概念执行多线程编程创建线程的写法1.继承Thread&#xff0c;重写run2.实现Runnable接口3.使用匿名内部类&#xff0c;继承Thread4.使用匿名内部类&#xff0c;实现Runable5.使用Lambda表达式 Thread用法…

C语言模拟银行排队叫号(链队)

一.队列 队列是一种具有先进先出&#xff08;FIFO&#xff09;特性的线性数据结构&#xff0c;它只允许在队列的两端进行插入和删除操作。队列的一端称为队尾&#xff08;rear&#xff09;&#xff0c;另一端称为队头&#xff08;front&#xff09;。新元素总是插入在队列的队…

怎么把m4a转换成mp3,分享几个方法给大家!

录音文件中经常出现m4a后缀的音频格式&#xff0c;但通常只能在特定的音频播放器中播放。如果你想把m4a转换成mp3&#xff0c;下面是四种简单易行的方法&#xff0c;适用于Windows 10操作系统。 方法一&#xff1a;使用记灵在线工具转换m4a成mp3 工具地址&#xff1a;记灵在线…

elsticsearch入门

查看所有索引&#xff08;表&#xff09; 向索引&#xff08;表&#xff09;中添加数据&#xff1a; 自定义id添加数据&#xff1a; 自定义id添加数据&#xff1a;方式二 查询数据&#xff1a; 查询索引&#xff08;表&#xff09;中全部数据&#xff1a; 全量修改单条数据&…

File类与IO流

1. java.io.File类的使用 1.1 概述 File类及本章下的各种流&#xff0c;都定义在java.io包下。一个File对象代表硬盘或网络中可能存在的一个文件或者文件目录&#xff08;俗称文件夹&#xff09;&#xff0c;与平台无关。&#xff08;体会万事万物皆对象&#xff09;File 能新…

【Leetcode -剑指Offer 22.链表中倒数第k个结点 -203.移除链表元素】

Leetcode Leetcode -剑指Offer 22.链表中倒数第k个结点Leetcode -203.移除链表元素 Leetcode -剑指Offer 22.链表中倒数第k个结点 题目&#xff1a;输入一个链表&#xff0c;输出该链表中倒数第k个节点。为了符合大多数人的习惯&#xff0c;本题从1开始计数&#xff0c;即链表…

数据结构(三)—— 哈希表

文章目录 一、哈希表积累1.1 哈希map1.2 哈希set 二、哈希表基础三、题3.1 242 有效的字母异位词3.2 349 两个数组的交集3.3 202 快乐数3.4 1 两数之和3.5 54 四数相加II 一、哈希表积累 什么时候想到用哈希法&#xff1a;当要需要查询一个元素是否出现过、判断一个元素是否出…

awvs安装批量扫描

文章目录 安装批量扫描 安装 1.2.AWVS下载   该工具可在官方网站下载&#xff0c;但免费下载的是14天试用版本。   官网下载&#xff1a;AWVS   百度云下载&#xff1a;百度云下载https://pan.baidu.com/s/1UO7GzL0CMemJ_TMQnHNOuA?pwdg1bm 提取码&#xff1a;g1bm 批…