○、說明
筆記適用於Linux的2.6.10以後的核心。
筆記以Linux
Device
Driver3提供的scull程式(scull目錄中的main.c和scull.h)為記錄主線,並以該驅動程式中的各種系統調用和函數調用流程為記
錄順序。比如,module_init( )和module_exit(
)為相對應的一對系統調用,一般書籍中都會放在一起討論,但是本筆記卻不會這樣,而是在需要調用的時候才會涉及,因此module_init(
)會放在筆記開始時,也就是剛載入module時討論,而module_exit( )則會放在筆記結束前,也就是要卸載module時再加以討論。
該筆記的的目的是為了對Linux Device Drvier3中提到的各個知識點作一下整理,理清一下頭緒,從而能讓我對Linux驅動程式加深整體或者全域上的理解。
註:個人理解,有誤難免!
*******************************************
驅動程式module的工作流程主要分為四個部分:
1、 用Linux提供的命令載入驅動module
2、 驅動module的初始化(初始化結束後即進入“潛伏”狀態,直到有系統調用)
3、 當操作裝置時,即有系統調用時,調用驅動module提供的各個服務函數
4、 卸載驅動module
一、 驅動程式的載入
Linux驅動程式分為兩種形式:一種是直接編譯進核心,另一種是編譯成module形式,然後在需要該驅動module時手動載入。對於前者,還有待學習。
Module形式的驅動,Linux提供了兩個命令用來載入:modprobe和insmod。
其
中modprobe可以解決驅動module的依賴性,即假如正載入的驅動module若引用了其他module提供的核心符號或者其他資源,則
modprobe就會自動載入那些module,不過,使用modprobe時,必須把要載入的驅動module放在當前模組搜尋路徑中。而insmod
命令不會考慮驅動module的依賴性,但是它卻可以載入任意目錄下的驅動module。
一般來說,在驅動開發階段,使用/sbin/insmod比較方便,因為不用將module放入當前module搜尋路徑中。
一旦使用insmod載入模組,則Linux核心就會調用module_init(scull_init_module)特殊宏,其中scull_init_module是驅動初始化函數,可自訂名稱。
在用insmod載入module時,還可以給module提供模組參數,但是這需要在驅動原始碼中加入幾條語句,讓模組參數對insmod和驅動程式可見,如:
static char *whom=”world”;
static int howmany=10;
module_param(howmany,int,S_IRUGO);
module_param(whom,charp,S_IRUGO);
這樣,當使用/sbin/insmod scull.ko whom=”string” howmany=20這樣的命令載入驅動時,whom和howmay的值就會傳入scull驅動模組了。
驅動程式module被載入後,若對裝置進行操作(如open,read,write等),驅動module就會調用相應的函數響應該操作。
那麼,當對裝置進行操作時,驅動module又怎麼知道是自己應該有所響應,而不是其他的驅動module呢,也就是說,Linux核心怎麼知道應該調用哪一個驅動module呢?
目
前我只知道有兩種方式將裝置與驅動module聯絡在一起(也許應該說提供訪問裝置的一種途徑比較恰當):其一是通過某些裝置的ID(比如PCI裝置和
USB裝置的Device ID和Product
ID),Linux核心根據這些ID調用驅動module;其二是在/dev目錄下根據裝置的主次裝置號建立對應的裝置節點(即裝置檔案),這樣當操作
/dev目錄下的裝置檔案時,就會調用相應的驅動module。
二、 驅動module的初始化
使用insmod載入驅動module時,需要讓驅動module為裝置做一些初
始化動作,主要目的是讓Linux核心知道這個裝置(或者說module?),以及在以後對該裝置進行操作(如open,read,write等等)時,
讓Linux核心知道,本module擁有哪些函數可以服務於系統調用。
因此,scull_init_module函數中主要做了以下幾件事情:
a) 分配並註冊主裝置號和次裝置號
b) 初始化代表裝置的struct結構體:scull_dev
c) 初始化互斥體init_MUTEX(本筆記不整理)
d) 初始化在核心中代表裝置的cdev結構體,最主要是將該裝置與file_operations結構體聯絡起來。
1、 分配並註冊主次裝置號
裝置號是在驅動module中分配並註冊的,也就是說,驅動module擁有這個裝置號(我的理解),而/dev目錄下的裝置檔案卻是根據這個裝置號建立的,因此,當訪問/dev目錄下的裝置檔案時,驅動module就知道,自己該出場服務了(當然是由核心通知)。
在Linux核心看來,主裝置號標識裝置對應的驅動程式,告訴Linux核心使用哪一個驅動程式為該裝置(也就是/dev下的裝置檔案)服務;而次裝置號則用來標識具體且唯一的某個裝置。
在核心中,用dev_t類型(其實就是一個32位的不帶正負號的整數)的變數來儲存裝置的主次裝置號,其中高12位表示主裝置號,弟20位表示次裝置號。
裝置獲得主次裝置號有兩種方式:一種是手動給定一個32位元,並將它與裝置聯絡起來(即用某個函數註冊);另一種是調用系統函數給裝置動態分配一個主次裝置號。
對於手動給定一個主次裝置號,使用以下函數:
int register_chrdev_region(dev_t first, unsigned int count, char *name)
其中first是我們手動給定的裝置號,count是所請求的連續裝置號的個數,而name是和該裝置號範圍關聯的裝置名稱,它將出現在/proc/devices和sysfs中。
比
如,若first為0x3FFFF0,count為0x5,那麼該函數就會為5個裝置註冊裝置號,分別是0x3FFFF0,0x3FFFF1,
0x3FFFF2,0x3FFFF3,0x3FFFF4,其中0x3(高12位)為這5個裝置所共有的主裝置號(也就是說這5個裝置都使用同一個驅動程
序)。而0xFFFF0,0xFFFF1,0xFFFF2,0xFFFF3,0xFFFF4就分別是這5個裝置的次裝置號了。
需要注意的是,若
count的值太大了,那麼所請求的裝置號範圍可能會和下一個主裝置號重疊。比如若first還是為0x3FFFF0,而count為0x11,那麼
first+count=0x400001,也就是說為最後兩個裝置分配的主裝置號已經不是0x3,而是0x4了!
用這種方法註冊裝置號有一個缺點,那就是若該驅動module被其他人廣泛使用,那麼無法保證註冊的裝置號是其他人的Linux系統中未分配使用的裝置號。
對於動態分配裝置號,使用以下函數:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name)
該函數需要傳遞給它指定的第一個次裝置號firstminor(一般為0)和要分配的裝置數count,以及裝置名稱,調用該函數後自動分配得到的裝置號儲存在dev中。
動
態分配裝置號可以避免手動指定裝置號時帶來的缺點,但是它卻也有自己的缺點,那就是無法預先在/dev下建立裝置節點,因為動態分配裝置號不能保證在每次
載入驅動module時始終一致(其實若在兩次載入同一個驅動module之間並沒有載入其他的module,那麼自動分配的裝置號還是一致的,因為核心
分配裝置號並不是隨機的,但是書上說某些核心開發人員預示不久的將來會用隨機方式進行處理),不過,這個缺點可以避免,因為在載入驅動module後,我
們可以讀取/proc/devices檔案以獲得Linux核心分配給該裝置的主裝置號。
Linux Device
Driver3提供了一個指令碼scull_load和scull_unload,可以在動態分配的情況下為裝置建立和刪除裝置節點。其實它也是利用了
awk工具從/proc/devices中擷取了資訊,然後才用mknod在/dev下建立裝置節點。
其實scull_load和scull_unload指令碼同樣可以適用於其他驅動程式,只要重新定義變數並調整mknod那幾行語句就可以了。
與主次裝置號相關的3個宏:
MAJOR(dev_t dev):根據裝置號dev獲得主裝置號;
MINOR(dev_t dev):根據裝置號dev獲得次裝置號;
MKDEV(int major, int minor):根據主裝置號major和次裝置號minor構建裝置號。
2、 初始化代表裝置的scull_dev結構體
scull原始碼中定義了一個scull_dev結構體,包括qset,qutuam,訊號量sem以及cdev等欄位。其中qset和qutuam的初始化對於Linux驅動程式的知識點來說毫不相關,因此不加討論。
我
只要知道,在載入module時所調用的module初始化函數中,可以初始化一些裝置相關的變數。但是根據Linux Device
Drvier3作者的意思,裝置相關的變數或者一些資源最好應當在open函數中初始化,比如像中斷號等,雖然在module初始化函數中註冊也是允許
的,但最好是在第一次開啟裝置,也就是open函數中再行分配。
3、 初始化互斥體init_MUTEX
互斥體MUTEX,也就是訊號量的一個變種,與completion,自旋鎖spinlock等等都與驅動中的並發和競態相關,以後再說。
4、 初始化在核心中代表裝置的cdev結構體
其實在Linux核心中,cdev結構體才是真正代表了某個裝置。在核心調用裝置的open,read等操作之前,必須先分配並註冊一個或者多個cdev結構。
我
想可以這麼理解,主次裝置號是涉外的,主要用來在與外部(指的是驅動module和Linux核心以外)互動時確定身份;而cdev結構體則是涉內的,當
需要在module內部,或者與Linux核心之間傳遞一些變數,指標,buffer等東東,或者要調用驅動module中的某個服務函數時,就要用到
cdev結構體了。
在scull函數中,cdev結構體的分配,註冊與初始化使用了一個自訂的scull_setup_cdev函數,在該函數中,主要由以下4條語句對cdev進行初始化:
cdev_init(&dev->cdev, &scull_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &scull_fops;
err = cdev_add (&dev->cdev, devno, 1);
(dev變數是scull程式定義的代表裝置的一個結構體,它包含了cdev結構體,對於dev來說,cdev結構體應該說就是它的核心)
第一條語句是初始化cdev結構體,比如為cdev結構體分配記憶體,為cdev結
構體指定file_operations等等,而第三條語句的作用初看起來似乎與第一條有所重複。但scull程式中既然這麼寫想必就有它的用意,也許需
要看Linux核心原始碼才能搞明白,但目前我是這麼理解的:第一條語句中有關file_operations的部分是為了告訴Linux核心,該
cdev結構體相關的file_operations是scull_fops;而第二條語句則是真正為cdev指定了它的file_operations
欄位就是scull_fops。
scull_fops是file_operations類型的變數,file_operations也是一個結構體,而且是Linux驅動程式中很重要的一個結構體,在scull程式中,定義如下:
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};
以
上定義中,第一條.owner欄位說明本file_operations結構體的擁有者是本驅動module,而接下來的幾個欄位則是告訴驅動
module,當有相應的系統調用到達該module時,module應該調用哪一個函數來為該系統調用服務。比如說,若有一個open系統調用到達
module,則module通過查詢file_operations結構體就知道了,與open系統調用相關的是scull_open函數,於是
module就調用scull_open函數來為open系統調用服務了。其他幾個欄位也完全類似。
當然,Linux核心定義的file_operations結構體還包括其他一些欄位,比如非同步讀寫等等,但還是等用到的時候再說吧。
對cdev初始化的第二條語句是dev->cdev.owner = THIS_MODULE,這條語句就是說正在初始化的cdev結構體的擁有者就是本module
對cdev初始化的最後一條語句是err = cdev_add
(&dev->cdev, devno,
1),該語句的目的就是告訴核心,該cdev結構體資訊。因為cdev_add函數有可能調用失敗,所以需要檢測該函數調用的傳回值。而一旦
cdev_add調用成功返回,那麼我們的裝置就“活”了!也就是說,外部應用程式對它的操作就會被核心允許且調用。因此在驅動程式還沒有完全準備好處理
裝置上的操作時,就絕不能調用cdev_add。
三、 裝置操作
驅動module因為由insmod的載入而進行了初始化之後,就會進入“潛伏”狀態,也就是說,如果沒有系統調用(如open,read等),那麼module中定義的其他函數就絕不會運行!
這裡所說的裝置操作,是指當有系統調用到達驅動module時,module就該調用某個或某些函數有所動作。
對於驅動開發來說,我主要關心的只有一點,那就是系統調用怎樣把一些外部應用程式中的變數值傳遞給驅動module。
scull程式中與裝置操作相關的函數主要分為三類:初始化函數,實際的操作服務函數和清理函數。其中初始化函數只有一個,就是open函數,而操作服務函數則包括read,write,llseek等等函數,至於清理函數則是release函數。
1、open函數
open函數提供給驅動程式以初始化的能力,從而為以後的操作做準備。
說
起來在用insmod載入驅動後也有一個初始化動作,但那個初始化是相對於整個Linux核心,或者說是針對整個module在涉外時的全域意義上的初始
化;而open函數的初始化則是相對於裝置操作來說的,是屬於驅動內部的初始化,比如為以後read操作時用到的某個變數(如file結構體)作一下初始
化,再比如初始化一下裝置,清空一下buffer等等。
在大部分的驅動程式中,open應該完成如下工作:
a、 確定要開啟的具體裝置
b、 檢查裝置特定的錯誤(諸如裝置未就緒或類似的硬體問題)
c、 如果裝置是首次開啟,則對其作一下初始化
d、 如有必要,更新f_op指標
e、 分配並填寫置於filp->private_data裡的資料結構
open函數的原型如下(指的是在file_operations結構體中的定義):
int (*open)(struct inode *inode, struct file *filp)
在驅動開發時要做的,就是為該函數作具體實現,當然對open函數的名稱可以自訂,只要在填寫file_operations結構體中的open欄位時,將自訂的open函數名稱填上就可以了。在scull程式中,用的就是scull_open函數名。
在open函數原型中,有inode和filp兩個參數,都是外部應用程式在操作
裝置時通過調用系統調用傳遞給驅動module的。於是驅動module就可以通過這兩個參數來確定要開啟的具體裝置了。其實這裡所說的具體裝置,並不是
說驅動module需要從系統安裝的所有裝置中確定它所要服務的裝置,而是指module需要從某一類擁有相同主裝置號的裝置中確定它要服務的裝置。
之
所以這麼說,是因為驅動module是對應於某一個主裝置號的所有裝置的。換一句話說,就是Linux核心只管裝置的主裝置號,而不理會裝置的次裝置號是
什麼,如果有兩個,三個或者更多個裝置擁有同一個主裝置號,那麼不管外部應用程式要操作這些裝置中的哪一個,Linux核心都只會調用同一個驅動
module。但是驅動module卻不能不管次裝置號了,因為它是跟某一個具體的裝置打交道的,所以它需要根據open系統調用時傳遞給它的參數中找到
次裝置號,從而確定那唯一的一個裝置(也許驅動module也可以同時操作幾個裝置,但一時也想不起來)。
但是上面所說的通過次裝置號找到具體的裝置,只是其中一種方法;另外還有一種方法就是通過cdev結構體確定某個具體裝置。
設
備所擁有的cdev結構體,或者次裝置號,都儲存在open函數的inode參數中。我們可以使用container_of宏通過inode所擁有的
cdev確定具體裝置,也可以使用iminor宏從inode所擁有的i_rdev確定次裝置號(i_rdev是inode結構體中的一個dev_t類型
的變數,其中儲存了真正的裝置主次編號)。
對於open函數中的file參數,scull程式主要用它來做兩件事:其一是將
根據cdev獲得的代表裝置的scull_dev結構體儲存到file->private_data中,這樣就可以方便今後對該裝置結構體的訪問
了,而不用每次都調用container_of宏或者iminor宏來找到裝置結構體了;其二是根據file結構體中的f_flags欄位來確定,這次的
open調用,是以寫方式開啟裝置,還是以讀方式來開啟裝置。
2、read函數
驅動module的file_operations結構體中可以定義很多裝置操作服務函數,但是我現在關心的是這些函數怎樣與系統調用,或者說是外部應用程式互動,而不管具體的裝置操作怎麼實現,所以只記錄read函數作為代表。
read函數的原型如下:
ssize_t read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
read函數原型中有4個參數,分別是filp,buf,count和f_pos。
其中file結構體指標參數可以用來確定我們要操作的裝置,因為在open函數中,我們將代表裝置的結構體儲存到了filp的private_data欄位中。
buf
參數是一個指向使用者空間的buffer的指標(buf前面的__user表示使用者空間),對於read來說,就是可以把要傳送給外部應用程式的資料放入這
個buffer中。當然,我們不能簡單地將資料copy到這個buffer中,而是要使用Linux核心提供的一些函數,比如copy_to_user函
數。
count是請求傳送的資料長度。
f_pos是一個指向“long offset type”對象的指標,指明外部應用程式在檔案中進行存取操作的位置。
read函數的傳回值是有符號整數類型的指,一般是read操作的實際存取數。
3、release函數
release函數的作用正好與open相反,有時候release函數的實現被稱為device_close,而不是device_release。但無論那種形式,這個裝置方法都應該完成如下任務:
a、釋放open分配的,儲存在file->private_data中的所有內容。
b、在最後一次關閉操作時關閉裝置。
relese函數由close系統調用引起,但並不是每一次close系統調用都
會引起release函數的調用。只有那些真正釋放裝置資料結構的close系統調用才會引起release函數的調用。因為Linux核心為每個
file結構體維護其被引用多少次的計數器,只有當file結構體的計數器歸0時,close系統調用才會引用release函數,這隻在刪除這個結構時
才會發生,因此每次open驅動程式都只會看到一次對應的一次release調用。
四、 卸載驅動module
每個重要的模組都需要一個清除函數,該函數在模組被移除前登出介面並向系統中返回所有資源。如果一個模組未定義清除函數,則核心不允許卸載該模組。
Linux
驅動module的卸載可以用/sbin/rmmod
scull.ko命令,這時Linux核心就會調用驅動程式中用module_exit(scull_cleanup_module)特殊宏定義的清除函
數,也就是說,module_exit聲明用來協助Linux核心找到模組的清除函數,在scull程式中,清除函數就是
scull_cleanup_module函數。
模組的清除函數需要撤銷初始化函數註冊的所有資源,並且習慣上(但不是必須的)以與初始化函數注
冊相反的順序進行撤銷。需要注意的是,這裡指的初始化函數是指用module_init宏聲明的初始化函數,而不是指open函數,與open函數對應的
應當是release函數。
在scull程式中,清除函數主要做了兩件事:一是free了所有為scull裝置分配的記憶體;二是收回了初始化函數所註冊的裝置號。