Chapter 3: CHAR DRIVERS
时间:2006-09-20 来源:aaronwong
Chapter 3: CHAR DRIVERS
本章介绍如何写一个完整的模块化的字符设备驱动。引进一个工作于内存区域的虚拟字符设备驱动scull为例(Simple Character Utility for Loading Localities),因而当谈及scull时,“设备”的含义与“scull所用的内存区域”相当。
每台计算机都有内存,因此scull不依赖于硬件,scull仅操作内存的某一区域,并使用kmalloc来申请内存。任何人都可编译和运行scull,并且可以交叉移植到任何可运行linux的计算机体系结构上。另外,scull不做任何有实际用途的事情,但它可以说明内核与字符设备驱动程序的接口,并允许用户运行一些测试程序。
3.1 The Design of scull
编写驱动程序的第一步是定义驱动程序提供给用户程序的功能(机制)——用户程序与驱动程序的接口,以及可用的资源和可实现的功能。由于我们的“设备”是计算机内存的一部分,因而灵活性很大,它可以是顺序存取设备,也可以是随即存取设备,可以是一个或多个设备,等等。
本章要讲解的scull实现如下设备的驱动:
scull0——scull3:
4个设备,每个设备由一片内存区域组成,都是全局性和持久性的。全局性是指,如果设备被多次打开,所有打开它的文件描述符(file descriptor)共享其中的数据。持久性是指,如果关闭设备后重新再打开,数据不丢失。由于可以使用常用命令访问该设备,如cp、cat以及shell I/O重定向等,这个设备的操作非常有趣。本章将深入探讨它的内部结构。
3.2 Major and Minor Numbers
通过访问文件系统的设备文件(或“节点”)来访问字符设备;设备文件通常在/dev目录下。
使用ls –l命令可以看到设备的主设备号和从设备号。例如某设备文件信息如下:
crw-rw-rw- 1 root dialout 4, 64 Jun 30 11:19 ttys0
则设备ttys0的主设备号为4,从设备号为64。
主设备号标识设备对应的驱动程序。在打开设备文件时,内核利用其主设备号来为其分派所对应的正确的驱动程序。
次设备号则仅供由主设备号所指定的驱动程序使用;内核的其他部分不使用它,而仅将其传递给驱动程序。因此一个驱动程序控制若干个设备是很平常的,而次设备号则为驱动程序提供了一种区分各个(同类)设备的方法。
2.4内核引入了一种新的(可选)特性,叫作设备文件系统即devfs。若使用devfs,对设备文件的管理将大为简化,这与传统的方法有很大区别;另外,这种新的文件系统造成了几项user-visible的不兼容,并且它尚未作为系统发布版的缺省特性(在写该书时)。之前关于增加一个新驱动和设备文件的描述以及后面的介绍都假定devfs不存在。本章后面会填补这个空挡,见“The Device Filesystem”。
不使用devfs时,向系统增加一个新的驱动程序就是要为其指定一个主设备号。应该在驱动程序(或模块)初始化时,通过调用下面的函数来指定主设备号,这个函数定义在<linux/fs.h>中:
int register_chrdev(unsigned int major,const char *name, struct file_operations *fops);
函数返回值指示操作的成功或失败。出错时返回一个负值,成功时返回0或正值。参数major是申请的主设备号;name是设备名,将出现在/proc/devices(是要在加载模块前手动添加到该文件中呢,还是加载模块时会自动添加到该文件中?);fops则是指向一个函数指针数组的指针(即函数指针数组的首地址,在中文版中,将函数指针数组翻译为跳转表),用于完成对设备函数的调用,在“File Operations”一节有详细说明。
主设备号是一个用来索引静态字符设备驱动的整数;“Dynamic Allocation of Major Numbers”一节将叙述如何选择一个主设备号。2.0内核支持128种设备;2.2和2.4内核则增加到256种。次设备号也是8位二进制数,但它并不传给register_chrdev因为如前所述,它只被驱动程序使用。
一旦将设备注册到内核表中,对它的操作就与给定的主设备号相联系。无论何时对与主设备号相匹配的字符设备文件进行操作,内核都会从file_operations结构体中找出并调用相应的函数。因此,传递给register_chrdev的指针应该指向一个驱动程序内的全局结构体而非仅在模块初始化函数内有效的局部结构体(即在驱动程序中,这个file_operation类型的结构体应该定义在包括模块初始化函数在内的所有函数外部)。
下一个问题是如何给程序提供一个设备文件,通过这个设备文件,程序就可以请求你的驱动程序。设备文件必须放到/dev目录下并且与你的设备的驱动程序的主设备号和次设备号相关联。
在文件系统上创建一个设备节点的命令是mknod;执行该命令必须有超级用户的特权。除了要创建的设备文件的名字以外,该命令还带3个参数。如下例:
mknod /dev/scull0 c 254 0
创建了一个字符设备(c),其主设备号是254,次设备号是0。次设备号的范围是0到255。
注意,一旦由mknod命令创建了一个设备文件,则该设备文件会一直存在除非被显式删除(例如rm /dev/scull0),这与保存在磁盘上的其他数据一样。
3.2.1 Dynamic Allocation of Major Numbers
某些主设备号已经静态地指派给了大部分常见设备。在内核源代码树的Documentation/device.txt文件中可以找到这些设备的列表。由于许多编号已经分配了,为新设备选择一个唯一的编号是很困难的——可配置的设备要比主设备号多得多。
所幸,可以对主设备号进行动态分配。如果调用register_chrdev时将major设为0,则该函数会自动选择一个空闲的号码并返回作为该设备的主设备号。返回的主设备号总是正值,而返回负值时表明出错。注意如下两种情况的细微差别:若调用者请求一个动态的主设备号时函数register_chrdev返回值为所分配的主设备号,而当成功地注册到一个预先定义的主设备号时(既不采用动态分配而采用静态指派方式),函数返回值为0而非主设备号。
对于private dirvers,强烈建议使用动态分配的方法来得到主设备号。相反,如果你的设备普遍应用在大多数场合甚至要被包含在官方的内核树中,你就需要指派一个主设备号作为专用。
动态分配的缺点是:由于分配给你的主设备号不能保证总是一样的,因而你无法用mknod命令事先创建设备节点(即设备文件)。这意味着你将不能使用Chapter 11中介绍的关于loading-on-demand of your driver的先进特性。对于用于一般用途的驱动程序,这不是什么问题,因为一旦分配了设备号,你就可以从/proc/devices读取相关的设备号信息。
为了加载一个用动态分配来得到主设备号的驱动程序,对insmod的调用需要被替换为一个简单的脚本,这个脚本先调用insmod,再读/proc/devices以得到主设备号,并创建设备文件。
/proc/devices文件一般有类似下面的内容:
Character devices:
1 mem
2 pty
3 ttyp
4 ttys
6 lp
7 vcs
10 misc
13 input
14 sound
21 sg
180 usb
Block devices:
5 fd
8 sd
11 sr
65 sd
66 sd
加载动态分配主设备号的模块的脚本可以使用象awk这类工具从/proc/devices文件中获取信息,并在/dev目录下创建文件。
(关于awk的使用见http://blog.chinaunix.net/u/23458/showart.php?id=173202)
下面这个脚本,scull_load,是scull发行中的一部分。使用以模块形式发行的驱动程序的用户可以从系统的rc.local文件中调用这个脚本,或者在需要模块时手工调用。rc.local可在/etc/rc.d/下找到。(中文版中还提到一种方法:使用kerneld。)
#!/bin/sh
module=”scull”
device =”scull”
mode =”664”
#invoke insmod with all arguments we were passed
#and use a pathname, as newer modutils don’t look in. by default
/sbin/insmod –f ./$module.o $* ||exit
#remove stale nodes
rm –f /dev/$(device)[0-3]
major=’awk “\\$2==\”$module\” {print \\$1}” /proc/devices’
mknod /dev/$(device)0 c $major 0
mknod /dev/$(device)1 c $major 1
mknod /dev/$(device)2 c $major 2
mknod /dev/$(device)3 c $major 3
#give appropriate group/permissions, and change the group.
#Not all distributions have staff; some have “wheel” instead.
group=”staff”
grep ‘^staff:’ /etc/group > /dev/null || group=’wheel’
chgrp $group /dev/${device}[0-3]
chmod $mode /dev/${device}[0-3]
只要重新定义脚本中的变量并对mknod命令行进行修正该脚本同样可以用于其驱动程序。
读者可能对最后几行迷惑不解:为什么要改变设备的组(group)和权限(mode)呢?原因是该脚本只能由超级用户运行,因而新建的设备文件也属于root。默认权限位只允许root对其有写访问权限,而其他用户只有读权限。通常设备文件需要不同的访问策略,比如只对某一组用户开放访问权限,因而需要改变某些情况下的访问权限。在Chapter 5的“Access Control on a Device File”部分,sculluid代码将说明驱动程序如何实现自己的设备访问授权。
随后的scull_unload脚本则用于清理/dev目录并卸载模块。
作为成对使用加载(load)和卸载(unload)模块的脚本的一个变化,我们可以写一个初始化(init)脚本,放于所发布的驱动程序的目录下,同时实现这两个脚本的功能[1]。作为scull源代码的一部分,我们给出一个相当完整的初始化脚本的可配置化的例子,叫scull.init;它接受常见的参数——“start”,“stop”或“restart”等——并且承担scull_load和scull_unload双重角色。
如果重复地创建和删除/dev下的节点有点过分的话,有一个解决办法。如果你仅加载和卸载单个驱动程序,你只需在你第一次使用脚本来创建设备文件之后使用rmmod和insmod即可:动态的设备号不是随机化的,在你不会干扰其他的动态模块的情况下你可以指望选择同样的设备号。不过这个技巧显然不能适用于同时对多个驱动程序的情况。
在我看来,最好的指派主设备号的办法是,默认采用动态分配,同时在加载模块时,甚至在编译时,将指定主设备号的选择余地留给自己(driver writer)。我们建议使用的代码与自动端口检测的代码十分相似。Scull的实现使用了一个全局变量scull_major,用来保存所选择的主设备号。该变量初始化为SCULL_MAJOR,其缺省值是0,表示“使用动态分配”,它在scull.h中定义。这样,用户就可以选择接受缺省的动态分配方式,还是另外选择一个特定的主设备号,这即可以在编译之前通过修正SCULL_MAJOR这个宏的值来实现,也可以在使用insmod加载模块时在命令行中指定一个scull_major的值来实现。最后,通过使用scull_load脚本,用户可以在scull_load的命令行中将参数传递给insmod。
这是我在scull源代码中使用的获取主设备号的代码:
result = register_chrdev(scull_major, “scull”, &scull_fops);//返回负值表示出错;
if (result<0){
printk(KERN_WARNING “scull: can’t get major %d\n”, scull_major);
return result;
}
if (scull_major == 0) scull_major = result; /* dynamic */
3.2.2 Removing a Driver from the System
当从系统卸载模块时,必须释放其主设备号。这通过在模块的cleanup函数中加入unregister_chrdev函数来实现,格式如下:
int unregister_chrdev(unsigned int major, const char *name);
函数的major和name参数分别是要卸载的相关设备的主设备号和设备名。内核会比对与该主设备号对应的模块在注册(register)时的设备名:如果两者不一致,则返回-EINVAL。如果主设备号超出了允许的范围,内核同样会返回-EINVAL。
在cleanup_module函数中注销资源失败会产生很不好的后果,当你下次试图读取/proc/devices时会出错,因为该文件中的一个name(设备名)仍然指向该模块以前的内存空间,而那片内存已经不存在了(no longer mapped)。这种错误称为oops,当访问无效地址时,内核就会打印该信息。
当你卸载驱动程序而又没能注销主设备号时,这种情况很难恢复,因为unregister_chrdev中调用了strcmp函数,strcmp使得必须使用与之前的模块不同的设备名(name)。一旦你注销主设备号失败,则除了必须重新加载同一模块,还要加载另一个用来注销主设备号的模块。幸运的话,这个有问题的模块能获取与上次相同的地址,并且设备名字符串(name string)将保存在同一位置,如果你没有改变模块代码的话。当然更安全的办法是重新引导你的系统。
除了卸载模块,你还经常需要在卸载驱动程序时删除设备节点(设备文件)。这项工作可通过与加载时相成套的脚本来完成。我们的例子中使用scull_unload脚本来完成这个任务,当然另一种办法是象前面提到的使用scull.init脚本,加命令参数stop。
如果动态设备文件没能从/dev中删除,则可能造成不可预知的错误:如果为两个驱动程序动态分配的主设备号相同,开发者计算机上的一个空闲(spare,空闲的,多余的)的/dev/framegrabber就可能在一个月后引用一个报警设备。“No such file or directory”要比新的驱动程序所产生的打开/dev/framegrabber的后果要好得多。
问题:卸载驱动程序或模块时,没能注销主设备号这种情况是什么原因导致的呢?动态的设备文件没能删除又是什么原因导致的呢?如果在unregiter模块时,major与name不一致,是否能卸载与major对应的模块?是否能卸载与name对应的模块?还是两者都不能卸载,只会返回错误码?
3.2.3 dev_t and kdev_t
至此我们已经讨论了主设备号。下面讨论次设备号以及驱动程序是如何使用次设备号来区分设备的。
内核每次调用一个驱动程序时,它都告诉驱动程序它正在操作哪个设备。主设备号和次设备号合起来构成一个数据类型并供驱动程序来识别特定的设备。这个复合的设备号(主、次设备号合起来)驻留在稍后介绍的“inode”结构中的i_rdev域中。一些驱动函数接收一个指向inode结构的指针作为第一个参数(注:驱动程序中的init函数中的register_dev中规定第一个参数为主设备号,所以程序员可以另外写一个注册函数,使得这个注册函数可以接收指向inode结构的指针,然后从中分解出主设备号来传递给register_dev函数)。这个指针通常也称为inode,函数可以通过查看inode->i_rdev分解出设备号。
历史上,Unix使用dev_t(device type)来保存设备号。它过去是定义在<sys/types.h>中的一个16-bit的整型值(主、次设备号各占8-bit)。现在,经常需要超过256个次设备号,但是由于有许多应用(程序)都“know”(了解)dev_t的内部结构,如果改变了dev_t的内部结构就会造成这些应用无法运行,因此改变dev_t是很困难的。
在Linux内核内部却使用了一个新类型kdev_t。对于每个内核函数来说,这个新数据类型都被设计为一个黑箱。用户程序对kdev_t全然不知,而内核函数则不知道kdev_t的内部结构和机制(只知道入口和出口)。如果kdev_t一直是隐藏的,它可以在内核的不同版本间根据需要而变化,而不必修改每个人的设备驱动程序。
kdev_t的相关信息被禁闭在<linux/kdev_t.h>中,其中大部分是注释。因为<linux/fs.h>中已经包含了这个头文件,因此在驱动程序中不必显式地包含这个文件。
如下这些宏和函数是你可以对kdev_t执行的操作:
MAJOR(kdev_t dev);
从kdev_t结构中分解出主设备号。
MINOR(kdev_t dev);
分解出次设备号。
MKDEV(int ma, int mi);
通过主设备号和次设备号创建一个kdev_t。
kdev_t_to_nr(kdev_t dev);
将一个kdev_t类型转换为一个整数(dev_t)。
to_kdev_t(int dev);
将一个整数转换为kdev_t。注意内核模式中没有定义dev_t,因此使用了int。
只要你的代码中使用了这些操作来处理设备号,即使内部数据结构改变时,它仍能继续工作。
3.3 File Operations
在接下来的几节中,我们将看看驱动程序可对相关设备所进行的各种操作。在内核内部,一个打开的设备是通过一个file结构来标识的,内核使用file_operations结构来访问驱动程序的函数。它们都在<linux/fs.h>中定义,file_operations是一个函数指针数组。每个文件(包括设备文件在内)都有自己的一套函数集(通过包含一个叫做f_op的指向一个file_operations结构的字段(field)来实现)。这些操作大部分负责执行系统调用,因而命名为open,read等。我们可以把文件认为是一个“object”(对象),而把这些对它的函数操作看作是其“methods”,使用面向对象的编程技术来表示由对象声明的关于自身的操作。
通常,一个file_operations结构体或者是指向它的一个指针称为fops;我们已经见过一个这样的指针作为一个参数传递给register_chrdev调用。该结构中的各个域必须指向驱动程序中用来执行某一特定功能和操作的函数,或者定义为NULL来表明不支持的操作。各个函数在定义为NULL时,内核的反应是不一样的,在这一节的后面会给出列表。
随着不断向内核添加新的功能,file_operations结构变得越来越大。当然,新增的操作可能会引发设备驱动程序的兼容性问题。在驱动程序中的该结构的初始化常使用标准C语法,新的操作通常加在结构的尾部;重新编译驱动程序时会在这个新操作的位置插入一个NULL值,因此选择默认行为就可以了。
后来,内核开发者转向了一个“tagged”(标签化的)初始化格式,允许通过操作名字(name)来初始化结构的域,这样就可解决由于数据结构改变而带来的大部分问题。标签化的初始化并不是标准C语法但却是一种对GNU编译器的非常有用的扩展。
下面的列表介绍了应用程序所能够对设备进行的所有的操作的调用函数,作为一个简要的参考。
本章的余下部分,在描述另一个重要的数据结构之后(file结构体,实际上它包含了一个指向其自身file_operations结构体的指针),说明了大部分重要操作的角色,并提供了相关提示、忠告和代码实例。后面的章节还会涉及更复杂的操作,这会涉及到象内存管理、块操作等内容。
(暂略file_operations结构中的操作函数列表)。
scull设备驱动程序只实现了大部分重要的设备操作,并使用了标签化的格式来声明其file_operationgs结构:
struct file_operations scull_fops={
llseek: scull_llseek,
read: scull_read,
write: scull_write,
ioctl: scull_ioctl,
open: scull_open,
release: scull_release,
};
这中标签化的声明方式在结构体的定义发生变化时,使得驱动程序的兼容性更好,并使得代码更紧缩,可读性更强。标签化的初始化方式还允许按不同顺序排列结构体成员;在某些情况下,通过将频繁访问的结构体成员放置于相同的硬件缓冲线(hardware cache line)上,系统性能可显著提高。
通常还需要设置file_operations结构的owner域。在一些内核代码中,你会经常看见owner与该结构的其他成员一起初始化,使用标签化的语法如下:
owner:THIS_MODULE,
这种方法对2.4内核有效。一个兼容性更好的办法是使用SET_MODULE_OWNER宏,它定义在<linux/module.h>中。scull对其如下初始化:
SET_MODULE_OWNER(&scull_fops);
这个宏对任何含有owner域的结构都有效;在本书的后续章节还会提到这个域。
3.4 The file Structure
定义在<linux/fs.h>中的struct file是设备驱动程序中又一个最重要的数据结构。注意,file与用户程序中的FILE没有任何关联。FILE是在C库中定义并且从不出现在内核代码中。而struct file是一个内核结构,从不出现在用户程序中。
file结构代表一个打开的文件(an open file)。(并不只针对设备驱动程序而言,系统中的每个打开的文件在内核空间都有与其相关的struct file。)它由内核在open时创建并传递给任何对该文件进行操作的函数,直到close。文件关闭后,内核释放这个数据结构。一个打开的文件与struct inode所表示的“磁盘文件”不同。
在内核代码中,指向struct file的指针通常称为file或者filp(“file pointer”)。为了避免与这个数据结构自身相混淆,我们一直称这个指针为filp。这样,file就表示这个结构本身,而filp则是指向该结构的指针。
注:filp指向struct file,struct file中又有成员struct file_operations *f_op,f_op就是指向相关文件操作结构的指针(函数数组指针),则filp->f_op就是相应的struct file(打开的文件)的相关操作函数数组的指针。
struct file中最重要的域或字段(field)罗列如下。(暂略)
3.5 Open and Release
现在我们已经快速地浏览了这些域(字段),下面我们开始在实际的scull函数中使用这些字段。
注:open方法和release方法分别对应于file_operations结构中的int (*open)(struct inode *, struct file *)和int (*release)(struct inode *, struct file *)两个字段。
3.5.1 The open Method
open方法是驱动程序用来为以后的操作完成初始化准备工作的。此外,open还会增加设备的使用计数(usage count),这样就可防止模块在文件被关闭前被卸载。Chapter 2中“The Usage Count”部分描述的使用计数(count)会在release方法中削减。
在大部分驱动程序中,open完成以下工作:
l 增加使用计数(usage count)
l 检查设备相关的错误(诸如设备未就绪或类似的硬件问题)
l 如果是首次打开设备,还要完成对设备的初始化
l 识别次设备号,如有必要则更新f_op指针
l 分配和填写要放在filp->private_data中的数据
在scull中,上面的大部分操作都要依赖于被打开的设备的次设备号。因此,第一件事是识别要操作的是哪个设备。这可以通过查看inode->i_rdev完成。
我们已经讨论过,内核不使用设备的次设备号,因此驱动程序可以随意使用次设备号。事实上,不同的次设备号用于访问不同的设备,或以不同的方式打开同一设备。例如,/dev/st0(次设备号为0)和/dev/st1(次设备号为1)表示不同的SCSI磁带驱动器,而/dev/nst0(次设备号为128)却与/dev/st0对应同一物理设备,只是操作上不同(当它关闭时不重绕磁带)。所有的磁带设备文件都有不同的次设备号,以便驱动程序把它们区分开来。
驱动程序从来不知道所打开的设备的名字,它只知道设备号——并且用户可以为方便起见而给设备起别名,而完全不用原有的名字。如果你创建2个具有相同的主/次设备号的设备文件,则对应的设备其实是同一个,并且无法区分它们。使用一个符号链接或硬链接也会有相同的效果,而给设备起别名的推荐方法就是创建一个符号链接。
scull驱动程序是这样使用次设备号的:次设备号一共为8bits,最高的单元组(nibble)即高4位识别设备类型(personality),低4位则供你识别不同的设备个体,如果这个设备类型支持多个设备实体(包括对同一设备的不同操作规则)的话。这样,scull0与scullpipe0的次设备号的高4位不同,而scull0与scull1则是低4位不同[1]。源代码中定义了两个宏(TYPE和NUM),用于从设备号中解析出(extract)这些位,如下:
# define TYPE(dev) (MINOR(dev) >> 4) /* high nibble */MINOR()函数见3.2.3
# define NUM(dev) (MINOR(dev) & 0xf) /* low nibble */
——————
[1]划分位区(bit splitting)是使用次设备号的一种典型方法。注意这里的位区划分方法是scull驱动程序定义的,不同的设备驱动有不同的定义。例如IDE驱动程序,使用高2位识别磁盘号,低6为识别分区号。
对于每一设备类型,scull定义了一个相关的file_operations结构,并在open时放于filp->f_op。下面的代码描述了多个fops的实现:
struct file_operations *scull_fop_array[]={
&scull_fops, /* type 0 */
&scull_priv_fops, /* type 1 */
&scull_pipe_fops, /* type 2 */
&scull_sngl_fops, /* type 3 */
&scull_user_fops, /* type 4 */
&scull_wusr_fops, /* type 5 */
};
# define SCULL_MAX_TYPE 5
/* In scull_open, the fop_array is used according to TYPE(dev) */
int type = TYPE(inode->i_rdev);
//inode->i_rdev中保存了复合的设备号,TYPE是前面定义的宏操作,得到次设备号高4位信息。
if (type > SCULL_MAX_TYPE) return –ENODEV;
filp->f_op = scull_fop_array[type];
内核根据主设备号来调用open;驱动程序scull使用上面给出的宏来处理次设备号。TYPE则用于索引scull_fop_array数组,以从中解析出被打开设备的操作方法集。
在scull中,filp->f_op被赋值为由次设备号中规定的设备类型(高4位)所决定的正确的file_operations结构。然后调用新的fops中定义的open方法。(总的驱动程序的open方法中会象上面这样识别次设备号,这里不同的次设备号对应有不同类型的设备个体或设备操作方法集即file_operations结构,这个结构中又有自己的open方法。可以这样理解,多个设备或多种操作共用同一驱动程序时,实际上是一种层次化的驱动程序结构)。通常,驱动程序不调用自己的fops,因为它们被内核用来分派正确的驱动程序方法。但当你的open方法必须处理不同设备类型时,在根据被打开设备的次设备号修正fops指针之后,就可能需要调用fops->open了。
scull_open的实际代码如下。它使用了前面的代码段中定义的TYPE和NUM两个宏来划分次设备号:
int scull_open(struct inode *inode, struct file *filp)
{
Scull_Dev *dev; /* device information */
int num = NUM(inode->i_rdev);
int type = TYPE(inode->i_rdev);
/ *
* If private data is not valid, we are not using devfs
* so use the type (from minor nr.) to select a new f_op
*/
if (!filp->private_data && type) {
if (type > SCULL_MAX_TYPE) return –ENODEV;
filp->f_op = scull_fop_array[type];
return filp->f_op->open (inode, filp); /* dispatch to specific open */
}
/* type 0, check the device number (unless private_data valid) */
dev = (Scull_Dev *)filp->private_data;
if (!dev) {
if (num >= scull_nr_devs) return –ENODEV;
dev = &scull_devices[num];
filp->private_data = dev; /* for other methods */
}
MOD_INC_USE_COUNT; /* Before we maybe sleep */
/* now trim to 0 the length of the device if open was write-only */
if ((filp->f_flags & O_ACCMODE) == O_WRONLY) {
if (down_interruptible (&dev->sem)) {
MOD_DEC_USE_COUNT;
return –ERESTARTSYS;
}
scull_trim (dev); /* ignore errors */
up (&dev->sem);
}
return 0; /* success */
}
用来保存内存区域的数据结构是Scull_Dev;全局变量scull_nr_devs和scull_devices[](全部小写)分别是可用设备数和指向Scull_Dev的指针数组。
对down_interruptible和up的调用暂时忽略。
这段代码看起来工作很少,因为在调用open时它不做任何针对特定设备的处理。它也不需要做什么,因为设备scull0-3被设计为是全局的永久性的。特别地,由于我们没有为sculls维护其打开计数(即模块的使用计数),因而也没有类似于“首次打开时初始化设备”的动作。
既然内核可以通过file_operations结构中的owner字段来维护模块的使用计数,你可能不明白为什么我们在此要人工增加这个计数。原因是老版本的内核需要模块来完成所有关于维护其使用计数的工作——owner机制以前并不存在。为与老版本内核兼容,scull自增加其使用计数。这种行为将导致使用计数在2.4内核的系统上变得过大,但并不是什么问题,因为当模块不被使用时,使用计数就会变为0。
唯一在设备上的实际操作是,当打开设备用于写操作时,将设备截断为长度0。这样做的原因是,截断是scull设计的一部分:用一个较短的文件覆盖设备,以便缩小设备数据区。这与打开一个普通文件用于写操作时将其截断为长度0很相似。如果设备被打开用于读操作,则这个动作不做任何事情。
稍后在查看其他scull个体(personalities)时,我们将看到一个真正的初始化工作是如何完成的。
3.5.2 The Release Method
release方法是open方法逆过程。有时device_release又叫做device_close。它要完成的任务如下:
l 释放(deallocate)所有由open在filp->private_data中分配的内存和资源
l 在最后一次关闭操作时关闭设备
l 使用计数减1
scull的基本模型中没有要关闭的硬件,因而只需最少的代码就可以完成释放操作[5]:
[5]由于scull_open为每个设备都用不同的fops替换了filp->f_ops,因而不同种类的设备会使用不同的函数完成关闭操作。这一点我们将在后面看到。
int scull_release(struct inode *inode, struct file *filp)
{
MOD_DEC_USE_COUNT;
return 0;
}
如果你在open时增加了使用计数,则在此将使用计数减1是非常重要的,因为如果使用计数不降为0的话,内核将无法卸载模块。
如果一个从没被打开的文件被关闭了又如何使计数保持一致呢?不管怎样,dup和fork这两个系统调用都会在不调用open的情况下将一个打开的文件复制为两个;每个copy都会在程序终止时关闭。例如,大多数程序并不打开它们的stdin文件(或设备),但它们都会在终止时关闭它。
答案很简单:并不是每次进行close系统调用时都会引起release方法的调用。只有这些释放(release)设备数据结构的close系统调用才会调用release方法——就与它的名字一样。内核有一个计数器用于保存某个file结构被使用的次数。fork和dup都不会创建新的file结构(只有open会创建file结构);它们只是增加对已有结构的计数。
仅当一个file结构被清除,这时对file结构的计数变为0,close系统调用才执行release方法。这种release方法与close系统调用之间的关系保证了对模块的使用计数总是一致的(即不会发生混乱)。
注意,每当应用程序调用close时,flush方法也被调用。然而,很少有驱动程序执行flush,因为除非与release有关,否则通常在关闭的时候没有什么要做的。
你可能会想,前面的讨论甚至在没有显式地关闭其打开的文件的情况下终止应用程序时,仍然有效:内核会通过内部的close系统调用在退出进程时自动关闭相关文件。
3.6 scull’s Memory Usage
在介绍读写操作之前,我们最好了解以下scull是如何进行内存分配的,以及为什么要这样分配。“How”需要透彻理解代码,“why”则说明驱动程序设计人员需要如何作出选择,尽管scull绝不是一个典型设备。
这一节只讨论scull中内存的分配策略,并不涉及在写实际驱动程序时需要的硬件管理技巧。这些技巧将在Chapter 8和Chapter 9中讲到。因此,如果你对内存操作的scull驱动程序的内部工作原理不感兴趣的话,可以跳过这节。
(暂略)