20135327郭皓--读书笔记六
第十七章 设备与模块
在本章中,关于设备驱动和设备管理,我们讨论四种内核成分。
- 设备类型:在所有 Unix 系统中为了统一普通设备的操作所采用的分类.
- 模块: Linux 内核中用于按需加载和卸载目标码的机制.
- 内核对象:内核数据结构中支持面向对象的简单操作,还支持维护对象之间的父子关系。
- sysfs :表示系统中设备树的一个文件系统。
17 .1 设备类型
在 Linux 以及所有 Unix 系统中,设备被分为以下三种类型:
- 块设备
- 字符设备
- 网络设备
块设备通常缩写为 blkdev,它是可寻址的,寻址以块为单位,块大小随设备不同而不同;块设备通常支持重定位(seeking)操作,也就是对数据的随机访问。 字符设备通常缩写为 cdev,它是不可寻址的,仅提供数据的流式访问,就是一个个字符,或者一个个字节。 网络设备最常见的类型有时也以以太网设备(ethemet devices)来称呼,它提供了对网络(例如Internet)的访问,这是通过-个物理适配器(如你的膝上型计算机的 802.11 卡)和一种特定的协议(如 IP协议)进行的。网络设备打破了Unix的“所有东西都是文件”的设计原则,它不是通过设备节点来访问,而是通过套接字 API 这样的特殊接口来访问。
17.2 模块
Linux内核是模块化组成的,它允许内核在运行时动态地向其中插入或从中删除代码。这些代码(包括相关的子例程、数据、函数人口和函数出口)被一并组合在一个单独的二进制镜像中,即所谓的可装载内核模块中,或简称为模块。
17.2.1 Hello, World
hello_ini的函数是模块的入口点,它通过module_ init()例程注册到系统中,在内核装载时被调用。调用module_initO 实际上不是真正的函
数调用,而是一个宏调用,它唯一的参数便是模块的初始化函数。模块的所有初始化函数必须符合下面的形式:
int my _ init (void) ;
因为init 函数通常不会被外部函数直接调用,所以你不必导出该函数,故它可标记为static类型.
hello_ exit()函数是模块的出口函数,它由module_exit()例程应册到系统。退出函数必须符合以下形式:
void my_exit (void);
与init 函数一样,你也可以标记其为static.MODULE_LICENSE()宏用于指定模块的版权。如果载入非GPL模块到系统内存,则会在内核中设置被污染标识一一这个标识只起到记录信息的作用。
17.2.2 构建模块
你的驱动程序是一个钓鱼竿和计算机的接口,名为Fish Master XL 3000
1. 放在内核派代码树中
你需要在 drivers/char/目录下建 立一个名为 fishing 的子目录。接下来需要向drivers/cbar/下的Makefile文件中添加一行。编辑drivers/char/Makefile/并加入:
obj -m +• fishing/
这行编译指令告诉模块构建系统,在编译模块时需要进入 fishing/子目录中。
在drivers的bar/fishing/下,需要添加一个新Makefile 文件,其中需要有下面这行指令:
obj-m +=fishing.o
一切就绪了,此刻构建系统运行将会进入 fishing/ 目录下,并且将 fishing.c 编译为 fishing.ko 模块.虽然你写的扩展名是.o,但是模块被编译后的扩展名却是.ko。
2. 放在内核代码外
如果你喜欢脱离内核源代码树来维护和构建你的模块,把自己作为一个圈外入,那你要做的就是在你自己的源代码树目录中建立一个Makefile 文件,它只需事一行指令:
obj -m : = fishing.o
这条指令就可把 fishing.c 编译成fishing.ko。 如果你有多个源文件, 那么用两行就足够:
obj -m := fishing.o
fishing-objs := fishing-main.o fishing-line.o
这样一来, fisbing-main.c 和 fishing-line.c就一起被编译和连接到 fishing.ko 模块内了。
模块在内核内和在内核外构建的最大区别在于构建过程。 当模块在内核源代码树外围时,你必须告诉 make 如何找到内核源代码文件和基础 Makefile 文件。 不过要完成这个工作同样不难:
make -c / kernel/source/ location SUBDI RS=$PWD modules
在这个例子中, kernel/source/location是你配置的内核源代码树。
17.2.3 安装模块
下面的构建命令用来安装编译的模块到合适的目录下:
make modules install
17.2.4 产生模块依赖性
多数 Linux发布版都能自动产 生这些依赖关系信息,而且在每次启动时更新。若想产生内核依赖关系的信息, root用户可运行命令:
depmod
为了执行更快的更新操作,那么可以只为新模块生成依赖信息,而不是生成所有的依赖关系,这时root用户可运行命令:
depmod -A
17.2.5 戴入模块
载入模块最简单的方法是通过 insmod 命令,这是个功能很有限的命令,它能傲的就是请求内核载入指定的模块。 insmod程序不执行任何依赖性分析或进一步的错误检查。它用法简单, 以 root 身份运行命令:
insmod module.ko
类似的,卸载一个模块,你可使用 rmmod 命令,它同样需要以 root 身份运行:
rmmod module
这两个命令是很简单,但是它们一点也不智能。先进工具 modprobe 提供了模块依赖性分析、错误智能检查、错误报告以及许多其他功能和选项。
我强烈建议大家用这个命令。 为了在内核via modprobe中插入模块,需要以root身份运行:
modprobe module [ module parameters ]
modprobe 命令也可用来从内核中卸载模块,当然这也需要以 root 身份运行:
modprobe -r modules
17.2.6 管理配置选项
如果你建立了一个新子目录,而且也希望kconfig 文件存在子该目录中的话,那么你必须在一个己存在的kconfig文件中将它引入。你需要加入下面一行指令
source "drivers/char/fishing/Kcor,lfig”
这里所谓存在的Kconfig 文件可能是drivers/char/Kconfig。
配置选项第一行定义了该选项所代表的配置目标。注意CONFIG_ 前缀并不需要写上。第二行声明选项类型为住istate,也就是说可以编译进内核( Y ),也可作为模块编译( M),或者干脆不编译它( N)。第三行指定了该选项的默认选择,这里默认操作是不编译它。
17.2.7 模块参数
Linux提供了这样一个简单框架一一它可允许驱动程序声明参数,从而用户可以在系统启动 或者模块装载时再指定参数值,这些参数对于驱动程序属于全局变量。
定义一个模块参数可通过宏module_param() 完成:
module_param(name, type, perm);
参数name既是用户可见的参数名,也是你模块中存放模块参数的变量名.参数 type 则存放 了参数的类型,它可以是byte、 short、 ushort、 int、 uint、 long、 ulong、 charp、bool或invbool, 它们分别代表字节型、短整型、无符号短整形、整型、无符号整型、长整形、无符号长整型;最后一个参数 perm 指定了模块在 sysfs 文件系统下对应文件的权限,该值可以是八进制的格式, 比如“44 (所有者 可以读写,组内可以读,其他人可以读):或是 S_Ifoo 的定义形式,比如 S_IRUGO I S_IWUSR (任何人可读, user可写):如果该值是零,则表示禁止所有的 sysfs项。
有可能模块的外部参数名称不同于它对应的内部变量名称,这时就该使用宏 module_param_ named()定义了:
modul e_param_named(name, variable, type, perm);
如果需要,也可使内核直接拷贝字符事到指定的字符数组。module_param_string()完成上述任务:
module_param_string(name, string, len, perm);
你可以将内部参数数组命名区别于外部参数,这时你需使用宏:
module_param_array_named(name, array, type, nump, perm) ;
最后,你可使用 MODULE_PARM_DESC()描述你的参数:
static unsigned short size - 1; module_param(size, ushort , 0644) ; MODULE_PARM_DESC(size ,”The size in inches of the fishing pole. ”);
17.2.8 导出符号表
在内核中,导出内核函数需要使用特殊的指令: EXPORT_ SYMBOL()和EXPORT_SYMBOL_GPL()。导出的内核函数可以被模块调用,而来导出的函数模块则无陆被调用。
假定get_pirate_ beard_ color() 同时也定义在一个可访问的头文件中,那么现在任何模块都可以访问它。有一些开发者希望自己的接口仅仅对GPL-兼容的模块可见,内核连接器使用MODULE_LICENSE()宏可满足这个要求。如果你希望先前的函数仅仅对标记为GPL 协议的模块可见,那么你就需要用:
EXPORT_SYMBOL_GPL(get_pirate_beard_color);
17.3 设备模型
设备模型提供了一个独立的机制专门来表示设备,并描述其在系统中的拓扑结构,从而使得系统具有以下优点:
- 代码重复最小化.
- 提供诸如引用计数这样的统一机制。
- 可以列举系统中所有的设备,观察它们的状态,并且查看它们连接的总结.
- 可以将系统中的全部设备结构以树的形式完整、有效地展现出来一一包括所有的总统和内部连接。
- 可以将设备和其对应的驱动联系起来,反之亦然。
- 可以将设备按照类型加以归类,比如分类为输入设备,而无需理解物理设备的拓扑结构.
- 可以沿设备树的叶子向其根的方向依次遍历,以保证能以正确顺序关闭各设备的电源。
17.3.1 kobject
设备模型的核心部分就是kobject (kernel objt),它自struct kobject 结构体表示,定义于头文件<linux/kobject>中。sd 指针指向sysfs_ dirent 结构体,该结构体在叨地中表示的就是这kobject. 从sysfs 文件系统内部看,这个结构体是表示kobject的一个inode 结构体.kref提供引用计数。ktype 和kset 结构体对kobject 对象进行描述和分类.
17.3.2 ktype
kobject 对象被关联到一种特殊的类型,目p ktype (kernel object type 的缩写)。ktype 由kobj_type 结构体表示,定义于头文件<linux/kobjt.h>中
ktype 的存在是为了描述一族kobject 所具有的普遍特性.如此一来,不再需要每个kolct部分别定义自己的特性,而是将这些普遍的特性在ktype 结构中一次定义,然后所有“同类”的kobject 都能共享一样的特性。
sysfs_ops 变量指向sysfs_ops 结构体。该结构体描述了sysfs 文件读写时的特性。
17.3.3 kset
kset 是kobject 对象的集合体。把它看成是一个容器,可将所有相关的koject 对象,比如“全部的块设备”置于同一位置。kobject 的kset 指针指向相应的kset 集合。kset 集合由kset 结构体表示,定义于头文件
<linux/kobject.h>中:
在这个结构中,其中list 连接该集合( kset )中所有的kobject 对象, list_lock 是保护这个链表中元素的自旋锁。uevent 就是用户事件(user event )的缩写,提供了与用户空间热插拔信息进行通信的机制。
17.3.4 kobject 、ktype 和kset 的相互关系
这里最重要的家伙是kobject,它由struct koject 表示。kobject 为我们引入了诸如引用计数(reference counting)、父子关系和对象名称等基本对象道具,并且是以一个统一的方式提供这些功能。不过kobject 本身意义井不大,通常情况下它需要被嵌入到其他数据结构中,让那些包含
它的结构具有了kobject 的特性。
kobject 叉归入了称作kset 的集合, kset 集合由struct kset 结构体表示。kset 提供了两个功能。
- 第一,其中嵌入的kobj创作为kobject 组的基类。
- 第二, kset 将相关的kobject 集合在一起。在sysfs 中,这些相关的koject 将以独立的目录出现在文件系统中。这些相关的目录,也许是给定目录的所有子目录,它们可能处于同一个kset 。
17.3.5 管理和操作kobject
使用kobjcet 的第一步需要先来声明和初始化。kobject 通过函数ko均ect_init 进行初始化,该函数定义在文件<linux/kobject.h>中
void kobject_init(struct kobject •kobj, struct kobj_type •ktype);
这个工作往往会在kobject 所在的上层结构体初始化时完成。如果kobject 未被清空,那么只需要调用memset() 即可:
memset(kobj, 0, sizeof (*kobj ) );
这多步操作也可以自kobject.te()来自动处理,它返回一个新分配的kobject:
struct kobject •kobject_create(void);
17.3.6 引用计数
递增和递减引用计数:增加一个引用计数可通过koject_getO 函数完成
struct kobject • kobject_get(struct kobject •kobj) ;
该函数正常情况下将返回一个指向kobject的指针,如果失败则返回NULL指针:碱少引用计数通过kobject_putO 完成,这个指令也声明在《linux/kobject.h》中
void kobject_put(struct kobject •kobj);
kref:该函数将使得引用计数减1 ,如果计数减少到零,则要调用作为参数提供的release() 函数.注意ON()声明,提供的时回拨。函数不能简单地采用凶叫,它必须是一个仅接收一个kref结构体作为参数的特有函数,而且还没有返回值. kref_put() 函数返回0,但有一种情况下它返回I,那就是在对该对象的最后一个引用减1时。
17.4 sysfs
sys俗文件系统是一个处于内存中的虚拟文件系统,它为我们提供kobject 对象层次结构的视圈。帮助用户能以一个简单文件系统的方式来观察系统中各种设备的拓扑结构.借助属性对象, kobject 可以用导出文件的方式,将内核变量提供给用户读取或写入sysfs 的诀窍是把kobject 对象与目录项(directory entries)紧密联系起来,这点是通过kobject 对象中的dentry 字段实现的.
由于kobject 被映射到目录项,同时对象层次结构也已经在内存中形成了一棵树,因此sys的生成便水到渠成般地简单了.
Sysf单的根目录下包含了至少十个目录。其中最重要的目录是devices , 该目录将设备模型导出到用户空间. 目录结构就是系统中实际的设备拓扑
17.4.1 sysfs 中添加和删除kobject
仅仅初始化kobject 是不能自动将其导出到sysfs 中的,想要把kot加ct 导入sysfs,你需要用到函数kobject_add():
int kobject_add (struct kobj ect •kobj , struct kobj ect •parent, const char •fmt , . .. ) ;
辅助函数kobject_create_and_add()把kobject_ createO 和koect_add一个函数中:
struct kobject •kobject_create_and_add(const char • name, struct kobject •parent) ;
从sysfs 中删除一个kobject 对应文件目录,需使用函数kobject_del() :
void kobject _del(struct kobject *kobj );
17.4.2 向sysfs 中添加文件
默认属性:默认的文件集合是通过kobject 和kset 中的ktype 字段提供的。因此所有具有相同类型的kobject 在它们对应的sysfs 目录下都拥有相同的默认文件集合。其中名称字段提供了该属性的名称,最终出现在sysfs 中的文件名就是它。owner 字段在存在所属模块的情况下指向其所属的module结构体。store()方法在写操作时调用,它会从buffer 中读取size 大小的字节,并将其存放入a即表示的属性结构体变量中。缓冲区的大小总是为PAGE_SIZE 或更小些。该函数如果执行成功,则将返回实际从buffer 中读取的字节数:如果失败,则返回负数的错误码。
创建新属性:事实上,因为所有具有相同ktype 的kobject,在本质上区别不大的情况下,都应是相互接近的.也就是说,比如对于所有的分区而言,它们完全可以具有同样的属性集会。这不但可以让事情简单,有助于代码合井,还使类似对象在sy的目录中外现一致。sysfs_create_ file()接口:
int sysfs_create_file(struct kobject *kobj, const struct attribute *attr);
除了添加文件外,还有可能需要创建符号连接。在sysfs 中创建一个符号连接相当简单:
int sysfs_create_link(struct kobject *kobj, struct kobject ·get, char name);
删除新属性:删除一个属性需通过函数sysfs_remove_ file() 完成:
void sysfs_remove_file (struct kobject *kobj, const struct attribute *at tr);
一旦调用返回,给定的属性将不再存在于给定的ko均ect 目录中。另外由sysfs_ creat_ link()创建的符号连接可通过函数sysfs_remove link()删除:
void sysfs_remove_link(struct kobject *kobj , char *name);
sysfs约定:当前sy拙文件系统代替了以前需要由ioctl() (作用于设备节点)和procfs 文件系统完成的功能。但是为了保持sysfs 干净和直观,开发者必须遵从以下约定。
- 首先, sysfs 属性应该保证每个文件只导出一个值,该值应该是文本形式而且映射为简单C类型。
- 其次,在sy峙中要以一个清晰的层次组织数据。
- 最后,记住sysfs 提供内核到用户空闹的服务,这多少有些用户空间的ABI (应用程序二进制接口〉的作用。
17.4.3 内核事件层
每个事件都被赋予了一个动词或动作字符串表示信号。该字符串会以“被修改过”或“未挂载”等词语来描述事件。在内核代码中向用户空间发送信号使用函数kobject_uevent():
int kobject_uevent(struct kobject *kobj,enum kobject_action action);
第一个参数指定发送该信号的koject 对象。实际的内核事件将包含该koject 映射到sysfs 的路径。第二个参数指定了描述该信号的“动作”或“动词”
使用kobject 和属性不但有利于很好的实现基于sysfs 的事件,同时也有利于创建新kojects对象和属性来表示新对象和数据一一它们尚未出现在sysfs中.
17.5 小结
本章中,我们考察的内核功能涉及设备驱动的实现和设备树的管理,包括模块、 kobject (以 及相关的 kset和 ktype)和 sysfs。这些功能对于设备驱动程序的开发者来说是至关重要的,因为 这能够让他们写出更为模块化、更为高级的驱动程序. 这章讨论了内核中我们要学习的最后一个子系统,从下面开始要介绍一些普遍的但却重要的 主题,这些主题是任何一个内核开发者都需要了解的,首先要讲的就是调试!