盡管LDD3中說對多數程序員掌握設備驅動模型不是必要的,但對于嵌入式Linux的底層程序員而言,對設備驅動模型的學習非常重要。
Linux設備模型的目的:為內核建立一個統一的設備模型,從而又一個對系統結構的一般性抽象描述。換句話說,Linux設備模型提取了設備操作的共同屬性,進行抽象,并將這部分共同的屬性在內核中實現,而為需要新添加設備或驅動提供一般性的統一接口,這使得驅動程序的開發變得更簡單了,而程序員只需要去學習接口就行了。
在正式進入設備驅動模型的學習之前,有必要把documentation/filesystems/sysfs.txt讀一遍(不能偷懶)。sysfs.txt主要描述/sys目錄的創建及其屬性,sys目錄描述了設備驅動模型的層次關系,我們可以簡略看一下/sys目錄,
block:所有塊設備
devices:系統所有設備(塊設備特殊),對應struct device的層次結構
bus:系統中所有總線類型(指總線類型而不是總線設備,總線設備在devices下),bus的每個子目錄都包含
--devices:包含到devices目錄中設備的軟鏈接
--drivers:與bus類型匹配的驅動程序
class:系統中設備類型(如聲卡、網卡、顯卡等)
fs:一些文件系統,具體可參考filesystems /fuse.txt中例子
dev:包含2個子目錄
--char:字符設備鏈接,鏈接到devices目錄,以:命名
--block:塊設備鏈接
Linux設備模型學習分為:Linux設備底層模型,描述設備的底層層次實現(kobject);Linux上層容器,包括總線類型(bus_type)、設備(device)和驅動(device_driver)。
====??Linux設備底層模型 ====
謹記:像上面看到的一樣,設備模型是層次的結構,層次的每一個節點都是通過kobject實現的。在文件上則體現在sysfs文件系統。
kobject結構
內核中存在struct kobject數據結構,每個加載到系統中的kobject都唯一對應/sys或者子目錄中的一個文件夾。可以這樣說,許多kobject結構就構成設備模型的層次結構。每個kobject對應一個或多個struct attribute描述屬性的結構。
點擊(此處)折疊或打開
struct kobject?{
const?char?*name;?/*?對應sysfs的目錄名?*/
struct list_head entry;?/*?kobjetct雙向鏈表?*/
struct kobject?*parent;?/*?指向kset中的kobject,相當于指向父目錄?*/
struct kset?*kset;?/*指向所屬的kset?*/
struct kobj_type?*ktype;?/*負責對kobject結構跟蹤*/
struct sysfs_dirent?*sd;
struct kref kref;?/*kobject引用計數*/
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結構是組成設備模型的基本結構,最初kobject設計只用來跟蹤模塊引用計數,現已增加支持,
——?sysfs表述:在sysfs中的每個對象都有對應的kobject
—— 數據結構關聯:通過鏈接將不同的層次數據關聯
—— 熱插拔事件處理:kobject子系統將產生的熱插拔事件通知用戶空間
kobject一般不單獨使用,而是嵌入到上層結構(比如struct device,struct device_driver)當中使用。kobject的創建者需要直接或間接設置的成員有:ktype、kset和parent。kset我們后面再說,parent設置為NULL時,kobject默認創建到/sys頂層目錄下,否則創建到對應的kobject目錄中。重點來分析ktype成員的類型,
點擊(此處)折疊或打開
#include?
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);
};
ktype包含了釋放設備、默認屬性以及屬性的實現方法幾個重要成員。每個kobject必須有一個release方法,并且kobject在該方法被調用之前必須保持不變(處于穩定狀態)。默認屬性的結構如下,
點擊(此處)折疊或打開
#include?
struct attribute?{
const?char?*name;?/*?屬性名稱?*/
mode_t mode;?/*?屬性保護:只讀設為S_IRUGO,可寫設為S_IWUSR?*/
}
kobj_type中的default_attrs為二級結構指針,可以對每個kobject使用多個默認屬性,最后一個屬性使用NULL填充。struct sysfs_ops結構則如下,
點擊(此處)折疊或打開
struct sysfs_ops?{
ssize_t?(*show)(struct kobject?*,?struct attribute?*,char?*);
ssize_t?(*store)(struct kobject?*,struct attribute?*,const?char?*,?size_t);
};
show方法用于將傳入的指定屬性編碼后放到char *類型的buffer中,store則執行相反功能:將buffer中的編碼信息解碼后傳遞給struct attribute類型變量。兩者都是返回實際的屬性長度。
一個使用kobject的簡單例子如下,
點擊(此處)折疊或打開
#include?
#include?
#include?
#include?
#include?
#include?
MODULE_AUTHOR("xhzuoxin");
MODULE_LICENSE("Dual BSD/GPL");
void my_obj_release(struct kobject?*kobj)
{
printk("release ok.n");
}
ssize_t my_sysfs_show(struct kobject?*kobj,?struct attribute?*attr,?char?*buf)
{
printk("my_sysfs_show.n");
printk("attrname:%s.n",?attr->name);
sprintf(buf,?"%s",?attr->name);
return strlen(attr->name)?+?1;
}
ssize_t my_sysfs_store(struct kobject?*kobj,?struct attribute?*attr,?const?char?*buf,
size_t count)
{
printk("my_sysfs_store.n");
printk("write:%sn",?buf);
return count;
}
struct sysfs_ops my_sysfs_ops?=?{
.show?=?my_sysfs_show,
.store?=?my_sysfs_store,
};
struct attribute my_attrs?=?{
.name?=?"zx_kobj",
.mode?=?S_IRWXUGO,
};
struct attribute?*my_attrs_def[]?=?{
&my_attrs,
NULL,
};
struct kobj_type my_ktype?=?{
.release?=?my_obj_release,
.sysfs_ops?=?&my_sysfs_ops,
.default_attrs?=?my_attrs_def,
};
struct kobject my_kobj?;
int?__init kobj_test_init(void)
{
printk("kobj_test init.n");
kobject_init_and_add(&my_kobj,?&my_ktype,?NULL,?"zx");
return 0;
}
void __exit kobj_test_exit(void)
{
printk("kobj_test exit.n");
kobject_del(&my_kobj);
}
module_init(kobj_test_init);
module_exit(kobj_test_exit);
例子中有兩個函數,用于初始化添加和刪除kobject結構,
點擊(此處)折疊或打開
int?kobject_init_and_add(struct kobject?*kobj,?struct kobj_type?*ktype,
struct kobject?*parent,?const?char?*fmt,?...);?/*?fmt指定kobject名稱?*/
void kobject_del(struct kobject?*kobj);
加載模塊后,在/sys目錄下增加了一個叫zx達到目錄,zx目錄下創建了一個屬性文件zx_kobj,使用tree /sys/zx查看。
內核提供了許多與kobject結構相關的函數,如下:
點擊(此處)折疊或打開
//?kobject初始化函數
void kobject_init(struct kobject?*?kobj);
//?設置指定kobject的名稱
int?kobject_set_name(struct kobject?*kobj,?const?char?*format,?...);
//?將kobj 對象的引用計數加,同時返回該對象的指針
struct kobject?*kobject_get(struct kobject?*kobj);
//?將kobj對象的引用計數減,如果引用計數降為,則調用kobject release()釋放該kobject對象
void kobject_put(struct kobject?*?kobj);
//?將kobj對象加入Linux設備層次。掛接該kobject對象到kset的list鏈中,增加父目錄各級kobject的引//?用計數,在其parent指向的目錄下創建文件節點,并啟動該類型內核對象的hotplug函數
int?kobject_add(struct kobject?*?kobj);
//?kobject注冊函數,調用kobject init()初始化kobj,再調用kobject_add()完成該內核對象的注冊
int?kobject_register(struct kobject?*?kobj);
//?從Linux設備層次(hierarchy)中刪除kobj對象
void kobject_del(struct kobject?*?kobj);
//?kobject注銷函數.?與kobject register()相反,它首先調用kobject del從設備層次中刪除該對象,再調//?用kobject put()減少該對象的引用計數,如果引用計數降為,則釋放kobject對象
void kobject_unregister(struct kobject?*?kobj);
kset結構
我們先看上圖,kobject通過kset組織成層次化的結構,kset將一系列相同類型的kobject使用(雙向)鏈表連接起來,可以這樣 認為,kset充當鏈表頭作用,kset內部內嵌了一個kobject結構。內核中用kset數據結構表示為:
點擊(此處)折疊或打開
#include?
struct kset?{
struct list_head list;?/*?用于連接kset中所有kobject的鏈表頭?*/
spinlock_t list_lock;?/*?掃描kobject組成的鏈表時使用的鎖?*/
struct kobject kobj;?/*?嵌入的kobject?*/
const?struct kset_uevent_ops?*uevent_ops;?/*?kset的uevent操作?*/
};
與kobject?相似,kset_init()完成指定kset的初始化,kset_get()和kset_put()分別增加和減少kset對象的引用計數。Kset_add()和kset_del()函數分別實現將指定keset對象加入設備層次和從其中刪除;kset_register()函數完成kset的注冊而kset_unregister()函數則完成kset的注銷。
==== 設備模型上層容器 ====
這里要描述的上層容器包括總線類型(bus_type)、設備(device)和驅動(device_driver),這3個模型環環相扣,參考圖9-2。為何稱為容器?因為bus_type/device/device_driver結構都內嵌了Linux設備的底層模型(kobject結構)。為什么稱為上層而不是頂層?因為實際的驅動設備結構往往內嵌bus_type/device/device_driver這些結構,比如pci,usb等。
總線類型、設備、驅動3者之間關系:
在繼續之前,自我感覺需要區分2個概念:總線設備與總線類型。總線設備本質上是一種設備,也需要像設備一樣進行初始化,但位于設備的最頂層,總線類型是一種在設備和驅動數據結構中都包含的的抽象的描述(如圖9-2),總線類型在/sys/bus目錄下對應實體,總線設備在/devices目錄下對應實體。
總線類型bus_type
內核對總線類型的描述如下:
點擊(此處)折疊或打開
struct bus_type?{
const?char?*name;?/*?總線類型名?*/
struct bus_attribute?*bus_attrs;?/*?總線的屬性?*/
struct device_attribute?*dev_attrs;?/*?設備屬性,為每個加入總線的設備建立屬性鏈表?*/
struct driver_attribute?*drv_attrs;?/*?驅動屬性,為每個加入總線的驅動建立屬性鏈表?*/
/*?驅動與設備匹配函數:當一個新設備或者驅動被添加到這個總線時,這個方法會被調用一次或多次,若指定的驅動程序能夠處理指定的設備,則返回非零值。必須在總線層使用這個函數,?因為那里存在正確的邏輯,核心內核不知道如何為每個總線類型匹配設備和驅動程序?*/
int?(*match)(struct device?*dev,?struct device_driver?*drv);
/*在為用戶空間產生熱插拔事件之前,這個方法允許總線添加環境變量(參數和 kset 的uevent方法相同)*/
int?(*uevent)(struct device?*dev,?struct kobj_uevent_env?*env);
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);
const?struct dev_pm_ops?*pm;
struct subsys_private?*p;?/*?一個很重要的域,包含了device鏈表和drivers鏈表?*/
};
接著對bus_type中比較關注的幾個成員進行簡述,
[1] struct bus_attribute結構,device_attribute與driver_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);
};
[2] subsys_private中包含了對加入總線的設備的鏈表描述和驅動程序的鏈表描述,省略的部分結構如下
點擊(此處)折疊或打開
struct subsys_private?{
struct kset subsys;
struct kset?*devices_kset;?/*?使用kset構建關聯的devices鏈表頭?*/
struct kset?*drivers_kset;?/*?使用kset構建關聯的drivers鏈表頭?*/
struct klist klist_devices;?/*?通過循環可訪問devices_kset的鏈表?*/
struct klist klist_drivers;?/*?通過循環可訪問drivers_kset的鏈表?*/
struct bus_type?*bus;?/*?反指向關聯的bus_type結構?*/
......
};
bus_type通過掃描設備鏈表和驅動鏈表,使用mach方法查找匹配的設備和驅動,然后將struct device中的*driver設置為匹配的驅動,將struct device_driver中的device設置為匹配的設備,這就完成了將總線、設備和驅動3者之間的關聯。
bus_type只有很少的成員必須提供初始化,大部分由設備模型核心控制。內核提供許多函數實現bus_type的注冊注銷等操作,新注冊的總線可以再/sys/bus目錄下看到。
點擊(此處)折疊或打開
struct bus_type ldd_bus_type?=?{?/*?bus_type初始化?*/
.name?=?"ldd",
.match?=?ldd_match,?/*?方法實現參見實例?*/
.uevent?=?ldd_uevent,?/*?方法實現參見實例?*/
};
ret?=?bus_register(&ldd_bus_type);?/*?注冊,成功返回0?*/
if?(ret)
return ret;
void bus_unregister(struct bus_type?*bus);?/*?注銷?*/
設備device
設備通過device結構描述,
點擊(此處)折疊或打開
struct device?{
struct device?*parent;?/*?父設備,總線設備指定為NULL?*/
struct device_private?*p;?/*?包含設備鏈表,driver_data(驅動程序要使用數據)等信息?*/
struct kobject kobj;
const?char?*init_name;?/*?初始默認的設備名,但@device_add調用之后又重新設為NULL?*/
struct device_type?*type;
struct mutex mutex;?/*?mutex?to?synchronize calls?to?its driver?*/
struct bus_type?*bus;?/*?type of bus device?is?on?*/
struct device_driver?*driver;?/*?which driver has allocated this device?*/
void?*platform_data;?/*?Platform specific data,?device core doesn't touch it?*/
struct dev_pm_info power;
#ifdef CONFIG_NUMA
int?numa_node;?/*?NUMA node this device?is?close?to?*/
#endif
u64?*dma_mask;?/*?dma mask?(if?dma'able device)?*/
u64 coherent_dma_mask;/*?Like dma_mask,?but?for
alloc_coherent mappings as
not?all hardware supports
64 bit addresses?for?consistent
allocations such descriptors.?*/
struct device_dma_parameters?*dma_parms;
struct list_head dma_pools;?/*?dma pools?(if?dma'ble)?*/
struct dma_coherent_mem?*dma_mem;?/*?internal?for?coherent mem override?*/
/*?arch specific additions?*/
struct dev_archdata archdata;
#ifdef CONFIG_OF
struct device_node?*of_node;
#endif
dev_t devt;?/*?dev_t,?creates the sysfs?"dev"?設備號?*/
spinlock_t devres_lock;
struct list_head devres_head;
struct klist_node knode_class;
struct?class?*class;
const?struct attribute_group?**groups;?/*?optional groups?*/
void?(*release)(struct device?*dev);
};
設備在sysfs文件系統中的入口可以有屬性,這通過struct device_attribute單獨描述,提供device_create_file類型函數添加屬性。
點擊(此處)折疊或打開
/*?interface?for?exporting device attributes?*/
struct device_attribute?{
struct attribute attr;
ssize_t?(*show)(struct device?*dev,?struct device_attribute?*attr,
char?*buf);
ssize_t?(*store)(struct device?*dev,?struct device_attribute?*attr,
const?char?*buf,?size_t count);
};
使用宏DEVICE_ATTR宏可以方便地再編譯時構建設備屬性,構建好屬性之后就必須將屬性添加到設備。
點擊(此處)折疊或打開
/*?最終生成變量dev_attr_##_name描述屬性,
*?比如DEVICE_ATTR(zx,S_IRUGO,show_method,NULL);
*?則create_file中entry傳入實參為dev_attr_zx?*/
DEVICE_ATTR(_name,_mode,_show,_store);
/*屬性文件的添加與刪除使用以下函數?*/
int?device_create_file(struct device?*device,?struct device_attribute?*?entry);
void device_remove_file(struct device?*?dev,?struct device_attribute?*?attr);
總線設備的注冊:總線設備與一般設備一樣,需要單獨注冊,與一般設備不同,總線設備的parent與bus域設為NULL。一般設備注冊注銷函數為
點擊(此處)折疊或打開
int?device_register(struct device?*dev);?/*?成功返回0,需要檢查返回值?*/
void device_unregister(struct device?*dev);
實際創建新設備時,不是直接使用device結構,而是將device結構嵌入到具體的設備結構當中,比如
點擊(此處)折疊或打開
struct ldd_device?{
char?*name;?/*?設備名稱?*/
struct ldd_driver?*driver;?/*?ldd設備關聯的驅動?*/
struct device dev;?/*?嵌入的device結構?*/
};
/*?同時提供根據device結構獲取ldd_device結構的宏定義?*/
#define to_ldd_device(dev)?container_of(dev,?struct ldd_device,?dev);
驅動device_driver
驅動結構描述,
點擊(此處)折疊或打開
struct device_driver?{
const?char?*name;?/*?驅動名稱,在sysfs中以文件夾名出現?*/
struct bus_type?*bus;?/*?驅動關聯的總線類型?*/
struct module?*owner;
const?char?*mod_name;?/*?used?for?built-in?modules?*/
bool suppress_bind_attrs;?/*?disables bind/unbind via sysfs?*/
#if?defined(CONFIG_OF)
const?struct of_device_id?*of_match_table;
#endif
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);
const?struct attribute_group?**groups;
const?struct dev_pm_ops?*pm;
struct driver_private?*p;
};
struct driver_private?{?/*?定義device_driver中的私有數據類型?*/
struct kobject kobj;?/*?內建kobject?*/
struct klist klist_devices;?/*?驅動關聯的設備鏈表,一個驅動可以關聯多個設備?*/
struct klist_node knode_bus;
struct module_kobject?*mkobj;
struct device_driver?*driver;?/*?連接到的驅動鏈表?*/
};
#define to_driver(obj)?container_of(obj,?struct driver_private,?kobj)
與設備和總線類似,驅動可以有屬性,需要單獨定義并添加。
點擊(此處)折疊或打開
/*?sysfs interface?for?exporting driver attributes?*/
struct driver_attribute?{
struct attribute attr;
ssize_t?(*show)(struct device_driver?*driver,?char?*buf);
ssize_t?(*store)(struct device_driver?*driver,?const?char?*buf,
size_t count);
};
DRIVER_ATTR(_name,_mode,_show,_store);?/*?最終創建變量driver_attr_##_name描述屬性?*/
/*屬性文件創建的方法:*/
int?driver_create_file(struct device_driver?*?drv,?struct driver_attribute?*?attr);
void driver_remove_file(struct device_driver?*?drv,?struct driver_attribute?*?attr);
驅動的注冊與注銷
點擊(此處)折疊或打開
/*注冊device_driver 結構的函數?*/
int?driver_register(struct device_driver?*drv);
void driver_unregister(struct device_driver?*drv);
與設備結構一樣,在編寫新設備的驅動程序時,常常將device_driver結構嵌入到新設備結構當中使用。
====?實例分析 ====
實例源代碼主要來自LDD3提供的示例代碼,因為LDD3的代碼是linux-2.6.10版本,因此需要對源代碼做一些修改。所有源代碼參見:device_model.zip。因為兩個模塊關聯,我們這使用一個Makefile文件同時編譯2個模塊,如下
點擊(此處)折疊或打開
obj-m?:=?lddbus.o sculld.o
lddbus模塊分析
包括2個文件,lddbus.c(example/lddbus/)與lddbus.h(example/include/)。lddbus.h中使用extern申明了將要使用EXPORT_SYMBOL導出的變量ldd_bus_type,lddbus.c中創建了總線類型ldd_bus_type以及總線設備ldd_bus。
lddbus.h
-> extern ldd_bus_type
lddbus.c
-> ldd_bus_type (EXPORT_SYMBOL)
-> ldd_bus
由于版本變遷,對源代碼做了修改,(i)熱插拔不再使用hotplug函數,因此將該操作去掉了;(ii)dev->bus_id[]改成了使用dev_set_name()設置設備名稱,使用init_name也可以設置,但后來發現init_name會在調用device_add之后就被賦值為NULL,這導致一個重大內核錯誤(kernel panic),將在后面詳述。
分析源代碼:作者定義了ldd_device與ldd_driver,兩個變量分別內嵌device與device_driver結構,然后分別為ldd_device定義了注冊函數register_ldd_device和注銷函數unregister_ldd_device,對ldd_driver也做了類似的工作。還宏定義了to_ldd_driver和to_ldd_device來使用內嵌結構(device/device_driver)訪問更上層的容器ldd_device和ldd_driver。但是不用著急,實際模塊裝載時沒有使用ldd_device或者ldd_driver,而是將它們和相關的注冊注銷等操作使用EXPORT_SYMBOL導出到其它模塊使用(這將在實例sculld模塊中看到)。
struct ldd_device/register_ldd_device/unregister_ldd_device
-> struct device/ device_register/device_unregister
-> to_ldd_device
struct ldd_driver也類似
LDD3的Makefile中普遍使用了CFLAGS變量,但在新的內核版本中,該變量與內核Makefile的CFLAGS變量沖突,因此將所有的Makefile的CFLAGS變量替換成了EXTRA_CFLAGS。
裝載模塊后,查看/sys/bus目錄下,增加了ldd文件夾,/sys/devices目錄下增加了ldd0文件夾。
sculld模塊分析
sculld模塊是接著lddbus在加載lddbus基礎上進行的,sculld使用了lddbus中導出的ldd_device和ldd_driver結構。我們大致分析下總體的設備和驅動注冊的調用關系,
scull_init()
->register_ldd_driver()? //?由lddbus模塊導出
->driver_register()?
->sculld_register_dev()
->register_ldd_dev()? //?由lddbus模塊導出
->device_register()
裝載程序后查看bus/ldd/devices目錄下,bus/ldd/drivers目錄下多了驅動程序,多了4個設備,devices/ldd0下也多了4個設備。
關于kernel panic錯誤
在修改lddbus與sculld中,裝載sculld模塊時遇到如下錯誤,同時鍵盤大寫字母指示燈閃爍,操作系統被鎖定,只能強制關機。現在記錄分析及解決錯誤的過程,
從網上找到資料,kernel panic類型錯誤要跟蹤信息,還好,使用的虛擬機,把出錯的狀態截屏了。kernnel panic錯誤分硬件和軟件,一般是由于指針指向了NULL。硬件有EIP指示出錯位置,如上圖有一行
EIP:[] strncmp+0x11/0x38
好了,strncmp就是指示出錯位置,然后到源代碼中找到使用該函數地方,出錯前為
!strncmp(dev->init_name,?driver->name,?strlen(driver->name));
前面說過,dev->init_name在調用device_register之后就被設置為NULL了,好了,就是它了,改成如下(通過kobj訪問設備名稱)就OK。
!strncmp(dev->kobj.name,?driver->name,?strlen(driver->name));
?
評論
查看更多