之前对于字符设备驱动只是停留在会用,知道步骤,怎么去改的层面上。
最近开始更为细致的重看,做一些总结,主要自己再梳理一遍。
看懂本篇需要一定的基础,很多都跟rtos内核相似。其次,先会用会改字符设备驱动之后,之前的文章里有相关字符驱动的编写,然后可以看这篇分析。
在应用层,当我们用open对一个设备节点打开,发生了什么呢?
过程大致如下:
- 在虚拟文件系统VFS中查找对应与字符设备对应struct inode节点
- 遍历散列表cdev_map,根据inod节点中的cdev_t设备号找到cdev对象
- 创建struct file对象
- 初始化struct file对象,将struct file对象中的file_operations成员指向struct cdev对象中的file_operation成员
- 回调open函数
这样说,可能有点抽象,但可以先大致看一下。
下面,针对上图具体分析一下。
我们在使用open函数,发生系统调用进入内核,也就是对应的sys_open函数,sys_open函数又会调用do_sys_open函数。
do_sys_open函数首先调用get_unused_fd_flags来获取一个未被使用的文件描述符,同时这个描述符也是我们在应用层open得到的值。
具体看下图。
得到文件描述符之后,紧接着会调用do_filp_open函数,得到一个新的file结构体,同时该描述符是指向该结构体的。
来具体看一下do_dentry_open这个函数。
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;
}
……
}
这样要说明一点的是, inode上面的file_operation并不是自己构造的file_operation,而是字符设备通用的def_chr_fops,所以自己的构建的是在应用程序调用open之后,才会绑定在文件上。这样其实就解释了之前我们写open函数的时候、只是返回了一个0,但是文件也能正常打开是为什么了。
而inode->i_fop就是指向了def_chr_fops。
具体来看一下这个函数。
const struct file_operations def_chr_fops = {
.open = chrdev_open,
.llseek = noop_llseek,
};
同样也是一个file_operations结构体。
具体看chrdev_open干了什么。
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;//1.获取结构体cdev是否为空
if (!p) {
struct kobject *kobj;
int idx;
spin_unlock(&cdev_lock);
//2.由设备号来查找cdev结构体的kobj成员
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
if (!kobj)
return -ENXIO;
//3.通过container_of得到cdev
new = container_of(kobj, struct cdev, kobj);
spin_lock(&cdev_lock);
/* Check i_cdev again in case somebody beat us to it whilewe dropped␣,the lock.*/
p = inode->i_cdev;
if (!p) {
inode->i_cdev = p = new;//4.将得到的cdev给设备节点
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);//5.代替原结构体中的fops
if (filp->f_op->open) {
ret = filp->f_op->open(inode, filp);//6.调用自己构建的open
if (ret)
goto out_cdev_put;
}
return 0;
out_cdev_put:
cdev_put(p);
return ret;
}
见代码中的注释,如此,对于open究竟干了什么就十分清楚了。
ok,既然了解了这些,那么下面就对整个字符设备驱动的创建过程进行一个梳理。
在linux中,我们使用设备编号来表示设备,主设备号区分设备类别,次设备号标识具体的设备。cdev结构体被内核用来记录设备号。
使用设备的时候,我们通常会打开设备节点,通过设备节点的inode结构体、file结构体最终找到file_operations结构体。
在内核中,dev_t用来表示设备编号,是一个32位的数,其中高12位表示主设备号,低20位表示次设备号。
重点看的是cdev结构体。
内核通过一个哈希表来记录设备编号。
哈希表由数组和链表组成,吸收数组查找快,链表增删效率高,容易拓展等优点。
其实这么一看,是不是觉得有点眼熟!
在freertos里面,任务列表中也是这样的,一个优先级就是一个链表。
这里就是,一个主设备号就是一个链表,通过次设备号依次增加。
很好理解,这里就不解释了。
有了cdev结构体之后,就可以创建设备节点了。
设备节点在/dev下。
file结构体
内核中用file结构体表示每一个打开的文件,每打开一个文件,内核就会创建一个结构体,并且将对该文件的操作函数传递给该结构体的成员变量f_op。
在上面分析的时候,其实有提及。
VFS inode
它是linux管理文件系统的最基本单位。
inode结构体在内核内部表示一个文件,它与表示已经打开的文件描述符的结构体(file)是不同的。我们可以使用多个file文件结构表示同一个文件的多个文件描述符,但是所有的file文件结构全部必须只能指向一个inode结构体。
它主要包括两个:
- dev_t i_rdev:设备文件的节点,包含了设备号
- struct cdev *i_cdev
通常使用设备号来定位cdev。
字符设备驱动框架
- 首先要有设备号,通常用动态分配
- 然后实现file_operation并保存到cdev中,实现cdev的初始化
- 使用cdev_add()注册cdev到内核中,告诉内核我们的工作
- 为了调用file_operations,我们还需要创建设备节点
这样梳理下来,非常清晰,每一步都是紧紧关联的。
有问题欢迎私信留言。
参考野火linux驱动资料