当前位置: 首页 > news >正文

[Linux]学习笔记系列 -- lib/zlib DEFLATE压缩与解压缩


title: zlib
categories:

  • linux
  • lib
    tags:
  • linux
  • lib
    abbrlink: 73887d3a
    date: 2025-10-03 09:01:49

文章目录

  • lib/zlib DEFLATE压缩与解压缩
      • 历史与背景
        • 这项技术是为了解决什么特定问题而诞生的?
        • 它的发展经历了哪些重要的里程碑或版本迭代?
        • 目前该技术的社区活跃度和主流应用情况如何?
      • 核心原理与设计
        • 它的核心工作原理是什么?
        • 它的主要优势体现在哪些方面?
        • 它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
      • 使用场景
        • 在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
        • 是否有不推荐使用该技术的场景?为什么?
      • 对比分析
        • 请将其 与 其他相似技术 进行详细对比。
  • include/linux/decompress/mm.h
    • STATIC 静态注入时使用简单的malloc
    • 正常调用时 使用kmalloc
  • include/linux/zconf.h
  • lib/zlib_inflate/inflate.c
    • zlib_inflateInit2
    • zlib_inflate
      • HEAD waiting for magic header
      • DICTID DICT TYPE
      • TYPEDO 等待类型位,包括最后一个标志位,跳过检查以退出新块的膨胀
      • STORED 等待存储的大小(length 和 complement)
      • COPY 等待输入或输出复制存储的块
      • LEN 等待 length/lit 代码
      • LIT 字面值 等待输出空间写入文本
  • lib/decompress_inflate.c

在这里插入图片描述

lib/zlib DEFLATE压缩与解压缩

https://github.com/wdfk-prog/linux-study

历史与背景

这项技术是为了解决什么特定问题而诞生的?

内核中集成的 zlib 库是为了提供一个通用、可靠且经过充分验证的数据压缩和解压缩方案。数据压缩的根本目标是通过算法减少数据占用的存储空间或网络传输带宽。在内核这个层面,引入 zlib 主要解决了以下具体问题:

  • 节省存储空间:对于存储受限的系统(尤其是早期的嵌入式设备),需要通过压缩文件系统来存储更多的内容。
  • 加快启动速度:Linux内核镜像本身可以被压缩。在启动时,一个小型解压程序(stub)会先解压内核镜像到内存中再执行。虽然解压需要CPU时间,但从慢速存储设备(如早期的磁盘、Flash)读取一个较小的压缩文件所需的时间,远少于读取一个大的未压缩文件的时间,因此总体上加快了启动速度。
  • 提高网络效率:在一些网络协议(如PPP)中,对传输的数据进行压缩可以显著减少网络带宽的占用。
  • 内存优化-:通过在内存中创建压缩的块设备(RAM disk),可以在有限的物理内存中存放更多的数据,这在内存紧张的系统中非常有用。
它的发展经历了哪些重要的里程碑或版本迭代?
  • zlib库的诞生zlib 库由Jean-loup Gailly和Mark Adler于1995年首次公开发布,作为DEFLATE压缩算法的一个开源实现,最初主要用于libpng图像库。
  • 早期内核集成:Linux内核很早就集成了zlib的功能。例如,根据代码注释,inflate.c 在1993年就由Hannu Savolainen为引导Linux而适配,并随着内核发展而演进。 至少从2.6.12版本开始,zlib 已经是内核源码树中的一个标准部分。
  • 专门的内核版本:内核中的zlib是官方zlib库的一个修改版。 最重要的修改是内存管理方式。内核中不允许动态内存分配(可能导致睡眠),因此内核版的zlib被修改为要求所有内存都在使用前预先分配好。
  • 硬件加速:随着技术发展,一些硬件平台(如IBM s390)开始提供DEFLATE算法的硬件加速指令。内核中的zlib库也相应地进行了扩展,以便在支持的硬件上利用这些指令来极大地提升压缩和解压缩性能。
目前该技术的社区活跃度和主流应用情况如何?

