在本章中,关于设备驱动和设备管理,我们讨论四种内核成分。
●设备类型:在所有Unix系统中为了统一普通设备的操作所采用的分类。
●模块: Linux内核中用于按需加载和卸载目标码的机制。
●内核对象:内核数据结构中支持面向对象的简单操作,还支持维护对象之间的父子关系。
●sysfs :表示系统中设备树的-一个文件系统。

17.1设备类型

在Linux以及所有Unix系统中,设备被分为以下三种类型:

  • 块设备
    块设备通常缩写为blkdev,它是可寻址的,寻址以块为单位,块大小随设备不同而不同;块设备通常支持重定位(seeking) 操作,也就是对数据的随机访问。
    块设备的例子有硬盘、蓝光、光碟,还有如Flash这样的存储设备。
    块设备是通过称为“块设备节点”的特殊文件来访问的,并且通常被挂载为文件系统。我们在第13章已经讨论过了文件系统,在第14章已经讨论过了块设备。

  • 字符设备
    字符设备通常缩写为cdev,它是不可寻址的,仅提供数据的流式访问,就是-一个个字符,或者一个个字节。字符设备的例子有键盘、鼠标、打印机,还有大部分伪设备。
    字符设备是通过称为“字符设备节点”的特殊文件来访问的。与块设备不同,应用程序通过直接访问设备节点与字符设备交互。

  • 网络设备
    网络设备最常见的类型有时也以以太网设备(ethernet devices) 来称呼,它提供了对网络(例如Internet)的访问,这是通过一个物理适配器(如你的膝上型计算机的802.11卡)和一种特定的协议(如IP协议)进行的。
    网络设备打破了Unix的“所有东西都是文件”的设计原则,它不是通过设备节点来访问,而是通过套接字API这样的特殊接口来访问。

Linux还提供了不少其他设备类型,但都是针对单个任务,而非通用的。一个特例是“杂项设备”(miscellaneous device),通常简写为miscdev,它实际上是个简化的字符设备。杂项设备使驱动程序开发者能够很容易地表示一个简单设备——实际 上是对通用基本架构的一种折中。

并不是所有设备驱动都表示物理设备。有些设备驱动是虚拟的,仅提供访问内核功能而已。我们称为“伪设备”(pseudodevice),最常见的如内核随机数发生器(通过/dev/random和/dev/urandom访问)、空设备(通过/dev/null访问)、零设备(通过/dev/zero访问)、满设备(通过/dev/full访问),还有内存设备(通过/dev/mem访问)。
然而,大部分设备驱动是表示物理设备的。

17.2模块

尽管Linux是“单块内核”(monolithic)的操作系统——这是说整个系统内核都运行于一个单独的保护域中,但是Linux内核是模块化组成的,它允许内核在运行时动态地向其中插入或从中删除代码。
这些代码(包括相关的子例程、数据、函数入口和函数出口)被一并组合在一个单独的二进制镜像中,即所谓的可装载内核模块中,或简称为模块。
支持模块的好处是基本内核镜像可以尽可能地小,因为可选的功能和驱动程序可以利用模块形式再提供。模块允许我们方便地删除和重新载人内核代码,也方便了调试工作。而且当热插拔新设备时,可通过命令载入新的驱动程序。

1)Hello, World

与开发我们已经讨论过的大多数内核核心子系统不同,模块开发更接近编写新的应用系统,
因为至少在模块文件中具有人口和出口点。
虽然编写“Hello, World" 程序作为实例实属陈词滥调了,但它的确很让人喜爱。内核模块
Hello,World 出场了:

/*
* he11o.c. _The He11o, WorldI 我们的第一个内核模块
#include <1inux/ init.h>
#include <1inux/ module .h>
#include <linux/kernel. h>
* he11o_ init-初始化函数,当模块装载时被调用,如果成功装载返回零,否则返回非零值
static int hello_ init (void)
printk (KERN_ ALERT "I bear a charmed life. \n") ;
return 0;
hello_ exit-退出函数,当模块卸载时被调用
*/
static void he11o_exit (void)
printk (KERN_ _ALERT "Out, out, brief candle!\n") ;
module_init (hel1o_init) ;
module_exit (he1lo_exit) ;
MODULE_ LICENSE("GPL");
MODULE_ AUTTHOR (”Shakespeare") ;
MODULE_ DESCRIPTION("A He11o,World Module");

这大概是我们所能见到的最简单的内核模块了, hello_ ini() 函数是模块的人口点,它通过module_ init) 例程注册到系统中,在内核装载时被调用。调用module. init0 实际上不是真正的函数调用,而是-一个宏调用,它唯一的参数便是模块的初始化函数。模块的所有初始化函数必须符合下面的形式:
int my
init (void);
因为init函数通常不会被外部函数直接调用,所以你不必导出该函数,故它可标记为static类型。.
init函数会返回一个int型数值,如果初始化(或你的init 函数想做的事情)顺利完成,那么它的返回值为零;否则返回一个非零值。
这个init函数仅仅打印了一条简单的消息,然后返回零。在实际的模块中,init 函数还会注册资源、初始化硬件、分配数据结构等。如果这个文件被静态编译进内核映像中,其init函数将存放在内核映像中,并在内核启动时运行。

