分类: LINUX
2006-12-02 17:00:11
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时,向系统增加一个新的驱动程序就是要为其指定一个主设备号。应该在驱动程序(或模块)初始化时,通过调用下面的函数来指定主设备号,这个函数定义在
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 “”$module\” {print }” /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的后果要好得多。
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)来保存设备号。它过去是定义在
现在的Linux内核内部使用了一个新类型kdev_t。对于每个内核函数来说,这个新数据类型都被设计为一个黑箱。用户程序对kdev_t全然不知,而内核函数也不知道kdev_t的内部结构和机制(只知道入口和出口)。如果kdev_t一直是隐藏的,它可以在内核的不同版本间根据需要而变化,而不必修改每个人的设备驱动程序。
kdev_t的相关信息被禁闭在
如下这些宏和函数是你可以对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结构来访问驱动程序的函数。它们都在
通常,一个file_operations结构体或者是指向它的一个指针称为fops;我们已经见过一个这样的指针作为一个参数传递给register_chrdev调用。该结构中的各个域必须指向驱动程序中用来执行某一特定功能和操作的函数,或者定义为NULL来表明不支持的操作。各个函数在定义为NULL时,内核的反应是不一样的,在这一节的后面会给出列表。
随着不断向内核添加新的功能,file_operations结构变得越来越大。当然,新增的操作可能会引发设备驱动程序的兼容性问题。在驱动程序中的该结构的初始化常使用标准C语法,新的操作通常加在结构的尾部;重新编译驱动程序时会在这个新操作的位置插入一个NULL值,因此选择默认行为就可以了。
后来,内核开发者转向了一个“tagged”(标签化的)初始化格式,允许通过操作名字(name)来初始化结构的域,这样就可解决由于数据结构改变而带来的大部分问题。标签化的初始化并不是标准C语法但却是一种对GNU编译器的非常有用的扩展。
下面的列表介绍了应用程序所能够对设备进行的所有的操作的调用函数,作为一个简要的参考。
本章的余下部分,在描述另一个重要的数据结构之后(file结构体,实际上它包含了一个指向其自身file_operations结构体的指针),说明了大部分重要操作的角色,并提供了相关提示、忠告和代码实例。后面的章节还会涉及更复杂的操作,这会涉及到诸如内存管理、阻塞型操作等内容。
下面给出了2.4系列内核中file_operations结构所包括的操作。各操作的返回值如果为0表示成功,为负则说明发生了错误,除非另外指出。
loff_t ( *llseek ) ( struct
file *, loff_t, int ) ;
方法llseek用来修改文件的当前读写位置,并将新位置作为(正的)返回值返回。参数loff_t是一个“长偏移量”,即使在32位平台上也至少占用64位数据宽度。出错时返回一个负值。如果驱动程序没有设置这个函数,那么相对于文件尾(end-of-file, EOF)的定位操作就会失败,而其他的定位操作将修改file结构(本章稍后在“file结构”一节介绍)中的位置计数器并成功返回。
ssize_t ( *read ) ( struct
file *, char *, size_t, loff_t * ) ;
用来从设备中读取数据。当该函数指针被赋为NULL时,将导致read系统调用出错并返回-EINVAL(Invalid argument)。函数返回非负值表示成功读取的字节数(返回值为“signed size”型,通常就是目标平台上的固有整数类型)。
ssize_t ( *write ) ( struct
file *, const char *, size_t, loff_t * ) ;
向设备发送数据。如果没有这个函数,write系统调用会向调用它的(用户)程序返回一个-EINVAL。如果返回非负值表示成功写入的字节数。
int ( *readdir ) ( struct
file *, void *, filldir_t ) ;
对于设备文件来说,这个字段应该为NULL。它仅用于读取目录,并只对文件系统有效。
unsigned int ( *poll ) (
struct file *, struct poll_table_struct * ) ;
poll方法是poll和select这两个系统调用的后端实现。这两个系统调用可用来查询设备是否可读或可写,或是否处于某种特殊状态。这两个系统调用是可阻塞的,直至设备变为可读或可写状态为止。如果驱动程序没有定义它的poll方法,它所驱动的设备就会被认为既可读也可写,并且不会处于其他的特殊状态。返回值是一个描述设备状态的位掩码。
int ( *ioctl ) ( struct
inode *, struct file *, unsigned int, unsigned long ) ;
ioctl系统调用提供了一种执行设备特定的命令的方法(如格式化软盘的某个磁道,这既不是读操作也不是写操作)。另外,内核还能识别一部分ioctl命令,而不必调用fops表中的ioctl。如果设备不提供ioctl入口点,则对于任何内核未预先定义的ioctl请求,ioctl系统调用将返回错误-ENOTTY。如果该方法返回一个非负值,则相同的值会被返回给调用程序以表示调用成功。
int ( *mmap ) ( struct file
*, struct vm_area_struct * ) ;
mmap用于请求将设备内存映射到进程地址空间。如果设备没有实现这个方法,那么mmap系统调用将返回-ENODEV。
int ( *open ) ( struct
inode *, struct file * ) ;
尽管这始终是对设备文件执行的第一个操作,然而却并不要求驱动程序一定要声明这个方法。如果定义为NULL,则设备的打开操作将永远成功,但系统不会通知驱动程序。
int ( *flush ) ( struct file
* ) ;
对flush操作的调用发生在进程关闭(本进程下)设备文件描述符副本的时候,它应该执行(并等待)设备上尚未完成的操作。请不要将它同用户程序使用的fsync操作相混淆。目前,flush仅用于网络文件系统(NFS)代码中。如果flush被置为NULL,则它只是简单地不被调用。
int ( *release ) ( struct
inode *, struct file * ) ;
当file结构被释放时,将调用这个操作。与open相仿,也可以没有release。(注意:release不是在进程每次调用close时都会被调用。只要file结构被共享——如在fork或dup调用之后,release就会等到所有的副本都关闭之后才会得到调用。如果希望在关闭任意一个副本时,刷新那些待处理的数据,则应实现flush方法)。
int ( *fsync ) ( struct
inode *, struct dentry *, int ) ;
该方法是fsync系统调用的后端实现,用户调用它来刷新待处理的数据。如果驱动程序没有实现这一方法,fsync系统调用就返回-EINVAL。
int ( *fasync ) ( int,
struct file *, int ) ;
这个操作用来通知设备,它的FASYNC标志发生了变化。异步通知是比较高级的话题,将在Chapter 5介绍。如果设备不支持异步通知,那么该字段可以为NULL。
int ( *lock ) ( struct file
*, int, struct file_lock * ) ;
lock方法用于实现文件锁定,锁定是常规文件不可缺少的特性,但设备驱动程序几乎从来不会实现这个方法。
ssize_t ( *readv ) ( struct
file *, const struct iovec *, unsigned long, loff_t * ) ;
ssize_t ( *writev ) (
struct file *, const struct iovec *, unsigned long, loff_t * ) ;
这些方法用来实现“分散/聚集(scatter/gather)”型的读写操作(2.3内核新增)。应用程序有时需要进行涉及多个内存区域的单次读或写操作,利用这两个系统调用可完成这类工作,而不必强加额外的数据拷贝操作。
struct module *owner ;
这个字段并不是一个方法。它是指向“拥有”该结构的模块的指针,内核使用该指针维护模块的使用计数。
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宏,它定义在
SET_MODULE_OWNER(&scull_fops);
这个宏对任何含有owner域的结构都有效;在本书的后续章节还会提到这个域。
3.4 The
file Structure
定义在
file结构代表一个打开的文件(an
open file)。(并不只针对设备驱动程序而言,系统中的每个打开的文件在内核空间都有与其相关的struct file结构。)它由内核在open时创建并传递给任何对该文件进行操作的函数,直到close。文件的所有实例都关闭后,内核释放这个数据结构。一个打开的文件(即file结构体)与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)罗列如下。
mode_t f_mode ;
文件模式通过FMODE_READ和FMODE_WRITE位来标识文件是否可读或可写(或可读写)。由于内核在调用驱动程序的read和write之前已经检查了权限,因而不必在自己的ioctl函数中查看这两个字段来检查读写权限。例如,一个未得到允许的写操作在驱动程序还不知道的情况下就已经被内核拒绝了。
loff_t f_pos ;
当前的读/写位置。loff_t是一个64位的数(用gcc的术语说就是long long)。如果驱动程序需要知道文件中的当前位置,就可以读取这个值,但不能修改它(read和write会使用它们接收到的最后那个指针参数来更新这一位置,而不是直接对filp->f_pos操作)。
unsigned int f_flags ;
文件标志,如O_RDONLY、O_NONBLOCK和O_SYNC。驱动程序为了支持非阻塞型操作需要检查这个标志,而其他标志很少用到。注意,检查读/写权限应该查看f_mode而不是f_flags。所有这些标志都定义在
struct
file_operations *f_op ;
与文件相关的操作。内核在执行open操作时对这个指针赋值,以后需要处理这些操作时就读取这个指针。filp->f_op中的值决不会为方便引用而保存起来,也就是说,你可以在任何需要的时候修改文件的关联操作,在返回给调用者之后,新的操作方法就会立即生效。例如,对应于主设备号1(/dev/null、/dev/zero 等等)的open 代码根据要打开的次设备号替换filp->f_op 中的操作。这种技巧允许相同主设备号下的设备实现多种操作行为,而不会增加系统调用的负担。这种替换文件操作的能力在面向对象编程技术中称为“方法重载”。
void *private_data;
open系统调用在调用驱动程序的open方法前将这个指针置为NULL。驱动程序可以将这个字段用于任何目的或者忽略这个字段。驱动程序可以用这个字段指向已分配的数据,但是一定要在内核销毁file结构前在release方法中释放内存。private_data是跨系统调用时保存状态信息的非常有用的资源,我们的大部分示例都使用了它。
struct dentry *f_dentry ;
文件对应的目录项(dentry)结构。驱动开发者不用关心该结构。
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) & 0x
——————
[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被设计为是全局的永久性的。特别地,由于我们并不维护scull的打开计数,而只维护模块的使用计数,因而也没有类似于“首次打开时初始化设备”的动作。
既然内核可以通过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的情况下,创建已打开的文件的副本,但每个副本都会在程序终止时被关闭。例如,大多数程序并不打开它们的stdin文件(或设备),但它们都会在终止时关闭它。
答案很简单:并不是每次进行close系统调用时都会引起release方法的调用。只有这些释放(release)设备数据结构的close系统调用才会调用release方法——就与它的名字一样。内核有一个计数器用于保存某个file结构被使用的次数。fork和dup都不会创建新的file结构(只有open会创建file结构);它们只是增加对已有结构的计数。
仅当一个file结构被清除,这时对file结构的计数变为0,close系统调用才执行release方法。这种release方法与close系统调用之间的关系保证了对模块的使用计数总是一致的(即不会发生混乱)。
注意,每当应用程序调用close时,flush方法也被调用。然而,很少有驱动程序执行flush,因为通常在关闭(close)的时候除了调用release外,没有什么要做的。
你可能会想,前面的讨论甚至在没有显式地关闭其打开的文件的情况下终止应用程序时,仍然有效:内核在退出进程时,会通过内部的close系统调用自动关闭相关文件。
3.6 scull’s
Memory Usage
在介绍读写操作之前,我们最好了解以下scull是如何进行内存分配的,以及为什么要这样分配。“How”需要透彻理解代码,“why”则说明驱动程序设计人员需要如何作出选择,尽管scull绝不是一个典型设备。
这一节只讨论scull中内存的分配策略,并不涉及在写实际驱动程序时需要的硬件管理技巧。这些技巧将在Chapter 8和Chapter 9中讲到。因此,如果你对内存操作的scull驱动程序的内部工作原理不感兴趣的话,可以跳过这节。
scull使用的内存区域,就是这里所谓的设备,其长度是可变的。写的越多,它就变得越长;用更短的文件以覆盖方式写设备时则会变短。
scull的实现代码中只使用了kmalloc和kfree,而没有采取分配整个页面的方法,实际上分配整个页面会更有效。
另外,我们没有限制“设备”的尺寸。从理论角度看,对所管理的数据任意增加限制总是很糟糕的想法。从实际的角度看,为了在内存短缺的情况下进行测试,可利用scull暂时将系统的内存吃光。进行这样的测试有助于你了解系统内部。可以使用命令cp /dev/zero
/dev/scull0用关所有的系统RAM,也可以用dd工具选择复制多少数据到scull设备中。
在scull中,每个设备都是一个指针链表,其中每个指针都指向一个Scull_Dev结构。默认情况下,每个这样的结构通过一个中间指针数组最多可引用4000000个字节。我们发布的源代码使用了一个有1000个指针的数组,每个指针指向一个4000字节的区域。我们把每个内存区称为一个量子(quantum),而这个指针数组(或其长度)被称为量子集(quantum set)。scull设备和它的内存区见Figure 3-1。
为量子和量子集选择合适的数值是一个策略问题,而非机制问题,而且最优数值依赖于如何使用设备。因此scull设备的驱动程序不应对量子和量子集的尺寸强制使用某个特定的数值。在scull设备中,用户可以采用几种方式来修改这些值:在编译时,可以修改scull.h 中的宏SCULL_QUANTUM和SCULL_QSET;而在模块加载时,可以设置scull_quantum 和scull_qset 的整数值;或者在运行时,使用ioctl 修改当前值和默认值。
使用宏和整数值同时允许在编译期间和加载阶段进行配置,这种方法和前面选择主设备号的方法类似。对于驱动程序中任何不确定的或与策略相关的数值,我们都可以使用这种技巧。
余下的惟一问题是如何选择默认数值。在这个例子里,量子和量子集未充分填满会导致内存浪费,而量子和量子集过小则会在进行内存分配、释放和指针链接等操作时增加系统开销,默认数值的选择问题就在于寻找这两者之间的最佳平衡点。
此外,还必须考虑kmalloc 的内部设计,然而目前我们还无法涉及这一点。kmalloc的内部结构将在Chapter 7 “The Real Story of kmalloc”探讨。
默认数值的选择基于这样的假设,在测试scull 时,可能会有大块的数据写到其中,但大多数情况下,对该设备的正常使用可能只传递几千字节的数据量。
用来保存设备信息的数据结构如下:
typedef struct Scull_Dev {
void **data;
struct Scull_Dev *next; /* 下一个链表项 */
int quantum; /* 当前的量子大小 */
int qset; /* 当前的量子集大小 */
unsigned long size;
devfs_handle_t handle; /* 仅在devfs 方式下使用 */
unsigned int access_key; /* 由sculluid 和scullpriv 使用 */
struct semaphore sem; /* 互斥信号量 */
} Scull_Dev;
下面的代码片断说明了如何利用Scull_Dev来保存数据。scull_trim 函数负责释放
整个数据区,并且在文件以写方式打开时由scull_open 调用。它简单地遍历链表,
释放所有找到的量子和量子集。
int scull_trim(Scull_Dev *dev)
{
Scull_Dev *next, *dptr;
int qset = dev->qset; /*
"dev" 不能为null */
int i;
for (dptr = dev; dptr; dptr =
next) { /* 遍历所有的链表项 */
if (dptr->data) {
for (i = 0; i < qset; i++)
if (dptr->data[i])
kfree(dptr->data[i]);
kfree(dptr->data);
dptr->data=NULL;
}
next=dptr->next;
if (dptr != dev) kfree(dptr); /* 释放链表中除第一个节点以外的所有节点 */
}
dev->size = 0;
dev->quantum = scull_quantum;
dev->qset = scull_qset;
dev->next = NULL;
return 0;
}
3.7 A Brief
Introduction to Race Conditions(暂略)
3.8 read
and write
read方法和write方法完成类似的工作,即从应用程序拷贝数据或拷贝数据到应用程序。因此它们的原型非常相似。
ssize_t read(struct file *filp, char *buff, size_t
count, loff_t *offp);
ssize_t write(struct file *filp, const char *buff,
size_t count, loff_t *offp);
两种方法中,filp都是文件指针,count是所请求的数据传输的size(字节数);buff参数是一个指针,指向用户缓冲区,缓冲区中保存着要写的数据或者是准备存放新读入的数据的空的缓冲区;offp是一个指向“long
offset type”对象的指针,指示用户所访问的文件位置。read()和write()的返回值是“signed size type”(ssize_t类型),这个稍后讨论。
关于数据传输,这两个设备操作方法需要完成的主要任务是在内核地址空间与用户地址空间之间传输数据。使用通常的指针操作或memcpy函数是无法完成这个操作的。出于多种原因,在内核空间中是不能直接使用用户空间地址的。
内核空间地址与用户空间地址之间的一个显著差别是,用户空间的内存可以被交换出来(can be swapped
out)。当内核访问一个用户空间指针时,相关的页面可能不在内存中(而内核空间的页面总是在内存中),这样就产生一个页面错误或页面失效(page fault)。本章和Chapter 5 ”Using
the ioctl Argument”中介绍的函数使用了一些隐藏机制来恰当地处理页面失效的错误,甚至在CPU还在内核空间执行时。
注意到这一点是有趣的:Linux 2.0的x86端口对用户空间和内核空间使用了完全不同的内存映射。因此,用户空间指针不能从内核空间得到参照而解析出来。
如果目标设备是一个扩展板而非RAM区域,同样的问题也会发生,因为驱动程序必须在用户缓冲区和内核空间之间拷贝数据(也有可能是在内核空间和I/O memory之间)。
跨空间的拷贝在Linux中是通过定义在
scull中的read和write代码需要实现在用户地址空间和内核地址空间之间拷贝一整段数据。这个功能通过下面的内核函数来实现,它们可以拷贝任意字节长度的数组:
unsigned long copy_to_user(void *to, const void *from,
unsigned long count);
unsigned long copy_from_user(void *to, const void *from,
unsigned long count);
尽管这些函数完成与普通的memcpy函数类似的功能,但是当从内核代码中访问用户空间时必须还要注意额外的一个问题:所寻址的用户页面可能不在当前的内存中,在所请求的页面向指定位置传入过程中,页面失效处理器(page-fault handler)可能会使该进程进入休眠状态。例如,必须从交换区读取页面时就会发生这种情况。对于驱动程序员来说,净效果(net result)就是任何访问用户空间的函数都必须是可重入的(re-entrant),而且能够与其他驱动程序函数并发执行(见Chapter 5 “Writing
Reentrant Code”)。这就是我们使用信号量(semaphore)来控制并发访问的原因。
这两个函数的作用并不局限于从或者向用户空间拷贝数据:它们还检验用户空间的指针是否有效。如果指针无效,则不执行拷贝;另一方面,如果在拷贝过程中碰到无效地址,则只会拷贝部分数据。两种情况下,函数返回值都是仍待拷贝的内存字节数。这里scull代码发现这类错误时就会返回,如果不为0就返回-EFAULT给用户。
关于用户空间的访问以及无效的用户空间指针的深入讨论见Chapter 5 “Using
the ioctl Argument”。不过,值得一提的是,如果你不需要检查用户空间指针,你可以调用__copy_to_user和__copy_from_user用作替换。这非常有用,例如,如果你知道你已经检查了相应参数的时候。
谈到实际的设备方法,read方法的工作就是从设备向用户空间(使用copy_to_user)拷贝数据;write方法就是从用户空间向设备(使用copy_from_user)拷贝数据。每次read或write系统调用要求传输一定量的字节,但是驱动程序可自由传输较少的数据——读写的精确规则稍有不同,本章后面会讨论。
不管读写方法传输的数据量是多少,它们都要更新在*offp的文件位置,以便在成功完成系统调用后指向当前的文件位置。大部分时候offp参数仅仅是一个指向filp->f_pos(不是f_ops,这里是file
position),但是为了支持pread和pwrite系统调用,会用到一个不同的指针,这两个系统调用以单个原子操作来完成lseek和read或write的等价操作。
图3-2表示了一个典型的read操作是如何使用其参数的。
read和write方法在发生错误时都返回一个负值。一个大于等于0的返回值告诉调用程序一共成功传输了多少字节。如果在正确传输一些数据之后发生了错误,返回值则是成功传输的字节数,到下一次调用该函数时才会报错。
内核函数返回一个负数来指示错误,同时这个数值还指示不同类型的错误(见Chapter 2 “Error Handling in
init_module”),运行在用户空间的程序总会看到-1作为错误返回值。它们需要进一步访问变量errno来获知发生了什么错误。内核不处理errno。
3.8.1 The read Method
调用程序对read返回值的解释如下:
l
如果返回值等于count参数传递给read系统调用的值,所请求的字节数的传输就成功完成了,这是最好的情况。
l
如果返回值是正的,但是比count小,表明只有部分数据成功传送。这种情况因设备的不同可能有许多原因。大部分情况下,程序会重新读数据。例如,如果你使用fread函数读数据,这个库函数会不断进行系统调用直到所请求的数据传输完成。
l
如果返回值为0,它表示已经到达文件尾(end-of-file)。
l
负的返回值表明发生了错误。该负值指明发生了何种错误,并在
没有在上面的列表中列出来的一种情况是“没有数据,但以后可能会有”。这种情况下,read系统调用应该阻塞。我们将在Chapter 5 “Blocking
I/O”一节处理阻塞出入。
scull代码使用了这些规则。特别是,它采用了部分读(partial-read)规则。每次调用scull_read只处理单个数据量子(a single data
quantum),而不必执行循环操作收集所有数据,这使得代码更短更易读。如果读程序确实需要更多的数据,它可以重新调用scull_read。如果使用标准I/O库(如fread 等)来读取设备,应用程序就不会察觉到数据传送的量子化过程。
如果当前的读位置超出了设备尺寸(size),scull的read方法返回0来告知程序这里已经没有数据了(换句话说,已经到达文件尾)。如果进程A正在读设备,而此时进程B打开设备进行写操作,于是将设备截断为长度0,就会发生这种情况。进程A突然发现自己超出了文件尾,并且在下次调用read时返回0。
下面是read方法的代码:
3.8.2 The write Method
与read相似,根据如下返回值规则,write也可传输少于所请求的数据量:
l
若返回值等于count,则完成了所请求数目的字节传送。
l
若返回值是小于count的一个正数,则只传输了部分数据。程序很可能会再次读取余下的数据。
l
如果返回0,则什么也没写。这个结果不是错误,而且不会返回错误编码(error code)。再说明一次,标准库会重复调用write。Chapter 5 “Blocking
I/O”会介绍阻塞型write,届时将对这种情形作详尽考察。
l
返回负值表明有错误发生。与read一样,有效的错误值的定义在
很不幸,一些设计得不好的程序会在发生部分传输的情形时报错。因为有些程序员习惯于认为write调用的结果要么是成功要么就失败;当然大部分时候是这样,并且需要设备支持。这一缺陷在scull的实现中已得到修正,但我不想把代码搞的太复杂,能说明问题就行了。
与read方法一样,scull的write方法代码每次也只处理单个量子(quantum):
3.8.3 readv and writev
Unix系统长久以来都支持两个可选的系统调用叫做readv和writev。这些“矢量(vector)”型对一个结构体数组进行操作,每个结构体都含有一个指向一个缓冲区的指针和一个(缓冲区)长度值。readv调用可指定数量的数据轮流读取到各个缓冲区。writev则将每个缓冲区的内容聚合到一起并象单个写操作一样一次性地将数据取出。
而Linux则直到
矢量操作的函数原型如下:
ssize_t ( *readv
) ( struct file *filp, const struct iovec *iov, unsigned long count, loff_t
*ppos ) ;
ssize_t (
*writev ) ( struct file *filp, const struct iovec *iov, unsigned long count,
loff_t *ppos ) ;
这里,filp和ppos参数与read和write函数中的一样。iovec结构体定义在
struct iovec {
void
*iov_base;
_ _kernel_size_t
iov_len;
} ;
每个iovec 结构都描述了一个要传输的数据块——这个数据块的起始位置为iov_base(在用户空间中),长度为iov_len 个字节。函数中的count 参数指明要操作多少个iovec结构。这些结构由应用程序创建,而内核在调用驱动程序之前会把它们拷贝到内核空间。
向量化操作最简单的实现,可能就是只传递每个iovec结构的地址和长度给驱动程序的read 或write 函数。不过,正确而有效率的操作经常需要驱动程序做一些更为巧妙的事情。例如,磁带驱动程序的writev 就应将所有iovec结构的内容作为磁带上的单个记录写入。
但是,很多驱动程序并不期望通过自己实现这些方法来获益。所以,scull 忽略了它们。内核将会通过read/write 来模拟它们,而最终结果仍然如此。
3.9 Playing with the New Devices
准备好上述四个方法后,就可以编译和测试驱动程序了;它保留你写入的全部数据,直至用新数据覆盖它们。这个设备有点像长度只受物理RAM 容量限制的数据缓冲区。可以试试用cp、dd 或者输入/ 输出重定向等命令来测试这个驱动程序。
依据写入scull 的数据量,用free 命令可以看到空闲内存的缩减和扩增。
为了进一步证实每次是否只读写一个量子,可以在驱动程序的适当位置加入printk,通过它可以了解到程序读/ 写大数据块时会发生什么事情。此外,还可以用工具strace 来监视应用程序调用系统调用的情况以及它们的返回值。跟踪cp 或ls -l>/dev/scull0 会显示出量子化的读写过程。下一章将详细介绍监视(或调试)技术。
3.10 The
Device Filesystem
3.11 Backward
Compatibility
Quick Reference
本章介绍了下列符号和头文件。file_operations结构和file结构的字段清单并没有在这里给出。
#include
“文件系统”头文件,它是编写设备驱动程序必需的头文件。所有重要的函数都在这里声明。
int
register_chrdev(unsigned int major, const char *name,
struct
file_operations *fops);
注册字符设备驱动程序。如果主设备号不为0,则不加修改地使用;如果主设备号为0,系统将动态地给这个设备分配一个新设备号。
int
unregister_chrdev(unsigned int major, const char *name);
在卸载时注销驱动程序。major 和name 字符串都必须存放与注册驱动程序时相同的值。
kdev_t
inode->i_rdev;
当前设备的设备“号”,可以从inode 结构中获取。
int MAJOR(kdev_t
dev);
int MINOR(kdev_t
dev);
这两个宏从设备项中分解出主/ 次设备号。
kdev_t MKDEV(int
major, int minor);
这个宏由主/ 次设备号构造kdev_t 数据项。
SET_MODULE_OWNER(struct
file_operations *fops)
这个宏用来设置给定file_operations 结构中的owner 字段。
#include
定义与信号量相关的函数和数据类型。
void sema_init
(struct semaphore *sem, int val);
将信号量初始化为一个给定值。互斥信号量通常被初始化为1。
int
down_interruptible (struct semaphore *sem);
void up (struct
semaphore *sem);
分别用于获取信号量(必要时转入睡眠)和释放信号量。
#include
#include
segment.h 在2.0 及以上版本的内核中定义与跨地址空间拷贝相关的函数。2.1开发版系列将其名字改为uaccess.h。
unsigned long
__copy_from_user (void *to, const void *from, unsigned long count);
unsigned long
__copy_to_user (void *to, const void *from, unsigned long count);
在用户空间和内核空间之间拷贝数据。
void
memcpy_fromfs(void *to, const void *from, unsigned long count);
void
memcpy_tofs(void *to, const void *from, unsigned long count);
这些函数在2.0 版本内核中,用来在用户空间和内核空间之间拷贝字节数组。
#include
devfs_handle_t
devfs_mk_dir (devfs_handle_t dir, const char *name,
void *info);
devfs_handle_t
devfs_register (devfs_handle_t dir, const char *name,
unsigned int flags,
unsigned int major,
unsigned int minor, umode_t mode, void *ops,
void *info);
void
devfs_unregister (devfs_handle_t de);
这些是用于在设备文件系统(devfs)中注册设备的基本函数。