2.4 打开文件
先来整理一下打开文件的基本过程:
以上是打开文件时候需要用到的函数的基本调用流程,从系统调用开始,这些函数集中在fs/open.c 和 fs/namei.c中。
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(AT_FDCWD, filename, flags, mode);
}
当在用户空间调用open的时候,通过系统调用转到内核态时会调用到以上函数。
int vfs_open(const struct path *path, struct file *file)
{
file->f_path = *path;
return do_dentry_open(file, d_backing_inode(path->dentry), NULL);
}
略过中间的那些函数,当调用到vfs_open的时候,会有一个path对象和file对象。path的结构不复杂:
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
} __randomize_layout;
这里的dentry就是我们要打开的文件所属的dentry,d_backing_inode(path->dentry)用来得到文件的inode对象。
而这儿的file,到面前为止还是一个刚刚初始化的空对象。
static int do_dentry_open(struct file *f,
struct inode *inode,
int (*open)(struct inode *, struct file *))
{
.......
f->f_op = fops_get(inode->i_fop);
if (WARN_ON(!f->f_op)) {
error = -ENODEV;
goto cleanup_all;
}
......
if (!open)
open = f->f_op->open;
if (open) {
error = open(inode, f);
if (error)
goto cleanup_all;
}
......
}
这里会通过fops_get(inode->i_fop)得到inode中的file_operations对象,而后用它指向的open函数来打开文件。
file_operations是个函数指针的集合,不同的文件系统都有自己的实现,比如在ext2中的定义为:
const struct file_operations ext2_file_operations = {
.llseek = generic_file_llseek,
.read_iter = ext2_file_read_iter,
.write_iter = ext2_file_write_iter,
.unlocked_ioctl = ext2_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ext2_compat_ioctl,
#endif
.mmap = ext2_file_mmap,
.open = dquot_file_open,
.release = ext2_release_file,
.fsync = ext2_fsync,
.get_unmapped_area = thp_get_unmapped_area,
.splice_read = generic_file_splice_read,
.splice_write = iter_file_splice_write,
};
而在内存中生成文件类型的inode的时候,都会将它设置给inode的i_fop对象:
inode->i_fop = &ext2_file_operations;
而在ramfs中也有类似的实现,不过它是通过调用file/inode.c中的init_special_inode来实现的。
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
} else if (S_ISBLK(mode)) {
inode->i_fop = &def_blk_fops;
inode->i_rdev = rdev;
} else if (S_ISFIFO(mode))
inode->i_fop = &pipefifo_fops;
else if (S_ISSOCK(mode))
; /* leave it no_open_fops */
else
printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"
" inode %s:%lu\n", mode, inode->i_sb->s_id,
inode->i_ino);
}
这段函数通过判断设备类型来确定具体的操作,如果是字符设备(S_ISCHR(mode))的话:
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
音频设备属于字符设备,所以它的i_fop被赋予def_chr_fops:
const struct file_operations def_chr_fops = {
.open = chrdev_open,
.llseek = noop_llseek,
};
而这里的open指向了chrdev_open。可以看到这里并没有实现read,write等函数,后面会看到事实上在打开文件的时候,file中的f_op会被替换掉,那儿会有这些函数的实现。
static int chrdev_open(struct inode *inode, struct file *filp)
{
const struct file_operations *fops;
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);
/* Check i_cdev again in case somebody beat us to it while
we dropped the lock. */
p = inode->i_cdev;
if (!p) {
inode->i_cdev = p = new;
list_add(&inode->i_devices, &p->list);
new = NULL;
} else if (!cdev_get(p))
ret = -ENXIO;
} else if (!cdev_get(p))
ret = -ENXIO;
spin_unlock(&cdev_lock);
cdev_put(new);
if (ret)
return ret;
ret = -ENXIO;
fops = fops_get(p->ops);
if (!fops)
goto out_cdev_put;
replace_fops(filp, fops);
if (filp->f_op->open) {
ret = filp->f_op->open(inode, filp);
if (ret)
goto out_cdev_put;
}
return 0;
out_cdev_put:
cdev_put(p);
return ret;
}
chrdev_open的实现在fs/char_dev.c中。
这段函数中定义了变量struct cdev *p,再看函数结尾处的几行代码:
fops = fops_get(p->ops);
if (!fops)
goto out_cdev_put;
replace_fops(filp, fops);
通过fops_get得到p指向的file_operations对象ops,然后再通过replace_fops用它替换掉file中的f_op。后续就是通过它来操作文件了。
再看函数的开头处:
p = inode->i_cdev;
p来自于indoe的i_cdev,它又是如何来的呢?这里是字符设备需要特殊处理的地方。
先看cdev的定义:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
} __randomize_layout;
它包含一个file_operations对象,还有一个dev_t对象。另外再char_dev.c中定义了全局变量:
static struct kobj_map *cdev_map;
再回到chardev_open中:
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
if (!kobj)
return -ENXIO;
new = container_of(kobj, struct cdev, kobj);
if (!p) {
inode->i_cdev = p = new;
这里通过kobj_lookup通过inode中的设备号得到一个kobj,再从中取出一个cdev对象new,然乎将它赋值给inode->i_cdev。
如上所述,inode中最初并没有给i_cdev赋值,而留到open的时候。不过也就是第一次open的时候需要,后续打开的时候inode中已经保留该值了。
再看一个函数:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
if (WARN_ON(dev == WHITEOUT_DEV))
return -EBUSY;
error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;
kobject_get(p->kobj.parent);
return 0;
}
这段函数将cdev 对象保存到cdev_map中。也就是说要使用open功能,需要事先调用过cdev_add函数,对于alsa,这部分代码在sound/core/sound.c中实现:
static int __init alsa_sound_init(void)
{
snd_major = major;
snd_ecards_limit = cards_limit;
if (register_chrdev(major, "alsa", &snd_fops)) {
pr_err("ALSA core: unable to register native major device number %d\n", major);
return -EIO;
}
......
register_chrdev 中传入了主设备号,名字和snd_fops。register_chrdev将会负责生成cdev 对象,并调用cdev_add函数。看一下snd_fops:
static const struct file_operations snd_fops =
{
.owner = THIS_MODULE,
.open = snd_open,
.llseek = noop_llseek,
};
当打开一个音频设备时,最后会调用到snd_open函数。
后续的事情就由alsa模块自己处理了。