设备与模块
1、设备类型
除了常见的字符设备、块设备、网络设备和杂项设备外,还有一些虚拟的设备,它们被称为“伪设备”。例如,随机数发生器(通过/dev/random和/dev/urandom访问)、空设备(通过/dev/null访问)、零设备(通过/dev/zero访问)、满设备(通过/dev/full访问)和内存设备(通过/dev/mem访问)。
2、模块
MODULE_LICENSE()宏用于指定模块的版权。如果载入非GPL模块到系统内存,则会在内核中设置被污染表示——这个设置只起到记录信息的作用。版权许可证具有两个目的:
- (1)具有通告的目的。当oops中设置了被污染的标识时, 内核开发者对bug的报告缺乏信任。
- (2)非GPL模块不能调动GPL_only符号。
MODULE_AUTHOR() 宏和MODULE_DESCRIPTION() 用于记录作者信息和简要描述信息。
2.1、构建
你的代码可以放在系统树中,也可以作为一个补丁打到系统中,或者作为一个模块代码在内核树之外维护。
2.1.1、放在内核源码树中
如果是数量不多的代码文件,可以根据设备的类型直接放在对应的文件夹中,例如字符设备为driver/char中。如果数量比较多时,为了方便管理,最好重新建一个新的目录,建完目录后需要向driver/char下的Makefile文件中添加一行:
obj-m+=xxxx/ //xxxx为文件名
这行指令会告诉系统在编译时需要进入xxxx的子目录中,不过通常我们会这样写:
obj-$(CONFIG_XXXXX)+= xxxx/
以一个宏来作为编译选项进行选择性编译,然后在xxxx/目录中,添加一个Makefile,其中需要包含以下指令:
obj-$(CONFIG_XXXXX)+=xxxx.o
2.1.2、放在内核源码树外
把上面的 $(CONFIG_XXXXX) 改为 ‘m’ 即可。
2.3、模块默认路径
编译后的模块默认会放到目录 /lib/modules/version/kernel中。
2.4、depmod
使用depmod命令可以使内核产生依赖关系信息。使用depmod -A可以只为新模块生成依赖信息。依赖关系信息存放在 /lib/modules/version/modules.dep中。
2.5、加载模块
使用insmode命令加载模块,insmode命令指示做个请求,不会执行相关的以来分析和进一步的错误检查。对应的卸载命令为rmmod。
使用modprobe加载模块,格式为:
modprobe module [parameters]
加载时后面的参数在模块加载时传入内核。
modprobe命令会在加载模块时自动加载任何它所依赖的有关模块。
对应的卸载命令为modprobe -r modules,命令会卸载所依赖的模块。
2.6、模块参数
可以在驱动程序中声明参数,从而在系统启动时或者模块加载时在指定参数值,这些参数对于驱动程序来讲属于全局变量。模块参数同时也会出现在sysfs文件系统中。
定义一个模块参数可以通过宏module_param() 完成:
module_param(name,type,perm);
name是用户可见的参数名,也是存放模块参数的变量名。
type存放参数的类型。
perm制定了模块在sysfs文件系统下对应文件权限,八进制格式。
module_param() 并没有定义变量,需要在使用前定义一个变量,然后传入给name参数。
如果想要外部可见参数的名称和内部变量名称不同,可以使用module_param_named() 宏定义:
module_param_named(name,varialbe,type,perm);
name参数表示外部可见的参数名称;
varialbe参数对应内部全局变量名称;
传递字符串到指定字符数组,使用宏module_param_string:
module_param_string(name,string,len,perm);
name参数表示外部可见的参数名称;
string对应内存变量名称;
可以接受逗号分隔的参数序列,这些参数通过宏module_param_array() 存储在数组中:
module_param_array(name,type,nump,perm);
nump是一个整型指针,存放数组项数;
name指定的数组必须是静态分配的。
可以使用MODULE_PARM_DESC() 宏来描述你的参数。
所有的宏都定义在 <linux/module.h> 中。
2.7、导出符号表
在内核中,导出函数需要特定的指令:
EXPORT_SYMBOL()
EXPORT_SYMBOL_GPL()
3、设备模型
3.1、kobject
2.6内核引入了一个新的特性——统一设备模型。设备模型提供了一个独立的机制专门表示设备,并描述其在系统中的拓扑结构。设备模型的核心就是kobject结构体,定义在 <linux/kobject.h> 中:
struct kobject {
const char *name;
struct list_head entry;
struct kobject *parent;
struct kset *kset;
struct kobj_type *ktype;
struct kernfs_node *sd; /* sysfs directory entry */
struct kref kref;
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
struct delayed_work release;
#endif
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;
};
kobject结构体的设计类似与面向对象中的对象类,提供了诸如引用计数、名称和父指针等字段,可以创建清晰的对象结构。
name指向了此kobject名称。
parent指针指向了kobject的父对象。
sd指针指向了kernfs_node结构体,kernfs_node结构体是kernfs层次结构的构建块。
kref提供引用计数。ktype和kset结构体对kobject对象进行描述和分类。
kobject结构体通常嵌入到其他结构中,单独的意义不大。比如定义在 <linux/cdev.h> 中的struct cdev 就需要用到kobj结构。
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
嵌入kobject的结构体可以成为对象层次架构中的一部分。比如cdev结构体就可以通过其父指针cdev->kobj.parent和链表cdev->kobj.entry插入到对象层次结构中。
3.2、ktype
ktype由kobj_type结构表示,ktype的存在是为了描述一簇kobject所具有的的普遍特性。这样,就不需要为每个kobject都分别定义在自己的特性。所有同类的kobject共享一样的特性。 ktype定义在 <linux/kobject.h> 中:
struct kobj_type {
void (*release)(struct kobject *kobj);
const struct sysfs_ops *sysfs_ops;
struct attribute **default_attrs;
const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
const void *(*namespace)(struct kobject *kobj);
};
release指针指向在kobject引用计数减至零时要被调用的析构函数,该函数负责释放所有kobject使用的内存和其他相关清理操作。
sysfs_ops变量指向sysfs_ops结构体。该结构体描述了sysfs文件读写时的特性。
default_attrs指向了attribute 结构体数组。这些结构体定义了所属kobject相关的默认属性。这些属性描述了给定对象的特征,如果该kobject导出到sysfs中,那么这些属性都将相应地作为文件而导出。数组最后一项必须是NULL。
3.3、kset
kset是kobject对象的集合。它是一个容器,可将所有相关的kobject对象置于同一位置中。kset和ktype的区别在于:具有相同ktype的kobject可以被分组到不同的kset中。
kobject的kset指针指向对应的kset集合。kset集合由kset结构体表示,定义于头文件 <linux/kobject.h> 中:
struct kset {
struct list_head list;
spinlock_t list_lock;
struct kobject kobj;
const struct kset_uevent_ops *uevent_ops;
};
list连接该集合中的所有kobject对象;
list_lock用来保护这个链表;
kobj指向kobject对象,代表了该集合的基类;
uevent_ops指向kset_uevent_ops结构体,该结构体用于处理集合中kobject对象的热拔插操作。
uevent表示用户事件,提供了与用户空间热插拔信息通信的机制。
3.4、三者的关系
kobject用来去嵌入到其他数据结构中,使得结构具有kobject的特性(层次等);
ktype定义了kobject的一些默认特性:析构、sysfs行为和一些默认属性;
kset嵌入kobject中作为kobjcet组的基类。其次,kset将相关的kobject集合在一起,在sysfs中,这些相关的kobject将以独立的目录出现在文件系统中。
3.5、初始化kobject
首先需要声明和初始化,由kobject_init进行初始化,函数定义在 <linux/kobject.h> 中:
void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
第一个参数为需要初始化的kobject结构对象,初始化之前需要清空对象,一般由上层结构体初始化时完成。
也可以直接使用kobject_create() 函数直接完成:
struct kobject *kobject_create(void)
函数返回初始化完的kobject指针。
初始化完成后就可以对其他字段进行填充。
3.6、引用计数
3.6.1、递增递减引用计数
初始化完成后,kobject的引用计数字段会被置为1。只要引用计数不为零,那么对象就不会被释放,会一直保存在内存之中。
递增一个引用计数使用kobject_get() 函数完成,定义在 <linux/kobject.h> 中:
struct kobject *kobject_get(struct kobject *kobj);
//用法例子
int kobject_rename(struct kobject *kobj, const char *new_name)
{
...
kobj = kobject_get(kobj);
...
}
成功,返回原来指向kobject的指针,失败返回NULL。
递减引用计数通过kobject_put() 函数完成:
void kobject_put(struct kobject *kobj);
如果kobject引用计数减少到零,则与该kobject关联的ktype中的析构函数将被调用。
3.6.2、struct kref
kref结构定义在 <linux/kref.h> 中:
struct kref {
atomic_t refcount;
};
在使用kref之前,需要使用kref_init() 函数初始化:
static inline void kref_init(struct kref *kref)
{
atomic_set(&kref->refcount, 1);
}
使用kref_get() 函数获得kref的引用(kobject_get函数中会调用),函数声明在 <linux/kref.h> 中:
static inline void kref_get(struct kref *kref)
{
/* If refcount was 0 before incrementing then we have a race
* condition when this kref is freeing by some other thread right now.
* In this case one should use kref_get_unless_zero()
*/
WARN_ON_ONCE(atomic_inc_return(&kref->refcount) < 2);
}
减少kref的引用使用kref_put() 函数(kobject_put函数中会调用):
static inline int kref_sub(struct kref *kref, unsigned int count,
void (*release)(struct kref *kref))
{
WARN_ON(release == NULL);
if (atomic_sub_and_test((int) count, &kref->refcount)) {
release(kref); //如果引用计数减少到零,则调用release函数。
return 1;
}
return 0;
}
static inline int kref_put(struct kref *kref, void (*release)(struct kref *kref))
{
return kref_sub(kref, 1, release);
}
kref_put函数通常返回零,只有在该对象的最后一个引用减一时才返回1。
4、sysfs
sysfs文件系统是一个处于内存中的虚拟文件系统,为我们提供kobject对象层次结构的试图。能够帮助用户以一个简单文件系统的方式观察系统中各种设备的拓扑结构。
sysfs的诀窍是把kobject对象与目录项紧密联系在一起(通过kobject对象的dentry字段实现)。由于kobject被映射到目录项,同时对象层次结构也已经在内存中形成了一棵树,因此sysfs的结构就生成了:
- block目录下的每个子目录都对应着系统中一个已注册的块设备。每个目录下又包含了该块设备的所有分区。
- bus目录提供了一个系统总线视图。
- class目录包含了以高层功能逻辑组织起来的系统设备试图。
- dev目录包含已注册设备节点的视图。
- devices目录是系统设备拓扑结构视图,它直接映射出了内核中设备结构体的组织层次。
- fireware目录包含了一些诸如ACPI、EDD、EFI等底层子系统的特殊树。
- fs目录是已注册文件系统的目录。
- kernel目录包含内核配置项和状态信息。
- module目录包含系统已加载模块信息。
- power目录包含系统范围的电源管理数据。
其中devices目录直接将设备模型导出到用户空间,其他目录下很多数据都是将devices目录下的数据加以转换加工而成。例如/sys/class/目录下很多设备中包含的符号连接都指向了/sys/devices目录中。
4.1、sysfs中添加和删除kobject
通过kobjcet_add() 函数可以将kobjcet导入到sysfs中,函数定义在lib/kobject.c中,声明在 <linux/kobject.h> 中:
int kobject_add(struct kobject *kobj, struct kobject *parent,
const char *fmt, ...)
kobject在sysfs中的位置取决于kobject在对象层次结构中的位置。如果kobject的父指针被设置,那么在sysfs中kobject将被映射到其父目录下的子目录;如果父指针没有被设置那么kobject将被映射到kset->kobj中的子目录。如果给定的kobject中的parent和kset字段没有被设置,那么就认定kobject没有父对象,就会被映射到sysfs的根目录。因此在调用kobject_add() 函数之前应该设置好恰当的parent和kset字段。
sysfs中代表kobject的目录名字由fmt指定,fmt接受printf样式的字符串。
辅助函数kobject_create_and_add() 把kobject_create() 和kobjcet_add() 函数结合在一起:
struct kobject *kobject_create_and_add(const char *name, struct kobject *parent)
name为指针形式传入的目录名称。
从sysfs中删除一个kobject对应文件目录,需使用函数kobject_del() :
void kobject_del(struct kobject *kobj);
4.2、向sysfs中添加文件
4.2.1、默认属性
默认的文件集合是通过kobject中的ktype字段提供的。具有相同类型的kobject在它们对应的sysfsa目录下都拥有相同的默认文件集合。kobj_type字段中包含了default_attrs字段,它是一个attribute结构体数组指针。这些attribute属性负责将内核数据映射成sysfs中的文件:
struct attribute {
const char *name;
umode_t mode;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
bool ignore_lockdep:1;
struct lock_class_key *key;
struct lock_class_key skey;
#endif
};
name字段提供了属性的名称,最终将出现在sysfs中形成文件名。
mode字段描述了该属性在sysfs中的权限。sysfs中所有文件和目录的uid和gid标志均为零。
default_attrs字段列出了默认的属性,而sysfs_ops字段则描述了该如何使用它们。sysfs_ops字段指向了一个定义于文件 <linux/sysfs.h> 的同名结构体:
struct sysfs_ops {
ssize_t (*show)(struct kobject *, struct attribute *, char *);
ssize_t (*store)(struct kobject *, struct attribute *, const char *, size_t);
};
当从用户空间读取sysfs项时调用show() 方法。它会拷贝提供的属性值到指定的buffer(参数三)缓存区中,缓冲区大小为PAGE_SIZE(4096)。函数执行成功返回实际写入的字节数;失败返回负的错误码。
用户空间写入sysfs时调用store() 方法。它会从buffer中读取size大小的字节,并将其放入指定的属性结构体变量中。缓冲区的大小总是PAGE_SIZE或更小。如果执行成功返回从buffer中读取的字节数;失败返回负的错误码。
4.2.2、创建新属性
有时候一些特殊的kobject实例需要有一些额外的自己的属性。内核提供了在默认属性上添加新属性的接口:
int sysfs_create_files(struct kobject *kobj, const struct attribute **ptr);
ptr传入新的属性到kobject对象中。在调用前,给定的属性将被赋值。如果成功,函数返回零,失败返回负的错误码。
除了添加文件之外,还有可能需要创建符号链接:
int __must_check sysfs_create_link(struct kobject *kobj, struct kobject *target,
const char *name);
创建的符号连接名由name指定,链接kobj对应的目录映射到target指定的目录。成功返回零,失败返回负的错误码。
4.2.3、删除新属性
删除一个属性使用sysfs_remove_files() 函数:
void sysfs_remove_files(struct kobject *kobj, const struct attribute **ptr);
一旦调用返回,给定kobject中和ptr相同的属性就被删除。
符号链接通过sysfs_remove_link() 函数删除:
void sysfs_remove_link(struct kobject *kobj, const char *name);
创建和删除file属性的函数定义在fs/sysfs/file.c中;创建和删除link属性的函数定义在fs/sysfs/symlinkc中。
4、sysfs约定
可以利用在设备映射的sysfs目录中添加一个sysfs属性,代替在设备节点上实现一新的ioctl()。采用这种方法避免了在调用ioctl()时使用类型不正确的参数和弄乱/proc目录结构。
但是为了保持sysfs的干净和直观的特点,开发人员必须遵循以下约定:
- 1、sysfs属性应该保证每个文件只导出一个数据,这个数据应该是文本形式而且为简单的C类型。如果需要多个数据,应该清晰的表达出其中的意思(可以在多个数据之间加空格后,整体当作一个数据导出);
- 2、要保证sysfs有一个清晰的层次组织数据。父子关系要正确、kobject相关属性要正确;
- 3、sysfs提供内核到用户空间的服务,用户程序可以检测和获得其存在性、位置、取值以及sysfs目录和文件行为。任何情况下都不应该改变现有的文件。此外,更改给定的属性而不修改其名称和位置是不正确的。
5、内核事件层
每个事件都被赋予了一个动词或动词字符串表示信号。该字符串会以“被修改过”或“未挂载”等词语来描述事件。
每个事件都有一个可选的负载(playload)。内核事件层使用sysfs属性代表负载。
内核事件使用netlink从内核空间传递到用户空间。netlink是一个用于传输网络信息的多点传送套接字。使用时只需要用户空间实现一个系统后台服务用于监听套接字,处理任何读到的消息,并将事件传送到系统栈中。这样的方式会把事件融入D-BUS系统中,D-BUS系统实现了一套系统范围的消息总线,可以帮助内核如同系统中其他组件一样地发出信号。
内核代码中向用户空间发送信号使用kobject_uevent() 函数,定义在lib/kobject_uevent.c中:
int kobject_uevent(struct kobject *kobj, enum kobject_action action);
第一个参数指定发送该信号的kobject对象。实际的内核事件将包含此kobject映射到sysfs的路径。
第二个参数指定了描述该信号的“动作”或“动词”。为了防止出错,不会直接给函数传递字符串。而是使用一个kobject_action的枚举类型,定义在文件 <linux/kobject_uevent.h:> 中:
enum kobject_action {
KOBJ_ADD,
KOBJ_REMOVE,
KOBJ_CHANGE,
KOBJ_MOVE,
KOBJ_ONLINE,
KOBJ_OFFLINE,
KOBJ_MAX
};
这些值都被映射为字符串"mount"、“unmount”、“add”、“remove”、"change"等。当不够用时可以添加新的动作。
6、总结
主要是kobject和sysfs,个人觉得,实际使用时可能用sysfs创建一个文件来代替ioctl的方式用得比较多。