37、Linux内核文件系统开发:uxfs详解

Linux内核文件系统开发:uxfs详解

1. 文件创建与链接管理

在创建文件之前,许多UNIX实用程序会调用 stat() 系统调用来检查文件是否存在。这会促使内核调用 ux_lookup() 函数。若文件名不存在,内核会在dcache中存储一个负dentry。这样,后续再次对同一文件调用 stat() 时,内核无需再次访问文件系统就能知道文件不存在。

例如,使用 cp 命令将文件复制到 foo 时, strace 命令的输出如下:

lstat64("foo", 0xbffff8a0) = -1 ENOENT (No such file or directory)
stat64("file", {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
open("file", O_RDONLY|O_LARGEFILE) = 3
open("foo", O_WRONLY|O_CREAT|O_LARGEFILE, 0100644) = 4

cp 命令在调用 open() 创建新文件之前,会对两个文件都调用 stat() 系统调用。

以下是 cp 命令调用 stat() 系统调用时对 ux_lookup() 的调用示例:

Breakpoint 5, ux_lookup (dip=0xcd73cba0, dentry=0xcb5ed3a0) 
at ux_dir.c:367
367        struct ux_inode     *uip = (struct ux_inode *)
(gdb) bt
#0  ux_lookup (dip=0xcd73cba0, dentry=0xcb5ed3a0) at ux_dir.c:367
#1  0xc01482c0 in real_lookup (parent=0xcb5ed320, name=0xc97ebf5c,
flags=0)
    at namei.c:305
#2  0xc0148ba4 in link_path_walk (name=0xcb0f700b "", nd=0xc97ebf98)
    at namei.c:590
#3  0xc014943a in __user_walk (
 name=0xd0856920 "\220D\205–,K\205–ÃK\205–<L\205–", 
flags=9, nd=0xc97ebf98)
    at namei.c:841
#4  0xc0145807 in sys_stat64 (filename=0x8054788 "file", 

statbuf=0xbffff720, flags=1108542220) 
at stat.c:337
#5  0xc010730b in system_call ()

内核在调用 ux_lookup() 之前会分配dentry。

由于文件不存在, cp 命令会调用 open() 来创建文件,这会使内核调用 ux_create() 函数:

Breakpoint 6, 0xd0854494 in ux_create 
(dip=0xcd73cba0, dentry=0xcb5ed3a0, mode=33188)
(gdb) bt
#0  0xd0854494 in ux_create (dip=0xcd73cba0, dentry=0xcb5ed3a0, 
mode=33188)
#1  0xc014958f in vfs_create (dir=0xcd73cba0, dentry=0xcb5ed3a0, 
mode=33188)
    at namei.c:958
#2  0xc014973c in open_namei (pathname=0xcb0f7000 "foo", 
flag=32834, 
    mode=33188, nd=0xc97ebf74) at namei.c:1034
#3  0xc013cd67 in filp_open (filename=0xcb0f7000 "foo", 
flags=32833, 
    mode=33188) at open.c:644
#4  0xc013d0d0 in sys_open (filename=0x8054788 "foo", 
flags=32833, mode=33188) 
at open.c:788
#5  0xc010730b in system_call ()

传递给 ux_create() 的dentry地址与传递给 ux_lookup() 的相同。若文件创建成功,dentry将更新以引用新创建的inode。

ux_create() 函数(第629 - 691行)需要执行以下几个任务:
- 调用 ux_find_entry() 检查文件是否存在。若存在,则返回错误。
- 调用内核的 new_inode() 例程分配一个新的内核inode。
- 调用 ux_ialloc() 分配一个新的uxfs inode。
- 调用 ux_diradd() 将新文件名添加到父目录。
- 初始化新inode,并对新inode和父inode调用 mark_dirty_inode() ,确保它们会被写入磁盘。

ux_ialloc() 函数(第413 - 434行)的工作方式很直接,它操作uxfs超级块的字段。检查是否还有可用的inode( s_nifree > 0 ),然后遍历 s_inode[] 数组,直到找到一个空闲条目。将其标记为 UX_INODE_INUSE ,减少 s_ifree 字段的值,并返回inode编号。

ux_diradd() (第485 - 539行)函数用于将新文件名添加到父目录,需要处理两种情况:
- 现有目录块中有空间:可以直接写入新文件的名称和inode编号。将包含新条目的缓冲区标记为脏并释放。
- 现有目录块中没有空间:需要调用 ux_block_alloc() 函数(第441 - 469行)为新目录分配一个新块来存储名称和inode编号。

