36、开发 Linux 内核文件系统

开发 Linux 内核文件系统

在 Linux 内核文件系统的开发中,有诸多关键的操作和函数需要我们深入了解。下面将详细介绍文件系统中目录查找、路径名解析、inode 操作等方面的内容。

1. 目录查找与路径名解析

在处理路径名解析时,文件系统有三个主要的入口点,分别是 ux_readdir() ux_lookup() ux_read_inode() 。当用户在根目录下执行 ls 命令时,可以很好地观察这三个函数的协同工作。

当文件系统挂载时,内核会获取根目录的句柄,并导出以下操作:

struct inode_operations ux_dir_inops = {
    create: 
        ux_create,
    lookup: 
        ux_lookup,
    mkdir:
        ux_mkdir,
    rmdir: 
        ux_rmdir,
    link: 
        ux_link,
    unlink:
        ux_unlink,
};

struct file_operations ux_dir_operations = {
    read: 
        generic_read_dir,
    readdir:
        ux_readdir,
    fsync:
        file_fsync,
};

内核在目录级别有两个用于名称解析的调用:
- 调用 ux_readdir() 以获取所有目录条目的名称。文件系统挂载后,内存中唯一的 inode 是根 inode,因此此操作只能在根 inode 上调用。
- 给定一个文件名,可以调用 ux_lookup() 函数在目录中查找该名称。如果找到,该函数应返回该名称对应的 inode。

2. 读取目录条目

当执行 ls 命令时,它需要知道指定目录或当前工作目录中的所有条目信息。这涉及调用 getdents() 系统调用,其原型如下:

int getdents(unsigned int fd, struct dirent *dirp, unsigned int count);

dirp 指针指向一个内存区域,其大小由 count 指定。内核将尝试读取尽可能多的目录条目,并返回读取的字节数。 dirent 结构体如下所示:

struct dirent
{
    long d_ino;                 /* inode number */
    off_t d_off;                /* offset to next dirent */
    unsigned short d_reclen;    /* length of this dirent */
    char d_name [NAME_MAX+1];   /* file name (null-terminated) */
}

为了读取所有目录条目, ls 可能需要根据传递的缓冲区大小和目录中的条目数量多次调用 getdents()

为了填充传递给内核的缓冲区,可能会多次调用 ux_readdir() 函数。该函数的定义如下:

int
ux_readdir(struct file *filp, void *dirent, filldir_t filldir)

每次调用该函数时,目录内的当前偏移量会增加。 ux_readdir() 执行的第一步是将现有偏移量映射到块号,如下所示:

pos = filp->f_pos;
blk = (pos + 1) / UX+BSIZE;
blk = uip->iaddr[blk];

首次进入时, pos 为 0,因此要读取的块将是 i_addr[0] 。将对应于该块的缓冲区读入内存,并进行搜索以定位所需的文件名。每个块由 UX_DIRS_PER_BLOCK ux_dirent 结构组成。假设块中适当偏移处的条目有效( d_ino 不为 0),则调用 filldir() 例程(所有文件系统都使用的通用内核函数)将条目复制到用户的地址空间。

对于找到的每个目录条目,或者如果遇到空目录条目,则目录内的偏移量按如下方式递增:

filp->f_pos += sizeof(struct ux_dirent);

以记录如果再次调用 ux_readdir() 时从何处开始下一次读取。

3. 文件名查找

从文件系统的角度来看,路径名解析是一个相当直接的过程。只需要提供 inode_operations 向量的 lookup() 函数,该函数会接收父目录的句柄和要搜索的名称。

ux_lookup() 函数在 ux_dir.c (第 838 到 860 行)中被调用,传递父目录 inode 和一个部分初始化的 dentry 用于查找文件名。 ux_lookup() 需要处理两种情况:
- 名称不存在于指定目录中 :在这种情况下,将返回 EACCES 错误,内核会将 dentry 标记为负。如果再次请求搜索相同的名称,内核会在 dcache 中找到负条目并向用户返回错误。
- 名称位于目录中 :在这种情况下,文件系统应调用 iget() 来分配一个新的 Linux inode。

ux_lookup() 执行的主要任务是调用 ux_find_entry() ,如下所示:

inum = ux_find_entry(dip, (char *)dentry->d_name.name);

ux_find_entry() 函数在 ux_inode.c (第 1031 到 1054 行)中会遍历目录中的所有块( i_addr[] ),并调用 sb_bread() 将每个适当的块读入内存。对于每个块,可能有 UX_DIRS_PER_BLOCK ux_dirent 结构。如果目录条目未使用,则 d_ino 字段将设置为 0。如果找到有效条目, ux_lookup() 会调用 iget() 将 inode 读入内存。