hello_exit(函数是模块的出口函数,它由module_exit() 例程注册到系统。在模块从内存卸载时,内核便会调用hello_ exit(. 退出函数可能会在返回前负责清理资源,以保证硬件处于一致状态或者做其他的一些操作。简单说来,exit 函数负责对init函数以及在模块生命周期过程中
所做的一切事情进行撤销工作,基本上就是清理工作。在退出函数返回后,模块就被卸载了。
退出函数必须符合以下形式:
void my_ exit (void);
与init函数一样,你也可以标记其为static.如果上述文件被静态地编译到内核映像中,那么退出函数将不被包含,而且永远都不会被调
用(因为如果不是编译成模块的话,那么代码就不需从内核中卸载)。

MODULE LICENSEO宏用于指定模块的版权。如果载入非GPL模块到系统内存,则会在内核中设置被污染标识——这一个标识只起到记录信息的作用。版权许可证具有两大目的。首先,它具有通告的目的。当oops中设置了被污染的标识时,很多内核开发者对bug的报告缺乏信任,因为他们认为二进制模块(也就是开发者不能调试它)被装载到了内核。其次,非GPL模块不能调用GPL only符号,本章后续的“导出符号表"一节将对其加以描述。最后还要说明,MODULE AUTHORO宏和MODULE DESCRIPTIONO 宏指定了代码作者和模块的简要描述,它们完全是用作信息记录目的。

2)构建模块

在2.6内核中,由于采用了新的"kbuild" 构建系统,现在构建模块相比从前更加容易。构建过程的第一步是决定在哪里管理模块源码。
你可以把模块源码加入到内核源代码树中,
或者是作为一个补丁
或者是最终把你的代码合并到正式的内核代码树中;
另一种可行的方式是在内核源代码树之外维护和构建你的模块源码。

1.放在内核源代码树中
a)
最理想的情况莫过于你的模块正式成为Linoux内核的一部分,这样就会被存放入内核源代码树中。把你的模块代码正确地置于内核中,开始的时候难免需要更多的维护,但这样通常是一劳永逸的解决之道。
当你决定了把你的模块放入内核源代码树中,下一步要清楚你的模块应在内核源代码树中处于何处。设备驱动程序存放在内核源码树根目录下/drivers的子目录下,在其内部,设备驱动文件被进一步按照类别、 类型或特殊驱动程序等更有序地组织起来。如字符设备存在于drivers/char/目录下,而块设备存放在drivers/block/目录下,USB设备则存放在drivers/usb/目录下。文件的具体组织规则并不须绝对墨守成规,不容打破,你可看到许多USB设备也属于字符设备。但是不管怎么样,这些组织关系对我们来说相当容易理解,而且很也准确。

b)
假定你有一个字符设备,而且希望将它存放在drivers/char/目录下,那么要注意,在该目录下同时会存在大量的C源代码文件和许多其他目录。所以对于仅仅只有一两个源文件的设备驱动程序,可以直接存放在该目录下;但如果驱动程序包含许多源文件和其他辅助文件,那么可以创建一个新子目录。这期间并没有什么金科玉律。

c)
假设想建立自己代码的子目录,你的驱动程序是一个钓鱼竿和计算机的接口,名为Fish Master XL 3000,那么你需要在drivers/char/目录下建立一个名为fishing的子目录。
接下来需要向drivers/char/ 下的Makefile文件中添加一行。编辑derivers/charMakefile/并加入:

obj-m += fishing/ #我们可以使用“+=”操作符给变量追加值,              objective    n. 目的; 目标;

这行编译指令告诉模块构建系统,在编译模块时需要进入fishing/子目录中。更可能发生的情况是,你的驱动程序的编译取决于一个特殊配置选项:比如,可能的CONFIG_FISHING_POLE (请看17.2.6节,它会告诉你如何加入一个新的编译选项)。如果这样,你需要用下面的指令代替刚才那条指令:

obj-$ (CONFIG_PISHING_POLB) += fishing/#我们可以使用“+=”操作符给变量追加值

最后,在drivers/char/ishing/下,需要添加一个新Makefile文件,其中需要有下面这行指令:

obj-m += fishing.o#我们可以使用“+=”操作符给变量追加值

d)
一切就绪了,此刻构建系统运行将会进入fishing/ 目录下,并且将fishing.c编译为fishing.ko模块。虽然你写的扩展名是.o,但是模块被编译后的扩展名却是.ko。再一个可能,让你的钓鱼竿驱动程序编译时有编译选项 ,那么你可能需要这么来做:

obj-$ (CONFIG FISHING_POLE) += fishing.o #我们可以使用“+=”操作符给变量追加值

以后,假如你的钓鱼竿驱动程序需要更加智能化——它可以自动检测钓鱼线,这可是最新的鱼竿“必备要求”呀。这时驱动程序源文件可能就不再只有一个了。别怕,朋友,你只要把你的Makefle做如下修改就可搞定:

obj-$ (CONFIG_PISHING_POLE) += fishing.o      #我们可以使用“+=”操作符给变量追加值
fishing-objs := fishing-main.o fishing-line.o  #我们可以使用“+=”操作符给变量赋值,用于赋值的变量注意:某A处某变量做赋值变量,不能使用A处后面该变量的变化后的值,只能使用在A处前面该变量已定义的值。

每当设置了CONFIG FISHING_ POLE, fishing main.c和fshing-line.c,这几个源文件就一起被编译和连接到fishing.ko模块内。
最后一个注意事项是,在构建文件时你可能需要额外的编译标记 ,如果这样,你只需在Makefile中添加如下指令:

EXTRA_ CFLAGS += -DTITANIUM_ POLE  #我们可以使用“+=”操作符给变量追加值

e总结)
如果喜欢把你的源文件置于driver/char/目录下,并且不建立新目录的话,那么你要做的便是将前面提到的行
( 也就是原来处drivers/char/ishing/下你自己的Makefle中的)都加入到drivers/char/Makefle中。
开始编译吧,运行内核构建过程和原来一样。 如果你的模块编译取决于配置选项,比如有CONFIG_ FISHING_ POLE 约束,那么在编译前首先要确保选项被允许。

2.放在内核代码外
a)
如果你喜欢脱离内核源代码树来维护和构建你的模块,把自己作为一个圈外人,那你要做的就是在你自己的源代码树目录中建立一个Makefle文件,它只需要-行指令 :

ob1-m := fishing.o   #我们可以使用“+=”操作符给变量赋值,用于赋值的变量注意:某A处某变量做赋值变量,不能使用A处后面该变量的变化后的值,只能使用在A处前面该变量已定义的值。

这条指令就可把fishing.c编译成fishing.ko。如果你有多个源文件,那么用两行就足够:

obj-m := fishing.o   #我们可以使用“+=”操作符给变量赋值,用于赋值的变量注意:某A处某变量做赋值变量,不能使用A处后面该变量的变化后的值,只能使用在A处前面该变量已定义的值。
fishing-objs := fishing -main.o fishing-line.o #我们可以使用“+=”操作符给变量赋值,用于赋值的变量注意:某A处某变量做赋值变量,不能使用A处后面该变量的变化后的值,只能使用在A处前面该变量已定义的值。

这样一来,fishing main.c和fishing-line.c就一起被编译和连接到fishing.ko 模块内了。
b)
模块在内核内和在内核外构建的最大区别在于构建过程。当模块在内核源代码树外围时,你必须告诉make如何找到内核源代码文件和基础Makefile文件。不过要完成这个工作同样不难:

make -C /kernel/ source/1ocation SUBDIRS= $PWD modules
#subdirectory n. 子目录        PWD:获取当前所在路径       SUBDIRS=$(PWD):告诉内核源码到指定目录去编译内核程序
#使用“=”号,在“=”左侧是变量,右侧是变量的值。变量在声明时需要给予初值,而在使用时,需要给在变量名前加上“$”符号,但最好用小括号“()”或是大括号“{}”把变量给包括起来。
eaidk@ubuntu:~/msg_ws$ make -help
Usage: make [选项] [目标] ...
选项:-b, -m                                       忽略兼容性。-B, --always-make                            无条件地制定所有目标。-C DIRECTORY, --directory=DIRECTORY          在执行任何操作之前,请切换到目录。            -d                                           打印大量调试信息。    --debug[=FLAGS]                              打印各种类型的调试信息。-e, --environment-overrides                  环境变量覆盖生成文件。            --eval=STRING                                将STRING求值为makefile语句。-f FILE, --file=FILE, --makefile=FILE        将FILE读取为生成文件。               -h, --help                                   打印此消息并退出。-i, --ignore-errors                          忽略配方中的错误。          -I DIRECTORY, --include-dir=DIRECTORY        在目录中搜索包含的生成文件。                -j [N], --jobs[=N]                          一次允许N个作业;无参数的无限作业。-k, --keep-going                            当一些目标无法实现时,继续前进。 -l [N], --load-average[=N], --max-load[=N]  除非负载低于N,否则不要启动多个作业。-L, --check-symlink-times                   在符号链接和目标之间使用最新的mtime。-n, --just-print, --dry-run, --recon        不要实际运行任何配方;把它们打印出来。  -o FILE, --old-file=FILE, --assume-old=FILE 考虑一下FILE非常旧,不要重新制作它。    -O[TYPE], --output-sync[=TYPE]              按类型同步并行作业的输出。  -p, --print-data-base                       打印制造商的内部数据库。-q, --question                              不运行配方;退出状态显示是否为最新。         -r, --no-builtin-rules       禁用内置隐式规则。-R, --no-builtin-variables   禁用内置变量设置。-s, --silent, --quiet        不要重复食谱。      -S, --no-keep-going, --stop  关闭-k。                    -t, --touch                  触摸目标,而不是重新制作。--trace                      打印跟踪信息。-v, --version                打印制造商的版本号并退出。-w, --print-directory        打印当前目录。--no-print-directory         关闭-w,即使它已隐式打开。-W FILE, --what-if=FILE, --new-file=FILE, --assume-new=FILE    考虑FILE是无限新的。                     --warn-undefined-variables   引用未定义变量时发出警告。此程序是为x86_64-pc-linux-gnu构建的
向报告错误<bug-make@gnu.org>eaidk@ubuntu:~/msg_ws$

在这个例子中,/kerme/source/location是你配置的内核源代码树。回想一下,不要把要处理的内核源代码树放在/usr/src/linux下,而要移到你home目录下某个方便访问的地方。

3)安装模块

编译后的模块将被装人到目录/lib/modules/版本/kernel/下,在kernel/目录下的每一个目录都对应着内核源码树中的模块位置。
如果使用的是2.6.34内核,而且将你的模块源代码直接放在drivers/char/下,那么编译后的钓鱼竿驱动程序的存放路径将是: /lib/modules/2.6.34/kerne/drivers/char/fishing.ko.

下面的构建命令用来安装编译的模块到合适的目录下:

make modules_ install #通常需要以root权限运行。

4)产生模块依赖性

Linux模块之间存在依赖性,也就是说钓鱼模块依赖于鱼饵模块,那么当你载入钓鱼模块时,鱼饵模块会被自动载入。
这里需要的依赖信息必须事先生成。多数Linux发布版都能自动产生这些依赖关系信息,而且在每次启动时更新。若想产生内核依赖关系的信息,root 用户可运行命令

depmod

为了执行更快的更新操作,那么可以只为新模块生成依赖信息,而不是生成所有的依赖关系,这时root用户可运行命令

depmod -A

模块依赖关系信息存放在/1ib/modules/version/modules.dep文件中。

5)载入模块

a)insmod与rmmod
载入模块最简单的方法是通过insmod命令,这是个功能很有限的命令,它能做的就是请求内核载入指定的模块。
insmod 程序不执行任何依赖性分析或进一步的错误检查。 它用法简单,以root身份运行命令:

insmod fishing. ko#这里,fishing.ko 是要载入的模块名称。

类似的,卸载一个模块,你可使用rmmod命令,它同样需要以root身份运行:

rmmod fishing#卸载钓鱼竿模块。

b)modprobe与modprobe
这两个命令是很简单,但是它们一点也不智能。先进工具modprobe提供了模块依赖性分析、错误智能检查、错误报告以及许多其他功能和选项。我强烈建议大家用这个命令。
为了在内核via modprobe中插人模块,需要以root身份运行:

modprobe module #其中,参数module指定了需要载入的模块名称,后面的參数将在模块加载时传入内核。(请看17.2.7一节对模块参数的讨论)。
#modprobe命令不但会加载指定的模块,而且会自动加载任何它所依赖的有关模块。所以说它是加载模块的最佳机制。

modprobe命令也可用来从内核中卸载模块,当然这也需要以root身份运行:

modprobe -r modules#参数modules指定一个或多个需要卸载的模块。
#与mmod命令不同,modprobe 也会卸载给定模块所依赖的相关模块,但其前提是这些相关模块没有被使用。Linux 用户手册第8部分提供了上述命令的使用参考,里面包括了命令选项和用法。

6)管理配置选项

在前面的内容中我们看到,只要设置了CONFIG_ FISHING_ POLE配置选项,钓鱼竿模块就将被自动编译。
虽然配置选项在前面已经讨论过了,但这里我们将继续以钓鱼竿驱动程序为例,再看看一个新的配置选项如何加入。

由于2.6内核中新引入了“kbuild” 系统,因此,加入一个新配置选项现在可以说是易如反掌。
你所需做的全部就是向kconfig文件中添加一项,用以对应内核源码树。对驱动程序而言,kconfig通常和源代码处于同一目录。
如果钓鱼竿驱动程序在目录drivers/char/下,那么你便会发现drivers/char/kconfg也同时存在。

如果你建立了一个新子目录,而且也希望kconfig文件存在于该目录中的话,那么你必须在一个已存在的kconfg文件中将它引入。你需要加入下面一行指令:
source “drivers/ char /ishi ng/ Kconfig”

