开发 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 文件系统,我们要在该文件系统上进行一系列操作,包括创建文件、删除文件、查看目录等。
-
挂载文件系统
plaintext # mount -f uxfs /dev/fd0 /mnt
挂载过程中,内核会调用ux_read_inode()读取根 inode(inode 2),为后续操作做准备。 -
创建文件
plaintext # touch /mnt/testfile-
调用
new_inode()分配一个新的 in-core inode。 -
调用
ux_ialloc()分配一个新的 uxfs 磁盘 inode。 - 初始化 in-core 和磁盘 inode。
- 标记超级块为脏,因为空闲 inode 数组和摘要已被修改。
- 标记新 inode 为脏,以便将新内容刷新到磁盘。
-
调用
-
查看目录
plaintext # ls /mnt-
调用
ux_readdir()读取根目录条目。 -
多次调用
ux_readdir()以读取所有目录条目,将其复制到用户地址空间。
-
调用
-
删除文件
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 内核文件系统开发中的关键操作、实际应用示例以及注意事项等方面,通过表格、流程图和详细的步骤说明,希望能让你对文件系统开发有更深入的理解。
超级会员免费看
2676

被折叠的 条评论
为什么被折叠?



