【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 不同。

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

相关推荐
楼田莉子2 小时前
Linux学习:进程信号
linux·运维·服务器·c++·学习
KeeBoom2 小时前
嵌入式 Linux 应用开发完全手册——阅读笔记14
linux·笔记
进击切图仔2 小时前
新装 Ubuntu 20.04.6 中安装 ssh.server 功能
linux·ubuntu·ssh
松涛和鸣2 小时前
69、Linux字符设备驱动实战
linux·服务器·网络·arm开发·数据库·驱动开发
TangDuoduo00052 小时前
【Linux下LED基础设备驱动】
linux·驱动开发
cyber_两只龙宝2 小时前
haproxy--使用socat工具实现对haproxy权重配置的热更新
linux·运维·负载均衡·haproxy·socat
٩( 'ω' )و2602 小时前
linux网络--基础概念
linux·网络
zhang6183992 小时前
Linux中不同服务器之间迁移python 虚拟环境-conda-pack
linux·运维·python
HIT_Weston2 小时前
121、【Ubuntu】【Hugo】首页板块配置:list 模板(一)
linux·ubuntu·list