Linux字符設(shè)備驅(qū)動內(nèi)幕

哈嘍,我是老吳,繼續(xù)記錄我的學(xué)習(xí)心得。
一、保持專注的幾個技巧
將最重要的事放在早上做。
待在無干擾環(huán)境下,比如圖書館。
意識到剛坐下開始投入工作前,有點負面小情緒是特別正常的現(xiàn)象。
讓“開心一刻”成為計劃的一部分。
擁有合情合理的日計劃和周計劃。
二、Linux 字符設(shè)備驅(qū)動內(nèi)幕 (1)
正文目錄:
1. 什么是字符設(shè)備驅(qū)動?
2.?快速體驗字符設(shè)備驅(qū)動和應(yīng)用程序?(超簡單的?demo)
3.?字符設(shè)備在內(nèi)核里的抽象
????3.1?字符設(shè)備核心代碼概覽
????3.2?對字符設(shè)備進行抽象:?struct?cdev
??? 3.3 對字符設(shè)備的操作進行抽象:struct file_operations
4.?更多值得學(xué)習(xí)的知識點
5.?相關(guān)參考
寫作目的:
探索 Linux 字符設(shè)備驅(qū)動。
測試環(huán)境:
Ubuntu 16.04 Gcc 5.4.0
1. 什么是字符設(shè)備驅(qū)動?
現(xiàn)實世界中存在著成千上萬的硬件設(shè)備,這些設(shè)備在硬件特性和使用方式上都各不相同。Linux 系統(tǒng)的大牛們從這些形形色色的設(shè)備中提取出共性,將它們抽象為三大類:字符設(shè)備、塊設(shè)備和網(wǎng)絡(luò)設(shè)備。 
基于代碼質(zhì)量和復(fù)用性的考慮,Linux 內(nèi)核針對每一類硬件設(shè)備都提供了對應(yīng)的驅(qū)動模型框架,一般包括基本的內(nèi)核設(shè)施和文件系統(tǒng)接口。開發(fā)人員在寫某類設(shè)備驅(qū)動程序時,能一套完整的驅(qū)動模型框架可以使用,從而將精力放在硬件設(shè)備本身的控制上。
簡單的 Linux 設(shè)備驅(qū)動程序結(jié)構(gòu)圖: 
詳細一點的 Linux 設(shè)備驅(qū)動程序結(jié)構(gòu)圖:

2. 快速體驗字符設(shè)備驅(qū)動和應(yīng)用程序 (超簡單的 demo)
1) 字符設(shè)備驅(qū)動 (chrdev_drv.c):
字符設(shè)備的打開和讀函數(shù):
static?struct?cdev?chr_dev;?//?字符設(shè)備抽象
static?dev_t?ndev;??????????//?設(shè)備號
static?int?chr_open(struct?inode?*nd,?struct?file?*filp)
{
????printk("chr_open,?major=%d,?minor=%d\n",?MAJOR(nd->i_rdev),?MINOR(nd->i_rdev));
????return?0;
}
static?ssize_t?chr_read(struct?file?*filp,?char?__user?*u,?size_t?sz,?loff_t?*off)
{
????printk("In?chr_read()\n");
????return?0;
}
static?int?chr_release(struct?inode?*nd,?struct?file?*filp)
{
????printk("In?chr_release()\n");
????return?0;
}
文件操作函數(shù)集:
struct?file_operations?chr_ops?=
{
????.owner?=?THIS_MODULE,
????.open?=?chr_open,
????.read?=?chr_read,
????.release?=?chr_release,
};
模塊加載和卸載:
static?int?demo_init(void)
{
????int?ret;
????cdev_init(&chr_dev,?&chr_ops);
????ret?=?alloc_chrdev_region(&ndev,?0,?1,?"chr_dev");
????if(ret?0)
????????return?ret;
????printk("demo_init():major=%d,?minor=%d\n",?MAJOR(ndev),?MINOR(ndev));
????ret?=?cdev_add(&chr_dev,?ndev,?1);
????if(ret?0)
????????return?ret;
????return?0;
}
static?void?demo_exit(void)
{
????printk("demo_exit...\n");
????cdev_del(&chr_dev);
????unregister_chrdev_region(ndev,?1);
}
2) 訪問字符設(shè)備的應(yīng)用程序:
int?main()
{
????int?ret;
????char?buf[32];
????int?fd?=?open("/dev/chr_dev",?O_RDONLY|O_NDELAY);
????if(fd?0)
????{
????????printf("open?file?%s?failed!\n",?CHR_DEV_NAME);
????????return?-1;
????}
????read(fd,?buf,?32);
????close(fd);
????return?0;
}
3) 不用完全理解代碼的含義,直接看運行效果:
#?編譯驅(qū)動程序
$?make?KERNELDIR=XXX/linux?ARCH=arm?CROSS_COMPILE=arm-linux-
#?編譯應(yīng)用程序
$?arm-linux-gcc?chrdev_app.c?-o?chrdev_app
#?加載驅(qū)動模塊
$?insmod?chrdev_drv.ko
demo_init():major=242,?minor=0
#?手動創(chuàng)建字符設(shè)備文件節(jié)點
$?mknod?/dev/chr_dev?c?242?0
#?運行應(yīng)用程序
$?./chrdev_app
chr_open,?major=242,?minor=0
In?chr_read()
In?chr_release()
從上面測運行結(jié)果可知,應(yīng)用程序調(diào)用 chrdev_app.c / open() 會導(dǎo)致驅(qū)動程序 chrdev_drv.c / struct file_operations chr_ops->open() 被調(diào)用,read 操作也是類似。
內(nèi)核是如何實現(xiàn)上述功能的?
帶著這個困惑來了解字符設(shè)備驅(qū)動的框架,才不會迷失在內(nèi)核里各種復(fù)雜的代碼細節(jié)里。
3 字符設(shè)備在內(nèi)核里的抽象
3.1 字符設(shè)備核心代碼概覽
在深入閱讀各種代碼之前,先整體地概覽一遍將會涉及到的程序文件,找出核心主干,能有效地避免陷入繁雜的代碼細節(jié)中。
1) 分解 C source 文件,fs/char_dev.c (Linux-4.14):
作用:
char_dev.c 是字符設(shè)備驅(qū)動框架的核心實現(xiàn)文件,它位于 fs 目錄中,說明了字符設(shè)備驅(qū)動和文件系統(tǒng)是緊密聯(lián)系在一起的。
內(nèi)容(以重要性排序):
1> public 函數(shù):
//?1.?字符設(shè)備子系統(tǒng)初始化
void?__init?chrdev_init(void)?
//?2.?struct?cdev?管理相關(guān)
void?chrdev_show(struct?seq_file?*f,?off_t?offset)?
void?cdev_put(struct?cdev?*p)?
void?cd_forget(struct?inode?*inode)?
int?cdev_add(struct?cdev?*p,?dev_t?dev,?unsigned?count)?
void?cdev_set_parent(struct?cdev?*p,?struct?kobject?*kobj)?
int?cdev_device_add(struct?cdev?*cdev,?struct?device?*dev)?
void?cdev_device_del(struct?cdev?*cdev,?struct?device?*dev)?
void?cdev_del(struct?cdev?*p)?
struct?cdev?*cdev_alloc(void)?
void?cdev_init(struct?cdev?*cdev,?const?struct?file_operations?*fops)
//?3.?設(shè)備號管理相關(guān)
__register_chrdev_region(unsigned?int?major,?unsigned?int?baseminor,?
__unregister_chrdev_region(unsigned?major,?unsigned?baseminor,?int?minorct)?
int?register_chrdev_region(dev_t?from,?unsigned?count,?const?char?*name)?
int?alloc_chrdev_region(dev_t?*dev,?unsigned?baseminor,?unsigned?count,?
int?__register_chrdev(unsigned?int?major,?unsigned?int?baseminor,?
void?unregister_chrdev_region(dev_t?from,?unsigned?count)?
void?__unregister_chrdev(unsigned?int?major,?unsigned?int?baseminor,
在 Linux 代碼中,雙下劃線(__)開始的函數(shù)名表示這是一個低層接口, 應(yīng)當(dāng)小心使用。如果你調(diào)用這個函數(shù),確信你知道你在做什么。也就是說,閱讀代碼時,雙下劃線開頭的函數(shù)可以暫時放一邊。
struct cdev 管理相關(guān):cdev_add() / cdev_del() / cdev_init(),需重點關(guān)注。
設(shè)備號管理相關(guān):alloc_chrdev_region() / register_chrdev_region(),需重點關(guān)注。
2> public 變量:
const?struct?file_operations?def_chr_fops?=?{
?.open?=?chrdev_open,
?.llseek?=?noop_llseek,
};
類似于高級語言的多態(tài)機制,在 Linux 各個子系統(tǒng)的驅(qū)動框架中一般用一個統(tǒng)一的 file_operations->open 函數(shù)關(guān)聯(lián)到各個具體的硬件驅(qū)動的 struct file_operations,需要重點關(guān)注。
3> private 變量:
static?struct?char_device_struct?{
?struct?char_device_struct?*next;
?unsigned?int?major;
?unsigned?int?baseminor;
?int?minorct;
?char?name[64];
?struct?cdev?*cdev;??/*?will?die?*/
}?*chrdevs[CHRDEV_MAJOR_HASH_SIZE];
static?struct?kobj_map?*cdev_map;?
static?struct?kobj_type?ktype_cdev_default;
static?struct?kobj_type?ktype_cdev_dynamic;
struct char_device_struct *chrdevs[] 數(shù)組是字符設(shè)備驅(qū)動框架的核心數(shù)據(jù)結(jié)構(gòu),需重點關(guān)注。
4> private 函數(shù):
static?inline?int?major_to_index(unsigned?major)?
static?int?find_dynamic_major(void)?
static?struct?kobject?*cdev_get(struct?cdev?*p)?
static?int?chrdev_open(struct?inode?*inode,?struct?file?*filp)?
static?void?cdev_purge(struct?cdev?*cdev)?
static?struct?kobject?*exact_match(dev_t?dev,?int?*part,?void?*data)?
static?int?exact_lock(dev_t?dev,?void?*data)?
static?void?cdev_unmap(dev_t?dev,?unsigned?count)?
static?void?cdev_default_release(struct?kobject?*kobj)?
static?void?cdev_dynamic_release(struct?kobject?*kobj)?
static?struct?kobject?*base_probe(dev_t?dev,?int?*part,?void?*data)
helper 類函數(shù),不太重要。
2) 分解 C header 文件,include/linux/cdev.h (Linux-4.14):
作用:
包含字符設(shè)備驅(qū)動相關(guān)結(jié)構(gòu)體的定義、以及一些字符設(shè)備核心 API 的聲明。
內(nèi)容:
1> struct cdev 結(jié)構(gòu)體:
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 cdev 是字符設(shè)備驅(qū)動的核心抽象,需重點關(guān)注。
3.2 對字符設(shè)備進行抽象: struct cdev
編寫字符設(shè)備驅(qū)動程序就是為了管理和控制字符設(shè)備,Linux 內(nèi)核將字符設(shè)備抽象為一 個數(shù)據(jù)結(jié)構(gòu):struct cdev。這個結(jié)構(gòu)體似乎從 Linux-2.6 到現(xiàn)在 Linux-5.8 都沒有發(fā)生變化。
1) struct cdev 成員簡介:
目前沒必要完全理解這些成員的作用,有個大概印象就好:
struct kobject kobj: 內(nèi)嵌的內(nèi)核對象,與 Linux 設(shè)備驅(qū)動模型相關(guān),后續(xù)需專門寫一篇文章來介紹。
struct module *owner: 指向擁有這個結(jié)構(gòu)體的模塊的指針,這個成員用來在它的操作還在被使用時阻止模塊被卸載,一般初始化為 THIS_MODULE;
struct file_operations *ops: 在Linux通用文件模型下,字符設(shè)備的文件操作函數(shù)集,后續(xù)詳解。
struct list_head list:用于管理 struct cdev 的鏈表,后續(xù)需專門寫一篇文章來介紹。
字符設(shè)備的設(shè)備號,由主設(shè)備號和次設(shè)備號構(gòu)成,后續(xù)需專門寫一篇文章來介紹。
2) 兩種方式創(chuàng)建 struct cdev 對象:
這里用對象一詞,是為了引導(dǎo)大家用面向?qū)ο蟮乃季S來看待 Linux 內(nèi)核的設(shè)計。
面向過程還是面向?qū)ο螅粦?yīng)該和語言綁定在一起,應(yīng)該理解為 2 種不同的編程思維。人腦是很美妙的,在不同的場景,只要你愿意,它就能應(yīng)用不同的思維方式來解決問題。設(shè)計 Linux 內(nèi)核代碼的神牛們,可謂是各個都是面向?qū)ο缶幊痰拇笈#毩?xí)編程就應(yīng)該練習(xí)對事物的抽象能力,C 程序員如果覺得自己缺乏這方面的能力,不如學(xué)習(xí)一下 Java 編程。
靜態(tài)定義:
static?struct?cdev?chr_dev;
動態(tài)分配:
struct?cdev?*my_cdev?=?cdev_alloc();
cdev_alloc() 不僅會為struct cdev對象分配內(nèi)存空間,還會對該對象進行必要的初始化:
struct?cdev?*cdev_alloc(void)
{
?struct?cdev?*p?=?kzalloc(sizeof(struct?cdev),?GFP_KERNEL);
?if?(p)?{
??INIT_LIST_HEAD(&p->list);
??kobject_init(&p->kobj,?&ktype_cdev_dynamic);
?}
?return?p;
}
一個值得注意的點:
我搜索了一下內(nèi)核源碼,發(fā)現(xiàn)大多數(shù)驅(qū)動都選擇靜態(tài)定義 struct cdev,這樣更簡單省事。
3) 某個真實字符硬件的抽象:
數(shù)據(jù)結(jié)構(gòu) struct cdev 作為字符設(shè)備的抽象,僅僅是為了滿足 Linux 內(nèi)核對字符設(shè)備驅(qū)動程序框架結(jié)構(gòu)設(shè)計的需要。
現(xiàn)實中,一個具體的字符硬件設(shè)備的數(shù)據(jù)結(jié)構(gòu)的抽象往往要復(fù)雜得多,在這種情況下 struct cdev 常常作為一種內(nèi)嵌的成員變量出現(xiàn)在實際設(shè)備的數(shù)據(jù)結(jié)構(gòu)中。
例如 drvier/watchdog/watchdog_dev.c:
struct?watchdog_core_data?{
?struct?kref?kref;
?struct?cdev?cdev;
?struct?watchdog_device?*wdd;
?struct?mutex?lock;
?unsigned?long?last_keepalive;
?unsigned?long?last_hw_keepalive;
?struct?delayed_work?work;
?unsigned?long?status;??/*?Internal?status?bits?*/
};
4) 初始化 cdev 對象:
void?cdev_init(struct?cdev?*cdev,?const?struct?file_operations?*fops)
{
?memset(cdev,?0,?sizeof?*cdev);
?INIT_LIST_HEAD(&cdev->list);
?kobject_init(&cdev->kobj,?&ktype_cdev_default);
?cdev->ops?=?fops;
}
cdev_init() 最重要的作用就是將 struct cdev 對象和 struct file_operations 對象綁定在一起。
一些值得注意的點:
cdev_init() 和 cdev_alloc() 中有一部分功能是重疊的(例如 kobject_init()),所以 cdev_init() 只能搭配靜態(tài)定義 struct cdev 的方式來使用。
如果采用 cdev_alloc() 動態(tài)分配 struct cdev 對象,則需自行 cdev->ops = fops 。
3.3 對字符設(shè)備的操作進行抽象:struct file_operations
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?(*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);
?int?(*lock)?(struct?file?*,?int,?struct?file_lock?*);
?[...];
}?__randomize_layout;
目前沒必要完全理解這些成員的作用,有個大概印象就好。
字符設(shè)備驅(qū)動程序中一個極其關(guān)鍵的數(shù)據(jù)結(jié)構(gòu),字符設(shè)備驅(qū)動程序的編寫,就是是圍繞著如何實現(xiàn) struct file_operations 中的那些函數(shù)指針成員而展開的。
通過內(nèi)核文件系統(tǒng)組件在其間的穿針引線,應(yīng)用程序中對文件類函數(shù)(open、read、write)的調(diào)用,將最終被轉(zhuǎn)接到 struct file_operations 中對應(yīng)函數(shù)指針的具體實現(xiàn)上,后續(xù)需專門寫一篇文章來介紹。
鑒于大多數(shù)人的注意力無法在一篇文章里上集中太久,更多的內(nèi)容將放在后面的文章里。建議大家可以先自行閱讀相關(guān)書籍,不是自己理解到的東西是消化不了的。
4. 更多值得學(xué)習(xí)的知識點
字符設(shè)備號的構(gòu)成與管理
字符設(shè)備的注冊
生成字符設(shè)備文件的方式有哪些?
創(chuàng)建字符設(shè)備文件是發(fā)生了什么?
字符設(shè)備文件是如何關(guān)聯(lián)到字符設(shè)備驅(qū)動的?
分析一些真實的字符設(shè)備驅(qū)動
5. 相關(guān)參考
《Linux 內(nèi)核文檔》
《Linux設(shè)備驅(qū)動程序》(ldd) / 第 3 章節(jié)
《深入Linux設(shè)備驅(qū)動程序內(nèi)核機制》(ildd) / 第 2 章節(jié)
《精通Linux設(shè)備驅(qū)動程序開發(fā)》(eldd) / 第 2 章節(jié)
《Linux設(shè)備驅(qū)動開發(fā)詳解》(ldds) / 第 6 章節(jié)
《深入Linux內(nèi)核架構(gòu)》(plka) / 第 6 章節(jié)
《嵌入式應(yīng)用開發(fā)完全手冊》

