1、scull( Simple Character Utility for Loading
Localities)的设计
Scull是一个字符驱动,它操作一块内存区域,就好像它是一个设备,因此在以下的介绍中我们可以互换的使用设备和scull操作的内存区。
编写驱动的第一步是定义驱动将要提供给用户的功能。
模块实现的每种设备都被引用为一种类型。
Scull源码实现:
scull0到scull3四个设备,每个都由一个全局永久的内存区组成,全局表示如果设备被多次打开设备中含有的数据将由所有打开它的文件描述符所共享,永久表示如果设备关闭又重新打开,数据不会丢失。
Scullpipe0到scullpipe3四个FIFO(队列特性,先入先出)设备,行为像管道,一个进程读的内容来自另一个进程所写的,如果多个进程读同一个设备,它们将竞争数据。
Scullsingle、scullpriv、sculluid、scullwuid这些设备与scull0相似,但是在允许打开的时候有一些限制,scullsingle同一时刻(即一次)只允许一个进程使用驱动,scullpriv对每个虚拟终端是私有的,因为每个控制台/终端的进程有不同的内存区,sculluid和scullwuid可以多次打开,但是一次只能一个用户打开。
每个scull设备扮演了不同的驱动角色,并且难度不同。
2、主次编号
字符设备通过文件系统中的名字来存取,这些名字是特殊文件,即设备文件(节点),通常位于/dev目录下,在这个目录下,使用ls -l命令将会看到设备节点的信息,在想命令输出的第一列中以“c”标识的为字符设备,同理“b”标识的为块设备。
在使用ls -l命令的显示中,我们会看到以逗号分隔的两个数字,在最后修改日期的前面,这些数字就是设备节点的主次设备编号。
传统上,主编号标识与设备相关联的驱动,例如,/dev/null和/dev/zero都由驱动1来管理,而虚拟控制台和串口终端都由驱动4来管理,同样,vcs1和vcsa1设备都由驱动7管理,现在linux内核允许多个驱动共享主编号,但是通常大部分设备仍然是一个主编号对应一个驱动。
次编号被内核用来决定引用那个设备(即使用次编号在本地设备数组中查找,例如,两个功能相同的设备(比如两个磁盘),使用的驱动相同,所以设备节点文件中主次编号就起到了确定设备的作用)。
2.1、设备编号的内部表示
在内核中,dev_t类型用来持有设备编号,包括主次编号。2.6.0内核中,dev_t是32位的量,12位用作主编号,20位用作次编号。
获取一个dev_t的主次编号,使用宏:
MAJOR(dev_t dev);
MINOR(dev_t dev);
反之,如果你有主次编号,需要转换为一个dev_t可以使用:
MKDEV(int major , int minor);
2.2、分配和释放设备编号
在建立一个字符驱动时,你的驱动需要做的第一件事就是获取一个或多个设备编号来使用,为此需要的函数是:
Int register_chrdev_region(dev_t first , unsigned int count , char *name);
First是要分配的起始设备编号,first的次编号通常是0,count是请求的连续设备编号的总数。那么连接到这个编号范围的设备的名字,它会出现在/proc/devices和sysfs中。
分配成功函数返回0,失败返回一个负的错误码,这同大部分内核函数相同。这个函数需要你事先知道需要的主设备编号,但是通常你不会知道,这就需要动态分配主编号。具体函数为:
Int alloc_chrdev_region(dev_t* dev , unsigned int firstminor , unsigned int count , char *name);
函数中dev是一个只输出的参数,在函数成功后持有你分配范围的第一个数,firstminor应当是请求的第一个要用的次编号,通常是0,count和name同上一个函数一样。
无论怎么样分配设备编号,在不使用的时候都应该释放,设备编号释放使用的函数为:
Void unregister_chrdev_region(dev_first , unsigned int count);
调用这个函数的地方通常是模块的清理函数。
在用户程序使用设备之前,你的驱动需要连接到具体的设备操作函数上。
对于新驱动的主编号应该使用动态分配,也就是使用alloc_chrdev_region而不是register_chrdev_region
动态分配的缺点是无法提前串接设备节点,因为分配给你的模块的主编号会变化,但这些不影响驱动的正常使用,因为编号分配后可以从/proc/devices中读取。
为使用动态主编号来加载一个驱动,需要编写一个脚本代替insmod,脚本中在insmod之后,读取/proc/devices来创建/dev下面的设备节点文件。脚本如下:
#!/bin/sh
module="scull" #重定义变量
device="scull"
mode="664"
# invoke insmod with all arguments we got
# and use a pathname, as newer modutils don't look in . by default
/sbin/insmod ./$module.ko $* || exit 1 #加载模块
# remove stale nodes
rm -f /dev/${device}[0-3] #移除已有设备节点
major=$(awk "\\$2==\"$module\" {print \\$1}" /proc/devices) #从/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 -q '^staff:' /etc/group || group="wheel"
chgrp $group /dev/${device}[0-3] #改变设备的组和模式
chmod $mode /dev/${device}[0-3]
与加载脚本对应的是清理脚本,用来清理/dev目录并去除模块。
除此之外你还可以编写一个init脚本,它接受传统参数,如start stop restart 并且完成加载和卸载脚本的功能。
动态分配主编号的典型源码:
if (scull_major) {
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs, "scull");
} else {
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
scull_major = MAJOR(dev);
}
if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
}