使用设备之前我们通常都需要调用
open
函数,这个函数一般用于设备专有数据的初始化,申请相关资源及进行设备的初始化等工作,对于简单的设备而言,open
函数可以不做具体的工作,你在应用层通过系统调用 open
打开设备时,如果打开正常,就会得到该设备的文件描述符,之后,我们就可以通过该描述符对设备进行 read
和
write
等操作;
open
函数到底做了些什么工作?下图中列出了 open
函数执行的大致过程。
户空间使用
open()
系统调用函数打开一个字符设备时
(int fd = open(
“
dev/xxx
”
, O_RDWR))
大致
有以下过程:
• 在虚拟文件系统 VFS 中的查找对应与字符设备对应 struct inode 节点• 遍历散列表 cdev_map ,根据 inod 节点中的 cdev_t 设备号找到 cdev 对象• 创建 struct file 对象(系统采用一个数组来管理一个进程中的多个被打开的设备,每个文件秒速符作为数组下标标识了一个设备对象)• 初始化 struct file 对象,将 struct file 对象中的 file_operations 成员指向 struct cdev 对象中的 file_operations 成员( file->fops = cdev->fops )• 回调 file->fops->open 函数
我们使用的
open
函数在内核中对应的是
sys_open
函数,
sys_open
函数又会调用
do_sys_open
函 数。在 do_sys_open
函数中,首先调用函数
get_unused_fd_flags
来获取一个未被使用的文件描述符 fd,该文件描述符就是我们最终通过
open
函数得到的值。紧接着,又调用了
do_filp_open
函数, 该函数通过调用函数 get_empty_filp
得到一个新的
file
结构体,之后的代码做了许多复杂的工作, 如解析文件路径,查找该文件的文件节点 inode
等,直接来到了函数
do_dentry_open
函数,如下 所示。
//do_dentry_open 函数(内核源码/fs/open.c)
static int do_dentry_open(struct file *f,struct inode *inode,int(*open)(struct inode *,
struct file *),const struct cred *cred)
{
……
f->f_op = fops_get(inode->i_fop);
……
if (!open)
open = f->f_op->open;
if (open) {
error = open(inode, f);
if (error)
goto cleanup_all;
}
……
}
•
第
4
行:使用
fops_get
函数来获取该文件节点
inode
的成员变量
i_fop
,我们使用 mknod 创建字符设备文件时,将
def_chr_fops
结构体赋值给了该设备文件
inode
的
i_fop
成 员。
•
第
7
行:到了这里,我们新建的
file
结构体的成员
f_op
就指向了
def_chr_fops
。
注:def_chr_fops 是字符设备通用的操作函数,类比于我们自己在写驱动程序时的file_operation
// def_chr_fops 结构体(内核源码/fs/char_dev.c)
const struct file_operations def_chr_fops = {
.open = chrdev_open,
.llseek = noop_llseek,
};
最终,会执行
def_chr_fops
中的
open
函数,也就是
chrdev_open
函数,可以理解为一个字符设备的通用初始化函数,根据字符设备的设备号,找到相应的字符设备,从而得到操作该设备的方法。
//chrdev_open 函数(内核源码/fs/char_dev.c)
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 最终将创建的文件结构体 file 的成员 f_op 替换成了 cdev 对应的 ops 成员,并执行 ops 结构体中的 open 函数。
最后,调用上图的
fd_install
函数,完成文件描述符和文件结构体
file
的关联,之后我们使用对该文件描述符 fd
调用
read
、
write
函数,最终都会调用
file
结构体对应的函数,实际上也就是调用cdev 结构体中
ops
结构体内的相关函数。
总结一下整个过程,当我们使用
open
函数,打开设备文件时,会根据该设备的文件的设备号找到相应的设备结构体,从而得到了操作该设备的方法。也就是说如果我们要添加一个新设备的话,我们需要提供一个设备号,一个设备结构体以及操作该设备的方法(file_operations
结构体)。