这里所谓存在的Kconfig文件可能是drivers/char/Kconfg.Kconfig文件很方便加入一个配置选型,请看钓鱼竿模块的选项,如下所示:

配置选项第一行定义了该选项所代表的配置目标。注意CONFIG_前缀并不需要写上。

配置选项第二行声明选项类型为tristate, 也就是说可以编译进内核(Y), 也可作为模块编译(M),或者干脆不编译它(N)。
如果编译选项代表的是一个系统功能,而不是一个模块,那么编译选项将用bool指令代替tristate, 这说明它不允许被编译成模块。
处于指令之后的引号内文字为该选项指定了名称。

配置选项第三行指定了该选项的默认选择,这里默认操作是不编译它(N)。 也可以把默认选择指定为编
译进内核(Y),或者编译成一个模块(M)。对驱动程序而言,默认选择通常为不编译进内核(N)。

Help指令的目的是为该选项提供帮助文档。各种配置工具都可以按要求显示这些帮助。因
为这些帮助是面向编译内核的用户和开发者的,所以帮助内容简洁扼要。一般的用户通常不会编
译内核,但如果他们想试试,往往也能理解配置帮助的意思。

除了上述选项以外,还存在其他选项。比如depends指令规定了在该选项被设置前,首先要设置的选项。
假如依赖性不满足, 那么该选项就被禁止。比如,如果你加入指令:
depends on PISH_ TANK#到配置选项中,那么就意味着在CONFIG_ FISH_ TANK被选择前,我们的钓鱼竿模块是不能使用的(Y或者M)。

Select 指令和depends类似,它们只有一点不同之处——只要是select指定了谁,它就会强行将被指定的选项打开。所以这个指令可不能向depends 那样滥用一通,因为它会自动的激活其他配置选项。它的用法和depends一样。比如:
select BAIT#意味着当CONFIG_ FISHING_ POLE被激活时,配置选项CONFIG_ BAIT 必然一起被激活。

如果select和depends同时指定多个选项,那就需要通过&&指令来进行多选。使用depends时,你还可以利用叹号前缀来指明禁止某个选项。比如:
depends on EXAMPLB_ DRIVERS !NO_ FISHING_ ALLOWED#这行指令就指定驱动程序安装要求打开CONFIG EXAMPLE DRIVERS选项,同时要禁止CONFIG NO_ FISHING ALLOWED选项。

tristate和bool选项往往会结合if指令一起使用, 这表示某个选项取决于另一个配置选项。
如果条件不满足,配置选项不但会被禁止,甚至不会显示在配置工具中,比如,要求配置系统只有在CONFIG x86配置选项设置时才显示某选项。请看下面指令:
bool “Deep Sea Mode” if OCEAN

If指令也可与default指令结合使用,强制只有在条件满足时default选项才有效。
配置系统导出了一些元选项(meta-option)以简化生成配置文件。比如选项CONFIG_EMBEDDED是用于关闭那些用户想要禁止的关键功能(比如要在嵌入系统中节省珍贵的内存)

选项CONFIG BROKEN_ ON_ _SMP用来表示驱动程序并非多处理器安全的。通常该项不应设置,标记它的目的是确保用户能知道该驱动程序的弱点。当然,新的驱动程序不应该使用该标志。

最后要说明CONFIG EXPERIMENTAL选项,它是一个用于说明某项功能尚在试验或处于beta版阶段的标志选项。该选项默认情况下关闭,同样,标记它的目的是为了让用户在使用驱动程序前明白潜在风险。

7)模块参数

Linux提供了这样一个简单框架——它可允许驱动程序声明参数,从而用户可以在系统启动或者模块装载时再指定参数值,这些参数对于驱动程序属于全局变量。
值得一提的是模块参数同时也将出现在sysfs 文件系统中(见本章后面的介绍),这样一来,无论是生成模块参数,还是管理模块参数的方式都变得灵活多样了。

a)宏module_ param()
定义一个模块参数可通过宏module_ param() 完成:

module_ param (name ,type, perm) ;
//参数name既是用户可见的参数名,也是你模块中存放模块参数的变量名。
//参数type则存放了参数的类型,它可以是bytc、short、 ushort、 int、 uint、 long、 ulong、 charp、 bool 或invbool,它们分别代表字节型、短整型、无符号短整形、整型、无符号整型、长整形、无符号长整型、字boolean类型存放在int变量中,其余的类型都一致对应C语言的变量类型。
//最后一个参数 perm指定了模块在sysfs文件系统下对应文件的权限,该值可以是八进制的格式,比如0644 (所有者可以读写,组内可以读,其他人可以读);或是S_ Ifoo 的定义形式,比如S_ IRUGO | s_ IWUSR(任何人可读,user 可写);如果该值是零,则表示禁止所有的sysfs项。

上面的宏其实并没有定义变量,你必须在使用该宏前进行变量定义。通常使用类似下面的语句完成定义:

/*在模块参数控制下,我们允许在钓鱼竿上用活鱼饵*/
static int allow_ live_ bait=1;
/*默认功能允许*/
module_ param(allow_ live_ bait, bool, 0644); /* 一个Boolean类型*/

这个值处于模块代码文件外部,换句话说,allow live_ bait 是个全局变量。

b)宏module_ param_named()
有可能模块的外部参数名称不同于它对应的内部变量名称,这时就该使用宏module_param_named()定义了:
module_param_named (name,variable, type, perm) ;
参数name是外部可见的参数名称,参數variable是参数对应的内部全局变量名称。比如:
static unsigned int max_test = DEFAULT_MAX_LINE_TEST;
module_param_named (maximum_ 1ine_ test, max_ test, int, 0);//外部可见的参数名maximum_ 1ine_ test,对应的内部全局变量名max_ test,参数类型int,文件的权限0

c)charp类型
通常,需要用一个charp类型来定义模块参数(一个字符串),内核将用户提供的这个字符串拷贝到内存,而且将变量指向该字符串。比如:
static char *name;
module_ param (name, charp, 0);

d)宏module_param_string()
如果需要,也可使内核直接拷贝字符串到指定的字符数组。宏module_param_string()可完成上述任务:
module_ param_string (name, string,len, perm) ;
这里参数name为外部参数名称,参数string是对应的内部变量名称,参数len是string命名缓冲区的长度(或更小的长度,但是没什么太大的意义),参數perm是sysfs文件系统的访问权限(如果为零,则表示完全禁止sysfs项),比如:
static char species [BUP_LEN] ;
module_ param string (specifies, species, BUP_LEN, 0) ;