创建硬链接涉及向文件系统添加新文件名,并增加其所引用inode的链接计数。在某些方面,其路径与 ux_create() 非常相似,但不创建新的uxfs inode。

ln 命令会调用 stat() 系统调用来检查两个文件名是否已经存在。由于链接名不存在,会创建一个负dentry。然后 ln 命令调用 link() 系统调用,通过 ux_link() 进入文件系统。 ux_link() 的原型如下:

int
ux_link(struct dentry *old, struct inode *dip, struct dentry *new);

执行 $ ln filea fileb 命令时, old dentry引用 filea new fileb 的负dentry。

ux_link() 除了调用 ux_diradd() 将新名称添加到父目录外,还会增加inode的链接计数,调用 d_instantiate() 将负dentry映射到inode,并将其标记为脏。

unlink() 系统调用由 ux_unlink() 函数(第893 - 902行)管理。该函数只需减少inode的链接计数并将inode标记为脏。若链接计数达到零,内核将调用 ux_delete_inode() 从文件系统中实际删除inode。

以下是文件创建和链接管理的流程图:

graph TD;
    A[调用stat()] --> B{文件是否存在};
    B -- 不存在 --> C[存储负dentry];
    B -- 存在 --> D[继续操作];
    C --> E[调用open()];
    E --> F[调用ux_create()];
    F --> G[调用ux_find_entry()];
    G -- 存在 --> H[返回错误];
    G -- 不存在 --> I[调用new_inode()];
    I --> J[调用ux_ialloc()];
    J --> K[调用ux_diradd()];
    K --> L[初始化inode并标记为脏];
    M[执行ln命令] --> N[调用stat()];
    N --> O{文件名是否存在};
    O -- 不存在 --> P[创建负dentry];
    P --> Q[调用link()];
    Q --> R[调用ux_link()];
    R --> S[调用ux_diradd()];
    R --> T[增加inode链接计数];
    T --> U[调用d_instantiate()];
    U --> V[标记为脏];
    W[执行unlink()] --> X[调用ux_unlink()];
    X --> Y[减少inode链接计数];
    Y --> Z{链接计数是否为零};
    Z -- 是 --> AA[调用ux_delete_inode()];
    Z -- 否 --> AB[继续];
2. 目录的创建与删除

目录创建与文件创建略有不同,内核会自行执行查找操作,而不是由应用程序先调用 stat() 。例如,创建目录时:

Breakpoint 5, ux_lookup (dip=0xcd73cba0, dentry=0xcb5ed420) 
at ux_dir.c:367
367        struct ux_inode     *uip = (struct ux_inode *)
(gdb) bt
#0  ux_lookup (dip=0xcd73cba0, dentry=0xcb5ed420) at ux_dir.c:367
#1  0xc01492f2 in lookup_hash (name=0xc97ebf98, base=0xcb5ed320) 
at namei.c:781
#2  0xc0149cd1 in lookup_create (nd=0xc97ebf90, is_dir=1) 
at namei.c:1206
#3  0xc014a251 in sys_mkdir (pathname=0xbffffc1c "/mnt/dir", mode=511)
    at namei.c:1332
#4  0xc010730b in system_call ()

由于文件名不存在(假设之前不存在),会创建一个负dentry,然后传递给 ux_mkdir() (第698 - 780行):

Breakpoint 7, 0xd08546d0 in ux_mkdir (dip=0xcd73cba0, dentry=0xcb5ed420,
 mode=493)
(gdb) bt
#0  0xd08546d0 in ux_mkdir (dip=0xcd73cba0, dentry=0xcb5ed420, mode=493)
#1  0xc014a197 in vfs_mkdir (dir=0xcd73cba0, dentry=0xcb5ed420,
mode=493)
    at namei.c:1307
#2  0xc014a282 in sys_mkdir (pathname=0xbffffc1c "/mnt/dir", mode=511)
    at namei.c:1336
#3  0xc010730b in system_call ()

ux_mkdir() 的初始步骤与 ux_create() 类似:
- 调用 new_inode() 分配一个新的内核inode。
- 调用 ux_ialloc() 分配一个新的uxfs inode,并调用 ux_diradd() 将新目录名添加到父目录。
- 初始化内核inode和uxfs磁盘inode。

此外,还需要为新目录分配一个块来存储 "." ".." 条目。调用 ux_block_alloc() 函数,将返回的块号存储在 i_addr[0] 中,将 i_blocks 设置为1,将inode的大小( i_size )设置为512(数据块的大小)。

要删除目录条目,调用 ux_rmdir() 函数(第786 - 831行)。该函数的第一步是检查目录inode的链接计数。若大于2,则目录不为空,返回错误。

ux_rmdir() 的堆栈回溯如下:

Breakpoint 8, 0xd0854a0c in ux_rmdir (dip=0xcd73cba0, dentry=0xcb5ed420)
(gdb) bt 
#0  0xd0854a0c in ux_rmdir (dip=0xcd73cba0, dentry=0xcb5ed420)
#1  0xc014a551 in vfs_rmdir (dir=0xcd73cba0, dentry=0xcb5ed420) 
at namei.c:1397
#2  0xc014a696 in sys_rmdir (pathname=0xbffffc1c "/mnt/dir") 
at namei.c:1443
#3  0xc010730b in system_call ()

ux_rmdir() 需要执行以下任务:
- 调用 ux_dirdel() 从父目录中删除目录名。
- 释放所有目录块。
- 通过增加超级块的 s_nifree 字段并标记 s_nifree[] 中的槽位来释放inode。

dirdel() 函数(第545 - 576行)遍历每个目录块,将找到的每个 ux_dirent 结构的 d_ino 字段与传递的名称进行比较。若找到匹配项,则将 d_ino 字段设置为0,表示该槽位空闲。

以下是目录创建和删除的操作步骤列表:
| 操作 | 步骤 |
| — | — |
| 创建目录 | 1. 内核执行查找操作
2. 创建负dentry
3. 调用 ux_mkdir()
4. 调用 new_inode() 分配内核inode
5. 调用 ux_ialloc() 分配uxfs inode
6. 调用 ux_diradd() 添加目录名到父目录
7. 初始化inode
8. 分配块存储 "." ".." 条目 |
| 删除目录 | 1. 调用 ux_rmdir()
2. 检查目录inode的链接计数
3. 若链接计数大于2,返回错误
4. 调用 ux_dirdel() 删除目录名
5. 释放所有目录块
6. 释放inode |

3. uxfs中的文件I/O

文件I/O通常是文件系统实现中最具挑战性的部分之一,为提高文件系统性能,需要投入大量时间。在Linux中,可以轻松实现一个功能完备的文件系统,同时在I/O路径上花费较少时间,因为Linux有许多通用函数可用于处理与页面缓存和缓冲区缓存的交互。

以下是文件I/O的主要入口点,通过 file_operations 结构导出:

struct file_operations ux_file_operations = {
        llseek:    generic_file_llseek,
        read:      generic_file_read,
        write:     generic_file_write,
        mmap:      generic_file_mmap,
};

对于所有与文件I/O相关的主要操作,文件系统会调用Linux的通用文件I/O例程。地址空间相关操作如下:

struct address_space_operations ux_aops = {
        readpage:         ux_readpage,
        writepage:        ux_writepage,
        sync_page:        block_sync_page,
        prepare_write:    ux_prepare_write,
        commit_write:     generic_commit_write,
        bmap:             ux_bmap,
};

对于此向量中定义的所有函数,uxfs也会调用通用内核例程。例如, ux_readpage() 函数(第976 - 980行):

int
ux_readpage(struct file *file, struct page *page)
{
        return block_read_full_page(page, ux_get_block);
}

在介绍文件I/O进入文件系统的流程之前,先了解 ux_get_block() (第929 - 968行)的工作原理:

int
ux_get_block(struct inode *inode, long block, 
             struct buffer_head *bh_result, int create)

当内核需要访问文件中尚未缓存的部分时,会调用 ux_getblock() 函数。 block 参数是文件内的逻辑块, create 参数指示内核是要读取还是写入文件。若 create 为0,内核是读取文件;若 create 为1,文件系统需要在 block 引用的偏移处分配存储。

block 为0时,文件系统需要填充 buffer_head 的相应字段:

bh_result->b_dev = inode->i_dev;
bh_result->b_blocknr = uip->i_addr[block];

内核将执行实际的数据读取操作。若 create 为1,文件系统需要调用 ux_block_alloc() 分配一个新的数据块,并设置 i_addr[] 的相应槽位以引用新块。分配完成后,需要在内核执行I/O操作之前初始化 buffer_head 结构。

3.1 从常规文件读取

文件系统对于从常规文件读取没有特定操作,在 file_operations 向量中指定了 generic_file_read() 函数。

设置 ux_get_block() 断点,使用 cat 程序从uxfs文件系统读取 passwd 文件。 passwd 文件有三个数据块,第一次触发断点时:

