第十七章 设备与模块
关于设备驱动与设备管理,我们讨论四种内核成分。
- 设备类型
- 模块
- 内核对象
- sysfs
17.1设备类型
在Linux以及所有Unix系统中,设备被分为以下三种类型:
- 块设备,块设备通常缩写为blkdev,它是可寻址的,寻址以块为单位,块大小随设备不同而不同;块设备通常支持重定位操作,也就是对数据的随机访问。块设备是通过称为“块设备节点”的特殊文件来访问,并且通常被挂载为文件系统。
- 字符设备,字符设备通常缩写为cdev,它是不可寻址的,仅提供数据的流式访问,就是一个一个字符,或者一个一个字节。字符设备通过称为“字符设备节点”的特殊文件访问。与块设备不同,应用程序直接通过访问设备节点与字符设备交互。
- 网络设备最常见的类型有事也以以太网设备来称呼,它提供了对网络的访问,这是通过一个物理适配器和一种特定的协议来进行的。它不通过设备节点来访问,而是通过套接字API这样的特殊接口来访问。
并不是所有设备驱动都表示物理设备。有些设备驱动是虚拟的,仅提供访问内核功能而已。我们成为“伪设备”。
17.2模块
支持模块的好处是基本内核镜像可以尽可能地小,因为可选的功能和驱动程序可以利用模块形式再提供。模块允许我们方便地删除和重新载入内核代码,也方便了调试工作。
17.2.1Hello,World
MOUDLE_LICENSE()
宏用于指定模块的版权。
MOUDLE_AUTHOR()
宏和MOUDLE_DESCRIPTION()
宏指定了代码作者和模块的简要描述,它们完全是用作信息记录目的。
17.2.2构建模块
1.放在内核源代码树中
- 你需要在
drivers/char/
目录下建立一个名为fishing
的子目录。 - 接下来需要在
drivers/char/
下的Makefile
文件中添加一行。编辑drivers/char/Makefile/
并加入obj-m += fishing/
。该行编译指令告诉模块构建系统,在编译模块时需要进入fishing/
子目录中。更可能发生的情况是驱动程序的编译取决于一个特殊配置选项,比如可能的CONFIG_FISHING_POLE
,如果这样,需要用下面的指令替换刚才的指令:obj-$(CONFIG_FISHING_POLE) +=fishing/
- 最后,在
drivers/char/fishing/
下,需要添加一个新的Makefile
文件,其中需要有这条指令:obj-m += fishing.o
2.放在内核代码外
- 在你自己的源代码树目录中建立一个
Makefile
文件,只需要一行指令:obj-m := fishing.o
- 告诉make如何找到内核源代码文件和基础的
Makefile
文件。make –C /kernel/source/location SUBDIRS=$PWD modules
17.2.3安装模块
下面的构建命令用来安装编译的模块到合适的目录下:make moudles_install
,通常需要以root权限运行。
17.2.4产生模块依赖性
- 若想产生内核依赖关系的信息,root用户可运行命令
depmod
。 - 为了执行更快的更新操作,那么可以只为新模块生成依赖信息,而不是生成所有的依赖关系,这是root用户可运行命令的
depmod -A
。
17.2.5载入模块
- 载入模块,以root身份运行命令:
insmod module.ko
- 卸载一个模块,以root身份运行:
rmmod module
- 为了在内核
via modprobe
中插入模块,需要以root身份运行:modprobe module[module parameters]
modprobe
命令也可用来从内核中卸载模块,当然这也需要以root身份运行:modprobe -r modules
与rmmod命令不同,modprobe
也会卸载给定模块所依赖的相关模块,但其前提是这些相关模块没有被使用。
17.2.6管理配置选项
如果你建立了一个新子目录,而且也希望kconfig文件存在于该目录中的话,那么你必须在一个已存在的kconfig文件中将它引入。你需要加入下面一行指令:source "drivers/char/fishing/Kconfig"
17.2.7模块参数
定义一个模块参数可通过宏module_param()
完成:module_param(name,type,perm);
参数name既是用户可见的参数名,也是你模块中存放模块参数的变量名。参数type则存放了参数的类型。最后一个参数perm指定了模块在sysfs文件系统下对应文件的权限。
17.2.8导出符号表
在内核中,导出内核函数需要使用特殊的指令:EXPORT_SYMBOL()
和EXPORT_SYMBOL_GPL()
。
导出的内核函数可以被模块调用。导出的内核符号表被看作导出的内核接口,甚至称为内核API。
17.3设备模型
17.3.1kobject
设备模型的核心部分是kobject(kernel object)
。kobject
提供了诸如引用计数、名称和父指针等字段,可以创建对象的层次结构。
name指针指向kobject的名称。
parent指针指向kobject的父对象。
kref提供引用计数,ktype和kset结构体对kobject对象进行描述和分类。
struct kobject {
constchar *name;
structlist_head entry;
structkobject *parent;
structkset *kset;
structkobj_type *ktype;
structsysfs_dirent *sd;
structkref kref;
unsignedint state_initialized:1;
unsignedint state_in_sysfs:1;
unsignedint state_add_uevent_sent:1;
unsignedint state_remove_uevent_sent:1;
unsignedint uevent_suppress:1;
};
17.3.2ktype
struct kobj_type {
void(*release)(struct kobject *kobj);
structsysfs_ops *sysfs_ops;
structattribute **default_attrs;
};
ktype是为了描述kobject所具有的普遍特性。
release指针指向在kobject引用计数减至0时要被调用的析构函数。该函数负责释放所有kobject使用的内存和其他相关清理工作。
sysfs_ops
变量指向sysfs_ops
结构体。该结构体描述了sysfs文件读写时的特性。
default_attrs
指向一个attribute结构体数组。这些结构体定义了该kobject相关的默认属性。
17.3.3kset
kset是kobject对象的集合体。kset可把kobject集中到一个集合中,而ktype描述相关类型kobject所共有的特性。区别:具有相同ktype的kobject可以被分组到不同的kset。就是说在Linux内核中只有少数一些的ktype,却有多个kset。
struct kset {
structlist_head list;
spinlock_tlist_lock;
structkobject kobj;
structkset_uevent_ops *uevent_ops;
};
list连接kset中所有的kobject对象,list_lock
是保护这个链表元素的自旋锁,kobj指向的kobject对象代表了该集合的基类。uevent_ops
指向一个结构体用于处理集合中kobject对象的热插拔操作。
17.3.4kobject、ktype和kset的相互关系
kobject本身的意义不大,通常情况下需要被嵌入到其他数据结构中,让那些包含它的结构具有了kobject的特性。
kobject与一个特别的ktype对象关联,ktype定义了一些kobject相关的默认特性:析构行为、sysfs行为以及别的一些默认属性。
kobject又归入了称作kset的集合。kset提供了两个功能:1、其中嵌入的kobject作为kobject组的基类2、kset将相关的kobject集合在一起。
17.3.5管理和操作kobject
- 使用kobject的第一步需要先来生命和初始化。kobject通过函数kobiect_init进行初始化。
- 该函数的第一个参数就是需要初始化的kobject对象,在调用初始化函数前,kobject必须清空。
- 在清零后,就可以安全的初始化parent和kset字段。
- 这多步操作也可以由
kobject_create()
来自动处理,它返回一个新分配的kobject。
17.3.6引用计数
kobject的主要功能之一就是为我们提供了一个统一的引用计数系统。
1.递增和递减引用计数
- 增加一个引用计数可通过
kobject_get()
函数完成:struct kobject * kobject_get(struct kobject *kobj);
- 减少引用计数通过
kobject_put()
完成:void kobject_put(struct kobject *kobj);
2.kref
struct kref{
atomic_t refcount;
};
其中唯一的字段是用来存放引用计数的原子变量。那么为什么采用结构体?这是为了便于进行类型检测。
17.4sysfs
sys文件系统是一个处于内存中的虚拟文件系统,它为我们提供了kobject对象层次结构的视图。
17.4.1sysfs中添加和删除kobject
想要把kobject导入sysfs,需要用到函数kobject_add ()
:int kobject_add(struct kobject *kobj,struct kobject *parent,const char *fmt, ...);
kobject在sysfs中的位置取决于kobject在对象层次结构中的位置。
从sysfs中删除一个kobject对应文件目录,需要使用函数kobject_del()
:void kobject_del(struct kobject *kobj);
17.4.2向sysfs中添加文件
1.默认属性
默认的文件集合是通过kobject和kset中的ktype字段提供的。因此所有具有相同类型的kobject在它们对应的sysfs目录下都拥有相同的默认文件集合。这些属性负责将内核数据映射成sysfs中的文件。
2.创建新属性
内核为能在默认集合之上,再添加新属性而提供了sysfs_create_file()
接口:int sysfs_cerate_file(struct kobject *kobj,const struct attribute *attr);
除了添加文件外,还有可能需要创建符号连接。在sysfs中创建一个符号连接相当简单:int sysfs_create_link(struct kobject *kobj,struct kobject *target,char *name);
3.删除属性
删除一个属性需通过函数sysfs_remove_file()
:void sysfs_remove_file(struct kobject *kobj,const struct attribute *attr);
由sysfs_creat_link()
创建的符号连接可通过函数sysfs_remove_link()
删除:void sysfs_remove_link(struct kobject *kobj,char *name);
4.sysfs约定
- 首先,sysfs属性应该保证每个文件只导出一个值,该值应该是文本形式而且映射为简单C类型。其目的是为了避免数据的过度结构化或太凌乱。
- 其次,在sysfs中要以一个清晰的层次组织数据。
- 最后,记住sysfs提供内核到用户空间的服务,这多少有些用户空间ABI(应用程序二进制接口)的作用。
17.4.3内核事件层
- 内核事件层实现了内核到用户的消息通知系统——就是建立在上文一直讨论的kobject基础之上。
- 每个事件都被赋予了一个动词或动作字符串表示信号。该字符串会以“被修改过”或“未挂载”等词语来描述事件。
- 最后,每个事件都会有一个可选的负载。
- 使用kobject和属性不但有利于很好的实现基于sysfs的事件,同时也有利于创建新kobjects对象和属性来表示新对象和数据——它们尚未出现在sysfs中。