杂项字符驱动程序

作者:Alessandro Rubini

有时人们需要编写“小型”设备驱动程序,以支持自定义的技巧——无论是硬件还是软件方面的。为此,以及为了托管一些真正的驱动程序,Linux 内核导出了一个接口,允许模块注册它们自己的小型驱动程序。misc 驱动程序就是为此目的而设计的。这里介绍的代码旨在与 Linux 内核 2.0 版本一起运行。

在 UNIX、Linux 和类似的操作系统中,每个设备都由两个数字标识:“主”设备号和“次”设备号。可以通过调用 ls -l /dev 来查看这些数字。每个设备驱动程序都会向内核注册其主设备号,并完全负责管理其次设备号。使用具有该主设备号的任何设备都将落在同一个设备驱动程序上,而与次设备号无关。因此,每个驱动程序都需要注册一个主设备号,即使它只处理单个设备,例如指点工具。

由于内核保留了设备驱动程序的静态表,因此随意分配主设备号会相当浪费 RAM。因此,Linux 内核为简单的驱动程序(那些将注册单个入口点的驱动程序)提供了一个简化的接口。请注意,一般来说,为每个设备分配整个主设备号命名空间是有益的。这允许处理多个终端、多个串行端口和多个磁盘分区,而内核本身没有任何开销:单个驱动程序负责所有这些,并使用次设备号来区分。

主设备号 10 官方分配给 misc 驱动程序。模块可以使用 misc 驱动程序注册单独的次设备号,并处理小型设备,只需要一个入口点。

注册次设备号

misc 驱动程序导出两个函数供用户模块注册和注销自己的次设备号

#include <linux/miscdevice.h>
int misc_register(struct miscdevice * misc);
int misc_deregister(struct miscdevice * misc);

每个用户模块都可以使用注册函数为其次设备号创建自己的入口点,并在卸载时注销以释放资源。

miscdevice.h 文件还以以下方式声明了 struct miscdevice

struct miscdevice {
        int minor;
        const char *name;
        struct file_operations *fops;
        struct miscdevice *next, *prev;
};

