2、 初始化代表设备的scull_dev结构体
scull源代码中定义了一个scull_dev结构体,包括qset,qutuam,信号量sem以及cdev等字段。其中qset和qutuam的初始化对于Linux驱动程序的知识点来说毫不相关,因此不加讨论。
我只要知道,在加载module时所调用的module初始化函数中,可以初始化一些设备相关的变量。但是根据Linux Device Drvier3作者的意思,设备相关的变量或者一些资源最好应当在open函数中初始化,比如像中断号等,虽然在module初始化函数中注册也是允许的,但最好是在第一次打开设备,也就是open函数中再行分配。
3、 初始化互斥体init_MUTEX
互斥体MUTEX,也就是信号量的一个变种,与completion,自旋锁spinlock等等都与驱动中的并发和竞态相关,以后再说。
4、 初始化在内核中代表设备的cdev结构体
其实在Linux内核中,cdev结构体才是真正代表了某个设备。在内核调用设备的open,read等操作之前,必须先分配并注册一个或者多个cdev结构。
我想可以这么理解,主次设备号是涉外的,主要用来在与外部(指的是驱动module和Linux内核以外)交互时确定身份;而cdev结构体则是涉内的,当需要在module内部,或者与Linux内核之间传递一些变量,指针,buffer等东东,或者要调用驱动module中的某个服务函数时,就要用到 cdev结构体了。
在scull函数中,cdev结构体的分配,注册与初始化使用了一个自定义的scull_setup_cdev函数,在该函数中,主要由以下4条语句对cdev进行初始化:
cdev_init(&dev->cdev, &scull_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &scull_fops;
err = cdev_add (&dev->cdev, devno, 1);
(dev变量是scull程序定义的代表设备的一个结构体,它包含了cdev结构体,对于dev来说,cdev结构体应该说就是它的核心)
第一条语句是初始化cdev结构体,比如为cdev结构体分配内存,为cdev结构体指定file_operations等等,而第三条语句的作用初看起来似乎与第一条有所重复。但scull程序中既然这么写想必就有它的用意,也许需要看Linux内核源代码才能搞明白,但目前我是这么理解的:第一条语句中有关file_operations的部分是为了告诉Linux内核,该 cdev结构体相关的file_operations是scull_fops;而第二条语句则是真正为cdev指定了它的file_operations 字段就是scull_fops。
scull_fops是file_operations类型的变量,file_operations也是一个结构体,而且是Linux驱动程序中很重要的一个结构体,在scull程序中,定义如下:
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};
以上定义中,第一条.owner字段说明本file_operations结构体的拥有者是本驱动module,而接下来的几个字段则是告诉驱动 module,当有相应的系统调用到达该module时,module应该调用哪一个函数来为该系统调用服务。比如说,若有一个open系统调用到达 module,则module通过查询file_operations结构体就知道了,与open系统调用相关的是scull_open函数,于是 module就调用scull_open函数来为open系统调用服务了。其他几个字段也完全类似。
当然,Linux内核定义的file_operations结构体还包括其他一些字段,比如异步读写等等,但还是等用到的时候再说吧。
对cdev初始化的第二条语句是dev->cdev.owner = THIS_MODULE,这条语句就是说正在初始化的cdev结构体的拥有者就是本module
对cdev初始化的最后一条语句是err = cdev_add (&dev->cdev, devno, 1),该语句的目的就是告诉内核,该cdev结构体信息。因为cdev_add函数有可能调用失败,所以需要检测该函数调用的返回值。而一旦 cdev_add调用成功返回,那么我们的设备就“活”了!也就是说,外部应用程序对它的操作就会被内核允许且调用。因此在驱动程序还没有完全准备好处理设备上的操作时,就绝不能调用cdev_add。
三、 设备操作
驱动module因为由insmod的加载而进行了初始化之后,就会进入“潜伏”状态,也就是说,如果没有系统调用(如open,read等),那么module中定义的其他函数就绝不会运行!
这里所说的设备操作,是指当有系统调用到达驱动module时,module就该调用某个或某些函数有所动作。
对于驱动开发来说,我主要关心的只有一点,那就是系统调用怎样把一些外部应用程序中的变量值传递给驱动module。
scull程序中与设备操作相关的函数主要分为三类:初始化函数,实际的操作服务函数和清理函数。其中初始化函数只有一个,就是open函数,而操作服务函数则包括read,write,llseek等等函数,至于清理函数则是release函数。
1、open函数
open函数提供给驱动程序以初始化的能力,从而为以后的操作做准备。
说起来在用insmod加载驱动后也有一个初始化动作,但那个初始化是相对于整个Linux内核,或者说是针对整个module在涉外时的全局意义上的初始化;而open函数的初始化则是相对于设备操作来说的,是属于驱动内部的初始化,比如为以后read操作时用到的某个变量(如file结构体)作一下初始化,再比如初始化一下设备,清空一下buffer等等。
在大部分的驱动程序中,open应该完成如下工作:
a、 确定要打开的具体设备
b、 检查设备特定的错误(诸如设备未就绪或类似的硬件问题)
c、 如果设备是首次打开,则对其作一下初始化
d、 如有必要,更新f_op指针
e、 分配并填写置于filp->private_data里的数据结构
open函数的原型如下(指的是在file_operations结构体中的定义):
int (*open)(struct inode *inode, struct file *filp)
在驱动开发时要做的,就是为该函数作具体实现,当然对open函数的名称可以自定义,只要在填写file_operations结构体中的open字段时,将自定义的open函数名称填上就可以了。在scull程序中,用的就是scull_open函数名。
在open函数原型中,有inode和filp两个参数,都是外部应用程序在操作设备时通过调用系统调用传递给驱动module的。于是驱动module就可以通过这两个参数来确定要打开的具体设备了。其实这里所说的具体设备,并不是说驱动module需要从系统安装的所有设备中确定它所要服务的设备,而是指module需要从某一类拥有相同主设备号的设备中确定它要服务的设备。
之所以这么说,是因为驱动module是对应于某一个主设备号的所有设备的。换一句话说,就是Linux内核只管设备的主设备号,而不理会设备的次设备号是什么,如果有两个,三个或者更多个设备拥有同一个主设备号,那么不管外部应用程序要操作这些设备中的哪一个,Linux内核都只会调用同一个驱动 module。但是驱动module却不能不管次设备号了,因为它是跟某一个具体的设备打交道的,所以它需要根据open系统调用时传递给它的参数中找到次设备号,从而确定那唯一的一个设备(也许驱动module也可以同时操作几个设备,但一时也想不起来)。
但是上面所说的通过次设备号找到具体的设备,只是其中一种方法;另外还有一种方法就是通过cdev结构体确定某个具体设备。
设备所拥有的cdev结构体,或者次设备号,都保存在open函数的inode参数中。我们可以使用container_of宏通过inode所拥有的 cdev确定具体设备,也可以使用iminor宏从inode所拥有的i_rdev确定次设备号(i_rdev是inode结构体中的一个dev_t类型的变量,其中保存了真正的设备主次编号)。
对于open函数中的file参数,scull程序主要用它来做两件事:其一是将根据cdev获得的代表设备的scull_dev结构体保存到file->private_data中,这样就可以方便今后对该设备结构体的访问了,而不用每次都调用container_of宏或者iminor宏来找到设备结构体了;其二是根据file结构体中的f_flags字段来确定,这次的 open调用,是以写方式打开设备,还是以读方式来打开设备。
