linux內(nèi)核驅(qū)動中面向?qū)ο蟮幕疽?guī)則和實(shí)現(xiàn)方法
- 內(nèi)核版本 Linux Kernel 2.6.34, 與 Robert.Love的《Linux Kernel Development》(第三版)所講述的內(nèi)核版本一樣
- 源代碼下載路徑: https://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.34.tar.bz2
- 眾所周知,現(xiàn)代的軟件項(xiàng)目變得越來越復(fù)雜,Linux內(nèi)核更是世界上最大最復(fù)雜的軟件工程。引用自《代碼大全》中的觀點(diǎn),面向?qū)ο蟮脑O(shè)計(jì)思想是一種管理軟件系統(tǒng)復(fù)雜性的手段。
- 人的精力有限,一個程序員在同一時間,只專注于軟件系統(tǒng)的一小部分,才能最大的發(fā)揮工作效率。構(gòu)建軟件就像設(shè)計(jì)大型建筑,一個時間點(diǎn)上,我們聚焦在建筑結(jié)構(gòu)藍(lán)圖設(shè)計(jì)時,就不要過分地將注意力分散的建筑內(nèi)的電線布局,第N層的排水管道如何施工這些細(xì)節(jié)問題上,而是應(yīng)該聚焦在整體設(shè)計(jì)上。
- 面向?qū)ο笏枷刖褪且环N在代碼編寫之上的軟件系統(tǒng)結(jié)構(gòu)設(shè)計(jì)的思想。面向?qū)ο蟮恼Z言用于描述系統(tǒng)框架結(jié)構(gòu)是怎么樣的,需要什么模塊,模塊之間的關(guān)系如何,如何遵守開閉準(zhǔn)則,方便后期的維護(hù)和開發(fā)等一些設(shè)計(jì)意圖層次上的問題。
- 面向?qū)ο蟮脑O(shè)計(jì)思想不太關(guān)心xxx函數(shù)實(shí)現(xiàn)如何初始化,要注冊什么結(jié)構(gòu),要把xxx寄存器狀態(tài)設(shè)置為0等流程細(xì)節(jié)的問題。這些應(yīng)該是屬于面向過程設(shè)計(jì)時的問題。
- 面向過程與面向?qū)ο蟮乃枷胗猛静煌瑳]有好壞之分。面向?qū)ο笏枷敫鼉A向于程序之上的頂層設(shè)計(jì)與程序系統(tǒng)結(jié)構(gòu)設(shè)計(jì),然后真正要實(shí)現(xiàn)一個函數(shù)細(xì)節(jié)的時候,還是需要面向過程地分析細(xì)節(jié)如何實(shí)現(xiàn),需要初始化哪些變量,注冊哪些結(jié)構(gòu),設(shè)置哪些寄存器等面向過程的問題。
- 面向?qū)ο蟮乃枷牒驼Z言無關(guān),并不是C++或者java 、Python等語言才有的。面向?qū)ο笏枷耄请S著軟件系統(tǒng)的復(fù)雜度越來越高,面對大規(guī)模軟件系統(tǒng)設(shè)計(jì)的問題,而提出的一種管理大型軟件系統(tǒng)設(shè)計(jì)的思想。只是在C語言出現(xiàn)時,計(jì)算機(jī)軟硬件系統(tǒng)還在起步階段,面向?qū)ο蟮乃枷肷形窗l(fā)展(可能Keep it simple and stupid 原則也是讓C語言語法盡量保持簡單的原因),因而C語言中缺乏面向?qū)ο笙嚓P(guān)的核心關(guān)鍵詞語法的支持。而JAVA、Python等一些1990年代之后問世的語言,受到C++語言影響以及面向?qū)ο笏枷氲闹饾u流行,在語法層面就提供了面向?qū)ο蟮暮诵年P(guān)鍵詞支持,可以說在處理面向?qū)ο髥栴}上具有先天優(yōu)勢。
- 雖然C語言不支持很多面向?qū)ο蟮暮诵年P(guān)鍵詞,但是隨著Linux內(nèi)核,F(xiàn)fmpeg,Nginx等大規(guī)模以C語言編寫的開源軟件項(xiàng)目的發(fā)展與推廣,C語言遇到的軟件復(fù)雜度增加以及系統(tǒng)設(shè)計(jì)與系統(tǒng)長期維護(hù)的問題,與JAVA、C++編程遇到的復(fù)雜度問題是想通的。并且,面向?qū)ο笏枷胍彩怯捎陂_發(fā)者們在開發(fā)過程中遇到瓶頸才提出來的,這些問題,不管是用C語言編程還是JAVA編程,都會客觀存在。因而用C語言模擬JAVA等面向?qū)ο蟮恼Z言,采用面向?qū)ο蟮乃枷脒M(jìn)行系統(tǒng)頂層設(shè)計(jì)是很有必要的。
- JAVA是一種類似C語言的命令式語言(用于區(qū)別Lisp等函數(shù)式編程語言),并且是設(shè)計(jì)良好的純面向?qū)ο蟮恼Z言。JAVA有不少關(guān)鍵詞與C語言相同,并且JAVA標(biāo)準(zhǔn)制定委員會的很多成員也是C/C++標(biāo)準(zhǔn)制定委員會的。因而借鑒JAVA的面向?qū)ο笏枷耄治鼍帉懨嫦驅(qū)ο蟮腃語言程序,是相當(dāng)有幫助的。JAVA不同于C++, C++現(xiàn)在的定位是多種范式語言,細(xì)節(jié)太多,在面向?qū)ο筇匦缘脑O(shè)計(jì)上也不夠友好方便。所以我更傾向于借鑒JAVA的編程思想來分析編程面向?qū)ο蟮腃語言代碼。
- 文章中很多面向?qū)ο笏枷氲某鎏帲紒碜訨AVA。作為Linux和C語言嵌入式系統(tǒng)相關(guān)的開發(fā)者,可能大部分缺乏JAVA的編程經(jīng)驗(yàn)。所以希望讀者抽空學(xué)習(xí)一下JAVA,做一些小練習(xí),拓寬編程的知識面,這樣更能夠領(lǐng)會面向?qū)ο笤O(shè)計(jì)的精髓,還能提出一些自己的新的看法。我認(rèn)為,JAVA實(shí)際上是在C語言基礎(chǔ)上的的一種改進(jìn),很多關(guān)鍵詞與C相同,但是又彌補(bǔ)了C語言的不少缺陷,并且專注在面向?qū)ο蟮脑O(shè)計(jì)上。
- 本文也是希望起到拋磚引玉的作用,如有概念性問題,還請批評指正。
- 將面向?qū)ο蟮母拍钜牖贚inux系統(tǒng)的C語言程序開發(fā)是很有必要的。雖然面向?qū)ο笏枷霑砗芏嘈碌母拍钚g(shù)語(繼承,多態(tài)等),很多做Linux驅(qū)動開發(fā)的工程師都是電子工程相關(guān)專業(yè)出身,這些概念可能剛接觸會稍顯晦澀,但是習(xí)慣性以面向?qū)ο笏枷敕治龃笠?guī)模軟件系統(tǒng)之后,能夠幫你快速得掌握整個系統(tǒng)的結(jié)構(gòu)以及原作者的設(shè)計(jì)意圖,而不會陷入到一個個API的實(shí)現(xiàn)細(xì)節(jié)中,只見樹木,不見森林。接手維護(hù)一個大型軟件項(xiàng)目,首先要知道原作者的意圖,即知道原作者為什么要這么編程,才能理清楚軟件設(shè)計(jì)思路,進(jìn)而修改擴(kuò)展源代碼。
- 面向?qū)ο蟮男g(shù)語,是一種通用的規(guī)則,大家都掌握該規(guī)則,然后按照規(guī)則上的術(shù)語溝通,就能夠用簡短的語言表述出程序的意圖。例如面向?qū)ο笏枷胫械男g(shù)語——繼承基類,實(shí)現(xiàn)多態(tài)函數(shù),如果用面向過程思想描述就是——定義一個XXX結(jié)構(gòu)體,再定義XX和YY函數(shù),用XX和YY函數(shù)填充XXX結(jié)構(gòu)體中的A函數(shù)指針和B函數(shù)指針,再在初始化函數(shù)中調(diào)用C函數(shù)注冊這個XXX結(jié)構(gòu)體。不同思想所用術(shù)語的繁簡程度,高下立判。過長的語言描述,容易帶來更多的誤解,信息丟失,理解不全等溝通障礙,這時,專用術(shù)語的優(yōu)勢就體現(xiàn)出來了。
- 面向?qū)ο笏枷胪驮O(shè)計(jì)模式是分不開的。比如Linux內(nèi)核中的通知鏈系統(tǒng),用設(shè)計(jì)模式的術(shù)語來說,叫觀察者模式,有學(xué)過該設(shè)計(jì)模式的讀者,立馬明白程序大概做了什么。但是如果不了解這一套語言溝通規(guī)則,就代碼講代碼,可能又是一堆這樣的過程描述性語言——定義一個xxx頭,定義xx結(jié)構(gòu)體,設(shè)置xx結(jié)構(gòu)體的回調(diào)函數(shù),回調(diào)函數(shù)輸入?yún)?shù)是什么,返回什么,注冊xx結(jié)構(gòu)到xxx頭……
- 單純地學(xué)習(xí)面向?qū)ο笏枷牖蛘咴O(shè)計(jì)模式,如何不結(jié)合實(shí)際的代碼來分析具體案例,過一段時間可能就會忘記書上講了什么東西。
- 平時編寫小規(guī)模程序時候,只需要一個人,不需要面向?qū)ο蟮乃枷刖湍芡瓿桑蚨X得面向?qū)ο筮@些東西都是書上的理論,實(shí)際上又用不著,但是一旦遇到Linux內(nèi)核源碼這種復(fù)雜級別的程序,就不知如何下手,容易一臉懵逼。
- Linux內(nèi)核是世界上最大的軟件工程項(xiàng)目之一,經(jīng)過了20多年的發(fā)展和完善,內(nèi)部子系統(tǒng)結(jié)構(gòu)經(jīng)過了不斷重構(gòu)(refracting)和反復(fù)迭代設(shè)計(jì),其代碼質(zhì)量也是越來越高,這其中肯定從面向?qū)ο蟮木幊趟枷胫薪梃b模仿了很多東西。當(dāng)然閱讀Linux內(nèi)核源碼時,由于歷史原因,還是會有很多代碼的命名,架構(gòu)和設(shè)計(jì)風(fēng)格不那么完美(例如videobuf的第一版)。所以閱讀內(nèi)核源碼的時候,要學(xué)會其精華,同時也要拋棄一些不良的設(shè)計(jì)。
- JAVA Python等面向?qū)ο笳Z言有大量的開源框架,讓我們從實(shí)戰(zhàn)中體驗(yàn)面向?qū)ο笏枷牒驮O(shè)計(jì)模式。C語言的框架類庫可能沒有JAVA那么豐富,但是Linux內(nèi)核作為C語言的代表作品,其中的設(shè)計(jì)思想是很值得用面向?qū)ο蟮恼Z言分析一遍的,尤其是設(shè)備驅(qū)動相關(guān)的代碼,有很多層抽象才變成了用戶態(tài)都喜愛的文件。
- 其實(shí)拋開Linux內(nèi)核中動態(tài)運(yùn)行,維護(hù)系統(tǒng)運(yùn)轉(zhuǎn)的線程,其中大部分驅(qū)動代碼都是靜態(tài)存在于內(nèi)存,等待被調(diào)用的。這樣看來Linux內(nèi)核中維持系統(tǒng)運(yùn)轉(zhuǎn)的進(jìn)程就像JAVA虛擬機(jī),而內(nèi)核中等待被調(diào)用的代碼,就像JDK的框架和類庫,需要用戶態(tài)去調(diào)用,也需要內(nèi)核態(tài)驅(qū)動開發(fā)者利用框架進(jìn)一步擴(kuò)展。這樣看來JDK與Linux內(nèi)核設(shè)計(jì)思想也有就有了很大共通之處,用面向?qū)ο笏枷敕治鯨inux內(nèi)核設(shè)備驅(qū)動也就是一種高屋建瓴,了解各個子系統(tǒng)結(jié)構(gòu)框架的通用思想。
- 網(wǎng)絡(luò)上有一些C語言面向?qū)ο笏枷刖幊痰奈恼拢侵皇橇懔闵⑸⒌卣砹艘恍┯^點(diǎn),不夠系統(tǒng)化,案例也過于簡單。所以我希望結(jié)合Linux內(nèi)核這個大型的實(shí)際軟件系統(tǒng),更加系統(tǒng)化地在C語言編程中描述和應(yīng)用面向?qū)ο笏枷搿?/p>
- 綜上所述,作為Linux系統(tǒng)C語言開發(fā)者,帶著面向?qū)ο蟮乃枷耄瑥牟煌囊暯莵韺W(xué)習(xí)Linux內(nèi)核吧!
- 熟悉結(jié)構(gòu)化C語言編程的讀者,對抽象與封裝應(yīng)該不陌生,在此簡要帶過,抽象和封裝是面向?qū)ο缶幊趟枷氲幕A(chǔ)。
- 抽象即抽出事物最本質(zhì)的特征來從某個方面描述這個事物。例如,牧羊犬和藏獒,它們抽象出來都是狗,都有會“汪汪汪”叫,會吃骨頭等狗所擁有的行為特征。對于不需要分辨其到底是牧羊犬還是藏獒的用戶,只需要知道一個物體是狗,那么肯定會聯(lián)想到它會“汪汪汪”地叫這個特點(diǎn)。
- 在C語言數(shù)據(jù)結(jié)構(gòu)中,我們所描述的ADT抽象數(shù)據(jù)類型,就是對各種數(shù)據(jù)對象模型的抽象,在此不多累述。
- 封裝,在C語言編程中,大部分時候用一個函數(shù)調(diào)用(API)將一個復(fù)雜過程的細(xì)節(jié)屏蔽起來,用戶不需要了解細(xì)節(jié),只需要調(diào)用該函數(shù)就能實(shí)現(xiàn)相應(yīng)的行為。例如吃飯函數(shù),將盛飯,動筷子,夾菜,張嘴,咀嚼,下咽等細(xì)節(jié)屏蔽起來,我們只需要調(diào)用吃飯函數(shù),默認(rèn)就實(shí)現(xiàn)了一遍這樣的流程。
- 面向?qū)ο笏枷胫械姆庋b使用更廣泛,即一個對象類(C語言中用結(jié)構(gòu)體代替),需要隱藏用戶不需要也不應(yīng)該知道的行為和屬性。用戶在訪問對象時,不需要了解被封裝的對象和屬性,就能使用該對象類,同時對象類也應(yīng)該通過權(quán)限設(shè)置,禁止用戶過多地了解被封裝的對象屬性與行為。
- 總之,抽象與封裝的思想都是為了讓用戶不需要了解對象過多的細(xì)節(jié),就能直接通過API來使用對象,從而達(dá)到模塊化編程,,程序員分工合作,各自負(fù)責(zé)維護(hù)自己負(fù)責(zé)模塊對象細(xì)節(jié)的作用。
- 在面向?qū)ο?OOP)程序設(shè)計(jì)中,當(dāng)我們定義一個class的時候,可以從某個現(xiàn)有的class繼承,新的class稱為子類(Subclass),而被繼承的class稱為基類、父類或超類(Baseclass、Super class)。
- 典型的繼承關(guān)系如圖1所示,動物是一個基類,貓、狗和老鼠都是動物的子類,子類擁有父類的特征,我們稱子類繼承了父類的特征。比如貓、狗和老鼠都繼承了動物都需要進(jìn)食獲取能量,能夠發(fā)出叫聲特征等。
- 繼承描述的是一種IS-A的關(guān)系,例如C繼承了B,那么我們稱B是基類,C是子類,C IS B(例如:貓是動物),但是我們不能說B IS C(比如:動物是貓)。
- 繼承關(guān)系和多態(tài)函數(shù)結(jié)合起來,就很容易達(dá)到管理系統(tǒng)對象復(fù)雜度的用途。例如,一個對象的實(shí)例,無論這個對象實(shí)例是貓、狗還是老鼠,我們只需要知道它是動物就OK了,我們可以把貓和狗之間當(dāng)做它們的基類對象——動物類的實(shí)例來訪問,調(diào)用動物發(fā)出叫聲的函數(shù),經(jīng)過叫聲函數(shù)在貓和狗中的多態(tài)函數(shù)實(shí)現(xiàn),如果對象是貓,則會發(fā)出“喵喵”的叫聲,如果對象是狗,則發(fā)出“汪汪”的叫聲。
Figure 1 典型的繼承關(guān)系
- 在Linux 內(nèi)核C代碼中,class類都用struct結(jié)構(gòu)體來模擬
- Linux內(nèi)核設(shè)備驅(qū)動中,字符設(shè)備模型是典型的基類,而video_device、 framebuffer、miscdevice都是字符設(shè)備,滿足IS-A的關(guān)系,因而都繼承了字符設(shè)備的特性(支持字符設(shè)備open, ioctl, read,write等典型的訪問方法函數(shù)), 都是字符設(shè)備的子類。
- 字符設(shè)備基類與video_device、 framebuffer、miscdevice等繼承體系關(guān)系的UML描述圖如何2所示。由于C語言并沒有嚴(yán)格的繼承關(guān)系語法支持,加上多級繼承的緣故,所以實(shí)現(xiàn)這種繼承關(guān)系需要一些C語言技巧,細(xì)節(jié)上需要仔細(xì)鉆研代碼,推敲。但是細(xì)節(jié)上的障礙并不阻礙我們從面向?qū)ο筮@種較高的層次來閱讀和管理Linux設(shè)備驅(qū)動的代碼,理解這種繼承關(guān)系。
- 用面向?qū)ο笏枷敕治鯨inux內(nèi)核,重點(diǎn)是理解代碼模塊之間的關(guān)系和設(shè)計(jì)思想、意圖,至于如何處理繼承關(guān)系的代碼細(xì)節(jié),實(shí)際上內(nèi)核各個子系統(tǒng)框架已經(jīng)通過精妙的C代碼地處理過了,雖然不是嚴(yán)格的面向?qū)ο螅撬悸飞洗笾孪嗤?/p>
- 字符設(shè)備對象struct cdev在include/linux/cdev.h中聲明, Linux內(nèi)核中相當(dāng)多的驅(qū)動類型都抽象成字符設(shè)備cdev(就如同貓、狗抽象成動物),cdev通過和文件系統(tǒng)的inode節(jié)點(diǎn)關(guān)聯(lián),對用戶態(tài)而言,抽象成字符設(shè)備類型的文件(這也是為什么用戶態(tài)看來,所有設(shè)備都是抽象的問題)。
- struct file_Operations是字符設(shè)備cdev最重要的組成部分,這個組件包含了cdev的核心函數(shù)方法(open/read/write/ioctl等),繼承字符設(shè)備的子類都需要實(shí)現(xiàn)自己的struct file_operations方法,以實(shí)現(xiàn)子類的多態(tài)函數(shù)。
- 由于字符設(shè)備cdev類的繼承體系以及其struct file_operations中函數(shù)的多態(tài)實(shí)現(xiàn),所以用戶態(tài)程序可以通過訪問字符設(shè)備cdev的方法來訪問framebuffer、video_device等各種具體的設(shè)備驅(qū)動(類比于訪問動物的叫聲函數(shù)的方法,來調(diào)用具體的動物,如貓、狗的叫聲函數(shù))。
- 綜合而言,繼承關(guān)系最重要的優(yōu)點(diǎn)就是,通過基類對象以及多態(tài)函數(shù)來訪問具體子類對象實(shí)例。
Figure 2 Linux內(nèi)核字符設(shè)備驅(qū)動之間的繼承關(guān)系
- Linux內(nèi)核中用C語言實(shí)現(xiàn)繼承關(guān)系的方法和技巧有以下幾種:
* 具體子類實(shí)現(xiàn)和基類的抽象差異性不大,繼承體系只有一級繼承,不需要做過多擴(kuò)展時,在基類函數(shù)中加入空指針PRiv域即可。子類的私有特殊屬性對象,可以放到空指針priv即可,子類相關(guān)的函數(shù)中,可以通過自定義的解析方法,強(qiáng)制轉(zhuǎn)換,解析priv對象。
struct base_dev { int attr; int (*func1)(); void *priv;}*具體子類實(shí)現(xiàn)和基類的抽象差異性較大,繼承體系只有一級繼承,需要擴(kuò)展基類時,可以將基類對象嵌入到子類中,在訪問到具體的子類時,通過內(nèi)核特有的container_of()類型的函數(shù),獲取子類對象。
例如圖3所示video_device對象的聲明:
在得到基類對象cdev之后,通過 container_of(vdev, struct video_device, cdev) 可以在vdev中獲取structvideo_device對象的實(shí)例,進(jìn)而訪問struct v4l2_file_operations中的相關(guān)多態(tài)文件操作函數(shù)。
Figure 3 video_device對象通過嵌入cdev從而繼承cdev類
* 具體子類實(shí)現(xiàn)和基類的抽象差異性較大,繼承體系只有多級(一般只有2級), 需要一個抽象層來管理基類與子類的繼承關(guān)系.
例如; framebuffer對象,具體的例如vga16設(shè)備的framebuffer,是用struct fb_info來描述的。
而在具體的vga16fb設(shè)備驅(qū)動子類中,在init加載時,都會調(diào)用register_framebuffer()函數(shù)將 vga16fb的struct fb_info描述對象注冊到registered_fb[32]這個全局?jǐn)?shù)組中(其實(shí)用鏈表更好,支持動態(tài)擴(kuò)展,就可以超過32個fb對象的限制了),并且在會創(chuàng)建一個以FB_MAJOR為主設(shè)備號的次設(shè)備節(jié)點(diǎn)(創(chuàng)建節(jié)點(diǎn),意味著有一個字符設(shè)備cdev對象實(shí)例化了)。
而在framebuffer子類對象的初始化函數(shù)fbmem_init()中,已經(jīng)創(chuàng)建了一個framebuffer的字符設(shè)備cdev的子類對象,并設(shè)置了FB_MAJOR的主節(jié)點(diǎn)號。
這樣在用戶態(tài)通過 framebuffer的主設(shè)備節(jié)點(diǎn)字符設(shè)備cdev 的子類對象實(shí)例-- > cdev 注冊的fb_fops 函數(shù)方法 -- > 數(shù)組registered_fb[32] 中的fb_info 子類對象實(shí)例 --> 調(diào)用fbops中的文件操作多態(tài)函數(shù)。
通過這個調(diào)用路徑,從cdev的抽象對象訪問到fb_info的子類具體對象,從而實(shí)現(xiàn)了多級繼承體系以及多態(tài)函數(shù)的調(diào)用。
- 綜上所述,Linux內(nèi)核C語言編程,根據(jù)繼承級數(shù)不同,實(shí)際對象和抽象對象差異不同,實(shí)現(xiàn)繼承和多態(tài)的方法也存在多樣化,但是宏觀上的思路是一樣的,用面向?qū)ο蟮恼Z言來說,就是要繼承基類,實(shí)現(xiàn)多態(tài),讓用戶態(tài)程序能夠以訪問抽象字符設(shè)備文件的方法,訪問具體驅(qū)動設(shè)備。
3). 不嚴(yán)格的基類與抽象基類
- 在JAVA面向?qū)ο蟾拍?#20540;,有抽象基類的概念, C++中有類似的虛基類的概念。抽象基類指基類對象的函數(shù)方法都是虛函數(shù),并且抽象基類不能夠直接實(shí)例化,必須被子類繼承,然后實(shí)例化子類,由子類真正定義基類函數(shù)的實(shí)現(xiàn)。
- C語言的struct結(jié)構(gòu)體中,不能直接定義函數(shù),實(shí)現(xiàn)函數(shù)體。只能夠通過聲明函數(shù)指針的方式將函數(shù)指針嵌入到結(jié)構(gòu)體中,然后在定義結(jié)構(gòu)體或者實(shí)例化時,才真正給指針賦值實(shí)際的函數(shù)地址。類似struct cdev 極其重要的組件struct file_operations的聲明如圖4所示。
- 在面對對象編程中,基類與子類最重要的差別就是函數(shù)的多態(tài)實(shí)現(xiàn)上。比如基類動物的叫聲函數(shù),假如默認(rèn)發(fā)出一種“哦哦”的聲音,而子類貓的叫聲函數(shù)通過多態(tài)覆蓋基類的叫聲函數(shù),發(fā)出“喵喵”聲,子類狗的叫聲函數(shù)通過多態(tài)覆蓋基類的叫聲函數(shù),發(fā)出“汪汪”聲,從而實(shí)現(xiàn)了子類和基類的差異化。
- 那么問題來了,在C語言中,定義或者實(shí)例化一個結(jié)構(gòu)體對象,例如 struct cdev mycdev;mycdev->ops->ioctl = myioctl; 那么mycdev到底是structcdev這個基類的實(shí)例,還是繼承了struct cdev對象的cdev的子類的實(shí)例(雖然定義了struct cdev對象,但是cdev的函數(shù)方法被覆蓋重寫了,這里就是歧義產(chǎn)生點(diǎn),在面向?qū)ο笏枷胫校宇惒艜诶^承基類之后覆蓋重寫基類的函數(shù))。
Figure 4 struct cdev基類及其核心函數(shù)方法的聲明
- 為了溝通交流上的統(tǒng)一,消除理解歧義,這里需要做一些妥協(xié)折中,放寬松面向?qū)ο笏枷氲囊?guī)則限制,制定幾條Linux內(nèi)核C語言面向?qū)ο缶幊痰淖远x規(guī)則,才可以在沒有語法關(guān)鍵字支持的條件下,模擬OOP編程,將OOP靈活應(yīng)用到內(nèi)核設(shè)備驅(qū)動模型的分析。關(guān)于基類與繼承的幾條折中妥協(xié)的規(guī)則如下:
1. 在C語言面向?qū)ο缶幊讨校驗(yàn)榻Y(jié)構(gòu)體本身只能嵌入函數(shù)指針,所以不區(qū)分基類與抽象基類,我們用一個大概的基類概括這兩種情況。
2. 定義一個結(jié)構(gòu)體對象實(shí)例,例如struct cdev mycdev; 如果mycdev中的structfile_operations *ops中的函數(shù)方法是用戶自己實(shí)現(xiàn)的,那么我們就認(rèn)為mydev對象是cdev的子類。如果mydev的mycdev中的struct file_operations *ops中的函數(shù)方法全部是采用Linux內(nèi)核提供的默認(rèn)的實(shí)現(xiàn)函數(shù),那么我們就認(rèn)為mycdev就是cdev類的一個實(shí)例,是一個基類對象的實(shí)例。實(shí)際上Linux內(nèi)核為很多內(nèi)核基類對象都提供了默認(rèn)的實(shí)現(xiàn)函數(shù)(我們可以稱為基類函數(shù)的實(shí)現(xiàn)),在對象的構(gòu)造函數(shù)中,我們會給對應(yīng)的函數(shù)指針賦值。在實(shí)例化一個字符設(shè)備為抽象的字符設(shè)備文件時,我們都會創(chuàng)建inode節(jié)點(diǎn),而在這個過程中,調(diào)用的init_special_inode()函數(shù)如圖5所示。可見cdev對象創(chuàng)建過程中都采用了默認(rèn)的def_chr_fops實(shí)例化基類函數(shù)cdev的structfile_operations *ops的函數(shù)方法。
Figure 5 實(shí)例化字符設(shè)備cdev的過程中,使用Linux內(nèi)核默認(rèn)的基類函數(shù)方法def_chr_fops實(shí)例化cdev
3. 大部分情況下,定義cdev對象,實(shí)際上都是cdev的子類,因?yàn)閏dev本身抽象層次太高,默認(rèn)實(shí)現(xiàn)的函數(shù)方法也只提供了open的方法,open也只會最終調(diào)用實(shí)際字符設(shè)備驅(qū)動的open函數(shù),并沒有實(shí)現(xiàn)驅(qū)動的有效的功能。cdev就類似于動物這個基類,實(shí)際上還是抽象基類,世界上并沒有一種真正叫做動物的對象實(shí)例,但是貓、狗才有真正的實(shí)例對象。從動物到貓和狗的對象,實(shí)際上還應(yīng)該分出一些中間的子類,例如貓科動物,犬科動物,貓是貓科動物的子類,狗是犬科動物的子類。同理,framebuffer是繼承cdev的子類,vga16fb才是真正的vga16圖形顯示器的驅(qū)動程序(要實(shí)例化成/dev/*下的節(jié)點(diǎn))。
4. 從基類到子類的繼承關(guān)系,最重要的思想是從抽象的基類對象到子類的具體對象的一個逐漸具體化的過程。在管理軟件系統(tǒng)復(fù)雜度時,通過這種抽象到具體的過程,應(yīng)用開發(fā)者只需了解一些抽象概念,調(diào)用抽象的API。而具體對象的細(xì)節(jié)維護(hù)則交給子類的維護(hù)者管理。所以繼承關(guān)系,重要的是看清楚抽象到具體的思維方法,C語言實(shí)現(xiàn)這種繼承關(guān)系的細(xì)節(jié)是次要的。
4). 單繼承與接口
- 在真正的面向?qū)ο缶幊陶Z言中,C++支持多重繼承而JAVA不支持多重繼承,多重繼承會使得對象繼承關(guān)系變得復(fù)雜化,同時會埋有鉆石問題的隱患(如圖6所示)。
Figure 6 多重繼承中存在的鉆石問題,如果哺乳動物和食肉動物實(shí)現(xiàn)了相同的函數(shù)方法,狗在多重繼承遇到不同基類相同函數(shù)的時候,容易引發(fā)混亂
- 在C語言面向?qū)ο缶幊痰囊?guī)則中,我建議模仿JAVA的單繼承機(jī)制,另外用JAVA中接口實(shí)現(xiàn)機(jī)制(interface)代替可能在C++中的多繼承機(jī)制。
- 接口在面向?qū)ο缶幊讨忻枋隽艘环NLIKE-A的關(guān)系。例如機(jī)器狗,它不是動物,它在事物分類里面應(yīng)該是機(jī)器而不是真正的狗,機(jī)器狗與牧羊犬,哈巴狗有著本質(zhì)的區(qū)別,牧羊犬可以作為狗的一種子類,是一種繼承關(guān)系,但是機(jī)器狗就不可以,生物學(xué)家也不認(rèn)為機(jī)器狗是真正的狗。但是機(jī)器狗可以和狗一樣發(fā)出“汪汪汪”的叫聲,所以我們可以說機(jī)器狗與狗是LIKE-A而不是IS-A的關(guān)系,機(jī)器狗和機(jī)器才是IS-A的關(guān)系。這樣可以說,機(jī)器狗是機(jī)器,但是它實(shí)現(xiàn)了狗的“汪汪汪”的叫聲接口(當(dāng)然它不能繼承真正狗的DNA)。繼承關(guān)系與接口實(shí)現(xiàn)關(guān)系的差別如圖7所示。
Figure 7 單繼承體系中,繼承關(guān)系與接口實(shí)現(xiàn)的區(qū)別
- Linux內(nèi)核設(shè)備驅(qū)動中,也會遇到類似的多繼承問題,例如三星framebuffer的驅(qū)動(s3c-fb.c),既是字符設(shè)備類型的驅(qū)動,又是虛擬平臺總線(platform_driver)類型的驅(qū)動。所以需要制定面向?qū)ο蟮囊?guī)則,管理類似多繼承的問題。
- 關(guān)于Linux內(nèi)核C語言編程中,需要為多繼承與接口相關(guān)的幾條規(guī)則,來適應(yīng)上述出現(xiàn)的相關(guān)問題:
1. Linux內(nèi)核C語言模擬JAVA的單繼承機(jī)制,不支持多重繼承,遇到同時具備cdev對象與platform_driver對象兩種類型的驅(qū)動時,只繼承其中一個對象,另一個則作為接口實(shí)現(xiàn),以描述LIKE-A的概念。
2. 對于類似s3c-fb.c這類驅(qū)動,我們認(rèn)為它是framebuffer 與 cdev的子類,實(shí)現(xiàn)struct platform_driver這個虛擬總線實(shí)例化接口,因?yàn)閟3c-fb驅(qū)動核心的功能特性是framebuffer的顯示緩存功能邏輯,而struct platform_driver這個接口的相關(guān)函數(shù),只是在動態(tài)實(shí)例化framebuffer設(shè)備節(jié)點(diǎn)的時候調(diào)用一次,并非framebuffer本質(zhì)的特征(就像機(jī)器狗,本質(zhì)特性是機(jī)器的特性)。所以我們說s3c-fb IS-A framebuffer, s3c-fb LIKE-A struct platform_driver。
3. 由于驅(qū)動設(shè)備的復(fù)雜性,并不像自然界的事物容易看出繼承關(guān)系。所以在研究內(nèi)核設(shè)備驅(qū)動單繼承關(guān)系的時候,不要拘泥于條條框框,要根據(jù)自己的研究目的來選擇基類與繼承關(guān)系。例如圖6所示的,如果要研究狗的哺乳動物屬性,在單繼承條件下,我們可以認(rèn)為狗繼承了哺乳動物,實(shí)現(xiàn)了食肉動物的接口。
4. 在研究類似s3c-fb.c這類驅(qū)動時,如果關(guān)注重點(diǎn)在s3c-fb的顯示緩沖等framebuffer特性上,我們就認(rèn)為s3c-fb繼承了framebuffer,實(shí)現(xiàn)了struct platform_driver的虛擬總線實(shí)例化接口。如果我們關(guān)注點(diǎn)在s3c-fb如何識別fb設(shè)備動態(tài)識別實(shí)例化,如果通過sys文件系統(tǒng)進(jìn)行電影管理的特性,那么我們可以認(rèn)為s3c-fb繼承了platform_driver類,實(shí)現(xiàn)了framebuffer的接口(盡管這種case比較少見)。
5. 總之,在研究Linux內(nèi)核設(shè)備驅(qū)動單繼承與接口實(shí)現(xiàn)規(guī)則時,要主動權(quán)衡研究目的,根據(jù)需要選擇繼承的基類與實(shí)現(xiàn)的接口。但是字符設(shè)備驅(qū)動在大多數(shù)情況下,我們都認(rèn)為XX驅(qū)動繼承了字符設(shè)備cdev,實(shí)現(xiàn)了platform_driver的虛擬總線實(shí)例化接口。
5). 通過基類訪問子類的方法
- 在面向?qū)ο缶幊讨校ㄟ^繼承關(guān)系,我們將子類對象賦值給基類對象的時候,可以通過基類對象,調(diào)用多態(tài)函數(shù)訪問子類對象的實(shí)際函數(shù)。
-在Linux內(nèi)核設(shè)備驅(qū)動中,我們在用戶態(tài)open一個字符設(shè)備,然后調(diào)用字符設(shè)備的read/write/ioctl函數(shù),最終也會調(diào)用到內(nèi)核態(tài)設(shè)備驅(qū)動程序相應(yīng)的read/write/ioctl函數(shù)的實(shí)現(xiàn),從而模擬了通過基類與多態(tài)函數(shù)的特性來訪問子類的目的。
-實(shí)際上,Linux內(nèi)核會維護(hù)基類與子類cdev對象實(shí)例的鏈表,當(dāng)用戶態(tài)發(fā)起read/write/ioctl等字符設(shè)備系統(tǒng)調(diào)用函數(shù)時,read/write/ioctl等字符設(shè)備系統(tǒng)調(diào)用函數(shù)會通過/dev/*下的字符設(shè)備,設(shè)備節(jié)點(diǎn)號的方式(主節(jié)點(diǎn)號major,次節(jié)點(diǎn)號minor)從cdev鏈表的子類中,找到對應(yīng)子類的cdev對象實(shí)例,然后判斷是否為空并調(diào)用子類cdev->ops->read/write/ioctl等實(shí)際子類的多態(tài)函數(shù)實(shí)現(xiàn),從而最終實(shí)現(xiàn)了通過訪問基類的多態(tài)函數(shù),最終訪問到子類實(shí)際的多態(tài)函數(shù),這個面向?qū)ο蟮奶匦浴?/p>
4.多態(tài)與實(shí)例化
1). 多態(tài)
- 在面向?qū)ο蟪绦蛟O(shè)計(jì)中,屬于繼承關(guān)系的基類classanimal 與子類 class dog 都實(shí)現(xiàn)了相同的函數(shù)方法bark()時,我們說子類的bark()覆蓋了基類的bark()函數(shù)。如果一個 class animal 基類被實(shí)例化為一個class dog對象,那么調(diào)用animal.bark()時,實(shí)際上會調(diào)用dog.bark()。這樣我們稱父類與子類相同的函數(shù)方法bark()為多態(tài)函數(shù)。
- 在C++中,多態(tài)是通過動態(tài)綁定實(shí)現(xiàn)的,程序在運(yùn)行的時候,基類會通過虛函數(shù)表來查找子類的多態(tài)函數(shù)實(shí)現(xiàn),當(dāng)然這個過程都是系統(tǒng)運(yùn)行庫做的(run time),自己實(shí)現(xiàn)是相當(dāng)復(fù)雜。
- 在Linux內(nèi)核中,模擬多態(tài)的方法要簡單一些,實(shí)際上是在基類的函數(shù)方法中,通過獲取子類對象,再嵌套調(diào)用子類對象的同名函數(shù)來實(shí)現(xiàn)的。圖8為framebuffer設(shè)備驅(qū)動的多態(tài)函數(shù)read實(shí)現(xiàn)。實(shí)際上,framebuffer的cdev的struct file_operations對象的read函數(shù)會調(diào)用子類struct fb_info對象中同名fb_read函數(shù)(如果子類未實(shí)現(xiàn)該函數(shù),則不調(diào)用),從而模擬了繼承關(guān)系中基類同名函數(shù)通過多態(tài)的方法調(diào)用子類同名函數(shù)的行為。
Figure 8 framebuffer driver 中read函數(shù)的多態(tài)實(shí)現(xiàn)
2). 實(shí)例化
- 在C語言面向?qū)ο缶幊虝r,如第3節(jié)繼承與接口中在第3段不嚴(yán)格的基類與抽象基類,提出的一個關(guān)于實(shí)例化與繼承的問題,定義一個變量struct cdev mycdev; mycdev到底代表cdev的子類還是cdev的實(shí)例,在該章節(jié)中,已有規(guī)則說明在何種場景下mydev代表cdev的子類。
- 因而,在Linux內(nèi)核中,關(guān)于對象實(shí)例化,還需要以下幾條規(guī)則:
1. 定義一個類似 struct cdev mycdev;這樣的結(jié)構(gòu)體變量mycdev,雖然mydev會占用內(nèi)存空間,但是mydev并并不算實(shí)例化內(nèi)核設(shè)備驅(qū)動,只有mydev真正在/dev/*目錄下創(chuàng)建了設(shè)備節(jié)點(diǎn),才是一個內(nèi)核驅(qū)動設(shè)備的實(shí)例。
2. 大部分情況下,內(nèi)核設(shè)備是通過總線的接口(包括platform虛擬總線,也包括USB、SPI、I2C等真正的總線,只要是繼承structbus_type基類對象的總線都可以)的probe()函數(shù)進(jìn)行實(shí)例化的。例如三星的framebuffer驅(qū)動s3c-fb.c,就是通過實(shí)現(xiàn)struct platform_driver這個接口,在接口的probe()函數(shù)中,為識別到的framebuffer設(shè)備創(chuàng)建/dev/*下的節(jié)點(diǎn),實(shí)現(xiàn)s3c-fb字符設(shè)備的實(shí)例化。
3. 在不實(shí)現(xiàn)總線接口的字符設(shè)備中,定義一個struct cdev mycdev;之后,在模塊加載module_init()的時候,也是可以調(diào)用構(gòu)造函數(shù)(初始化函數(shù)),創(chuàng)建/dev/*下的設(shè)備節(jié)點(diǎn),從而完成實(shí)例化的。這種情況下,mycdev可以代表一個設(shè)備的實(shí)例,這種情況下模擬了面向?qū)ο笤O(shè)計(jì)模型中的單例模式,這也是可行的。
5.聚合(組合)
- 聚合在面向?qū)ο笾写硪环NHAS-A的關(guān)系,比如struct cdev對象HAS-A struct file_operations對象,我們就認(rèn)為structfile_operations對象與struct cdev對象是聚合關(guān)系。
- 在面向?qū)ο笾校酆详P(guān)系主要是為了區(qū)別于繼承關(guān)系,例如V4L2子系統(tǒng)中,structvideo_device 有struct v4l2_file_operations對象,但是實(shí)際上structv4l2_file_operations中的函數(shù)都是struct file_operations中的同名函數(shù),并且基類的struct file_operations中的同名函數(shù)最終會模擬多態(tài)函數(shù)的方式,調(diào)用到struct v4l2_file_operations中的函數(shù)。因而我們認(rèn)為struct video_device對象中的struct v4l2_file_operations對象是繼承自struct cdev對象,而不是聚合關(guān)系(HAS-A).但是struct video_device對象中的v4l2_std_id tvnorms對象,在struct cdev基類對象中是不存在的,是structvideo_device對象特有的,struct video_device HAS-A v4l2_std_idtvnorms, 因而我們認(rèn)為v4l2_std_id tvnorms對象與struct video_device對象是聚合關(guān)系。
- 由于C語言沒有嚴(yán)格的面向?qū)ο箨P(guān)鍵詞標(biāo)準(zhǔn)來支持,所以聚合和繼承的區(qū)別,還是需要人為分類維護(hù)。如果子類與基類有同名函數(shù),并且子類同名函數(shù)被基類同名函數(shù)所調(diào)用,那么同名函數(shù)所在的*_operations對象都認(rèn)為是從基類繼承過來的,子類所擁有的與基類無關(guān)的對象,我們才認(rèn)為是子類的聚合。
6. 模板與泛型
- Linux內(nèi)核中為了簡化復(fù)雜對象的定義,提供了很多#define宏來模仿面向?qū)ο笾械哪0搴头盒蜋C(jī)制。
1). 模板
- 典型的模板宏代碼如Linux內(nèi)核信號量的模板include/linux/semaphore.h,為簡化信號量的定義與初始化,提供了模板函數(shù)。
#define DECLARE_MUTEX(name) / structsemaphore name = __SEMAPHORE_INITIALIZER(name, 1)
2). 泛型
- 典型的泛型宏代碼如Linux內(nèi)核網(wǎng)絡(luò)部分的socket地址泛型(include/linux/net.h ),定義一個socket地址,socket地址的具體數(shù)據(jù)類型在實(shí)際定義的時候才由type參數(shù)確定。
#define DECLARE_SOCKADDR(type, dst,src) / typedst = ({ __sockaddr_check_size(sizeof(*dst)); (type) src; })7. 開閉原則
- 開閉原則是可復(fù)用的面向?qū)ο蟠a設(shè)計(jì)的基石。
- 開閉原則是指,代碼要對擴(kuò)展開放,對修改關(guān)閉。
- Linux內(nèi)核設(shè)計(jì)中,通過繼承關(guān)系,以及用戶態(tài)與內(nèi)核態(tài)隔離,限定使用一組API實(shí)現(xiàn)用戶態(tài)與內(nèi)核態(tài)通信的機(jī)制,使得內(nèi)核代碼對Linux內(nèi)核態(tài)的設(shè)備驅(qū)動擴(kuò)展與開發(fā)是開放的,而對Linux用戶態(tài)應(yīng)用程序的各種可能的修改關(guān)閉。
- 通過開閉原則,也實(shí)現(xiàn)了程序員的分工,Linux內(nèi)核對內(nèi)核維護(hù)的程序員擴(kuò)展開放,對用戶態(tài)應(yīng)用開發(fā)程序員的修改關(guān)閉。整個內(nèi)核設(shè)計(jì)思路是符合開閉原則的。
8. 設(shè)計(jì)模式
- 設(shè)計(jì)模式是在面向?qū)ο蟪绦蛟O(shè)計(jì)中總結(jié)出來的一些常用的代碼復(fù)用規(guī)則。Linux內(nèi)核設(shè)計(jì)中,大量地參考了經(jīng)典的設(shè)計(jì)模式,這里僅僅舉兩個例子,讀者在閱讀各驅(qū)動源碼時,需要自我總結(jié)一些設(shè)計(jì)模式。
1). 觀察者模式(訂閱者/發(fā)布者)
- 觀察者模式定義了一種一對多的依賴關(guān)系,讓多個觀察者對象同時監(jiān)聽某一個主題對象。這個主題對象在狀態(tài)發(fā)生變化時,會通知所有觀察者對象,使它們能夠自動更新自己。
- Linux內(nèi)核的通知鏈模型是典型的觀察者模式,其介紹可以參閱本博客另一篇文章《Linux內(nèi)核重點(diǎn)精要》中通知鏈模型的相關(guān)的介紹。
2).橋接模式(handle/body)
- 橋接模式的設(shè)計(jì)意圖將抽象部分與它的實(shí)現(xiàn)部分分離,使它們都可以獨(dú)立地變化。
- Linux內(nèi)核中使用的最重要的橋接模式,在于萬物皆文件的思想。即將用戶態(tài)的抽象字符設(shè)備文件,與實(shí)際的字符設(shè)備驅(qū)動實(shí)現(xiàn)分離,從而使得文件描述符和內(nèi)核設(shè)備驅(qū)動可以分別在用戶態(tài)和內(nèi)核態(tài)獨(dú)立變化,只需要在open的時候?qū)⒊橄笪募c實(shí)際的設(shè)備驅(qū)動關(guān)聯(lián)起來即可。
- 抽象字符設(shè)備文件與實(shí)際的內(nèi)核設(shè)備驅(qū)動橋接模式的UML簡化圖如圖9所示。
Figure 9 Linux內(nèi)核設(shè)備驅(qū)動模型中經(jīng)典的橋接模式
9. 參考文獻(xiàn)
- 《Linux內(nèi)核設(shè)計(jì)與實(shí)現(xiàn)》—— Robert.Love
- 《設(shè)計(jì)模式》——伽馬等(四人組)
- 《JAVA編程思想》——Bruce.Eckel
- 《代碼大全》——SteveMcConnell
- 《C語言面向?qū)ο缶幊獭?—— foruok的博客
新聞熱點(diǎn)
疑難解答