4. 文件系统/内核交互以列出目录

下面通过一个示例展示在根目录上运行 ls 时内核与文件系统的交互。设置断点在 ux_lookup() ux_readdir() ux_read_inode() 三个函数上,然后在刚挂载的文件系统上执行 ls 命令。假设要挂载的文件系统包含 lost+found 目录(inode 3)和一个 passwd 文件的副本(inode 4)。

graph LR
    A[挂载文件系统] -->|调用 ux_read_inode| B(读取 inode 2)
    B --> C[执行 ls /mnt]
    C -->|调用 ux_readdir| D(读取根目录条目)
    D -->|调用 ux_lookup| E(查找 lost+found)
    E -->|调用 ux_read_inode| F(读取 inode 3)
    F -->|调用 ux_readdir| G(再次读取根目录条目)
    G -->|调用 ux_lookup| H(查找 passwd)
    H -->|调用 ux_read_inode| I(读取 inode 4)

具体步骤如下:
1. 设置断点:
plaintext (gdb) b ux_lookup Breakpoint 8 at 0xd0854b32: file ux_dir.c, line 367. (gdb) b ux_readdir Breakpoint 9 at 0xd0854350 (gdb) b ux_read_inode Breakpoint 10 at 0xd0855312: file ux_inode.c, line 54.
2. 挂载文件系统:
plaintext # mount -f uxfs /dev/fd0 /mnt Breakpoint 10, ux_read_inode (inode=0xcd235280) at ux_inode.c:54 54 unsigned long ino = inode->i_ino; (gdb) p inode->i_ino $19 = 2
这是读取 inode 2 的请求,是 ux_read_super() 操作的一部分。
3. 执行 ls /mnt
plaintext # ls /mnt Breakpoint 9, 0xd0854350 in ux_readdir (filp=0xcd39cc60, dirent=0xccf0dfa0, filldir=0xc014dab0 <filldir64>)
这是从根目录读取目录条目的请求。
4. 后续调用:
- 调用 ux_lookup() 查找 lost+found ,然后调用 ux_read_inode() 读取 inode 3。
- 再次调用 ux_readdir() ,然后调用 ux_lookup() 查找 passwd ,最后调用 ux_read_inode() 读取 inode 4。

通过这些步骤, ls 命令可以获取目录条目并调用 stat() 系统调用获取文件信息。

5. inode 操作

在文件系统开发中,inode 的操作至关重要,包括从磁盘读取 inode、分配新的 inode、将 inode 写入磁盘以及删除 inode 等操作。

5.1 从磁盘读取 inode

ux_read_inode() 函数(第 1061 到 1109 行)由内核的 iget() 函数调用,用于将 inode 读入内存。通常是由于内核调用 ux_lookup() 而触发。该函数接收一个部分初始化的 inode 结构,其作用是将 inode 读入内存,并将基于磁盘的 inode 的相关字段复制到传入的 inode 结构中。

具体步骤如下:
1. 将 inode 编号转换为文件系统内的块号:
c block = UX_INODE_BLOCK + ino; bh = sb_bread(inode->i_sb, block)
每个 uxfs inode 在磁盘上都有自己的块,inode 0 从 UX_INODE_BLOCK 定义的块号开始。
2. 将 inode 复制到 in-core inode 中由 i_private 字段定义的位置。 i_private 字段在 ux_fs.h 中定义为:
c #define i_private u_generic_ip
3. 在释放缓冲区之前,更新 in-core inode 的字段以反映磁盘上的 inode。
4. 根据文件类型初始化 inode 结构的 i_op i_fop i_mapping 字段:
c if (di->i_mode & S_IFDIR) { inode->i_mode |= S_IFDIR; inode->i_op = &ux_dir_inops; inode->i_fop = &ux_dir_operations; } else if (di->i_mode & S_IFREG) { inode->i_mode |= S_IFREG; inode->i_op = &ux_file_inops; inode->i_fop = &ux_file_operations; inode->i_mapping->a_ops = &ux_aops; }

5.2 分配新的 inode

虽然没有直接导出给内核的操作来分配新的 inode,但在创建目录、常规文件时需要分配新的 inode。由于 uxfs 不支持符号链接,因此在创建常规文件或目录时会分配新的 inode。需要执行以下几个任务:
- 调用 new_inode() 分配一个新的 in-core inode。
- 调用 ux_ialloc() 分配一个新的 uxfs 磁盘 inode。
- 初始化 in-core 和磁盘 inode。
- 标记超级块为脏 —— 空闲 inode 数组和摘要已被修改。
- 标记 inode 为脏,以便将新内容刷新到磁盘。

5.3 将 inode 写入磁盘

