
眾所周知,Linux內核主要包括三種驅動模型,字符設備驅動,塊設備驅動以及網絡設備驅動。
其中,Linux字符設備驅動,可以說是Linux驅動開發中最常見的一種驅動模型。
我們該系列文章,主要為了幫助大家快速入門Linux驅動開發,該篇主要來了解一些字符設備驅動的框架和機制。
系列文章基于
Kernel 4.19
?
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;
結構體名稱:cdev
文件位置:include/linux/cdev.h
主要作用:cdev可以理解為char device,用來抽象一個字符設備。
核心成員及含義:
kobj:表示一個內核對象。owner:指向該模塊的指針ops:指向文件操作的指針,包括open、read、write等操作接口list:用于將該設備加入到內核模塊鏈表中dev:設備號,由主設備號和次設備號構成count:表示有多少個同類型設備,也間接表示設備號的范圍__randomize_layout:一個編譯器指令,用于隨機化結構體的布局,以增加安全性。?
struct?file_operations?{
????struct?module?*owner;
????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?*);
????int?(*iterate)?(struct?file?*,?struct?dir_context?*);
????int?(*iterate_shared)?(struct?file?*,?struct?dir_context?*);
????__poll_t?(*poll)?(struct?file?*,?struct?poll_table_struct?*);
????long?(*unlocked_ioctl)?(struct?file?*,?unsigned?int,?unsigned?long);
????long?(*compat_ioctl)?(struct?file?*,?unsigned?int,?unsigned?long);
????int?(*mmap)?(struct?file?*,?struct?vm_area_struct?*);
????unsigned?long?mmap_supported_flags;
????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);
????int?(*lock)?(struct?file?*,?int,?struct?file_lock?*);
????ssize_t?(*sendpage)?(struct?file?*,?struct?page?*,?int,?size_t,?loff_t?*,?int);
????unsigned?long?(*get_unmapped_area)(struct?file?*,?unsigned?long,?unsigned?long,?unsigned?long,?unsigned?long);
????int?(*check_flags)(int);
????int?(*flock)?(struct?file?*,?int,?struct?file_lock?*);
????ssize_t?(*splice_write)(struct?pipe_inode_info?*,?struct?file?*,?loff_t?*,?size_t,?unsigned?int);
????ssize_t?(*splice_read)(struct?file?*,?loff_t?*,?struct?pipe_inode_info?*,?size_t,?unsigned?int);
????int?(*setlease)(struct?file?*,?long,?struct?file_lock?**,?void?**);
????long?(*fallocate)(struct?file?*file,?int?mode,?loff_t?offset,
??????????????loff_t?len);
????void?(*show_fdinfo)(struct?seq_file?*m,?struct?file?*f);
#ifndef?CONFIG_MMU
????unsigned?(*mmap_capabilities)(struct?file?*);
#endif
????ssize_t?(*copy_file_range)(struct?file?*,?loff_t,?struct?file?*,
????????????loff_t,?size_t,?unsigned?int);
????int?(*clone_file_range)(struct?file?*,?loff_t,?struct?file?*,?loff_t,
????????????u64);
????int?(*dedupe_file_range)(struct?file?*,?loff_t,?struct?file?*,?loff_t,
????????????u64);
????int?(*fadvise)(struct?file?*,?loff_t,?loff_t,?int);
}?__randomize_layout;
結構體名稱:file_operations
文件位置:include/linux/fs.h
主要作用:正如其名,主要用來描述文件操作的各種接口,Linux一切接文件的思想,內核想要操作哪個文件,都需要通過這些接口來實現。
核心成員及含義:
open:打開文件的函數read:讀取文件的函數。write:寫入文件的函數。release:關閉文件的函數。flush:刷新文件的函數,通常在關閉文件時調用。llseek:改變文件讀寫指針位置的函數。fsync:將文件數據同步寫入磁盤的函數。poll:詢問文件是否可被非阻塞讀寫?
typedef?u32?__kernel_dev_t;
typedef?__kernel_dev_t??dev_t;
類型名稱:dev_t
文件位置:include/linux/types.h
主要作用:表示字符設備對應的設備號,其中包括主設備號和次設備號。
?

