樹莓派的內核模塊編程之字符設備驅動

引言

在linux系統中,硬件設備是通過特殊的設備文件與內核進行通信,這些設備文件根據所傳輸的數據量與傳輸速度被分為字符設備(character device)與塊設備(block device),並且都被放在/dev目錄下。本文主要介紹字符設備的相關內容,並說明如何在樹莓派上編寫字符設備驅動。

一、字符設備與塊設備

linux系統通過特殊的設備文件來控制硬件設備與linux內核之間的通信,這些設備文件統一放在/dev目錄下。在linux系統中設備文件分為兩類,分別是字符設備(character device)與塊設備(block device)。字符設備的特點是數據傳輸速度較慢、數據傳輸量小並且數據的查詢頻率較低。通常來說,鍵盤、鼠標與聲卡硬件設備等都屬於字符設備。與字符設備相反,塊設備的特點是數據傳輸量較大並且數據搜索頻率較高,塊設備通常包括硬盤驅動、cdrom與ramdisk。

此外,linux內核分別針對字符設備與塊設備提供了兩種不同的API(應用編程接口)。對於字符設備而言,由於其傳輸的數據量小,字符設備的系統調用直接與字符設備驅動通信。塊設備則不同,用戶空間與字符設備驅動的通信是通過文件管理子系統與塊設備子系統來完成的。這兩個子系統的作用是為塊設備驅動準備所需的系統資源(例如緩存空間),從而保證讀寫的數據有足夠的緩存空間,同時也負責合理地安排設備驅動的讀寫操作,從而實現更高的讀寫性能。

本文主要介紹字符設備的相關內容,並通過一個簡單的樹莓派字符設備驅動器來演示如何編寫字符設備驅動。

二、設備的主次設備號

在linux系統中,用戶空間可以通過文件系統中的文件名與字符設備進行通信,這裡的文件名通常被稱為特殊文件、設備文件或者文件系統樹(file system tree)中的節點。根據linux系統管理慣例,設備文件通常存放在/dev目錄下,如果在/dev目錄下輸入命令ls -l命令,我們可以看到如下圖所示的結果。如圖所示,輸出結果第一列只有字母c和b兩種情況,其中字母c表示字符設備,字母b表示塊設備。

樹莓派的內核模塊編程之字符設備驅動

此外,在上圖中我們還可以看到每個設備條目中日期前面有兩個數字,這些是設備的主次設備號。例如,120是chardev的主設備號(MajorNumber),1是chardev的次設備號(Minor Number)。通常來說,主設備號用於表示設備文件所使用的驅動,次設備號用於標識同一個驅動所服務的不同設備文件。

linux內核使用dev_t數據類型來存儲主次設備號,該數據類型的定義在<linux>中給出。dev_t有32位比特,其中12個比特存儲主設備號,20個比特存儲次設備號。如果需要獲得主次設備號,則需要通過<linux>中定義的兩個函數來獲取,分別是:MAJOR(dev_tdev)與MINOR(dev_t dev)。反之,如果需要將自定義的主次設備號轉換成dev_t類型,則可以使用函數MKDEV(int major, int minor)。/<linux>/<linux>

三、主次設備號的分配與釋放

實現字符設備驅動的第一步是分配與該驅動關聯的主次設備號,完成這一操作的函數定義在<linux>中,函數名為register_chrdev_region,函數原型如下:/<linux>

intregister_chrdev_region(dev_t first, unsigned int count, char *name)

在register_chrdev_region函數中,參數first是字符設備所使用設備號中的第一個設備號,通常來說first參數的次設備號部分為0,但這並不是強制要求。參數count是所請求設備號的總個數。參數name是與設備號相關聯的字符設備名稱,該名稱將會出現在/proc/devices與sysfs中。與大多數內核函數一樣,如果register_chrdev_region函數的返回值為0,說明設備號分配成功,如果該函數返回負值,則說明設備號分配失敗。

