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