LDD-The Linux Device Model
Linux Device Model是一个复杂的数据结构,将系统中的电源管理、设备、和用户空间的交互联结在一起。
Kobjects, Ksets, and Subsystems
struct kobject是设备模型的基础数据结构,包含以下功能:
- 对象的引用计数
- sysfs中的每一个文件都由一个kobject创建
- 设备模型大而复杂,kobject构成其的基本组件
- kobject负责产生硬件相关的事件通知用户空间
Kobject Basics
struct kobject定义在linux/kobject.h,kobject很少单独使用,而是作为其他数据结构的内嵌成员,就像面相对象的编程语言的基类。为了能够根据某一对象的kobject成员变量来获取该对象的指针,内核实现了container_of宏。例如代码struct cdev *device = container_of(kp, struct cdev, kobj);
。
kobject对象的初始化包括三步:
- 创建一个kobject对象,通过memset函数将其清零
- 调用
void kobject_init(struct kobject *kobj)
,将obj的引用计数设为1 - 通过
int kobject_set_name(struct kobject *kobj, const char *format, ...);
设置kobject的名称
kobject的引用计数用来管理对象的生命周期,操作的函数如下:
struct kobject *kobject_get(struct kobject *kobj);
void kobject_put(struct kobject *kobj);
在修改引用计数时,需要注意和kobject相关的其他对象是否还在使用此kobject。
kobject的引用计数为零时,需要将其释放。但是kobject的创建代码很难知道什么时候引用计数到达零,尤其是sysfs的存在——用户程序可能长时间打开一个文件。因此,当引用计数为零时,通过kobject的release函数将其释放。
release函数并不存在kobject内,而是保存在包含kobject的数据结构内的struct kobj_type内,每个kobject都必须包含一个kobj_type成员,可能直接包含在kobject结构体内(ktype),也可能kobject是一个kset的成员,kobj_type由kset提供。宏struct kobj_type *get_ktype(struct kobject *kobj);
可以获取kobject的kobj_type指针。
Kobject Hierarchies, Ksets, and Subsystems
kobject结构相互连接,构成一个分层的结构,和其描述的系统结构相对应。有两种独立的连接机制:parent指针和ksets。
parent指针位于struct kobject,指向上一层的kobject,主要用于sysfs的分层结构中。
kset是不同对象内相同类型的集合——kobject的顶层容器类。每个kset都有sysfs文件的表示;kobject并不一定有sysfs(和前文所述不符),但是kset的kobject成员一定有sysfs文件。struct kset定义如下(摘自Linux-3.16.6):
struct kset {
struct list_head list;
spinlock_t list_lock;
struct kobject kobj;
const struct kset_uevent_ops *uevent_ops;
};
可以看到,kset中包含kobject对象,用来标识该kset对象,而且可以通过kobject将kset对象连接起来。
kset,kobject,kobj_type之间的关系有点复杂,以代码为例说明(Linux-3.16.6/drivers/base/bus.c):
int __init buses_init(void)
{
bus_kset = kset_create_and_add("bus", &bus_uevent_ops, NULL);
if (!bus_kset)
return -ENOMEM;
system_kset = kset_create_and_add("system", NULL, &devices_kset->kobj);
if (!system_kset)
return -ENOMEM;
return 0;
}
buses_init函数先创建一个名为bus_kset的kset对象,然后根据set_create_and_add的定义可知,
struct kset *kset_create_and_add(const char *name,
const struct kset_uevent_ops *uevent_ops,
struct kobject *parent_kobj)
{
struct kset *kset;
int error;
kset = kset_create(name, uevent_ops, parent_kobj);
if (!kset)
return NULL;
error = kset_register(kset);
if (error) {
kfree(kset);
return NULL;
}
return kset;
}
bus_kset名为bus,指向父kobject的指针为空,即bus位于最上层;而且kset_create会将创建的kset对象的kobject域的ktype类型设为kset_ktype、kset设为空,表明该kobject不属于任何kset。同样地,buses_init创建了system_kset对象,名为system,父kobject为device_kset的kobject域。
在bus.c中查找system_kset的引用,发现这个system文件夹位于系统中/sys/devices/system。查找device_kset的引用,发现定义在文件linux-3.16.6/drivers/base/core.c,代码注释说明对应文件/sys/devices。查看系统中的目录结构,和代码所示一致。
subsystem通常出现在sysfs的最上一层,例如block_subsys(/sys/block),devices_subsys(/sys/devices)。驱动程序一般不需要创建新的subsys。subsys对应的数据结构定义如下:
struct subsystem {
struct kset kset;
struct rw_semaphore rwsem;
};
每个kset对象都必须对应一个subsystem对象,然而上述代码我并没有在代码目录中找到,声明subsystem的代码decl_subsys(name, struct kobj_type *type, struct kset_hotplug_ops *hotplug_ops);
也没有。书中所述subsystem的相关函数也没找到。
Low-Level Sysfs Operations
由上文可知,kobjects是sysfs虚拟文件系统的实现机制,操作sysfs的代码需要包含头文件linux/sysfs.h。sysfs中的文件根据以下规则创建:
- kobject_add会在sysfs中创建一个目录,通常包含若干属性
- kobject的name域的值就是目录的名称
- 目录的层次和内核中的数据结构的层次一致
Default Attributes
kobject在创建时需要指定一些属性值,通过kobj_type指明
struct kobj_type {
void (*release)(struct kobject *);
struct sysfs_ops *sysfs_ops;
struct attribute **default_attrs;
};
default_attrs域指明了属性值,sysfs_ops域提供了实现这些属性的方法。属性定义如下:
struct attribute {
char *name;
struct module *owner;
mode_t mode;
};
name是属性名,owner是实现这个属性的模块,mode是要应用到此属性值的保护位。mode的宏定义在linux/stat.h中,default_attrs的最后一项必须填充为0,作为结束标识。
属性的具体实现方式sysfs_ops定义如下:
struct sysfs_ops {
ssize_t (*show)(struct kobject *kobj, struct attribute *attr,
char *buffer);
ssize_t (*store)(struct kobject *kobj, struct attribute *attr,
const char *buffer, size_t size);
};
用户空间读取属性值时,show函数将传入的attr的值解码,放入PAGE_SIZE大小的buffer中,并返回数据的真实长度。sysfs的约定是返回的属性值应该是单独的可读的值。对于一个kobject对象,所有的属性值都通过相同的show函数获取。
store函数和show函数类似,将保存在buffer中的长度为size的数据解码,保存在数据结构中,并返回实际解码的数据的长度。只有对应的属性值可写时,才可以调用store函数。
Nondefault Attributes
要添加或者删除非默认的属性值,可以通过下列函数:
int sysfs_create_file(struct kobject *kobj, struct attribute *attr);
int sysfs_remove_file(struct kobject *kobj, struct attribute *attr);
新增的属性值会以文件的形式出现在sysfs目录下,调用remove函数时也会将相应的文件删除。
上述的属性值指的是sysfs目录下的文件,比如/sys/devices/msr/power下的各个文件,都对应一个设备的属性值。
Binary Attributes
当用户空间和设备之间的数据传输需要保证其不会被改变,比如某些配置寄存器的值,或是通过属性值传输固件的代码,就需要通过二进制的属性值来实现。
二进制属性值定义如下:
struct bin_attribute {
struct attribute attr;
size_t size;
ssize_t (*read)(struct kobject *kobj, char *buffer,
loff_t pos, size_t size);
ssize_t (*write)(struct kobject *kobj, char *buffer,
loff_t pos, size_t size);
};
attr域用来标识该bin_attribute对象,包括名称、所有者、权限;size是属性值的最大长度(0表示没有上限);read和write函数与普通的字符设备的读写函数工作机制相同。二进制属性值不能像默认属性值一样创建,必须通过函数调用显式创建和删除:
int sysfs_create_bin_file(struct kobject *kobj,
struct bin_attribute *attr);
int sysfs_remove_bint_file(struct kobject *kobj,
struct bin_attribute *attr);
Symbolic Links
sysfs中的符号链接用来显示目录之间关系,例如/sys/devices下的文件代表系统中的所有设备,/sys/bus代表设备驱动,/sys/bus/pci/devices是指向/sys/devices的符号链接。
在sysfs中创建符号链接很简单:int sysfs_create_link(struct kobject *kobj, struct kobject *target, char *name);
。name是软链接的名称,kobj是创建链接的目录,即name会作为一个新的属性值(文件)出现在kboj目录下。需要注意的是,即使软链接的目标已经删除,软链接依然存在。
要删除软链接,可以通过函数void sysfs_remove_link(struct kobject *kobj, char *name);
。
Hotplug Event Generation
热插拔事件由内核向用户发送,当kobject被创建或者被删除(执行kobject_add,或者kobject_del)时,通过执行/sbin/hotplug产生。
热插拔事件通过系列函数进行控制:
struct kset_hotplug_ops {
int (*filter)(struct kset *kset, struct kobject *kobj);
char *(*name)(struct kset *kset, struct kobject *kobj);
int (*hotplug)(struct kset *kset, struct kobject *kobj,
char **envp, int num_envp, char *buffer, int buffer_size);
};
指向struct kset_hotplug_ops的指针保存在kset中,如果一个kobject没有包含它的kset,内核会根据parent指针遍历kobject,直到找到一个kset。
内核想要为一个kobejct对象生成一个热插拔事件时,会调用filter函数判断是否确实需要产生事件;如果函数返回0,不会产生事件。
调用用户空间的热插拔程序时,要传递的子系统的名称是该函数的唯一参数,name函数用来获取这个名称参数。
热插拔脚本所需的其他信息通过环境(环境变量?)传递,hotplug可以在调用脚本前添加所需的环境变量。envp以NAME=value的形式保存额外的环境变量,个数为num_envp。变量需要编码在buffer中,长度为buffer_size。如果向envp中添加了变量,要将最后一项设为NULL。
Buses, Devices, and Drivers
Buses
总线是处理器和设备间的通道,总线之间可以相互连接,设备模型(device model)代表总线之间的连接及这些总线控制的设备。
总线用bus_type结构表示,定义在linux/device.h中:
struct bus_type {
char *name;
struct subsystem subsys;
struct kset drivers;
struct kset devices;
int (*match)(struct device *dev, struct device_driver *drv);
struct device *(*add)(struct device * parent, char * bus_id);
int (*hotplug) (struct device *dev, char **envp,
int num_envp, char *buffer, int buffer_size);
/* Some fields omitted */
};
name是总线的名称,例如pci;每个总线都是一个子系统subsystem;包含两个kset对象,分别代表该总线已有的设备驱动和总线上的所有设备;还有一些应用于该总线的方法。
书中以lddbus为例,介绍了总线的注册方法。首先需要初始化一个bus_type结构体:
struct bus_type ldd_bus_type = {
.name = "ldd",
.match = ldd_match,
.hotplug = ldd_hotplug,
};
设备模型的核心已经将结构体内的大部分成员初始化完成,我们只需要提供名称和相关的函数。要将总线注册到系统中:ret = bus_register(&ldd_bus_type);
,需要根据返回值判断操作是否成功;成功会在/sys/bus下创建新的目录。移除的函数为bus_unregister(struct bus_type *bus);
。
bus_type结构体中包含若干函数,使得总线的代码能够像设备核心和驱动程序之间的媒介,2.6.10内核中这些函数定义如下:
int (*match)(struct device *device, struct device_driver *driver);
总线上有新的设备或者驱动添加时,会调用该函数判断给定的设备能否由给定的驱动处理,如果可以返回非零值。
int (*hotplug)(struct device *device, char **envp, int num_envp, char *buffer, int buffer_size);
用来在产生热插拔事件前添加环境变量。
开发总线程序时需要对总线上的所有设备或者驱动进行遍历,内核提供了辅助函数int bus_for_each_dev(struct bus_type *bus, struct device *start, void *data, int (*fn)(struct device *, void *));
可以遍历总线上的所有设备,并将遍历到的设备和传入的data传递给fn。如果start为NULL,遍历从总线的第一个设备开始;否则从start后的第一个设备开始。如果fn返回非零值,遍历停止,并将这个返回值返回。
函数int bus_for_each_drv(struct bus_type *bus, struct device *start, void *data, int (*fn)*(struct device_driver *, void *));
类似,只是对总线上的驱动进行遍历。
两个函数在执行时都会持有总线子系统的读/写信号量,因此同时进行两个操作会导致系统死锁,修改总线的操作也会导致死锁。
几乎Linux设备模型的每一层都有操作属性值的结构,总线这一层也一样,通过linux/device.h中定义的bus_attribute定义:
struct bus_attribute {
struct attribute attr;
ssize_t (*show)(struct bus_type *bus, char *buf);
ssize_t (*store)(struct bus_type *bus, const char *buf,
size_t count);
};
宏BUS_ATTR(name, mode, show, store);
能够在编译时产生bus_attribute对象,属性名name前会添加bus_attr_前缀。
Devices
Linux系统中的每个设备都有一个struct device来描述,
struct device {
struct device *parent;
struct kobject kobj;
char bus_id[BUS_ID_SIZE];
struct bus_type *bus;
struct device_driver *driver;
void *driver_data;
void (*release)(struct device *dev);
/* Several fields ommited */
};
parent域指明设备连接的父设备,通常是总线或者主机控制器,如果parent域为NULL,说明此设备位于最顶层。
kobject域是代表该设备的kobject,并通过该对象将设备添加到系统的层次结构中,device->kobj->parent
总是等同于&device->parent->kobj
。
bus_id域在总线上唯一地标识该设备,例如PCI设备通过域、总线、设备、和功能号码组成PCI ID。
bus域指明设备连接的总线类型。
driver域是管理该设备的驱动程序。
driver_data是驱动程序可能会用到的数据。
release函数在删除该设备的最后一个引用后会通过嵌入其中的kobject的release函数调用,所有注册到core的device结构都必须有release函数。
设备注册和注销通过以下函数实现:
int device_register(struct device *dev);
void device_unregister(struct device *dev);
设备注册完成后就能在/sys/devices/下看到新设备,如果新设备是总线类型,之后添加到该总线的设备也会出现在/sys/device/new_bus/下。
sysfs下的设备项也可以有各种属性:
struct device_attribute {
struct attribute_attr;
ssize_t (*show)(struct device *dev, char *buf);
ssize_t (*store)(struct device *dev, const char *buf, size_t count);
};
参数对象可以通过宏DEVICE_ATTR(name, mode, show, store)
创建,创建的对象会加上dev_attr_
前缀。
属性文件的创建和删除通过以下函数完成:
int device_create_file(struct device *device, struct device_attribute *entry);
void device_remove_file(struct device *device, struct device_attribute *attr);
通常情况下,系统中的每个子系统还会定义自己的设备类型来描述设备,例如struct pci_dev和struct usb_device,其中又包含struct device类型。
这样的话,每个设备可以有自己的名字,和struct device中的bus_id区分开。
Device Drivers
设备模型会追踪系统已知的所有驱动,以便driver core添加新的设备时能够匹配驱动,驱动程序可以抽象出不依赖于设备的信息。驱动的结构体定义如下:
struct device_driver {
char *name;
struct bus_type *bus;
struct kobject kobj;
struct list_head devices;
int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
/* ... */
};
name是驱动名,出现在sysfs中,bus是总线类型,devices是绑定到驱动的所有设备,probe用来查询制定的设备是否存在,以及该设备能否由此驱动处理,remove函数在设备从系统中移除时调用,shutdown函数在关机时调用。
设备注册和注销的函数定义如下:
int driver_register(struct device_driver *drv);
void driver_unregister(struct device_driver *drv);
驱动的属性值定义如下:
struct driver_attribtute {
struct attribute attr;
ssize_t (*show)(struct device_driver *drv, char *buf);
ssize_t (*store)(struct device_driver *drv, const char *buf, size_t count);
};
也可通过宏DRIVER_ATTR(name, mode, show, store);
声明。
对应的属性文件操作函数如下:
int driver_create_file(struct device_driver *drv, struct driver_attribute *attr);
void driver_remove_file(struct device_driver *drv, struct driver_attribute *attr);
和struct device一样,struct device_driver通常也嵌入到子系统的驱动结构体中,例如struct pci_driver,这样每个驱动对象都可以有自己的名称。
Classes
类是更高层的设备抽象,无关其底层实现细节,驱动可能会分别SCSI磁盘或者ATA磁盘,但从类这一层来看,他们都是磁盘。
系统中的所有类都会出现在/sys/class目录,比如/sys/class/net目录下包含所有的网络设备,/sys/class/input目录下包含所有的输入设备,但是块设备出现在/sys/block目录下。
通常情况下,设备的驱动程序不需要显式的在/sys/class下创建设备的目录,相应的文件目录会自动创建。
The class_simple Interface
class_simple接口能够方便地向系统添加新的类。首先需要创建类struct class_simple *class_simple_create(struct module *owner, char *name);
,name是类名;void class_simple_destroy(struct class_simple *cs);
可以销毁创建的类。
类创建完成后,通过struct class_device *class_simple_device_add(struct class_simple *cs, dev_t devnum, struct device *device, const char *fmt, ...);
添加设备到类中,fmt参数指明创建的设备名。函数执行后,会在class下添加一个dev属性,值为设备号。如果device参数不是NULL,还会创建一个指向/sys/devices的符号链接。还可以通过class_device_create_file添加其他属性值到设备项中。
增删设备时类会产生热插拔事件,如果驱动需要为用户空间的事件处理函数添加环境变量,可以通过设置热插拔回调函数int class_simple_set_hotplug(struct class_simple *cs, int (*hotplug)(struct class_device *dev, char **envp, int num_envp, char *buffer, int buffer_size));
实现。
设备移除时,相应的类项也要删除void class_simple_device_remove(dev_t dev);
。
The Full Class Interface
如果class_simple不能满足需要,就需要完整的类机制,定义如下:
struct class {
char *name;
struct class_attribute *class_attrs;
struct class_device_attribute *class_dev_attrs;
int (*hotplug)(struct class_device *dev, char **envp, int num_envp,
char *buffer, int buffer_size);
void (*release)(struct class_device *dev);
void (*class_release)(struct class *class);
/* Some fields omitted */
};
每个类需要有不同的name,出现在/sys/class下;创建类时,class_attrs中的所有属性值都会创建,class_dev_attrs是属于该类的所有设备的默认属性值。hotplug函数在热插拔事件产生时用来添加新的环境变量,上文所述。release在设备从类中移除时调用,class_release在类被移除时调用。
类的各种操作函数和总线、设备等类似:
int class_register(struct class *cls);
void class_unregister(struct class *cls);
struct class_attribute {
struct attribute attr;
ssize_t (*show)(struct class *cls, char *buf);
ssize_t (*store)(struct class *cls, const char *buf, size_t count);
};
CLASS_ATTR(name, mode, show, store);
int class_create_file(struct class *cls, const struct class_attribute *attr);
void class_remove_file(struct class *cls, const struct class_attribute *attr);
类的主要用途是作为属于该类的设备的容器,定义如下:
struct class_device {
struct kobject kobj;
struct class *class;
struct device *dev;
void *class_data;
char class_id[BUF_ID_SIZE];
};
class_id是出现在sysfs下的设备名,class是所属的类名,dev是相关的设备,class_data可以用来保存私有数据。
class_device的操作函数和class类似:
int class_device_register(struct class_device *cd);
void class_device_unregister(struct class_device *cd);
int class_device_rename(struct class_device *cd, char *new_name);
struct class_device_attribute {
struct attribute attr;
ssize_t (*show)(struct class_device *cls, char *buf);
ssize_t (*store)(struct class_device *cls, const char *buf, size_t count);
};
CLASS_DEVICE_ATTR(name, mode, show, store);
int class_device_create_file(struct class_device *cls, const struct class_device_attribute *attr);
void class_device_remove_file(struct class_device *cls, const struct class_device_attribute *attr);
类子系统有一个其他子系统不具有的概念,接口,但是最好理解为设备增添的触发机制,定义如下:
struct class_interface {
struct class *class;
int (*add)(struct class_device *cd);
void (*remove)(struct class_device *cd);
};
int class_interface_register(struct class_interface *intf);
void class_interface_unregister(struct class_interface *intf);
当一个类设备添加到class_inteface的类时,会调用add函数,设备移除时,会调用remove函数。
Put It All Together
下面以PCI子系统为例,说明PCI设备是如何与驱动模型交互的。
Add a Device
PCI子系统声明了一个总线类型,名为pci_bus_type,初始化如下:
struct bus_type pci_bus_type = {
.name = "pci",
.match = pci_bus_match,
.hotplug = pci_hotplug,
.suspend = pci_device_suspend,
.resume = pci_device_resume,
.dev_attrs = pci_dev_attrs,
};
调用bus_register函数时,会加载PCI子系统到内核中,pci_bus_type就会注册到驱动核心中;驱动核心会在/sys/bus/pci下创建devices和drivers两个目录。
所有的PCI驱动都必须定义一个struct pci_driver类型的变量,指明该驱动所拥有的功能;这个变量包含一个struct device_driver类型的变量,该变量在PCI驱动注册时由PCI核心进行初始化。
初始化完成后,PCI核心可以将PCI驱动注册到驱动核心,进而可以绑定到其支持的PCI设备。
PCI核心在平台相关的代码(负责和PCI总线通信)的帮助下,开始探测PCI的地址空间,寻找所有的PCI设备,并为找到的设备创建struct pci_dev数据结构:
struct pci_dev {
struct pci_bus *bus;
/* ... */
unsigned int devfn;
unsigned short vendor;
unsigned short device;
unsigned short subsystem_vendor;
unsigned short subsystem_device;
unsigned int class;
/* ... */
struct pci_driver *driver;
/* ... */
struct device dev;
/* ... */
};
和总线相关的数据域(devfn,vendor,device等)由PCI核心初始化,struct device变量(dev域)的parent域指向该PCI设备依赖的PCI总线设备,bus域指向pci_bus_type变量,name和bus_id根据设备的信息设置。
设备信息初始化完成后,通过device_register函数注册到驱动核心中。
在device_register函数内部,驱动核心初始化设备信息结构体的一些域,将设备的kobject注册到kobject核心中(会产生热插拔事件),并将设备添加到其父设备持有的设备列表中,以便正确描述设备的层级结构。
然后,设备会被添加到所属总线的设备列表中,比如pci_bus_type的列表中。之后注册到该总线的所有驱动会被遍历,总线的match函数会被调用,用来识别设备。
match函数会将传递给它的struct device类型的参数转化为struct pci_dev类型,还会将struct device_driver转化为struct pci_driver,以便判断此驱动是否能支持此设备。如果不能,返回0给驱动核心,驱动核心会对列表中的下一个驱动进行判断。
如果匹配成功,返回1给驱动核心,驱动核心将struct device的driver域指向该驱动,并调用驱动的probe函数。
在PCI驱动真正注册到驱动核心前,probe函数指向pci_device_probe函数,会再次判断当前的驱动是否支持该设备,并增加设备的引用计数,调用PCI驱动的probe函数。
如果PCI驱动的probe函数发现自己无法处理该设备,返回一个负数,驱动核心会对驱动列表的下一个表项进行判断。如果probe函数判断可以处理该设备,还会进行一些初始化工作,以便正确处理设备,并返回0。驱动核心会将设备添加到目前已经绑定到该驱动的设备列表中,并且在sysfs中创建相应的设备目录。
Remove a Device
移除PCI设备时,会调用pci_remove_bus_device函数,完成一些PCI的清理工作和家务活,然后调用device_unregister函数,传入struct pci_device的struct device成员。
device_unregister函数内,驱动核心会删除符号链接,将设备从其设备列表中删除,调用kobject_del函数,传入struct device的struct kobject成员。
kobject_del函数会删除kobject对应的sysfs文件,移除设备的kobject引用,如果引用计数为0,调用PCI设备的release函数,即pci_release_dev,释放struct pci_dev占用的内存空间。
至此,设备完全从系统中删除。
Add a Driver
PCI驱动通过函数pci_register_driver将自己注册到PCI核心中,pci_register_driver函数会初始化struct pci_driver内的struct device_driver结构体;然后PCI核心在驱动核心调用driver_register函数传入struct pci_driver结构体内的struct device_driver成员。
driver_register函数初始化struct device_driver内的一些锁,然后调用bus_add_driver函数,完成以下工作:
- 寻找驱动关联的总线,如果没有找到,函数立即返回
- 根据驱动的名称和关联的总线创建驱动的sysfs目录
- 获得总线内部的锁,遍历注册到总线上的所有设备,调用match函数,就像添加新设备的时候,如果匹配成功,执行绑定的剩余过程
Remove a Driver
PCI驱动的删除过程只需要调用pci_unregister_driver函数,此函数又会调用驱动核心的driver_unregister函数,传入struct pci_driver的struct device_driver成员。
driver_unregister函数会清理sysfs下的属性值对应的文件,然后遍历关联到该驱动的所有设备,并调用release函数。
当驱动的所有设备都解绑后,驱动执行以下unique代码:
down(&drv->unload_sem);
up(&drv->unload_sem);
这个操作在返回函数(release)调用者之前完成,代码需要保证在返回之前驱动的所有引用计数变为0,因此要持有这个锁。driver_unregister函数通常在卸载模块的退出执行路径调用,只要还有设备引用驱动,驱动就需要保留在内存中,直到这个锁被释放,从而告知内核何时可以安全的移除驱动。
注:3.16内核中struct device_driver的unload_sem成员已经被删除。
Hotplug
内核将热插拔看作硬件、内核和内核驱动之间的相互作用,而用户将热插拔看作内核和用户空间通过应用程序/sbin/hotplug(现代Linux系统CentOS7中,已经没有这个应用程序)相互作用,内核想要告知用户空间内核中发生了某种类型的热插拔事件时会调用该程序。
Dynamic Devices
热插拔指计算机在运行时添加或移除设备,之前的计算机系统只需要在启动时扫描所有的设备,直到系统关机时,设备才会移除。随着USB、CardBus、PCI热插拔控制器的出现,Linux内核必须能够处理设备毫无预警的消失。
不同的总线类型处理设备移除的方式页不同,例如PCI、CardBus和PCMCIA设备在移除前的一小段时间,驱动通常会收到remove函数的通知,所有从PCI总线读取数据的操作都会返回0xFF。
热插拔不仅限于传统的外围设备,现在Linux内核甚至能处理核心的系统组件的增删,例如CPU,内存。
The /sbin/hotplug Utility
正如上文所说,系统增加或者移除设备时,内核会调用用户空间的/sbin/hotplug程序,产生一个热插拔事件。这个程序其实是一个bash脚本,会调用/etc/hotplug.d/下的一系列的程序(然而这个目录CentOS7下也没找到)。
/sbin/hotplug通过一些环境变量告知热插拔程序内核中发生的事件,包括以下几个环境变量:
- ACTION:add或者remove
- DEVPATH:sysfs下的路径,指向正在创建或者销毁的kobject
- SEQNUM:热插拔时间的序列号,64位,按序递增
- SUBSYSTEM
接着,书中介绍下列总线各自独有的系统变量:IEEE1394、Networking、PCI、Input、USB、SCSI、Laptop docking stations、s/390 and zSeries。
Using /sbin/hotplug
既然所有的设备在添加或者移除时内核都会调用/sbin/hotplug脚本,有一些工具可以通过hotplug运行,例如Linux Hotplug脚本和udev工具。
Linux热插拔脚本是/sbin/hotplug第一个调用的对象,这些脚本会根据内核为新发现的设备设置的环境变量尝试匹配内核模块。
正如前文所述,驱动使用MODULE_DEVICE_TABLE宏时,depmod程序会创建一个/lib/module/KERNEL_VERSION/modules.*map的文件,*取决于驱动的总线类型。热插拔脚本会根据这些文件确定支持内核发现的设备要加载的模块,脚本会加载所有能够匹配的模块,让内核判断那个模块最适合。这些脚本不会在移除设备时卸载任何模块,防止意外将要移除的设备的驱动控制的其他设备关闭。
创建统一的驱动模型的主要原因之一是为了允许用户空间能够动态的管理/dev文件树,之前通过devfs实现,但是由于缺少维护者以及一些难以修复的核心漏洞而难以继续使用。一些内核开发者意识到,如果所有的设备信息都可以从用户空间获取,就能够维护/dev文件树。
devfs有一些基础的设计缺陷,需要修改每个设备驱动来支持,还需要驱动能够识别自己在/dev文件树中的名字和位置,而且不能妥善处理主从设备号的分配,不允许用户空间简单地重命名设备,导致设备的重命名只能由内核完成。
随着Linux安装在大型服务器上,如何管理大量的设备成为许多用户的问题,如何保证一个设备总是以固定的名字出现在系统中变得很困难。
于是,udev工具应运而生,基于导出到用户空间的所有设备信息,设备的命名也由用户空间处理,带来的极大的灵活性。
为了使udev正常工作,设备驱动所需做的所有工作,就是保证分给驱动控制的设备的主从设备号通过sysfs导出到用户空间,通过下列子系统来分配设备号就无需进行导出工作:tty、misc、usb、input、scsi、block、i2c、network、frame buffer。
udev会寻找sysfs下/class/目录下的dev文件,确定设备的主从设备号,设备驱动需要为其控制的所有设备创建那个文件,可以通过class_simple接口实现。
第一步,通过class_simple_create函数创建一个struct class_simple对象:
static struct class_simple *foo_class;
...
foo_class = class_simple_create(THIS_MODULE, "foo");
if (IS_ERR(foo_class)) {
printk(KERN_ERR "Error creating foo class.\n");
goto error;
}
上述代码会在/sys/class/foo创建一个目录,当一个新设备绑定到驱动时,要分配从设备号给它,通过class_simple_device_add函数class_simple_device_add(foo_class, MKDEV(FOO_MAJOR, minor), NULL, "foo%d", minor);
,在/sys/class/foo下创建一个名为fooN的子目录,N是从设备号,目录下会创建一个名为dev的文件,即udev所需的设备号。
驱动程序从 设备解绑时,通过class_simple_device_remove函数移除sysfs项class_simple_device_remove(MKDEV(FOO_MAJOR, minor));
。
当驱动完全关闭时,需要调用class_simple_destory函数销毁创建的struct class_simple对象class_simple_destory(foo_class);
。
dev文件包含注设备号:从设备号,可以通过print_dev_t函数将主从设备号正确格式化。
Dealing with Firmware
作为一个驱动作者,可能遇到设备在正常工作前必须要下载固件的情况,将固件编码到驱动中是一个错误,不能更新,而且容易出错,还有版权问题。
The Kernel Firmware Interface
正确的做法是从用户空间获取需要的固件,不要从内核空间打开包含固件的文件,通过固件接口:
#include <linux/firmware.h>
int request_firmware(const struct firmware **fw, char *name,
struct device *device);
name指明所需的固件名,通常指厂商提供的固件文件名,如果加载成功,返回0。fw指向下列结构体:
struct firmware {
size_t size;
u8 *data;
};
包含真正的固件,可以下载到设备中。但是固件数据由用户提供,没有任何检查。固件下载完毕后,要将占用的资源释放void release_firmware(struct firmware *fw);
。
由于request_firmware需要用户空间提供数据,可能会休眠,如果驱动程序在请求固件时不能休眠,可以通过
int request_firmware_nwait(struct module *module,
char *name, struct device *device, void *context,
void (*cont)(const struct firmware *fw, void *context));
module通常为THIS_MODULE,如果顺利的话函数会开始固件的加载过程,返回0。加载完成后调用cont,如果加载失败,fw为NULL。
How It Works
固件子系统和sysfs、热插拔机制交互,调用request_firmware函数时,会在/sys/class/firmware下以设备名创建目录,包含下列三个属性值:
- loading:由加载固件的用户程序设置为1,加载完成后设为0,写入-1会导致加载过程中止
- data:保存固件数据,用户程序将固件写入该文件
- /sys/devices目录下的文件项的符号链接
sysfs项创建完成后,内核会产生一个热插拔事件,传递环境变量FIRMWARE给热插拔处理函数,指明传递给request_firmware函数的名称。处理函数需要定位固件文件,根据提供的属性将其拷贝到内核空间,如果找不到文件,需要将loading文件设为-1。
如果一个固件请求在10s内没有响应,内核返回失败状态给驱动。超时的时间可以通过/sys/class/firmware/timeout设置。
志:驱动注册指的是驱动所属的子系统类型(如PCI)将驱动注册到驱动的核心的过程,或者指驱动程序将自己注册到设备所属的子系统的核心中;设备注册指的是设备将其自身注册到驱动核心的过程(从书中393页内容推知)
总线、设备、驱动都有自己的核心(core)。
作者:glob
出处:http://www.cnblogs.com/adera/
欢迎访问我的个人博客:https://blog.globs.site/
本文版权归作者和博客园共有,转载请注明出处。