花了一周时间细细地对看了ldd3的中文和英文版的《The Linux Device Model》一章,现做以下学习笔记:
一、概述
为什么需要一个设备模型?
就是为了统一 获取系统信息的 数据结构系统。而且此数据结构系统要支持多种功能特性。
目前设备模型能够支持的特性和任务
n 电源管理和系统关机
n 与用户空间通信
n 可热挺拔设备
n 设备类
n 管理对象生命周期
linux设备模型在sysfs中印象
linux设备模型通过sysfs与用户空间通信。它关注三个实体总线,设备和驱动。具体的总线目录下会有驱动和设备目录,设备目录里的文件放的是对sys顶层目录中devices目录里设备的连接。
linux设备模型在能看到的表现形式上就是一些通用的数据结构和函数。
二、构成设备模型的底层数据结构及相关操作
kobjects,ksets,subsystems
subsystems在后来的模型中去除了该结构
kobjects的四个功能
n 对象引用计数(kref)
n sysfs表示(每个kobject在sysfs中都是一个目录)
n 数据结构粘和(个人理解通过container_of类宏实现)
n 热插拔事件处理(通常提供事件信息,具体处理函数在高层结构中)
struct kobject {
const char *name;
struct list_head entry;
struct kobject *parent;
struct kset *kset;
struct kobj_type *ktype;
struct sysfs_dirent *sd;
struct kref kref;
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;
};
每一个kobject必须有一个释放函数,否则kobject代表的对象可能无法释放。当kobject的计数为0时,代码被异步通知,执行kobject的release方法。
kobj_type结构是描述kobject属性的结构,在kset内嵌的kobject中也会找到这个结构。通常使用kset中的kobj_type提供的属性和方法有较高的使用优先级。
kset可看作是kobj_type的集合。kobj_type关注的是kobject本身的属性,kset关注的是kobject的集合。
在sysfs的目录层次中,每个kobject经过kobject_add()函数后就会在sysfs中呈现一个目录。目录的入口是它的parent代表的目录。因为kobject被包含在kset中,kset内嵌的kobject一般就是被包含的kobject的parent域。所以kset在sysfs中呈现的是可以包含多个目录的目录。
在kobject中有一个kobj_type的结构,它的具体形式
struct kobj_type{
void (*release)(struct kobject *);
struct sysfs_ops *sysfs_ops;
struct attribute **default_attrs;
};
struct attribute{
char *name;
struct module *owner;
mode_t mode;
};
struct sysfs_ops{
ssize_t(*show)(struct kobject *kobject,struct attribute *attr,char *buff);
ssize_t(*store)(struct kobject *kobject,struct attribute *attr,const char *buffer,size_t size);
attribute,在sysfs中呈现的是一个文件,name指向文件名,mode就是读写权限保护位。
show方法仅是将信息格式写入buff中,即完成。
当然还可以能过调用sysfs_create_file()函数创建更多的属性文件。
一般情况,sysfs调用属性来控制一个单一的人可读的文本格式文件。但是,有的属性文件要处理大量的非人可读的二进制数据。例如用户空间向设备下载固件时,处理的就是二进制数据。因此需要struct bin_attribute结构的数据。它的创建与删除函数为
int sysfs_create_bin_file(struct kobject *kobj,struct bin_attribute *attr);
int sysfs_remove_bin_file(struct kobject *kobj,struct bin_attribute *attr);
关于kset
struct kset {
struct list_head list;
spinlock_t list_lock;
struct kobject kobj;(内嵌的kobject)
struct kset_uevent_ops *uevent_ops;
};
struct kset_uevent_ops {
int (*filter)(struct kset *kset, struct kobject *kobj);
const char *(*name)(struct kset *kset, struct kobject *kobj);
int (*uevent)(struct kset *kset, struct kobject *kobj,
struct kobj_uevent_env *env);
};
kset_uevent_ops是用来处理热插拔事件。当一个对象被加入或去除,对应的kobject就会相应的加入或去除。但是当产生的事件被传入到用户空间之间,驱动工作都有机会决定是否屏蔽此事件(filter方法),传入的参数(相关子系统称)(name方法),添加变量信息(uevent方法)。
三、高级结构(bus,devices,drivers)
总线是处理器和设备之间的通道,出于设备模型的考虑,所有的设备都要挂在总线上。
总线用数据结构bus_type表征
struct bus_type {
char * name;
struct subsystem subsys;
struct kset drivers;
struct kset devices;
struct bus_attribute * bus_attrs;
struct device_attribute * dev_attrs;
struct driver_attribute * drv_attrs;
int (*match)(struct device * dev, struct device_driver * drv);
int (*hotplug) (struct device *dev, char **envp, int num_envp, char *buffer, int buffer_size); int (*suspend)(struct device * dev, pm_message_t state);
int (*resume)(struct device * dev);
};
bus_type中,只有少数成员要求初始化。名子,和附带的方法(match必须有)。总线有自己的注册和操作方法,也有自己的属性。总线属性是通过struct bus_attribute结构来表征。系统提供了一个重要的宏来创建和初始化bus_attribute结构
BUS_ATTR(name,mode,show,store);
这个宏指定了bus_attribute的名字为bus_attr_name,读写权限为mode,属性读写方法分别为store和read。
属性应该由bus_create_file()函数来创建,bus_remove_file()函数来去除。
设备在linux系统中用struct device来表征。
struct device {
……
struct device *parent;
struct kobject kobj;
char bus_id[BUS_ID_SIZE]; /* position on parent bus */
const char *init_name; /* initial name of the device */
struct bus_type *bus; /* type of bus device is on */
struct device_driver *driver; /* which driver has allocated this device */
void *driver_data; /* data private to the driver */
void (*release)(struct device *dev);
……
};
在device被初始化时至少初始化四个成员:parent,bus_id,bus,release
设备有自己的注册和操作方法,也有自己的属性,和前面介绍的总线类似。
struct device包含了设备模型所需要的信息。但是在高级的设备表示中,struct device并不单独使用,而是嵌入到struct pci_dev,struct usb_device这样的结构中。在高级设备注册时,其实只是给struct device中的某些成员赋值,然后调用device_register()函数。
顺便提一下,实际的总线也是一种设备,使用时也要设备结构体进行初始化。比如platform总线
struct device platform_bus = {
.bus_id = "platform",
};
struct bus_type platform_bus_type = {
.name = "platform",
.dev_attrs = platform_dev_attrs,
.match = platform_match,
.uevent = platform_uevent,
.pm = PLATFORM_PM_OPS_PTR,
};
int __init platform_bus_init(void)
{
int error;
error = device_register(&platform_bus);
if (error)
return error;
error = bus_register(&platform_bus_type);
if (error)
device_unregister(&platform_bus);
return error;
}
bus_register()注册plaform总线到内核,device_register()注册plafform总线设备到内核
设备模型跟踪所有系统已知的驱动。跟踪的原因是驱动核心能够配置设备的驱动,一旦驱动被已知对象识别,设备的一些特性和配置就可以获知。
struct device_driver {
const char *name;
struct bus_type *bus;
struct module *owner;
const char *mod_name; /* used for built-in modules */
int (*probe) (struct device *dev);
int (*remove) (struct device *dev);
void (*shutdown) (struct device *dev);
int (*suspend) (struct device *dev, pm_message_t state);
int (*resume) (struct device *dev);
struct attribute_group **groups;
struct pm_ops *pm;
struct driver_private *p;
};
其中name,bus,probe,remove成员都要初始化的。它的注册和属性方法和struct device类似。同样,device_driver也是作为内嵌的成员嵌入到高级驱动结构中的。当然,在高级驱动注册到系统时,也是初始化了device_driver的成员然后调用driver_register()完成的。
四、类
类是一个设备的高级视图,它关心用户空间做什么,而不是设备如何连接和工作的。
类关系常常由高级代码来处理,不是一定要驱动的明确支持。
很多情况下,类是内核向用户导出信息的最好方法。
驱动核心由2 个接口管理类,class_simple接口和full class接口
class_simple接口只是比full class接口操作少了些。但同样都要创建类,向类中加入设备。full class接口,多了步创建和注册类设备和相关的属性操作。
class interfaces类接口,最好作为一种触发机制可用来在设备进入或离开类时得到通知。得到通知后如进行设备设备,添加更多属性,完成相应系统清理工作等等。
五、device_register()和driver_register()过程
device_register(),初始化device中的一些成员。注册device的kobject产生热插拔事件,
然后增加设备到父设备列表里,这样做有利于设备有正确的组织层次。设备接下来被添加到以总线为根据的设备列表中,接着检查总线的所有驱动。此时会调用总线的match()方法。如果总线上的设备和驱动匹配成功返回1,然后调用匹配驱动的probe方法。在probe方法里,匹配可能会再次被检查。如果probe不能处理设备则此方法会向驱动核心返回一个负值。驱动核心会重新为设备找驱动。如果可以处理,设备会被初始化,然后被加入到目前驱动所支持的设备列表里。最后创建设备连接。
driver_register(),初始化device_driver结构中的一些锁,调用bus_add_driver函数。该函数会做以下事情:找到driver对应的总线,然后在sysfs中创建入口。总线的内部锁被抢占后,总线就检查所有的设备。调用总线的match()方法,剩下的过程和device_register()是一样的。
六、热插拔
无论何时一个设备从系统中增删,都产生一个“热挺拔事件”。内核就会调用用户空间程序/sbin/hotplug.热插拔程序会和一个命令行参数一起调用。相关子系统会设置一些环境变量来帮助热插拔程序了解刚才发生的热插拔事件。这个参数可由kset的name方法设定。如果没有使用这个方法,则使用kset自身的名子。
为/sbin/hotplug设定的环境变量包括
ACTION,对象的添加和去除
DEVPATH,指向发生变化的kobj目录路径
SEQNUM,热插拔事件的序列号
SUBSYSTEM,命令行参数
SUBSYSTEM可以设定的值:ieee1394,net,pci,input,usb,scsi,dock,dasd等。
因为linux内核在设备添加或去除时都会调用/sbin/hotplug,所以用户空间已经出现了两个很实用的工具:Linux hotplug scripts和udev。Linux hotplug scripts是/sbin/hotplug在用户空间中的第一个调用。它是根据环境变量为设备找到一个匹配模块。Linux hotplug scripts会利用另外一个工具depmod,它根据MODULE_DEVICE_TABLE宏创建一个moudule map text file。然后再由Linux hotplug scripts根据这个文件找到对应的模块。如果使用的工具不是depmod,而是modprobe,那么就不需要创建moudule map text file。udev的一个主要功能就是实现/dev目录下设备的动态管理。但是为了udev的正常工作,所有的设备主次设备号必须通过sysfs导出。对于一些子系统如tty,misc,usb,input,scsi,block,i2c,network,framebuffer等已经指定了导出了主次设备号,驱动不需要做任何工作。但是一些由自己指定设备号的驱动要经过些修改才能让udev工作正常。
udev会根据/sys/class/目录下的dev文件获取设备的主次设备号。每一个驱动必须为设备创建此文件。通过class_simple_create和class_simple_device_add调用可以创建出此dev文件。
七、固件
有些设备必须下载了固件后才能正常工作。在linux中,采取由用户态向设备传送固件。
固件传输有两个接口,request_firmware和request_firmware_nowait。
固件子系统和sysfs,hotplug机制一起工作的。当调用request_firmware时,三个属性被创建
loading,指示加载状态
data,接受固件二进制数据
device,一个设备的符号连接
当设备进入系统后,sysfs入口被创建。内核产生一个热插拔事件,环境变量中会包括FIRMWARE这个变量。此变量用来设定request_firmware调用中的name参数。然后固件处理函数会将固件加载到设备,如果有异常,会进行相应的处理。