Breakpoint 1, ux_get_block (inode=0xcf23a420, 
block=0, bh_result=0xc94f4740, create=0) 
at ux_file.c:21
21        struct super_block *sb = inode->i_sb;
(gdb) bt
#0  ux_get_block (inode=0xcf23a420, block=0, bh_result=0xc94f4740,
create=0)
    at ux_file.c:21
#1  0xc0140b1f in block_read_full_page (page=0xc1250fc0, 
    get_block=0xd0855094 <ux_get_block>) at buffer.c:1781
#2  0xd08551ba in ux_readpage (file=0xcd1c9360, page=0xc1250fc0)
    at ux_file.c:67
#3  0xc012e773 in do_generic_file_read (filp=0xcd1c9360, 
ppos=0xcd1c9380, 
    desc=0xc96d1f5c, actor=0xc012eaf0 <file_read_actor>) 
at filemap.c:1401
#4  0xc012ec72 in generic_file_read (filp=0xcd1c9360, buf=0x804eb28 "", 
    count=4096, ppos=0xcd1c9380) at filemap.c:1594
#5  0xc013d7c8 in sys_read (fd=3, buf=0x804eb28 "", count=4096)
    at read_write.c:162
#6  0xc010730b in system_call ()

这里有两个uxfs入口点:调用 ux_readpage() 将整页数据读入页面缓存;调用 ux_get_block() 。由于文件I/O以系统页面大小为倍数进行, block_read_full_page() 函数被调用来填充页面。文件只有三个512字节的块,不足以填满一个页面(4KB),内核需要尽可能多地读取数据,然后将页面的其余部分零填充。

后续调用 ux_get_block() 时, block 参数分别为1和2,依次读取文件的下一个512字节和最后的512字节。

对于uxfs,从文件读取非常简单,编写好 get_block() 函数后,文件系统几乎无需其他操作。

3.2 写入常规文件

写入文件的机制与读取常规文件类似。使用 cp 命令将 passwd 文件复制到uxfs文件系统,设置 ux_get_block() 断点,第一次触发断点时:

Breakpoint 1, ux_get_block (inode=0xcd710440, 
block=0, bh_result=0xc96b72a0, create=1) 
at ux_file.c:21
21        struct super_block *sb = inode->i_sb;
(gdb) bt
#0  ux_get_block (inode=0xcd710440, block=0, 
bh_result=0xc96b72a0, create=1)
    at ux_file.c:21
#1  0xc014074b in __block_prepare_write (inode=0xcd710440, 
page=0xc125e640, from=0, to=1024,
get_block=0xd0855094 <ux_get_block>) 
at buffer.c:1641
#2  0xc0141071 in block_prepare_write (page=0xc125e640, from=0, to=1024, 
    get_block=0xd0855094 <ux_get_block>) at buffer.c:1960
#3  0xd08551dd in ux_prepare_write (file=0xcd1c9160, page=0xc125e640,
from=0, to=1024) 
at ux_file.c:74
#4  0xc013085f in generic_file_write (file=0xcd1c9160, 
buf=0xbffff160 
"root:x:0:0:root:/root:/bin/bash\nbin:x:1:1:bin:/bin:/sbin/nologin\ndaem
on:x:2:2:daemon:/sbin:/sbin/nologin\nadm:x:3:4:adm:/var/adm:/sbin/nologi
n\nlp:x:4:7:lp:/var/spool/lpd:/sbin/nologin\nsync:x:5:0:sync:/"..., 
    count=1024, ppos=0xcd1c9180) at filemap.c:3001
#5  0xc013d8e8 in sys_write (fd=4, 
    buf=0xbffff160 
"root:x:0:0:root:/root:/bin/bash\nbin:x:1:1:bin:/bin:/sbin/nologin\ndaem
on:x:2:2:daemon:/sbin:/sbin/nologin\nadm:x:3:4:adm:/var/adm:/sbin/nologi
n\nlp:x:4:7:lp:/var/spool/lpd:/sbin/nologin\nsync:x:5:0:sync:/"..., 
    count=1024) at read_write.c:188
#6  0xc010730b in system_call ()

此时 create 标志设置为1,表示需要为文件分配一个块。分配块后,初始化 buffer_head ,将 passwd 文件的前512字节复制到缓冲区。若缓冲区和inode标记为脏,它们将被刷新到磁盘。

后续触发断点时, block 参数分别为1和2,分别为文件分配另一个块以覆盖512 - 1023的范围和分配最后一个所需的块。

与读取常规文件一样,写入常规文件对于文件系统来说也是一个容易实现的功能。

3.3 内存映射文件

虽然不详细描述内存映射文件在Linux内核中的工作机制,但可以展示文件系统如何通过与读取和写入常规文件相同的机制来支持映射文件。