值得一提的是,使用register_chrdev_region函數需要提前知道系統當前未使用的主設備號,如果出現主設備號衝突的情況會造成設備號分配失敗。/proc/devices文件列舉了正在使用的主設備號,從中選擇一個未使用的主設備號即可。

當需要卸載字符設備驅動時,需要釋放剛才所獲取的設備號,可以通過unregister_chrdev_region函數來完成該操作,函數原型如下:

unregister_chrdev_region(dev_tfirst, unsigned int count)

通常unregister_chrdev_region函數放置在模塊的退出函數中。

四、字符設備的重要數據結構體

本節主要介紹與字符設備驅動緊密相關的三個數據結構體,分別是file_operations,file與inode。

file_operations

file_operations結構體的定義在<linux>中給出,主要作用是將字符設備驅動的操作函數與字符設備的設備號相關聯。下面我們會介紹該結構體中重要的成員,在這些成員中會發現一些指針參數包含一個特殊的字符串__user,該字符串主要作用是告知大家該指針指向用戶空間的地址,不能被內核解析。在模塊程序編譯的過程中,__user並不會產生任何實際作用,但可以幫助linux內核找出用戶空間的地址誤用錯誤。/<linux>

下面我們介紹file_operations結構體中重要的成員:

struct module *owner

file_operations中的第一個成員並不是一個操作函數,而是一個指向file_operations所有者模塊的指針,該字段的目的是為了防止file_operations中操作函數在執行過程中被卸載。通常來說,該字段被初始化為THIS_MODULE,其定義在<linux>中給出。/<linux>

ssize_t (*read) (struct file *,char __user *, size_t, loff_t *)

該函數的功能是從內核中獲取數據。如果在該函數指針為空的情況下字符設備驅動調用了該函數,則會產生-EINVAL錯誤。正常運行的情況下,該函數的返回值為正數,表示成功讀入的字節數。

ssize_t (*write) (struct file *,const char __user *, size_t, loff_t)

該函數的功能是向內核中寫入數據。如果該函數指針為空時字符設備驅動調用了該函數,則會產生-EINVAL錯誤。正常運行的情況下,該函數的返回值為正數,表示成功寫入的字節數。

int (*open) (struct inode *,struct file *)

該函數通常是字符設備驅動所進行的第一個操作,與讀寫函數不同的是,該函數指針可以為空。

int (*release) (struct inode *)

該方法通常在file數據結構體被釋放時使用,與open類似,release可以為空指針。

file

file結構體的定義在<linux>中給出的,該結構體表示一個打開的文件,由open函數創建,並以參數的形式傳遞給其他函數。當調用close函數後,該結構體會被釋放。值得注意的是,這裡的file結構體與用戶空間的FILE指針無任何關係,FILE是標準C函數庫中定義的指針,絕不會出現在內核中。file結構體中重要的成員如下:/<linux>

mode_t f_mode

文件的模式通過FMODE_READ與FMODE_WRITE表明了該文件是可讀、可寫還是可讀寫的。

loff_t f_pos

該成員表示了當前文件讀寫的位置。loff_t有64位比特,字符設備驅動可以通過read函數與write函數來獲取當前文件的位置。我們不能直接更改該參數的取值,只能通過read函數與write函數來更新該取值。

struct file_operations *f_op

該成員說明了與file相關聯的操作函數。f_op中的取值並不會保存在內核中,這意味著我們可以在根據設備文件的不同來改變文件操作函數,也說明我們可以在不增加系統調用負載的情況下,在同一個主設備號中使用多種文件操作行為。

void *private_data

在調用open方法前,該指針會被設置為空。這裡我們可以隨便設置該成員,我們可以讓該指針指向已分配好的數據,但我們需要記住在release方法中釋放該數據的存儲空間。private_data經常用於存儲系統調用的狀態信息。

inode

