用户态缓存:环形缓冲区(Ring Buffer)

news2024/9/21 9:00:08

目录

环形缓冲区(Ring Buffer)简介

为什么选择环形缓冲区?

代码解析

1. 头文件与类型定义

1.1 头文件保护符

1.2 包含必要的标准库

1.3 类型定义

2. 环形缓冲区结构体

2.1 结构体成员解释

3. 辅助宏与内联函数

3.1 min 宏

3.2 is_power_of_two 内联函数

3.3 roundup_power_of_two 内联函数

4. 环形缓冲区的基本操作

4.1 创建新缓冲区

4.2 获取缓冲区长度

4.3 释放缓冲区

4.4 添加数据到缓冲区

4.5 从缓冲区移除数据

4.6 清空缓冲区的一部分数据

4.7 在缓冲区中搜索特定字符串

4.8 获取写入缓冲区的可写指针

5. 其他辅助函数

5.1 判断缓冲区是否为空

5.2 判断缓冲区是否已满

5.3 获取缓冲区中剩余的空间

6. 代码中的关键概念与实现

6.1 环形地址计算

6.2 缓冲区大小为2的幂次方

6.3 双指针机制

7. 综合应用

7.1 在用户态缓存区中的应用

7.2 处理生产者与消费者速度不匹配

7.3 结合之前的内容

8. 总结


环形缓冲区(Ring Buffer)简介

环形缓冲区是一种高效的数据结构,广泛应用于生产者-消费者模型中。在网络通信中,尤其是用户态缓存区中,环形缓冲区通过循环使用固定大小的内存区域,减少数据移动和内存管理开销,提升数据传输效率。

为什么选择环形缓冲区?

  • 减少数据移动:数据在缓冲区中循环写入和读取,避免了频繁的数据拷贝操作。
  • 高效缓存管理:适用于高并发场景,能够快速响应数据的读写请求。
  • 简化内存管理:固定大小的缓冲区结构简化了内存分配和释放。

代码解析

让我们逐步解析环形缓冲区代码,理解其各个部分的功能和实现细节。

1. 头文件与类型定义

#ifndef _ringbuffer_h
#define _ringbuffer_h

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// #include <limits.h>  // for uint_max
#include <stdint.h>
#include <unistd.h>

typedef struct ringbuffer_s buffer_t;

buffer_t * buffer_new(uint32_t sz);

uint32_t buffer_len(buffer_t *r);

void buffer_free(buffer_t *r);

int buffer_add(buffer_t *r, const void *data, uint32_t sz);

int buffer_remove(buffer_t *r, void *data, uint32_t sz);

int buffer_drain(buffer_t *r, uint32_t sz);

int buffer_search(buffer_t *r, const char* sep, const int seplen);

uint8_t * buffer_write_atmost(buffer_t *r);

#endif
1.1 头文件保护符
#ifndef _ringbuffer_h
#define _ringbuffer_h
...
#endif
  • 作用:防止头文件被多次包含,避免重复定义错误。
1.2 包含必要的标准库
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// #include <limits.h>  // for uint_max
#include <stdint.h>
#include <unistd.h>
  • 标准库:提供了内存管理(mallocfree)、字符串操作(memcpy)、固定宽度整数类型(uint32_tuint8_t)等功能。
1.3 类型定义
typedef struct ringbuffer_s buffer_t;
  • 作用:为 struct ringbuffer_s 定义一个别名 buffer_t,简化后续代码的书写。

2. 环形缓冲区结构体

struct ringbuffer_s {
    uint32_t size;
    uint32_t tail;
    uint32_t head;
    uint8_t * buf;
};
2.1 结构体成员解释
  • size (uint32_t):缓冲区的总大小,以字节为单位。通常为2的幂次方,便于使用位运算实现环形效果。

  • tail (uint32_t):指向缓冲区的写入位置。每次添加数据时,tail 会递增。

  • head (uint32_t):指向缓冲区的读取位置。每次移除数据时,head 会递增。

  • buf (uint8_t *):指向实际数据存储区的指针。数据以字节形式存储。

3. 辅助宏与内联函数

#define min(lth, rth) ((lth)<(rth)?(lth):(rth))

static inline int is_power_of_two(uint32_t num) {
    if (num < 2) return 0;
    return (num & (num - 1)) == 0;
}

static inline uint32_t roundup_power_of_two(uint32_t num) {
    if (num == 0) return 2;
    int i = 0;
    for (; num != 0; i++)
        num >>= 1;
    return 1U << i;
}
3.1 min
#define min(lth, rth) ((lth)<(rth)?(lth):(rth))
  • 作用:返回两个值中的较小者,简化代码中的条件判断。
3.2 is_power_of_two 内联函数
static inline int is_power_of_two(uint32_t num) {
    if (num < 2) return 0;
    return (num & (num - 1)) == 0;
}
  • 作用:判断一个数是否为2的幂次方。
  • 原理:2的幂次方数在二进制中只有一个1,其减1后所有位都会变为1,按位与结果为0。
3.3 roundup_power_of_two 内联函数
static inline uint32_t roundup_power_of_two(uint32_t num) {
    if (num == 0) return 2;
    int i = 0;
    for (; num != 0; i++)
        num >>= 1;
    return 1U << i;
}
  • 作用:将一个数向上舍入到最近的2的幂次方。
  • 实现:通过不断右移操作,计算出需要的位数,然后使用位移生成对应的2的幂次方数。

4. 环形缓冲区的基本操作

4.1 创建新缓冲区
buffer_t * buffer_new(uint32_t sz) {
    if (!is_power_of_two(sz)) sz = roundup_power_of_two(sz);
    buffer_t * buf = (buffer_t *)malloc(sizeof(buffer_t) + sz);
    if (!buf) {
        return NULL;
    }
    buf->size = sz;
    buf->head = buf->tail = 0;
    buf->buf = (uint8_t *)(buf + 1);
    return buf;
}
  • 功能:分配并初始化一个新的环形缓冲区。
  • 步骤
    1. 检查传入的大小 sz 是否为2的幂次方。如果不是,则向上舍入到最近的2的幂次方数。
    2. 分配内存:sizeof(buffer_t) + szbuffer_t结构体和实际数据存储区 buf 一起分配。
    3. 初始化结构体成员:
      • size:设置为调整后的大小 sz
      • headtail:初始化为0,表示缓冲区为空。
      • buf:指向结构体之后的内存区域,即实际的数据存储区。
4.2 获取缓冲区长度
uint32_t buffer_len(buffer_t *r) {
    return rb_len(r);
}

static uint32_t
rb_len(buffer_t *r) {
    return r->tail - r->head;
}
  • 功能:返回缓冲区中当前存储的数据长度。
  • 实现:通过 tail - head 计算当前缓冲区中有效数据的字节数。
4.3 释放缓冲区
void buffer_free(buffer_t *r) {
    free(r);
    r = NULL;
}
  • 功能:释放之前分配的缓冲区内存。
  • 注意:设置指针为 NULL 只是为了防止悬挂指针,但在函数外部无效。
4.4 添加数据到缓冲区
int buffer_add(buffer_t *r, const void *data, uint32_t sz) {
    if (sz > rb_remain(r)) {
        return -1;
    }
    uint32_t i;
    i = min(sz, r->size - (r->tail & (r->size - 1)));

    memcpy(r->buf + (r->tail & (r->size - 1)), data, i);
    memcpy(r->buf, data+i, sz-i);

    r->tail += sz;
    return 0;
}

static uint32_t
rb_remain(buffer_t *r) {
    return r->size - r->tail + r->head;
}
  • 功能:将数据添加到缓冲区中。
  • 步骤
    1. 检查剩余空间
      • 使用 rb_remain(r) 计算缓冲区中剩余的可用空间。
      • 如果要添加的数据大小 sz 超过剩余空间,返回错误 -1
    2. 计算可写入的字节数 i
      • 使用 min(sz, r->size - (r->tail & (r->size - 1))) 计算可以连续写入的最大字节数,避免跨越缓冲区末尾。
    3. 数据拷贝
      • 第一部分:将前 i 字节的数据拷贝到缓冲区当前位置。
      • 第二部分:如果有剩余的数据(sz - i),将其从缓冲区的起始位置开始拷贝,形成环形。
    4. 更新 tail:将 tail 指针增加 sz,标记新数据的结束位置。
    5. 返回成功:返回 0 表示数据添加成功。
