第三章 scull 设备号
进入第三章了,这一章的目标是写一个完成的字符设备驱动。书中以一个scull项目为例,开始介绍字符设备驱动。值得一提的是,scull不依赖于特定的硬件设备,其实对于scull来说,它的设施就是一片内存空间。作者让内存来充当这个驱动的硬件设备。就这一章来说,内存模拟了四个“设备”:scull0 to scull3。这四个设备是全局静态的。虽然他们是由内存组成的,我们把他们当做实际的设备就好了,因为它们用起来跟实际的设备其实也没有什么区别。
对于像我这种初学者来说,很多概念都是新的,值得仔细研究。
主设备号和次设备号。
Linux的设备管理是和文件系统紧密结合的,各种设备都以文件的形式存放在/dev目录下,称为设备文件。应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。为了管理这些设备,系统为设备编了号,每个设备号又分为主设备号和次设备号。主设备号用来区分不同种类(这个种类其实就是指使用了同一个驱动)的设备,而次设备号用来区分同一类型的多个设备。对于常用设备,Linux有约定俗成的编号,如硬盘的主设备号是3。
内核中的dev_t类型( 在<linux/types.h>中定义)用来记录设备号。用Source Insight追踪这个dev_t 类型之后,发现这个东西其实就等同于unsigned int。实际上,dev_t就是一个32位的量,它的高12位表示主设备号,低20位表示次设备号,就这么简单。<linux/kdev_t.h>中定义了几个宏,用于一些关于dev_t的操作,代码如下:
1 #define MINORBITS 20
2 #define MINORMASK ((1U << MINORBITS) - 1)
3
4 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
5 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
6 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
MAJOR用于获取主设备号,MINOR用于获取次设备号,MKDEV用于把你的主设备号和次设备号组织成dev_t的形式(主设备号为ma,次设备号为mi)。知道了dev_t的高12位表示主设备号和低20位表示次设备号之后,上面的代码要弄懂还是毫无压力的。
要编写字符设备驱动的话,第一个要解决的问题就是得到一个或几个可用的设备号。获取设备号的方法有两个。直接指定设备号,使用int register_chrdev_region(dev_t first, unsigned int count, char *name)函数。动态分配设备号,使用int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name)函数。既然有设备编号的分配,就自然有设备编号的释放了,void unregister_chrdev_region(dev_t first, unsigned int count)用以释放设备编号。显然,结合第二章helloworld的内容,register_chrdev_region和alloc_chrdev_region在设备驱动的初始化函数中被调用,unregister_chrdev_region在设备驱动的退出函数中被调用。
关于分配主设备号的方法选择,书上的建议是默认的使用动态分配,然后允许模块在加载时指定设备号(这里跟上一章的模块参数相关),指定了设备号之后,使用静态分配方法。源码如下:
1 int scull_major = SCULL_MAJOR;
2 int scull_minor = 0;
3 .
4 .
5 module_param(scull_major, int, S_IRUGO);
6 module_param(scull_minor, int, S_IRUGO);
7 .
8 .
9 int scull_init_module(void)
10 {
11 int result, i;
12 dev_t dev = 0;
13 if (scull_major) {
14 dev = MKDEV(scull_major, scull_minor);
15 result = register_chrdev_region(dev, scull_nr_devs, "scull");
16 } else {
17 result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,
18 "scull");
19 scull_major = MAJOR(dev);
20 }
21 if (result < 0) {
22 printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
23 return result;
24 }
25 .
26 .
27 }
其中,SCULL_MAJOR的初值为0。
动态分配设备号的问题在于不能提前创建设备节点,应为你还不知道你的设备号。不过在模块家在之后,这个编号可以在/proc/devices读取它。在insmod之后,在读取/proc/devices文件获取设备号,在创建设备节点就行了。
相应的,源码中提供了一个scull_load脚本来替代insmod,先看代码:
1 #!/bin/sh
2 module="scull"
3 device="scull"
4 mode="664"
5 # invoke insmod with all arguments we got
6 # and use a pathname, as newer modutils don't look in . by default
7 /sbin/insmod ./$module.ko $* || exit 1
8 # remove stale nodes
9 rm -f /dev/${device}[0-3]
10 major=$(awk "\\$2= =\"$module\" {print \\$1}" /proc/devices)
11 mknod /dev/${device}0 c $major 0
12 mknod /dev/${device}1 c $major 1
13 mknod /dev/${device}2 c $major 2
14 mknod /dev/${device}3 c $major 3
15 # give appropriate group/permissions, and change the group.
16 # Not all distributions have staff, some have "wheel" instead.
17 group="staff"
18 grep -q '^staff:' /etc/group || group="wheel"
19 chgrp $group /dev/${device}[0-3]
20 chmod $mode /dev/${device}[0-3]
在insmod加载模块之后,一个个的把设备节点创建好。major=$(awk "\\$2= =\"$module\" {print \\$1}" /proc/devices)这句的语义虽然不是很清楚,大致的作用就跟查询数据库差不多:从/proc/devices取出一条数据的第一个字段,条件为这条数据的第二个字段=$module,呵呵呵。
/proc/devices的内容格式大致如下:
1 Character devices:
2 1 mem
3 2 pty
4 3 ttyp
5 4 ttyS
6 6 lp
7 7 vcs
8 10 misc
9 13 input
10 14 sound
11 21 sg
12 180 usb
13 Block devices:
14 2 fd
15 8 sd
16 11 sr
17 65 sd
18 66 sd
第一个字段其实也就是主设备号了。通过在运行scull_load脚本时传递参数,还能够用静态的方法来分配设备号。