Linux Ext3 目录索引及其nfs readdir 过程的实现
张 前 锋
( qfzhanglinux@gmail.com)
Linux ext3 文件系统目录索引的实现
1. ext3 文件系统采用目录索引技术来加快对大的目录文件的访问(比如在保存10万个文
件的某目录下查找一个指定的文件). Ext3目录索引技术根据每个目录项名称的hash
值来确定这个文件放在Htree的哪个页节点上.
2. 每个目录文件的HTree结构包括索引块或目录项块: 索引块包含索引项(index entry)的
数组,每个索引项包含一个hash key值和一个指向目录项块的逻辑指针; 目录项块
包含正常的目录项(directory entry)列表,和不采用目录索引技术时一样. 索引项的
hash key部分是其指向的目录块上所有目录项对应的hash key的最小值,即key下界.
每个索引块的第一个索引项的hash字段被作了特殊的利用,其记录的是该索引块的
当前索引项数和索引项数的上限即(count,limit),以加速代码的处理. 为了和普通的目
录块兼容,索引块的头部还有一个fake_dirent的特殊目录项,其reclen为blocksize,
以让整个索引块看起来就象一个大的空目录项. root索引块更特殊,其总是位于目录
文件的第0块,而且其看起来就象是一个仅包含 “.”和 “..” 两个目录项的普通目录块.
Root索引和普通索引块的数据结构如下
struct dx_root /* 根索引块 */
{
struct fake_dirent dot;
char dot_name[4];
struct fake_dirent dotdot;
char dotdot_name[4];
struct dx_root_info
{
__le32 reserved_zero;
u8 hash_version;
u8 info_length; /* 8 */
u8 indirect_levels;
u8 unused_flags;
}
info;
struct dx_node /* 普通索引块 */
{
struct fake_dirent fake;
struct dx_entry entries[0];
}
struct fake_dirent /* faked 目录项 */
{
__le32 inode;
__le16 rec_len;
u8 name_len;
u8 file_type;
};
/* 索引项 */
struct dx_entry
/* 第一个索引项的hash 部分 */
struct dx_countlimit
struct dx_entry entries[0];
}
{
__le32 hash;
__le32 block;
}
{
__le16 limit;
__le16 count;
}
3. 目录项块中的目录项是按自然的FIFO及空档插入的顺序放置的, 当目录项块满时会
导致split, 一个目录块变成两个目录块,其对应的一个index entry 变成两个index
entries, 可能进一部导致索引块被split.
4. Ext3 支持一级或二级目录索引块, 当root索引块满时,就会由一级索引变为二级索
引. 当发生目录项块split时,前一个块的最后一个directory entry的hash值和后一个
块的第一个directory entry 的hash值可能是相同的,这种情况叫做collision, Ext3能
处理这种冲突,在种情况搜素一个目录项时,可能需要读取两个目录项块.
Linux ext3 文件系统对readdir 的支持
1.无论是本地用户进程执行getdents()系统调用还是Linux 的nfs服务器端对READDIR请
求的响应,最终会调用ext3_dx_readdir() 来读取带有索引(indexed)的ext3目录的内容
(the entries). 其中本地用户的getdents()系统调用在内核中调用ext3_dx_readdir()的代
码路径如下
sys_getdents() => vfs_readdir() => ext3_readdir() => ext3_dx_readdir()
Nfs服务器 端执行如下路径调用ext3_dx_readdir()
nfsd_proc_readdir() => nfsd_readdir() => vfs_readdir() => ext3_readdir() => ext3_dx_readdir()
2. ext3_dx_readdir采用几个重要的数据结构来实现nfs或本地的READDIR操作, 见下图。
struct dir_private_info {
struct rb_root root;
struct rb_node *curr_node;
struct fname *extra_fname;
loff_t last_pos;
__u32 curr_hash;
__u32 curr_minor_hash;
__u32 next_hash;
}
struct fname {
__u32 hash;
__u32 minor_hash;
struct rb_node rb_hash;
struct fname *next;
__u32 inode;
__u8 name_len;
__u8 file_type;
char name[0];
}
其中struct fname 用来表示一个目录entry的信息在被READDIR读入到用户空间或nfs
的Reply包之前的存放位置. 单个的struct fname只能代表一个目录entry, 但多个具有相
同hash值的目录entries的struct fname通过其next字段组织成一个单向链表. Struct
dir_private_info 用来将一个目录文件的全部entries组织成Red-Black树,其root字段指
向rbtree的根, rbtree的节点由各struct fname结构的rb_hash字段形成,当然具有相同
hash值的entries只需要一个rbtree节点。 Struct dir_private_info由其打开的目录文件
struct file 的private 字段指向,即每个被打开的被索引的目录文件都可有一个struct
dir_private_info. Struct dir_private_info 中还有一些字段,用以存放READDIR操作执行
过程中的状态, 如curr_hash, last_pos, curr_node, extra_fname.
3.在对一个被打开的目录文件第一次调用ext3_dx_readdir时(如以cookie=0对一个目录
文件执行READDIR操作时), ext3_dx_readdir会调用ext3_htree_fill_tree()将全部目录项
读入内存,并创建一个由各个struct fname为节点组成的rbtree, 以便READDIR操作能
按hash值的顺序读取该目录的entries.
4. ext3_htree_fill_tree的工作就是通过索引htree的组织方式,以hash值为序遍历目录的
索引块,从而找到全部的目录块. 对于每一个目录块,ext3_htree_fill_tree 调用
htree_dirblock_to_tree()读取其中的目录entries,用其信息产生struct fname,加入到
该打开目录的rbtree 中。 对于单个目录entry, htree_dirblock_to_tree 调用
ext3_htree_store_dirent()来产生对应的struct fname, 并将该struct fname 加入到
rbtree(作为一个节点)或加入到已有节点的单向链表中. 总之rbtree的不同节点的struct
fname具有不同的hash值,同一个节点的单向链表的各struct fname具有相同的hash
值. ext3_htree_fill_tree 也可能会直接调用ext3_htree_store_dirent将一些特殊的目录
entries 如 “.”和 “..” 加入到打开目录的rbtree中.
5. 只要rbtree已经建立, ext3_dx_readdir就会通过while循环遍历rbtree的节点,对每一
个节点,调用call_filldir()将其struct fname的单向链表中的各个entry的信息生成到用
户空间或nfs Reply 包中去. 如果call_filldir 调用失败,则ext3_dx_readdir 退出.
call_filldir遍历struct fname的单向链表,调用一个fill_dir钩子函数将具有相同hash值
的目录entries 的内容生成到目的缓冲区. ext3 本地的READDIR 操作使用一个通用的
filldir()函数, nfs2 v2的READDIR操作使用的fill_dir函数是nfssvc_encode_entry().
6. call_filldir()调用fill_dir的失败是一个很重要的行为. 由于fill_dir钩子函数所做的工作只
是将一个struct fname的内容生成到目标缓冲区中,纯粹的内存拷贝,赋值操作, 所以
fill_dir失败的唯一情况是目标缓冲区已满. 在fill_dir失败的情况下,call_filldir会将正在
操作的rbtree 节点的hash 值保存到目录的struct file 的f_pos 字段,并把该节点的
struct fname 单向链表的剩余未遍历的链表地址保存到 struct dir_private_info 的
exta_fname 指针中. fill_dir 的失败使call_fill()失败退出,进一步使ext3_dx_readdir 从
while 循环中退出,标志本次READDIR 操作结束. ext3_dx_readdir 在退出时,会将
struct file的f_pos字段保存到struct dir_private_info的last_pos.
7.在使用目录索引的情况下,打开目录的struct file的f_pos字段并不是象访问普通文件
一样,表示当前正在访问的文件线性偏移, 而是表示当前正要访问的目录项(以struct
fname形式存在)的hash值, struct dir_private_info 的last_pos字段具有类似的含义,
其保存上次ext3_dx_readdir 调用中最近操作的 struct fname 的hash 值. Struct
dir_private_info的curr_node保存ext3_dx_readdir最近访问的rbtree的struct fname节
点指针. 这些关于该打开目录的当前访问位置信息,在下一次READDIR操作执行时,
会很有用.
8. ext3_dx_readdir在开始执行时,会检查struct dir_private_info的extra_fname指针,如
果其非空,则立即调用call_filldir处理extra_fname指向的单向链表,把上轮call_filldir
调用没处理完的内容处理完,然后再进入ext3_dx_readdir的while主循环,继续处理上
轮调用后剩下的其他rbtree节点, 直到所有rbtree节点被处理完毕或call_filldir调用失
败.
9. ext3_dx_readdir在开始执行时,会比较struct file的f_pos和struct dir_private_info 的
last_pos值,如果不等,则表示客户端(用户空间或nfs client)调用了lseek()类操作,企
图重新选择READDIR操作的开始位置,则ext3_dx_readdir会放弃已有的rbtree, 调用
ext3_htree_fill_tree从f_pos指定的hash值开始重新建构一个rbtree, 在此基础上开始
新一轮的call_filldir操作, 并且hash值比f_pos小的entries在此轮就不用再处理了
10. ext3_dx_readdir在开始执行时,也会检查当前目录文件对应的struct inode, 如果有变
化,说明该目录文件最近被修改,则ext3_dx_readdir 会放弃已有的rbtree, 调用
ext3_htree_fill_tree从当前的hash值位置重新建构一个rbtree, 在此基础上开始新一轮
的call_filldir操作, 并且hash值比curr_hash值小的entries在此轮就不用再处理了
关于 ”.” 和 “..” 两个目录项被READDIR 读出的次序
1. 在ext3文件系统关闭了dir_index功能的情况下, 所有目录想都是按照其在目录文件中
的线性偏移顺序读出的,其中 “.” 是第一个目录项,“..”是第二个目录项,所以NFS客
户端或本地的getdents()操作总是能先获取 “.”, 然后获取 “..”
2. 在ext3文件系统激活了dir_index功能的情况下,所有目录项都是按照其hash值的顺
序被加入到rbtree, 按hash值从小到大的次序被NFS客户端或getdents()读取的. 但
ext3 对 “.” 和 “..” 两个目录项的处理比较特殊:
• 如果执行READDIR操作时,该目录文件的大小还没有超过一个磁盘块,则所以目
录项全部在第0 块, 则ext3_htree_fill_tree() 函数的代码会直接调用
htree_dirblock_to_tree() 函数将该块的全部目录项加入到rbtree 中 , 在
htree_dirblock_to_tree()中,全部目录项的hash 值,包括 “.” 和 “..”, 都是通过
ext3fs_dirhash()函数计算出来的. ext3fs_dirhash算出的hash值,由一个目录项名
和文件系统超级块中保存的seed值共同决定, 所以相同的文件名字符串,在不同
的文件系统中算出的hash值可能不一样。在本人的测试系统中, “..” 的hash值
比 “.” 小,所以 “..” 比 “.” 先读出
• 如果执行READDIR操作时,该目录文件的大小已经超过一个磁盘块, 则第0块变
成了纯粹的root索引块,htree_dirblock_to_tree()对其头部的 “.”和 “..” 两个目录项
需直接调用ext3_htree_store_dirent()读取,并且为 “.” 指定了hash值 “0”, 为 “..”
制定了hash 值 “2”, 所以在加入到rbtree中后,”.” 总是在 “..” 的前面.
NFS V2 READDIR 操作的实现
1. 每次nfs READDIR 请求都会包括目录filehandle,count,cookie 三个参数。其中
filehandle指定要读取的目录文件,count指示要读取的目录内容大小,cookie参数指
示要读取的目录项起点位置. 在不支持目录索引的情况下,cookie参数指示的是目录
项在目录文件中的线性偏移;在支持目录索引的情况下,cookie参数指示的是目录项
的hash值
2. 在nfs的通信层收到READDIR请求后,Linux nfs服务器代码会调用nfs_proc_readdir
来处理请求,该函数会调用nfsd_readdir(). nfsd_readdir() 在往下调用vfs_readdir()之
前,会调用vfs_llseek(file, offset, 0)来设置该目录struct file的f_pos, 其中offset就
是来自nfs READDIR 的cookie 参数,也就是说该调用告诉ext3_dx_readdir 本次
READDIR 操作的启点,刚开始时,这个offset 一般是0, 也就是告诉ext3 从最小的
hash 值开始读目录项. nfsd_readdir()在调用完vfs_readdir 操作之后,会调用
offset=vfs_llseek(file,0,1) 读取该目录struct file 的f_pos, 并将offset 返回给
nfs_proc_readdir 作为READDIR Reply 的尾部cookie 值(即返回的最后一个目录项的
cookie字段的值),也就是说,当一次READDIR操作结束前,ext3_dx_readdir最后访
问的hash值都要被返回给客户端, 以告诉客户端下一次READDIR采用的cookie值.
ext3_dx_readdir返回给READDIR Reply的hash值,或者是某次调用call_filldir中断时
的hash值,或者是EXT_HTRR_EOF, 表示已到目录文件结尾.
3. Nfs READDIR Reply 返回给客户端的内容包括一系列目录项,即(name, len, inode,
cookie)结构的数组,其中cookie指示的是列表中下一个目录项,或者下一个应读取
的目录项的hash值,非尾部的cookie值可用于随机性的检查某些目录项时使用,但
一般很少用. 要注意的是,nfssvc_encode_entry 函数在将一个目录项的内容填入
READDIR Reply包的缓冲区时,其尚无法知道下一个目录项的hash值,因此需要采
用一些软件技巧完成cookie字段的设置, 参看如下代码片段的解释可以理解
int nfssvc_encode_entry(struct readdir_cd *ccd, const char *name,
int namlen, loff_t offset, u64 ino, unsigned int d_type)
{
struct nfsd_readddires *cd;
...
if (cd->offset)
cd->offset = htonl(offset); /* 根据记住的地址设置前一个entry的cookie值 */
...
*p++ = htonl((u32) ino); /* 文件ID */
p = xdr_encode_array(p, name, namlen); /* 文件名串和长度 */
cd->offset = p; /* 保存需要等下一次再设置的offset字段的位置 */
*p++ = ~(u32) 0; /* 暂时将offset字段置为0xffffffff */
...
}
static int nfsd_proc_readdir(struct svc_rqst *rqstp, struct nfsd_readdirargs *argp,
struct nfsd_readdirres *resp)
{
...
offset = argp->cookie;
nfserr = nfsd_readdir(rqstp, &argp->fh, &offset, &resp->common,
nfssvc_encode_entry, nfssvc_encode_entry32);
/* offset中返回的是该目录文件下一次要访问的目录项的hash 值 */
...
if (resp->offset)
resp->offset = htonl(offset); /* 设置最后一个entry的cookie值 */
...
}
本地用户getdents 系统调用返回的struct linux_dirent 记录的d_off 字段也具有和
READDIR Reply 的cookie 相同的含义,其也指向下个要读取的目录项的hash 值,
ext3的实现中对这个d_off的处理也有类似的代码
long sys_getdents(unsigned int fd, struct linux_dirent __user * dirent, unsigned int count)
{
...
error = vfs_readdir(file, filldir, &buf); /* 最新的entry由filldir函数调用后完成创建 */
...
lastdirent = buf.previous;
if (lastdirent) {
if (put_user(file->f_pos, &lastdirent->d_off)) /* 设置上一个entry的cookie 值 */
error = -EFAULT;
else
error = count - buf.count;
}
...
}
4. 一般来说,如果严格按照READDIR Reply尾部cookie建议的位置派发新的READDIR
请求,则一系列的READDIR请求能正确完成目录项的读取,不会出现重复读取目录
项,或漏掉某目录项的情况. 即使是有很多个(如1000个)目录项具有相同的hash值,
也不会出现问题. 因为即使一次READDIR 操作的count参数接纳不完这些具有相同
hash值的entries,下一次READDIR请求使用相同的hash值作为cookie, 仍然能按照
struct dir_private_info的extra_fname所指示的单向链表位置完成其他的entries的远
程读取,且不必再远程读取已经处理过的有相同hash值的entries
5. 按照vxwork的方式,一次READDIR请求读取多个目录项, 但只承认第一个目录项的
情况,会和ext3的带索引的目录readdir的实现方式不兼容. 当READDIR Reply返回
四个目录项 ent1, ent2, ent3, ent4, 并且ent2和ent3返回有相同的cookie值时(假
设ent1 和ent4 的cookie 不同),尾部的ent4 的cookie 指示的hash 值,或者比
ent3,ent4 的hash 更大,或者是EXT3_HTREE_EOF, 总之就是不同于ent3/ent4 的
hash. 这种情况下,使用ent2/ent3 的cookie 值发出READDIR 请求,会重新读取
ent3 和ent4, 因为服务器端的struct dir_private_info 的curr_hash 已经大于
ent3/ent4的hash值,也就是说服务器端所记录的状态位置,已经离开了ent3,ent4
对应的rbtree树节点, 所以只有重新读取该rbtree节点的全部单项链表中的目录项.
vxwork客户端的工作方式,不使用READDIR Reply返回的尾部的cookie, 其本质上是
一种随机的目录项访问, 这种随机的目录访问当然不能保证前后两次调用的承接性,
当然也不应该用这种方式来实现用户空间的lsdir操作. 另外,一次只承认一个目录
项,这种低效率的方式让人难以理解.
实现ext3 目录索引相关的重要函数
函数名称/功能描述(英文)
dx_probe()
根据目录项或hash值查找指向其目录块的索引列表,目录项的索引列表记录在 “struct dx_frame” 结构
的数组中 (该函数由 ext3_dx_find_entry, ext3_dx_ad_entry, ext3_htree_fill_tree等调用)
dx_release()
释放 "struct dx_frame" 数组指向的全部索引块
ext3_htree_next_block()
更新 “struct dx_frame” 数组, 使其中的索引信息指向下一个目录块的目录项 (由 ext3_htree_fill_tree调
用)
htree_dirblock_to_tree()
将目录块中的全部目录项信息存入rbtree中(由ext3_htree_fill_tree调用)
ext3_tree_store_dirent()
将一个目录项信息存入rbtree节点中(由htree_dirblock_to_tree 调用)
ext3_htree_fill_tree()
按hash顺序扫描目录(即htree左序遍历), 将目录项读入到内存的rbtree中
dx_make_map()
创建一个 “struct dx_map_entry” 数组映射目录块中的目录项
dx_sort_map()
根据hash 字段的值对 “struct dx_map_entry” 数组进行排序
search_dirblock()
在目录块缓冲区中查找某目录项(按名字)
dx_insert_block()
在 “struct dx_frame” 指示的索引项之后插入一个索引项
make_indexed_dir()
将一个 1块大小的普通目录转换成 3块大小的索引目录, 该函数由ext3_add_entry()调用
dx_move_dirents()
将 “struct dx_map_entry” 数组指示的一个目录块缓冲区中离散的目录项拷贝到另一个目录块缓冲区中
紧密放置
dx_pack_dirents()
压缩目录块中的目录项,挤掉中间空的目录项
do_split()
将一个满的目录块拆分成两个,并为第二个目录块增加一个索引项
ext3fs_dirhash()
将文件名hash成一个32位的值
call_filldir()
将目录项信息从rbtree节点的 "struct fname" 结构拷贝到用户空间
参考内容
1. Linux 2.6.9 内核下 fs/ext3/dir.c, namei.c 源码文件的内容
2. Linux 2.6.9 内核下 fs/nfsd/nfsproc.c, vfs.c 源码文件的内容
3. Linux 2.6.9 内核下 fs/readdir.c 源码文件的内容
By
张 前 锋( qfzhanglinux@gmail.com)
张 前 锋
( qfzhanglinux@gmail.com)
Linux ext3 文件系统目录索引的实现
1. ext3 文件系统采用目录索引技术来加快对大的目录文件的访问(比如在保存10万个文
件的某目录下查找一个指定的文件). Ext3目录索引技术根据每个目录项名称的hash
值来确定这个文件放在Htree的哪个页节点上.
2. 每个目录文件的HTree结构包括索引块或目录项块: 索引块包含索引项(index entry)的
数组,每个索引项包含一个hash key值和一个指向目录项块的逻辑指针; 目录项块
包含正常的目录项(directory entry)列表,和不采用目录索引技术时一样. 索引项的
hash key部分是其指向的目录块上所有目录项对应的hash key的最小值,即key下界.
每个索引块的第一个索引项的hash字段被作了特殊的利用,其记录的是该索引块的
当前索引项数和索引项数的上限即(count,limit),以加速代码的处理. 为了和普通的目
录块兼容,索引块的头部还有一个fake_dirent的特殊目录项,其reclen为blocksize,
以让整个索引块看起来就象一个大的空目录项. root索引块更特殊,其总是位于目录
文件的第0块,而且其看起来就象是一个仅包含 “.”和 “..” 两个目录项的普通目录块.
Root索引和普通索引块的数据结构如下
struct dx_root /* 根索引块 */
{
struct fake_dirent dot;
char dot_name[4];
struct fake_dirent dotdot;
char dotdot_name[4];
struct dx_root_info
{
__le32 reserved_zero;
u8 hash_version;
u8 info_length; /* 8 */
u8 indirect_levels;
u8 unused_flags;
}
info;
struct dx_node /* 普通索引块 */
{
struct fake_dirent fake;
struct dx_entry entries[0];
}
struct fake_dirent /* faked 目录项 */
{
__le32 inode;
__le16 rec_len;
u8 name_len;
u8 file_type;
};
/* 索引项 */
struct dx_entry
/* 第一个索引项的hash 部分 */
struct dx_countlimit
struct dx_entry entries[0];
}
{
__le32 hash;
__le32 block;
}
{
__le16 limit;
__le16 count;
}
3. 目录项块中的目录项是按自然的FIFO及空档插入的顺序放置的, 当目录项块满时会
导致split, 一个目录块变成两个目录块,其对应的一个index entry 变成两个index
entries, 可能进一部导致索引块被split.
4. Ext3 支持一级或二级目录索引块, 当root索引块满时,就会由一级索引变为二级索
引. 当发生目录项块split时,前一个块的最后一个directory entry的hash值和后一个
块的第一个directory entry 的hash值可能是相同的,这种情况叫做collision, Ext3能
处理这种冲突,在种情况搜素一个目录项时,可能需要读取两个目录项块.
Linux ext3 文件系统对readdir 的支持
1.无论是本地用户进程执行getdents()系统调用还是Linux 的nfs服务器端对READDIR请
求的响应,最终会调用ext3_dx_readdir() 来读取带有索引(indexed)的ext3目录的内容
(the entries). 其中本地用户的getdents()系统调用在内核中调用ext3_dx_readdir()的代
码路径如下
sys_getdents() => vfs_readdir() => ext3_readdir() => ext3_dx_readdir()
Nfs服务器 端执行如下路径调用ext3_dx_readdir()
nfsd_proc_readdir() => nfsd_readdir() => vfs_readdir() => ext3_readdir() => ext3_dx_readdir()
2. ext3_dx_readdir采用几个重要的数据结构来实现nfs或本地的READDIR操作, 见下图。
struct dir_private_info {
struct rb_root root;
struct rb_node *curr_node;
struct fname *extra_fname;
loff_t last_pos;
__u32 curr_hash;
__u32 curr_minor_hash;
__u32 next_hash;
}
struct fname {
__u32 hash;
__u32 minor_hash;
struct rb_node rb_hash;
struct fname *next;
__u32 inode;
__u8 name_len;
__u8 file_type;
char name[0];
}
其中struct fname 用来表示一个目录entry的信息在被READDIR读入到用户空间或nfs
的Reply包之前的存放位置. 单个的struct fname只能代表一个目录entry, 但多个具有相
同hash值的目录entries的struct fname通过其next字段组织成一个单向链表. Struct
dir_private_info 用来将一个目录文件的全部entries组织成Red-Black树,其root字段指
向rbtree的根, rbtree的节点由各struct fname结构的rb_hash字段形成,当然具有相同
hash值的entries只需要一个rbtree节点。 Struct dir_private_info由其打开的目录文件
struct file 的private 字段指向,即每个被打开的被索引的目录文件都可有一个struct
dir_private_info. Struct dir_private_info 中还有一些字段,用以存放READDIR操作执行
过程中的状态, 如curr_hash, last_pos, curr_node, extra_fname.
3.在对一个被打开的目录文件第一次调用ext3_dx_readdir时(如以cookie=0对一个目录
文件执行READDIR操作时), ext3_dx_readdir会调用ext3_htree_fill_tree()将全部目录项
读入内存,并创建一个由各个struct fname为节点组成的rbtree, 以便READDIR操作能
按hash值的顺序读取该目录的entries.
4. ext3_htree_fill_tree的工作就是通过索引htree的组织方式,以hash值为序遍历目录的
索引块,从而找到全部的目录块. 对于每一个目录块,ext3_htree_fill_tree 调用
htree_dirblock_to_tree()读取其中的目录entries,用其信息产生struct fname,加入到
该打开目录的rbtree 中。 对于单个目录entry, htree_dirblock_to_tree 调用
ext3_htree_store_dirent()来产生对应的struct fname, 并将该struct fname 加入到
rbtree(作为一个节点)或加入到已有节点的单向链表中. 总之rbtree的不同节点的struct
fname具有不同的hash值,同一个节点的单向链表的各struct fname具有相同的hash
值. ext3_htree_fill_tree 也可能会直接调用ext3_htree_store_dirent将一些特殊的目录
entries 如 “.”和 “..” 加入到打开目录的rbtree中.
5. 只要rbtree已经建立, ext3_dx_readdir就会通过while循环遍历rbtree的节点,对每一
个节点,调用call_filldir()将其struct fname的单向链表中的各个entry的信息生成到用
户空间或nfs Reply 包中去. 如果call_filldir 调用失败,则ext3_dx_readdir 退出.
call_filldir遍历struct fname的单向链表,调用一个fill_dir钩子函数将具有相同hash值
的目录entries 的内容生成到目的缓冲区. ext3 本地的READDIR 操作使用一个通用的
filldir()函数, nfs2 v2的READDIR操作使用的fill_dir函数是nfssvc_encode_entry().
6. call_filldir()调用fill_dir的失败是一个很重要的行为. 由于fill_dir钩子函数所做的工作只
是将一个struct fname的内容生成到目标缓冲区中,纯粹的内存拷贝,赋值操作, 所以
fill_dir失败的唯一情况是目标缓冲区已满. 在fill_dir失败的情况下,call_filldir会将正在
操作的rbtree 节点的hash 值保存到目录的struct file 的f_pos 字段,并把该节点的
struct fname 单向链表的剩余未遍历的链表地址保存到 struct dir_private_info 的
exta_fname 指针中. fill_dir 的失败使call_fill()失败退出,进一步使ext3_dx_readdir 从
while 循环中退出,标志本次READDIR 操作结束. ext3_dx_readdir 在退出时,会将
struct file的f_pos字段保存到struct dir_private_info的last_pos.
7.在使用目录索引的情况下,打开目录的struct file的f_pos字段并不是象访问普通文件
一样,表示当前正在访问的文件线性偏移, 而是表示当前正要访问的目录项(以struct
fname形式存在)的hash值, struct dir_private_info 的last_pos字段具有类似的含义,
其保存上次ext3_dx_readdir 调用中最近操作的 struct fname 的hash 值. Struct
dir_private_info的curr_node保存ext3_dx_readdir最近访问的rbtree的struct fname节
点指针. 这些关于该打开目录的当前访问位置信息,在下一次READDIR操作执行时,
会很有用.
8. ext3_dx_readdir在开始执行时,会检查struct dir_private_info的extra_fname指针,如
果其非空,则立即调用call_filldir处理extra_fname指向的单向链表,把上轮call_filldir
调用没处理完的内容处理完,然后再进入ext3_dx_readdir的while主循环,继续处理上
轮调用后剩下的其他rbtree节点, 直到所有rbtree节点被处理完毕或call_filldir调用失
败.
9. ext3_dx_readdir在开始执行时,会比较struct file的f_pos和struct dir_private_info 的
last_pos值,如果不等,则表示客户端(用户空间或nfs client)调用了lseek()类操作,企
图重新选择READDIR操作的开始位置,则ext3_dx_readdir会放弃已有的rbtree, 调用
ext3_htree_fill_tree从f_pos指定的hash值开始重新建构一个rbtree, 在此基础上开始
新一轮的call_filldir操作, 并且hash值比f_pos小的entries在此轮就不用再处理了
10. ext3_dx_readdir在开始执行时,也会检查当前目录文件对应的struct inode, 如果有变
化,说明该目录文件最近被修改,则ext3_dx_readdir 会放弃已有的rbtree, 调用
ext3_htree_fill_tree从当前的hash值位置重新建构一个rbtree, 在此基础上开始新一轮
的call_filldir操作, 并且hash值比curr_hash值小的entries在此轮就不用再处理了
关于 ”.” 和 “..” 两个目录项被READDIR 读出的次序
1. 在ext3文件系统关闭了dir_index功能的情况下, 所有目录想都是按照其在目录文件中
的线性偏移顺序读出的,其中 “.” 是第一个目录项,“..”是第二个目录项,所以NFS客
户端或本地的getdents()操作总是能先获取 “.”, 然后获取 “..”
2. 在ext3文件系统激活了dir_index功能的情况下,所有目录项都是按照其hash值的顺
序被加入到rbtree, 按hash值从小到大的次序被NFS客户端或getdents()读取的. 但
ext3 对 “.” 和 “..” 两个目录项的处理比较特殊:
• 如果执行READDIR操作时,该目录文件的大小还没有超过一个磁盘块,则所以目
录项全部在第0 块, 则ext3_htree_fill_tree() 函数的代码会直接调用
htree_dirblock_to_tree() 函数将该块的全部目录项加入到rbtree 中 , 在
htree_dirblock_to_tree()中,全部目录项的hash 值,包括 “.” 和 “..”, 都是通过
ext3fs_dirhash()函数计算出来的. ext3fs_dirhash算出的hash值,由一个目录项名
和文件系统超级块中保存的seed值共同决定, 所以相同的文件名字符串,在不同
的文件系统中算出的hash值可能不一样。在本人的测试系统中, “..” 的hash值
比 “.” 小,所以 “..” 比 “.” 先读出
• 如果执行READDIR操作时,该目录文件的大小已经超过一个磁盘块, 则第0块变
成了纯粹的root索引块,htree_dirblock_to_tree()对其头部的 “.”和 “..” 两个目录项
需直接调用ext3_htree_store_dirent()读取,并且为 “.” 指定了hash值 “0”, 为 “..”
制定了hash 值 “2”, 所以在加入到rbtree中后,”.” 总是在 “..” 的前面.
NFS V2 READDIR 操作的实现
1. 每次nfs READDIR 请求都会包括目录filehandle,count,cookie 三个参数。其中
filehandle指定要读取的目录文件,count指示要读取的目录内容大小,cookie参数指
示要读取的目录项起点位置. 在不支持目录索引的情况下,cookie参数指示的是目录
项在目录文件中的线性偏移;在支持目录索引的情况下,cookie参数指示的是目录项
的hash值
2. 在nfs的通信层收到READDIR请求后,Linux nfs服务器代码会调用nfs_proc_readdir
来处理请求,该函数会调用nfsd_readdir(). nfsd_readdir() 在往下调用vfs_readdir()之
前,会调用vfs_llseek(file, offset, 0)来设置该目录struct file的f_pos, 其中offset就
是来自nfs READDIR 的cookie 参数,也就是说该调用告诉ext3_dx_readdir 本次
READDIR 操作的启点,刚开始时,这个offset 一般是0, 也就是告诉ext3 从最小的
hash 值开始读目录项. nfsd_readdir()在调用完vfs_readdir 操作之后,会调用
offset=vfs_llseek(file,0,1) 读取该目录struct file 的f_pos, 并将offset 返回给
nfs_proc_readdir 作为READDIR Reply 的尾部cookie 值(即返回的最后一个目录项的
cookie字段的值),也就是说,当一次READDIR操作结束前,ext3_dx_readdir最后访
问的hash值都要被返回给客户端, 以告诉客户端下一次READDIR采用的cookie值.
ext3_dx_readdir返回给READDIR Reply的hash值,或者是某次调用call_filldir中断时
的hash值,或者是EXT_HTRR_EOF, 表示已到目录文件结尾.
3. Nfs READDIR Reply 返回给客户端的内容包括一系列目录项,即(name, len, inode,
cookie)结构的数组,其中cookie指示的是列表中下一个目录项,或者下一个应读取
的目录项的hash值,非尾部的cookie值可用于随机性的检查某些目录项时使用,但
一般很少用. 要注意的是,nfssvc_encode_entry 函数在将一个目录项的内容填入
READDIR Reply包的缓冲区时,其尚无法知道下一个目录项的hash值,因此需要采
用一些软件技巧完成cookie字段的设置, 参看如下代码片段的解释可以理解
int nfssvc_encode_entry(struct readdir_cd *ccd, const char *name,
int namlen, loff_t offset, u64 ino, unsigned int d_type)
{
struct nfsd_readddires *cd;
...
if (cd->offset)
cd->offset = htonl(offset); /* 根据记住的地址设置前一个entry的cookie值 */
...
*p++ = htonl((u32) ino); /* 文件ID */
p = xdr_encode_array(p, name, namlen); /* 文件名串和长度 */
cd->offset = p; /* 保存需要等下一次再设置的offset字段的位置 */
*p++ = ~(u32) 0; /* 暂时将offset字段置为0xffffffff */
...
}
static int nfsd_proc_readdir(struct svc_rqst *rqstp, struct nfsd_readdirargs *argp,
struct nfsd_readdirres *resp)
{
...
offset = argp->cookie;
nfserr = nfsd_readdir(rqstp, &argp->fh, &offset, &resp->common,
nfssvc_encode_entry, nfssvc_encode_entry32);
/* offset中返回的是该目录文件下一次要访问的目录项的hash 值 */
...
if (resp->offset)
resp->offset = htonl(offset); /* 设置最后一个entry的cookie值 */
...
}
本地用户getdents 系统调用返回的struct linux_dirent 记录的d_off 字段也具有和
READDIR Reply 的cookie 相同的含义,其也指向下个要读取的目录项的hash 值,
ext3的实现中对这个d_off的处理也有类似的代码
long sys_getdents(unsigned int fd, struct linux_dirent __user * dirent, unsigned int count)
{
...
error = vfs_readdir(file, filldir, &buf); /* 最新的entry由filldir函数调用后完成创建 */
...
lastdirent = buf.previous;
if (lastdirent) {
if (put_user(file->f_pos, &lastdirent->d_off)) /* 设置上一个entry的cookie 值 */
error = -EFAULT;
else
error = count - buf.count;
}
...
}
4. 一般来说,如果严格按照READDIR Reply尾部cookie建议的位置派发新的READDIR
请求,则一系列的READDIR请求能正确完成目录项的读取,不会出现重复读取目录
项,或漏掉某目录项的情况. 即使是有很多个(如1000个)目录项具有相同的hash值,
也不会出现问题. 因为即使一次READDIR 操作的count参数接纳不完这些具有相同
hash值的entries,下一次READDIR请求使用相同的hash值作为cookie, 仍然能按照
struct dir_private_info的extra_fname所指示的单向链表位置完成其他的entries的远
程读取,且不必再远程读取已经处理过的有相同hash值的entries
5. 按照vxwork的方式,一次READDIR请求读取多个目录项, 但只承认第一个目录项的
情况,会和ext3的带索引的目录readdir的实现方式不兼容. 当READDIR Reply返回
四个目录项 ent1, ent2, ent3, ent4, 并且ent2和ent3返回有相同的cookie值时(假
设ent1 和ent4 的cookie 不同),尾部的ent4 的cookie 指示的hash 值,或者比
ent3,ent4 的hash 更大,或者是EXT3_HTREE_EOF, 总之就是不同于ent3/ent4 的
hash. 这种情况下,使用ent2/ent3 的cookie 值发出READDIR 请求,会重新读取
ent3 和ent4, 因为服务器端的struct dir_private_info 的curr_hash 已经大于
ent3/ent4的hash值,也就是说服务器端所记录的状态位置,已经离开了ent3,ent4
对应的rbtree树节点, 所以只有重新读取该rbtree节点的全部单项链表中的目录项.
vxwork客户端的工作方式,不使用READDIR Reply返回的尾部的cookie, 其本质上是
一种随机的目录项访问, 这种随机的目录访问当然不能保证前后两次调用的承接性,
当然也不应该用这种方式来实现用户空间的lsdir操作. 另外,一次只承认一个目录
项,这种低效率的方式让人难以理解.
实现ext3 目录索引相关的重要函数
函数名称/功能描述(英文)
dx_probe()
根据目录项或hash值查找指向其目录块的索引列表,目录项的索引列表记录在 “struct dx_frame” 结构
的数组中 (该函数由 ext3_dx_find_entry, ext3_dx_ad_entry, ext3_htree_fill_tree等调用)
dx_release()
释放 "struct dx_frame" 数组指向的全部索引块
ext3_htree_next_block()
更新 “struct dx_frame” 数组, 使其中的索引信息指向下一个目录块的目录项 (由 ext3_htree_fill_tree调
用)
htree_dirblock_to_tree()
将目录块中的全部目录项信息存入rbtree中(由ext3_htree_fill_tree调用)
ext3_tree_store_dirent()
将一个目录项信息存入rbtree节点中(由htree_dirblock_to_tree 调用)
ext3_htree_fill_tree()
按hash顺序扫描目录(即htree左序遍历), 将目录项读入到内存的rbtree中
dx_make_map()
创建一个 “struct dx_map_entry” 数组映射目录块中的目录项
dx_sort_map()
根据hash 字段的值对 “struct dx_map_entry” 数组进行排序
search_dirblock()
在目录块缓冲区中查找某目录项(按名字)
dx_insert_block()
在 “struct dx_frame” 指示的索引项之后插入一个索引项
make_indexed_dir()
将一个 1块大小的普通目录转换成 3块大小的索引目录, 该函数由ext3_add_entry()调用
dx_move_dirents()
将 “struct dx_map_entry” 数组指示的一个目录块缓冲区中离散的目录项拷贝到另一个目录块缓冲区中
紧密放置
dx_pack_dirents()
压缩目录块中的目录项,挤掉中间空的目录项
do_split()
将一个满的目录块拆分成两个,并为第二个目录块增加一个索引项
ext3fs_dirhash()
将文件名hash成一个32位的值
call_filldir()
将目录项信息从rbtree节点的 "struct fname" 结构拷贝到用户空间
参考内容
1. Linux 2.6.9 内核下 fs/ext3/dir.c, namei.c 源码文件的内容
2. Linux 2.6.9 内核下 fs/nfsd/nfsproc.c, vfs.c 源码文件的内容
3. Linux 2.6.9 内核下 fs/readdir.c 源码文件的内容
By
张 前 锋( qfzhanglinux@gmail.com)