LINUX DEVICE DRIVER(2ND)第3章 字符设备驱动程序(2,主设备号和次设备号)

主设备号和次设备号
通过访问文件系统中的名字来访问字符设备,通常这些名字被称作文件系统中的特殊的文件或这设备文件或者简单节点(simply nodes),它们位于/dev目录下。字符设备驱动的特殊文件,可以通过ls -l输出的第一列中的“c”标明。/dev下还有块设备,但它们是“b”来识别;尽管如下介绍的某些内容也适用于块设备,但我们这章只关注字符设备。
如果你执行ls -l命令,在设备文件条目中的最新修改日期前你会看到二个数(用逗号分隔),这个位置通常显示文件长度。这二个数就是相应设备的主设备号和次设备号。下面的列表给出了我使用的系统上的一些设备。它们的主设备号是10,1和4,而次设备号是0,3,5,64,5和129。
crw-rw-rw-    1     root        root        1,    3     Feb 23 1999  null
crw-------              1     root        root        10, 1     Feb 23 1999  psaux
crw-------              1     rubini     tty          4,    1     Aug 16 22:22        tty1
crw-rw-rw-    1     root        dialout    4,    64  Jun 30 11:19 ttyS0
crw-rw-rw-    1     root        dialout    4,    65  Aug 16 00:00 ttyS1
crw-------              1     root        sys         7,    1     Feb 23 1999  vcs1
crw-------              1     root        sys         7,    129  Feb 23 1999 vcsa1
crw-rw-rw-    1     root        root        1,    5     Feb 23 1999 zero
主设备号识别设备对应的驱动程序。例如,/dev/null和/dev/zero都由驱动程序1管理,而所有的虚拟控制台和串口终端都由驱动程序4管理,同样,vcs1 和 vcsa1由驱动程序7管理。当设备打开(open)时,内核利用主设备号分派执行相应的驱动程序。
次设备号只由相应的设备驱动程序使用;内核的其他部分不使用它,仅将它传递给驱动程序。所以一个驱动程序管理若干个设备并不为奇(如上面的例子所示),次序号提供了一种区分它们的方法。
尽管2.4版本的内核引入了一种新的特性(可选):设备文件系统,devfs。如果使用了这种文件系统,那设备文件将变得简单,但也很原来有很大的不同。另一方面,这新的文件系统带来了一些用户可见的不兼容特性。当我们写这书时,系统的发行版本默认没有选择这一特性。原先介绍的和将要介绍的怎么样添加一个新的驱动程序都假设了devfs不存在。这个缺口将在“设备文件系统”这一章补上。
当没有使用devfs时,向系统增加一个驱动程序意味着要赋值它一个主设备号。这一赋值过程应该在驱动程序(模块)的初始化过程中完成,它调用如下函数,这个函数定义在<linux/fs.h>:
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
返回值提示成功或者失败。返回一个负值,表示出错;返回零或正值,表示成功。参数major是所请求的主设备号,name是你的设备的名字,它将在/proc/devices中出现,fops是一个指向函数队列的指针,利用它完成对设备函数的调用,本章稍后将在“文件操作”一节中介绍这些内容。
主设备号是一个用来索引静态字符设备组的整数,“动态分配主设备号”将在本章的稍后部分中介绍怎样选择一个主设备号。2.0内核支持128个设备驱动,而2.2和2.4内核支持256个(保留数值0和255为将来使用)。而次版本号(8位字节的数)并没有传递给 register_chrdev函数,因为次版本号是驱动程序自己使用的。开发团队为了增加内核可能支持的设备数量而带来了很大的压力,在开发树2.5版本内核的目标中,设备号至少是16位的。
一旦设备驱动程序注册到内核表中,它的操作都与分配的主设备号匹配,何时在字符设备文件上操作都与它的主设备号相关联,内核都会通过 file_operations结构体查找并调用相应的驱动程序中的函数。为了这个原因,传递给 register_chrdev的指针应该是指向驱动程序中的全局结构体,而不是一个局部的一个模块初始化函数。
接下来的问题就是如何给程序一个名字以被它们用来请求你的设备驱动程序。这个名字必须插入到/dev目录中,并与你的驱动程序的主设备号和次设备号相连。
在文件系统上创建一个设备节点的命令是mknod,而且你必须是超级用户才能操作。除了要创建的节点名字外,该命令还带三个参数。例如,命令:
mknod /dev/scull0 c 254 0
创建一个字符设备(c),主设备号是254,次设备号是0。由于历史原因,次设备号应该在0-255范围内,有时它们存储在一个字节中。存在很多原因扩展可使用的次设备号的范围,但就现在而言,仍然有8位限制。
请注意:如果一旦用mknod生成了一个特别的设备文件,它就永远存在了硬盘上,除非你明白的删除了它。你可以通过执行命令rm /dev/scull0来删除例子中的设备。
动态分配主设备号
某些主设备号已经静态地分配给了大部分公用设备。在内核源码树的Documentation/device.txt文件中可以找到这些设备的列表。由于许多数字已经分配了,为新设备选择一个唯一的号码是很困难的——用户的设备数量要比可用的主设备号多得多。
很幸运(或是更应该感谢某些人的智慧),你可以动态请求分配主设备号。如果你调用register_chrdev时的major为0的话,这个函数就会选择一个空闲号码并做为返回值返回。主设备号总是正的,而如果返回负值的话,就表示是错误码。请注意这二种情况下,操作存在着微小的差别:如果调用者请求动态分配一个号码,则函数返回已经分配好的主设备号。而返回0则表明成功分配调用预先指定好的主设备号。
作为个人的设备,我们强烈推荐你使用动态分配机制获取你的主设备号,而不要随便选择一个一个当前不用的设备号做为主设备号。当然,如果你的设备在社会上能大范围的使用,并且已经包含到官方的内核树中,你需要分配一个主设备号作为专用。
动态分配的缺点是,由于分配给你的主设备号不能保证总是一样的,无法事先创建设备节点。这意味着你不能用加载命令加载你的驱动程序,这个高级特性将在11章中介绍。对于设备的普通使用,这不是什么问题,这是因为一旦分配了设备号,你就可以从/proc/devices读到。
为了加载一个使用动态分配主设备号的设备驱动程序,对insmod的调用可以用调用insmod、读/proc/devices后的一个简单的脚本来代替生成特殊文件。 (To load a driver using a dynamic major number, therefore, the invocation of insmod can be replaced by a simple script that after calling insmod reads /proc/devices in order to create the special file(s).)
/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:
2 fd
8 sd
11 sr
65 sd
66 sd
加载动态分配主设备号的模块的脚本可以利用象awk这类工具来写,该脚本从/proc/devices中获取信息,并在/dev中创建文件。
下面这个脚本,scull_load,是scull发行中的一部分。使用以模块形式发行的驱动程序的用户可以在系统的rc.local文件中调用这个脚本,或是在需要模块时手工调用。
#!/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 1
# 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那几行就可以了。上面那个脚本创建4个设备,4是scull源码中的默认值。
脚本的最后几行看起来有点古怪:为什么要改变设备的组和模式呢?原因是只有超级用户才能运行这段脚本。默认允许位只允许root对其有写访问权,而其他只有读权限。正常情况下,设备节点需要不同的策略,因此某些访问权限需要进行某些修改。虽然这个脚本允许一组用户访问,但你要作一些修改。稍后,在第5章的“设备文件的访问控制”一节中的sculluid源码,将展示设备驱动程序如何实现自己的设备访问授权。scull_unload脚本用来整理/dev目录,并删掉这个模块。
作为交替使用加载和卸载的一对脚本,你可以一个初始化脚本,用来放在你发布的目录下。作为scull的源码一部分,我们提供了相当完整的和可配置的初始化脚本的例子:scull.init;它接受传统的参数如“start” 、“stop” 、 “restart”,来执行scull_load 和 scull_unload的角色。.
如果重复地创建和删除/dev节点似乎有点过分的话,这里一个有用的解决方法。如果你只是单单加载和卸载一个简单的驱动,你可以在第一次利用脚本生成特殊文件后使用rmmod 和 insmod命令来完成,你也可以计算出你选择的号码同时也不用弄乱其他的(动态)模块,对开发避免繁长的脚本是非常有用的。当然,这方法不适合同一时间运行多个驱动的情况。
就我们的观点中,分配主设备号的最佳方式是默认采用动态分配,同时也留有在加载时,甚至是编译时,指定主设备号的选择权。采用我的建议的代码将与自动端口探测的代码十分类似。scull的实现使用了一个全局变量,scull_major,来保存所选择的设备号。该变量是由在scull.h中定义的常数SCULL_MAJOR初始化的,该值在所发行的源码中为0,即“选择动态分配”。用户可以使用这个默认值,也可以指定某个特定的主设备号,在编译前修改宏定义即可,也可以在ins_mod命令行中指定scull_major的值。最后,通过使用scull_load脚本,用户可以使用scull_load的命令行中将参数传递给insmod。
这里是我在scull.c源码中的获取主设备号的代码:
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 */
从系统中删除设备驱动程序
当从系统中卸载一个模块时,必需释放主设备号。在清除(cleanup)模块的函数中调用如下函数完成该操作:
int unregister_chrdev(unsigned int major, const char *name);
参数是要释放的主设备号和相应的设备名。内核对这个名字和设备号对应的名字进行比较:如果不同,返回-ENINVAL。如果主设备号超出了所允许的范围或是并未分配给这个设备,内核一样返回-EINVAL。
在清除(cleanup)函数中如注销资源失败会有非常不好的后果。当下次试图读取/proc/devices时将产生一个一个错误,是由于其中一个name字串仍然指向模块内存,而那片内存已经不存在了。这种失效称为oops,因为当内核在访问无效地址时将打印这样的消息。
当你卸载驱动程序而又没有注销主设备号时,这种情况将产生很难弥补的过错,即便为此专门写一个“补救”模块也无济于事,因为unregister_chrdev中的trcmp使用不同与原始模块对应的指针(名字)。当你注销主设备号失败时,你必须同时重新加载原模块和为注销这个主设备号而创建的模块。如果你没有修改过代码,那这个有缺点的模块将幸运地获得同个地址,而名字将存放在这个地址中。当然,作为安全的选择,是重起系统。
除了卸载模块,你还经常需要在卸载驱动程序时删除设备节点。我们用加载模块时使用的一对脚本中的另一个来完成这项工作。对于我们的样例设备,脚本scull_unload完成这个工作,作为选择,你也可以调用scull.init中的stop。
如果动态节点没有从/dev中删除,就会有可能造成不可预期的错误:开发者计算机上的一个没有删除的/dev/framegrabber就有可能在一个月后引用一个火警设备,如果这二个设备都使用动态获得主设备号。当打开这个/dev/framegrabber 设备时,产生“没有这个文件或目录”的错误总要比打开一个新设备所产生的后果要好得多。
dev_t和kdev_t
到目前为止,我们已经讨论了主设备号。现在是讨论次设备号和驱动程序如何使用次设备号来区分设备的时候了。
每次内核调用一个设备驱动程序时,它都告诉驱动程序它正在操作哪个设备。主设备号和次设备号合在一起构成一个数据类型并用来标别某个设备。组合的设备号(主设备号和次设备号合在一起)保存在 “inode”结构的i_rdev域中,inode将在稍后介绍。一些驱动程序接收一个指向struct inode的指针做为第一个参数。这个指针通常也称为inode(通常驱动开发这也这样称呼),函数可以通过查看inode->i_rdev分解出设备号。
历史上,Unix通过申明dev_t变量(设备类型device type)来保存设备号。dev_t通常是<sys/types.h>中定义的一个16位整数。而现在有时需要超过256个次设备号,但是由于有许多应用(包括C库在内)都“了解”dev_t的内部结构,所以改变dev_t是很困难的,因为如果改变dev_t的内部结构就会造成这些应用无法运行。因此,虽然许多系统(groundwork)已经放置了更大的设备号,但是他们目前仍然只限制使用16位整数。
然而,在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
只要你通过这些函数来操作设备号,那它哪怕只是作为内部数据结构变换,也将继续运行。
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值