【Linux】文件系统核心(二):深入 Ext2 底层:Block Group 结构 + inode 索引,轻松锁定文件的增删改查

目录

前言

一、宏观认识

[二、Block Group](#二、Block Group)

[三、Block Group内部结构](#三、Block Group内部结构)

[3.1、超级块------Super Block](#3.1、超级块——Super Block)

3.2、块组描述符表------GDT

[3.3、块位图------Block Bitmap](#3.3、块位图——Block Bitmap)

[3.4、inode位图------Inode Bitmap](#3.4、inode位图——Inode Bitmap)

[3.5、i 节点表 ------inode Table](#3.5、i 节点表 ——inode Table)

[3.6、数据块------Data Blocks](#3.6、数据块——Data Blocks)

四、inode索引数据块逻辑

[4.1、 inode和datablock映射](#4.1、 inode和datablock映射)

[思考:基于 inode 号的文件增、删、查、改到底在做什么?](#思考:基于 inode 号的文件增、删、查、改到底在做什么?)

[1. 查:根据 inode 号定位并读取文件](#1. 查:根据 inode 号定位并读取文件)

[2. 改:修改文件内容或元数据](#2. 改:修改文件内容或元数据)

[3. 增:基于 inode 号创建新文件](#3. 增:基于 inode 号创建新文件)

[4. 删:删除文件或硬链接](#4. 删:删除文件或硬链接)

[💡 结论:](#💡 结论:)

4.2、文件与目录名

4.2.1、路径解析

4.2.2、路径缓存

4.3、挂载分区

五、软硬链接

5.1、硬链接

5.2、软链接


前言

上一篇博客我们还遗留了几个问题,说到底就是文件系统的管理逻辑,那么今天我们就以经典的Ext2文件系统进行讲解,让我们更深入理解文件在磁盘上的管理,并与前面的文件也IO联系起来。

一、宏观认识

磁盘的空间一般都非常大,操作系统想要直接进行管理就比较困难,这时候就可以借鉴"分治"的管理思想。

比如国家的管理:先划分省,再由省划分市,再由市划分乡镇...,这样只要管理好乡镇,就相当于管理好了市,管理好了市,就相当于管理好了省,以此类推,就管理好了整个国家。

操作系统对磁盘的管理也是这样,先将磁盘进行划区(Partition) ,然后对一个区引入文件系统进行管理,其他区复制该管理方式即可实现对整盘的管理。本质就是将区格式化为某种格式的文件系统

上图中启动块(Boot Sector)的大小是确定的,为1KB,由PC标准规定,用来存储磁盘分区信 息和启动信息,任何文件系统都不能修改启动块。启动块之后才是ext2文件系统的开始。

二、Block Group

前面讲到磁盘的最小存储单元是扇区(0.5KB) ,但如果以扇区为单位向磁盘读写数据就会导致效率太低,所以Ext2文件系统以数据块(常见为4KB,8个扇区)为单位进行数据的读写 。而系统默认的一个Block Group包含8192个数据块,即32MB

以最小数据块为单位访问磁盘,不仅能一次性读写更多数据,还能利用数据的局部性原理提前缓存相关数据,从而显著提高内存缓存命中率和操作系统的整体效率。

所以,Ext2文件系统将区又划分块进行管理 ,类似于省分治市。每个块都有相同的结构,最小数据块的大小在格式化时进行配置,常见的块大小有 1KB、2KB、4KB,部分架构下最大支持 8KB,Block Group的大小根据数据块大小而异。通常情况下块的大小为4KB(8个扇区)

++注意:++

文件系统最小存储单位为 数据块(假设4KB),所以在Block Group中所有的结构都是以 最小数据块为单位进行存储的。即超级块,位图,inode节点表,inode节点,数据块的大小都是以 最小数据块(4KB)为单位的

三、Block Group内部结构

3.1、超级块------Super Block

存放文件系统本身的结构信息,描述整个分区的文件系统信息。

记录的信息主要有:bolck 和 inode的 总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写 入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。

可见其重要性,如果Super Block的信息被破坏,可 以说整个文件系统结构就被破坏了。所以,Super Block没有放在区分里,而是放在块中,相当于有多个备份,这些Super Block中的数据保持一致。

复制代码
/*
* Structure of the super block
*/
// 超级块的结构定义
struct ext2_super_block 
{
    __le32 s_inodes_count; // 索引节点(inode)总数

    __le32 s_blocks_count; // 数据块总数
    
    __le32 s_r_blocks_count; // 保留数据块总数(预留供超级用户/系统使用,防止磁盘占满导致系统异常)
    __le32 s_free_blocks_count; // 空闲数据块数量(动态更新)
    
    __le32 s_free_inodes_count; // 空闲索引节点(inode)数量(动态更新)
    
    __le32 s_first_data_block; // 第一个数据块的编号(标记文件系统中数据块的起始位置)
    
    __le32 s_log_block_size; // 数据块大小(以对数形式存储,实际大小=1024×2^该字段值)
    
    __le32 s_log_frag_size; // 碎片大小(以对数形式存储,用于碎片管理)
    
    __le32 s_blocks_per_group; // 每个块组包含的数据块数量
    
    __le32 s_frags_per_group; // 每个块组包含的碎片数量
    
    __le32 s_inodes_per_group; // 每个块组包含的索引节点(inode)数量
    
    __le32 s_mtime; // 最后一次挂载文件系统的时间(时间戳格式)
    
    __le32 s_wtime; // 最后一次对文件系统执行写操作的时间(时间戳格式)
    
    __le16 s_mnt_count; // 累计挂载次数(挂载一次则自增1)
    
    __le16 s_max_mnt_count; // 最大允许挂载次数(达到该次数后,建议执行fsck文件系统检查)
    
    // ...

    __le16 s_inode_size; // 单个索引节点(inode)结构体的大小(字节数)
    
    __le16 s_block_group_nr; // 该超级块所属的块组编号(用于备份超级块的识别)
    
    char s_last_mounted[64]; // 最后一次挂载的目录路径(记录该文件系统上次挂载到系统中的哪个目录)
    // ...
};

3.2、块组描述符表------GDT

描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。

每个块组描 述符存储一个块组 的描述信息,如在这个块组中从哪里开始是inode Table,从哪里开始是Data Blocks,空闲的inode和数据块还有多少个等等。块组描述符在每个块组的开头都有一份拷贝。

复制代码
/*
* Structure of a blocks group descriptor
*/
// 块组描述符的结构定义
struct ext2_group_desc
{
    __le32 bg_block_bitmap; // 该块组中块位图所在的数据块编号(通过该编号可直接找到块位图)
    
    __le32 bg_inode_bitmap; // 该块组中inode位图所在的数据块编号(通过该编号可直接找到inode位图)
    
    __le32 bg_inode_table; // 该块组中 inode表的起始数据块编号(通过该编号可直接找到 inode 表的起始位置)
    
    __le16 bg_free_blocks_count; // 该块组中当前的空闲 数据块数量(动态更新,创建/删除文件时变化)
    
    __le16 bg_free_inodes_count; // 该块组中当前的空闲 inode数量(动态更新,创建/删除文件时变化)
    
    __le16 bg_used_dirs_count; // 该块组中当前已创建的 目录数量(动态更新,创建/删除目录时变化)
    
    __le16 bg_pad;// 填充字段(用于内存对齐,保证后续字段符合 CPU 访问规范,提升读写效率)
    
    __le32 bg_reserved[3]; // 预留字段(为 Ext2 后续版本扩展预留空间,当前未使用)
};

3.3、块位图------Block Bitmap

Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。

用一个比特位映射一个数据块(Data Blocks),以 0/1 标记对应的数据块的占用情况。

3.4、inode位图------Inode Bitmap

与块位图类似,Inode Bitmap的每个bit表示一个inode是否空闲可用。

3.5、i 节点表 ------inode Table

文件 = 属性 + 内容,属性数据就存储在inode Table 中。

  • 存放文件属性 如 文件大小,所有者,最近修改时间等
  • 当前分组所有Inode属性的集合
  • inode编号以分区为单位,整体划分,不可跨分区,说明另一个分区的inode编号又从1开始。
复制代码
/*
 * Structure of an inode on the disk
 */
// 磁盘上的 inode 结构体定义(即 inode 表中单个节点的结构)
struct ext2_inode 
{
    __le16  i_mode; // 文件类型与访问权限(如普通文件、目录、读写执行权限等,对应 chmod 命令修改的内容)
    
    __le16  i_uid; // 文件所有者的用户 ID(通常 root 为 0,普通用户为递增数值)
    
    __le32  i_size; // 文件大小(以字节为单位,仅对普通文件有效)
    
    __le32  i_atime; // 最后一次访问文件的时间(如 cat 读取文件,不会修改该文件的内容,仅更新此时间)
    
    __le32  i_ctime; // 文件元数据最后修改的时间(如修改文件权限、所有者,会更新此时间;创建文件时初始化)
    
    __le32  i_mtime; // 文件内容最后修改的时间(如 vim 编辑保存文件,会更新此时间,对应 stat 命令查看的 mtime)
    
    __le32  i_dtime; // 文件删除时间(文件被删除时记录此时间戳,未删除时为 0)
    
    __le16  i_gid; // 文件所有者的用户组 ID(对应文件所属的用户组)
    
    __le16  i_links_count;  // 硬链接数量(文件的硬链接计数,当计数为 0 时,才会真正释放 inode 和数据块)
    
    __le32  i_blocks; // 该文件占用的数据块总数(注意:此处块通常为 512 字节一个,与 Ext2 的文件系统块不同)
    
    __le32  i_flags; // 文件标志位(用于控制文件的特殊行为,如是否允许追加写入、是否为只读等)
    
    // ...
    
    __le32  i_block[15]; // 数据块指针(直接+间接,共15个)
    // 文件数据块的指针数组(共 15 个指针,分为 3 类:
    // 1-12:直接指针,直接指向存储文件内容的数据块
    // 13:一级间接指针,指向一个存储数据块编号的间接块
    // 14:二级间接指针,指向一个存储一级间接块编号的块
    // 15:三级间接指针,指向一个存储二级间接块编号的块)
    
    // ...
};

++补充:++

  • 每个inode 大小均为 128 字节,而一个最小的数据块为4KB(4096字节),则一个数据块可以存储4096 / 128 = 32 个inode。
  • 通过inode号就能索引到inode table。

3.6、数据块------Data Blocks

数据块:存放文件内容,也就是一个一个的Block。

根据不同的文件类型有以下几种情况:

  • 对于普通文件,文件的数据存储在数据块中。
  • 对于目录文件,该目录下的所有文件名和目录名存储在所在目录的数据块中(即目录文件的文件内容为文件名和目录名),除了文件名外,ls -l命令 看到的其它信息保存在该文件的inode中
  • Data Block 号按照分区划分,不可跨分区。

Data Blocks的大小和最小存储单元数据块的大小一致为4KB。

复制代码
/*
 * Structure of a directory entry
 */
// 目录项的结构定义
struct ext2_dir_entry
{
    __le32  inode; // 该文件名对应的 inode 编号(通过该编号可找到对应的 ext2_inode 结构体,获取文件元数据)
    
    __le16  rec_len; // 该目录项的总长度(字节数,用于跳过当前目录项,查找下一个目录项,支持变长对齐)
    
    __le16  name_len; // 文件名的实际长度(字节数,最大不超过 255)
    
    char    name[]; // 文件名(变长数组,存储实际的文件名,长度由 name_len 指定,无结束符 '\0')
};

通过inode 就可以索引到对应文件的数据块。

四、inode索引数据块逻辑

4.1、inode和datablock映射

在 Ext2 文件系统中创建文件并写入数据时:

(1)操作系统先通过 inode 位图找到空闲 inode 节点并标记为已用,再通过块位图分配足够的空闲数据块并完成标记;

(2)随后将数据块编号存入 inode 的 **i_block[15] **指针数组,建立 inode 与数据块的关联,把文件数据写入对应数据块并完善 inode 元数据;

(3)最后在所属目录的数据块中新增 **ext2_dir_entry **结构体,完成「文件名 - inode 号」的映射,整个创建与写入流程就此完成。

复制代码
struct ext2_inode 
{
    // ...

    __le32  i_block[15]; // 数据块指针(直接+间接,共15个)
    // 文件数据块的指针数组(共 15 个指针,分为 3 类:
    // 1-12:直接指针,直接指向存储文件内容的数据块
    // 13:一级间接指针,指向一个存储数据块编号的间接块
    // 14:二级间接指针,指向一个存储一级间接块编号的块
    // 15:三级间接指针,指向一个存储二级间接块编号的块)
    
    // ...
};
  1. 对于比较小的文件,inode 直接索引一个数据块进行存储;

  2. 一级间接块索引表指针指向一个数据块,32为下一个指针大小为4字节,而一个数据块为4096字节,则可以存储4096 / 4 = 1024 个指针,可以索引1024个数据块存储文件内容数据,大小为 1024 * 4KB = 4MB;

  3. 二级间接块索引表指针指向一个数据块,存储1024个指针,然后每个指针又指向一个数据块,则可以索引 1024 * 1024 = 1048576个数据块,大小为 4GB。

  4. 三级间接块索引表指针最终可以索引 1024^3 = 1073741824 个数据块,存储4TB数据。

思考:基于 inode 号的文件增、删、查、改到底在做什么?

1. 查:根据 inode 号定位并读取文件

  • 核心操作
    1. 通过 inode 号在分区的**inode table**中找到对应的 inode 节点。
    2. 读取 inode 节点中存储的元数据(权限、大小、时间戳、数据块指针等)。
    3. 按照 inode 中的数据块指针(直接 / 间接索引),找到所有数据块并读取内容。
  • 本质:通过 inode 号直接绕过文件名,定位到文件的元数据和实际存储位置,是最底层的文件读取方式。

2. 改:修改文件内容或元数据

改操作分为两种场景,底层逻辑不同:

  • 修改文件内容
    1. 定位 inode 节点,检查 inode 的写权限。
    2. 若写入内容未超出已分配的数据块大小:直接在原数据块中覆盖写入。
    3. 若写入内容需要扩展:inode 会申请新的数据块,并更新自身的索引指针,同时修改 inode 中的文件大小、修改时间戳。
  • 修改文件元数据:直接修改 inode 节点中存储的信息(如权限、所有者、时间戳),无需改动数据块。

3. 增:基于 inode 号创建新文件

bash 复制代码
touch test.c // 创建文件

ls -i test.c // 查看文件inode号

创建一个新文件主要有以下4个操作:

  1. **存储属性:**内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
  2. **存储数据:**该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块 数据复制到300,下一块复制到500,以此类推。
  3. **记录分配情况:**文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
  4. **添加文件名到目录:**新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到 目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。

4. 删:删除文件或硬链接

删除操作的核心是处理 inode 的链接计数和数据块回收:

  1. 找到目标 inode 号对应的 inode 节点,将其链接计数(link count)-1
  2. 从对应目录的目录项中,删除 "文件名→inode 号" 的映射记录。
  3. 若链接计数降至 0:
    • 将 inode 节点标记为 "空闲",回收至 inode table 的空闲链表。
    • 将 inode 指向的所有数据块标记为 "空闲",回收至块位图(block bitmap)。
  • 本质:文件真正的删除发生在 "链接计数为 0" 时,此时系统才会回收 inode 和数据块;若仅删除一个硬链接,只要链接计数 > 0,文件数据仍存在。
💡 结论:
  • 分区之后的格式化操作,就是对分区进行分组,在每个分组中写入SB、GDT、Block Bitmap、Inode Bitmap等管理信息这些管理信息统称: 文件系统
  • 只要知道文件的inode号,就能在指定分区中确定是哪一个分组,进而在哪一个分组确定 是哪一个inode。
  • 拿到inode文件属性和内容就全部都有了。
    定位 inode table 的过程完全不需要 inode 号、也不需要任何 inode 节点 ,**inode table**是文件系统格式化时就固定在分区的独立元数据区域。

系统通过 ** 超级块(Super Block)** 即可直接定位,inode 号仅用于定位**inode table**内部的某个具体 inode 节点。

复制代码
struct ext2_super_block 
{
    /* 1. 块大小相关:辅助计算 inode table 占用的磁盘块数、间接推导偏移 */
    __le32 s_log_block_size;     

    /* 2. 单个 inode 节点的大小(字节):核心用于计算 inode 在 table 中的偏移 */
    __le16 s_inode_size;          

    /* 3. 每个块组的 inode 总数:描述单个块组中 inode table 的容量 */
    __le32 s_inodes_per_group;   

    /* 4. 整个分区的 inode 总数:间接推导 inode table 的总规模(跨所有块组) */
    __le32 s_inodes_count;       

    /* 5. 第一个可用的 inode 号:限定 inode table 的有效索引范围 */
    __le32 s_first_ino;          

    /* 6. 单个块组中 inode 位图的起始块号:辅助管理 inode table 的空闲状态 */
    __le32 s_inode_bitmap;       

    /* 7. 单个块组中 inode table 的起始块号:直接定位 inode table 的物理位置(核心中的核心) */
    __le32 s_inode_table;        
};

4.2、文件与目录名

💡 我们在访问文件时使用的都是文件名,没有用到 inode 号啊?

💡 目录是不是文件呢?

Linux中一切皆文件,目录当然也是一个文件,所以目录也有属性和内容。属性与普通文件相同;而其内容则是文件名与 inode号的映射关系

4.2.1、路径解析

💡 问题:打开当前工作目录文件,查看当前工作目录文件的内容,还得知道当前工作目录的上级目录文件的inode,不也得知道当前工作目录的上级目录吗?

答:由于文件再带路径,所以系统会自动递归解析文件的完整路径,逐层查找目录项

举个例子:访问**/home/user/test.txt**,系统的底层操作是:

  1. 先找到根目录 / 的 inode 号(固定为 2,系统保留),解析根目录的目录项,找到 home 对应的 inode 号;
  2. 再通过**home** 的 inode 号找到其目录数据块,解析目录项,找到**user** 对应的 inode 号;
  3. 最后通过 user 的 inode 号找到其目录数据块,解析目录项,找到 **test.txt**对应的 inode 号;
  4. 拿到 test.txt 的 inode 号后,进行后续的增删改查操作。

💡 问题:可是路径谁提供?

创建文件本质就是在磁盘文件系统中,新建目录文件。而你新建的任何文件,都在你或者系 统指定的目录下新建,而这就是天然就有路径!
💡注意:

  • 所以,我们知道了:访问文件必须要有目录+文件名=路径的原因
  • 根目录固定文件名,inode号,无需查找,系统开机之后就必须知道

4.2.2、路径缓存

通过上面我们就知道了,访问文件都要从根目录开始进行路径解析,但每次这样不是太慢了嘛!

所以,当我们打开目录时,Linux会进行进行路径缓存,即操作系统在内存进行路径维护

Linux中,在内核中维护树状路径结构的内核结构体叫做: struct dentry

bash 复制代码
struct dentry 
{
    atomic_t d_count;                // 目录项引用计数
    unsigned int d_flags;            // 目录项标志位(由d_lock自旋锁保护)
    spinlock_t d_lock;               // 每个目录项的专属自旋锁
    struct inode *d_inode;           // 该文件名所属的inode节点指针
                                     // 若为NULL,表示此为负目录项(对应不存在的文件/目录)
    
    struct hlist_node d_hash;        // 哈希查找链表的节点(用于目录项哈希表)
    struct dentry *d_parent;         // 指向父目录的目录项指针
    struct qstr d_name;              // 封装后的目录项文件名(含名、长度、哈希值)
    struct list_head d_lru;          // 最近最少使用链表的节点(用于缓存回收)
    
    union 
    {
        struct list_head d_child;        // 加入父目录子节点链表的节点
        struct rcu_head d_rcu;           // RCU机制的回收头(用于安全释放目录项内存)
    } d_u;
    struct list_head d_subdirs;      // 指向当前目录项的所有子目录/文件节点链表头
    struct list_head d_alias;        // 指向同一inode的别名目录项链表(硬链接专用)
    unsigned long d_time;            // 时间戳(由d_revalidate函数使用,验证目录项有效性)
    struct dentry_operations *d_op;  // 指向目录项的操作函数集指针
    struct super_block *d_sb;        // 指向目录项树的根超级块(所属文件系统的超级块)
    void *d_fsdata;                  // 文件系统专属的私有数据
#ifdef CONFIG_PROFILING              // 若开启性能分析配置
    struct dcookie_struct *d_cookie; // 性能分析的cookie结构(若有)
#endif
    int d_mounted;                   // 标记是否为文件系统挂载点
    unsigned char d_iname[DNAME_INLINE_LEN_MIN]; // 内嵌短文件名缓冲区(存储短文件名,内存优化)
};

注意:

• 每个文件其实都要有对应的dentry结构,包括普通文件。这样所有被打开的文件,就可以在内存中形成整个树形结构;

• 整个树形节点也同时会隶属于LRU (Least Recently Used,最近最少使用) 结构中,进行节点淘汰;

• 整个树形节点也同时会隶属于Hash,方便快速查找;

• 更重要的是,这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何文件,都在先在这 棵树下根据路径进行查找,找到就返回属性inode和内容,没找到就从磁盘加载路径,添加dentry 结构,缓存新路径。

4.3、挂载分区

有个问题,inode 号是以分区为单位进行划分的,仅在同一分区不冲突,即分区1,分区2,分区3...,中就会都有一个inode n(n号节点),那这些相同的inode 号会不会相互影响?

答:当然不会,因为Linux 将不同的磁盘分区挂载到不同目录下,形成统一的文件路径体系,所以根据路径就能够区分inode号来自哪个分区。

无论哪个分区的文件,最终都会有一个唯一的绝对路径,Linux 通过这个路径先定位到文件所属的分区,再在该分区内通过 inode 编号找到对应的 inode 结构体,后续再通过 inode 的索引机制访问数据块。路径成为了跨分区识别 inode 的 "唯一标识",从根本上解决了 inode 编号重复的问题。

五、软硬链接

5.1、硬链接

硬链接本质为让多个文件名与一个inode 关联,创建硬链接后,所有链接文件的 inode 编号、文件内容、权限完全一致,内核会记录该 inode 的 "硬链接数"(即绑定的文件名数量)。

bash 复制代码
ln abc def    // 让 def文件与abc建立硬链接

ls -i abc def  // 查看abc与def的inode

文件 abc与文件def 的inode 相同。

bash 复制代码
ls -li abc  // 查看abc的硬链接数

💡++注意:++

当我们删除硬链接的文件时,只有当硬链接数变为0时,系统才会释放该 inode 对应的磁盘空间(真正删除文件内容)。

5.2、软链接

软链接是 Linux 中基于文件路径的引用方式 ,核心是创建一个特殊的小文件 ,该文件本身仅存储目标文件 / 目录的绝对 / 相对路径,而非目标的 inode 号或数据,访问软链接时,系统会自动根据存储的路径跳转到目标文件 / 目录,类似 Windows 的快捷方式,是 Linux 中跨分区、跨文件系统引用文件的核心方式。

bash 复制代码
ln -s abc abc.s  // 创建abc的软链接abc.s

ls -li  // 查看文件信息

软链接是创建一个新文件,inode 不同。

😄 创作不易,你的点赞和关注都是对我莫大的鼓励,再次感谢您的观看😘

相关推荐
春日见11 分钟前
端到端自动驾驶综述
linux·人工智能·算法·机器学习·自动驾驶
Trouvaille ~26 分钟前
【项目篇】从零手写高并发服务器(六):EventLoop事件循环——Reactor的心脏
linux·运维·服务器·c++·高并发·epoll·reactor模式
bai_lan_ya27 分钟前
linux -- 文件IO
linux·服务器
林鸿群36 分钟前
Ubuntu 26.04 本地安装 GitLab CE 完整教程(非 Docker 方式)
linux·ubuntu·gitlab·私有部署·代码托管·ubuntu 26.04·omnibus
勇闯逆流河1 小时前
【Linux】Linux进程概念(进程优先级,进程切换详解)
linux·运维·服务器
老师好,我是刘同学1 小时前
30个核心Linux命令速查手册
linux
fsj2009yx1 小时前
如何把无公网的求生之路2服务器借助VPS转发注册到steam master列表中
linux·wireguard·求生之路2
慵懒的猫mi1 小时前
deepin UOS AI 助手接入飞书(Feishu)配置指南
linux·人工智能·ai·gpt-3·飞书·文心一言·deepin
Jiozg1 小时前
ES安装到linux(ubuntu)
linux·ubuntu·elasticsearch
面对疾风叭!哈撒给1 小时前
Linux之docker-compose使用(redis、nginx、tdengine、java应用)
linux·redis·docker