e)宏module_ param array()
你可接受逗号分隔的参数序列,这些参数序列可通过宏module_ param array() 存储在C数组中:
module_ param_array(name, type, nump, perm) ;
参数name仍然是外部参数以及对应内部变量名,参数type是数据类型,参数perm是sysfs文件系统访向权限,这里新参数是nump,它是一个整型指针,该整型存放数组项数。注意由参数name指定的数组必须是静态分配的,内核需要在编译时确定数组大小,从而保证不会造成溢出。该函数用法相当简单,比如:
static int fish [MAX_ FISH] ;
static int nr_ fish;
module_ param_array (fish,int, &nr_fish, 0444) ;

f)宏module_ param_ array_ named()
你可以将内部参数数组命名区别于外部参数,这时你需使用宏:
module_ param_ array_ named (name, array, type, nump, perm) ;
其中参数和其他宏一致。
最后,你可使用MODULB_PARM_ DESCO描述你的参数:
static unsigned short size = 1:
module_param(size, ushort, 0644) ;
MODULE_PARM_ DBSC(size, “The size in inches of the fishing pole.”) ;

g)总结
上述所有宏需要包含<linux/module.h>头文件。

8)导出符号表

模块被载入后,就会被动态地连接到内核。注意,它与用户空间中的动态链接库类似,只有当被显式导出后的外部函数,才可以被动态库调用。

在内核中,导出内核函数需要使用特殊的指令: EXPORT_SYMBOL() 和EXPORT_SYMBOL_GPL()。

导出的内核函数可以被模块调用,而未导出的函数模块则无法被调用。模块代码的链接和调用规则相比核心内核镜像中的代码而言,要更加严格。核心代码在内核中可以调用任意非静态接口,因为所有的核心源代码文件被链接成了同一个镜像。当然,被导出的符号表所含的函数必然也要是非静态的。

导出的内核符号表被看做导出的内核接口,甚至称为内核API。导出符号相当简单,在声明函数后,紧跟上EXPORT_ SYMBOL0指令就搞定了,比如:

/*
*get_ pirate_ beard_ color -返回当前priate胡须的颜色,
*@pirate 是-一个指向pirate结构体的指针;颜色定义在文件<1inux/beard_ colors .h>中
*/
int get_pirate_beard_color (struct pirate *p)
{
return p->beard.color;
}EXPORT_SYMBOL(get_pirate_beard_color) ;

假定get pirate_ beard color() 同时也定义在一个可访问的头文件中,那么现在任何模块都可以访问它。有一些开发者希望自己的接口仅仅对GPL-兼容的模块可见,内核连接器使用MODULE LICENSEO宏可满足这个要求。如果你希望先前的函数仅仅对标记为GPL协议的模块可见,那么你就需要用EXPORT_SYMBOL_GPL():
EXPORT_ SYMBOL_ GPL (get_ pirate_ beard_ color) i

如果你的代码被配置为模块,那么你就必须确保当它被编译为模块时,它所用的全部接口都已被导出,否则就会产生连接错误(而且模块不能成功编译)。

17.3设备模型

2.6内核增加了一个引入注目的新特性一统一设备模型 (device model)。设备模型提供了一个独立的机制专门来表示设备,并描述其在系统中的拓扑结构,从而使得系统具有以下优点:
●代码重复最小化。
●提供诸如引用计数这样的统一机制。
●可以列举系统中所有的设备,观察它们的状态,并且查看它们连接的总线。
●可以将系统中的全部设备结构以树的形式完整、有效地展现出来一 包括所有的总线和内
部连接。
●可以将设备和其对应的驱动联系起来,反之亦然。
●可以将设备按照类型加以归类,比如分类为输入设备,而无需理解物理设备的拓扑结构。
●可以沿设备树的叶子向其根的方向依次遍历,以保证能以正确顺序关闭各设备的电源。
最后一点是实现设备模型的最初动机。若想在内核中实现智能的电源管理,就需要建立表示系统中设备拓扑关系的树结构。当在树上端的设备关闭电源时,内核必须首先关闭该设备节点以下的(处于叶子上的)设备电源。比如内核需要先关闭-一个USB鼠标,然后才可关闭USB控制器;同样内核也必须在关闭PCI总线前先关闭USB控制器。简而言之,若要准确而又高效地完成上述电源管理目标,内核无疑需要一棵设备树。

1)统一设备模型kobject

cdev structure -该对象代表一个字符设备
<linux/cdev.h> 中的 struct cdev

struct cdev{struct kobj ect kobj ;struct module * owner ;const struct file_ operations *ops;struct list_ head list;dev_ t dev;unsigned int count ;
};

统一设备模型的核心部分就是 kobject,通过下面对kobject结构体的介绍,可以大致了解它是如何使得各个物理设备能够以树结构的形式组织起来的。
kobject 本身不代表什么实际的内容,一般都是嵌在其他数据结构中来发挥作用。(感觉有点像内核数据结构链表的节点)

cdev(char device)中嵌入了kobject之后,就可以通过 cdev->kboj.parent 建立cdev之间的层次关系,通过 cdev->kobj.entry 获取关联的所有cdev设备等。
总之,嵌入了kobject之后,cdev设备之间就有了树结构关系,cdev(char device)设备和其他设备之间也有可层次关系。

kobject的定义在 <linux/kobject.h> 中

struct kobject {const char        *name;             /* name指针指向此kobject的名称。同时kobject的名字将显示在sysfs文件系统中,作为一个目录的名字。 */struct list_head   entry;           /* kobject 链表 */struct kobject     *parent;       /* parent指针指向kobject的父对象。这样一来, kobject 就会在内核中构造一个对象层次结构,并且可以将多个对象间的关系表现出来。就如你所看到的,这便是sysfs的真正面目:一个用户空间的文件系统,用来表示内核中kobiject对象的层次结构。 */struct kset        *kset;            /* kobject 的集合,接下来有详细介绍 */struct kobj_type   *ktype;          /* kobject 的类型,接下来有详细介绍 */struct sysfs_dirent    *sd;          /* sd指针指向sysfs _dirent 结构体,该结构体在sysfs 中表示的就是这个kobject。从sysfs文件系统内部看,这个结构体是表示kobject的一个inode结构体。 */struct kref         kref;             /* 提供 kobject 的引用计数 *//* 一些标志位  */unsigned int state_initialized:1;       unsigned int state_in_sysfs:1;unsigned int state_add_uevent_sent:1;unsigned int state_remove_uevent_sent:1;unsigned int uevent_suppress:1;
};

