精品国产人成在线_亚洲高清无码在线观看_国产在线视频国产永久2021_国产AV综合第一页一个的一区免费影院黑人_最近中文字幕MV高清在线视频

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

ArmSoM系列板卡 嵌入式Linux驅(qū)動開發(fā)實戰(zhàn)指南 之 字符設(shè)備驅(qū)動

Rockchip系列教程 ? 來源:Rockchip系列教程 ? 作者:Rockchip系列教程 ? 2024-04-10 09:53 ? 次閱讀

字符設(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è)備變得很容易。

wKgZomYVR9GANLMOAAFxQgXISF0915.png

在硬件層,我們可以通過查看硬件的原理圖、芯片的數(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)

c:字符設(shè)備(character device)

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開始遞增。

wKgaomYVR9OAGVv_AAETnsc6MH0295.png

一般來說,主設(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))#define MINOR(dev)   ((unsigned int) ((dev) & MINORMASK))#define MKDEV(ma,mi) (((ma) 

第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è)備。

wKgZomYVR9WASZGkAAC9RMKyvw4820.jpg

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ù)和返回值如下:

參數(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)核源碼。

wKgaomYVR9qAcUPSAADjDuTB7TA823.png

我們創(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。

wKgZomYVR92AUt-UAACJG4BzABI739.png

當我們使用上述命令,創(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í)行的大致過程。

wKgaomYVR-OAKo6uAAF_H6aO4oA236.png

用戶空間使用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)如下。

wKgZomYVR-WADrHQAAB5XtbvGRs826.jpg

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

wKgaomYVR-eAesRFAABlv1S12rA617.png

我們從/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,見下圖。

wKgaomYVR5uAciciAAAZskFo9sY352.png

以root權(quán)限運行chrdev_test,測試程序,效果見下圖。

wKgZomYVR5uASIGjAAAVPnqgWaY321.png

實際上,我們也可以通過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ū)動框架的理解。

