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;
}
共3页: 1 [2] [3] 下一页 |