上一篇文章《Linux設備模型 (1)》主要介紹了Linux設備模型在用戶空間的接口sysfs,用戶通過這個接口可以一覽內核設備的全貌。本文將從Linux內核的角度來看一看這個設備模型是如何構建的。
在Linux內核裏,kobject是組成Linux設備模型的基礎,一個kobject對應sysfs裏的一個目錄。從面向對象的角度來說,kobject可以看作是所有設備對象的基類,因爲C語言並沒有面向對象的語法,所以一般是把kobject內嵌到其他結構體裏來實現類似的作用,這裏的其他結構體可以看作是kobject的派生類。Kobject爲Linux設備模型提供了很多有用的功能,比如引用計數,接口抽象,父子關係等等。引用計數本質上就是利用kref實現的,至於kref的細節可以參考我之前的文章《Linux內核裏的“智能指針”》。
另外,Linux設備模型還有一個重要的數據結構kset。Kset本身也是一個kobject,所以它在sysfs裏同樣表現爲一個目錄,但它和kobject的不同之處在於kset可以看作是一個容器,如果你把它類比爲C++裏的容器類如list也無不可。Kset之所以能作爲容器來使用,其內部正是內嵌了一個雙向鏈表結構struct list_head。對於list_head的細節可以參考《玩轉C鏈表》一文。
下面這幅圖可以用來表示kobject和kset在內核裏關係。
在接下來的篇幅裏我們會逐步看到這個關係圖在內核裏是如何建立的。本文的示例代碼可以從這裏下載,下文中的兩個實作都在這個示例代碼裏。
Kobject
Kobject在Linux內核裏的定義如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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;
unsigned
int uevent_suppress:1;
}; |
在《Linux設備模型 (1)》裏面我們介紹到內核裏的設備之間是以樹狀形式組織的,在這種組織架構裏比較靠上層的節點可以看作是下層節點的父節點,反映到sysfs裏就是上級目錄和下級目錄之間的關係,在內核裏,正是kobject幫助我們實現這種父子關係。在kobject的定義裏,name表示的是kobject在sysfs中的名字;指針parent用來指向kobject的父對象;Kref大家應該比較熟悉了,kobject通過它來實現引用計數;Kset指針用來指向這個kobject所屬的kset,下文會再詳細描述kset的用法;對於ktype,如果只是望文生義的話,應該是用來描述kobject的類型信息。Ktype的定義如下:
1
2
3
4
5
|
struct
kobj_type { void
(*release)( struct
kobject *kobj); const
struct sysfs_ops *sysfs_ops;
struct
attribute **default_attrs; }; |
函數指針release是給kref使用的,當引用計數爲0這個指針指向的函數會被調用來釋放內存。sysfs_ops和attribute是做什麼用的呢?前文裏提到,一個kobject對應sysfs裏的一個目錄,而目錄下的文件就是由sysfs_ops和attribute來實現的,其中,attribute定義了kobject的屬性,在sysfs裏對應一個文件,sysfs_ops用來定義讀寫這個文件的方法。Ktype裏的attribute是默認的屬性,另外也可以使用更加靈活的手段,本文的重點還是放在default attribute。
下面看一個實作。在這個實作裏,我們定義一個內嵌kobject的結構。
1
2
3
4
|
struct
my_kobj { int
val; struct
kobject kobj; }; |
最終我們的目的是在內核裏構建這樣的架構。
對應sysfs裏的目錄關係是:
mykobj1/
|-- mykobj2
| |-- name
| `-- val
|-- name
`-- val
這是module_init代碼。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
static
int __init mykobj_init( void )
{ printk(KERN_INFO
"mykobj_init\n" );
obj1 = kzalloc( sizeof ( struct
my_kobj), GFP_KERNEL); if
(!obj1) { return
-ENOMEM; }
obj1->val = 1;
obj2 = kzalloc( sizeof ( struct
my_kobj), GFP_KERNEL); if
(!obj2) { kfree(obj1);
return
-ENOMEM; }
obj2->val = 2;
my_type.release = obj_release;
my_type.default_attrs = my_attrs;
my_type.sysfs_ops = &my_sysfsops;
kobject_init_and_add(&obj1->kobj, &my_type, NULL,
"mykobj1" );
kobject_init_and_add(&obj2->kobj, &my_type, &obj1->kobj,
"mykobj2" );
return
0; } |
這段代碼可以分作三個部分。第一部分是分配obj1和obj2並賦值;第二部分是初始化kobj_type變量my_type;第三部分是調用kobject_init_and_add函數來初始化kobject並把它加入到設備模型的體系架構(也就是上文中提到的內核中的那棵樹)中。kobject_init_and_add是簡化的寫法,這個函數也可以分兩步完成:kobject_init和kobject_add。
int
kobject_init_and_add( struct
kobject *kobj, struct
kobj_type *ktype, struct
kobject *parent, const
char *fmt, ...);
void
kobject_init( struct
kobject *kobj, struct
kobj_type *ktype); int
kobject_add( struct
kobject *kobj, struct
kobject *parent, const
char *fmt, ...); |
kobject_init用來初始化kobject結構,kobject_add用來把kobj加入到設備模型之中。在實作中,我們先對obj1進行初始化和添加的動作,調用參數裏,parent被賦爲NULL,表示obj1沒有父對象,反映到sysfs裏,my_kobj1的目錄會出現在/sys下,obj2的父對象設定爲obj1,那麼my_kobj2的目錄會出現在/sys/my_kobj1下面。
前面提到,kobject也提供了引用計數的功能,雖然本質上是利用kref,但也提供了另外的接口供用戶使用。
1
2
|
struct
kobject *kobject_get( struct
kobject *kobj); void
kobject_put( struct
kobject *kobj); |
kobject_init_and_add和kobject_init這兩個函數被調用後,kobj的引用計數會初始化爲1,所以在module_exit時要記得用kobject_put來釋放引用計數。
我們再回到實作中,看看如何使用ktype。代碼裏,my_attrs是這樣定義的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
struct
attribute name_attr = { .name =
"name" , .mode = 0444,
}; struct
attribute val_attr = { .name =
"val" , .mode = 0666,
}; struct
attribute *my_attrs[] = { &name_attr,
&val_attr,
NULL,
}; |
結構體struct attribute裏的name變量用來指定文件名,mode變量用來指定文件的訪問權限。這裏需要着重指出的是,數組my_attrs的最後一項一定要賦爲NULL,否則會造成內核oops。
sysfs_ops的代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
ssize_t my_show( struct
kobject *kobj, struct
attribute *attr, char
*buffer) { struct
my_kobj *obj = container_of(kobj, struct
my_kobj, kobj); ssize_t count = 0;
if
( strcmp (attr->name,
"name" ) == 0) {
count =
sprintf (buffer,
"%s\n" , kobject_name(kobj));
}
else
if ( strcmp (attr->name,
"val" ) == 0) {
count =
sprintf (buffer,
"%d\n" , obj->val);
}
return
count; } ssize_t my_store( struct
kobject *kobj, struct
attribute *attr, const
char *buffer,
size_t size)
{ struct
my_kobj *obj = container_of(kobj, struct
my_kobj, kobj); if
( strcmp (attr->name,
"val" ) == 0) {
sscanf (buffer,
"%d" , &obj->val);
}
return
size; } struct
sysfs_ops my_sysfsops = { .show = my_show,
.store = my_store,
}; |
讀文件會調用my_show,寫文件會調用my_store。
最後是module_exit:
1
2
3
4
5
6
7
8
9
10
11
12
|
static
void __exit mykobj_exit( void )
{ printk(KERN_INFO
"mykobj_exit\n" );
kobject_del(&obj2->kobj);
kobject_put(&obj2->kobj);
kobject_del(&obj1->kobj);
kobject_put(&obj1->kobj);
return ;
} |
kobject_del的作用是把kobject從設備模型的那棵樹裏摘掉,同時sysfs裏相應的目錄也會刪除。這裏需要指出的是,釋放的順序應該是先子對象,後父對象。因爲kobject_init_and_add和kobject_add這兩個函數會調用kobject_get來增加父對象的引用計數,所以kobject_del需要調用kobject_put來減少父對象的引用計數。在本例中,如果先通過kobject_put來釋放obj1,那kobject_del(&obj2->kobj)就會出現內存錯誤。
在這個實作中,我們建立了兩個對象obj1和obj2,obj1是obj2的父對象,如果推廣開來,obj1可以有更多的子對象。在Linux內核中,這種架構方式其實並無太大的實際價值,有限的用處之一是在sysfs裏創建子目錄(Linux內核裏有這種用法,這種情況下,直接調用內核提供的kobject_create來實現,不需要自定義數據結構並內嵌kobject),而且,創建子目錄也是有其他的辦法的。我們知道,Linux設備模型最初的目的是爲了方便電源管理,這就需要從上到下的遍歷,在這種架構裏,通過obj1並無法訪問其所有的子對象。這個實作最大的意義在於可以讓我們比較清晰的理解kobject如何使用。通常情況下,kobject只需要在葉節點裏使用,上層的節點要使用kset。
Kset
Kset的定義如下:
1
2
3
4
5
6
|
struct
kset { struct
list_head list; spinlock_t list_lock;
struct
kobject kobj; const
struct kset_uevent_ops *uevent_ops;
}; |
Kset結構裏的kobj表明它也是一個kobject,list變量用來組織它所有的子對象。
我們直接看一個實作。在這個實作裏,我們將構建如下的架構。
對應sysfs裏的目錄關係是:
my_kset/
|-- mykobj1
| |-- name
| `-- val
`-- mykobj2
|-- name
`-- val
這個實作和前一個差別很小,下面只簡略的引用一些代碼。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
static
int __init mykset_init( void )
{ printk(KERN_INFO
"mykset_init\n" );
my_kset = kset_create_and_add( "my_kset" , NULL, NULL);
if
(!my_kset) { return
-ENOMEM; }
// Allocate obj1 and obj2
// ...
obj1->kobj.kset = my_kset;
obj2->kobj.kset = my_kset;
// Init my_type
// ...
kobject_init_and_add(&obj1->kobj, &my_type, NULL,
"mykobj1" );
kobject_init_and_add(&obj2->kobj, &my_type, NULL,
"mykobj2" );
return
0; } static
void __exit mykset_exit( void )
{ printk(KERN_INFO
"mykset_exit\n" );
// Release obj1 and obj2
// ...
kset_unregister(my_kset);
return ;
} |
在module_init裏,我們首先調用kset_create_and_add創建my_kset,接下來把my_kset賦給obj1和obj2,最後調用kobject_init_and_add來添加obj1和obj2。這裏需要注意的是,kobject_init_and_add參數裏的parent都是NULL,在這種情況下,obj1和obj2的父對象由kobject結構裏的kset指針決定,在這個實作裏就是my_kset。在module_exit裏,我們還需要額外調用kset_unregister來釋放之前創建的my_kset。
注:看過LDD3的讀者應該對Linux Device Model一章中的subsystem還有印象,我在這裏註明一下,從2.6.23開始Linux內核就拋棄了subsystem,subsystem其實只是kset的一個馬甲,所以拋棄它對Linux設備模型並沒什麼影響。