📄 (ldd) ch03-字符设备驱动程序(转载).txt
字号:
这里是我在scull.c中使用的获取主设备号的代码:
(代码)
从系统中删除设备驱动程序
当从系统中卸载一个模块时,应该释放主设备号。这一操作可以在cleanup_module中调
用如下函数完成:
(代码)
参数是要释放的主设备号和相应的设备名。内核对这个名字和设备号对应的名字进行比
较:如果不同,返回-ENINVAL。如果主设备号超出了所允许的范围或是并未分配给这个
设备,内核一样返回-EINVAL。在cleanup_module中注销资源失败会有非常不号的后果。
下次读取/proc/devices时,由于其中一个name字串仍然指向模块内存,而那片内存已经
不存在了,系统将产生一次失效。这种失效称为Oops*,内核在访问无效地址时将打印这
样的消息。
当你卸载驱动程序而又无法注销主设备号时,这种情况是无法恢复的,即便为此专门写
一个“补救”模块也无济于事,因为unregister_chrdev中调用了strcmp,而strcmp将使
用未映射的name字串,当释放设备时就会使系统Oops。无需说明,任何视图打开这个异
常的设备号对应的设备的操作都会Oops。
除了卸载模块,你还经常需要在卸载驱动程序时删除设备节点。如果设备节点是在加载
时创建的,可以写一个简单的脚本在卸载时删除它们。对于我们的样例设备,脚本scull
时创建的,可以写一个简单的脚本在卸载时删除它们。对于我们的样例设备,脚本scull
_unload完成这个工作。如果动态节点没有从/dev中删除,就会有可能造成不可预期的错
误:如果动态分配的主设备号相同,开发者计算机上的一个空闲/dev/framegrabber就有
可能在一个月后引用一个火警设备。“没有这个文件或目录”要比这个新设备所产生的
后果要好得多。
dev_t和kdev_t
到目前为止,我们已经谈论了主设备号。现在是讨论次设备号和驱动程序如何使用次设
备号来区分设备的时候了。
每次内核调用一个设备驱动程序时,它都告诉驱动程序它正在操作哪个设备。主设备号
和次设备号合在一起构成一个数据类型并用来标别某个设备。设备号的组合(主设备号
和次设备号合在一起)驻留在稍后介绍的“inode”结构的i_rdev域中。每个驱动程序接
收一个指向struct inode的指针做为第一个参数。这个指针通常也称为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。如果kde
v_t一直是隐藏的,它可以在内核的不同版本间任意变化,而不必修改每个人的设备驱动
v_t一直是隐藏的,它可以在内核的不同版本间任意变化,而不必修改每个人的设备驱动
程序。
有关kdev_t的信息被禁闭在<linux/kdev_t.h>中,其中大部分是注释。如果你对代码后
的哲学感兴趣的话,这个头文件是一段很有指导性的代码。因为<linux/fs.h>已经包含
了这个头文件,没有必要显式地包含这个文件。
不幸的是,kdev_t类型是一个“现代”概念,在内核版本1.2中没有这个类型。在较新的
内核中,所有的引用设备的内核变量和结构字段都是kdev_t的,但是在1.2.13中同样的
变量却是dev_t的。如果你的驱动程序只使用它接收的结构字段,而不声明自己的变量的
话,这不会有什么问题的。如果你需要声明自己的设备类型变量,为了可移植性你应该
在你的头文件中加入如下几行:
(代码)
这段代码是样例源码中的sysdep.h头文件的一部分。我不会在源码中在引用dev_t,但是
要假设前一个条件语句已经执行了。
如下这些宏和函数是你可以对kdev_t执行的操作:
MAJOR(kdev_t dev);
从kdev_t结构中分解出主设备号。
从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。
与Linux 1.2相关的头文件定义了同样的操作dev_t的函数,但没有那两个转换函数,这
也就是为什么上面那个条件代码简单地将它们定义返回它们的参数值。
文件操作
在接下来的几节中,我们将看看驱动程序能够对它管理的设备能够完成哪些不同的操作
在接下来的几节中,我们将看看驱动程序能够对它管理的设备能够完成哪些不同的操作
。在内核内部用一个file结构标别设备,而且内核使用file_operations结构访问驱动程
序的函数。这一设计是我们所看到的Linux内核面向对象设计的第一个例证。我们将在以
后看到更多的面向对象设计的例证。file_operations结构是一个定义在<linux/fs.h>中
的数指针表。结构struct file将在以后介绍。
我们已经register_chrdev调用中有一个参数是fops,它是一个指向一组操作(open,re
ad等等)表的指针。这个表的每一个项都指向由驱动程序定义的处理相应请求的函数。
对于你不支持的操作,该表可以包含NULL指针。对于不同函数的NULL指针,内核具体的
处理行为是不同的,下一节将逐一介绍。
随着新功能不断加入内核,file_operations结构已逐渐变得越来越大(尽管从1.2.0到2
..0.x并没有增加新字段)。这种增长应该不会有什么副作用,因为在发现任何尺寸不匹
配时,C编译器会将全局或静态struct变量中的未初始化字段填0。新的字段都加到结构
的末尾*,所以在编译时会插入一个NULL指针,系统会选择默认行为(记住,对于所有模
块需要加载的新内核,都要重新编译一次模块)。
在2.1开发用内核中,有些与fops字段相关的函数原型发生了变化。这些变化将在第17章
“近期发展”的“文件操作”一节中介绍。
纵览不同操作
下面的列表将介绍应用程序能够对设备调用的所有操作。这些操作通常称为“方法”,
用面向对象的编程术语来说就是说明一个对象声明可以操作在自身的动作。
用面向对象的编程术语来说就是说明一个对象声明可以操作在自身的动作。
为了使这张列表可以用来当作索引,我尽量使它简洁,仅仅介绍每个操作的梗概以及当
使用NULL时的内核默认行为。你可以在初次阅读时跳过这张列表,以后再来查阅。
在介绍完另一个重要数据结构(file)后,本章的其余部分将讲解最重要的一些操作并
提供一些提示,告诫和真实的代码样例。由于我们尚不能深入探讨内存管理和异步触发
机制,我们将在以后的章节中介绍这些更为复杂操作。
struct file_operations中的操作按如下顺序出现,除非注明,它们的返回0时表示成功
,发生错误时返回一个负的错误编码:
int (*lseek)(struct inode *, struct file *, off_t, int);
方法lseek用来修改一个文件的当前读写位置,并将新位置做为(正的)返回值返回。出
错时返回一个负的返回值。如果驱动程序没有设置这个函数,相对与文件尾的定位操作
失败,其他定位操作修改file结构(在“file结构”中介绍)中的位置计数器,并成功
返回。2.1.0中该函数的原型发生了变化,第17章“原型变化”将讲解这些内容。
int (*read)(struct inode *, struct file *, char *, int);
用来从设备中读取数据。当其为NULL指针时将引起read系统调用返回-EINVAL(“非法参
数”)。函数返回一个非负值表示成功的读取了多少字节。
数”)。函数返回一个非负值表示成功的读取了多少字节。
int (*write)(struct inode *, struct file *, const char *, int);
向设备发送数据。如果没有这个函数,write系统调用向调用程序返回一个-EINVAL。注
意,版本1.2的头文件中没有const这个说明符。如果你自己在write方法中加入了const
,当与旧头文件编译时会产生一个警告。如果你没有包含const,新版本的内核也会产生
一个警告;在这两种情况你都可以简单地忽略这些警告。如果返回值非负,它就表示成
功地写入的字节数。
int (*readdir)(struct inode *, struct file *, void *, filldir_t);
对于设备节点来说,这个字段应该为NULL;它仅用于目录。
int (*select)(struct inode *, struct file *, int, select_table *);
select一般用于程序询问设备是否可读和可写,或是否一个“异常”条件发生了。如果
指针为NULL,系统假设设备总是可读和可写的,而且没有异常需要处理。“异常”的具
体含义是和设备相关的。在当前的2.1开发用内核中,select的实现方法完全不同。(见
第17章的“poll方法”)。返回值告诉系统条件满足(1)或不满足(0)。
int (*ioctl)(struct inode *, struct file *, unsigned int, unsigned long);
系统调用ioctl提供一中调用设备相关命令的方法(如软盘的格式化一个磁道,这既不是
读操作也不是写操作)。另外,内核还识别一部分ioctl命令,而不必调用fops表中的io
ctl。如果设备不提供ioctl入口点,对于任何内核没有定义的请求,ioctl系统调用将返
回-EINVAL。当调用成功时,返回给调用程序一个非负返回值。
int (*mmap)(struct inode *, struct file *, struct vm_area_struct *);
mmap用来将设备内存映射到进程内存中。如果设备不支持这个方法,mmap系统调用将返
回-ENODEV。
int (*open)(struct inode *, struct file *);
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -