Linux 2.6 字符設備驅動程序

 

Linux 2.6字符設備驅動程序

、說明
筆記適用於Linux2.6.10以後的內核。
筆記以Linux
Device
Driver3
提供的scull程序(scull目錄中的main.cscull.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提供了兩個命令用來加載:modprobeinsmod

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這樣的命令加載驅動時,whomhowmay的值就會傳入scull驅動模塊了。
驅動程序module被加載後,若對設備進行操作(如openreadwrite等),驅動module就會調用相應的函數響應該操作。
那麼,當對設備進行操作時,驅動module又怎麼知道是自己應該有所響應,而不是其他的驅動module呢,也就是說,Linux內核怎麼知道應該調用哪一個驅動module呢?

前我只知道有兩種方式將設備與驅動module聯繫在一起(也許應該說提供訪問設備的一種途徑比較恰當):其一是通過某些設備的ID(比如PCI設備和
USB
設備的Device IDProduct
ID
),Linux內核根據這些ID調用驅動module;其二是在/dev目錄下根據設備的主次設備號創建對應的設備節點(即設備文件),這樣當操作

/dev
目錄下的設備文件時,就會調用相應的驅動module
二、驅動module的初始化
使用insmod加載驅動module時,需要讓驅動module爲設備做一些初
始化動作,主要目的是讓Linux內核知道這個設備(或者說module?),以及在以後對該設備進行操作(如openreadwrite等等)時,
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/devicessysfs中。

如,若first0x3FFFF0count0x5,那麼該函數就會爲5個設備註冊設備號,分別是0x3FFFF00x3FFFF1
0x3FFFF2
0x3FFFF30x3FFFF4,其中0x3(高12位)爲這5個設備所共有的主設備號(也就是說這5個設備都使用同一個驅動程
序)。而0xFFFF00xFFFF10xFFFF20xFFFF30xFFFF4就分別是這5個設備的次設備號了。
需要注意的是,若
count
的值太大了,那麼所請求的設備號範圍可能會和下一個主設備號重疊。比如若first還是爲0x3FFFF0,而count0x11,那麼
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_loadscull_unload,可以在動態分配的情況下爲設備創建和刪除設備節點。其實它也是利用了
awk
工具從/proc/devices中獲取了信息,然後才用mknod/dev下創建設備節點。
其實scull_loadscull_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結構體,包括qsetqutuam,信號量sem以及cdev等字段。其中qsetqutuam的初始化對於Linux驅動程序的知識點來說毫不相關,因此不加討論。

只要知道,在加載module時所調用的module初始化函數中,可以初始化一些設備相關的變量。但是根據Linux Device
Drvier3
作者的意思,設備相關的變量或者一些資源最好應當在open函數中初始化,比如像中斷號等,雖然在module初始化函數中註冊也是允許

的,但最好是在第一次打開設備,也就是open函數中再行分配。
3初始化互斥體init_MUTEX
互斥體MUTEX,也就是信號量的一個變種,與completion,自旋鎖spinlock等等都與驅動中的併發和競態相關,以後再說。

4
初始化在內核中代表設備的cdev結構體
其實在Linux內核中,cdev結構體纔是真正代表了某個設備。在內核調用設備的openread等操作之前,必須先分配並註冊一個或者多個cdev結構。

想可以這麼理解,主次設備號是涉外的,主要用來在與外部(指的是驅動moduleLinux內核以外)交互時確定身份;而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_operationsscull_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的加載而進行了初始化之後,就會進入潛伏狀態,也就是說,如果沒有系統調用(openread),那麼module中定義的其他函數就絕不會運行!
這裏所說的設備操作,是指當有系統調用到達驅動module時,module就該調用某個或某些函數有所動作。
對於驅動開發來說,我主要關心的只有一點,那就是系統調用怎樣把一些外部應用程序中的變量值傳遞給驅動module
scull
程序中與設備操作相關的函數主要分爲三類:初始化函數,實際的操作服務函數和清理函數。其中初始化函數只有一個,就是open函數,而操作服務函數則包括readwritellseek等等函數,至於清理函數則是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函數原型中,有inodefilp兩個參數,都是外部應用程序在操作
設備時通過調用系統調用傳遞給驅動module的。於是驅動module就可以通過這兩個參數來確定要打開的具體設備了。其實這裏所說的具體設備,並不是
說驅動module需要從系統安裝的所有設備中確定它所要服務的設備,而是指module需要從某一類擁有相同主設備號的設備中確定它要服務的設備。

所以這麼說,是因爲驅動module是對應於某一個主設備號的所有設備的。換一句話說,就是Linux內核只管設備的主設備號,而不理會設備的次設備號是
什麼,如果有兩個,三個或者更多個設備擁有同一個主設備號,那麼不管外部應用程序要操作這些設備中的哪一個,Linux內核都只會調用同一個驅動
module
。但是驅動module卻不能不管次設備號了,因爲它是跟某一個具體的設備打交道的,所以它需要根據open系統調用時傳遞給它的參數中找到
次設備號,從而確定那唯一的一個設備(也許驅動module也可以同時操作幾個設備,但一時也想不起來)。
但是上面所說的通過次設備號找到具體的設備,只是其中一種方法;另外還有一種方法就是通過cdev結構體確定某個具體設備。

備所擁有的cdev結構體,或者次設備號,都保存在open函數的inode參數中。我們可以使用container_of宏通過inode所擁有的
cdev
確定具體設備,也可以使用iminor宏從inode所擁有的i_rdev確定次設備號(i_rdevinode結構體中的一個dev_t類型
的變量,其中保存了真正的設備主次編號)
對於open函數中的file參數,scull程序主要用它來做兩件事:其一是將
根據cdev獲得的代表設備的scull_dev結構體保存到file->private_data中,這樣就可以方便今後對該設備結構體的訪問
了,而不用每次都調用container_of宏或者iminor宏來找到設備結構體了;其二是根據file結構體中的f_flags字段來確定,這次的
open
調用,是以寫方式打開設備,還是以讀方式來打開設備。
2read函數
驅動modulefile_operations結構體中可以定義很多設備操作服務函數,但是我現在關心的是這些函數怎樣與系統調用,或者說是外部應用程序交互,而不管具體的設備操作怎麼實現,所以只記錄read函數作爲代表。
read函數的原型如下:
ssize_t read(struct file *filp,  char __user *buf,  size_t count,  loff_t *f_pos)

read
函數原型中有4個參數,分別是filpbufcountf_pos
其中file結構體指針參數可以用來確定我們要操作的設備,因爲在open函數中,我們將代表設備的結構體保存到了filpprivate_data字段中。
buf
參數是一個指向用戶空間的buffer的指針(buf前面的__user表示用戶空間),對於read來說,就是可以把要傳送給外部應用程序的數據放入這
buffer中。當然,我們不能簡單地將數據copy到這個buffer中,而是要使用Linux內核提供的一些函數,比如copy_to_user
數。
count
是請求傳送的數據長度。
f_pos
是一個指向“long offset type”對象的指針,指明外部應用程序在文件中進行存取操作的位置。
read
函數的返回值是有符號整數類型的指,一般是read操作的實際存取數。
3release函數
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設備分配的內存;二是收回了初始化函數所註冊的設備號。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章