inode結構體主要用於在內核中表示文件,與file不同的是,file表示的是已經打開的文件,也就是說我們可以針對同一個文件建立多個file結構體,但所有這些file結構體都指向同一個inode結構體。

inode結構體中包含大量與文件相關的信息,但通常我們只關心如下兩個成員:

dev_t i_rdev

該成員存儲了設備文件的真實設備號。

struct cdev *i_cdev

cdev結構體在內核中表示字符設備。

此外,內核還提供了兩種宏從inode中獲取主次設備號,分別是:

unsigned int iminor(struct inode*inode)

unsigned int imajor(struct inode*inode)

五、字符設備的註冊

正如之前所述,內核中使用cdev結構體代表字符設備。在調用字符設備的相關操作函數前,我們首先要分配並註冊一個或多個cdev結構體,cdev的相關注冊函數包含在<linux>中。/<linux>

目前有兩種方法可以分配並初始化cdev結構體,如果需要在模塊運行過程中初始化一個獨立的cdev結構體,可以通過如下代碼實現:

struct cdev *my_dev =cdev_alloc();

my_cdev -> ops = &my_fops;

有時候我們也需要將cdev嵌入在我們自己的設備結構體中,在這種情況下我們需要提前初始化cdev結構體,初始化方法如下:

void cdev_init(struct cdev *cdev,struct file_operation *fops);

無論採用上述哪種方法來初始化cdev結構體,cdev結構體中的owner成員都應該被設置為THIS_MODULE。

完成cdev結構體的初始化後,最後一步就是要告訴內核關於該結構體的信息,我們可以使用如下函數:

int cdev_add(struct cdev *dev,dev_t num, unsigned int count)

這裡dev是cdev結構體,num是該設備的第一個設備號,count該設備所關聯的所有設備數量,通常來說count被設置為1。如果cdev_add返回值為負數,說明該設備並未加入到系統內核中。此外我們還應該確保我們的驅動器可以處理所需的所有操作後,再調用cdev_add。

如果要將字符設備從系統中移除,則需要調用如下函數:

void cdev_del(struct cdev *dev)

六、字符設備的重要方法

open方法

open方法主要用於驅動器在準備階段完成初始化操作,為以後的操作函數提供便利。在大多數驅動器中,open方法應該完成如下任務:

  1. 檢查設備相關的錯誤;
  2. 如果設備是第一次被啟動,則初始化設備;
  3. 更新f_op指針;
  4. 分配並填充需要放入private_data中的數據。

release方法

release方法的功能是逆轉open方法,有時我們也會發現該方法會調用device_close,而不是device_release。不管用何種方法,release方法主要完成如下任務:

  1. 撤銷private_data;
  2. 關閉設備。

read方法與write方法

read與write方法所進行的操作類似,即完成內核空間與用戶空間的數據交互,這兩個方法的函數原型如下:

ssize_t read(struct file *filp,char __user *buff, size_t count, loft_t *offp)

ssize_t write(struct file *filp,const char __user *buff, size_t count, loff_t *offp)

上述兩種方法中,filp是文件指針,count是所需傳遞的數據量,buff參數指向了緩存讀寫數據的用戶緩存空間,offp表明了當前用戶接入文本的位置。

值得注意的是,read與write方法中的buff參數是用戶空間的指針,內核並不能直接解析該地址。因此我們需要通過內核提供的函數來讀取buff參數所指向的緩存空間。這裡我們可以使用兩個定義在中的內核函數,分別是put_user()函數與get_user()函數。對於實際的設備方法而言,read方法的任務是將數據從設備文件讀取到用戶空間(使用put_user),而write方法則是將數據從用戶空間寫入到設備(使用get_user)。

七、示例程序

字符設備驅動代碼下載方式如下:

鏈接:https://pan.baidu.com/s/13-0DWZY17huWPFRVjkY64g

提取碼:kqyj

將上述代碼下載至樹莓派後,按下圖命令運行即可:

樹莓派的內核模塊編程之字符設備驅動



分享到:


相關文章: