ArmSoM系列板卡 嵌入式Linux驅動開發實戰指南 之 字元裝置驅動

ArmSoM开源硬件發表於2024-04-09

字元裝置驅動

本章,我們將學習字元裝置使用、字元裝置驅動相關的概念,理解字元裝置驅動程式的基本框架,並從原始碼上分析字元裝置驅動實現和管理等。 主要分為下面五部分:

  1. Linux裝置分類;
  2. 字元裝置的抽象,字元裝置設計思路;
  3. 字元裝置相關的概念以及資料結構,瞭解裝置號等基本概念以及file_operations、file、inode相關資料結構;
  4. 字元字元裝置驅動程式框架,例如核心是如何管理裝置號的;系統關聯、呼叫file_operation介面,open函式所涉及的知識等等。
  5. 裝置驅動程式實驗。

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語言中沒有物件導向語言的繼承的語法,但是我們可以透過結構體的包含來實現繼承,這種抽象提取了裝置的共性, 為上層提供了統一介面,使得管理和操作裝置變得很容易。

chrdev01

在硬體層,我們可以透過檢視硬體的原理圖、晶片的資料手冊,確定底層需要配置的暫存器,這類似於裸機開發, 將對底層暫存器的配置,讀寫操作放在檔案操作介面裡面,也就是實現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)
  • c:字元裝置(character device)
  • b:塊裝置(block device)
  • p:管道(pipe)
  • s:套接字(socket)

如下圖:ashmem 是一個字元裝置c, 它的主裝置號是10,次裝置號是124;initctl是一個符號連結,隨後的許可權字元 rwxrwxrwx 表示該連結的所有者、同組使用者和其他使用者均有讀(r)、寫(w)和執行(x)的許可權;loop0 是一個塊裝置,它的主裝置號是7,次裝置號為0,同時可以看到loop0-loop3共用一個主裝置號,次裝置號由0開始遞增。

chrdev02

一般來說,主裝置號指向裝置的驅動程式,次裝置號指向某個具體的裝置。如上圖,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來管理當前系統中的所有字元裝置。

chrdev03

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. 字元裝置驅動程式框架

講了很多次字元裝置驅動程式框架,那到底什麼是字元檔案程式框架呢?我們可以從下面的思維導圖來解讀核心原始碼。

./chrdev04.png

我們建立一個字元裝置的時候,首先要的到一個裝置號,分配裝置號的途徑有靜態分配和動態分配; 拿到裝置的唯一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。

./chrdev05.png

當我們使用上述命令,建立了一個字元裝置檔案時,實際上就是建立了一個裝置節點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:%lu\n", 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函式執行的大致過程。

./chrdev06.png

使用者空間使用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函式,可以理解為一個字元裝置的通用初始化函式,根據字元裝置的裝置號, 找到相應的字元裝置,從而得到操作該裝置的方法,程式碼實現如下。

./chrdev07.png

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_dev
static struct cdev chr_dev;
static int __init chrdev_init(void)
{
int ret = 0;
printk("chrdev init\n");
//第一步
//採用動態分配的方式,獲取裝置編號,次裝置號為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 devno\n");
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 cdev\n");
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 exit\n");
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("\nopen\n");
return 0;
}
static int chr_dev_release(struct inode *inode, struct file *filp)
{
printk("\nrelease\n");
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 <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
char *wbuf = "Hello World\n";
char rbuf[128];
int main(void)
{
printf("EmbedCharDev test\n");
//開啟檔案
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/kernel
CROSS_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-gcc

obj-m := chrdev.o
out = chrdev_test

all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
$(CROSS_COMPILE) -o $(out) main.c

.PHONY:clean
clean:
$(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.ko
cat /proc/devices

./chrdev08.png

我們從/proc/devices檔案中,可以看到我們註冊的字元裝置EmbedCharDev的主裝置號為234。 注意此主裝置號下面會用到,大家開發板根據實際指調整

mknod /dev/chrdev c 234 0

以root許可權使用mknod命令來建立一個新的裝置chrdev,見下圖。

./chrdev09.png

以root許可權執行chrdev_test,測試程式,效果見下圖。

./chrdev10.png

實際上,我們也可以透過echo或者cat命令,來測試我們的裝置驅動程式。

echo "EmbedCharDev test" > /dev/chrdev

當我們不需要該核心模組的時候,我們可以執行以下命令:

rmmod chrdev.ko

rm /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_dev
static 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("\nopen\n ");
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];
};
//字元裝置1
static struct chr_dev vcdev1;
//字元裝置2
static 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 init\n");
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 exit\n");
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("open\n");
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("release\n");
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.ko

sudo mknod /dev/chrdev1 c 234 0

sudo 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/chrdev1

cat /dev/chrdev2

可以看到裝置chrdev1中儲存了字串“hello world”,而裝置chrdev2中儲存了字串“123456”。 只需要幾行程式碼,就可以實現一個驅動程式,控制多個裝置。

總結一下,一個驅動支援多個裝置的具體實現方式的重點在於如何運用file的私有資料成員。 第一種方法是透過將各自的資料緩衝區放到該成員中,在讀寫函式的時候,直接就可以對相應的資料緩衝區進行操作; 第二種方法則是透過將我們的資料緩衝區和字元裝置結構體封裝到一起,由於檔案結構體inode的成員i_cdev儲存了對應字元裝置結構體, 使用container_of宏便可以獲得封裝後的結構體的地址,進而得到相應的資料緩衝區。

到這裡,字元裝置驅動就已經講解完畢了。如果你發現自己有好多不理解的地方,學完本章之後,建議重新梳理一下整個過程, 有助於加深對整個字元裝置驅動框架的理解。

相關文章