zlib 是内核中最基础、最稳定、应用最广泛的压缩库之一。它被认为是数据压缩领域的“事实标准”之一。在内核中,它被大量关键子系统所使用:

  • 文件系统:如SquashFSCramFS、以及Btrfsext4(可选功能)等都使用zlib进行数据压缩。
  • 内存管理zram(压缩的RAM块设备)可以使用zlib作为其压缩后端。
  • 内核启动:内核镜像压缩的默认选项之一。
  • 网络:一些协议栈中用于数据压缩。

虽然近年来出现了压缩/解压速度更快的算法(如LZ4, Zstd),但zlib凭借其良好的压缩率、广泛的硬件支持和极高的可靠性,仍然在许多场景下是首选或默认的解决方案。

核心原理与设计

它的核心工作原理是什么?

lib/zlib_deflate/deflate.c(压缩)和 lib/zlib_inflate/inflate.c(解压缩)共同实现了DEFLATE算法,该算法是zlibgzip格式的核心。DEFLATE算法巧妙地结合了两种成熟的压缩技术:

  1. LZ77(Lempel-Ziv 1977):这是一种字典压缩算法。它的核心思想是在数据流中寻找重复的字节序列。当找到一个重复序列时,不再存储序列本身,而是存储一个指向之前出现过的相同序列的指针(距离)该序列的长度。这样,对于有大量重复内容的数据(如文本、代码),可以获得很好的压缩效果。这个“之前出现过的数据”的窗口大小通常是32KB。
  2. 霍夫曼编码(Huffman Coding):这是一种熵编码算法。经过LZ77处理后,输出流中包含了两种信息:单个的字节(未找到重复时)和(长度, 距离)对。霍夫曼编码会分析这些输出符号的出现频率,为出现频率高的符号分配较短的二进制码,为出现频率低的符号分配较长的二进制码。这进一步压缩了LZ77的输出结果。

deflate.c负责执行上述压缩过程,而inflate.c则执行逆过程:读取霍夫曼编码的数据,解码出字面量字节或(长度, 距离)对,如果是后者,则从已经解压的输出缓冲区中复制相应的数据,从而逐步重建出原始数据。

它的主要优势体现在哪些方面?
  • 良好的压缩率zlib通常能提供相当不错的压缩比,优于那些主要追求速度的算法(如LZO、LZ4)。
  • 标准化与可移植性zlib是一个经过了时间考验的开放标准(RFC 1950, 1951, 1952),确保了不同系统和应用之间的兼容性。
  • 可靠性:作为一个非常成熟的库,其代码健壮,能很好地处理各种数据,甚至包括损坏的输入。
  • 内存占用可控:内核版的zlib允许预先分配工作区,使其在内存受限的嵌入式环境中也能可靠运行。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
  • 性能:与现代压缩算法相比,zlib的压缩和解压缩速度通常较慢。在性能是首要考虑因素的场景下(例如,需要极快启动速度的系统),LZ4或Zstd可能是更好的选择。
  • CPU消耗:相对于LZO/LZ4等简单算法,zlib在实现其较高压缩率时需要消耗更多的CPU周期。
  • 单线程限制:标准的zlib实现是单线程的。虽然可以在更高层面进行并行化(例如,SquashFS可以并行解压不同的数据块),但单个数据流的处理无法利用多核优势。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
  • 只读压缩文件系统:对于嵌入式设备的固件、Live CD/USB等场景,SquashFS和早期的CramFS是理想选择。它们在制作时一次性使用zlib将文件系统压缩,以获得高压缩率,从而节省宝贵的Flash空间。运行时,内核的inflate模块负责解压读取的文件数据。
  • 需要较高压缩率的RAM盘:当使用zram作为交换设备(swap)或通用RAM盘时,如果系统的CPU性能相对充裕,而内存极度紧张,选择zlib作为压缩算法可以最大化内存的利用率。
  • 网络数据传输:在带宽受限的网络连接中(例如早期的PPP拨号网络),使用zlib压缩数据可以有效提升有效吞吐率。
是否有不推荐使用该技术的场景?为什么?
  • 性能优先的场景:当解压缩速度直接影响系统响应能力时,不推荐使用zlib。例如,在配置了高速NVMe SSD的现代计算机上,内核启动时解压镜像的时间可能成为瓶颈。在这种情况下,使用解压速度极快的LZ4,尽管内核镜像会稍大一些,但总的启动时间可能会更短。
  • 频繁写操作的压缩:对于需要频繁进行写操作的压缩文件系统或存储层,zlib较高的压缩CPU开销可能会成为性能瓶颈。