4.5 从缓冲区移除数据
int buffer_remove(buffer_t *r, void *data, uint32_t sz) {
    assert(!rb_isempty(r));
    uint32_t i;
    sz = min(sz, r->tail - r->head);

    i = min(sz, r->size - (r->head & (r->size - 1)));
    memcpy(data, r->buf+(r->head & (r->size - 1)), i);
    memcpy(data+i, r->buf, sz-i);

    r->head += sz;
    return sz;
}

static uint32_t
rb_isempty(buffer_t *r) {
    return r->head == r->tail;
}
  • 功能:从缓冲区中移除并读取数据。
  • 步骤
    1. 断言缓冲区不为空:使用 assert(!rb_isempty(r)) 确保缓冲区中有数据可读。
    2. 调整读取大小:将 sz 限制为缓冲区中实际存储的数据量 r->tail - r->head
    3. 计算可连续读取的字节数 i
      • 使用 min(sz, r->size - (r->head & (r->size - 1))) 计算可以连续读取的最大字节数,避免跨越缓冲区末尾。
    4. 数据拷贝
      • 第一部分:将前 i 字节的数据从缓冲区当前位置拷贝到目标缓冲区。
      • 第二部分:如果有剩余的数据(sz - i),将其从缓冲区的起始位置开始拷贝。
    5. 更新 head:将 head 指针增加 sz,标记数据的读取位置。
    6. 返回读取的字节数:返回实际读取的数据大小 sz
4.6 清空缓冲区的一部分数据
int buffer_drain(buffer_t *r, uint32_t sz) {
    if (sz > rb_len(r))
        sz = rb_len(r);
    r->head += sz;
    return sz;
}
  • 功能:从缓冲区中清除 sz 字节的数据,而不读取到用户空间。
  • 步骤
    1. 调整清除大小:将 sz 限制为缓冲区中实际存储的数据量 rb_len(r)
    2. 更新 head:将 head 指针增加 sz,标记数据的清除位置。
    3. 返回清除的字节数:返回实际清除的数据大小 sz
4.7 在缓冲区中搜索特定字符串
int buffer_search(buffer_t *r, const char* sep, const int seplen) {
    int i;
    for (i = 0; i <= rb_len(r)-seplen; i++) {
        int pos = (r->head + i) & (r->size - 1);
        if (pos + seplen > r->size) {
            if (memcmp(r->buf+pos, sep, r->size-pos))
                return 0;
            if (memcmp(r->buf, sep+r->size-pos, pos+seplen-r->size) == 0) {
                return i+seplen;
            }
        }
        if (memcmp(r->buf+pos, sep, seplen) == 0) {
            return i+seplen;
        }
    }
    return 0;
}
  • 功能:在缓冲区中搜索特定的分隔符 sep,用于界定数据包的边界(如协议解析)。
  • 步骤
    1. 遍历缓冲区:从 head 开始,逐个字节检查是否匹配 sep
    2. 计算当前检查的位置 pos:使用 (r->head + i) & (r->size - 1) 实现环形地址计算。
    3. 处理跨越缓冲区末尾的情况
      • 如果 pos + seplen > r->size,表示分隔符跨越缓冲区末尾,需要分两部分比较。
      • 第一部分:比较从 pos 到缓冲区末尾的部分。
      • 第二部分:比较从缓冲区起始位置到剩余长度的部分。
    4. 匹配成功:如果找到匹配的分隔符,返回分隔符的位置(偏移量 i + seplen)。
    5. 未找到匹配:返回 0,表示未找到。
4.8 获取写入缓冲区的可写指针
uint8_t * buffer_write_atmost(buffer_t *r) {
    uint32_t rpos = r->head & (r->size - 1);
    uint32_t wpos = r->tail & (r->size - 1);
    if (wpos < rpos) {
        uint8_t* temp = (uint8_t *)malloc(r->size * sizeof(uint8_t));
        memcpy(temp, r->buf+rpos, r->size - rpos);
        memcpy(temp+r->size-rpos, r->buf, wpos);
        free(r->buf);
        r->buf = temp;
        return r->buf;
    }
    return r->buf + rpos;
}
  • 功能:获取当前缓冲区中可写入数据的位置指针,最多可写入的字节数。
  • 步骤
    1. 计算读取和写入位置
      • rposhead 指针在缓冲区中的当前位置。
      • wpostail 指针在缓冲区中的当前位置。
    2. 判断是否需要重新排列缓冲区
      • 如果 wpos < rpos,表示写入位置已环绕到缓冲区的起始位置,需要将数据重新排列,使得写入位置连续。
      • 重新排列
        • 分配新的缓冲区 temp,大小与原缓冲区相同。
        • 将从 rpos 到缓冲区末尾的数据复制到 temp 的起始位置。
        • 将从缓冲区起始位置到 wpos 的数据复制到 temp 的剩余位置。
        • 释放原缓冲区内存,并将 buf 指针指向新的缓冲区 temp
        • 返回新的缓冲区起始位置。
    3. 直接返回写入位置
      • 如果不需要重新排列,直接返回 buf + rpos,即当前可写入的位置。

5. 其他辅助函数

5.1 判断缓冲区是否为空
static uint32_t
rb_isempty(buffer_t *r) {
    return r->head == r->tail;
}
  • 功能:检查缓冲区是否为空。
  • 返回值1 表示为空,0 表示不为空。
5.2 判断缓冲区是否已满
static uint32_t
rb_isfull(buffer_t *r) {
    return r->size == (r->tail - r->head);
}
  • 功能:检查缓冲区是否已满。
  • 返回值1 表示已满,0 表示未满。
5.3 获取缓冲区中剩余的空间
static uint32_t
rb_remain(buffer_t *r) {
    return r->size - r->tail + r->head;
}
  • 功能:计算缓冲区中剩余的可用空间。
  • 返回值:剩余空间的字节数。

6. 代码中的关键概念与实现

6.1 环形地址计算

在环形缓冲区中,headtail 指针是以字节为单位递增的。当指针超过缓冲区的大小时,通过位运算(& (r->size - 1))将其映射回缓冲区的起始位置,实现环形效果。

(r->tail & (r->size - 1))
  • 条件r->size 通常为2的幂次方,这样 (r->size - 1) 就是一个全1的二进制数,可以用来快速计算模运算。
6.2 缓冲区大小为2的幂次方

为了简化环形地址的计算,缓冲区的大小通常设置为2的幂次方。这不仅提高了效率,还使得位运算成为可能,从而加快了数据的读写操作。

6.3 双指针机制
  • head:指向下一个读取位置。
  • tail:指向下一个写入位置。
  • 优势:通过维护两个指针,可以高效地管理生产者(写入)和消费者(读取)之间的数据流动,避免数据冲突和竞争条件。

7. 综合应用

7.1 在用户态缓存区中的应用

在用户态缓存区中,环形缓冲区用于存储和管理网络数据的读写操作。生产者(如内核协议栈)将数据添加到缓冲区,消费者(如应用程序)从缓冲区读取数据。通过环形缓冲区的高效管理,确保数据传输的流畅性和可靠性。

7.2 处理生产者与消费者速度不匹配

当生产者(如内核协议栈)生成数据的速度快于消费者(应用程序)的处理速度时,缓冲区可以暂存这些数据,避免数据丢失。同样,当消费者处理数据的速度快于生产者生成数据的速度时,缓冲区也能有效地管理数据流动,确保数据的连续性。

7.3 结合之前的内容

用户态缓存:高效数据交互与性能优化icon-default.png?t=O83Ahttps://blog.csdn.net/weixin_43925427/article/details/142354725?fromshare=blogdetail&sharetype=blogdetail&sharerId=142354725&sharerefer=PC&sharesource=weixin_43925427&sharefrom=from_link在之前的讲解中,我们提到了读写缓存区在网络通信中的重要性,以及不同的缓冲区设计(固定内存块、环形缓冲区、链式缓冲区)对性能和效率的影响。环形缓冲区通过减少数据移动和优化内存管理,提升了数据传输的效率和系统的整体性能。

8. 总结