每次修改 inode 后,在卸载文件系统之前必须将 inode 写入磁盘。在 uxfs 中,当 inode 被修改时,只需要将 inode 标记为脏:

mark_inode_dirty(inode);

内核将调用 ux_write_inode() 函数将脏 inode 写入磁盘。该函数(第 1115 到 1141 行)通过 superblock_operations 向量导出。

具体步骤如下:
1. 定位 inode 所在的块号,通过将 inode 编号添加到 UX_INODE_BLOCK 来找到。
2. 调用 sb_bread() 将 inode 块读入内存。
3. 将 in-core inode 中感兴趣的字段复制到磁盘 inode,然后将磁盘 inode 复制到缓冲区。
4. 标记缓冲区为脏并释放它。由于缓冲区缓存缓冲区被标记为脏, kupdate 守护进程的定期运行将把它写入磁盘。

5.4 删除 inode

有两种情况需要释放 inode:
- 当需要删除目录时。
- 当 inode 链接计数达到零时。

例如,创建和删除文件的过程如下:

# touch /mnt/file
# rm /mnt/file

rm 命令调用 unlink() 系统调用。对于链接计数为 1 的文件,将导致文件被删除。 ux_delete_inode() 函数(第 1148 到 1168 行)需要执行以下任务:
- 释放文件引用的任何数据块,这涉及更新超级块的 s_nbfree 字段和 s_block[] 字段。
- 通过更新超级块的 s_nbfree 字段和 s_block[] 字段来释放 inode。
- 标记超级块为脏,以便将更改刷新到磁盘。
- 调用 clear_inode() 释放 in-core inode。

综上所述,在 Linux 内核文件系统的开发中,目录查找、路径名解析以及 inode 操作是核心内容。通过深入理解这些操作和函数的工作原理,我们可以更好地开发和优化文件系统。

开发 Linux 内核文件系统

6. 关键操作总结

为了更清晰地理解文件系统开发中的关键操作,下面将这些操作进行总结,形成表格和流程图。

操作类型 涉及函数 主要功能 调用场景
目录读取 ux_readdir() 读取目录条目,将其复制到用户地址空间 执行 ls 命令时
文件名查找 ux_lookup() 查找指定目录中的文件名,返回对应 inode 用户请求查找文件时
inode 读取 ux_read_inode() 从磁盘读取 inode 到内存 内核调用 ux_lookup()
inode 分配 new_inode() ux_ialloc() 分配新的 in-core 和磁盘 inode 创建常规文件或目录时
inode 写入 ux_write_inode() 将脏 inode 写入磁盘 inode 被修改后卸载文件系统前
inode 删除 ux_delete_inode() 释放 inode 及相关数据块 目录删除或 inode 链接计数为 0 时
graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A([开始]):::startend --> B(用户操作):::process
    B --> C{操作类型}:::decision
    C -->|ls 命令| D(ux_readdir()):::process
    C -->|查找文件| E(ux_lookup()):::process
    C -->|创建文件/目录| F(分配 inode):::process
    C -->|修改 inode| G(标记 inode 为脏):::process
    C -->|删除文件/目录| H(ux_delete_inode()):::process
    D --> I(读取目录条目):::process
    E --> J(查找文件名):::process
    J -->|找到| K(iget() 读取 inode):::process
    J -->|未找到| L(返回 EACCES 错误):::process
    F --> M(new_inode()):::process
    F --> N(ux_ialloc()):::process
    G --> O(ux_write_inode()):::process
    H --> P(释放数据块):::process
    H --> Q(释放 inode):::process
    I --> R(复制条目到用户空间):::process
    K --> S(获取文件信息):::process
    M --> T(初始化 in-core inode):::process
    N --> U(初始化磁盘 inode):::process
    O --> V(写入 inode 到磁盘):::process
    P --> W(更新超级块):::process
    Q --> X(更新超级块):::process
    R --> Y([结束]):::startend
    S --> Y
    T --> Y
    U --> Y
    V --> Y
    W --> Y
    X --> Y
7. 实际应用示例

下面通过一个更详细的实际应用示例,展示如何在实际场景中运用上述操作。