对比分析

请将其 与 其他相似技术 进行详细对比。

Linux内核中集成了多种压缩算法,它们在压缩率、压缩速度和解压速度之间做出了不同的权衡。

特性zlib (DEFLATE)LZOLZ4Zstandard (Zstd)
功能概述压缩率良好,速度适中,非常成熟的通用算法。极快的解压缩速度,压缩速度也很快,但压缩率较低。与LZO类似,解压缩速度极快,压缩速度也非常快,压缩率略低于LZO。提供了类似zlib的压缩率,但压缩和解压缩速度快得多,接近LZO/LZ4的水平。
压缩率良好良好至优秀
压缩速度中等非常快快至中等(可调)
解压速度中等非常快非常快
CPU使用中等低至中等
内存使用中等非常低可调
典型用途SquashFS, btrfs, zram, 内核镜像,网络协议。zram, btrfs, 内核镜像。zram, 内核镜像,因其极快的解压速度而日益流行。btrfs, zram, SquashFS,被认为是zlib的现代替代品。

include/linux/decompress/mm.h

STATIC 静态注入时使用简单的malloc

  1. free_mem_ptrarch/arm/boot/compressed/misc.c中传入.free_mem_ptr通过decompress_kernel函数的参数free_mem_ptr_p(堆栈上方的 malloc 空间)赋值
  2. free_mem_end_ptrarch/arm/boot/compressed/misc.c中传入.free_mem_end_ptr通过decompress_kernel函数的参数free_mem_end_ptr_p(表示动态内存分配空间的最大地址)赋值.大小64K
  3. output_dataarch/arm/boot/compressed/misc.c中传入.output_data通过decompress_kernel函数的参数output_data_p(内核执行地址)赋值
  • malloc直接将传入的malloc空间进行使用,但是使用前进行了对齐处理
  • 每次malloc都会执行对齐与向下分配
  • free直接将malloc指针指向最开始的地方.释放所有的空间
/** When an architecture needs to share the malloc()/free() implementation* between compilation units, it needs to have non-local visibility.*/
#ifndef MALLOC_VISIBLE
#define MALLOC_VISIBLE static
#endif/* A trivial malloc implementation, adapted from*  malloc by Hannu Savolainen 1993 and Matthias Urlichs 1994*/
STATIC_RW_DATA unsigned long malloc_ptr;
STATIC_RW_DATA int malloc_count;MALLOC_VISIBLE void *malloc(int size)
{void *p;if (size < 0)return NULL;if (!malloc_ptr)malloc_ptr = free_mem_ptr;malloc_ptr = (malloc_ptr + 7) & ~7;     /* Align */p = (void *)malloc_ptr;malloc_ptr += size;if (free_mem_end_ptr && malloc_ptr >= free_mem_end_ptr)return NULL;malloc_count++;return p;
}MALLOC_VISIBLE void free(void *where)
{malloc_count--;if (!malloc_count)malloc_ptr = free_mem_ptr;
}

正常调用时 使用kmalloc

#define malloc(a) kmalloc(a, GFP_KERNEL)
#define free(a) kfree(a)#define large_malloc(a) vmalloc(a)
#define large_free(a) vfree(a)

include/linux/zconf.h

/* deflate 的内存要求为 (以字节为单位):(1 << (windowBits 2))   (1 << (memLevel 9))即:128K(对于 windowBits=15)128K(对于 memLevel = 8)(默认值)加上几千字节的小对象。例如,如果要减少默认内存要求从 256K 到 128K,使用使 CFLAGS=“-O -DMAX_WBITS=14 -DMAX_MEM_LEVEL=7”当然,这通常会降低压缩率(天下没有免费的午餐)。inflate 的内存要求为 (以字节为单位) 1 << windowBits即 windowBits=15(默认值)的 32K 加上几千字节用于小物体。
*//* Maximum value for memLevel in deflateInit2 */
#ifndef MAX_MEM_LEVEL
#  define MAX_MEM_LEVEL 8
#endif/* Maximum value for windowBits in deflateInit2 and inflateInit2.* WARNING: reducing MAX_WBITS makes minigzip unable to extract .gz files* created by gzip. (Files created by minigzip can still be extracted by* gzip.)*/
#ifndef MAX_WBITS
#  define MAX_WBITS   15 /* 32K LZ77 window */
#endif