2)结构体kobject的成员ktype

kobject对象被关联到一种特殊的类型,即 ktype (kernel object type内核对象类型的缩写)。
ktype 由kobj_type结构体表示,定义于头文件<linux/kobject.h>中:
ktype是为了描述一族的kobject所具有的普遍属性,也就是将这一族的kobject的属性统一定义一下,避免每个kobject分别定义。

struct kobj_type {void (*release) (struct kobject *) ;//release指针指向在kobject引用计数减至零时要被调用的析构函数。该函数负责释放所有kobject使用的内存和其他相关清理工作。const struct sysfs_ ops *sysfs_ ops;//sysfs_ ops 变量指向sysfs. _ops 结构体。该结构体描述了sysfs 文件读写时的特性struct attribute **default_ attrs;  //default_ attrs 指向一个attribute结构体数组。这些结构体定义了该kobject相关的默认属性。属性描述了给定对象的特征,如果该kobject导出到sysfs 中,那么这些属性都将相应地作为文件而导出。数组中的最后一项必须为NULL。
};

3)结构体kobject的成员kset

kset是kobject对象的集合体。把它看成是一个容器,可将那些相关的kobject对象放入,比如“全部的块设备”置于同一位置。听起来kset与ktype非常类似,好像没有多少实质内容。那么“为什么会需要这两个类似的东西呢?”kset 可把kobject集中一个集合中,而ktype描述相关类型kobject所共有的特性,它们之间的重要区别在于:具有相同ktype的kobject可以被分组到不同的kset。
就是说,在Linux内核中,只有少数一些的ktype,却有多个kset。

kset的定义也不复杂,参见 <linux/kobject.h>

struct kset {struct list_head list;    /* 表示kset中所有kobject的链表 */spinlock_t list_lock;     /* 用于保护 list 的自旋锁*/struct kobject kobj;      /* kset中嵌入的一个kobject,使得kset也可以表现的像一样kobject一样*/struct kset_uevent_ops *uevent_ops;  /* 处理kset中kobject的热插拔事件 提供了与用户空间热插拔进行通信的机制 */
};

4)kobject、ktype和kset的相互关系

这3个概念中,kobject是最基本的。kset和ktype是为了将kobject进行分类,以便将共通的处理集中处理,从而减少代码量,也增加维护性。
这里kset和ktype都是为了将kobject进行分类,为什么会有2中分类呢?
从整个内核的代码来看,其实kset的数量是多于ktype的数量的,同一种ktype的kobject可以位于不同的kset中。
做个不是很恰当的比喻,如果把kobject比作一个人的话,kset相当于一个一个国家,ktype则相当于人种(比如黄种人,白种人等等)。
人种的类型只有少数几个,但是国家确有很多,人种的目的是描述一群人的共通属性,而国家的目地则是为了管理一群人。
同样,ktype侧重于描述,kset侧重于管理。

5)管理和操作kobject

当了解了kobject的内部基本细节后,我们来看管理和操作它的外部接口了。多数时候,驱动程序开发者并不必直接处理kobject,因为kobject是被嵌入到一些特殊类型结构体中的(就如在字符设备结构体中看到的情形),而且会由相关的设备驱动程序在“幕后”管理。即便如此,kobject 并不是有意在隐藏自己,它可以出现在设备驱动代码中,或者可以在设备驱动子系统本身中使用它。

使用kobjcet的第一步需要先来声明和初始化。kobject 通过函数kobject init 进行初始化,该函数定义在文件<linux/kobject.h>中:
void kobject_ init (struct kobject *kobj, struct kobj_ type *ktype) ;
该函数的第一个参数就是需要初始化的kobject对象,在调用初始化函数前,kobject 必须清空。这个工作往往会在kobject所在的上层结构体初始化时完成。如果kobject未被清空,那么只需要调用memset0即可:
memset (kobj, 0, sizeof (*kobj)) ;
在清零后,就可以安全的初始化parent和kset字段。例如,

struct kobject *kobj ;
kobj = kmalloc (sizeof (*kobj), GFP_ KERNEL) ;
if (!kobj)return - ENOMEM;
memset (kobj, 0sizeof (*kobj)) ;//处理kobject未被清空的情况
kobj->kset = my_ kset;
kobject_ init (kobj, y_ ktype) ; //初始化统一设备模型kobject

这多步操作也可以由kobject create() 来自动处理,它返回一个新分配的kobject :
struct kobject *kobject_create (void) ;
使用相当简单:

struct kobject *kobj;
kobj = kobject_ create{) ;
if (!kobj)return -ENOMEM;

大多数情况下,应该调用kobject_ create()创建kobject,或者是调用相关的辅助函数,而不是直接操作这个结构体。

kobject的相关操作都在 <linux/kobject.h> 中定义了,主要由以下一些:

extern void kobject_init(struct kobject *kobj, struct kobj_type *ktype);  /* 初始化一个kobject,设置它是哪种ktype */
extern int __must_check kobject_add(struct kobject *kobj,struct kobject *parent,const char *fmt, ...);   /* 设置此kobject的parent,将此kobject加入到现有对象层次结构中 */
extern int __must_check kobject_init_and_add(struct kobject *kobj,struct kobj_type *ktype,struct kobject *parent,const char *fmt, ...); /* 初始化kobject,完成kobject_add 函数的功能*/extern void kobject_del(struct kobject *kobj); /* 将此kobject从现有对象层次结构中取消 */extern struct kobject * __must_check kobject_create(void); /* 创建一个kobject,比kobject_init更常用 */
extern struct kobject * __must_check kobject_create_and_add(const char *name,struct kobject *parent); /* 创建一个kobject,并将其加入到现有对象层次结构中 */extern int __must_check kobject_rename(struct kobject *, const char *new_name);  /* 改变kobject的名称 */
extern int __must_check kobject_move(struct kobject *, struct kobject *); /* 给kobject设置新的parent */extern struct kobject *kobject_get(struct kobject *kobj); /* 增加kobject的引用计数 +1 */
extern void kobject_put(struct kobject *kobj);            /* 减少kobject的引用计数 -1 */extern char *kobject_get_path(struct kobject *kobj, gfp_t flag);  /* 生成并返回与给定的一个kobj和kset相关联的路径 */