審核編輯 黃宇

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 嵌入式
    +關(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
收藏 人收藏

    評論

    相關(guān)推薦

    【15年重磅】嵌入式入門及項目實戰(zhàn)開發(fā)【菜鳥必學項目】

    ://pan.baidu.com/s/1i3mVybb【嵌入式linux驅(qū)動開發(fā)系列】第六節(jié)Linux
    發(fā)表于 01-24 00:00

    零基礎(chǔ)學習嵌入式開發(fā)以及項目實戰(zhàn)開發(fā)

    linux下PWM驅(qū)動實現(xiàn)過程以及代碼詳解(2)http://pan.baidu.com/s/1i3mVybb【嵌入式linux驅(qū)動
    發(fā)表于 05-19 21:34

    嵌入式驅(qū)動開發(fā) Linux字符設(shè)備驅(qū)動

    1.嵌入式設(shè)備驅(qū)動概述2.字符設(shè)備驅(qū)動框架3.GPIO驅(qū)動
    發(fā)表于 10-09 17:21

    嵌入式 linux字符設(shè)備驅(qū)動的設(shè)計與應用,看完你就懂了

    本文通過實現(xiàn)對 PXA255開發(fā)板外圍字符設(shè)備(電機、數(shù)碼管、串口和 mini鍵盤)的操作和控制,詳細討論了嵌入式 linux
    發(fā)表于 04-26 06:35

    嵌入式Linux驅(qū)動開發(fā)

    想講好嵌入式Linux驅(qū)動開發(fā)并不容易,各位業(yè)界大神最基礎(chǔ)的字符驅(qū)動到中斷并發(fā)再到
    發(fā)表于 11-04 09:02

    Linux嵌入式驅(qū)動開發(fā)

    嵌入式驅(qū)動開發(fā)04——應用層和內(nèi)核層數(shù)據(jù)傳輸Linux嵌入式驅(qū)動
    發(fā)表于 12-17 06:22

    嵌入式驅(qū)動開發(fā)字符驅(qū)動

    嵌入式 驅(qū)動開發(fā)基礎(chǔ)2》 字符驅(qū)動 2008年畢業(yè)于沈陽航空航天大學電子...
    發(fā)表于 12-23 06:05

    嵌入式Linux設(shè)備驅(qū)動開發(fā)

    嵌入式Linux設(shè)備驅(qū)動開發(fā) Linux 設(shè)備
    發(fā)表于 09-10 13:10 ?82次下載
    <b class='flag-5'>嵌入式</b><b class='flag-5'>Linux</b><b class='flag-5'>設(shè)備</b><b class='flag-5'>驅(qū)動</b><b class='flag-5'>開發(fā)</b>

    嵌入式Linux字符設(shè)備驅(qū)動的設(shè)計與應用

    描述了基于嵌入式Linux字符設(shè)備驅(qū)動程序的設(shè)計方法和實現(xiàn)過程。以電機、數(shù)碼管、串口和mini鍵盤的驅(qū)
    發(fā)表于 02-23 15:45 ?24次下載

    嵌入式Linux字符設(shè)備驅(qū)動的設(shè)計與應用

    描述了基于嵌入式Linux字符設(shè)備驅(qū)動程序的設(shè)計方法和實現(xiàn)過程。以電機、數(shù)碼管、串口和mini鍵盤的驅(qū)
    發(fā)表于 07-14 17:31 ?31次下載

    基于PXA255開發(fā)板外圍字符設(shè)備嵌入式Linux字符設(shè)備驅(qū)動設(shè)計與應用

    驅(qū)動程序和應用程序的需求在成倍增長。本文通過實現(xiàn)對 PXA255開發(fā)板外圍字符設(shè)備(電機、數(shù)碼管、串口和 mini鍵盤)的操作和控制,詳細討論了
    發(fā)表于 08-21 10:19 ?1140次閱讀
    基于PXA255<b class='flag-5'>開發(fā)</b>板外圍<b class='flag-5'>字符</b><b class='flag-5'>設(shè)備</b>的<b class='flag-5'>嵌入式</b><b class='flag-5'>Linux</b><b class='flag-5'>字符</b><b class='flag-5'>設(shè)備</b><b class='flag-5'>驅(qū)動</b>設(shè)計與應用

    嵌入式Linux系統(tǒng)和驅(qū)動開發(fā)

    、linux的中斷系統(tǒng)、Linux內(nèi)核的移植等。有了內(nèi)核的基礎(chǔ),就可以學習嵌入式Linux設(shè)備驅(qū)動
    發(fā)表于 10-11 11:11 ?740次閱讀

    嵌入式Linux設(shè)備驅(qū)動程序開發(fā)基礎(chǔ)知識總結(jié)免費下載

    本文檔的主要內(nèi)容詳細介紹的是嵌入式Linux設(shè)備驅(qū)動程序開發(fā)基礎(chǔ)知識總結(jié)免費下載 嵌入式
    發(fā)表于 10-23 16:10 ?13次下載

    嵌入式Linux驅(qū)動開發(fā)從基礎(chǔ)到框架

    想講好嵌入式Linux驅(qū)動開發(fā)并不容易,各位業(yè)界大神最基礎(chǔ)的字符驅(qū)動到中斷并發(fā)再到
    發(fā)表于 11-01 16:58 ?14次下載
    <b class='flag-5'>嵌入式</b><b class='flag-5'>Linux</b><b class='flag-5'>驅(qū)動</b><b class='flag-5'>開發(fā)</b>從基礎(chǔ)到框架

    迅為基于RK3568開發(fā)板的嵌入式學習Linux驅(qū)動視頻

    迅為基于RK3568開發(fā)板的嵌入式學習Linux驅(qū)動視頻
    的頭像 發(fā)表于 05-19 16:30 ?809次閱讀
    迅為基于RK3568<b class='flag-5'>開發(fā)</b>板的<b class='flag-5'>嵌入式</b>學習<b class='flag-5'>之</b><b class='flag-5'>Linux</b><b class='flag-5'>驅(qū)動</b>視頻