Linux Driver

2015年5月13日 星期三

在Raspberry Pi & Pi2 上面學 Linux 驅動程式開發 -- 字元驅動裝置 --

實驗名稱

    Linux 驅動程式開發 -- 字元驅動裝置 --

實驗目的

    介紹字元驅動裝置在 Linux Kernel 裡面的作用和使用方式。

使用材料及設備

    硬體: Raspberry Pi B+

    軟體: 可編譯 Linux Module 的 Raspbian 系統。

原理介紹

字元裝置驅動程式為 Linux 系統模組和使用者空間之間,資料交換最為常用的方式。字元裝置驅動程式會以檔案的形式呈現,通常來說會檔案在建立之時會選擇統一在系統裝置資料夾 /dev/ 之中。

程式說明

程式碼內容

---------------- CODE -----------------

---------------- CODE -----------------

程式架構

整個程式碼架構的進入點為函式 init_module(),而離開點為函式 cleanup_module()。

在模組的進入點函式之中,先是向系統註冊了一組驅動裝置號碼,並初始化了一個字元裝置檔變數,並且把驅動裝置號碼和字元裝置註冊進系統之中。

程式架構之中多了檔案操作的進入點 my_open() 和離開點 my_close() 。

從模組傳遞資料到使用者空間程式的檔案操作函式 my_read() 。

從使用者空間傳遞資料到模組的檔案操作函式 my_write() 。


程式碼說明


Linux 系統驅動程式之中提供了字元裝置,讓使用者空間的程式可以透過該介面和核心做資料或指令的傳遞。

使用字元裝置驅動程式,模組程式需要向系統註冊一組裝置號碼。同時使用者空間需要在檔案系統路徑 /dev/ 裡產生一個檔案作為介面和該組裝置號碼作連結。

一但產生連結之後,使用者空間的程式就可以透過 open() 、 close() 等系統呼叫(system call) 來和模組產生溝通。

因此模組程式需要利用系統提供的工具函式向核心註冊一組裝置號碼(此處為 devno ),其資料型態為 dev_t 。然後再產生一個字元裝置檔變數(此處為 my_cdev) ,其資料形態為 struct cdev 。之後再裝置號碼和字元裝置檔變數一同註冊進去系統之中,如此一來就可以透過檔案的形式來和模組做資料溝通。

註冊裝置號碼 devno :

在模組的進入點函式 init_moudle() 裡,會先進行註冊裝置號碼的動作。裝置號碼分主要裝置號碼和次要裝置號碼,在這邊是自行決定一個主要裝置號碼為系統中尚未被註冊的號碼。可以看到模組中用下面的巨集將主要和次要裝置號碼設定給裝置號碼變數。

devno = MKDEV(MY_MAJOR, MY_MINOR);

然後向系統核心註冊該組號碼組合

register_chrdev_region(devno, count, "myLED");

產生字元裝置檔變數 my_cdev:

我們會先宣告一個全域變數

struct cdev my_cdev;

然後在模組的進入點函式 init_moudle() 裡使用 cdev_init() 函式對其初始化,初始化的同時要設定一組檔案操作函式註冊檔(變數名稱 my_fops ) :

cdev_init(&my_cdev, &my_fops);

檔案操作函式註冊檔,是一個資料結構為 struct file_operations 的變數。其中宣告了Linux 一系列的系統呼叫函式的指標,如範例中所用到的 open()、write()、read()、 close() 。 

視模組欲支援哪些系統呼叫函式,須實作該函式並且將其設定給對應的系統函式指標。舉例來說,該模組欲支援檔案操作功能: open()、write()、read()、close(),所以實作了函式 my_open()、my_read()、my_write()、my_close()。並將此四個實作出來的函式指標設給檔案操作函式註冊檔 my_fops 中對應的成員函式指標 open、write、read、close。並在設定完之後,交由函式 cdev_init() 掛載至對應的字元裝置檔。  

註冊字元裝置檔:

當向系統註冊完一組裝置號碼( devno ),並且設定好字元裝置檔變數 ( my_cdev ) 之後。接下來就是利用函式 cdev_add() 將其設定一同註冊到系統之中,以完成註冊字元驅動裝置的動作:

err = cdev_add(&my_cdev, devno, count);

如註冊失敗,傳回的 err 值會小於 1 。

在完成註冊字元驅動裝置的動作之後,使用者空間的程式碼就可以透過字元裝置檔來和核心中的模組程式進行溝通。而系統也提供系統核心呼叫 (system call) 函式庫,讓使用者進行溝通的動作,例如此次的四個檔案操作函式: open()、close()、read()、write()。

在檔案系統中製作裝置檔