6)结构体kobject的成员引用计数

kref记录kobject被引用的次数,当引用计数降到0的时候,则执行release函数释放相关资源。
kref的定义参见:<linux/kref.h>

struct kref {atomic_t refcount;  /* 只有一个表示引用计数的属性,atomic_t 类型表示对它的访问是原子操作 */
};
void kref_set(struct kref *kref, int num);  /* 设置引用计数的值 */
void kref_init(struct kref *kref);          /* 初始化引用计数 */
void kref_get(struct kref *kref);           /* 增加引用计数 +1 */
int kref_put(struct kref *kref, void (*release) (struct kref *kref)); /* 减少引用计数 -1 ,当减少到0时,释放相应资源 */

17.4 sysfs

文件系统层次结构标准 (FHS) 定义了在类Unix系统中的目录结构和目录内容。它由Linux基金会维护,最新版为2015年六月三日发布的3.0版,仅用于Linux的各类发行版中。
在FHS中:
/proc   将进程和内核信息以文件形式呈现的虚拟文件系统。在Linux中,与procfs mount(进程文件系统)对应
/sys    包含连接到本台计算机的设备信息

a)
sysfs文件系统是一个处于内存中的虚拟文件系统,它为我们提供了kobject 对象层次结构的视图。帮助用户能以一个简单文件系统的方式来观察系统中各种设备的拓扑结构。
借助属性对象,kobject 可以用导出文件的方式,将内核变量提供给用户读取或写入(可选)。

b)
虽然设备模型的初衷是为了方便电源管理而提出的一种设备拓扑结构,但是sysfs 是颇为意外的收获。为了方便调试,设备模型的开发者决定将设备结构树导出为一个文件系统。
这个举措很快被证明是非常明智的,首先sysfs代替了先前处于/proc下的设备相关文件;另外它为系统对象提供了一个很有效的视图。
实际上,sysfs 起初被称为driverfs,它早于kobject出现。最终sysfs使得我们认识到一个全新的对象模型非常有利于系统,于是kobject应运而生。
今天所有2.6内核的系统都拥有sysfs文件系统,而且几乎都毫无例外的将其挂载在sys目录下。

c)
sysfs的诀窍是把kobject对象与目录项(directoryentries)紧密联系起来,这点是通过kobject对象中的dentry字段实现的。
回忆第12章,dentry 结构体表示目录项,通过连接kobject到指定的目录项上,无疑方便地将kobject映射到该目录上。从此,把kobject导出形成文件系统就变得如同在内存中构建目录项一样简单。
好了,kobject 其实已经形成了一棵树一就是我们心爱的对象模型体系。由于kobject被映射到目录项,同时对象层次结构也已经在内存中形成了一棵树,因此sysfs的生成便水到渠成般地简单了。

d)
sysfs 的根目录下包含了至少十个目录: block、bus、 class、 dev、 devices. firmware. fs. kermel、module和power。block目录下的每个子目录都对应着系统中的一个已注册的块设备。
反过来,每个目录下又都包含了该块设备的所有分区。
bus目录提供了一个系统总线视图。
class 目录包含了以高层功能逻辑组织起来的系统设备视图。
dev 目录是已注册设备节点的视图。
devices 目录是系统中设备拓扑结构视图,它直接映射出了内核中设备结构体的组织层次。
firmware目录包含了一些诸如ACPI、EDD、EFI 等低层子系统的特殊树。
fs 目录是已注册文件系统的视图。
kemel 目录包含内核配置项和状态信息,
module 目录则包含系统已加载模块的信息。
power 目录包含系统范围的电源管理数据。

e)
并不是所有的系统都包含所有这些目录,还有些系统含有其他目录,但在这里尚未提到。其中最重要的目录是devices,该目录将设备模型导出到用户空间。
目录结构就是系统中实际的设备拓扑。其他目录中的很多数据都是将devices目录下的数据加以转换加工而得。
比如,/sys/class/net/ 目录是以注册网络接口这一高层概念来组织设备关系的,在这个目录中可能会有目录eth0,它里面包含的devices文件其实就是一个指回到devices下实际设备目录的符号连接。

f)
随便看看你可访问到的任何Linux系统的sys目录,这种系统设备视图相当准确和漂亮,而且可以看到class中的高层概念与devices中的低层物理设备,以及bus中的实际驱动程序之间互相联络是非常广泛的。
当你认识到这种数据是开放的,换句话说,这是内核中维持系统的,很好表示方式时,整个经历都弥足珍贵。

1)sysfs中添加和删除kobject

既然sysfs是kobject的视图,那么内核中肯定提供了在sysfs中操作kobject的API。
kobject结构体中与sysfs关联的字段就是 「struct sysfs_dirent *sd; 」这是一个目录项结构,它表示kobject在sysfs中的位置。

sysfs中添加和删除kobject非常简单,就是上面介绍的 kobject操作中提到的

extern int __must_check kobject_add(struct kobject *kobj,struct kobject *parent,const char *fmt, ...);   /* 设置此kobject的parent,将此kobject加入到现有对象层次结构中 */extern int __must_check kobject_init_and_add(struct kobject *kobj,struct kobj_type *ktype,struct kobject *parent,const char *fmt, ...); /* 初始化kobject,完成kobject_add 函数的功能*/extern void kobject_del(struct kobject *kobj); /* 将此kobject从现有对象层次结构中取消 *///... ...等等

添加了kobject之后,只会增加文件夹,不会增加文件。

因为kobject在sysfs中就是映射成一个文件夹。

添加删除kobject的示例代码

2)向sysfs中添加文件

kobject是映射成sysfs中的目录,那sysfs中的文件是什么呢?
其实sysfs中的文件就是kobject的属性,属性的来源有2个:
a)默认属性
默认属性 : kobject所关联的ktype中的 default_attrs 字段

默认属性 default_attrs 的类型是结构体 struct attribute, 定义在 <linux/sysfs.h>

struct attribute {const char        *name;   /* sysfs文件树中的文件名 */struct module        *owner; /* x86体系结构中已经不再继续使用了,可能在其他体系结构中还会使用 */mode_t            mode;   /* sysfs中该文件的权限 */
};

