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

目录

环形缓冲区(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.mzph.cn/web/53675.shtml

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

相关文章

OpenHarmony(鸿蒙南向)——平台驱动指南【MIPI CSI】

往期知识点记录&#xff1a; 鸿蒙&#xff08;HarmonyOS&#xff09;应用层开发&#xff08;北向&#xff09;知识点汇总 鸿蒙&#xff08;OpenHarmony&#xff09;南向开发保姆级知识点汇总~ 持续更新中…… 概述 功能简介 CSI&#xff08;Camera Serial Interface&#xf…

vue项目npm run serve 报错,Error: read ECONNRESET at TCP.onStreamRead

背景&#xff1a;vue2的项目&#xff0c;之前npm run serve一直可以正常使用&#xff0c;突然每次启动都会报错了&#xff0c;报错信息如下&#xff1a; node:events:492 throw er; // Unhandled error event ^ Error: read ECONNRESET at TCP.onStreamRead (n…

【动态规划-多重背包】【hard】力扣2585. 获得分数的方法数

考试中有 n 种类型的题目。给你一个整数 target 和一个下标从 0 开始的二维整数数组 types &#xff0c;其中 types[i] [counti, marksi] 表示第 i 种类型的题目有 counti 道&#xff0c;每道题目对应 marksi 分。 返回你在考试中恰好得到 target 分的方法数。由于答案可能很…

计算机毕业设计Python+Flask微博情感分析 微博舆情预测 微博爬虫 微博大数据 舆情分析系统 大数据毕业设计 NLP文本分类 机器学习 深度学习 AI

首先安装需要的python库&#xff0c; 安装完之后利用navicat导入数据库文件bili100.sql到mysql中&#xff0c; 再在pycharm编译器中连接mysql数据库&#xff0c;并在设置文件中将密码修改成你的数据库密码。最后运行app.py&#xff0c;打开链接&#xff0c;即可运行。 B站爬虫数…

Java语言程序设计基础篇_编程练习题**18.31 (替换单词)

目录 题目&#xff1a;**18.31 (替换单词) 习题思路 代码示例 运行结果 替换前 替换后 题目&#xff1a;**18.31 (替换单词) 编写一个程序&#xff0c;递归地用一个新单词替换某个目录下的所有文件中出现的某个单词。从命令行如下传递参数&#xff1a; java Exercise18…

CSS中的字体样式、文本样式、列表样式以及背景和渐变

一、字体样式和文本样式 1.span标签 span标签的作用&#xff1a;能让某几个文字或者是词语凸显出来 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-…

【ComfyUI】生成图细节更清晰——Consistency_Decoder

原文&#xff1a;https://github.com/openai/consistencydecoder comfyui: https://github.com/gameltb/Comfyui_Consistency_Decoder_VAE 博文资料下载&#xff1a;https://pan.baidu.com/s/1SwfA4T6iMsA8IrRrGXm4sg?pwd0925 安装 【秋葉aaaki】comfyui一键运行包 夸克网盘…

Vue3 + TS 实现同一项目同一链接,pc端打开是web应用,手机打开是H5应用

前言&#xff1a; 我自己搭建的项目基本都是用 postcss-px-to-viewport 插件进行适配的&#xff1b; 最近在做一个项目&#xff0c;需求是同样的功能&#xff0c;用户可以在电脑上打开操作使用&#xff0c;也可以在手机上登录进去操作使用&#xff0c;但是跳转链接是同一个&am…

LVS-DR实战案例,实现四层负载均衡

环境准备&#xff1a;三台虚拟机&#xff08;NET模式或者桥接模式&#xff09; 192.168.88.200 &#xff08;web1&#xff09;(安装nginx服务器作为测试) 192.168.88.201 &#xff08;服务器&#xff09;&#xff08;用于部署lvs-dr&#xff09; 192.168.88.202 (web2)…

猫头虎分享:Python库 Jinja2 的简介、安装、用法详解入门教程

猫头虎分享&#xff1a;Python库 Jinja2 的简介、安装、用法详解入门教程 &#x1f42f; 摘要 今天有粉丝问猫哥&#xff1a;“如何使用Jinja2进行Python模板渲染&#xff1f;”这是一个非常常见的问题&#xff0c;特别是在开发Web应用时。Jinja2是一个强大的模板引擎&#x…

一篇带你搞定数据结构散列表

数据结构入门学习&#xff08;全是干货&#xff09;——散列表 1 散列表 1.1 引子&#xff1a;散列的基本思路 C语言变量名的管理&#xff1a; 定义/声明&#xff1a;先定义后使用。插入与查找&#xff1a; 插入&#xff1a;新变量定义。查找&#xff1a;检查变量是否已定义。…

Remotion:使用前端技术开发视频

前言 最近做文章突然想到很多文章其实也可以用视频的方式来展现&#xff0c;以现在短视频的火爆程度&#xff0c;肯定能让更多的人看到。 恰巧最近看了很多关于动画的前端 js 库&#xff0c;那如果将这些动画帧连续起来&#xff0c;岂不是就成了一个视频吗&#xff1f; 而且…

smartctl 命令:查看硬盘健康状态

一、命令简介 ​smartctl​ 命令用于获取硬盘的 SMART 信息。 介绍硬盘SMART 硬盘的 SMART (Self-Monitoring, Analysis, and Reporting Technology) 技术用于监控硬盘的健康状态&#xff0c;并能提供一些潜在故障的预警信息。通过查看 SMART 数据&#xff0c;用户可以了解硬…

Python第一篇:Python解释器

一&#xff1a;python解释器 python解释器是一款程序&#xff0c;用于解释、执行Python源代码。 一般python解释器都是c python使用c编写的&#xff0c;还有j python用java编写的。 二&#xff1a;python下载 三&#xff1a;使用示例 python进入控制台&#xff0c;python。 三…

Claude 的上下文检索功能提升了 RAG 准确率,这会是人工智能革命?

前言 在人工智能领域不断进步的过程中&#xff0c;人们对更准确且具备上下文理解能力的响应的追求&#xff0c;催生了诸多突破性创新。 而 Claude 的上下文检索技术就是其中一项进步&#xff0c;有望显著提升检索增强生成 (RAG) 系统的表现。 可能有同学就要问了&#xff1a;…

uniapp实现在表单中展示多个选项,并且用户可以选择其中的一个或多个选项

前言 uni-data-checkbox是uni-app的一个组件,用于在表单中展示多个选项,并且用户可以选择其中的一个或多个选项。该组件可以通过设置不同的参数来控制选项的样式、布局和行为。 提示:以下是本篇文章正文内容,下面案例可供参考 uni-data-checkbox组件具有以下特点:: 1、跨…

Html--笔记01:使用软件vscode,简介Html5--基础骨架以及标题、段落、图片标签的使用

一.使用VSC--全称&#xff1a;Visual Studio Code vscode用来写html文件&#xff0c;打开文件夹与创建文件夹&#xff1a;①选择文件夹 ②拖拽文件 生成浏览器的html文件的快捷方式&#xff1a; &#xff01;enter 运行代码到网页的方法&#xff1a; 普通方法&#xff1a…

Debian与Ubuntu:深入解读两大Linux发行版的历史与联系

Debian与Ubuntu&#xff1a;深入解读两大Linux发行版的历史与联系 引言 在开源操作系统的领域中&#xff0c;Debian和Ubuntu是两款备受瞩目的Linux发行版。它们不仅在技术上有着密切的联系&#xff0c;而且各自的发展历程和理念也对开源社区产生了深远的影响。本文将详细介绍…

从零开始学习Python

目录 从零开始学习Python 引言 环境搭建 安装Python解释器 选择IDE 基础语法 注释 变量和数据类型 变量命名规则 数据类型 运算符 算术运算符 比较运算符 逻辑运算符 输入和输出 控制流 条件语句 循环语句 for循环 while循环 循环控制语句 函数和模块 定…

丹摩智算(damodel)部署stable diffusion实验

名词解释&#xff1a; 丹摩智算&#xff08;damodel&#xff09;&#xff1a;是一款带有RTX4090&#xff0c;Tesla-P40等显卡的公有云服务器。 stable diffusion&#xff1a;是一个大模型&#xff0c;可支持文生图&#xff0c;图生图&#xff0c;文生视频等功能 一.实验目标 …