(转载)Linux 内核文件系统与设备操作流程分析-1
时间:2010-12-29 来源:tuohuang0303
以下内容为转载,出于网站www.xfocus.net的http://www.xfocus.net/articles/200701/903.html
创建时间:2007-01-23 更新时间:2007-01-23
文章属性:原创
文章提交:sinister sinister
Email: [email protected]
Homepage:http://www.whitecell.org 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);
/*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;
//
// 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;
}