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
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)。
LDD-The Linux Device Model