通过详细解析这段环形缓冲区的代码,我们深入理解了环形缓冲区的结构和工作原理:

  • 高效的数据管理:通过固定大小的缓冲区和双指针机制,环形缓冲区实现了高效的数据读写操作。
  • 减少数据移动:利用环形地址计算和分段拷贝,避免了大量的数据拷贝和移动操作,提升了性能。
  • 灵活的空间管理:通过动态调整和优化(如 buffer_write_atmost 函数),环形缓冲区能够适应不同的数据量需求,保持高效运行。
  • 可靠的数据传输:在生产者和消费者速度不匹配的情况下,环形缓冲区通过暂存和管理数据,确保数据的完整性和可靠性。

参考:

0voice · GitHub

GitHub - TryTryTL/buffer_design

用户态缓存:高效数据交互与性能优化-CSDN博客

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

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

相关文章

【Python报错已解决】xlrd.biffh.XLRDError: Excel xlsx file; not supported

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

最新LinPay码支付 免签支付系统源码 免授权版本(含搭建教程)

最新LinPay码支付 免签支付系统源码 免授权版本 服务集成商兼容市面所有易支付&#xff0c;兼容所有商城LinPay是专为个人站长打造的聚合免签系统&#xff0c;拥有卓越的性能和丰富的功能。它采用全新轻量化的界面UI&#xff0c;让您能更方便快捷地解决知识付费和运营赞助的难…

中间件知识点-消息中间件(Rabbitmq)一

消息中间件介绍 MQ的作用(优点)主要有以下三个方面&#xff1a; a.异步 b.解耦 c.削峰 MQ的作用(缺点)主要有以下三个方面&#xff1a; a.系统可用性降低 b.系统复杂度提高 c.存在消息一致性问题需要解决 备注&#xff1a; 引入MQ后系统的复杂度会大大提高。 以前服务之间可以…

移动开发(三):使用.NET MAUI打包第一个安卓APK完整过程

目录 一、修改AndroidManifest.xml 配置APP基本信息权限 二、修改项目属性调整输出Android包格式为APK 三、项目发布 四、APP分发 五、总结 之前给大家介绍过使用使用.NET MAUI开发第一个安卓APP,今天给大家介绍如何打包成APK,然后安装到安卓手机正常运行。这里还是沿用…

如何下载ComfyUI开发版

看B站视频&#xff0c;见用绘世可以下载ComfyUI开发版&#xff0c;而我又不想在电脑里放太多东西&#xff0c;于是研究了一下&#xff0c;如何直接从GitHub网站下载。具体步骤看图示。 看压缩包内容&#xff0c;应该直接解压覆盖就可以了&#xff0c;暂未有时间测试。

【JS】ESMoudle机制与符号绑定

前言 JS 模块化有两种方式&#xff0c;分别为&#xff1a;CommonJS 和 ESModule。与 CommonJS 不同&#xff0c;ESModule 是静态模块系统&#xff0c;意味着在代码编译阶段&#xff08;而不是运行时&#xff09;&#xff0c;模块依赖关系就已经被确定。 ESModule 优势 更好地…

传知代码-多示例AI模型实现病理图像分类

代码以及视频讲解 本文所涉及所有资源均在传知代码平台可获取 概述 本文将基于多示例深度学习EPLA模型实现对乳腺癌数据集BreaKHis_v1的分类。EPLA模型是处理组织病理学图像的经典之作。EPLA模型是基于多示例学习来进行了&#xff0c;那么多示例学习模型对处理病理学图像具有…

VCNet论文阅读笔记

VCNet论文阅读笔记 0、基本信息 信息细节英文题目VCNet and Functional Targeted Regularization For Learning Causal Effects of Continuous Treatments翻译VCNet和功能目标正则化用于学习连续处理的因果效应单位芝加哥大学年份2021论文链接[2103.07861] VCNet和功能定向正…

java数据结构----树

