字符驱动之设备编号

scull 是一个字符驱动,像操作一个设备一样地操作一块内存区域。

scull 的优势在于它不依赖硬件;它只操作一些从内核分配的内存。

1 scull 的设计

        编写驱动的第一步是定义驱动将要提供给用户程序的能力(机制)。因为我们的"设备"是计算机内存的一部分,所以可自由做我们想做的事情。它可以是一个顺序的或者随机存取的设备, 一个或多个设备, 等等.

2 主次编号

        如果你发出 ls -l 命令, 你会看到在设备文件项中有 2 个数(由一个逗号分隔)在最后修改日期前面, 这里通常是文件长度出现的地方. 这些数字是给特殊设备的主次设备编号.
        下面的列表显示了一个典型系统上出现的几个设备. 它们的主编号是 1, 4, 7, 和 10, 而次编号是 1, 3, 5, 64, 65, 和 129.
crw-rw-rw-  1 root root 1, 3    Apr 11 2002 null
crw-------    1 root root  10, 1  Apr 11 2002 psaux
crw-------    1 root root  4, 1    Oct 28 03:04 tty1
crw-rw-rw- 1 root tty    4, 64   Apr 11 2002 ttys0
crw-rw----  1 root uucp 4, 65  Apr 11 2002 ttyS1
crw--w----  1 vcsa tty    7, 1    Apr 11 2002 vcs1
crw--w----  1 vcsa tty    7,129 Apr 11 2002 vcsa1
crw-rw-rw- 1 root root  1, 5    Apr 11 2002 zero
        传统上, 主编号标识设备相连的驱动. 例如, /dev/null 和 /dev/zero 都由驱动 1 来管理, 而虚拟控制台和串口终端都由驱动 4 管理; 同样, vcs1 和 vcsa1 设备都由驱动 7管理. 现代 Linux 内核允许多个驱动共享主编号, 但是你看到的大部分设备仍然按照一个主编号一个驱动的原则来组织.

        次编号被内核用来决定引用哪个设备. 依据你的驱动是如何编写的(如同我们下面见到的),你可以从内核得到一个你的设备的直接指针, 或者可以自己使用次编号作为本地设备数组的索引. 不论哪个方法, 内核自己几乎不知道次编号的任何事情, 除了它们指向你的驱动实现的设备.

 

在内核中, 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);

 

在建立一个字符驱动时你的驱动需要做的第一件事是获取一个或多个设备编号来使用.

为此目的的必要的函数是 register_chrdev_region, 在 中声明:
int register_chrdev_region(dev_t first, unsigned int count, char *name);
这里, first 是你要分配的起始设备编号. first 的次编号部分常常是 0.

count 是你请求的连续设备编号的总数. 注意, 如果 count 太大, 你要求的范围可能溢出到下一个次编号.

name 是应当连接到这个编号范围的设备的名子; 它会出现在 /proc/devices 和 sysfs 中.

如果分配成功进行, register_chrdev_region 的返回值是 0. 出错则返回一个负的错误码。

但是不确定设备使用哪个主编号的情况下,用这个函数:

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
使用这个函数, dev 是一个只输出的参数, 它在函数成功完成时持有你的分配范围的第一个数. fisetminor 应当是请求的第一个要用的次编号; 它常常是 0. count 和 name 参数如同给 request_chrdev_region 的一样.

不管你任何分配你的设备编号, 你应当在不再使用它们时释放它. 设备编号的释放使用:
void unregister_chrdev_region(dev_t first, unsigned int count);

对于新驱动, 强烈建议你使用动态分配来获取你的主设备编号!

        动态分配的缺点是你无法提前创建设备节点, 因为分配给你的模块的主编号会变化. 对于驱动的正常使用, 这不是问题, 因为一旦编号分配了, 你可从 /proc/devices 中读取它.

下面的脚本, snull_load, 是 scull 发布的一部分. 以模块发布的驱动的用户可以从系统的 rc.local 文件中调用这样一个脚本, 或者在需要模块时手工调用它.

#!/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)
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]


        这个脚本可以通过重定义变量和调整 mknod 行来适用于另外的驱动. 这个脚本仅仅展示了创建 4 个设备, 因为 4 是 scull 源码中缺省的.
        脚本的最后几行可能有些模糊:为什么改变设备的组和模式? 理由是这个脚本必须由超级用户运行, 因此新建的特殊文件由 root 拥有. 许可位缺省的是只有 root 有写权限, 而任何人可以读. 通常, 一个设备节点需要一个不同的存取策略, 因此在某些方面别人的存取权限必须改变. 我们的脚本缺省是给一个用户组存取, 但是你的需求可能不同. 在之后的"设备文件的存取控制"一节中, sculluid 的代码演示了驱动如何能够强制它自己的对设备存取的授权.
还有一个 scull_unload 脚本来清理 /dev 目录并去除模块.

#!/bin/sh

module="scullc"

device="scullc"

# invoke rmmod with all arguments we got

/sbin/rmmod $module $* || exit 1

# remove nodes

rm -f /dev/${device}[0-3] /dev/${device}

exit 0

        作为对使用一对脚本来加载和卸载的另外选择, 你可以编写一个 init 脚本, 准备好放在你的发布使用这些脚本的目录中. 作为 scull 源码的一部分, 我们提供了一个相当完整和可配置的 init 脚本例子, 称为 scull.init; 它接受传统的参数 -- start, stop, 和restart -- 并且完成 scull_load 和 scull_unload 的角色.
        如果反复创建和销毁 /dev 节点, 听来过分了, 有一个有用的办法. 如果你在加载和卸载单个驱动, 你可以在你第一次使用你的脚本创建特殊文件之后, 只使用 rmmod 和 insmod:这样动态编号不是随机的. 并且你每次都可以使用所选的同一个编号, 如果你不加载任何别的动态模块. 在开发中避免长脚本是有用的. 但是这个技巧, 显然不能扩展到一次多于一个驱动.
        安排主编号最好的方式, 我们认为, 是缺省使用动态分配, 而留给自己在加载时指定主编号的选项权, 或者甚至在编译时. scull 实现以这种方式工作; 它使用一个全局变量,scull_major, 来持有选定的编号(还有一个 scull_minor 给次编号). 这个变量初始化为
SCULL_MAJOR, 定义在 scull.h. 发布的源码中的 SCULL_MAJOR 的缺省值是 0, 意思是"使用动态分配". 用户可以接受缺省值或者选择一个特殊主编号, 或者在编译前修改宏定义或者在 insmod 命令行指定一个值给 scull_major. 最后, 通过使用 scull_load 脚本, 用户可以在 scull_load 的命令行传递参数给 insmod.
这是用在 scull 的源码中获取主编号的代码:

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;
}

 

你可能感兴趣的:(Linux驱动)