當在模組中完成註冊字元裝置檔之後,該模組即開支在核心中支援實作的檔案操作函式呼叫。但是在檔案系統之中並不會自動出現代表該模組的裝置檔,使用者空間的程式在此時仍然無法和該模組進行資料與指令的溝通,一般來說都會集中在系統目錄 /dev/ 底下。 

在完成註冊裝置號碼之後,變數 devno 即代表著該模組註冊在系統核心中的一組裝置號碼 。裝置號碼分為主要裝置號碼和次要裝置號碼。主要裝置號碼代表該模組,次要裝置號碼通常用來區別該模組所管理不同裝置。(一個模組可以控制多個裝置)在系統核心中,為區別不同的模組,主要裝置號碼都不得重覆。次要裝置號碼則無此限制,但交由模組自行管理。

使用者空間程式要使用模組所支援的系統檔案操作函式與之溝通,必須透過系統裝置檔案來進行。而該系統裝置檔案須和該模組的主要裝置號碼對應,此模組的主要裝置號碼為 200 。

而目前,系統裝置檔須用系統指令 mknod 來產生: (當然必須有系統管理者權限)

mknod /dev/<裝置檔案名稱> c <主要裝置號碼> <次要裝置號碼>

此模組申請了裝置數量為兩個,而次要裝置號碼為 0 (由0開始往上算),所以次要裝置號碼有 0 和 1 。故我們可以在目錄 /dev/ 下製作兩個裝置檔 my_dev0 和 my_dev1 :

$> sudo mknod /dev/my_dev0 c 200 0
$> sudo mknod /dev/my_dev1 c 200 1 

如此一來,我們可以撰寫一個簡單的程式來測試其實作的四個系統檔案操作:

-------
-------

到目前為止,我們已說明了如何在模組中進行註冊字元裝置,在使用者空間製作字元裝置檔。以下開始說明模組中的四個基本檔案操作函式要如何進行實作。


系統檔案操作函式 open: int my_open(struct inode * inod, struct file *filp)

系統檔案操作函式是用來開啟裝置檔案用的。在實作系統檔案操作函式 open() 對應的實作函式 my_open() 時,要注意其函式的參數介面必須符合上一行的形式。而在系統呼叫該函式的時候,會透過此參數介面傳入必要的資訊。例如,在呼叫到open: int my_open(...)時,系統會傳入一個struct inode * inod。而我們可以透過該傳入的參數 inod 以及函式 iminor() 得知,使用者空間的程式是透過哪一個次要裝置號碼對應的裝置檔案來呼叫系統檔案操作函式 open():

int major = imajor(inod);
int minor = iminor(inod);

因此模組檔的 my_open() 只是印出主要裝置號碼和次要裝置號碼之後就離開:

printk("\n*****Some body is opening me at major %d  minor %d*****\n",major, minor);


系統檔案操作函式 read: my_read(struct file * filp , char * buff, size_t len, loff_t * off)

該系統檔案操作函式是用來將模組的資料傳遞給使用者空間程式用的。類似實作 open: my_open() 函式,系統同樣會透過該參數介面傳入必須的資訊給模組。而在此,要得到主要裝置號碼和次要裝置號碼是透過傳入的 struct file * filp 參數:

int major = MAJOR(filp->f_dentry->d_inode->i_rdev);
int minor = MINOR(filp->f_dentry->d_inode->i_rdev); 

而傳入參數中的 char * buff 和 size_t len 則分別對應著使用者空間程式呼叫 read(file_p, buff, len) 的 buff 和 len 。而在模組中必須透過函式 copy_to_user() 來將資料傳遞給使用者空間的 buff:

int count = copy_to_user( buff, msg, len);



系統檔案操作函式 write: my_write(struct file * filp , char * buff, size_t len, loff_t * off)

該系統檔案操作函式是用來將使用者空間程式資料傳遞給用模組如同系統檔案操作函式 read(): my_read(),得到主要裝置號碼和次要裝置號碼是透過傳入的 struct file * filp 參數:

int major = MAJOR(filp->f_dentry->d_inode->i_rdev);
int minor = MINOR(filp->f_dentry->d_inode->i_rdev); 

而write(): my_write() 則是要複製使用者空間的資料到模組中,所以該用函式copy_from_user():

int count = copy_from_user( buff, msg, len);

系統檔案操作函式 close: my_close(struct inode * inod, struct file * filp)

該系統檔案操作函式是用來關閉該檔案用。欲得知使用者空間的程式是透過哪一個裝置號碼對應的檔案來呼叫系統檔案操作函式 open():

int major = imajor(inod);
int minor = iminor(inod);

執行結果



沒有留言 :

張貼留言