二叉查找树 二叉查找树的API设计 put方法的实现思想: public class BinaryTree<Key extends Comparable<Key>, Value> {private Node root;private int N;public int size(){return N;}public void put(Key key, Value value){root put(root,key,value);}public …

k8s 中的 Ingress 简介

一、关于 Ingress Ingress 是 K8s 中的一个 API 对象&#xff0c;用于管理和配置外部对集群内服务的访问。它可定义 HTTP 和 HTTPS 路由规则&#xff0c;将请求从集群外部的负载均衡器引导到相应的服务。Ingress 的灵活性使得我们能够实现高级的应用程序路由、SSL 终端和负载均…

一种新的电子邮件攻击方式:AiTM

新的攻击组利用合作伙伴组织之间的信任关系来绕过多重身份验证。 一种新的攻击方式开始出现&#xff0c;它利用合作伙伴组织之间的信任关系绕过多重身份验证。在一个利用不同组织之间关系的攻击中&#xff0c;攻击者成功地对四家或更多组织进行了商业电子邮件欺诈(BEC)攻击&…

中泰免签,准备去泰国旅游了吗?《泰语翻译通》app支持文本翻译和语音识别翻译,解放双手对着说话就能翻译。

泰国是很多中国游客的热门选择&#xff0c;现在去泰国旅游更方便了&#xff0c;因为泰国对中国免签了。如果你打算去泰国&#xff0c;那么下载一个好用的泰语翻译软件是很有必要的。 简单好用的翻译工具 《泰语翻译通》App就是为泰国旅游设计的&#xff0c;它翻译准确&#x…

Golang | Leetcode Golang题解之第420题强密码检验器

题目&#xff1a; 题解&#xff1a; func strongPasswordChecker(password string) int {hasLower, hasUpper, hasDigit : 0, 0, 0for _, ch : range password {if unicode.IsLower(ch) {hasLower 1} else if unicode.IsUpper(ch) {hasUpper 1} else if unicode.IsDigit(ch)…

Python | Leetcode Python题解之第421题数组中两个数的最大异或值

题目&#xff1a; 题解&#xff1a; class Trie:def __init__(self):# 左子树指向表示 0 的子节点self.left None# 右子树指向表示 1 的子节点self.right Noneclass Solution:def findMaximumXOR(self, nums: List[int]) -> int:# 字典树的根节点root Trie()# 最高位的二…

大模型中常见 loss 函数

loss 函数 首先&#xff0c;Loss 是允许不降到 0 的&#xff0c;模型计算的 loss 最终结果可以接近 0。 可以成为 loss 函数的条件## 常用 loss 以下函数调用基于 Pytorch&#xff0c;头文件导入&#xff1a; import torch.nn as nn 均方差&#xff08;MSE&#xff09; nn.…

基于微信小程序的剧本杀游玩一体化平台

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 基于微信小程序JavaSpringBootVueMySQL的剧…

详细分析Java中的ObjectMapper基本知识(附Demo)

目录 1. 基本知识2. 基本操作2.1 转换Java对象为JSON2.2 转换JSON为Java对象 3. 拓展 1. 基本知识 ObjectMapper 是 Jackson 数据处理库中的核心类之一&#xff0c;主要用于将 Java 对象转换为 JSON 和将 JSON 转换为 Java 对象 Jackson 是当前最流行的 JSON 处理库之一&…

秒懂Linux之消息队列与信号量(了解)

目录 前言 消息队列原理 信号量理论 信号量原理 IPC资源 前言 消息队列与信息量目前已经不常用了&#xff0c;大家也可以参考共享内存去了解基本原理即可。 消息队列原理 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法 每个数据块都被认为是有一个类型&…

ArcGIS10.2/10.6安装包下载与安装(附详细安装步骤)

相信从事地理专业的小伙伴来说&#xff0c;应该对今天的标题不会陌生。Arcgis是一款很常用的地理信息系统软件&#xff0c;主要用于地理数据的采集、管理、分析和展示。目前比较常见的版本有ArcGIS 10.2和ArcGIS 10.6。 不可否认&#xff0c;Arcgis具有强大的地图制作、空间分…

Linux环境Docker安装Mongodb

Linux环境Docker安装Mongodb 环境要求拉取指定版本镜像创建映射目录&#xff08;相当于数据存放于容器外&#xff0c;容器被删除不会影响数据&#xff09;启动容器 进入mongo命令行为指定db创建新用户查看mongodb的容器id进入命令行查看所有db切换db为指定db创建新用户使用新账…