lib/zlib_inflate/inflate.c

  1. wrap根据输入的windowBits的值来决定,如果windowBits小于0,则是zlib格式,否则是gzip格式

zlib_inflateInit2

int zlib_inflateInit2(z_streamp strm, int windowBits)
{struct inflate_state *state;if (strm == NULL) return Z_STREAM_ERROR;strm->msg = NULL;                 /* in case we return an error */state = &WS(strm)->inflate_state;strm->state = (struct internal_state *)state;if (windowBits < 0) {state->wrap = 0;windowBits = -windowBits;}else {state->wrap = (windowBits >> 4) + 1;}if (windowBits < 8 || windowBits > 15) {return Z_STREAM_ERROR;}state->wbits = (unsigned)windowBits;
#ifdef CONFIG_ZLIB_DFLTCC/** DFLTCC requires the window to be page aligned.* Thus, we overallocate and take the aligned portion of the buffer.*/state->window = PTR_ALIGN(&WS(strm)->working_window[0], PAGE_SIZE);
#elsestate->window = &WS(strm)->working_window[0];
#endifreturn zlib_inflateReset(strm);
}

zlib_inflate

  1. {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15};
    • 代码长度可以是 1…15,最小和最大的出现概率最低。因此 1 和 15 位于末尾。2 和 14 位于它们之前。以此类推。重复代码 16、17 和 18 几乎总是被使用,0 表示符号不存在,也是如此。所以这些代码最好放在开头。
    • 这个是Huffman编码后的长度序列的 Huffman 编码表所使用的huffman编码,这个表是根据统计与经验确定的.
int zlib_inflate(z_streamp strm, int flush)
{struct inflate_state *state;const unsigned char *next;  /* next input */unsigned char *put;         /* next output */unsigned have, left;        /* available input and output */unsigned long hold;         /* bit buffer */unsigned bits;              /* bits in bit buffer */unsigned in, out;           /* save starting available input and output */unsigned copy;              /* number of stored or match bytes to copy */unsigned char *from;        /* where to copy match bytes from */code this;                  /* current decoding table entry */code last;                  /* parent table entry */unsigned len;               /* length to copy for repeats, bits to drop */int ret;                    /* return code */static const unsigned short order[19] = /* permutation of code lengths */{16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15};/* Do not check for strm->next_out == NULL here as ppc zImageinflates to strm->next_out = 0 */if (strm == NULL || strm->state == NULL ||(strm->next_in == NULL && strm->avail_in != 0))return Z_STREAM_ERROR;state = (struct inflate_state *)strm->state;if (state->mode == TYPE) state->mode = TYPEDO;      /* skip check */LOAD();in = have;out = left;ret = Z_OK;for (;;)switch (state->mode) {}
}

HEAD waiting for magic header

  1. zlib_inflateReset()中设置了HEAD状态
  2. wrap为0,使用zlib,则跳转到TYPEDO
  3. wrap为1,使用gzip,检查开头格式
        case HEAD:if (state->wrap == 0) {state->mode = TYPEDO;break;}NEEDBITS(16);if (((BITS(8) << 8) + (hold >> 8)) % 31) {strm->msg = (char *)"incorrect header check";state->mode = BAD;break;}if (BITS(4) != Z_DEFLATED) {strm->msg = (char *)"unknown compression method";state->mode = BAD;break;}DROPBITS(4);len = BITS(4) + 8;if (len > state->wbits) {strm->msg = (char *)"invalid window size";state->mode = BAD;break;}state->dmax = 1U << len;strm->adler = state->check = zlib_adler32(0L, NULL, 0);state->mode = hold & 0x200 ? DICTID : TYPE;INITBITS();break;

DICTID DICT TYPE

  1. 对gunzip格式执行相应的检查
  2. 依次执行检查跳转下一个case
  3. 最后转入TYPEDO状态

TYPEDO 等待类型位,包括最后一个标志位,跳过检查以退出新块的膨胀

  1. 每个压缩数据块以包含以下数据的 3 个头部位开始,请注意,头部位不一定从字节边界开始,因为一个块不一定占用整数个字节。
        /*next 2 bits     BTYPEfirst bit       BFINAL  //BFINAL 仅在此为数据集的最后一个块时才被设置。*//*两种压缩情况之间的唯一区别是定义字面值/长度和距离字母表的霍夫曼码的方式TYPE 指定数据的压缩方式,如下:00 - no compression01 - compressed with fixed Huffman codes10 - compressed with dynamic Huffman codes11 - reserved (error)*/case TYPEDO:INFLATE_TYPEDO_HOOK(strm, flush);if (state->last) {BYTEBITS();state->mode = CHECK;break;}NEEDBITS(3);state->last = BITS(1);DROPBITS(1);switch (BITS(2)) {case 0:                             /* stored block */state->mode = STORED;break;case 1:                             /* fixed block */zlib_fixedtables(state);state->mode = LEN;              /* decode codes */break;case 2:                             /* dynamic block */state->mode = TABLE;break;case 3:strm->msg = (char *)"invalid block type";state->mode = BAD;}DROPBITS(2);break;

STORED 等待存储的大小(length 和 complement)

/** 输入中直到下一个字节边界的任何位都被忽略。块的其余部分包含以下信息0   1   2   3   4...+---+---+---+---+================================+|  LEN  | NLEN  |... LEN bytes of literal data...|+---+---+---+---+================================+* LEN 是块中数据字节的数目。NLEN 是 LEN 的反码。
*/case STORED:BYTEBITS();                         /* go to byte boundary */NEEDBITS(32);if ((hold & 0xffff) != ((hold >> 16) ^ 0xffff)) {strm->msg = (char *)"invalid stored block lengths";state->mode = BAD;break;}state->length = (unsigned)hold & 0xffff;INITBITS();state->mode = COPY;fallthrough;

COPY 等待输入或输出复制存储的块

  • copy之后重新设置state->mode = TYPE
        case COPY:copy = state->length;if (copy) {if (copy > have) copy = have;if (copy > left) copy = left;if (copy == 0) goto inf_leave;memcpy(put, next, copy);have -= copy;next += copy;left -= copy;put += copy;state->length -= copy;break;}state->mode = TYPE;break;

LEN 等待 length/lit 代码

        case LEN://快速路径优化 if (have >= 6 && left >= 258) {RESTORE();inflate_fast(strm, out);LOAD();break;}//哈夫曼解码:根据固定的haffman编码进行解码for (;;) {this = state->lencode[BITS(state->lenbits)];if ((unsigned)(this.bits) <= bits) break;PULLBYTE();}//多级查找: 如果当前条目 this 的操作码 (op) 指示需要更多位if (this.op && (this.op & 0xf0) == 0) {last = this;for (;;) {this = state->lencode[last.val +(BITS(last.bits + last.op) >> last.bits)];if ((unsigned)(last.bits + this.bits) <= bits) break;PULLBYTE();}DROPBITS(last.bits);}DROPBITS(this.bits);//处理解码结果: state->length = (unsigned)this.val;//如果操作码 op 为 0,表示这是一个字面值,状态机切换到 LIT 模式。if ((int)(this.op) == 0) {state->mode = LIT;break;}//如果操作码包含标志位 32,表示这是一个块结束符,状态机切换到 TYPE 模式if (this.op & 32) {state->mode = TYPE;break;}//。如果操作码包含标志位 64,表示解码出错,设置错误消息并切换到 BAD 模式if (this.op & 64) {strm->msg = (char *)"invalid literal/length code";state->mode = BAD;break;}//额外位处理: 如果操作码指示需要额外的位(op & 15),这些位的数量存储在 state->extra 中,状态机切换到 LENEXT 模式以处理这些额外位state->extra = (unsigned)(this.op) & 15;state->mode = LENEXT;fallthrough;

LIT 字面值 等待输出空间写入文本

        case LIT:if (left == 0) goto inf_leave;*put++ = (unsigned char)(state->length);left--;state->mode = LEN;break;

lib/decompress_inflate.c

  1. 压缩后的文件:arch/arm/boot/compressed/piggy_data