ktype中的 default_attrs 字段(即默认属性)描述了sysfs中的文件,

b)使用默认属性
还有一个字段 sysfs_ops 则描述了如何使用默认属性。
struct sysfs_ops 的定义也在 <linux/sysfs.h>

struct sysfs_ops {/* 在读sysfs文件时该方法被调用 */ssize_t    (*show)(struct kobject *kobj, struct attribute *attr,char *buffer);/* 在写sysfs文件时该方法被调用 */ssize_t    (*store)(struct kobject *kobj,struct attribute *attr,const char *buffer, size_t size);
};

show 方法在读取sysfs中文件时调用,它会拷贝attr提供的属性到buffer指定的缓冲区
store 方法在写sysfs中文件时调用,它会从buffer中读取size字节的数据到attr提供的属性中

增加默认属性的示例代码

3)内核事件层

内核事件层也是利用kobject和sysfs来实现的,用户空间通过监控sysfs中kobject的属性的变化来异步的捕获内核中kobject发出的信号。
用户空间可以通过一种netlink的机制来获取内核事件。
内核空间向用户空间发送信号使用 kobject_uevent() 函数,具体参见: <linux/kobject.h>

int kobject_uevent(struct kobject *kobj, enum kobject_action action);

下面用个小例子演示一些内核事件的实现原理:

Linux内核设计与实现 第17章 设备与模块相关推荐

  1. Linux 内核设计与实现之第17章 设备与模块

    1 设备类型    Linux中设备被分为三大类:块设备,字符设备,网络设备    块设备:寻址以块为单位,支持重定位操作(数据的随机访问).如硬盘,蓝光光碟等.块设备通过"块设备节点&qu ...

  2. Linux内核设计与实现 第18章 调试

    调试工作艰难是内核级开发区别于用户级开发的一个显著特点.相比于用户级开发,内核调试的难度确实要艰苦得多.更可怕的是,它带来的风险比用户级别更高,内核的一一个错误往往立刻就能让系统崩溃. 驾驭内核调试的 ...

  3. 《linux内核设计与实现》第一章

    第一章Linux内核简介 一.unix 1.Unix的历史 Unix是现存操作系统中最强大和最优秀的系统. --1969年由Ken Thompson和Dernis Ritchie的灵感点亮的产物. - ...

  4. Linux内核设计与实现 第19章 可移植性

    Linux是一个可移植性非常好的操作系统,它广泛支持许多不同体系结构的计算机.可移植性是指代码从一种体系结构移植到另外一种不同的体系结构上的方便程度.我们都知道Linux是可移植的,因为它已经能够在各 ...

  5. Linux内核设计与实现——第4章:进程调度

    多任务 抢占式和非抢占式 Linux的进程调度 2.5前的O(n)调度 2.5的O(1)调度 2.6的 完全公平调度算法(CFS) 策略 进程分类:I/O 和 CPU消耗型进程区别 进程优先级:nic ...

  6. 《linux内核设计与实现》读书笔记第一、二章

    第一章 Linux内核简介 1.1 Unix的历史 1971年,Unix被移植到PDP-11型机中.  1973年,Unix操作系统用C语言改写--为Unix系统的广泛移植铺平了道路.  1977年, ...

  7. Linux内核设计与实现(13)第十三章:虚拟文件系统

    Linux内核设计与实现(13)第十三章:虚拟文件系统 1. 文件系统 1.1 文件系统定义: 1.2 文件系统分类 1.3 标准文件系统:Ext文件系统族 1.4 VFS 1.4.1 VFS 背景 ...

  8. Linux内核设计与实现(1)第一章:Linux内核简介

    Linux内核设计与实现(1)第一章:Linux内核简介 1. linux历史及与Unix关系 2. 内核组成 3. 用户空间和内核空间 4. 系统调用 5. 中断 6. Unix强大的原因 7. L ...

  9. 跟我一起玩《linux内核设计的艺术》第1章(四)——from setup.s to head.s,这回一定让main滚出来!(已解封)

    看到书上1.3的大标题,以为马上就要见着main了,其实啊,还早着呢,光看setup.s和head.s的代码量就知道,跟bootsect.s没有可比性,真多--这确实需要包括我在内的大家多一些耐心,相 ...

  10. 《Linux内核设计与实现》读书笔记(十七)- 设备与模块

    本章主要讨论与linux的设备驱动和设备管理的相关的4个内核成分,设备类型,模块,内核对象,sysfs. 主要内容: 设备类型 内核模块 内核对象 sysfs 总结 1. 设备类型 linux中主要由 ...

最新文章

  1. TensorFlow常用Python扩展包
  2. Python Qt GUI设计:QTableView、QListView、QListWidet、QTableWidget、QTreeWidget和QTreeWidgetltem表格和树类(提升篇—1)
  3. 你可能不需要一个 JavaScript 框架(二)
  4. 替换字符串空格 - Java - StringBuffer
  5. 怎样使用OpenCV进行人脸识别
  6. java解析xml转为Map
  7. nginx,excel模板下载
  8. python做excel表格教程视频_基于Python实现excel表格读写
  9. linux mtu日志,linux MTU调整
  10. 当try和finally都包含return时
  11. JEPLUS之APP自定义插件——JEPLUS软件快速开发平台
  12. php 父子id,父子关系PHP / MYSQL
  13. 在Linux中编译jrtplib
  14. linux下du和df结果不一致的原因及处理
  15. 谷歌浏览器chorme,wap浏览器,字…
  16. matlab fsolve fzero,Matlab中的fzero和fsolve函数
  17. 阿里云 ECS 7 天实践训练营 - day01 -基于ECS搭建FTP服务
  18. 用office toll plus 安装office2019 vol版
  19. 什么是DML与DDL
  20. scanf()函数的用法

热门文章

  1. NFT的未来:RFT、数据经济和Web3堆栈创新的推动者
  2. SQL Server认证培训与考试
  3. Vue-电子签名(E-Signature)
  4. 九阳豆浆机各型号字母数字代表的意思
  5. 运营必备九大互联网思维
  6. 机器学习算法笔记之K近邻算法(KNeighborsClassifier)
  7. C-ECAP认证规则说明
  8. 解决:The requested URL returned error: 403
  9. off-by-onedoublefree. 看雪10月ctf2017 TSRC 第四题赛后学习
  10. 利用Zeplin从设计图自动生成CSS,提高前端样式开发效率