file_operations 向量中,uxfs请求调用 generic_file_mmap() 函数,文件系统只需提供 get_block() 接口。

设置 ux_get_block() 断点,将文件映射为只读访问,触摸映射的第一个地址会触发页面错误。进入 ux_get_block() 时的堆栈跟踪如下:

Breakpoint 1, ux_get_block (inode=0xcf23a420, 
block=0, bh_result=0xc94bbba0, create=0) 
at ux_file.c:21
21        struct super_block *sb = inode->i_sb;
(gdb) bt
#0  ux_get_block (inode=0xcf23a420, block=0, 
bh_result=0xc94bbba0, create=0)
    at ux_file.c:21
#1  0xc0140b1f in block_read_full_page (page=0xc1238340, 
get_block=0xd0855094 <ux_get_block>) 
at buffer.c:1781
#2  0xd08551ba in ux_readpage (file=0xcd1c97e0, page=0xc1238340)
    at ux_file.c:67
#3  0xc012dd92 in page_cache_read (file=0xcd1c97e0, offset=3441203168)
    at filemap.c:714
#4  0xc012ddef in read_cluster_nonblocking (file=0xcd1c97e0, 

 offset=3475219664, filesize=1) 
at filemap.c:739
#5  0xc012f389 in filemap_nopage (area=0xc972a300, address=1073823744,
unused=0) 
at filemap.c:1911
#6  0xc012b512 in do_no_page (mm=0xcf996d00, vma=0xc972a300, 
 address=1073823744, write_access=0, page_table=0xc91e60a0) 
at memory.c:1249
#7  0xc012b76c in handle_mm_fault (mm=0xcf996d00, vma=0xc972a300, 

address=1073823744, write_access=0) 
at memory.c:1339
#8  0xc011754a in do_page_fault (regs=0xc952dfc4, error_code=4) 
at fault.c:263
#9  0xc01073fc in error_code ()

内核不是通过系统调用进入,而是响应错误进入。由于用户地址空间中没有映射文件的页面,当进程尝试访问文件时,会发生页面错误。内核确定内存页面的映射位置,然后从相应文件填充页面。

进入 ux_readpage() 函数,该函数会回调到内存管理器。为了填充数据页面,内核会反复调用 ux_get_block() ,直到读取一整页数据或到达文件末尾。若到达文件末尾,内核需要将页面零填充,以便当进程访问同一页面但超出文件末尾时,读取到零。

以下是文件I/O操作的总结表格:
| 操作类型 | 关键函数调用 | 主要步骤 |
| — | — | — |
| 读取常规文件 | generic_file_read ux_get_block ux_readpage | 1. 调用 generic_file_read
2. 触发 ux_readpage
3. 多次调用 ux_get_block 读取数据块
4. 零填充不足一页的数据 |
| 写入常规文件 | generic_file_write ux_get_block ux_prepare_write | 1. 调用 generic_file_write
2. 触发 ux_prepare_write
3. 多次调用 ux_get_block 分配数据块并写入数据
4. 标记缓冲区和inode为脏并刷新到磁盘 |
| 内存映射文件 | generic_file_mmap ux_get_block ux_readpage | 1. 调用 generic_file_mmap
2. 触发页面错误
3. 进入 ux_readpage
4. 多次调用 ux_get_block 填充页面数据 |

以下是文件I/O操作的流程图:

graph TD;
    A[读取常规文件] --> B[调用generic_file_read];
    B --> C[调用ux_readpage];
    C --> D[调用ux_get_block];
    D --> E{是否读完文件};
    E -- 否 --> D;
    E -- 是 --> F[零填充页面];
    G[写入常规文件] --> H[调用generic_file_write];
    H --> I[调用ux_prepare_write];
    I --> J[调用ux_get_block];
    J --> K{是否写完文件};
    K -- 否 --> J;
    K -- 是 --> L[标记缓冲区和inode为脏并刷新];
    M[内存映射文件] --> N[调用generic_file_mmap];
    N --> O[触发页面错误];
    O --> P[调用ux_readpage];
    P --> Q[调用ux_get_block];
    Q --> R{是否读完一页或到文件末尾};
    R -- 否 --> Q;
    R -- 是 --> S{是否到文件末尾};
    S -- 是 --> T[零填充页面];
    S -- 否 --> U[继续];

综上所述,uxfs在文件创建、链接管理、目录操作和文件I/O等方面都有其特定的实现方式和流程,通过合理利用Linux内核的通用函数,能够较为高效地完成各项操作。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值