字符設(shè)備驅(qū)動
本章,我們將學習字符設(shè)備使用、字符設(shè)備驅(qū)動相關(guān)的概念,理解字符設(shè)備驅(qū)動程序的基本框架,并從源碼上分析字符設(shè)備驅(qū)動實現(xiàn)和管理等。 主要分為下面五部分:
Linux設(shè)備分類;
字符設(shè)備的抽象,字符設(shè)備設(shè)計思路;
字符設(shè)備相關(guān)的概念以及數(shù)據(jù)結(jié)構(gòu),了解設(shè)備號等基本概念以及file_operations、file、inode相關(guān)數(shù)據(jù)結(jié)構(gòu);
字符字符設(shè)備驅(qū)動程序框架,例如內(nèi)核是如何管理設(shè)備號的;系統(tǒng)關(guān)聯(lián)、調(diào)用file_operation接口,open函數(shù)所涉及的知識等等。
設(shè)備驅(qū)動程序?qū)嶒灐?/p>
1. Linux設(shè)備分類?
linux是文件型系統(tǒng),所有硬件都會在對應的目錄(/dev)下面用相應的文件表示。 在windows系統(tǒng)中,設(shè)備大家很好理解,像硬盤,磁盤指的是實實在在硬件。 而在文件系統(tǒng)的linux下面,都有對于文件與這些設(shè)備關(guān)聯(lián)的,訪問這些文件就可以訪問實際硬件。 像訪問文件那樣去操作硬件設(shè)備,一切都會簡單很多,不需要再調(diào)用以前com,prt等接口了。 直接讀文件,寫文件就可以向設(shè)備發(fā)送、接收數(shù)據(jù)。 按照讀寫存儲數(shù)據(jù)方式,我們可以把設(shè)備分為以下幾種:字符設(shè)備、塊設(shè)備和網(wǎng)絡設(shè)備。
字符設(shè)備:指應用程序按字節(jié)/字符來讀寫數(shù)據(jù)的設(shè)備。 這些設(shè)備節(jié)點通常為傳真、虛擬終端和串口調(diào)制解調(diào)器、鍵盤之類設(shè)備提供流通信服務, 它通常不支持隨機存取數(shù)據(jù)。字符設(shè)備在實現(xiàn)時,大多不使用緩存器。系統(tǒng)直接從設(shè)備讀取/寫入每一個字符。 例如,鍵盤這種設(shè)備提供的就是一個數(shù)據(jù)流,當你敲入“cnblogs”這個字 符串時, 鍵盤驅(qū)動程序會按照和輸入完全相同的順序返回這個由七個字符組成的數(shù)據(jù)流。它們是順序的,先返回c,最后是s。
塊設(shè)備:通常支持隨機存取和尋址,并使用緩存器。 操作系統(tǒng)為輸入輸出分配了緩存以存儲一塊數(shù)據(jù)。當程序向設(shè)備發(fā)送了讀取或者寫入數(shù)據(jù)的請求時, 系統(tǒng)把數(shù)據(jù)中的每一個字符存儲在適當?shù)木彺嬷?。當緩存被填滿時,會采取適當?shù)牟僮?把數(shù)據(jù)傳走), 而后系統(tǒng)清空緩存。它與字符設(shè)備不同之處就是,是否支持隨機存儲。字符型是流形式,逐一存儲。 典型的塊設(shè)備有硬盤、SD卡、閃存等,應用程序可以尋址磁盤上的任何位置,并由此讀取數(shù)據(jù)。 此外,數(shù)據(jù)的讀寫只能以塊的倍數(shù)進行。
網(wǎng)絡設(shè)備:是一種特殊設(shè)備,它并不存在于/dev下面,主要用于網(wǎng)絡數(shù)據(jù)的收發(fā)。
Linux內(nèi)核中處處體現(xiàn)面向?qū)ο蟮脑O(shè)計思想,為了統(tǒng)一形形色色的設(shè)備,Linux系統(tǒng)將設(shè)備分別抽象為struct cdev, struct block_device,struct net_devce三個對象,具體的設(shè)備都可以包含著三種對象從而繼承和三種對象屬性和操作, 并通過各自的對象添加到相應的驅(qū)動模型中,從而進行統(tǒng)一的管理和操作
字符設(shè)備驅(qū)動程序適合于大多數(shù)簡單的硬件設(shè)備,而且比起塊設(shè)備或網(wǎng)絡驅(qū)動更加容易理解, 因此我們選擇從字符設(shè)備開始,從最初的模仿,到慢慢熟悉,最終成長為驅(qū)動界的高手。
2. 字符設(shè)備抽象?
Linux內(nèi)核中將字符設(shè)備抽象成一個具體的數(shù)據(jù)結(jié)構(gòu)(struct cdev),我們可以理解為字符設(shè)備對象, cdev記錄了字符設(shè)備的相關(guān)信息(設(shè)備號、內(nèi)核對象),字符設(shè)備的打開、讀寫、關(guān)閉等操作接口(file_operations), 在我們想要添加一個字符設(shè)備時,就是將這個對象注冊到內(nèi)核中,通過創(chuàng)建一個文件(設(shè)備節(jié)點)綁定對象的cdev, 當我們對這個文件進行讀寫操作時,就可以通過虛擬文件系統(tǒng),在內(nèi)核中找到這個對象及其操作接口,從而控制設(shè)備。
C語言中沒有面向?qū)ο笳Z言的繼承的語法,但是我們可以通過結(jié)構(gòu)體的包含來實現(xiàn)繼承,這種抽象提取了設(shè)備的共性, 為上層提供了統(tǒng)一接口,使得管理和操作設(shè)備變得很容易。
在硬件層,我們可以通過查看硬件的原理圖、芯片的數(shù)據(jù)手冊,確定底層需要配置的寄存器,這類似于裸機開發(fā), 將對底層寄存器的配置,讀寫操作放在文件操作接口里面,也就是實現(xiàn)file_operations結(jié)構(gòu)體; 在驅(qū)動層,我們將文件操作接口注冊到內(nèi)核,內(nèi)核通過內(nèi)部散列表來登記記錄主次設(shè)備號; 在文件系統(tǒng)層,新建一個文件綁定該文件操作接口,應用程序通過操作指定文件的文件操作接口來設(shè)置底層寄存器。
實際上,在Linux上寫驅(qū)動程序,都是做一些“填空題”。因為Linux給我們提供了一個基本的框架, 我們只需要按照這個框架來寫驅(qū)動,內(nèi)核就能很好的接收并且按我們所要求的那樣工作。有句成語工欲善其事,必先利其器, 在理解這個框架之前我們得花點時間來學習字符設(shè)備驅(qū)動相關(guān)概念及數(shù)據(jù)結(jié)構(gòu)。
3. 相關(guān)概念及數(shù)據(jù)結(jié)構(gòu)?
在linux中,我們使用設(shè)備編號來表示設(shè)備,主設(shè)備號區(qū)分設(shè)備類別,次設(shè)備號標識具體的設(shè)備。 cdev結(jié)構(gòu)體被內(nèi)核用來記錄設(shè)備號,而在使用設(shè)備時,我們通常會打開設(shè)備節(jié)點,通過設(shè)備節(jié)點的inode結(jié)構(gòu)體、 file結(jié)構(gòu)體最終找到file_operations結(jié)構(gòu)體,并從file_operations結(jié)構(gòu)體中得到操作設(shè)備的具體方法。
3.1. 設(shè)備號?
對于字符的訪問是通過文件系統(tǒng)的名稱進行的,這些名稱被稱為特殊文件、設(shè)備文件,或者簡單稱為文件系統(tǒng)樹的節(jié)點, Linux根目錄下有/dev這個文件夾,專門用來存放設(shè)備中的驅(qū)動程序,我們可以使用ls -l 以列表的形式列出系統(tǒng)中的所有設(shè)備。 其中,每一行表示一個設(shè)備,每一行的第一個字符表示設(shè)備的類型。
文件類型的字符及其意義如下:
-:普通文件(regular file)
d:目錄(directory)
l:符號鏈接(symbolic link)
b:塊設(shè)備(block device)
p:管道(pipe)
s:套接字(socket)
如下圖:ashmem 是一個字符設(shè)備c, 它的主設(shè)備號是10,次設(shè)備號是124;initctl是一個符號鏈接,隨后的權(quán)限字符 rwxrwxrwx 表示該鏈接的所有者、同組用戶和其他用戶均有讀(r)、寫(w)和執(zhí)行(x)的權(quán)限;loop0 是一個塊設(shè)備,它的主設(shè)備號是7,次設(shè)備號為0,同時可以看到loop0-loop3共用一個主設(shè)備號,次設(shè)備號由0開始遞增。
一般來說,主設(shè)備號指向設(shè)備的驅(qū)動程序,次設(shè)備號指向某個具體的設(shè)備。如上圖,I2C-0,I2C-1屬于不同設(shè)備但是共用一套驅(qū)動程序
3.1.1. 內(nèi)核中設(shè)備編號的含義?
在內(nèi)核中,dev_t用來表示設(shè)備編號,dev_t是一個32位的數(shù),其中,高12位表示主設(shè)備號,低20位表示次設(shè)備號。 也就是理論上主設(shè)備號取值范圍:0-2^12,次設(shè)備號0-2^20。 實際上在內(nèi)核源碼中__register_chrdev_region(…)函數(shù)中,major被限定在0-CHRDEV_MAJOR_MAX,CHRDEV_MAJOR_MAX是一個宏,值是512。 在kdev_t中,設(shè)備編號通過移位操作最終得到主/次設(shè)備號碼,同樣主/次設(shè)備號也可以通過位運算變成dev_t類型的設(shè)備編號, 具體實現(xiàn)參看下面代碼MAJOR(dev)、MINOR(dev)和MKDEV(ma,mi)。
dev_t定義 (內(nèi)核源碼/include/types.h)
typedef u32 __kernel_dev_t;typedef __kernel_dev_t dev_t;
設(shè)備號相關(guān)宏 (內(nèi)核源碼/include/linux/kdev_t.h)
#define MINORBITS 20#define MINORMASK ((1U < MINORBITS) - 1)#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))#define MKDEV(ma,mi) (((ma) < MINORBITS) | (mi))
第4-5行:內(nèi)核還提供了另外兩個宏定義MAJOR和MINOR,可以根據(jù)設(shè)備的設(shè)備號來獲取設(shè)備的主設(shè)備號和次設(shè)備號。
第6行:宏定義MKDEV,用于將主設(shè)備號和次設(shè)備號合成一個設(shè)備號,主設(shè)備可以通過查閱內(nèi)核源碼的Documentation/devices.txt文件,而次設(shè)備號通常是從編號0開始。
3.1.2. cdev結(jié)構(gòu)體?
內(nèi)核通過一個散列表(哈希表)來記錄設(shè)備編號。 哈希表由數(shù)組和鏈表組成,吸收數(shù)組查找快,鏈表增刪效率高,容易拓展等優(yōu)點。
以主設(shè)備號為cdev_map編號,使用哈希函數(shù)f(major)=major%255來計算組數(shù)下標(使用哈希函數(shù)是為了鏈表節(jié)點盡量平均分布在各個數(shù)組元素中,提高查詢效率); 主設(shè)備號沖突,則以次設(shè)備號為比較值來排序鏈表節(jié)點。 如下圖所示,內(nèi)核用struct cdev結(jié)構(gòu)體來描述一個字符設(shè)備,并通過struct kobj_map類型的 散列表cdev_map來管理當前系統(tǒng)中的所有字符設(shè)備。
cdev結(jié)構(gòu)體(內(nèi)核源碼/include/linux/cdev.h)
struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count;} __randomize_layout;
struct kobject kobj: 內(nèi)嵌的內(nèi)核對象,通過它將設(shè)備統(tǒng)一加入到“Linux設(shè)備驅(qū)動模型”中管理(如對象的引用計數(shù)、電源管理、熱插拔、生命周期、與用戶通信等)。
struct module *owner: 字符設(shè)備驅(qū)動程序所在的內(nèi)核模塊對象的指針。
const struct file_operations *ops: 文件操作,是字符設(shè)備驅(qū)動中非常重要的數(shù)據(jù)結(jié)構(gòu),在應用程序通過文件系統(tǒng)(VFS)呼叫到設(shè)備設(shè)備驅(qū)動程序中實現(xiàn)的文件操作類函數(shù)過程中,ops起著橋梁紐帶作用,VFS與文件系統(tǒng)及設(shè)備文件之間的接口是file_operations結(jié)構(gòu)體成員函數(shù),這個結(jié)構(gòu)體包含了對文件進行打開、關(guān)閉、讀寫、控制等一系列成員函數(shù)。
struct list_head list: 用于將系統(tǒng)中的字符設(shè)備形成鏈表(這是個內(nèi)核鏈表的一個鏈接因子,可以再內(nèi)核很多結(jié)構(gòu)體中看到這種結(jié)構(gòu)的身影)。
dev_t dev: 字符設(shè)備的設(shè)備號,有主設(shè)備和次設(shè)備號構(gòu)成。
unsigned int count: 屬于同一主設(shè)備好的次設(shè)備號的個數(shù),用于表示設(shè)備驅(qū)動程序控制的實際同類設(shè)備的數(shù)量。
3.2. 設(shè)備節(jié)點?
設(shè)備節(jié)點(設(shè)備文件):Linux中設(shè)備節(jié)點是通過“mknod”命令來創(chuàng)建的。一個設(shè)備節(jié)點其實就是一個文件, Linux中稱為設(shè)備文件。有一點必要說明的是,在Linux中,所有的設(shè)備訪問都是通過文件的方式, 一般的數(shù)據(jù)文件程序普通文件,設(shè)備節(jié)點稱為設(shè)備文件。
設(shè)備節(jié)點被創(chuàng)建在/dev下,是連接內(nèi)核與用戶層的樞紐,就是設(shè)備是接到對應哪種接口的哪個ID 上。 相當于硬盤的inode一樣的東西,記錄了硬件設(shè)備的位置和信息在Linux中,所有設(shè)備都以文件的形式存放在/dev目錄下, 都是通過文件的方式進行訪問,設(shè)備節(jié)點是Linux內(nèi)核對設(shè)備的抽象,一個設(shè)備節(jié)點就是一個文件。 應用程序通過一組標準化的調(diào)用執(zhí)行訪問設(shè)備,這些調(diào)用獨立于任何特定的驅(qū)動程序。而驅(qū)動程序負責將這些標準調(diào)用映射到實際硬件的特有操作。
3.3. 數(shù)據(jù)結(jié)構(gòu)?
在驅(qū)動開發(fā)過程中,不可避免要涉及到三個重要的的內(nèi)核數(shù)據(jù)結(jié)構(gòu)分別包括文件操作方式(file_operations), 文件描述結(jié)構(gòu)體(struct file)以及inode結(jié)構(gòu)體,在我們開始閱讀編寫驅(qū)動程序的代碼之前,有必要先了解這三個結(jié)構(gòu)體。
3.3.1. file_operations結(jié)構(gòu)體?
file_operation就是把系統(tǒng)調(diào)用和驅(qū)動程序關(guān)聯(lián)起來的關(guān)鍵數(shù)據(jù)結(jié)構(gòu)。這個結(jié)構(gòu)的每一個成員都對應著一個系統(tǒng)調(diào)用。 讀取file_operation中相應的函數(shù)指針,接著把控制權(quán)轉(zhuǎn)交給函數(shù)指針指向的函數(shù),從而完成了Linux設(shè)備驅(qū)動程序的工作。
下面是部分常用的字符操作介紹
struct file_operations { struct module *owner; //擁有該結(jié)構(gòu)的模塊指針,一般有THIS_MODULES loff_t (*llseek) (struct file *, loff_t, int); //用來修改文件當前讀寫的位置 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //從設(shè)備中同步讀取數(shù)據(jù) ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//向設(shè)備發(fā)送數(shù)據(jù) ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); //初始化一個異步讀取操作 ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); //初始化一個異步寫操作 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//執(zhí)行設(shè)備的I/O控制命令 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);//64bit系統(tǒng)上,32bit的ioctl調(diào)用將使用此函數(shù)指針代替 int (*mmap) (struct file *, struct vm_area_struct *);//用于請求將設(shè)備內(nèi)存映射到進程地址空間 int (*open) (struct inode *, struct file *);//打開設(shè)備,獲取設(shè)備描述符 int (*flush) (struct file *, fl_owner_t id);//刷新設(shè)備數(shù)據(jù)流 int (*release) (struct inode *, struct file *);//關(guān)閉設(shè)備 int (*fsync) (struct file *, loff_t, loff_t, int datasync);//刷新待處理的數(shù)據(jù) int (*fasync) (int, struct file *, int); //通知設(shè)備fasync標志發(fā)生變化}
在系統(tǒng)內(nèi)部,I/O設(shè)備的存取操作通過特定的入口點來進行,而這組特定的入口點恰恰是由設(shè)備驅(qū)動程序提供的。 通常這組設(shè)備驅(qū)動程序接口是由結(jié)構(gòu)file_operations結(jié)構(gòu)體向系統(tǒng)說明的,它定義在ebf_buster_linux/include/linux/fs.h中。 傳統(tǒng)上, 一個file_operation結(jié)構(gòu)或者其一個指針稱為 fops( 或者它的一些變體). 結(jié)構(gòu)中的每個成員必須指向驅(qū)動中的函數(shù), 這些函數(shù)實現(xiàn)一個特別的操作, 或者對于不支持的操作留置為NULL。當指定為NULL指針時內(nèi)核的確切的行為是每個函數(shù)不同的。
上面,我們提到read和write函數(shù)時,需要使用copy_to_user函數(shù)以及copy_from_user函數(shù)來進行數(shù)據(jù)訪問,寫入/讀取成 功函數(shù)返回0,失敗則會返回未被拷貝的字節(jié)數(shù)。
copy_to_user和copy_from_user函數(shù)(內(nèi)核源碼/include/asm-generic/uaccess.h)
static inline long copy_from_user(void *to, const void __user * from, unsigned long n)static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
函數(shù)參數(shù)和返回值如下:
to:指定目標地址,也就是數(shù)據(jù)存放的地址,
from:指定源地址,也就是數(shù)據(jù)的來源。
n:指定寫入/讀取數(shù)據(jù)的字節(jié)數(shù)。
返回值
寫入/讀取數(shù)據(jù)的字節(jié)數(shù)
3.3.2. file結(jié)構(gòu)體?
內(nèi)核中用file結(jié)構(gòu)體來表示每個打開的文件,每打開一個文件,內(nèi)核會創(chuàng)建一個結(jié)構(gòu)體,并將對該文件上的操作函數(shù)傳遞給 該結(jié)構(gòu)體的成員變量f_op,當文件所有實例被關(guān)閉后,內(nèi)核會釋放這個結(jié)構(gòu)體。如下代碼中,只列出了我們本章需要了解的成員變量。
file結(jié)構(gòu)體(內(nèi)核源碼/include/fs.h)
struct file {{......}const struct file_operations *f_op;/* needed for tty driver, and maybe others */void *private_data;{......}};
f_op:存放與文件操作相關(guān)的一系列函數(shù)指針,如open、read、wirte等函數(shù)。
private_data:該指針變量只會用于設(shè)備驅(qū)動程序中,內(nèi)核并不會對該成員進行操作。因此,在驅(qū)動程序中,通常用于指向描述設(shè)備的結(jié)構(gòu)體。
3.3.3. inode結(jié)構(gòu)體?
VFS inode 包含文件訪問權(quán)限、屬主、組、大小、生成時間、訪問時間、最后修改時間等信息。 它是Linux 管理文件系統(tǒng)的最基本單位,也是文件系統(tǒng)連接任何子目錄、文件的橋梁。 內(nèi)核使用inode結(jié)構(gòu)體在內(nèi)核內(nèi)部表示一個文件。因此,它與表示一個已經(jīng)打開的文件描述符的結(jié)構(gòu)體(即file 文件結(jié)構(gòu))是不同的, 我們可以使用多個file文件結(jié)構(gòu)表示同一個文件的多個文件描述符,但此時, 所有的這些file文件結(jié)構(gòu)全部都必須只能指向一個inode結(jié)構(gòu)體。 inode結(jié)構(gòu)體包含了一大堆文件相關(guān)的信息,但是就針對驅(qū)動代碼來說,我們只要關(guān)心其中的兩個域即可:
inode結(jié)構(gòu)體(內(nèi)核源碼/include/linux/fs.h)
struct inode { dev_t i_rdev; {......} union { struct pipe_inode_info *i_pipe; /* linux內(nèi)核管道 */ struct block_device *i_bdev; /* 如果這是塊設(shè)備,則設(shè)置并使用 */ struct cdev *i_cdev; /* 如果這是字符設(shè)備,則設(shè)置并使用 */ char *i_link; unsigned i_dir_seq; }; {......}};
dev_t i_rdev: 表示設(shè)備文件的結(jié)點,這個域?qū)嶋H上包含了設(shè)備號。
struct cdev *i_cdev: struct cdev是內(nèi)核的一個內(nèi)部結(jié)構(gòu),它是用來表示字符設(shè)備的,當inode結(jié)點指向一個字符設(shè)備文件時,此域為一個指向inode結(jié)構(gòu)的指針。
4. 字符設(shè)備驅(qū)動程序框架?
講了很多次字符設(shè)備驅(qū)動程序框架,那到底什么是字符文件程序框架呢?我們可以從下面的思維導圖來解讀內(nèi)核源碼。
我們創(chuàng)建一個字符設(shè)備的時候,首先要的到一個設(shè)備號,分配設(shè)備號的途徑有靜態(tài)分配和動態(tài)分配; 拿到設(shè)備的唯一ID,我們需要實現(xiàn)file_operation并保存到cdev中,實現(xiàn)cdev的初始化; 然后我們需要將我們所做的工作告訴內(nèi)核,使用cdev_add()注冊cdev; 最后我們還需要創(chuàng)建設(shè)備節(jié)點,以便我們后面調(diào)用file_operation接口。
注銷設(shè)備時我們需釋放內(nèi)核中的cdev,歸還申請的設(shè)備號,刪除創(chuàng)建的設(shè)備節(jié)點。
在實現(xiàn)設(shè)備操作這一段,我們可以看看open函數(shù)到底做了什么。
4.1. 驅(qū)動初始化和注銷?
4.1.1. 設(shè)備號的申請和歸還?
Linux內(nèi)核提供了兩種方式來定義字符設(shè)備,如下所示。
定義字符設(shè)備
//第一種方式static struct cdev chrdev;//第二種方式struct cdev *cdev_alloc(void);
第一種方式,就是我們常見的變量定義;第二種方式,是內(nèi)核提供的動態(tài)分配方式,調(diào)用該函數(shù)之 后,會返回一個struct cdev類型的指針,用于描述字符設(shè)備。
從內(nèi)核中移除某個字符設(shè)備,則需要調(diào)用cdev_del函數(shù),如下所示。
cdev_del函數(shù)
void cdev_del(struct cdev *p)
函數(shù)參數(shù)和返回值如下:
參數(shù):
p: 該函數(shù)需要將我們的字符設(shè)備結(jié)構(gòu)體的地址作為實參傳遞進去,就可以從內(nèi)核中移除該字符設(shè)備了。
返回值: 無
register_chrdev_region函數(shù)
register_chrdev_region函數(shù)用于靜態(tài)地為一個字符設(shè)備申請一個或多個設(shè)備編號。函數(shù)原型如下所示。
register_chrdev_region函數(shù)
int register_chrdev_region(dev_t from, unsigned count, const char *name)
函數(shù)參數(shù)和返回值如下:
參數(shù):
from:dev_t類型的變量,用于指定字符設(shè)備的起始設(shè)備號,如果要注冊的設(shè)備號已經(jīng)被其他的設(shè)備注冊了,那么就會導致注冊失敗。
count:指定要申請的設(shè)備號個數(shù),count的值不可以太大,否則會與下一個主設(shè)備號重疊。
name:用于指定該設(shè)備的名稱,我們可以在/proc/devices中看到該設(shè)備。
返回值: 返回0表示申請成功,失敗則返回錯誤碼
alloc_chrdev_region函數(shù)
使用register_chrdev_region函數(shù)時,都需要去查閱內(nèi)核源碼的Documentation/devices.txt文件, 這就十分不方便。因此,內(nèi)核又為我們提供了一種能夠動態(tài)分配設(shè)備編號的方式:alloc_chrdev_region。
調(diào)用alloc_chrdev_region函數(shù),內(nèi)核會自動分配給我們一個尚未使用的主設(shè)備號。 我們可以通過命令“cat /proc/devices”查詢內(nèi)核分配的主設(shè)備號。
alloc_chrdev_region函數(shù)原型
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
函數(shù)參數(shù)和返回值如下:
參數(shù):
dev:指向dev_t類型數(shù)據(jù)的指針變量,用于存放分配到的設(shè)備編號的起始值;
baseminor:次設(shè)備號的起始值,通常情況下,設(shè)置為0;
count、name:同register_chrdev_region類型,用于指定需要分配的設(shè)備編號的個數(shù)以及設(shè)備的名稱。
返回值: 返回0表示申請成功,失敗則返回錯誤碼
unregister_chrdev_region函數(shù)
當我們刪除字符設(shè)備時候,我們需要把分配的設(shè)備編號交還給內(nèi)核,對于使用register_chrdev_region函數(shù) 以及alloc_chrdev_region函數(shù)分配得到的設(shè)備編號,可以使用unregister_chrdev_region函數(shù)實現(xiàn)該功能。
unregister_chrdev_region函數(shù)(內(nèi)核源碼/fs/char_dev.c)
void unregister_chrdev_region(dev_t from, unsigned count)
函數(shù)參數(shù)和返回值如下:
參數(shù):
from:指定需要注銷的字符設(shè)備的設(shè)備編號起始值,我們一般將定義的dev_t變量作為實參。
count:指定需要注銷的字符設(shè)備編號的個數(shù),該值應與申請函數(shù)的count值相等,通常采用宏定義進行管理。
返回值: 無
register_chrdev函數(shù)
除了上述的兩種,內(nèi)核還提供了register_chrdev函數(shù)用于分配設(shè)備號。該函數(shù)是一個內(nèi)聯(lián)函數(shù),它不 僅支持靜態(tài)申請設(shè)備號,也支持動態(tài)申請設(shè)備號,并將主設(shè)備號返回,函數(shù)原型如下所示。
register_chrdev函數(shù)原型(內(nèi)核源碼/include/linux/fs.h文件)
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops){ return __register_chrdev(major, 0, 256, name, fops);}
函數(shù)參數(shù)和返回值如下:
參數(shù):
major:用于指定要申請的字符設(shè)備的主設(shè)備號,等價于register_chrdev_region函數(shù),當設(shè)置為0時,內(nèi)核會自動分配一個未使用的主設(shè)備號。
name:用于指定字符設(shè)備的名稱
fops:用于操作該設(shè)備的函數(shù)接口指針。
返回值: 主設(shè)備號
我們從以上代碼中可以看到,使用register_chrdev函數(shù)向內(nèi)核申請設(shè)備號,同一類字 符設(shè)備(即主設(shè)備號相同),會在內(nèi)核中申請了256個,通常情況下,我們不需要用到這么多個設(shè)備,這就造成了極大的資源浪費。
unregister_chrdev函數(shù)
使用register函數(shù)申請的設(shè)備號,則應該使用unregister_chrdev函數(shù)進行注銷。
unregister_chrdev函數(shù)(內(nèi)核源碼/include/linux/fs.h)
static inline void unregister_chrdev(unsigned int major, const char *name){__unregister_chrdev(major, 0, 256, name);}
函數(shù)參數(shù)和返回值如下:
參數(shù):
major:指定需要釋放的字符設(shè)備的主設(shè)備號,一般使用register_chrdev函數(shù)的返回值作為實參。
name:執(zhí)行需要釋放的字符設(shè)備的名稱。
返回值: 無
4.1.2. 初始化cdev?
前面我們已經(jīng)提到過了,編寫一個字符設(shè)備最重要的事情,就是要實現(xiàn)file_operations這個結(jié)構(gòu)體中的函數(shù)。 實現(xiàn)之后,如何將該結(jié)構(gòu)體與我們的字符設(shè)備結(jié)構(gòu)體相關(guān)聯(lián)呢?內(nèi)核提供了cdev_init函數(shù),來實現(xiàn)這個過程。
cdev_init函數(shù)(內(nèi)核源碼/fs/char_dev.c)
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
函數(shù)參數(shù)和返回值如下:
參數(shù):
cdev:struct cdev類型的指針變量,指向需要關(guān)聯(lián)的字符設(shè)備結(jié)構(gòu)體;
fops:file_operations類型的結(jié)構(gòu)體指針變量,一般將實現(xiàn)操作該設(shè)備的結(jié)構(gòu)體file_operations結(jié)構(gòu)體作為實參。
返回值: 無
4.2. 設(shè)備注冊和注銷?
cdev_add函數(shù)用于向內(nèi)核的cdev_map散列表添加一個新的字符設(shè)備,如下所示。
cdev_add函數(shù)(內(nèi)核源碼/fs/char_dev.c)
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
函數(shù)參數(shù)和返回值如下:
參數(shù):
p:struct cdev類型的指針,用于指定需要添加的字符設(shè)備;
dev:dev_t類型變量,用于指定設(shè)備的起始編號;
count:指定注冊多少個設(shè)備。
返回值: 錯誤碼
從系統(tǒng)中刪除cdev,cdev設(shè)備將無法再打開,但任何已經(jīng)打開的cdev將保持不變, 即使在cdev_del返回后,它們的FOP仍然可以調(diào)用。
cdev_del函數(shù)(內(nèi)核源碼/fs/char_dev.c)
void cdev_del(struct cdev *p)
函數(shù)參數(shù)和返回值如下:
參數(shù):
p:struct cdev類型的指針,用于指定需要刪除的字符設(shè)備;
返回值: 無
4.3. 設(shè)備節(jié)點的創(chuàng)建和銷毀?
創(chuàng)建一個設(shè)備并將其注冊到文件系統(tǒng)
device_create函數(shù)(內(nèi)核源碼/drivers/base/core.c)
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
函數(shù)參數(shù)和返回值如下:
參數(shù):
class:指向這個設(shè)備應該注冊到的struct類的指針;
parent:指向此新設(shè)備的父結(jié)構(gòu)設(shè)備(如果有)的指針;
devt:要添加的char設(shè)備的開發(fā);
drvdata:要添加到設(shè)備進行回調(diào)的數(shù)據(jù);
fmt:輸入設(shè)備名稱。
返回值: 成功時返回 struct device 結(jié)構(gòu)體指針, 錯誤時返回ERR_PTR().
刪除使用device_create函數(shù)創(chuàng)建的設(shè)備
device_destroy函數(shù)(內(nèi)核源碼/drivers/base/core.c)
void device_destroy(struct class *class, dev_t devt)
函數(shù)參數(shù)和返回值如下:
參數(shù):
class:指向注冊此設(shè)備的struct類的指針;
devt:以前注冊的設(shè)備的開發(fā);
返回值: 無
除了使用代碼創(chuàng)建設(shè)備節(jié)點,還可以使用mknod命令創(chuàng)建設(shè)備節(jié)點。
用法:mknod 設(shè)備名 設(shè)備類型 主設(shè)備號 次設(shè)備號
當類型為”p”時可不指定主設(shè)備號和次設(shè)備號,否則它們是必須指定的。 如果主設(shè)備號和次設(shè)備號以”0x”或”0X”開頭,它們會被視作十六進制數(shù)來解析;如果以”0”開頭,則被視作八進制數(shù); 其余情況下被視作十進制數(shù)。可用的類型包括:
b 創(chuàng)建(有緩沖的)區(qū)塊特殊文件
c, u 創(chuàng)建(沒有緩沖的)字符特殊文件
p 創(chuàng)建先進先出(FIFO)特殊文件
如:mkmod /dev/test c 2 0
創(chuàng)建一個字符設(shè)備/dev/test,其主設(shè)備號為2,次設(shè)備號為0。
當我們使用上述命令,創(chuàng)建了一個字符設(shè)備文件時,實際上就是創(chuàng)建了一個設(shè)備節(jié)點inode結(jié)構(gòu)體, 并且將該設(shè)備的設(shè)備編號記錄在成員i_rdev,將成員f_op指針指向了def_chr_fops結(jié)構(gòu)體。 這就是mknod負責的工作內(nèi)容,具體代碼見如下。
mknod調(diào)用關(guān)系 (內(nèi)核源碼/mm/shmem.c)
static struct inode *shmem_get_inode(struct super_block *sb, const struct inode *dir,umode_t mode, dev_t dev, unsigned long flags){ inode = new_inode(sb); if (inode) { ...... switch (mode & S_IFMT) { default: inode->i_op = &shmem_special_inode_operations; init_special_inode(inode, mode, dev); break; ...... } } else shmem_free_inode(sb); return inode;}
第10行:mknod命令最終執(zhí)行init_special_inode函數(shù)
init_special_inode函數(shù)(內(nèi)核源碼/fs/inode.c)
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev){ inode->i_mode = mode; if (S_ISCHR(mode)) { inode->i_fop = &def_chr_fops; inode->i_rdev = rdev; } else if (S_ISBLK(mode)) { inode->i_fop = &def_blk_fops; inode->i_rdev = rdev; } else if (S_ISFIFO(mode)) inode->i_fop = &pipefifo_fops; else if (S_ISSOCK(mode)) ; /* leave it no_open_fops */ else printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for" " inode %s:%lun", mode, inode->i_sb->s_id, inode->i_ino);}
第4-17行:判斷文件的inode類型,如果是字符設(shè)備類型,則把def_chr_fops作為該文件的操作接口,并把設(shè)備號記錄在inode->i_rdev。
inode上的file_operation并不是自己構(gòu)造的file_operation,而是字符設(shè)備通用的def_chr_fops, 那么自己構(gòu)建的file_operation等在應用程序調(diào)用open函數(shù)之后,才會綁定在文件上。接下來我們再看open函數(shù)到底做了什么。
5. open函數(shù)到底做了什么?
使用設(shè)備之前我們通常都需要調(diào)用open函數(shù),這個函數(shù)一般用于設(shè)備專有數(shù)據(jù)的初始化,申請相關(guān)資源及進行設(shè)備的初始化等工作, 對于簡單的設(shè)備而言,open函數(shù)可以不做具體的工作,你在應用層通過系統(tǒng)調(diào)用open打開設(shè)備時, 如果打開正常,就會得到該設(shè)備的文件描述符,之后,我們就可以通過該描述符對設(shè)備進行read和write等操作; open函數(shù)到底做了些什么工作?下圖中列出了open函數(shù)執(zhí)行的大致過程。
用戶空間使用open()系統(tǒng)調(diào)用函數(shù)打開一個字符設(shè)備時(int fd = open(“dev/xxx”, O_RDWR))大致有以下過程:
在虛擬文件系統(tǒng)VFS中的查找對應與字符設(shè)備對應 struct inode節(jié)點
遍歷散列表cdev_map,根據(jù)inod節(jié)點中的 cdev_t設(shè)備號找到cdev對象
創(chuàng)建struct file對象(系統(tǒng)采用一個數(shù)組來管理一個進程中的多個被打開的設(shè)備,每個文件秒速符作為數(shù)組下標標識了一個設(shè)備對象)
初始化struct file對象,將 struct file對象中的 file_operations成員指向 struct cdev對象中的 file_operations成員(file->fops = cdev->fops)
回調(diào)file->fops->open函數(shù)
我們使用的open函數(shù)在內(nèi)核中對應的是sys_open函數(shù),sys_open函數(shù)又會調(diào)用do_sys_open函數(shù)。在do_sys_open函數(shù)中, 首先調(diào)用函數(shù)get_unused_fd_flags來獲取一個未被使用的文件描述符fd,該文件描述符就是我們最終通過open函數(shù)得到的值。 緊接著,又調(diào)用了do_filp_open函數(shù),該函數(shù)通過調(diào)用函數(shù)get_empty_filp得到一個新的file結(jié)構(gòu)體,之后的代碼做了許多復雜的工作, 如解析文件路徑,查找該文件的文件節(jié)點inode等,直接來到了函數(shù)do_dentry_open函數(shù),如下所示。
do_dentry_open函數(shù)(位于 ebf-busrer-linux/fs/open.c)
static int do_dentry_open(struct file *f,struct inode *inode,int (*open)(struct inode *, struct file *),const struct cred *cred){ //…… f->f_op = fops_get(inode->i_fop); //…… if (!open) open = f->f_op->open; if (open) { error = open(inode, f); if (error) goto cleanup_all; } //……}
第4行:使用fops_get函數(shù)來獲取該文件節(jié)點inode的成員變量i_fop,在上圖中我們使用mknod創(chuàng)建字符設(shè)備文件時,將def_chr_fops結(jié)構(gòu)體賦值給了該設(shè)備文件inode的i_fop成員。
第7行:到了這里,我們新建的file結(jié)構(gòu)體的成員f_op就指向了def_chr_fops。
def_chr_fops結(jié)構(gòu)體(內(nèi)核源碼/fs/char_dev.c)
const struct file_operations def_chr_fops = { .open = chrdev_open, .llseek = noop_llseek,};
最終,會執(zhí)行def_chr_fops中的open函數(shù),也就是chrdev_open函數(shù),可以理解為一個字符設(shè)備的通用初始化函數(shù),根據(jù)字符設(shè)備的設(shè)備號, 找到相應的字符設(shè)備,從而得到操作該設(shè)備的方法,代碼實現(xiàn)如下。
chrdev_open函數(shù)(內(nèi)核源碼/fs/char_dev.c)
static int chrdev_open(struct inode *inode, struct file *filp){ const struct file_operations *fops; struct cdev *p; struct cdev *new = NULL; int ret = 0; spin_lock(&cdev_lock); p = inode->i_cdev; if (!p) { struct kobject *kobj; int idx; spin_unlock(&cdev_lock); kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx); if (!kobj) return -ENXIO; new = container_of(kobj, struct cdev, kobj); spin_lock(&cdev_lock); /* Check i_cdev again in case somebody beat us to it whilewe dropped the lock.*/ p = inode->i_cdev; if (!p) { inode->i_cdev = p = new; list_add(&inode->i_devices, &p->list); new = NULL; } else if (!cdev_get(p)) ret = -ENXIO; } else if (!cdev_get(p)) ret = -ENXIO; spin_unlock(&cdev_lock); cdev_put(new); if (ret) return ret; ret = -ENXIO; fops = fops_get(p->ops); if (!fops) goto out_cdev_put; replace_fops(filp, fops); if (filp->f_op->open) { ret = filp->f_op->open(inode, filp); if (ret) goto out_cdev_put; } return 0; out_cdev_put: cdev_put(p); return ret;}
在Linux內(nèi)核中,使用結(jié)構(gòu)體cdev來描述一個字符設(shè)備。
第8行:inode->i_rdev中保存了字符設(shè)備的設(shè)備編號,
第13行:通過函數(shù)kobj_lookup函數(shù)便可以找到該設(shè)備文件cdev結(jié)構(gòu)體的kobj成員,
第16行:再通過函數(shù)container_of便可以得到該字符設(shè)備對應的結(jié)構(gòu)體cdev。函數(shù)container_of的作用就是通過一個結(jié)構(gòu)變量中一個成員的地址找到這個結(jié)構(gòu)體變量的首地址。同時,將cdev結(jié)構(gòu)體記錄到文件節(jié)點inode中的i_cdev,便于下次打開該文件。
第38-43行:函數(shù)chrdev_open最終將該文件結(jié)構(gòu)體file的成員f_op替換成了cdev對應的ops成員,并執(zhí)行ops結(jié)構(gòu)體中的open函數(shù)。
最后,調(diào)用上圖的fd_install函數(shù),完成文件描述符和文件結(jié)構(gòu)體file的關(guān)聯(lián),之后我們使用對該文件描述符fd調(diào)用read、write函數(shù), 最終都會調(diào)用file結(jié)構(gòu)體對應的函數(shù),實際上也就是調(diào)用cdev結(jié)構(gòu)體中ops結(jié)構(gòu)體內(nèi)的相關(guān)函數(shù)。
總結(jié)一下整個過程,當我們使用open函數(shù),打開設(shè)備文件時,會根據(jù)該設(shè)備的文件的設(shè)備號找到相應的設(shè)備結(jié)構(gòu)體, 從而得到了操作該設(shè)備的方法。也就是說如果我們要添加一個新設(shè)備的話,我們需要提供一個設(shè)備號, 一個設(shè)備結(jié)構(gòu)體以及操作該設(shè)備的方法(file_operations結(jié)構(gòu)體)。
6. 字符設(shè)備驅(qū)動程序?qū)嶒?
6.1. 硬件介紹?
本節(jié)實驗使用到armsom-sige系列板。
6.2. 實驗代碼講解?
結(jié)合前面所有的知識點,首先,字符設(shè)備驅(qū)動程序是以內(nèi)核模塊的形式存在的, 我們要向系統(tǒng)注冊一個新的字符設(shè)備,需要這幾樣東西:字符設(shè)備結(jié)構(gòu)體cdev,設(shè)備編號, 以及最重要的操作方式結(jié)構(gòu)體file_operations。
下面,我們開始編寫我們自己的字符設(shè)備驅(qū)動程序。
6.2.1. 內(nèi)核模塊框架?
既然我們的設(shè)備程序是以內(nèi)核模塊的方式存在的,那么就需要先寫出一個基本的內(nèi)核框架,見如下所示。
內(nèi)核模塊加載函數(shù)
#define DEV_NAME "EmbedCharDev"#define DEV_CNT (1)#define BUFF_SIZE 128//定義字符設(shè)備的設(shè)備號static dev_t devno;//定義字符設(shè)備結(jié)構(gòu)體chr_devstatic struct cdev chr_dev;static int __init chrdev_init(void){ int ret = 0; printk("chrdev initn"); //第一步 //采用動態(tài)分配的方式,獲取設(shè)備編號,次設(shè)備號為0, //設(shè)備名稱為EmbedCharDev,可通過命令cat /proc/devices查看 //DEV_CNT為1,當前只申請一個設(shè)備編號 ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME); if (ret < 0) { printk("fail to alloc devnon"); goto alloc_err; } //第二步 //關(guān)聯(lián)字符設(shè)備結(jié)構(gòu)體cdev與文件操作結(jié)構(gòu)體file_operations cdev_init(&chr_dev, &chr_dev_fops); //第三步 //添加設(shè)備至cdev_map散列表中 ret = cdev_add(&chr_dev, devno, DEV_CNT); if (ret < 0) { printk("fail to add cdevn"); goto add_err; } return 0; add_err: //添加設(shè)備失敗時,需要注銷設(shè)備號 unregister_chrdev_region(devno, DEV_CNT); alloc_err: return ret; } module_init(chrdev_init);
第16行:使用動態(tài)分配(alloc_chrdev_region)的方式來獲取設(shè)備號,指定設(shè)備的名稱為“EmbedCharDev”,只申請一個設(shè)備號,并且次設(shè)備號為0。
第19行:這里使用C語言的goto語法,當獲取失敗時,直接返回對應的錯誤碼。成功獲取到設(shè)備號之后,我們還缺字符設(shè)備結(jié)構(gòu)體以及文件的操作方式。
第23行:以上代碼中使用定義變量的方式定義了一個字符設(shè)備結(jié)構(gòu)體chr_dev,調(diào)用cdev_init函數(shù)將chr_dev結(jié)構(gòu)體和文件操作結(jié)構(gòu)體相關(guān)聯(lián),該結(jié)構(gòu)體的具體實現(xiàn)下節(jié)見分曉。
第26行:最后我們只需要調(diào)用cdev_add函數(shù)將我們的字符設(shè)備添加到字符設(shè)備管理列表cdev_map即可。
第29行:此處也使用了goto語法,當添加設(shè)備失敗的話,需要將申請的設(shè)備號注銷掉。
模塊的卸載函數(shù)就相對簡單一下,只需要完成注銷設(shè)備號,以及移除字符設(shè)備,如下所示。
內(nèi)核模塊卸載函數(shù)
static void __exit chrdev_exit(void){ printk("chrdev exitn"); unregister_chrdev_region(devno, DEV_CNT); cdev_del(&chr_dev);}module_exit(chrdev_exit);
6.2.2. 文件操作方式的實現(xiàn)?
下面,我們開始實現(xiàn)字符設(shè)備最重要的部分:文件操作方式結(jié)構(gòu)體file_operations,見如下所示。
file_operations結(jié)構(gòu)體
#define BUFF_SIZE 128//數(shù)據(jù)緩沖區(qū)static char vbuf[BUFF_SIZE];static struct file_operations chr_dev_fops = { .owner = THIS_MODULE, .open = chr_dev_open, .release = chr_dev_release, .write = chr_dev_write, .read = chr_dev_read, };
由于這個字符設(shè)備是一個虛擬的設(shè)備,與硬件并沒有什么關(guān)聯(lián),因此,open函數(shù)與release直接返回0即可,我們重點關(guān)注write以及read函數(shù)的實現(xiàn)
chr_dev_open函數(shù)與chr_dev_release函數(shù)
static int chr_dev_open(struct inode *inode, struct file *filp){ printk("nopenn"); return 0;}static int chr_dev_release(struct inode *inode, struct file *filp){ printk("nreleasen"); return 0; }
我們在open函數(shù)與release函數(shù)中打印相關(guān)的調(diào)試信息,如上方代碼所示。
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos){ unsigned long p = *ppos; int ret; int tmp = count ; if (p > BUFF_SIZE) return 0; if (tmp > BUFF_SIZE - p) tmp = BUFF_SIZE - p; ret = copy_from_user(vbuf, buf, tmp); *ppos += tmp; return tmp;}
當我們的應用程序調(diào)用write函數(shù),最終就調(diào)用我們的gpio_write函數(shù)。
第3行:變量p記錄了當前文件的讀寫位置,
第6-9行:如果超過了數(shù)據(jù)緩沖區(qū)的大小(128字節(jié))的話,直接返回0。并且如果要讀寫的數(shù)據(jù)個數(shù)超過了數(shù)據(jù)緩沖區(qū)剩余的內(nèi)容的話,則只讀取剩余的內(nèi)容。
第10-11行:使用copy_from_user從用戶空間拷貝tmp個字節(jié)的數(shù)據(jù)到數(shù)據(jù)緩沖區(qū)中,同時讓文件的讀寫位置偏移同樣的字節(jié)數(shù)。
chr_dev_read函數(shù)
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos){ unsigned long p = *ppos; int ret; int tmp = count ; if (p >= BUFF_SIZE) return 0; if (tmp > BUFF_SIZE - p) tmp = BUFF_SIZE - p; ret = copy_to_user(buf, vbuf+p, tmp); *ppos +=tmp; return tmp;}
同樣的,當我們應用程序調(diào)用read函數(shù),則會執(zhí)行chr_dev_read函數(shù)的內(nèi)容。 該函數(shù)的實現(xiàn)與chr_dev_write函數(shù)類似,區(qū)別在于,使用copy_to_user從數(shù)據(jù)緩沖區(qū)拷貝tmp個字節(jié)的數(shù)據(jù)到用戶空間中。
6.2.3. 簡單測試程序?
下面,我們開始編寫應用程序,來讀寫我們的字符設(shè)備,如下所示。
main.c函數(shù)
#include #include #include #include char *wbuf = "Hello Worldn";char rbuf[128];int main(void){ printf("EmbedCharDev testn"); //打開文件 int fd = open("/dev/chrdev", O_RDWR); //寫入數(shù)據(jù) write(fd, wbuf, strlen(wbuf)); //寫入完畢,關(guān)閉文件 close(fd); //打開文件 fd = open("/dev/chrdev", O_RDWR); //讀取文件內(nèi)容 read(fd, rbuf, 128); //打印讀取的內(nèi)容 printf("The content : %s", rbuf); //讀取完畢,關(guān)閉文件 close(fd); return 0;}
第11行:以可讀可寫的方式打開我們創(chuàng)建的字符設(shè)備驅(qū)動
第12-15行:寫入數(shù)據(jù)然后關(guān)閉
第17-21行:再次打開設(shè)備將數(shù)據(jù)讀取出來
6.3. 實驗準備?
6.3.1. makefile修改說明?
makefile
KERNEL_DIR=/home/lhd/project/3588/linux5.10-rkr6/kernelCROSS_COMPILE=/home/lhd/project/3588/linux5.10-rkr6/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-gccobj-m := chrdev.oout = chrdev_testall: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules $(CROSS_COMPILE) -o $(out) main.c.PHONY:cleanclean: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean rm $(out)
Makefile與此前相比,增加了編譯測試程序部分。
第1行:該Makefile定義了變量KERNEL_DIR,來保存內(nèi)核源碼的目錄。
第2行: 指定了工具鏈
第5行:變量obj-m保存著需要編譯成模塊的目標文件名。
第6行:變量out保存著需要編譯成測試程序的目標文件名。
第8行:’$(MAKE)modules’實際上是執(zhí)行Linux頂層Makefile的偽目標modules。通過選項’-C’,可以讓make工具跳轉(zhuǎn)到源碼目錄下讀取頂層Makefile?!疢=$(CURDIR)’表明返回到當前目錄,讀取并執(zhí)行當前目錄的Makefile,開始編譯內(nèi)核模塊。CURDIR是make的內(nèi)嵌變量,自動設(shè)置為當前目錄。
第9行:交叉編譯工具鏈編譯測試程序。
6.3.2. 編譯命令說明?
make
編譯成功后,實驗目錄下會生成兩個名為”chrdev.ko”驅(qū)動模塊文件和” chrdev_test”測試程序。
6.4. 程序運行結(jié)果?
通過我們?yōu)榇蠹揖帉懞玫腗akefile,執(zhí)行make,會生成chrdev.ko文件和驅(qū)動測試程序chrdev_test, 通過nfs網(wǎng)絡文件系統(tǒng)或者scp,將文件拷貝到開發(fā)板。 執(zhí)行以下命令:
sudo insmod chrdev.kocat /proc/devices
我們從/proc/devices文件中,可以看到我們注冊的字符設(shè)備EmbedCharDev的主設(shè)備號為234。 注意此主設(shè)備號下面會用到,大家開發(fā)板根據(jù)實際指調(diào)整
mknod /dev/chrdev c 234 0
以root權(quán)限使用mknod命令來創(chuàng)建一個新的設(shè)備chrdev,見下圖。
以root權(quán)限運行chrdev_test,測試程序,效果見下圖。
實際上,我們也可以通過echo或者cat命令,來測試我們的設(shè)備驅(qū)動程序。
echo "EmbedCharDev test" > /dev/chrdev
當我們不需要該內(nèi)核模塊的時候,我們可以執(zhí)行以下命令:
rmmod chrdev.korm /dev/chrdev
使用命令rmmod,卸載內(nèi)核模塊,并且刪除相應的設(shè)備文件。
7. 一個驅(qū)動支持多個設(shè)備?
在Linux內(nèi)核中,主設(shè)備號用于標識設(shè)備對應的驅(qū)動程序,告訴Linux內(nèi)核使用哪一個驅(qū)動程序為該設(shè)備服務。但是, 次設(shè)備號表示了同類設(shè)備的各個設(shè)備。每個設(shè)備的功能都是不一樣的。如何能夠用一個驅(qū)動程序去控制各種設(shè)備呢? 很明顯,首先,我們可以根據(jù)次設(shè)備號,來區(qū)分各種設(shè)備;其次,就是前文提到過的file結(jié)構(gòu)體的私有數(shù)據(jù)成員private_data。 我們可以通過該成員來做文章,不難想到為什么只有open函數(shù)和close函數(shù)的形參才有file結(jié)構(gòu)體, 因為驅(qū)動程序第一個執(zhí)行的是操作就是open,通過open函數(shù)就可以控制我們想要驅(qū)動的底層硬件。
7.1. 硬件介紹?
本節(jié)實驗使用到armsom-sige7
7.2. 實驗代碼講解?
7.2.1. 實現(xiàn)方式一 管理各種的數(shù)據(jù)緩沖區(qū)?
下面介紹第一種實現(xiàn)方式,將我們的上一節(jié)程序改善一下,生成了兩個設(shè)備,各自管理各自的數(shù)據(jù)緩沖區(qū)。
chrdev.c修改
#define DEV_NAME "EmbedCharDev"#define DEV_CNT (2) (1)#define BUFF_SIZE 128//定義字符設(shè)備的設(shè)備號static dev_t devno;//定義字符設(shè)備結(jié)構(gòu)體chr_devstatic struct cdev chr_dev;//數(shù)據(jù)緩沖區(qū)static char vbuf1[BUFF_SIZE];static char vbuf2[BUFF_SIZE];
第2行:修改了宏定義DEV_CNT,將原本的個數(shù)1改為2,這樣的話,我們的驅(qū)動程序便可以管理兩個設(shè)備。
第9-10行:處修改為兩個數(shù)據(jù)緩沖區(qū)。
chr_dev_open函數(shù)修改
static int chr_dev_open(struct inode *inode, struct file *filp){ printk("nopenn "); switch (MINOR(inode->i_rdev)) { case 0 : { filp->private_data = vbuf1; break; } case 1 : { filp->private_data = vbuf2; break; } } return 0;}
我們知道inode結(jié)構(gòu)體中,對于設(shè)備文件的設(shè)備號會被保存到其成員i_rdev中。
第4行:在chr_dev_open函數(shù)中,我們使用宏定義MINOR來獲取該設(shè)備文件的次設(shè)備號,使用private_data指向各自的數(shù)據(jù)緩沖區(qū)。
第5-12行:對于次設(shè)備號為0的設(shè)備,負責管理vbuf1的數(shù)據(jù),對于次設(shè)備號為1的設(shè)備,則用于管理vbuf2的數(shù)據(jù),這樣就實現(xiàn)了同一個設(shè)備驅(qū)動,管理多個設(shè)備了。
接下來,我們的驅(qū)動只需要對private_data進行讀寫即可。
chr_dev_write函數(shù)
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos){ unsigned long p = *ppos; int ret; char *vbuf = filp->private_data; int tmp = count ; if (p > BUFF_SIZE) return 0; if (tmp > BUFF_SIZE - p) tmp = BUFF_SIZE - p; ret = copy_from_user(vbuf, buf, tmp); *ppos += tmp; return tmp;}
可以看到,我們的chr_dev_write函數(shù)改動很小,只是增加了第5行的代碼,將原先vbuf數(shù)據(jù)指向了private_data,這樣的話, 當我們往次設(shè)備號為0的設(shè)備寫數(shù)據(jù)時,就會往vbuf1中寫入數(shù)據(jù)。次設(shè)備號為1的設(shè)備寫數(shù)據(jù),也是同樣的道理。
chr_dev_read函數(shù)
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos){ unsigned long p = *ppos; int ret; int tmp = count ; char *vbuf = filp->private_data; if (p >= BUFF_SIZE) return 0; if (tmp > BUFF_SIZE - p) tmp = BUFF_SIZE - p; ret = copy_to_user(buf, vbuf+p, tmp); *ppos +=tmp; return tmp;}
同樣的,chr_dev_read函數(shù)也只是增加了第6行的代碼,將原先的vbuf指向了private_data成員。
7.2.2. 實現(xiàn)方式二 i_cdev變量?
我們回憶一下,我們前面講到的文件節(jié)點inode中的成員i_cdev,為了方便訪問設(shè)備文件,在打開文件過程中, 將對應的字符設(shè)備結(jié)構(gòu)體cdev保存到該變量中,那么我們也可以通過該變量來做文章。
定義設(shè)備
/*虛擬字符設(shè)備*/struct chr_dev {struct cdev dev;char vbuf[BUFF_SIZE];};//字符設(shè)備1static struct chr_dev vcdev1;//字符設(shè)備2static struct chr_dev vcdev2;
以上代碼中定義了一個新的結(jié)構(gòu)體struct chr_dev,它有兩個結(jié)構(gòu)體成員:字符設(shè)備結(jié)構(gòu)體dev以及設(shè)備對應的數(shù)據(jù)緩沖區(qū)。 使用新的結(jié)構(gòu)體類型struct chr_dev定義兩個虛擬設(shè)備vcdev1以及vcdev2。
chrdev_init函數(shù)
static int __init chrdev_init(void){ int ret; printk("4 chrdev initn"); ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME); if (ret < 0) goto alloc_err; //關(guān)聯(lián)第一個設(shè)備:vdev1 cdev_init(&vcdev1.dev, &chr_dev_fops); ret = cdev_add(&vcdev1.dev, devno+0, 1); if (ret < 0) { printk("fail to add vcdev1 "); goto add_err1; } //關(guān)聯(lián)第二個設(shè)備:vdev2 cdev_init(&vcdev2.dev, &chr_dev_fops); ret = cdev_add(&vcdev2.dev, devno+1, 1); if (ret < 0) { printk("fail to add vcdev2 "); goto add_err2; } return 0; add_err2: cdev_del(&(vcdev1.dev)); add_err1: unregister_chrdev_region(devno, DEV_CNT); alloc_err: return ret;}
chrdev_init函數(shù)的框架仍然沒有什么變化。
第10、17行:在添加字符設(shè)備時,使用cdev_add依次添加。
第23-24行:當虛擬設(shè)備1添加失敗時,直接返回的時候,只需要注銷申請到的設(shè)備號即可。
第25-26行:若虛擬設(shè)備2添加失敗,則需要把虛擬設(shè)備1移除,再將申請的設(shè)備號注銷。
chrdev_exit函數(shù)(位于../linux_driver/EmbedCharDev/2_SupportMoreDev/chrdev.c)
static void __exit chrdev_exit(void){ printk("chrdev exitn"); unregister_chrdev_region(devno, DEV_CNT); cdev_del(&(vcdev1.dev)); cdev_del(&(vcdev2.dev));}
chrdev_exit函數(shù)注銷了申請到的設(shè)備號,使用cdev_del移除兩個虛擬設(shè)備。
chr_dev_open以及chr_dev_release函數(shù)
static int chr_dev_open(struct inode *inode, struct file *filp){ printk("openn"); filp->private_data = container_of(inode->i_cdev, struct chr_dev, dev); return 0;}static int chr_dev_release(struct inode *inode, struct file *filp){ printk("releasen"); return 0;}
我們知道inode中的i_cdev成員保存了對應字符設(shè)備結(jié)構(gòu)體的地址,但是我們的虛擬設(shè)備是把cdev封裝起來的一個結(jié)構(gòu)體, 我們要如何能夠得到虛擬設(shè)備的數(shù)據(jù)緩沖區(qū)呢?為此,Linux提供了一個宏定義container_of,該宏可以根據(jù)結(jié)構(gòu)體的某個成員的地址, 來得到該結(jié)構(gòu)體的地址。該宏需要三個參數(shù),分別是代表結(jié)構(gòu)體成員的真實地址,結(jié)構(gòu)體的類型以及結(jié)構(gòu)體成員的名字。 在chr_dev_open函數(shù)中,我們需要通過inode的i_cdev成員,來得到對應的虛擬設(shè)備結(jié)構(gòu)體,并保存到文件指針filp的私有數(shù)據(jù)成員中。 假如,我們打開虛擬設(shè)備1,那么inode->i_cdev便指向了vcdev1的成員dev,利用container_of宏, 我們就可以得到vcdev1結(jié)構(gòu)體的地址,也就可以操作對應的數(shù)據(jù)緩沖區(qū)了。
chr_dev_write函數(shù)
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos){ unsigned long p = *ppos; int ret; //獲取文件的私有數(shù)據(jù) struct chr_dev *dev = filp->private_data; char *vbuf = dev->vbuf; int tmp = count ; if (p > BUFF_SIZE) return 0; if (tmp > BUFF_SIZE - p) tmp = BUFF_SIZE - p; ret = copy_from_user(vbuf, buf, tmp); *ppos += tmp; return tmp;}
對比第一種方法,實際上只是新增了第6行代碼,通過文件指針filp的成員private_data得到相應的虛擬設(shè)備。 修改第7行的代碼,定義了char類型的指針變量,指向?qū)O(shè)備的數(shù)據(jù)緩沖區(qū)。
chr_dev_read函數(shù)
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos){ unsigned long p = *ppos; int ret; int tmp = count ; //獲取文件的私有數(shù)據(jù) struct chr_dev *dev = filp->private_data; char *vbuf = dev->vbuf; if (p >= BUFF_SIZE) return 0; if (tmp > BUFF_SIZE - p) tmp = BUFF_SIZE - p; ret = copy_to_user(buf, vbuf+p, tmp); *ppos +=tmp; return tmp;}
讀函數(shù),與寫函數(shù)的改動部分基本一致,這里就只貼出代碼,不進行講解。
7.3. 實驗準備?
分別獲取兩個種方式的內(nèi)核模塊源碼,將使用配套代碼 /linux_driver放到內(nèi)核同級目錄下,進入到EmbedCharDev目錄,找到1_SupportMoreDev和2_SupportMoreDev。
7.3.1. makefile說明?
至于Makefile文件,與上一小節(jié)的相同,這里便不再羅列出來了。
7.3.2. 編譯命令說明?
在實驗目錄下輸入如下命令來編譯驅(qū)動模塊:
make
編譯成功后,實驗目錄下會分別生成驅(qū)動模塊文件
7.4. 程序運行結(jié)果?
通過NFS或者SCP將編譯好的驅(qū)動模塊拷貝到開發(fā)板中
下面我們 使用cat以及echo命令,對我們的驅(qū)動程序進行測試。
insmod 1_SupportMoreDev.kosudo mknod /dev/chrdev1 c 234 0sudo mknod /dev/chrdev2 c 234 1
通過以上命令,加載了新的內(nèi)核模塊,手動創(chuàng)建了兩個新的字符設(shè)備,主設(shè)備號根據(jù)/proc/devices中描述設(shè)置,分 別是/dev/chrdev1和/dev/chrdev2,開始進行讀寫測試:
echo "hello world" > /dev/chrdev1# 或者sudo sh -c "echo 'hello world' > /dev/chrdev1"echo "123456" > /dev/chrdev2# 或者sudo sh -c "echo '123456' > /dev/chrdev2"cat /dev/chrdev1cat /dev/chrdev2
可以看到設(shè)備chrdev1中保存了字符串“hello world”,而設(shè)備chrdev2中保存了字符串“123456”。 只需要幾行代碼,就可以實現(xiàn)一個驅(qū)動程序,控制多個設(shè)備。
總結(jié)一下,一個驅(qū)動支持多個設(shè)備的具體實現(xiàn)方式的重點在于如何運用file的私有數(shù)據(jù)成員。 第一種方法是通過將各自的數(shù)據(jù)緩沖區(qū)放到該成員中,在讀寫函數(shù)的時候,直接就可以對相應的數(shù)據(jù)緩沖區(qū)進行操作; 第二種方法則是通過將我們的數(shù)據(jù)緩沖區(qū)和字符設(shè)備結(jié)構(gòu)體封裝到一起,由于文件結(jié)構(gòu)體inode的成員i_cdev保存了對應字符設(shè)備結(jié)構(gòu)體, 使用container_of宏便可以獲得封裝后的結(jié)構(gòu)體的地址,進而得到相應的數(shù)據(jù)緩沖區(qū)。
到這里,字符設(shè)備驅(qū)動就已經(jīng)講解完畢了。如果你發(fā)現(xiàn)自己有好多不理解的地方,學完本章之后,建議重新梳理一下整個過程, 有助于加深對整個字符設(shè)備驅(qū)動框架的理解。
審核編輯 黃宇
-
嵌入式
+關(guān)注
關(guān)注
5045文章
18817瀏覽量
298489 -
Linux
+關(guān)注
關(guān)注
87文章
11123瀏覽量
207905 -
板卡
+關(guān)注
關(guān)注
3文章
90瀏覽量
16699 -
Rockchip
+關(guān)注
關(guān)注
0文章
70瀏覽量
18465
發(fā)布評論請先 登錄
相關(guān)推薦
評論