这五个字段具有以下含义

  • minor 是正在注册的次设备号。每个 misc 设备都必须具有不同的次设备号,因为这样的数字是 /dev 中的文件和驱动程序之间唯一的链接。

  • name 是此设备的名称,供人阅读:用户将在 /proc/misc 文件中找到该名称。

  • fops 是指向文件操作的指针,必须使用文件操作来对设备执行操作。文件操作已在 1996 年 4 月之前的“内核角”中描述过。(该文章可在 Web 上找到,网址为 https://linuxjournal.cn/issue24/kk24.html。)无论如何,本文稍后将重新讨论该主题。

  • nextprev 用于管理已注册驱动程序的循环链表。

调用 misc_register 的代码应在调用该函数之前清除 prevnext,并用有意义的值填充前三个字段。

misc 设备驱动程序的真正问题是“minor 字段的合理值是什么?” 次设备号的分配有两种方式:您可以使用“官方分配”的数字,或者您可以求助于动态分配。在后一种情况下,您的驱动程序会请求一个空闲的次设备号,内核会返回一个。

用于分配动态次设备号的典型代码序列如下

static struct miscdevice my_dev;
int init_module(void)
{
    int retval;
    my_dev.minor = MISC_DYNAMIC_MINOR;
    my_dev.name = "my";
    my_dev.fops = &my_fops;
    retval = misc_register(&my_dev);
    if (retval) return retval;
    printk("my: got minor %i\n",my_dev.minor);
    return 0;
}

不用说,真正的模块将在 init_module 中执行其他任务。成功注册后,新的 misc 设备将出现在 /proc/misc 中。此信息性文件报告了哪些 misc 驱动程序可用及其次设备号。加载 my 后,该文件将包含以下行

63 my
这表明 63 是返回的次设备号。如果您想在 /dev 中为您的 misc 模块创建一个入口点,您可以使用 列表 1 中所示的脚本。该脚本负责创建设备节点并为其提供所需的权限和所有权。

您可以选择找到一个未使用的次设备号并将其硬编码到您的驱动程序中。这将省去调用脚本来加载模块的麻烦,但不鼓励这种做法。为了保持代码的简洁性,drivers/char/misc.c 不会检查次设备号的重复。如果您选择的号码稍后分配给官方驱动程序,当您尝试访问您的模块和官方模块时,您将遇到麻烦。

如果同一个次设备号注册了两次,则只有第一个可以从用户空间访问。虽然看起来不公平,但这不能被认为是内核错误,因为没有数据结构被破坏。如果您希望注册一个安全的次设备号,则应使用动态分配。

内核源代码树中的 Documentation/devices.txt 文件列出了所有官方设备号,包括 misc 驱动程序的所有次设备号。

内核配置

如果您尝试编写自己的 misc 驱动程序,但 insmod 返回 unresolved symbol misc_register,则您的内核配置存在问题。

最初,misc 驱动程序被设计为所有“busmouse”驱动程序的包装器——用于每个非串行指点设备的内核驱动程序。只有当当前配置至少包含一个此类鼠标驱动程序时,才会编译该驱动程序。就在 2.0 版本之前,该实现的通用性被广泛接受,驱动程序从“mouse”重命名为“misc”。然而,仍然如此,除非您选择编译至少一个 misc 设备作为模块或本机驱动程序,否则该驱动程序不可用。

如果您的系统上没有安装任何此类设备,您仍然可以加载您的自定义 misc 模块,前提是您在配置内核时对以下问题回答“是”:

Support for user misc device modules (CONFIG_UMISC)

此选项指示即使未选择任何 misc 设备,也应编译 misc 驱动程序,从而允许运行时插入第三方模块。/proc/misc 文件和对动态次设备号的支持是在引入此选项时实现的,因为除非次设备号的分配是安全的,否则拥有自定义模块意义不大。

请注意,如果您的内核配置为仅将 busmice 作为模块加载,则除 /proc/misc 外,一切都将正常工作。仅当 miscdevice.c 直接链接到内核中时,才会创建 /proc 文件。CONFIG_UMISC 也处理这种情况。

操作如何分派

每次进程与设备驱动程序交互时,系统调用的实现都会通过 file_operations 结构将控制权交给正确的驱动程序。此结构由 struct file 携带:每个打开的文件描述符都与这样一个结构相关联,并且 file.f_op 指向其自己的 file_operations 结构。

此设置类似于面向对象的语言:每个对象(这里,每个文件)声明如何对其自身进行操作,以便高级代码独立于正在访问的实际文件。Linux 内核在其实现中充满了面向对象的编程,并且其中存在几个“操作”结构,每个不同的“对象”(inode、内存区域等)都有一个。

回到 misc 驱动程序。my_dev.fops 如何参与游戏?在打开时,内核分配一个新的 file 结构来描述正在打开的对象,并根据文件的类型初始化其操作结构。套接字、FIFO、磁盘文件和设备都有自己不同的操作。当设备打开时,根据主设备号通过引用数组来查找其操作。然后调用驱动程序中的 open 方法。然后,任何其他对文件执行操作的系统调用都将使用 file.f_op,而无需检查任何其他信息来源。因此,驱动程序可以替换 file.f_op 的值,以根据某些内部特性定制 struct file 的行为,即使该特性比主设备号更精细,因此从内核本身不可见。

misc 驱动程序的 open 方法能够通过修改 file.f_op 将操作分派到实际的底层驱动程序;分配的值是 my_dev.f_op 中的值。在操作被覆盖后,该方法调用 file.f_op->open(),以便底层驱动程序可以执行自己的初始化。在文件上调用的每个其他系统调用都将使用 file.f_op 的新值,并且底层驱动程序保持对其设备的完全控制。

一个例子:键盘 LED

由于到目前为止的讨论过于哲学化,现在是时候转向一个工作示例了。kiss 模块(键盘信息状态信号)是一个用于操作键盘 LED 的简单工具。它使用动态次设备号分配将自身注册为 misc 设备,并通过向其写入文本命令来控制。它接受几个单字节命令,例如“N”和“n”(用于打开和关闭 Num-Lock LED)、数字 0 到 7(用于使用 LED 显示该范围内的二进制数)等等。

我认为这里没有必要包含源代码,因为驱动程序所做的只是上面显示的 misc_register 代码。大多数附加代码处理命令的解释和 LED 的实际点亮。包含源代码和 README 文件的 tar 文件可以从 ftp://ftp.linuxjournal.com/pub/lj/listings/issue51/2920.tgz 获取。

与往常一样,本文随附的示例模块非常简单,在现实世界中几乎没有意义。然而,这一次,我认为它可能具有一定的意义。事实上,我电脑中的自定义硬件包括三个 LED,用于监控正在运行的进程数。在我看来,当您想知道为什么计算机没有响应时,这是一个有用的信息——这种情况在您编写有缺陷的驱动程序或打印过多诊断消息的驱动程序时非常常见。

Alessandro 仍然在使用 Linux 2.0,因为他正在花费他的业余时间用电烙铁构建自己的 misc 设备。他喜欢开源和户外活动,并通过 rubini@linux.it 阅读电子邮件。

加载 Disqus 评论