Linux 内核文件系统与设备操作流程分析Author: sinister
Email: sinister@whitecell.org
Homepage:http://www.whitecell.org
Date: 2007-01-23
本笔记对 linux kernel 的文件系统操作以及设备操作进行了分析,主要是针
对 ext3 文件系统的 open 流程的分析,目的是为了解答心中的几个疑问:
1、一个文件的操作流程,系统是如何把 struct file 与 struct dentry
以及 struct inode 结合起来的?
2、文件与设备驱动都是对 VFS(Virtual File System) 抽象出来的 struct file
进行操作的,那么系统是如何区分的?在哪里开始区分的?
3、linux 内核中没有类 UNIX VFS(Virtual File System) 提供的 struct vnode
结构,那么具体的文件操作是如何与实际文件系统的操作挂钩的?
4、超级块(super block)在文件与设备驱动操作中起到的作用?
5、在以前的尝试中对 struct file 做手脚为什么影响不到全局?
6、在文件系统内核有几个函数操作集?有何不同?分别是在什么时候赋值?
注:此文档是根据当时的分析过程记录的,分析顺序也就没有再更改过,
每个人读内核源码的思路不同,或者说目的不同,流程自然也就不同。
所以在别人看来我所记录的可能比较凌乱。如果真是这样,那我只能
说句抱歉,因为我并不打算再修改记录顺序。最后还是那句话,如果
您在阅读本文时发现了错误,还望得到您的指正。
我们知道在 linux kernel 中,如果想操作一个文件,首先要通过 filp_open()
这个 kernel api 来打开这个文件,那么我们就从这里入手分析。可以看到
filp_open() 函数只是个简单封状,具体实现是 do_filp_open() 函数,函数
本身先通过 open_namei() 函数得到一个 fd 对应的 struct nameidata 结构。
最后使用 nameidata_to_filp() 函数返回一个 struct file 结构。
static struct file *do_filp_open(int dfd, const char *filename, int flags,
int mode)
{
int namei_flags, error;
struct nameidata nd;
namei_flags = flags;
if ((namei_flags+1) & O_ACCMODE)
namei_flags++;
//
// 这个函数调用 path_lookup_xxx() 等函数根据路径名称
// 返回一个 struct nameidata 结构。这个函数完成了很多
// 工作,后面会随着疑问详细分析这个函数。这里只需要知
// 道它返回了一个 nameidata 结构。
//
error = open_namei(dfd, filename, namei_flags, mode, &nd);
if (!error)
//
// 这里返回的 struct file 结构已经创建并填充完毕了。
// 直接返回给调用者。
//
return nameidata_to_filp(&nd, flags);
return ERR_PTR(error);
}
这个函数根据 struct nameidata 结构返回一个 struct file。可以看到
struct file 是在使用了 __dentry_open() 函数后被填充的,且使用的第
一个参数是 nameidata->dentry,这也是为什么我们要获得 struct nameidata
的一个主要原因,其目的就是为了得到 struct dentry 结构。
struct file *nameidata_to_filp(struct nameidata *nd, int flags)
{
struct file *filp;
/* Pick up the filp from the open intent */
filp = nd->intent.open.file;
/* Has the filesystem initialised the file for us? */
if (filp->f_dentry == NULL)
//
// 这个函数主要就是填充一个 struct file 结构,通过这段
// 代码也可以看到,一个 struct file 是动态分配的。
//
filp = __dentry_open(nd->dentry, nd->mnt, flags, filp, NULL);
else
path_release(nd);
return filp;
}
此函数分配并填充一个 struct file 结构。从这个函数中很明显可以看到,
一个 struct file 结构是使用 struct dentry,struct inode,struct vfsmount
结构中的相关信息填充的。在 struct dentry 中有一个区域指向了 struct inode
结构,这也就是为什么我们要获得 struct dentry 原因之一。有了 struct inode
结构我们就可以得到一个文件的相关信息和实际文件系统所提供的函数,如 ext3
文件系统。或者是一个设备驱动所提供的方法,如字符设备驱动。为什么这么说?
看下面的详细记录。
static struct file *__dentry_open(struct dentry *dentry, struct vfsmount *mnt,
int flags, struct file *f,
int (*open)(struct inode *, struct file *))
{
struct inode *inode;
int error;
//
// 得到访问标志
//
f->f_flags = flags;
f->f_mode = ((flags+1) & O_ACCMODE) | FMODE_LSEEK |
FMODE_PREAD | FMODE_PWRITE;
//
// 通过 struct dentry 得到 struct inode 结构
//
inode = dentry->d_inode;
//
// 判断这个文件(inode) 是否有写权限,没有则
// 跳转到 cleanup_file 处退出
//
if (f->f_mode & FMODE_WRITE) {
error = get_write_access(inode);
if (error)
goto cleanup_file;
}
//
// 使用 vfsmount,dentry,inode 结构
// 填充 struct file 中相关域。
//
f->f_mapping = inode->i_mapping;
f->f_dentry = dentry;
f->f_vfsmnt = mnt;
f->f_pos = 0;
//
// 注意:这里使用的是 struct inode 中的 struct file_operations
// 回调函数来填充的 struct file->f_op。也就是说 struct file 中的
// 函数其实是 inode->file_operations 的一份复制品。而这个 struct
// file 很明显是动态创建的,也就是说 open 一个文件则会动态生成一个
// struct file 结构,并把 inode->i_fop 函数给它,struct file 并不是
// 全局唯一的,而是与进程相关的,在 task_struct 中的 files_struct
// 结构则是 struct file 的一个集合。这也就是为什么在 struct file
// 里做了手脚,影响的仅是当前进程,而不是全局的原因。;)
//
f->f_op = fops_get(inode->i_fop);
file_move(f, &inode->i_sb->s_files);
//
// 注意:这里调用了 struct file->f_op->open 函数,也就是说调用了
// struct inode->i_fop->open 函数。这里有必要注解一下,在 struct
// inode 结构中,有两套回调函数的方法集,一个是 struct
// file_operations 一个是 struct inode_operations。而对于 open 函数
// 只是存在 file_operations 当中,另一个则不存在。那么在 struct inode
// 这个 i_fop 函数集中有可能使用的是实际文件系统的函数,如
// ext3_file_operations 函数集。也有可能是一个设备驱动所提供的函数
// 方法如 def_chr_fops 函数集。
//
if (!open && f->f_op)
open = f->f_op->open;
if (open) {
error = open(inode, f);
if (error)
goto cleanup_all;
}
//
// 去掉相关标志位。
//
f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);
file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping);
/* NB: we're sure to have correct a_ops only after f_op->open */
if (f->f_flags & O_DIRECT) {
if (!f->f_mapping->a_ops ||
((!f->f_mapping->a_ops->direct_IO) &&
(!f->f_mapping->a_ops->get_xip_page))) {
fput(f);
f = ERR_PTR(-EINVAL);
} }
return f;
//
// 以下两个流程,只有失败时才会走到。释放 struct file 中
// 所有相关信息,并返回错误。
//
cleanup_all:
fops_put(f->f_op);
if (f->f_mode & FMODE_WRITE)
put_write_access(inode);
file_kill(f);
f->f_dentry = NULL;
f->f_vfsmnt = NULL;
cleanup_file:
put_filp(f);
dput(dentry);
mntput(mnt);
return ERR_PTR(error);
}
在上面详细分析中介绍的 struct file 中使用的 f_op 其实是 struct inode->i_fop
中的一个副本。写过设备驱动的人都知道,在使用 register_xxx 注册一个“字符”
或 “块” 设备驱动时,都要填充一个 struct file 结构以便与应用层交互。那么
这样就存在一个问题,大家都知道在 *nix 系统下文件与设备都是以文件形式存在的,
即都有 inode,而访问 file system 与 device driver 所使用的函数操作集
都是通过 struct inode 提供的,且都是一个 file_operations 函数集,那么系统
是如何区分所访问的是 file system 上的文件还是 device driver 呢?如果是
device driver 那么又是在什么地方初始化连接你所注册的回调函数呢?下面我们
以 ext3 文件系统为例,来看一下 ext3_read_inode() 函数的实现。至于这个函数
什么时候被调用,在哪里被调用的?以及下面注释中提到的 ext3 文件系统的 open
操作为什么为空操作等疑问会在后面章节中介绍,这里为了结合上下文,保持连贯
性,还是先讲一下这个函数。
void ext3_read_inode(struct inode * inode)
{
struct ext3_iloc iloc;
struct ext3_inode *raw_inode;
struct ext3_inode_info *ei = EXT3_I(inode);
struct buffer_head *bh;
int block;
//
// 篇幅所限,在这个函数中我们只列出相关代码。
//
#ifdef CONFIG_EXT3_FS_POSIX_ACL
ei->i_acl = EXT3_ACL_NOT_CACHED;
ei->i_default_acl = EXT3_ACL_NOT_CACHED;
#endif
ei->i_block_alloc_info = NULL;
//
// 注意:这里的 __ext3_get_inode_loc 是产生
// 一个磁盘 I/O 从磁盘读取真正的 struct inode
// 来填充 in core 类型的。注意这个函数使用的
// 第三个参数,为 0 的情况下产生 I/O 从磁盘
// 读取,否则从 buffer_head 磁盘缓存中查找。
//
if (__ext3_get_inode_loc(inode, &iloc, 0))
//
// 如果从磁盘获取 inode 失败则直接跳到退出处理,
// 不会进行下面的任何操作。
//
goto bad_inode;
......
......
//
// 可以看到,目录/文件/连接分别赋予了不同的函数集。
//
if (S_ISREG(inode->i_mode)) {
//
// 如果是普通文件的话,则使用 ext3_file_xxx 函数集
// 注意:在使用 ext3_file_operations 函数集时,它的
// open 函数对应的是 generic_file_open() 函数,而这个函数
// 除了判断大文件是否合法外,几乎就是一个空函数,也就是说
// 如果是在一个 ext3 文件系统上,open 操作其实没有任何具体
// 动作,是无意义的。为什么会这样呢?在后面介绍文件系统时
// 会讲到。
//
inode->i_op = &ext3_file_inode_operations;
inode->i_fop = &ext3_file_operations;
ext3_set_aops(inode);
} else if (S_ISDIR(inode->i_mode)) {
//
// 如果是目录的话,则要区别对待,使用 ext3_dir_xxx 函数集
//
inode->i_op = &ext3_dir_inode_operations;
inode->i_fop = &ext3_dir_operations;
} else if (S_ISLNK(inode->i_mode)) {
//
// 如果是连接的话,也要区别对待,使用 ext3_symlink_xxx 函数集
//
if (ext3_inode_is_fast_symlink(inode))
inode->i_op = &ext3_fast_symlink_inode_operations;
else {
inode->i_op = &ext3_symlink_inode_operations;
ext3_set_aops(inode);
}
} else {
//
// 如果以上三种情况都排除了,那么我们则认为他是一个设备驱动
// 注意:这里的仅对 inode->i_op 函数集进行了直接赋值。对于
// inode->i_fop 函数集使用的是 init_special_inode() 函数
// 进行的赋值
//
inode->i_op = &ext3_special_inode_operations;
if (raw_inode->i_block[0])
init_special_inode(inode, inode->i_mode,
old_decode_dev(le32_to_cpu(raw_inode->i_block[0])));
else
init_special_inode(inode, inode->i_mode,
new_decode_dev(le32_to_cpu(raw_inode->i_block[1])));
}
......
......
}
流程走到这个函数已经可以确定用户操作打开的是一个设备驱动,那么这里就要
继续判断打开的是哪种类型设备驱动和需要赋什么样的函数操作集。通过下面的
代码我们可以看到,系统只支持了四种设备驱动类型,也就是说系统注册设备驱
动类型只可能是 “字符”,“块”,“FIFO”,“SOCKET” 设备,其中的
“FIFO”,“SOCK” 还不是真实设备,这里我们称其为“伪” 设备,可能用词
不大准确,姑且在这里这么叫。
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
//
// 如果是字符设备,则使用 def_chr_fops 函数集
// 只有真实设备才有会设置 inode->i_rdev 字段
//
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
//
// 如果是块设备,则使用 def_blk_fops 函数集
// 只有真实设备才有会设置 inode->i_rdev 字段
//
} else if (S_ISBLK(mode)) {
inode->i_fop = &def_blk_fops;
inode->i_rdev = rdev;
//
// 如果是 FIFO,则使用 def_fifo_fops 函数集
//
} else if (S_ISFIFO(mode))
inode->i_fop = &def_fifo_fops;
//
// 如果是 SOCKET,则使用 def_sock_fops 函数集
//
else if (S_ISSOCK(mode))
inode->i_fop = &bad_sock_fops;
//
// 如果不是以上四种类型则忽略,并打印提示信息。
//
else
printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o)\n",
mode);
}
以上四种类型设备驱动的函数集都大同小异,这里我们仅以“字符”设备的函数
集为例,可以看到 file_operations 结构只设置了 open 方法,把它指向了
chrdev_open() 函数。那么我们的在设备驱动里指定的 struct file->f_op 函
数怎么被调用的?继续看 chrdev_open() 函数实现。
const struct file_operations def_chr_fops = {
.open = chrdev_open,
};
此函数主要完成的工作就是填充并调用用户给出的 struct file->f_op 结构中的
函数集。它首先尝试得到正确的字符设备结构,判断如果注册了相应的函数集则
调用。
int chrdev_open(struct inode * inode, struct file * filp)
{
struct cdev *p;
struct cdev *new = NULL;
int ret = 0;
spin_lock(&cdev_lock);
//
// 得到相应的字符设备结构
//
p = inode->i_cdev;
if (!p) {
struct kobject *kobj;
int idx;
spin_unlock(&cdev_lock);
//
// 如果此字符设备结构无效,则从设备对象管理中查找
//
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
if (!kobj)
return -ENXIO;
new = container_of(kobj, struct cdev, kobj);
spin_lock(&cdev_lock);
//
// 再次尝试获得正确的字符设备结构
//
p = inode->i_cdev;
if (!p) {
inode->i_cdev = p = new;
inode->i_cindex = idx;
list_add(&inode->i_devices, &p->list);
new = NULL;
//
// 使用 cdev_get() 函数判断相应设备结构的内核设备对象是否
// 有效
//
} else if (!cdev_get(p))
ret = -ENXIO;
//
// 如果有效,则调用 cdev_get() 函数继续判断相应设备结构的内核
// 设备对象是否有效,如果无效则表明此设备仍不可用。
//
} else if (!cdev_get(p))
ret = -ENXIO;
spin_unlock(&cdev_lock);
cdev_put(new);
//
// 如果到此字符设备还无效的话,则返回错误。
//
if (ret)
return ret;
//
// 注意:这里使用 cdev->file_operations 函数操作集来
// 填充的 struct file->f_op 这也是我们注册字符设备驱动
// 时所给出的函数集。
//
filp->f_op = fops_get(p->ops);
//
// 如果 struct file->f_op 无效,那么它所指向的函数集
// 肯定也无效,这样的话直接返回错误。注意:这里有一
// 种可能,那就是调用者虽注册了一个字符设备驱动,但是
// 并没有提供相应的操作集,或许调用者认为没有必要。
//
if (!filp->f_op) {
cdev_put(p);
return -ENXIO;
}
//
// 如果 open 函数有效那么则先锁定内核,调用此方法后
// 再解锁内核
//
if (filp->f_op->open) {
lock_kernel();
ret = filp->f_op->open(inode,filp);
unlock_kernel();
}
if (ret)
cdev_put(p);
return ret;
}
到这里我们可以知道,对文件或设备驱动的判断与函数集的赋值都是在文件
系统这一级区分的,也就是说在有 open 操作时是到具体的文件系统,如
ext3,并在 ext3 上再次区分出是否为设备驱动,这点很好理解,因为设备
驱动也是以文件形式存在的。分析到这里可以算是把对设备驱动的操作流程
弄清晰了。但这仅是对设备驱动的操作,别忘了上面还存在一大堆的疑问,
我们知道只有在触发调用了 ext3_read_inode() 时才会区分,那么它何时
被调用的?什么情况下调用的?为什么 ext3 的文件操作集中的 open 是
空操作呢?要解答这些问题,我们仍要从 open_namei() 函数开始进行分析。
在这个函数的实现过程中会根据标志的不同将路径名转换成 struct nameidata
结构,当得到此结构后还会根据目录,连接等做不同处理。这里我们只关心操作
流程,所以只对 path_look_xxx() 函数做跟踪分析。
int open_namei(int dfd, const char *pathname, int flag,
int mode, struct nameidata *nd)
{
int acc_mode, error;
struct path path;
struct dentry *dir;
int count = 0;
//
// 篇幅所限,在这个函数中我们只列出相关代码。
//
......
//
// 判断是否是建立标志,如果不是则使用 path_lookup_open()
//
if (!(flag & O_CREAT)) {
//
// 通过路径名查询 inode, dentry 并返回 nameidata 结构。
//
error = path_lookup_open(dfd, pathname, lookup_flags(flag),
nd, flag);
if (error)
return error;
goto ok;
}
/*
* Create - we need to know the parent.
*/
//
// 如果是建立标志则使用 path_lookup_create()
//
error = path_lookup_create(dfd,pathname,LOOKUP_PARENT,nd,flag,mode);
if (error)
return error;
......
}
上面的 path_lookup_open() 与 path_lookup_create() 都是一个很简单的封状
无条件的调用了 __path_lookup_intent_open() 函数,只不过是传输标志不同
而已。此函数在预先填充一些 struct nameidata 结构后继续调用 do_path_lookup()
完成查找。
static int __path_lookup_intent_open(int dfd, const char *name,
unsigned int lookup_flags, struct nameidata *nd,
int open_flags, int create_mode)
{
//
// 获得一个空的 struct file 结构
//
struct file *filp = get_empty_filp();
int err;
if (filp == NULL)
return -ENFILE;
//
// 先填充要返回的 struct nameidata 结构中的相关字段
//
nd->intent.open.file = filp;
nd->intent.open.flags = open_flags;
//
// 填充建立标志位,这个也就是 path_lookup_open()
// 与 path_lookup_create() 函数调用的区别
//
nd->intent.open.create_mode = create_mode;
//
// 根据路径调用 do_path_lookup() 得到一个 struct nameidata 结构
//
err = do_path_lookup(dfd, name, lookup_flags|LOOKUP_OPEN, nd);
if (IS_ERR(nd->intent.open.file)) {
if (err == 0) {
err = PTR_ERR(nd->intent.open.file);
path_release(nd);
}
} else if (err != 0)
release_open_intent(nd);
return err;
}
此函数根据 "/" 根路径与 AT_FDCWD 标志从不同位置得到 struct vfsmount 与
struct dentry 结构来填充 struct nameidata 中的相关字段,这里应该仅是占
位用。最终路径分解工作与查找由 link_path_walk() 函数来完成。
static int fastcall do_path_lookup(int dfd, const char *name,
unsigned int flags, struct nameidata *nd)
{
int retval = 0;
int fput_needed;
struct file *file;
//
// 当前进程的 struct file 集
//
struct fs_struct *fs = current->fs;
nd->last_type = LAST_ROOT; /* if there are only slashes... */
nd->flags = flags;
nd->depth = 0;
//
// 如果路径是根目录则从 fs_struct->altrootmnt 与 fs_struct->altroot
// 中得到 struct vfsmount 与 struct dentry 结构
//
if (*name=='/') {
read_lock(&fs->lock);
if (fs->altroot && !(nd->flags & LOOKUP_NOALT)) {
nd->mnt = mntget(fs->altrootmnt);
nd->dentry = dget(fs->altroot);
read_unlock(&fs->lock);
if (__emul_lookup_dentry(name,nd))
goto out; /* found in altroot */
read_lock(&fs->lock);
}
nd->mnt = mntget(fs->rootmnt);
nd->dentry = dget(fs->root);
read_unlock(&fs->lock);
//
// 如果路径不是根目录且有 AT_FDCWD 标志则从 fs_struct->pwdmnt
// 与 fs_struct->pwd 中得到 struct vfsmount 与 struct dentry 结构
// 这里应该表示是当前目录? FIXME
//
} else if (dfd == AT_FDCWD) {
read_lock(&fs->lock);
nd->mnt = mntget(fs->pwdmnt);
nd->dentry = dget(fs->pwd);
read_unlock(&fs->lock);
//
// 如果以上都不是的话则使用 fget_light() 得到一个 struct file
// 并从 struct file->f_vfsmnt 中得到 struct vfsmount 结构,而
// struct dentry 则使用 struct file->f_dentry 中的
//
} else {
//
// 注意:这里声明了一个 struct dentry 结构
//
struct dentry *dentry;
file = fget_light(dfd, &fput_needed);
retval = -EBADF;
if (!file)
goto out_fail;
//
// 使用 struct file 中的来填充
//
dentry = file->f_dentry;
retval = -ENOTDIR;
if (!S_ISDIR(dentry->d_inode->i_mode))
goto fput_fail;
retval = file_permission(file, MAY_EXEC);
if (retval)
goto fput_fail;
nd->mnt = mntget(file->f_vfsmnt);
nd->dentry = dget(dentry);
fput_light(file, fput_needed);
}
current->total_link_count = 0;
//
// 注意:这个函数才真正的分解路径,调用实际文件系统的操作。
// 它本身也是个简单封状,实际是使用 __link_path_walk() 函数
// 完成操作。
//
retval = link_path_walk(name, nd);
out:
if (likely(retval == 0)) {
if (unlikely(!audit_dummy_context() && nd && nd->dentry &&
nd->dentry->d_inode))
audit_inode(name, nd->dentry->d_inode);
}
out_fail:
return retval;
fput_fail:
fput_light(file, fput_needed);
goto out_fail;
}
在 link_path_walk() 函数中实际使用的函数为 __link_path_walk(),在这个函数
中分解路径,并依次调用 do_lookup() 函数完成实际的转换工作,do_lookup()
才有可能去调用实际文件系统的读磁盘 inode 操作。结合上下文,我们只关心读
取流程,不对路径分解算法做分析,所以只提取相关代码。
static fastcall int __link_path_walk(const char * name, struct nameidata *nd)
{
struct path next;
struct inode *inode;
int err;
unsigned int lookup_flags = nd->flags;
//
// 篇幅所限,在这个函数中我们只列出相关代码。
//
//
// 这里是一个大循环,目的是用来分解路径并在
// 分解的中间过程使用 do_lookup() 得到相关的
// inode 一直到最后指定的文件或路径。也就是说对
// 于象 /dir/temp/readme.txt 这种路径会首先从
// 根一直分解并调用 do_lookup() 得到其 inode
// 一直到得到最后的 readme.txt 为止。
//
for(;;) {
.......
//
// 从缓存或调用实际文件系统函数获取 inode 信息
//
err = do_lookup(nd, &this, &next);
if (err)
break;
......
last_with_slashes:
lookup_flags |= LOOKUP_FOLLOW | LOOKUP_DIRECTORY;
last_component:
/* Clear LOOKUP_CONTINUE iff it was previously unset */
nd->flags &= lookup_flags | ~LOOKUP_CONTINUE;
.......
//
// 这里是去掉了 LOOKUP_CONTINUE 标志后,又调用了一次。
//
err = do_lookup(nd, &this, &next);
if (err)
break;
......
}
path_release(nd);
return_err:
return err;
}
到这里才是查找对应 struct dentry 的具体操作,此函数首先从缓存中尝试获取
struct dentry 结构。如果获取失败,则调用 real_lookup() 函数使用实际文件
系统方法来读取 inode 信息。这里要明确 struct dentry 中包含了 struct inode
信息。
static int do_lookup(struct nameidata *nd, struct qstr *name,
struct path *path)
{
struct vfsmount *mnt = nd->mnt;
//
// 从 hlist 中获取 struct dentry 结构,hlist 代表的是
// 一个 inode 的缓存即是一个 HASH 表。
//
struct dentry *dentry = __d_lookup(nd->dentry, name);
//
// 如果没有找到则会调用 real_lookup() 实际文件系统方法
// 从磁盘中获取
//
if (!dentry)
goto need_lookup;
if (dentry->d_op && dentry->d_op->d_revalidate)
goto need_revalidate;
done:
//
// 如果从缓存中找到,则设置 struct path 并返回
//
path->mnt = mnt;
path->dentry = dentry;
__follow_mount(path);
return 0;
need_lookup:
//
// 使用实际文件系统方法,从磁盘中获得 inode 信息
//
dentry = real_lookup(nd->dentry, name, nd);
if (IS_ERR(dentry))
goto fail;
goto done;
need_revalidate:
dentry = do_revalidate(dentry, nd);
//
// 这里是缓存的分之。如果 struct dentry 无效还是需要调
// 用 real_lookup() 读取
//
if (!dentry)
goto need_lookup;
if (IS_ERR(dentry))
goto fail;
goto done;
fail:
return PTR_ERR(dentry);
}
在分析 real_lookup() 函数前,我们先来看一下 ext3 文件系统的 inode
结构。很明显可以看出 lookup 指向了 ext3_lookup() 函数。
struct inode_operations ext3_dir_inode_operations = {
//
// 为了更清晰,在这个结构中只列出我们感兴趣的字段
//
......
.lookup = ext3_lookup,
......
};
此函数先从缓存中查找对应的 inode,如果没有则新分配一个 struct dentry
结构,然后调用 parent->d_inode->i_op->lookup 即调用了 ext3_lookup()
函数来查找 inode。
static struct dentry * real_lookup(struct dentry * parent, struct qstr * name, struct nameidata *nd)
{
struct dentry * result;
//
// 篇幅所限,在这个函数中我们只列出相关代码。
//
//
// 获得上一层目录的 inode。别忘了我们是分解路径依次
// 调用的,所以上一层的 inode 肯定是存在的。
//
struct inode *dir = parent->d_inode;
......
//
// 先从缓存里查找。
//
result = d_lookup(parent, name);
if (!result) {
//
// 没找到的话,新分配一个 struct dentry 结构
// 注意:我们这里新分配了一个 struct dentry,
// 也就是说每一个目录或文件都需要一个 dentry 结构。
//
struct dentry * dentry = d_alloc(parent, name);
result = ERR_PTR(-ENOMEM);
if (dentry) {
//
// 这里也就是调用了 ext3_lookup() 函数,可以
// 看下上面介绍的 ext3_dir_inode_operations
// 结构
//
result = dir->i_op->lookup(dir, dentry, nd);
if (result)
dput(dentry);
else
result = dentry;
}
mutex_unlock(&dir->i_mutex);
return result;
}
.......
}
这里到了实际文件系统的查找函数。首先根据第一个参数,也就是上级的 dentry
从 ext3_dir_entry_2 中得到新的 dentry 结构,并从其中得到相关的 inode number,
再调用 iget() 函数去获取相应的 struct inode 结构,最后将此 inode 与 dentry
进行关联。
static struct dentry *ext3_lookup(struct inode * dir, struct dentry *dentry, struct nameidata *nd)
{
struct inode * inode;
struct ext3_dir_entry_2 * de;
struct buffer_head * bh;
if (dentry->d_name.len > EXT3_NAME_LEN)
return ERR_PTR(-ENAMETOOLONG);
//
// 得到新的 dentry 并返回一个磁盘缓存 buffer_head 结构
// 注意:这个 dentry 虽然是新分配的,但它所指向的 d_parent
// 与 d_inode 是有效的,也就是说上级目录相关信息是有效的。
// 返回的 de 里包含了 inode number。
//
bh = ext3_find_entry(dentry, &de);
//
// 注意:这里的 inode 默认置为 NULL
//
inode = NULL;
if (bh) {
unsigned long ino = le32_to_cpu(de->inode);
brelse (bh);
//
// 如果对应的超级块(super block)无效则直接返回错误
//
if (!ext3_valid_inum(dir->i_sb, ino)) {
ext3_error(dir->i_sb, "ext3_lookup",
"bad inode number: %lu", ino);
inode = NULL;
} else
//
// 有效则调用 iget() 函数得到正确的 struct inode
// 其实也就是根据超级块(super block)的函数集获取
//
inode = iget(dir->i_sb, ino);
if (!inode)
return ERR_PTR(-EACCES);
}
//
// 关键此 inode 对应的 dentry 结构并返回。
//
return d_splice_alias(inode, dentry);
}
在分析 iget() 函数之前,有必要先了解下超级块(super block)中的
相关字段与函数。
struct super_block {
//
// 为了更清晰,在这个结构中只列出我们感兴趣的字段
//
......
//
// 文件系统结构。在下面介绍 mount 挂载文件系统时
// 会有详细介绍。
//
struct file_system_type *s_type;
//
// 超级块(super block)函数集
//
struct super_operations *s_op;
......
};
下面是 ext3 文件系统的超级块(super block)函数集结构
static struct super_operations ext3_sops = {
//
// 为了更清晰,在这个结构中只列出我们感兴趣的字段
//
......
//
// 注意:这里的 ext3_read_inode() 是不是很眼熟
//
.read_inode = ext3_read_inode,
......
};
终于走到了最终的读取函数!这个函数非常简单,在判断一些有效性后,直接调用
超级块(super block)函数集中的 read_inode 方法,也就是我们前面介绍的 ext3_sops
函数集中的 ext3_read_inode() 函数。
static inline struct inode *iget(struct super_block *sb, unsigned long ino)
{
struct inode *inode = iget_locked(sb, ino);
if (inode && (inode->i_state & I_NEW)) {
//
// 这里调用的就是 ext3_read_inode() 函数
//
sb->s_op->read_inode(inode);
unlock_new_inode(inode);
}
return inode;
}
到这里我们可以解释 ext3_read_inode() 函数是何时调用的了,可以说是
open_namei() 函数在路径转换时间接的调用了 iget() 函数,而 iget() 函
数则是调用了已经注册好的超级块(super block)函数集 ext3_sops 中的
ext3_read_inode() 函数来获取相应的 inode。其实这也就可以解释为什么
在 struct inode->i_fop 中(也就是 ext3_file_operations 函数集中)
open 操作函数 generic_file_open() 是个空操作。因为其对应的 inode
已经在 open_namei()->iget() 中得到了,得到了一个 inode 其实在实际
文件系统中就是一个打开操作,得到了 inode 当然就可以对它进行读/写
操作了。只所以提供了一个 generic_file_open() 应该是占位用的,占位
的目的应该是为了可以使用用户提供的操作方法。也就是说,如果你自己
写了一个 open 操作并赋值给 struct inode->i_fop->open 的话,系统会
调用你所提供的这个 open 操作。我们在上面分析 __dentry_open() 函数时
已经指出了这个调用点。以上的疑问都得到了解答,但这里又再次引出了一
个疑问,那就是这个已经注册好了的 超级块(super block)函数集 ext3_sops
是什么时候注册的?要解答这个疑问我们只能从头,也就是 mount 文件系统
时进行分析。
在分析 mount 前我们首先来了解下如下结构,这个结构是在注册新的文件
系统时被作为参数传递的,注册文件系统的函数为 register_filesyste()。
struct file_system_type {
//
// 文件系统名称,如:ext3
//
const char *name;
int fs_flags;
//
// 实际文件系统的超级块(super block)函数。在 mount 时通
// 过它来得到超级块的信息,包含 inode 等。
//
int (*get_sb) (struct file_system_type *, int,
const char *, void *, struct vfsmount *);
void (*kill_sb) (struct super_block *);
//
// 当前模块
//
struct module *owner;
//
// 指向下一个文件系统地址
//
struct file_system_type * next;
struct list_head fs_supers;
struct lock_class_key s_lock_key;
struct lock_class_key s_umount_key;
};
我们再来看下 ext3 文件系统是如何填充这个结构的。
static struct file_system_type ext3_fs_type = {
.owner = THIS_MODULE,
.name = "ext3",
//
// 注意这里的回调函数指向了 ext3_get_sb()
//
.get_sb = ext3_get_sb,
.kill_sb = kill_block_super,
.fs_flags = FS_REQUIRES_DEV,
};
最终使用 register_filesystem( &ext3_fs_type ); 完成文件系统的注册。
这里仅是注册了文件系统,我们知道要使用一个文件系统首先要 mount 才可
使用。我们清楚了以上结构后,接着来看 vfs_kern_mount() 函数,这个函数
是内核最终实现 mount 的函数,这个函数的第一个参数即是上面提到的
file_system_type 结构,在 ext3 文件系统下传递的是 ext3_fs_type。函数
中调用的 type->get_sb 即触发了 ext3_get_sb() 函数。
struct vfsmount *
vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data)
{
struct vfsmount *mnt;
char *secdata = NULL;
int error;
if (!type)
return ERR_PTR(-ENODEV);
error = -ENOMEM;
//
// 根据名称分配一个新的 vfsmount 挂接点。
//
mnt = alloc_vfsmnt(name);
if (!mnt)
goto out;
if (data) {
secdata = alloc_secdata();
if (!secdata)
goto out_mnt;
error = security_sb_copy_data(type, data, secdata);
if (error)
goto out_free_secdata;
}
//
// 注意:这里调用了已注册文件系统的超级块(super block)函数
// 对于 ext3 文件系统来说,就是调用了 ext3_get_sb,可参考
// 以上对 file_system_type 的说明。
//
error = type->get_sb(type, flags, name, data, mnt);
if (error < 0)
goto out_free_secdata;
error = security_sb_kern_mount(mnt->mnt_sb, secdata);
if (error)
goto out_sb;
//
// 这里的挂接点是一个 dentry 结构
//
mnt->mnt_mountpoint = mnt->mnt_root;
//
// 把新的 vfsmount 结构赋给自身的 parent 这样可以
// 通过 parent 遍历出所有 mount 的文件系统
//
mnt->mnt_parent = mnt;
up_write(&mnt->mnt_sb->s_umount);
free_secdata(secdata);
return mnt;
//
// 以下流程只有出错时才会走到
//
out_sb:
dput(mnt->mnt_root);
up_write(&mnt->mnt_sb->s_umount);
deactivate_super(mnt->mnt_sb);
out_free_secdata:
free_secdata(secdata);
out_mnt:
free_vfsmnt(mnt);
out:
return ERR_PTR(error);
}
下面的 ext3_get_sb() 函数仅是个简单的封状,直接调用的 get_sb_bdev()
函数,但这里要注意 get_sb_bdev() 函数不是严格按照 ext3_get_sb() 函数
进行传递的,它本身多出了一个 ext3_fill_super 参数,而这个参数是以一个
回调函数形式提供的。
static int ext3_get_sb(struct file_system_type *fs_type,
int flags, const char *dev_name, void *data, struct vfsmount *mnt)
{
//
// 注意:这里多了一个 ext3_fill_super() 的回调函数。
//
return get_sb_bdev(fs_type, flags, dev_name, data, ext3_fill_super, mnt);
}
了解了以上结构我们再来看 ext3_fill_super() 函数的具体实现,这个函数的第
一个参数即是一个超级块(super block)结构。在此函数中将上面提到的 ext3 超级
块(super block) 函数集 ext3_sops 赋给了此结构。然后调用 iget() 函数触发
超级块(super block) 函数集。
static int ext3_fill_super (struct super_block *sb, void *data, int silent)
{
//
// 篇幅所限,在这个函数中我们只列出相关代码。
//
//
// 设置超级块的函数集
//
sb->s_op = &ext3_sops;
sb->s_export_op = &ext3_export_ops;
sb->s_xattr = ext3_xattr_handlers;
#ifdef CONFIG_QUOTA
sb->s_qcop = &ext3_qctl_operations;
sb->dq_op = &ext3_quota_operations;
#endif
INIT_LIST_HEAD(&sbi->s_orphan); /* unlinked but open files */
sb->s_root = NULL;
//
// 调用 iget() 函数得到相应的 inode。
//
root = iget(sb, EXT3_ROOT_INO);
//
// 根据得到的根 inode 分配超级块(super block)中的
// s_root 此字段是一个 struct dentry 结构。
//
sb->s_root = d_alloc_root(root);
//
// 如果根 dentry 无效则提示错误跳到失败处。
//
if (!sb->s_root) {
printk(KERN_ERR "EXT3-fs: get root inode failed\n");
iput(root);
goto failed_mount4;
}
//
// 如果根 inode 不是目录或者大小与块无效则提示错误
// 跳到失败处。
//
if (!S_ISDIR(root->i_mode) || !root->i_blocks || !root->i_size) {
dput(sb->s_root);
sb->s_root = NULL;
printk(KERN_ERR "EXT3-fs: corrupt root inode, run e2fsck\n");
goto failed_mount4;
}
}
至此所有流程都走到了,疑问也被一个个打破。我们在整体的梳理下流程。在内核
sys_open 被调用打开一个文件或者设备驱动时,调用 filp_open()->do_filp_open()
函数,在 do_filp_open() 函数中,首先利用 open_namei() 函数得到一个 struct
nameidata 结构,那么在这个过程中 __path_lookup_intent_open() 函数设置了
struct nameidata->intent.open 相关字段,然后调用 do_path_lookup() 函数,在这
个函数中设置了 struct nameidata->mnt 与 struct nameidata->dentry 相关字段后
调用了 _link_path_walk() 函数开始分解路径,并依次调用 do_lookup() 函数来
获得路径中个目录与最终文件的 struct inode。do_lookup() 函数先从 inode 缓存
即 hlist 中查找 inode,如果没有找到则调用 real_lookup() 函数,此函数分配
了一个 struct dentry 结构,然后使用上层目录的 struct inode->i_op->lookup()
方法来继续查找,这样就触发了 ext3_lookup() 函数,而此函数得到 struct dentry
与 inode number 后调用 iget() 函数来返回 struct inode。(这里有必要强调一点,
那就是不仅目录才有 struct dentry 结构,一个文件也拥有一个 struct dentry 结
构,这个从上面具体代码分析中可以看到)而 iget() 函数是使用 struct inode 超
级块(super block)中的函数 ext3_read_inode() 来最终完成从磁盘读取 inode 操
作,读到一个 in core 类型的 struct inode 后为了提供文件与设备读/写等操作设
置了 struct inode->i_op 与 struct inode->i_fop 函数集。其实以上步骤按照提供
的系统调用以及内核操作流程来理解等于是打开了一个文件或目录。这也就是为什么
在 ext3_file_operations 函数集中只有读/写等操作,而打开是空操作的原因。至于
为什么提供一个空操作函数,在上面分析时已经给出了,这里不在阐述。到此
struct inode,struct dentry, struct nameidata 结构都已完全填充好。在
open_namei() 调用返回后将得到的 nameidata 结构作为参数调用
nameidata_to_filp() 函数,在此函数当中使用 struct dentry 作参数调用了
__dentry_open() 函数,在这个函数中会动态初始化一个 struct file 结构,并使用
struct inode->i_fop 函数集来填充 struct file->f_op (别忘了,我们前面的 inode
结构中相关域都已经准备好了,这里直接拿来使用即可)。那么不管是文件还是设备驱
动,可以看出来是走到具体文件系统这里才开始区分的。如果是目录/文件/连接则直接
使用 ext3_file_xxx 或 ext3_dir_xxx 等函数集。如果操作对象是一个设备驱动的话
则使用 init_special_inode 来初始化不同的设备驱动,如果是一个字符设备驱动的
话则调用 chrdev_open() 函数来对应 struct file 操作集。而上面提到的超级块
(super block) 函数是在注册文件系统注册时由 register_filesystem() 函数注册 ,
在 mount 时由 vfs_kern_mount() 函数间接调用 ext3_fill_super() 函数时进行关联
的。具体可以看上面的代码分析,这里不在详述。所有流程清晰后我们再说一下 struct
inode 中的几个函数集的区别与作用。我们这里仅以文件/目录为例进行解释,struct
inode_operations 操作是对文件(inode)的建立/查找(打开)/删除/重命名操作,struct
file_operations 操作是对已经存在的文件的读/写/刷新/列目录(readdir)/发送控制字
操作。
参考:linux kernel source 2.6.19.1
/usr/fs/ext3/inode.c
/usr/fs/ext3/namei.c
/usr/fs/ext3/super.c
/usr/fs/ext3/file.c
/usr/fs/ext3/dir.c
/usr/fs/block_dev.c
/usr/fs/open.c
/usr/fs/dcache.h
/usr/fs/inode.c
/usr/fs/namei.c
/usr/fs/super.c
/inlucde/linux/mount.h
感谢 zhuzj 与我探讨。
感谢我的发小齐佳佳,并将本次劳动成果献给齐佳佳。
WSS(Whitecell Security Systems),一个非营利性民间技术组织,致力于各种系统安全技术的研究。坚持传统的hacker精神,追求技术的精纯。
WSS 主页:http://www.whitecell.org/
WSS 论坛:http://www.whitecell.org/forums/