/*** buf: 需要解压的buf* flush:则out_len限制为0x8000(32K),使用malloc分配* out_len: 为0则无限制
*/
static int INIT __gunzip(unsigned char *buf, long len,long (*fill)(void*, unsigned long),long (*flush)(void*, unsigned long),unsigned char *out_buf, long out_len,long *pos,void(*error)(char *x)) {u8 *zbuf;struct z_stream_s *strm;int rc;rc = -1;/* verify the gzip header */if (len < 10 ||zbuf[0] != 0x1f || zbuf[1] != 0x8b || zbuf[2] != 0x08) {if (pos)*pos = 0;error("Not a gzip file");goto gunzip_5;}/* skip over gzip header (1f,8b,08... 10 bytes total +* possible asciz filename)*/strm->next_in = zbuf + 10;strm->avail_in = len - 10;/* skip over asciz filename */if (zbuf[3] & 0x8) {do {/** If the filename doesn't fit into the buffer,* the file is very probably corrupt. Don't try* to read more data.*/if (strm->avail_in == 0) {error("header error");goto gunzip_5;}--strm->avail_in;} while (*strm->next_in++);}strm->next_out = out_buf;strm->avail_out = out_len;//MAX_WBITS 滑动窗口的大小rc = zlib_inflateInit2(strm, -MAX_WBITS);#ifdef CONFIG_ZLIB_DFLTCC/* Always keep the window for DFLTCC */
#elseif (!flush) {WS(strm)->inflate_state.wsize = 0;WS(strm)->inflate_state.window = NULL;}
#endifwhile (rc == Z_OK) {if (strm->avail_in == 0) {/* TODO: handle case where both pos and fill are set */len = fill(zbuf, GZIP_IOBUF_SIZE);if (len < 0) {rc = -1;error("read error");break;}strm->next_in = zbuf;strm->avail_in = len;}rc = zlib_inflate(strm, 0); //按照滑动窗口的大小进行解压缩/* Write any data generated */if (flush && strm->next_out > out_buf) {long l = strm->next_out - out_buf;if (l != flush(out_buf, l)) {rc = -1;error("write error");break;}strm->next_out = out_buf;strm->avail_out = out_len;}/* after Z_FINISH, only Z_STREAM_END is "we unpacked it all" */if (rc == Z_STREAM_END) {rc = 0;break;} else if (rc != Z_OK) {error("uncompression error");rc = -1;}}zlib_inflateEnd(strm);if (pos)/* add + 8 to skip over trailer */*pos = strm->next_in - zbuf+8;return rc; /* returns Z_OK (0) if successful */
}
http://www.dtcms.com/a/486539.html

相关文章:

  • 为电力设备装上“感知神经”:AHE100无线温湿度传感器,守护安全运行的隐形卫士
  • RocketMQ如何保证消息不丢失
  • CC1-二叉树的最小深度
  • 把 Python 应用打包成 Mac 应用程序 — 完整指南
  • 阿里云监控:SLS的使用
  • C语言面试题答案版(ai生成)
  • 做网站发广告重庆建站模板
  • 吃透大数据算法-用 “任务排队” 讲透 Kahn 算法的核心
  • 外贸网站建设 全球搜天津网址
  • MeshGPT:三角形网格生成的Decoder-Only Transformer范式解析
  • vllm论文中 内部碎片原因
  • 重庆市设计公司网站wordpress 计数js版
  • linux中mount的本质是什么?自己如何实现一个伪文件系统
  • wordpress哪个编辑器好用吗长春网站优化咨询
  • 深度学习经典网络解析:ResNet
  • qingdao城乡住房建设厅网站网站建设中的策略
  • 字节数开一面
  • 页面转wordpress辛集seo网站优化电话
  • 优化推广网站seo讷河做网站公司
  • ASP的checkbox
  • 【个人成长笔记】在Ubuntu中将Linux系统的文件夹名称从中文改回英文的完整指南
  • Hosmer-Lemeshow检验:逻辑回归模型拟合优度的守护者
  • 主流机器学习算法的快速应用指南
  • 优惠码购买lisahost季付款VPS评测分享
  • Samba共享服务搭建
  • k8s 持久化存储方案-NFS
  • 建一个网站都需要什么开发软件用什么编程软件
  • 北京网站设计优刻如何将网站上传到空间
  • 大模型嵌入 vs ES:语义搜索与关键字搜索
  • 仓颉编程(1)环境配置变量