字符设备驱动程序Word文档下载推荐.docx
《字符设备驱动程序Word文档下载推荐.docx》由会员分享,可在线阅读,更多相关《字符设备驱动程序Word文档下载推荐.docx(24页珍藏版)》请在冰豆网上搜索。
如果有多个进程读同一个设备,他们彼此间竞争数据。
通过scullpipe的内部结构可以了解阻塞型和非阻塞型读/写是如何实现的;
没有中断也会出现这样的情况。
尽管真实的驱动程序利用中断与它们的设备同步,但阻塞型和非阻塞型操作是非常重要的内容,从概念上讲与中断处理(第9章,中断处理,介绍)无关。
scullsingle
scullpriv
sculluid
scullwuid
这些设备与scull0相似,但在何时允许open操作时都不同方式的限制。
第一个(scullsingle)只允许一次一个进程使用驱动程序,而scullpriv对每个虚拟控制台是私有的(每个设备对虚拟控制台是私有的)。
sculluid和scullwuid可以多次打开,但每次只能有一个用户;
如果另一个用户锁住了设备,前者返回-EBUSY,而后者则实现为阻塞型open。
通过这些可以展示如何实现不同的访问策略。
每一个scull设备都展示了驱动程序不同的功能,而且都不同的难度。
本章主要讲解scull0-3的内部结构;
第5章,字符设备驱动程序的扩展操作,将介绍更复杂的设备:
“一个样例实现:
scullpipe”介绍scullpipe,“设备文件的访问控制”介绍其他设备。
主设备号和次设备号
通过访问文件系统的名字(或“节点”)访问字符设备,通常这些文件位于/dev目录。
设备文件是特殊文件,这一点可以通过ls-l输出的第一列中的“c”标明,它说明它们是字符节点。
/dev下还有块设备,但它们的第一列是“b”;
尽管如下介绍的某些内容也同样适用于块设备,现在我们只关注字符设备。
如果你执行ls命令,在设备文件条目的最新修改日期前你会看到两个数(用逗号分隔),这个位置通常显示文件长度。
这些数就是相应设备的主设备号和次设备号。
下面的列表给出了我使用的系统上的一些设备。
它们的主设备号是10,1和4,而次设备号是0,3,5,64-65和128-129。
(代码)
主设备号标识设备对应的驱动程序。
例如,/dev/null和/dev/zero都有驱动程序1管理,而所有的tty和pty都由驱动程序4管理。
内核利用主设备号将设备与相应的驱动程序对应起来。
次设备号只由设备驱动程序使用;
内核的其他部分不使用它,仅将它传递给驱动程序。
一个驱动程序控制若干个设备并不为奇(如上面的例子所示)――次顺便号提供了一种区分它们的方法。
向系统增加一个驱动程序意味着要赋予它一个主设备号。
这一赋值过程应该在驱动程序(模块)的初始化过程中完成,它调用如下函数,这个函数定义在<
linux/fs.h>
:
返回值是错误码。
当出错时返回一个负值;
成功时返回零或正值。
参数major是所请求的主设备号,name是你的设备的名字,它将在/proc/devices中出现,fops是一个指向跳转表的指针,利用这个跳转表完成对设备函数的调用,本章稍后将在“文件操作”一节中介绍这些函数。
主设备号是一个用来索引静态字符设备数组的整数。
在1.2.13和早期的2.x内核中,这个数组有64项,而2.0.6到2.1.11的内核则升至128。
由于只有设备才处理次设备号,register_chrdev不传递次设备号。
一旦设备已经注册到内核表中,无论何时操作与你的设备驱动程序的主设备号匹配的设备文件,内核都会通过在fops跳转表索引调用驱动程序中的正确函数。
接下来的问题就是如何给程序一个它们可以请求你的设备驱动程序的名字。
这个名字必须插入到/dev目录中,并与你的驱动程序的主设备号和次设备号相连。
在文件系统上创建一个设备节点的命令是mknod,而且你必须是超级用户才能创建设备。
除了要创建的节点名字外,该命令还带三个参数。
例如,命令:
创建一个字符设备(c),主设备号是127,次设备号是0。
由于历史原因,次设备号应该在0-255范围内,有时它们存储在一个字节中。
存在很多原因扩展可使用的次设备号的范围,但就现在而言,仍然有8位限制。
动态分配主设备号
某些主设备号已经静态地分配给了大部分公用设备。
在内核源码树的Documentation/device.txt文件中可以找到这些设备的列表。
由于许多数字已经分配了,为新设备选择一个唯一的号码是很困难的――不同的设备要不主设备号多得多。
很幸运(或是感谢某些人天才),你可以动态分配主设备号了。
如果你调用register_chrdev时的major为零的话,这个函数就会选择一个空闲号码并做为返回值返回。
主设备号总是正的,因此不会和错误码混淆。
我强烈推荐你不要随便选择一个一个当前不用的设备号做为主设备号,而使用动态分配机制获取你的主设备号。
动态分配的缺点是,由于分配给你的主设备号不能保证总是一样的,无法事先创建设备节点。
然而这不是什么问题,这是因为一旦分配了设备号,你就可以从/proc/devices读到。
为了加载一个设备驱动程序,对insmod的调用被替换为一个简单的脚本,它通过/proc/devices获得新分配的主设备号,并创建节点。
/proc/devices一般如下所示:
加载动态分配主设备号驱动程序的脚本可以利用象awk这类工具从/proc/devices中获取信息,并在/dev中创建文件。
下面这个脚本,scull_load,是scull发行中的一部分。
使用以模块形式发行的驱动程序的用户可以在/etc/rc.d/rc.local中调用这个脚本,或是在需要模块时手工调用。
此外还有另一种方法:
使用kerneld。
这个方法和其他模块的高级功能将在第11章“Kerneld和高级模块化”中介绍。
这个脚本同样可以适用于其他驱动程序,只要重新定义变量和调整mknod那几行就可以了。
上面那个脚本创建4个设备,4是scull源码中的默认值。
脚本的最后两行看起来有点怪怪的:
为什么要改变设备的组和权限呢?
原因是这样的,由root创建的节点自然也属于root。
默认权限位只允许root对其有写访问权,而其他只有读权限。
正常情况下,设备节点需要不同的策略,因此需要进行某些修改。
通常允许一组用户访问对设备,但实现细节却依赖于设备和系统管理员。
安全是个大问题,这超出了本书的范围。
scull_load中的chmod和chgrp那两行仅仅是最为处理权限问题的一点提示。
稍后,在第5章的“设备文件的访问控制”一节中将介绍sculluid源码,展示设备驱动程序如何实现自己的设备访问授权。
如果重复地创建和删除/dev节点似乎有点过分的话,有一个解决的方法。
如果你看了内核源码fs/devices.c的话,你可以看到动态设备号是从127(或63)之后开始的,你可以用127做为主设备号创建一个长命节点,同时可以避免在每次相关设备加载时调用脚本。
如果你使用了几个动态设备,或是新版本的内核改变了动态分配的特性,这个技巧就不能用了。
(如果内核发生了修改,基于内核内部结构编写的代码并不能保证继续可以工作。
)不管怎样,由于开发期间模块要不断地加载和卸载,你会发现这一技术在开发期间还是很有用的。
就我看来,分配主设备号的最佳方式是,默认采用动态分配,同时留给你在加载时,甚至是编译时,指定主设备号的余地。
使用我建议的代码将与自动端口探测的代码十分相似。
scull的实现使用了一个全局变量,scull_major,来保存所选择的设备号。
该变量的默认值是SCULL_MAJOR,在所发行的源码中为0,即“选择动态分配”。
用户可以使用这个默认值或选择某个特定的主设备号,既可以在编译前修改宏定义,也可以在ins_mod命令行中指定。
最后,通过使用scull_load脚本,用户可以在scull_load中命令行中将参数传递给insmod。
这里是我在scull.c中使用的获取主设备号的代码:
从系统中删除设备驱动程序
当从系统中卸载一个模块时,应该释放主设备号。
这一操作可以在cleanup_module中调用如下函数完成:
参数是要释放的主设备号和相应的设备名。
内核对这个名字和设备号对应的名字进行比较:
如果不同,返回-ENINVAL。
如果主设备号超出了所允许的范围或是并未分配给这个设备,内核一样返回-EINVAL。
在cleanup_module中注销资源失败会有非常不号的后果。
下次读取/proc/devices时,由于其中一个name字串仍然指向模块内存,而那片内存已经不存在了,系统将产生一次失效。
这种失效称为Oops*,内核在访问无效地址时将打印这样的消息。
当你卸载驱动程序而又无法注销主设备号时,这种情况是无法恢复的,即便为此专门写一个“补救”模块也无济于事,因为unregister_chrdev中调用了strcmp,而strcmp将使用未映射的name字串,当释放设备时就会使系统Oops。
无需说明,任何视图打开这个异常的设备号对应的设备的操作都会Oops。
除了卸载模块,你还经常需要在卸载驱动程序时删除设备节点。
如果设备节点是在加载时创建的,可以写一个简单的脚本在卸载时删除它们。
对于我们的样例设备,脚本scull_unload完成这个工作。
如果动态节点没有从/dev中删除,就会有可能造成不可预期的错误:
如果动态分配的主设备号相同,开发者计算机上的一个空闲/dev/framegrabber就有可能在一个月后引用一个火警设备。
“没有这个文件或目录”要比这个新设备所产生的后果要好得多。
dev_t和kdev_t
到目前为止,我们已经谈论了主设备号。
现在是讨论次设备号和驱动程序如何使用次设备号来区分设备的时候了。
每次内核调用一个设备驱动程序时,它都告诉驱动程序它正在操作哪个设备。
主设备号和次设备号合在一起构成一个数据类型并用来标别某个设备。
设备号的组合(主设备号和次设备号合在一起)驻留在稍后介绍的“inode”结构的i_rdev域中。
每个驱动程序接收一个指向structinode的指针做为第一个参数。
这个指针通常也称为inode,函数可以通过查看inode->
i_rdev分解出设备号。
历史上,Unix使用dev_t保存设备号。
dev_t通常是<
sys/types.h>
中定义的一个16位整数。
而现在有时需要超过256个次设备号,但是由于有许多应用(包括C库在内)都了解dev_t的内部结构,改变dev_t是很困难的,如果改变dev_t的内部结构就会造成这些应用无法运行。
因此,dev_t类型一直没有改变;
它仍是一个16位整数,而且次设备号仍限制在0-255内。
然而,在Linux内核内部却使用了一个新类型,kdev_t。
对于每一个内核函数来说,这个新类型被设计为一个黑箱。
它的想法是让用户程序不能了解kdev_t。
如果kdev_t一直是隐藏的,它可以在内核的不同版本间任意变化,而不必修改每个人的设备驱动程序。
有关kdev_t的信息被禁闭在<
linux/kdev_t.h>
中,其中大部分是注释。
如果你对代码后的哲学感兴趣的话,这个头文件是一段很有指导性的代码。
因为<
已经包含了这个头文件,没有必要显式地包含这个文件。
不幸的是,kdev_t类型是一个“现代”概念,在内核版本1.2中没有这个类型。
在较新的内核中,所有的引用设备的内核变量和结构字段都是kdev_t的,但是在1.2.13中同样的变量却是dev_t的。
如果你的驱动程序只使用它接收的结构字段,而不声明自己的变量的话,这不会有什么问题的。
如果你需要声明自己的设备类型变量,为了可移植性你应该在你的头文件中加入如下几行:
这段代码是样例源码中的sysdep.h头文件的一部分。
我不会在源码中在引用dev_t,但是要假设前一个条件语句已经执行了。
如下这些宏和函数是你可以对kdev_t执行的操作:
MAJOR(kdev_tdev);
从kdev_t结构中分解出主设备号。
MINOR(kdev_tdev);
分解出次设备号。
MKDEV(intma,intmi);
通过主设备号和次设备号返回kdev_t。
kdev_t_to_nr(kdev_tdev);
将kdev_t转换为一个整数(dev_t)。
to_kdev_t(intdev);
将一个整数转换为kdev_t。
注意,核心态中没有定义dev_t,因此使用了int。
与Linux1.2相关的头文件定义了同样的操作dev_t的函数,但没有那两个转换函数,这也就是为什么上面那个条件代码简单地将它们定义返回它们的参数值。
文件操作
在接下来的几节中,我们将看看驱动程序能够对它管理的设备能够完成哪些不同的操作。
在内核内部用一个file结构标别设备,而且内核使用file_operations结构访问驱动程序的函数。
这一设计是我们所看到的Linux内核面向对象设计的第一个例证。
我们将在以后看到更多的面向对象设计的例证。
file_operations结构是一个定义在<
中的数指针表。
结构structfile将在以后介绍。
我们已经register_chrdev调用中有一个参数是fops,它是一个指向一组操作(open,read等等)表的指针。
这个表的每一个项都指向由驱动程序定义的处理相应请求的函数。
对于你不支持的操作,该表可以包含NULL指针。
对于不同函数的NULL指针,内核具体的处理行为是不同的,下一节将逐一介绍。
随着新功能不断加入内核,file_operations结构已逐渐变得越来越大(尽管从1.2.0到2.0.x并没有增加新字段)。
这种增长应该不会有什么副作用,因为在发现任何尺寸不匹配时,C编译器会将全局或静态struct变量中的未初始化字段填0。
新的字段都加到结构的末尾*,所以在编译时会插入一个NULL指针,系统会选择默认行为(记住,对于所有模块需要加载的新内核,都要重新编译一次模块)。
在2.1开发用内核中,有些与fops字段相关的函数原型发生了变化。
这些变化将在第17章“近期发展”的“文件操作”一节中介绍。
纵览不同操作
下面的列表将介绍应用程序能够对设备调用的所有操作。
这些操作通常称为“方法”,用面向对象的编程术语来说就是说明一个对象声明可以操作在自身的动作。
为了使这张列表可以用来当作索引,我尽量使它简洁,仅仅介绍每个操作的梗概以及当使用NULL时的内核默认行为。
你可以在初次阅读时跳过这张列表,以后再来查阅。
在介绍完另一个重要数据结构(file)后,本章的其余部分将讲解最重要的一些操作并提供一些提示,告诫和真实的代码样例。
由于我们尚不能深入探讨内存管理和异步触发机制,我们将在以后的章节中介绍这些更为复杂操作。
structfile_operations中的操作按如下顺序出现,除非注明,它们的返回0时表示成功,发生错误时返回一个负的错误编码:
int(*lseek)(structinode*,structfile*,off_t,int);
方法lseek用来修改一个文件的当前读写位置,并将新位置做为(正的)返回值返回。
出错时返回一个负的返回值。
如果驱动程序没有设置这个函数,相对与文件尾的定位操作失败,其他定位操作修改file结构(在“file结构”中介绍)中的位置计数器,并成功返回。
2.1.0中该函数的原型发生了变化,第17章“原型变化”将讲解这些内容。
int(*read)(structinode*,structfile*,char*,int);
用来从设备中读取数据。
当其为NULL指针时将引起read系统调用返回-EINVAL(“非法参数”)。
函数返回一个非负值表示成功的读取了多少字节。
int(*write)(structinode*,structfile*,constchar*,int);
向设备发送数据。
如果没有这个函数,write系统调用向调用程序返回一个-EINVAL。
注意,版本1.2的头文件中没有const这个说明符。
如果你自己在write方法中加入了const,当与旧头文件编译时会产生一个警告。
如果你没有包含const,新版本的内核也会产生一个警告;
在这两种情况你都可以简单地忽略这些警告。
如果返回值非负,它就表示成功地写入的字节数。
int(*readdir)(structinode*,structfile*,void*,filldir_t);
对于设备节点来说,这个字段应该为NULL;
它仅用于目录。
int(*select)(structinode*,structfile*,int,select_table*);
select一般用于程序询问设备是否可读和可写,或是否一个“异常”条件发生了。
如果指针为NULL,系统假设设备总是可读和可写的,而且没有异常需要处理。
“异常”的具体含义是和设备相关的。
在当前的2.1开发用内核中,select的实现方法完全不同。
(见第17章的“poll方法”)。
返回值告诉系统条件满足
(1)或不满足(0)。
int(*ioctl)(structinode*,structfile*,unsignedint,unsignedlong);
系统调用ioctl提供一中调用设备相关命令的方法(如软盘的格式化一个磁道,这既不是读操作也不是写操作)。
另外,内核还识别一部分ioctl命令,而不必调用fops表中的ioctl。
如果设备不提供ioctl入口点,对于任何内核没有定义的请求,ioctl系统调用将返回-EINVAL。
当调用成功时,返回给调用程序一个非负返回值。
int(*mmap)(structinode*,structfile*,structvm_area_struct*);
mmap用来将设备内存映射到进程内存中。
如果设备不支持这个方法,mmap系统调用将返回-ENODEV。
int(*open)(structinode*,structfile*);
尽管这总是操作在设备节点上的第一个操作,然而并不要求驱动程序一定要声明这个方法。
如果该项为NULL,设备的打开操作永远成功,但系统不会通知你的驱动程序。
void(*release)(structinode*,structfile*);
当节点被关闭时调用这个操作。
与open相仿,release也可以没有。
在2.0和更早的核心中,close系统调用从不失败;
这种情况在版本2.1.31中有所变化(见第17章)。
int(*fsync)(structinode*,structfile*);
刷新设备。
如果驱动程序不支持,fsync系统调用返回-EINVAL。
int(*fasync)(structinode*,structfile*,int);
这个操作用来通知设备它的FASYNC标志的变化。
异步触发是比较先进的话题,将在第5章的“异步触发”一节中介绍。
如果设备不支持异步触发,该字段可以是NULL。
int(*check_media_change)(kdev_tdev);
check_media_change只用于块设备,尤其是象软盘这类可移动介质。
内核调用这个方法判断设备中的物理介质(如软盘)自最近一次操作以来发生了变化(返回1)或是没有(0)。
字符设备无需实现这个函数。
int(*revalidate)(kdev_tdev);
这是最后一项,与前面提到的那个方法一样,也只适用于块设备。
revalidate与缓冲区高速。
缓存有关。
我们将在第12章“加载块设备驱动程序”的“可移动设备”中介绍revalidate。
scull驱动程序中适用的file_operations结构如下:
structfile_operationsscull_fops={
llseek:
scull_llseek,
read:
scull_read,
write:
scull_write,
ioctl:
scull_ioctl,
open:
scull_open,
release:
scull_release,
}在最新的开发用内核中,某些原型已经发生了变化。
该列表是从2.0.x的头文件中提炼出来的,这里给出的原型对于大多数内核而言都是正确的。
内核2.1引入的变化(以及为了使我们的模块可移植所进行的修改)在针对不同操作的每一节和第17章的“文件操作”中详细介绍。
file结构
在<
中定义的structfile是设备驱动程序所适用的又一个最重要的数据结构。
注意,file与用户程序中的FILE没有任何关联。
FILE是在C库中定义且从不出现在内核代码中。
而structfile是一个内核结构,从不出现在用户程序中。
file结构代表一个“打开的文件”。
它有内核在open时创建而且在close前做为参数传递给如何操作在设备上的函数。
在文件关闭后,内核释放这个数据结构。
一个“打开的文件”与由structinode表示的“磁盘文件”有所不同。
在内核源码中,指向structfile的指针通常称为file或filp(“文件指针”)。
为了与这个结构相混淆,我将一直称指针为filp-flip是一个指针(同样,它也是设备方法的参数之一),而file是结构本身。
structfile中的最重要的字段罗列如下。
与上节相似,这张列表在首次阅读时可以略过。
在下一节中,我们将看到一些真正的C代码,我将讨论某些字段,到时你可以反过来查阅这张列表。
mode_tf_mode;
文件模式由FMODE_READ和FMODE_WRITE标别。
你可能需要在你的ioctl函数中查看这个域来来检查读/写权限,但由于内核在调用你的驱动程序的read和wri