linux设备驱动程序
合集下载
相关主题
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
主设备号和次设备号
通过访问文件系统的名字(或“节点” )访问字符设备,通常这些文件位于 /dev 目录。设备 文件是特殊文件,这一点可以通过 ls -l 输出的第一列中的“c”标明,它说明它们是字符节点。 /dev 下还有块设备, 但它们的第一列是 “b” ; 尽管如下介绍的某些内容也同样适用于块设备, 现在我们只关注字符设备。如果你执行 ls 命令,在设备文件条目的最新修改日期前你会看 到两个数(用逗号分隔) ,这个位置通常显示文件长度。这些数就是相应设备的主设备号和 次设备号。下面的列表给出了我使用的系统上的一些设备。它们的主设备号是 10,1 和 4, 而次设备号是 0,3,5,64-65 和 128-129。 (代码) 主设备号标识设备对应的驱动程序。例如,/dev/null 和/dev/zero 都有驱动程序 1 管理,而所 有的 tty 和 pty 都由驱动程序 4 管理。 内核利用主设备号将设备与相应的驱动程序对应起来。 次设备号只由设备驱动程序使用;内核的其他部分不使用它,仅将它传递给驱动程序。一个 驱动程序控制若干个设备并不为奇(如上面的例子所示) ――次顺便号提供了一种区分它们 的方法。 向系统增加一个驱动程序意味着要赋予它一个主设备号。 这一赋值过程应该在驱动程序 (模 块)的初始化过程中完成,它调用如下函数,这个函数定义在<linux/fs.h>: (代码) 返回值是错误码。当出错时返回一个负值;成功时返回零或正值。参数 major 是所请求的主 设备号,name 是你的设备的名字,它将在/proc/devices 中出现,fops 是一个指向跳转表的指 针,利用这个跳转表完成对设备函数的调用,本章稍后将在“文件操作”一节中介绍这些函 数。 主设备号是一个用来索引静态字符设备数组的整数。在 1.2.13 和早期的 2.x 内核中,这个数 组 有 64 项 , 而 2.0.6 到 2.1.11 的 内 核 则 升 至 128 。 由 于 只 有 设 备 才 处 理 次 设 备 号 , register_chrdev 不传递次设备号。 一旦设备已经注册到内核表中, 无论何时操作与你的设备驱动程序的主设备号匹配的设备文 件,内核都会通过在 fops 跳转表索引调用驱动程序中的正确函数。 接下来的问题就是如何给程序一个它们可以请求你的设备驱动程序的名字。 这个名字必须插 入到/dev 目录中,并与你的驱动程序的主设备号和次设备号相连。 在文件系统上创建一个设备节点的命令是 mknod,而且你必须是超级用户才能创建设备。 除 了要创建的节点名字外,该命令还带三个参数。例如,命令: (代码) 创建一个字符设备(c) ,主设备号是 127,次设备号是 0。由于历史原因,次设备号应该在 0-255 范围内,有时它们存储在一个字节中。存在很多原因扩展可使用的次设备号的范围, 但就现在而言,仍然有 8 位限制。
scull 的设计
编写设备驱动程序的第一步就是定义驱动程序提供给用户程序的能力( “机制” ) 。由于我们 的“设备”是电脑内存的一部分,我做什么都可以。它可以是顺便存取设备,也可以是随机 存取设备,可以是一个设备,也可以是多个,等等。 为了是 scull 更有用,可以成为编写真实设备的驱动程序的模板,我将向你展示如何在电脑 的内存之上实现若干设备抽象操作,每一种操作都有自己的特点。 scull 的源码实现如下设备。由模块实现的每一种设备都涉及一种类型: scull0-3 4 个设备,共保护了 4 片内存区,都是全局性的和持久性的。 “全局性”是指,如果打开设 备多次,所有打开它的文件描述符共享其中的数据。 “持久性”是指,如果设备关闭后再次 打开,数据不丢失。由于可以使用常用命令访问这个设备,如 cp,cat 以及 shell I/O 重定向 等,这个设备操作非常有趣;本章将深入探讨它的内部结构。 scullpipe0-3 4 个“fifo”设备,操作起来有点象管道。一个进程读取另一个进程写入的数据。如果有多 个进程读同一个设备,他们彼此间竞争数据。通过 scullpipe 的内部结构可以了解阻塞型和 非阻塞型读/写是如何实现的;没有中断也会出现这样的情况。尽管真实的驱动程序利用中 断与它们的设备同步, 但阻塞型和非阻塞型操作是非常重要的内Leabharlann Baidu, 从概念上讲与中断处理 (第 9 章,中断处理,介绍)无关。 scullsingle scullpriv sculluid scullwuid 这些设备与 scull0 相似,但在何时允许 open 操作时都不同方式的限制。第一个(scullsingle) 只允许一次一个进程使用驱动程序, 而 scullpriv 对每个虚拟控制台是私有的 (每个设备对虚 拟控制台是私有的) 。sculluid 和 scullwuid 可以多次打开,但每次只能有一个用户;如果另
一个用户锁住了设备,前者返回-EBUSY,而后者则实现为阻塞型 open。通过这些可以展示 如何实现不同的访问策略。 每一个 scull 设备都展示了驱动程序不同的功能,而且都不同的难度。本章主要讲解 scull0-3 的内部结构;第 5 章,字符设备驱动程序的扩展操作,将介绍更复杂的设备: “一个样例 实现:scullpipe”介绍 scullpipe, “设备文件的访问控制”介绍其他设备。
最后,通过使用 scull_load 脚本,用户可以在 scull_load 中命令行中将参数传递给 insmod。 这里是我在 scull.c 中使用的获取主设备号的代码: (代码)
从系统中删除设备驱动程序
当从系统中卸载一个模块时,应该释放主设备号。这一操作可以在 cleanup_module 中调用 如下函数完成: (代码) 参数是要释放的主设备号和相应的设备名。内核对这个名字和设备号对应的名字进行比较: 如果不同,返回-ENINVAL。如果主设备号超出了所允许的范围或是并未分配给这个设备, 内核一样返回-EINVAL。 在 cleanup_module 中注销资源失败会有非常不号的后果。 下次读取 /proc/devices 时,由于其中一个 name 字串仍然指向模块内存,而那片内存已经不存在了, 系统将产生一次失效。这种失效称为 Oops*,内核在访问无效地址时将打印这样的消息。 当你卸载驱动程序而又无法注销主设备号时, 这种情况是无法恢复的, 即便为此专门写一个 “补救”模块也无济于事,因为 unregister_chrdev 中调用了 strcmp,而 strcmp 将使用未映射 的 name 字串,当释放设备时就会使系统 Oops。无需说明,任何视图打开这个异常的设备号 对应的设备的操作都会 Oops。 除了卸载模块, 你还经常需要在卸载驱动程序时删除设备节点。 如果设备节点是在加载时创 建的,可以写一个简单的脚本在卸载时删除它们。对于我们的样例设备,脚本 scull_unload 完成这个工作。如果动态节点没有从/dev 中删除,就会有可能造成不可预期的错误:如果动 态分配的主设备号相同,开发者计算机上的一个空闲/dev/framegrabber 就有可能在一个月后 引用一个火警设备。 “没有这个文件或目录”要比这个新设备所产生的后果要好得多。
动态分配主设备号
某些主设备号已经静态地分配给了大部分公用设备。在内核源码树 的 Documentation/device.txt 文件中可以找到这些设备的列表。由于许多数字已经分配了,为新 设备选择一个唯一的号码是很困难的――不同的设备要不主设备号多得多。 很幸运(或是感谢某些人天才) ,你可以动态分配主设备号了。如果你调用 register_chrdev 时的 major 为零的话,这个函数就会选择一个空闲号码并做为返回值返回。主设备号总是正 的,因此不会和错误码混淆。 我强烈推荐你不要随便选择一个一个当前不用的设备号做为主设备号, 而使用动态分配机制 获取你的主设备号。 动态分配的缺点是, 由于分配给你的主设备号不能保证总是一样的, 无法事先创建设备节点。 然而这不是什么问题,这是因为一旦分配了设备号,你就可以从/proc/devices 读到。为了加 载一个设备驱动程序, 对 insmod 的调用被替换为一个简单的脚本, 它通过/proc/devices 获得 新分配的主设备号,并创建节点。 /proc/devices 一般如下所示: (代码) 加载动态分配主设备号驱动程序的脚本可以利用象 awk 这类工具从 /proc/devices 中获取信 息,并在/dev 中创建文件。 下面这个脚本,scull_load,是 scull 发行中的一部分。使用以模块形式发行的驱动程序的用 户可以在/etc/rc.d/rc.local 中调用这个脚本,或是在需要模块时手工调用。此外还有另一种方 法:使用 kerneld。这个方法和其他模块的高级功能将在第 11 章“Kerneld 和高级模块化” 中介绍。 (代码) 这个脚本同样可以适用于其他驱动程序, 只要重新定义变量和调整 mknod 那几行就可以了。 上面那个脚本创建 4 个设备,4 是 scull 源码中的默认值。 脚本的最后两行看起来有点怪怪的:为什么要改变设备的组和权限呢?原因是这样的,由 root 创建的节点自然也属于 root。默认权限位只允许 root 对其有写访问权,而其他只有读权 限。正常情况下,设备节点需要不同的策略,因此需要进行某些修改。通常允许一组用户访 问对设备, 但实现细节却依赖于设备和系统管理员。 安全是个大问题, 这超出了本书的范围。 scull_load 中的 chmod 和 chgrp 那两行仅仅是最为处理权限问题的一点提示。稍后,在第 5 章的“设备文件的访问控制”一节中将介绍 sculluid 源码,展示设备驱动程序如何实现自己 的设备访问授权。 如果重复地创建和删除/dev 节点似乎有点过分的话, 有一个解决的方法。如果你看了内核源 码 fs/devices.c 的话,你可以看到动态设备号是从 127(或 63)之后开始的,你可以用 127 做为主设备号创建一个长命节点, 同时可以避免在每次相关设备加载时调用脚本。 如果你使 用了几个动态设备,或是新版本的内核改变了动态分配的特性,这个技巧就不能用了。 (如 果内核发生了修改,基于内核内部结构编写的代码并不能保证继续可以工作。 )不管怎样, 由于开发期间模块要不断地加载和卸载,你会发现这一技术在开发期间还是很有用的。 就我看来,分配主设备号的最佳方式是,默认采用动态分配,同时留给你在加载时,甚至是 编译时,指定主设备号的余地。使用我建议的代码将与自动端口探测的代码十分相似。scull 的实现使用了一个全局变量, scull_major ,来保存所选择的设备号。该变量的默认值 是 SCULL_MAJOR,在所发行的源码中为 0,即“选择动态分配” 。用户可以使用这个默认值 或选择某个特定的主设备号, 既可以在编译前修改宏定义, 也可以在 ins_mod 命令行中指定。
字符设备驱动程序设计
本章的目标是编写一个完整的字符设备驱动程序。 由于这类驱动程序适合于大多数简单的硬 件设备,我们首先开放一个字符设备驱动程序。字符也相对比较好理解,比如说块设备驱动 程序。 我们的最终目标是写一个模块化的字符设备驱动程序, 但本章我们不再讲述有关模块 化的问题。 本章通篇都是从一个真实的设备驱动程序截取出的代码块:这个设备就是 scull,是“Simple Character Utility for Loading Localities”的缩写。尽管 scull 是一个设备,但它却是操作内存 的字符设备。这种情况的一个副作用就是,只要涉及 scull, “设备”这个词就可以同“scull 使用的内存区”互换使用。 scull 的优点是,由于每台电脑都有内存,所以它与硬件无关。 scull 用 kmalloc 分配内存, 而 且仅仅操作内存。任何人都可以编译和运行 scull,而且 scull 可以移植到所有 Linux 支持的 平台上。但另一方面,除了演示内核于字符设备驱动程序间的交互过程,可以让用户运行某 些测试例程外,scull 做不了“有用的”事。