一、基本概念
1. 前言
- 在蜗蜗的“Linux内核的整体架构”一文中有提到,由于Linux支持世界上几乎所有的、不同功能的硬件设备(这是Linux的优点),导致Linux内核中有一半的代码是设备驱动,而且随着硬件的快速迭代,设备驱动的代码量也在快速增长。它导致Linux内核非常臃肿、杂乱、不易维护。为了降低设备多样性带来的Linux驱动开发的复杂度,以及设备热拔插处理、电源管理等,Linux内核提出了设备模型(也称作driver Model)的概念。
- 设备模型将硬件设备归纳、分类,然后抽象出一套标准的数据结构和接口。驱动的开发,就简化为对内核基本数据结构的填充和实现。本文将会从设备模型的基本概念开始,通过分析内核相应的代码,逐步解析Linux设备模型的实现及使用方法。
- 内核2.4之前没有统一的设备驱动模型,内核2.4到2.6之间使用devfs,设备文件挂载在/dev目录。但它需要在驱动代码中调用
devfs_register
来对设备文件进行命名,用户空间不可改变。内核2.6版本后使用sysfs,设备文件挂载在/sys目录。 - sysfs是一个虚拟文件系统,类似proc文件系统sysfs下一个目录对应一个kobject对象sysfs下每一个目录所对应的inode节点会记录基本驱动对象kobject,从而将系统中的设备组成层次结构用户可以通过修改这些目录下的不同文件来配置驱动对象kobject的不同属性。
- 将设备分类、分层统一进行管理配合守护进程udev或者mdev,系统可以动态创建设备文件,命名规则可由用户自定义。
2. Linux设备模型的基本概念
2.1 Bus, class, device和device_driver的概念
下图是嵌入式系统常见的硬件拓扑的一个示例:
- 硬件拓扑描述Linux设备模型中四个重要概念中的三个:Bus,class和device(第四个为device driver,后面会说):
- Bus(总线):Linux认为(可以参考include/linux/device.h中struct bus_type的注释),总线是CPU和一个或多个设备之间信息交互的通道。而为了方便设备模型的抽象,所有的设备都应连接到总线上(无论是CPU内部总线、虚拟的总线还是platform Bus)。
- class(分类):在Linux设备模型中,class的概念非常类似面向对象程序设计中的class(类),它主要是集合具有相似功能或属性的设备,这样就可以抽象出一套可以在多个设备之间共用的数据结构和接口函数。因而从属于相同class的设备的驱动程序,就不再需要重复定义这些公共资源,直接从class中继承即可。
- device(设备):抽象系统中所有的硬件设备,描述它的名字、属性、从属的Bus、从属的class等信息。
- device driver(驱动):Linux设备模型用driver抽象硬件设备的驱动程序,它包含设备初始化、电源管理相关的接口实现。而Linux内核中的驱动开发,基本都围绕该抽象进行(实现所规定的接口函数)。
什么是platform Bus?
在计算机中有这样一类设备,它们通过各自的设备控制器,直接和CPU连接,CPU可以通过常规的寻址操作访问它们(或者说访问它们的控制器)。这种连接方式,并不属于传统意义上的总线连接。但设备模型应该具备普适性,因此Linux就虚构了一条platform Bus,供这些设备挂靠。
2.2 设备模型的核心思想
- Linux设备模型的核心思想是通过xxx手段,实现xxx目的:
- 用
struct device
和struct device_driver
两个数据结构,分别从“有什么用”和“怎么用”两个角度描述硬件设备。这样就统一了编写设备驱动的格式,使驱动开发从论述题变为填空体,从而简化了设备驱动的开发。 - 同样使用这两个数据结构,实现硬件设备的即插即用(热拔插)。
- 因为在Linux内核中,只要任何device和device driver具有相同的名字,内核就会执行
device_driver
结构中的初始化函数probe
,该函数会初始化设备,使其为可用状态。 - 对大多数热拔插设备而言,它们的device driver一直存在内核中。当设备没有插入时,其device结构不存在,因而其driver也就不执行初始化操作。当设备插入时,内核会创建一个device结构(名称和driver相同),此时就会触发driver的执行。这就是即插即用的概念。
- 通过"Bus–>device”类型的树状结构(见2.1章节的图例)解决设备之间的依赖,而这种依赖在开关机、电源管理等过程中尤为重要。
- 试想,一个设备挂载在一条总线上,要启动这个设备,必须先启动它所挂载的总线。很显然,如果系统中设备非常多、依赖关系非常复杂的时候,无论是内核还是驱动的开发人员,都无力维护这种关系。
- 设备模型中的这种树状结构,可以自动处理这种依赖关系。启动某一个设备前,内核会检查该设备是否依赖其它设备或者总线,如果依赖,则检查所依赖的对象是否已经启动,如果没有,则会先启动它们,直到启动该设备的条件具备为止。而驱动开发人员需要做的,就是在编写设备驱动时,告知内核该设备的依赖关系即可。
- 使用class结构,在设备模型中引入面向对象的概念,这样可以最大限度地抽象共性,减少驱动开发过程中的重复劳动,降低工作量。
二、kobject、kset、kobj_type
1. 前言
- kobject是Linux设备模型的基本单元,也是设备模型中最难理解的一部分(可参考内核文档Documentation/kobject.txt的表述)。因此有必要先把它分析清楚。
2. 基本概念
- 由前文可知,Linux设备模型的核心是使用Bus、class、device、driver四个核心数据结构,将大量不同功能的硬件设备及其驱动,以树状结构的形式,进行归纳、抽象,从而方便kernel的统一管理。而硬件设备的数量、种类是非常多的,这就决定了kernel中将会有大量的有关设备模型的数据结构。这些数据结构一定有一些共同的功能,需要抽象出来统一实现,否则就会不可避免的产生冗余代码。这就是kobject诞生的背景。
- 目前为止,kobject主要提供如下功能:
- 通过parent指针,可以将所有kobject以层次结构的形式组合起来。
- 使用一个引用计数(reference count),来记录kobject被引用的次数,并在引用次数变为0时把它释放(这是kobject诞生时的唯一功能)。
- 和sysfs虚拟文件系统配合,将每一个kobject及其特性,以文件的形式显示到用户空间。
注1:在Linux中,kobject几乎不会单独存在。它的主要功能,就是内嵌在一个大型的数据结构中,为这个数据结构提供一些底层的功能实现。
注2:Linux driver开发者,很少会直接使用kobject以及它提供的接口,而是使用构建在kobject之上的设备模型接口。
3. 代码解析
3.1 在Linux kernel 中的位置
在kernel源代码中,kobject由如下两个文件实现:
- include/linux/kobject.h
- lib/kobject.c
其中kobject.h为kobject的头文件,包含所有的数据结构定义和接口声明。kobject.c为核心功能的实现。
3.2 主要数据结构及其逻辑关系
- 在描述数据结构之前,有必要说明一下
kobject
, Kset
和ktype
这三个概念:
- kobject是基本数据类型,每个kobject都会在"/sys/“文件系统中以目录的形式出现。
- ktype代表kobject(严格地讲,是包含了kobject的数据结构)的属性操作集合。由于通用性,多个kobject可能共用同一个属性操作集,因此把ktype独立出来了。
- Kset是一个特殊的kobject(因此它也会在"/sys/“文件系统中以目录的形式出现),它用来集合相似的kobject(这些kobject可以是相同属性的,也可以不同属性的)。
- 他们的逻辑关系如下图所示:
- kset可批量管理kobject
- kset继承自kobject,且将一组kobject串连成一个链表进行统一管理
- kobject既可以通过parent指针找到上层kobject,也可以通过kset指针找到其上层kobject。但上层kobject对象无法遍历到下层,所以较少使用。
3.2.1 kobject的原型
- name:该kobject的名称,同时也是sysfs中的目录名称。由于kobject添加到kernel时,需要根据名字注册到sysfs中,之后就不能再直接修改该字段。如果需要修改kobject的名字,需要调用
kobject_rename
接口,该接口会主动处理sysfs的相关事宜。 - entry:用于将kobject加入到Kset中的
list_head
。 - parent:指向parent kobject,以此形成层次结构(在sysfs就表现为目录结构)。
- kset:该kobject属于的Kset(可以为NULL)。如果没有指定parent,则会把Kset作为parent(别忘了Kset是一个特殊的kobject)。
- ktype:该kobject属于的kobj_type。每个kobject必须有一个ktype,否则kernel会提示错误。
- sd:该kobject在sysfs中的表示。
- kref:
struct kref
类型(在include/linux/kref.h中定义)的变量,是一个可用于原子操作的引用计数。
- state_initialized:指示该kobject是否已经初始化,以在kobject的Init,Put,Add等操作时进行异常校验。
- state_in_sysfs:指示该kobject是否已在sysfs中呈现,以便在自动注销时从sysfs中移除。
- state_add_uevent_sent:记录是否已经向用户空间发送ADD uevent。
- state_remove_uevent_sent:记录是否已经向用户空间发送REMOVE uevent,
- uevent_suppress:如果该字段为1,则表示忽略所有上报的uevent事件。
注意:uevent提供了“用户空间通知”的功能实现,通过该功能,当内核中有kobject的增删改等动作时,会通知用户空间。
3.2.2 kset的原型
- list:指向该kset下所有的kobject的链表。
- list_lock:避免操作链表时产生竞态的自旋锁。
- kobj:该kset自己的kobject(kset是一个特殊的kobject,也会在sysfs中以目录的形式体现)。
- uevent_ops:该kset的uevent操作函数集,当kset中的某些kobject对象状态发生变化需要通知用户空间时,调用其中对应的函数来完成。当任何kobject需要上报uevent时,都要调用它所从属的kset的uevent_ops,添加环境变量,或者过滤event(kset可以决定哪些event可以上报)。因此,如果一个kobject不属于任何kset时,是不允许发送uevent的。
3.2.3 ktype的原型
- release:通过该回调函数,可以将包含该类型kobject的数据结构的内存空间释放掉。
- sysfs_ops:该种类型的kobject的sysfs文件系统接口(读属性接口show及写属性接口store)。
- default_attrs:该种类型的kobject的atrribute列表(所谓attribute,就是sysfs文件系统中的一个文件)。将会在kobject添加到内核时,一并注册到sysfs中。
- child_ns_type/namespace:和文件系统(sysfs)的命名空间有关,这里不再详细说明。
3.2.4 整个kobject机制的理解
- kobject的核心功能是:保持一个引用计数,当该计数减为0时,自动释放(由本文所讲的kobject模块负责) kobject所占用的meomry空间。这就决定了kobject必须是动态分配的,因为只有这样才能动态释放。
- kobject大多数的使用场景,是内嵌在大型的数据结构中(如Kset、device_driver等),因此这些大型的数据结构,也必须是动态分配、动态释放的。
- 那么释放的时机是什么呢?是内嵌的kobject释放时。但是kobject的释放是由kobject模块自动完成的(在引用计数为0时),那么怎么一并释放包含自己的大型数据结构呢?
- 这时ktype就派上用场了。因为ktype中的release回调函数负责释放kobject(甚至是包含kobject的数据结构)的内存空间
- 那么ktype及其内部函数,是由谁实现呢?是由上层数据结构所在的模块!因为只有它才清楚kobject嵌在哪个数据结构中,并通过kobject指针以及自身的数据结构类型,利用函数宏containerof找到需要释放的上层数据结构的指针,然后释放它。
- 所以,每一个内嵌kobject的数据结构,例如kset、device、device_driver等等,都要实现一个ktype,并定义其中的回调函数。同理,sysfs相关的操作也一样,必须经过ktype的中转,因为sysfs看到的是kobject,而真正的文件操作的主体,是内嵌kobject的上层数据结构!
- kobject是面向对象的思想在Linux kernel中的极致体现,但C语言的优势却不在这里,所以Linux kernel需要用比较巧妙的手段去实现它。
3.3 接口功能分析
3.3.1 kobject使用流程
- kobject大多数情况下(有一种例外,下面会讲)会嵌在其它数据结构中使用,其使用流程如下:
- 定义一个struct kset类型的指针,并在初始化时为它分配空间,添加到内核中;
- 根据实际情况,定义内嵌有kobject的自己所需的数据结构原型;
- 定义一个适合自己的ktype,并实现其中回调函数release;
- 在需要使用到包含kobject的数据结构时,动态分配该数据结构,并分配kobject空间,添加到内核中;
- 每一次引用数据结构时,调用
kobject_get
接口增加引用计数;引用结束时,调用kobject_put
接口,减少引用计数; - 当引用计数为0时,kobject模块会自动调用ktype所提供的release接口,释放上层数据结构以及kobject的内存空间。
- 上面有提过有一种例外就是:开发者只需要在sysfs中创建一个目录,而不需要其它的kset、ktype的操作。这时可以直接调用
kobject_create_and_add
接口,分配一个kobject结构并把它添加到内核中。
3.3.2 kobject的分配和释放
- 前面讲过,kobject必须动态分配,而不能静态定义或者位于堆栈之上,它的分配方法有两种:
- 通过kmalloc自行分配(一般是跟随上层数据结构分配),并在初始化后添加到kernel。这种方法涉及如下接口:
-
kobject_init
:初始化通过kmalloc等内存分配函数获得的struct kobject指针。主要执行逻辑为:
- 确认kobj和ktype不为空;
- 如果该指针已经初始化过(判断kobj->state_initialized),打印错误提示及堆栈信息(但不是致命错误,所以还可以继续);
- 初始化kobj内部的参数,包括引用计数kref、list_head、各种标志等;
- 将输入形参ktype赋予kobj->ktype
-
kobject_add
:如果koibject需要添加到sysfs中,则必须要调用kobject_add函数。它将初始化完成的kobject添加到kernel中,参数包括需要添加的kobject、该kobject的parent(用于形成层次结构,可以为空)、用于提供kobject name的格式化字符串。主要执行逻辑为:
- 确认kobj不为空,确认kobj已经初始化,否则错误退出;
- 调用内部接口
kobject_add_varg
,完成添加操作,具体调用逻辑如下:
- populate_dir中逐个处理内核对象所属对象类型的默认属性,对每个属性,调用sysfs_create_file函数在内核对象的目录下创建以属性名为名字的文件。kobject_add中只会为默认属性自动创建文件。
- kobject_add定义
-
kobject_init_and_add
,是上面两个接口的组合,不再说明。
内部接口=====
kobject_add_varg:解析格式化字符串,将结果赋予kobj->name,之后调用
kobject_add_internal
接口,完成真正的添加操作。kobject_add_internal:将kobject添加到kernel。主要执行逻辑为:
- 校验kobj以及kobj->name的合法性,若不合法打印错误信息并退出;
- 调用kobject_get增加该kobject的parent的引用计数(如果存在parent的话);
- 如果存在kset,则调用
kobj_kset_join
接口将本kobject加入到此kset的kobject链表中。同时,如果该kobject没有parent,却存在kset,则将它的parent设为kset(kset是一个特殊的kobject),并增加kset的引用计数;- 通过
create_dir
接口,调用sysfs的相关接口,在sysfs下创建该kobject对应的目录;- 如果创建失败,则执行后续的回滚操作,否则将kobj->state_in_sysfs置为1;
这种方式分配的kobject,会在引用计数变为0时,由kobject_put调用其ktype的release接口,释放内存空间,具体可参考后面有关kobject_put的讲解。
- 使用kobject_create创建
- kobject模块可以使用kobject_create自行分配空间,并内置了一个ktype(dynamic_kobj_ktype),用于在计数为0时释放空间。代码如下:
- kobject_create:该接口为kobj分配内存空间,并以
dynamic_kobj_ktype
为参数,调用kobject_init接口,完成后续的初始化操作。
- kobject_create_and_add:是kobject_create和kobject_add的组合,不再说明。
- dynamic_kobj_release:直接调用kfree释放kobj的空间。
3.3.3 kobject引用计数的加减
- 通过
kobject_get
和kobject_put
可以修改kobject的引用计数kref,并在kref为0时,调用对应ktype的release接口,释放占用空间。
- kobject_get:调用
kref_get
,增加引用计数。 - kobject_put:以内部接口kobject_release为参数,调用
kref_put
。kref模块会在引用计数为零时,调用kobject_release。
=内部接口=====
kobject_release:通过kref结构,获取kobject指针,并调用kobject_cleanup接口继续。
kobject_cleanup:负责释放kobject占用的空间,主要执行逻辑如下:
- 检查该kobject是否有ktype,如果没有,打印警告信息;
- 如果该kobject向用户空间发送了ADD uevent但没有发送REMOVE uevent,补发REMOVE uevent;
- 如果该kobject有在sysfs文件系统注册,调用kobject_del接口,删除它在sysfs中的注册;
- 调用该kobject的ktype的release接口,释放内存空间;
- 释放该kobject的name所占用的内存空间;
3.3.4 Kset的初始化、注册
- Kset是一个特殊的kobject,因此其初始化、注册等操作也会调用kobject的相关接口,除此之外,会有它特有的部分。另外,和kobject一样,kset的内存分配,可以由上层软件通过kmalloc自行分配,也可以由kobject模块负责分配,具体如下。
- kset_init:该接口用于初始化已分配的kset,主要包括调用
kobject_init_internal
初始化其kobject,然后初始化kset的链表。需要注意的时,如果使用此接口,上层软件必须提供该kset中的kobject的ktype。 - kset_register:先调用
kset_init
,然后调用kobject_add_internal
将其kobject添加到kernel。 - kset_unregister:直接调用
kobject_put
释放其kobject。当其kobject的引用计数为0时,即调用ktype的release接口释放kset占用的空间。 - kset_create_and_add:会调用内部接口
kset_create
动态创建一个kset,并调用kset_register
将其注册到kernel。
内部接口kset_create:该接口使用kzalloc分配一个kset空间,并定义一个kset_ktype类型的ktype,用于释放所有由它分配的kset空间。