假设我们有一个新的 uxfs 文件系统,我们要在该文件系统上进行一系列操作,包括创建文件、删除文件、查看目录等。

  1. 挂载文件系统
    plaintext # mount -f uxfs /dev/fd0 /mnt
    挂载过程中,内核会调用 ux_read_inode() 读取根 inode(inode 2),为后续操作做准备。

  2. 创建文件
    plaintext # touch /mnt/testfile

    • 调用 new_inode() 分配一个新的 in-core inode。
    • 调用 ux_ialloc() 分配一个新的 uxfs 磁盘 inode。
    • 初始化 in-core 和磁盘 inode。
    • 标记超级块为脏,因为空闲 inode 数组和摘要已被修改。
    • 标记新 inode 为脏,以便将新内容刷新到磁盘。
  3. 查看目录
    plaintext # ls /mnt

    • 调用 ux_readdir() 读取根目录条目。
    • 多次调用 ux_readdir() 以读取所有目录条目,将其复制到用户地址空间。
  4. 删除文件
    plaintext # rm /mnt/testfile

    • rm 命令调用 unlink() 系统调用。
    • 如果文件的链接计数为 1,调用 ux_delete_inode()
    • 释放文件引用的数据块,更新超级块的 s_nbfree 字段和 s_block[] 字段。
    • 释放 inode,更新超级块。
    • 标记超级块为脏,将更改刷新到磁盘。
    • 调用 clear_inode() 释放 in-core inode。
8. 注意事项

在开发和使用 uxfs 文件系统时,有一些注意事项需要牢记:

  • 内存管理 :确保在使用完 inode 和缓冲区后及时释放,避免内存泄漏。例如,在 ux_read_inode() 中读取完 inode 后,要正确释放缓冲区。
  • 错误处理 :在各个函数中,要对可能出现的错误进行处理。例如,在 ux_lookup() 中,如果未找到文件名,要返回 EACCES 错误。
  • 数据一致性 :在修改 inode 和超级块时,要确保数据的一致性。例如,在分配或删除 inode 时,要正确更新超级块的相关字段。
  • 性能优化 :可以考虑对一些频繁调用的操作进行优化,例如缓存常用的 inode 信息,减少磁盘 I/O 操作。
9. 总结

开发 Linux 内核文件系统是一个复杂而又有趣的过程。通过深入理解目录查找、路径名解析以及 inode 操作等核心内容,我们可以构建出高效、稳定的文件系统。

在实际开发中,要注意内存管理、错误处理、数据一致性和性能优化等方面的问题。同时,通过实际应用示例,我们可以更好地掌握这些操作的使用方法。

希望本文能为你在 Linux 内核文件系统开发方面提供有价值的参考,帮助你更好地完成相关开发任务。

以上内容涵盖了 Linux 内核文件系统开发中的关键操作、实际应用示例以及注意事项等方面,通过表格、流程图和详细的步骤说明,希望能让你对文件系统开发有更深入的理解。

本项目构建于RASA开源架构之上,旨在实现一个具备多模态交互能力的智能对话系统。该系统的核心模块涵盖自然语言理解、语音转文本处理以及动态对话流程控制三个主要方面。 在自然语言理解层面,研究重点集中于增强连续对话中的用户目标判定效能,并运用深度神经网络技术提升关键信息提取的精确度。目标判定旨在解析用户话语背后的真实需求,从而生成恰当的反馈;信息提取则专注于从语音输入中析出具有特定意义的要素,例如个体名称、空间位置或时间节点等具体参数。深度神经网络的应用显著优化了这些功能的实现效果,相比经典算法,其能够解析更为复杂的语言结构,展现出更优的识别精度与更强的适应性。通过分层特征学习机制,这类模型可深入捕捉语言数据中隐含的语义关联。 语音转文本处理模块承担将音频信号转化为结构化文本的关键任务。该技术的持续演进大幅提高了人机语音交互的自然度与流畅性,使语音界面日益成为高效便捷的沟通渠道。 动态对话流程控制系统负责维持交互过程的连贯性与逻辑性,包括话轮转换、上下文关联维护以及基于情境的决策生成。该系统需具备处理各类非常规输入的能力,例如用户使用非规范表达或对系统指引产生歧义的情况。 本系统适用于多种实际应用场景,如客户服务支持、个性化事务协助及智能教学辅导等。通过准确识别用户需求并提供对应信息或操作响应,系统能够创造连贯顺畅的交互体验。借助深度学习的自适应特性,系统还可持续优化语言模式理解能力,逐步完善对新兴表达方式与用户偏好的适应机制。 在技术实施方面,RASA框架为系统开发提供了基础支撑。该框架专为构建对话式人工智能应用而设计,支持多语言环境并拥有活跃的技术社区。利用其内置工具集,开发者可高效实现复杂的对话逻辑设计与部署流程。 配套资料可能包含补充学习文档、实例分析报告或实践指导手册,有助于使用者深入掌握系统原理与应用方法。技术文档则详细说明了系统的安装步骤、参数配置及操作流程,确保用户能够顺利完成系统集成工作。项目主体代码及说明文件均存放于指定目录中,构成完整的解决方案体系。 总体而言,本项目整合了自然语言理解、语音信号处理与深度学习技术,致力于打造能够进行复杂对话管理、精准需求解析与高效信息提取的智能语音交互平台。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值