上圖繪制是對字符設備驅動程序的數據結構以及
API的關系圖,有需要原始文件,可在公~號【嵌入式藝術】獲取。
?
驅動首先實現的就是加載和卸載函數,也是驅動程序的入口函數。
我們一般這么定義驅動的加載卸載函數:
static?int?__init?xxx_init(void)
{
}
static?void?__exit?xxx_exit(void)
{
????
}
module_init(xxx_init);
module_exit(xxx_exit);
這段代碼就是實現一個通用驅動的加載與卸載,關于module_init和module_exit的實現機制,可以查看之前總結文章。
?
每一類字符設備都有一個唯一的設備號,其中設備號又分為主設備號和次設備號,那么這兩個分別作用是什么呢?
簡單來說,主設備號用于區分是
IIC設備還是SPI設備,而次設備號用于區分IIC設備下,具體哪一個設備,是MPU6050還是EEPROM。
?
了解了設備號的概念,
Linux中設備號有那么多,那么我們該如何去使用正確的設備號呢?
設備號的分配方式有兩種,一種是動態分配,另一種是靜態分配,也可以理解為一種是內核自動分配,一種是手動分配。
靜態分配函數:
int?register_chrdev_region(dev_t?from,?unsigned?count,?const?char?*name);
from:表示已知的一個設備號count:表示連續設備編號的個數,(同類型的設備有多少個)name:表示設備或者驅動的名稱函數作用:以from設備號開始,連續分配count個同類型的設備號
?
**動態分配函數**:
int?alloc_chrdev_region(dev_t?*dev,?unsigned?baseminor,?unsigned?count,?const?char?*name);
dev:設備號的指針,用于存放分配的設備號的值baseminor:次設備號開始分配的起始值count:表示連續設備編號的個數,(同類型的設備有多少個)name:表示設備或者驅動的名稱函數作用:從baseminor次設備號開始,連續分配count個同類型的設備號,并自動分配一個主設備號,將主、次組成的設備號信息賦值給*dev
?
**這兩個函數最大的區別在于**:
register_chrdev_region:調用前,已預先定義好了主設備號和次設備號,調用該接口后,會將自定義的設備號登記加入子系統中,方便系統追蹤系統設備號的使用情況。alloc_chrdev_region:調用前,未定義主設備號和次設備號;調用后,主設備號以0來表示,以自動分配,并且將自動分配的設備號,同樣加入到子系統中,方便系統追蹤系統設備號的使用情況。?
這兩個函數的共同點在于:
系統維護了一個數組列表,用來登記所有的已使用的設備號信息,這兩個接口歸根到底也是將其設備號信息,登記到系統維護的設備號列表中,以免后續沖突使用。
在Linux中,我們可以通過cat /proc/devices命令,查看所有i登記的設備號列表。
?
后面有時間,我們可以詳細聊設備號的自動分配機制,管理機制。
?
設備號作為一種系統資源,當所對應的設備卸載時,當然也要將其所占用的設備號歸還給系統,無論時靜態分配,還是動態分配,最終都是調用下面函數來注銷的。
void?unregister_chrdev_region(dev_t?from,?unsigned?count);
from:表示已知的一個設備號count:表示連續設備編號的個數,(同類型的設備有多少個)函數作用:要注銷from主設備號下的連續count個設備
?
設備號的管理很簡單,在關鍵數據結構中,我們看到設備號的類型是dev_t,也就是u32類型表示的一個數值。
其中主設備號和次設備號的分界線,由MINORBITS宏定義指定:
#define?MINORBITS?20
也就是主設備號占用高12bit,次設備號占用低20bit
并且,內核還提供了相關API接口,來獲取主設備號和次設備號,以及生成設備號的接口,如下:
#define?MINORMASK?((1U?<
#define?MAJOR(dev)?((unsigned?int)?((dev)?>>?MINORBITS))
#define?MINOR(dev)?((unsigned?int)?((dev)?&?MINORMASK))
#define?MKDEV(ma,mi)?(((ma)?<
以上,通過移位操作,來實現主次設備號的獲取。
?
#define?CUSTOM_DEVICE_NUM?0
#define?DEVICE_NUM?1
#device?DEVICE_NAME?"XXXXXX"
static?dev_t?global_custom_major?=?CUSTOM_DEVICE_NUM;
static?int?__init?xxx_init(void)
{
????dev_t?custom_device_number=?MKDEV(global_custom_major,?0);?//?custom?device?number
????/*?device?number?register*/
????if?(global_custom_major)?{
????????ret?=?register_chrdev_region(custom_device_number,?DEVICE_NUM,?DEVICE_NAME);
????}?else?{
????????ret?=?alloc_chrdev_region(&custom_device_number,?0,?DEVICE_NUM,?DEVICE_NAME);
????????global_custom_major?=?MAJOR(custom_device_number);
????}
}
static?void?__exit?xxx_exit(void)
{
????unregister_chrdev_region(MKDEV(global_mem_major,?0),?DEVICE_NUM);
}
module_init(xxx_init);
module_exit(xxx_exit);
該函數實現了設備號的分配,如果主設備號為0,則采用動態配分的方式,否則采用靜態分配的方式。
?
更多干貨可見:高級工程師聚集地,助力大家更上一層樓!
?
了解完設備號的管理之后,我們來看下字符設備是如何管理的。
void?cdev_init(struct?cdev?*cdev,?const?struct?file_operations?*fops);
cdev:一個字符設備對象,也就是我們創建好的字符設備fops:該字符設備的文件處理接口函數作用:初始化一個字符設備,并且將所對應的文件處理指針與字符設備綁定起來。
?
int?cdev_add(struct?cdev?*p,?dev_t?dev,?unsigned?count);
p:一個字符設備指針,只想待添加的字符設備對象dev:該字符設備所負責的第一個設備編號count:該類型設備的個數函數作用:添加一個字符設備驅動到Linux系統中。
?
void?cdev_del(struct?cdev?*p);
p:指向字符設備對象的指針函數作用:從系統中移除該字符設備驅動
?
因為在Linux中,一切皆文件的思想,所以每一個字符設備,也都有一個文件節點來對應。
我們在初始化字符設備的時候,會將struct file_operations的對象與字符設備進行綁定,其作用是來處理該字符設備的open、read、write等操作。
我們要做的就是去實現我們需要的函數接口,如:
static?const?struct?file_operations?global_mem_fops?=?{
????.owner?=?THIS_MODULE,
????.llseek?=?global_mem_llseek,
????.read?=?global_mem_read,
????.write?=?global_mem_write,
????.unlocked_ioctl?=?global_mem_ioctl,
????.open?=?global_mem_open,
????.release?=?global_mem_release,
};
至此,我們一個基本的字符設備驅動程序的框架就基本了然于胸了
?
本篇文章,旨在通俗易懂的講解:
API接口希望對大家有所幫助。

