linux內核驅動中面向對象的基本規則和實現方法
- 內核版本 Linux Kernel 2.6.34, 與 Robert.Love的《Linux Kernel Development》(第三版)所講述的內核版本一樣
- 源代碼下載路徑: https://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.34.tar.bz2
- 眾所周知,現代的軟件項目變得越來越復雜,Linux內核更是世界上最大最復雜的軟件工程。引用自《代碼大全》中的觀點,面向對象的設計思想是一種管理軟件系統復雜性的手段。
- 人的精力有限,一個程序員在同一時間,只專注于軟件系統的一小部分,才能最大的發揮工作效率。構建軟件就像設計大型建筑,一個時間點上,我們聚焦在建筑結構藍圖設計時,就不要過分地將注意力分散的建筑內的電線布局,第N層的排水管道如何施工這些細節問題上,而是應該聚焦在整體設計上。
- 面向對象思想就是一種在代碼編寫之上的軟件系統結構設計的思想。面向對象的語言用于描述系統框架結構是怎么樣的,需要什么模塊,模塊之間的關系如何,如何遵守開閉準則,方便后期的維護和開發等一些設計意圖層次上的問題。
- 面向對象的設計思想不太關心xxx函數實現如何初始化,要注冊什么結構,要把xxx寄存器狀態設置為0等流程細節的問題。這些應該是屬于面向過程設計時的問題。
- 面向過程與面向對象的思想用途不同,沒有好壞之分。面向對象思想更傾向于程序之上的頂層設計與程序系統結構設計,然后真正要實現一個函數細節的時候,還是需要面向過程地分析細節如何實現,需要初始化哪些變量,注冊哪些結構,設置哪些寄存器等面向過程的問題。
- 面向對象的思想和語言無關,并不是C++或者java 、Python等語言才有的。面向對象思想,是隨著軟件系統的復雜度越來越高,面對大規模軟件系統設計的問題,而提出的一種管理大型軟件系統設計的思想。只是在C語言出現時,計算機軟硬件系統還在起步階段,面向對象的思想尚未發展(可能Keep it simple and stupid 原則也是讓C語言語法盡量保持簡單的原因),因而C語言中缺乏面向對象相關的核心關鍵詞語法的支持。而JAVA、Python等一些1990年代之后問世的語言,受到C++語言影響以及面向對象思想的逐漸流行,在語法層面就提供了面向對象的核心關鍵詞支持,可以說在處理面向對象問題上具有先天優勢。
- 雖然C語言不支持很多面向對象的核心關鍵詞,但是隨著Linux內核,Ffmpeg,Nginx等大規模以C語言編寫的開源軟件項目的發展與推廣,C語言遇到的軟件復雜度增加以及系統設計與系統長期維護的問題,與JAVA、C++編程遇到的復雜度問題是想通的。并且,面向對象思想也是由于開發者們在開發過程中遇到瓶頸才提出來的,這些問題,不管是用C語言編程還是JAVA編程,都會客觀存在。因而用C語言模擬JAVA等面向對象的語言,采用面向對象的思想進行系統頂層設計是很有必要的。
- JAVA是一種類似C語言的命令式語言(用于區別Lisp等函數式編程語言),并且是設計良好的純面向對象的語言。JAVA有不少關鍵詞與C語言相同,并且JAVA標準制定委員會的很多成員也是C/C++標準制定委員會的。因而借鑒JAVA的面向對象思想,分析編寫面向對象的C語言程序,是相當有幫助的。JAVA不同于C++, C++現在的定位是多種范式語言,細節太多,在面向對象特性的設計上也不夠友好方便。所以我更傾向于借鑒JAVA的編程思想來分析編程面向對象的C語言代碼。
- 文章中很多面向對象思想的出處,都來自JAVA。作為Linux和C語言嵌入式系統相關的開發者,可能大部分缺乏JAVA的編程經驗。所以希望讀者抽空學習一下JAVA,做一些小練習,拓寬編程的知識面,這樣更能夠領會面向對象設計的精髓,還能提出一些自己的新的看法。我認為,JAVA實際上是在C語言基礎上的的一種改進,很多關鍵詞與C相同,但是又彌補了C語言的不少缺陷,并且專注在面向對象的設計上。
- 本文也是希望起到拋磚引玉的作用,如有概念性問題,還請批評指正。
- 將面向對象的概念引入基于Linux系統的C語言程序開發是很有必要的。雖然面向對象思想會帶來很多新的概念術語(繼承,多態等),很多做Linux驅動開發的工程師都是電子工程相關專業出身,這些概念可能剛接觸會稍顯晦澀,但是習慣性以面向對象思想分析大規模軟件系統之后,能夠幫你快速得掌握整個系統的結構以及原作者的設計意圖,而不會陷入到一個個API的實現細節中,只見樹木,不見森林。接手維護一個大型軟件項目,首先要知道原作者的意圖,即知道原作者為什么要這么編程,才能理清楚軟件設計思路,進而修改擴展源代碼。
- 面向對象的術語,是一種通用的規則,大家都掌握該規則,然后按照規則上的術語溝通,就能夠用簡短的語言表述出程序的意圖。例如面向對象思想中的術語——繼承基類,實現多態函數,如果用面向過程思想描述就是——定義一個XXX結構體,再定義XX和YY函數,用XX和YY函數填充XXX結構體中的A函數指針和B函數指針,再在初始化函數中調用C函數注冊這個XXX結構體。不同思想所用術語的繁簡程度,高下立判。過長的語言描述,容易帶來更多的誤解,信息丟失,理解不全等溝通障礙,這時,專用術語的優勢就體現出來了。
- 面向對象思想往往和設計模式是分不開的。比如Linux內核中的通知鏈系統,用設計模式的術語來說,叫觀察者模式,有學過該設計模式的讀者,立馬明白程序大概做了什么。但是如果不了解這一套語言溝通規則,就代碼講代碼,可能又是一堆這樣的過程描述性語言——定義一個xxx頭,定義xx結構體,設置xx結構體的回調函數,回調函數輸入參數是什么,返回什么,注冊xx結構到xxx頭……
- 單純地學習面向對象思想或者設計模式,如何不結合實際的代碼來分析具體案例,過一段時間可能就會忘記書上講了什么東西。
- 平時編寫小規模程序時候,只需要一個人,不需要面向對象的思想就能完成,因而覺得面向對象這些東西都是書上的理論,實際上又用不著,但是一旦遇到Linux內核源碼這種復雜級別的程序,就不知如何下手,容易一臉懵逼。
- Linux內核是世界上最大的軟件工程項目之一,經過了20多年的發展和完善,內部子系統結構經過了不斷重構(refracting)和反復迭代設計,其代碼質量也是越來越高,這其中肯定從面向對象的編程思想中借鑒模仿了很多東西。當然閱讀Linux內核源碼時,由于歷史原因,還是會有很多代碼的命名,架構和設計風格不那么完美(例如videobuf的第一版)。所以閱讀內核源碼的時候,要學會其精華,同時也要拋棄一些不良的設計。
- JAVA Python等面向對象語言有大量的開源框架,讓我們從實戰中體驗面向對象思想和設計模式。C語言的框架類庫可能沒有JAVA那么豐富,但是Linux內核作為C語言的代表作品,其中的設計思想是很值得用面向對象的語言分析一遍的,尤其是設備驅動相關的代碼,有很多層抽象才變成了用戶態都喜愛的文件。
- 其實拋開Linux內核中動態運行,維護系統運轉的線程,其中大部分驅動代碼都是靜態存在于內存,等待被調用的。這樣看來Linux內核中維持系統運轉的進程就像JAVA虛擬機,而內核中等待被調用的代碼,就像JDK的框架和類庫,需要用戶態去調用,也需要內核態驅動開發者利用框架進一步擴展。這樣看來JDK與Linux內核設計思想也有就有了很大共通之處,用面向對象思想分析Linux內核設備驅動也就是一種高屋建瓴,了解各個子系統結構框架的通用思想。
- 網絡上有一些C語言面向對象思想編程的文章,但是只是零零散散地整理了一些觀點,不夠系統化,案例也過于簡單。所以我希望結合Linux內核這個大型的實際軟件系統,更加系統化地在C語言編程中描述和應用面向對象思想。
- 綜上所述,作為Linux系統C語言開發者,帶著面向對象的思想,從不同的視角來學習Linux內核吧!
- 熟悉結構化C語言編程的讀者,對抽象與封裝應該不陌生,在此簡要帶過,抽象和封裝是面向對象編程思想的基礎。
- 抽象即抽出事物最本質的特征來從某個方面描述這個事物。例如,牧羊犬和藏獒,它們抽象出來都是狗,都有會“汪汪汪”叫,會吃骨頭等狗所擁有的行為特征。對于不需要分辨其到底是牧羊犬還是藏獒的用戶,只需要知道一個物體是狗,那么肯定會聯想到它會“汪汪汪”地叫這個特點。
- 在C語言數據結構中,我們所描述的ADT抽象數據類型,就是對各種數據對象模型的抽象,在此不多累述。
- 封裝,在C語言編程中,大部分時候用一個函數調用(API)將一個復雜過程的細節屏蔽起來,用戶不需要了解細節,只需要調用該函數就能實現相應的行為。例如吃飯函數,將盛飯,動筷子,夾菜,張嘴,咀嚼,下咽等細節屏蔽起來,我們只需要調用吃飯函數,默認就實現了一遍這樣的流程。
- 面向對象思想中的封裝使用更廣泛,即一個對象類(C語言中用結構體代替),需要隱藏用戶不需要也不應該知道的行為和屬性。用戶在訪問對象時,不需要了解被封裝的對象和屬性,就能使用該對象類,同時對象類也應該通過權限設置,禁止用戶過多地了解被封裝的對象屬性與行為。
- 總之,抽象與封裝的思想都是為了讓用戶不需要了解對象過多的細節,就能直接通過API來使用對象,從而達到模塊化編程,,程序員分工合作,各自負責維護自己負責模塊對象細節的作用。
- 在面向對象(OOP)程序設計中,當我們定義一個class的時候,可以從某個現有的class繼承,新的class稱為子類(Subclass),而被繼承的class稱為基類、父類或超類(Baseclass、Super class)。
- 典型的繼承關系如圖1所示,動物是一個基類,貓、狗和老鼠都是動物的子類,子類擁有父類的特征,我們稱子類繼承了父類的特征。比如貓、狗和老鼠都繼承了動物都需要進食獲取能量,能夠發出叫聲特征等。
- 繼承描述的是一種IS-A的關系,例如C繼承了B,那么我們稱B是基類,C是子類,C IS B(例如:貓是動物),但是我們不能說B IS C(比如:動物是貓)。
- 繼承關系和多態函數結合起來,就很容易達到管理系統對象復雜度的用途。例如,一個對象的實例,無論這個對象實例是貓、狗還是老鼠,我們只需要知道它是動物就OK了,我們可以把貓和狗之間當做它們的基類對象——動物類的實例來訪問,調用動物發出叫聲的函數,經過叫聲函數在貓和狗中的多態函數實現,如果對象是貓,則會發出“喵喵”的叫聲,如果對象是狗,則發出“汪汪”的叫聲。
Figure 1 典型的繼承關系
- 在Linux 內核C代碼中,class類都用struct結構體來模擬
- Linux內核設備驅動中,字符設備模型是典型的基類,而video_device、 framebuffer、miscdevice都是字符設備,滿足IS-A的關系,因而都繼承了字符設備的特性(支持字符設備open, ioctl, read,write等典型的訪問方法函數), 都是字符設備的子類。
- 字符設備基類與video_device、 framebuffer、miscdevice等繼承體系關系的UML描述圖如何2所示。由于C語言并沒有嚴格的繼承關系語法支持,加上多級繼承的緣故,所以實現這種繼承關系需要一些C語言技巧,細節上需要仔細鉆研代碼,推敲。但是細節上的障礙并不阻礙我們從面向對象這種較高的層次來閱讀和管理Linux設備驅動的代碼,理解這種繼承關系。
- 用面向對象思想分析Linux內核,重點是理解代碼模塊之間的關系和設計思想、意圖,至于如何處理繼承關系的代碼細節,實際上內核各個子系統框架已經通過精妙的C代碼地處理過了,雖然不是嚴格的面向對象,但是思路上大致相同。
- 字符設備對象struct cdev在include/linux/cdev.h中聲明, Linux內核中相當多的驅動類型都抽象成字符設備cdev(就如同貓、狗抽象成動物),cdev通過和文件系統的inode節點關聯,對用戶態而言,抽象成字符設備類型的文件(這也是為什么用戶態看來,所有設備都是抽象的問題)。
- struct file_Operations是字符設備cdev最重要的組成部分,這個組件包含了cdev的核心函數方法(open/read/write/ioctl等),繼承字符設備的子類都需要實現自己的struct file_operations方法,以實現子類的多態函數。
- 由于字符設備cdev類的繼承體系以及其struct file_operations中函數的多態實現,所以用戶態程序可以通過訪問字符設備cdev的方法來訪問framebuffer、video_device等各種具體的設備驅動(類比于訪問動物的叫聲函數的方法,來調用具體的動物,如貓、狗的叫聲函數)。
- 綜合而言,繼承關系最重要的優點就是,通過基類對象以及多態函數來訪問具體子類對象實例。
Figure 2 Linux內核字符設備驅動之間的繼承關系
- Linux內核中用C語言實現繼承關系的方法和技巧有以下幾種:
* 具體子類實現和基類的抽象差異性不大,繼承體系只有一級繼承,不需要做過多擴展時,在基類函數中加入空指針PRiv域即可。子類的私有特殊屬性對象,可以放到空指針priv即可,子類相關的函數中,可以通過自定義的解析方法,強制轉換,解析priv對象。
struct base_dev { int attr; int (*func1)(); void *priv;}*具體子類實現和基類的抽象差異性較大,繼承體系只有一級繼承,需要擴展基類時,可以將基類對象嵌入到子類中,在訪問到具體的子類時,通過內核特有的container_of()類型的函數,獲取子類對象。
例如圖3所示video_device對象的聲明:
在得到基類對象cdev之后,通過 container_of(vdev, struct video_device, cdev) 可以在vdev中獲取structvideo_device對象的實例,進而訪問struct v4l2_file_operations中的相關多態文件操作函數。
Figure 3 video_device對象通過嵌入cdev從而繼承cdev類
* 具體子類實現和基類的抽象差異性較大,繼承體系只有多級(一般只有2級), 需要一個抽象層來管理基類與子類的繼承關系.
例如; framebuffer對象,具體的例如vga16設備的framebuffer,是用struct fb_info來描述的。
而在具體的vga16fb設備驅動子類中,在init加載時,都會調用register_framebuffer()函數將 vga16fb的struct fb_info描述對象注冊到registered_fb[32]這個全局數組中(其實用鏈表更好,支持動態擴展,就可以超過32個fb對象的限制了),并且在會創建一個以FB_MAJOR為主設備號的次設備節點(創建節點,意味著有一個字符設備cdev對象實例化了)。
而在framebuffer子類對象的初始化函數fbmem_init()中,已經創建了一個framebuffer的字符設備cdev的子類對象,并設置了FB_MAJOR的主節點號。
這樣在用戶態通過 framebuffer的主設備節點字符設備cdev 的子類對象實例-- > cdev 注冊的fb_fops 函數方法 -- > 數組registered_fb[32] 中的fb_info 子類對象實例 --> 調用fbops中的文件操作多態函數。
通過這個調用路徑,從cdev的抽象對象訪問到fb_info的子類具體對象,從而實現了多級繼承體系以及多態函數的調用。
- 綜上所述,Linux內核C語言編程,根據繼承級數不同,實際對象和抽象對象差異不同,實現繼承和多態的方法也存在多樣化,但是宏觀上的思路是一樣的,用面向對象的語言來說,就是要繼承基類,實現多態,讓用戶態程序能夠以訪問抽象字符設備文件的方法,訪問具體驅動設備。
3). 不嚴格的基類與抽象基類
- 在JAVA面向對象概念值,有抽象基類的概念, C++中有類似的虛基類的概念。抽象基類指基類對象的函數方法都是虛函數,并且抽象基類不能夠直接實例化,必須被子類繼承,然后實例化子類,由子類真正定義基類函數的實現。
- C語言的struct結構體中,不能直接定義函數,實現函數體。只能夠通過聲明函數指針的方式將函數指針嵌入到結構體中,然后在定義結構體或者實例化時,才真正給指針賦值實際的函數地址。類似struct cdev 極其重要的組件struct file_operations的聲明如圖4所示。
- 在面對對象編程中,基類與子類最重要的差別就是函數的多態實現上。比如基類動物的叫聲函數,假如默認發出一種“哦哦”的聲音,而子類貓的叫聲函數通過多態覆蓋基類的叫聲函數,發出“喵喵”聲,子類狗的叫聲函數通過多態覆蓋基類的叫聲函數,發出“汪汪”聲,從而實現了子類和基類的差異化。
- 那么問題來了,在C語言中,定義或者實例化一個結構體對象,例如 struct cdev mycdev;mycdev->ops->ioctl = myioctl; 那么mycdev到底是structcdev這個基類的實例,還是繼承了struct cdev對象的cdev的子類的實例(雖然定義了struct cdev對象,但是cdev的函數方法被覆蓋重寫了,這里就是歧義產生點,在面向對象思想中,子類才會在繼承基類之后覆蓋重寫基類的函數)。
Figure 4 struct cdev基類及其核心函數方法的聲明
- 為了溝通交流上的統一,消除理解歧義,這里需要做一些妥協折中,放寬松面向對象思想的規則限制,制定幾條Linux內核C語言面向對象編程的自定義規則,才可以在沒有語法關鍵字支持的條件下,模擬OOP編程,將OOP靈活應用到內核設備驅動模型的分析。關于基類與繼承的幾條折中妥協的規則如下:
1. 在C語言面向對象編程中,因為結構體本身只能嵌入函數指針,所以不區分基類與抽象基類,我們用一個大概的基類概括這兩種情況。
2. 定義一個結構體對象實例,例如struct cdev mycdev; 如果mycdev中的structfile_operations *ops中的函數方法是用戶自己實現的,那么我們就認為mydev對象是cdev的子類。如果mydev的mycdev中的struct file_operations *ops中的函數方法全部是采用Linux內核提供的默認的實現函數,那么我們就認為mycdev就是cdev類的一個實例,是一個基類對象的實例。實際上Linux內核為很多內核基類對象都提供了默認的實現函數(我們可以稱為基類函數的實現),在對象的構造函數中,我們會給對應的函數指針賦值。在實例化一個字符設備為抽象的字符設備文件時,我們都會創建inode節點,而在這個過程中,調用的init_special_inode()函數如圖5所示。可見cdev對象創建過程中都采用了默認的def_chr_fops實例化基類函數cdev的structfile_operations *ops的函數方法。
Figure 5 實例化字符設備cdev的過程中,使用Linux內核默認的基類函數方法def_chr_fops實例化cdev
3. 大部分情況下,定義cdev對象,實際上都是cdev的子類,因為cdev本身抽象層次太高,默認實現的函數方法也只提供了open的方法,open也只會最終調用實際字符設備驅動的open函數,并沒有實現驅動的有效的功能。cdev就類似于動物這個基類,實際上還是抽象基類,世界上并沒有一種真正叫做動物的對象實例,但是貓、狗才有真正的實例對象。從動物到貓和狗的對象,實際上還應該分出一些中間的子類,例如貓科動物,犬科動物,貓是貓科動物的子類,狗是犬科動物的子類。同理,framebuffer是繼承cdev的子類,vga16fb才是真正的vga16圖形顯示器的驅動程序(要實例化成/dev/*下的節點)。
4. 從基類到子類的繼承關系,最重要的思想是從抽象的基類對象到子類的具體對象的一個逐漸具體化的過程。在管理軟件系統復雜度時,通過這種抽象到具體的過程,應用開發者只需了解一些抽象概念,調用抽象的API。而具體對象的細節維護則交給子類的維護者管理。所以繼承關系,重要的是看清楚抽象到具體的思維方法,C語言實現這種繼承關系的細節是次要的。
4). 單繼承與接口
- 在真正的面向對象編程語言中,C++支持多重繼承而JAVA不支持多重繼承,多重繼承會使得對象繼承關系變得復雜化,同時會埋有鉆石問題的隱患(如圖6所示)。
Figure 6 多重繼承中存在的鉆石問題,如果哺乳動物和食肉動物實現了相同的函數方法,狗在多重繼承遇到不同基類相同函數的時候,容易引發混亂
- 在C語言面向對象編程的規則中,我建議模仿JAVA的單繼承機制,另外用JAVA中接口實現機制(interface)代替可能在C++中的多繼承機制。
- 接口在面向對象編程中描述了一種LIKE-A的關系。例如機器狗,它不是動物,它在事物分類里面應該是機器而不是真正的狗,機器狗與牧羊犬,哈巴狗有著本質的區別,牧羊犬可以作為狗的一種子類,是一種繼承關系,但是機器狗就不可以,生物學家也不認為機器狗是真正的狗。但是機器狗可以和狗一樣發出“汪汪汪”的叫聲,所以我們可以說機器狗與狗是LIKE-A而不是IS-A的關系,機器狗和機器才是IS-A的關系。這樣可以說,機器狗是機器,但是它實現了狗的“汪汪汪”的叫聲接口(當然它不能繼承真正狗的DNA)。繼承關系與接口實現關系的差別如圖7所示。
Figure 7 單繼承體系中,繼承關系與接口實現的區別
- Linux內核設備驅動中,也會遇到類似的多繼承問題,例如三星framebuffer的驅動(s3c-fb.c),既是字符設備類型的驅動,又是虛擬平臺總線(platform_driver)類型的驅動。所以需要制定面向對象的規則,管理類似多繼承的問題。
- 關于Linux內核C語言編程中,需要為多繼承與接口相關的幾條規則,來適應上述出現的相關問題:
1. Linux內核C語言模擬JAVA的單繼承機制,不支持多重繼承,遇到同時具備cdev對象與platform_driver對象兩種類型的驅動時,只繼承其中一個對象,另一個則作為接口實現,以描述LIKE-A的概念。
2. 對于類似s3c-fb.c這類驅動,我們認為它是framebuffer 與 cdev的子類,實現struct platform_driver這個虛擬總線實例化接口,因為s3c-fb驅動核心的功能特性是framebuffer的顯示緩存功能邏輯,而struct platform_driver這個接口的相關函數,只是在動態實例化framebuffer設備節點的時候調用一次,并非framebuffer本質的特征(就像機器狗,本質特性是機器的特性)。所以我們說s3c-fb IS-A framebuffer, s3c-fb LIKE-A struct platform_driver。
3. 由于驅動設備的復雜性,并不像自然界的事物容易看出繼承關系。所以在研究內核設備驅動單繼承關系的時候,不要拘泥于條條框框,要根據自己的研究目的來選擇基類與繼承關系。例如圖6所示的,如果要研究狗的哺乳動物屬性,在單繼承條件下,我們可以認為狗繼承了哺乳動物,實現了食肉動物的接口。
4. 在研究類似s3c-fb.c這類驅動時,如果關注重點在s3c-fb的顯示緩沖等framebuffer特性上,我們就認為s3c-fb繼承了framebuffer,實現了struct platform_driver的虛擬總線實例化接口。如果我們關注點在s3c-fb如何識別fb設備動態識別實例化,如果通過sys文件系統進行電影管理的特性,那么我們可以認為s3c-fb繼承了platform_driver類,實現了framebuffer的接口(盡管這種case比較少見)。
5. 總之,在研究Linux內核設備驅動單繼承與接口實現規則時,要主動權衡研究目的,根據需要選擇繼承的基類與實現的接口。但是字符設備驅動在大多數情況下,我們都認為XX驅動繼承了字符設備cdev,實現了platform_driver的虛擬總線實例化接口。
5). 通過基類訪問子類的方法
- 在面向對象編程中,通過繼承關系,我們將子類對象賦值給基類對象的時候,可以通過基類對象,調用多態函數訪問子類對象的實際函數。
-在Linux內核設備驅動中,我們在用戶態open一個字符設備,然后調用字符設備的read/write/ioctl函數,最終也會調用到內核態設備驅動程序相應的read/write/ioctl函數的實現,從而模擬了通過基類與多態函數的特性來訪問子類的目的。
-實際上,Linux內核會維護基類與子類cdev對象實例的鏈表,當用戶態發起read/write/ioctl等字符設備系統調用函數時,read/write/ioctl等字符設備系統調用函數會通過/dev/*下的字符設備,設備節點號的方式(主節點號major,次節點號minor)從cdev鏈表的子類中,找到對應子類的cdev對象實例,然后判斷是否為空并調用子類cdev->ops->read/write/ioctl等實際子類的多態函數實現,從而最終實現了通過訪問基類的多態函數,最終訪問到子類實際的多態函數,這個面向對象的特性。
4.多態與實例化
1). 多態
- 在面向對象程序設計中,屬于繼承關系的基類classanimal 與子類 class dog 都實現了相同的函數方法bark()時,我們說子類的bark()覆蓋了基類的bark()函數。如果一個 class animal 基類被實例化為一個class dog對象,那么調用animal.bark()時,實際上會調用dog.bark()。這樣我們稱父類與子類相同的函數方法bark()為多態函數。
- 在C++中,多態是通過動態綁定實現的,程序在運行的時候,基類會通過虛函數表來查找子類的多態函數實現,當然這個過程都是系統運行庫做的(run time),自己實現是相當復雜。
- 在Linux內核中,模擬多態的方法要簡單一些,實際上是在基類的函數方法中,通過獲取子類對象,再嵌套調用子類對象的同名函數來實現的。圖8為framebuffer設備驅動的多態函數read實現。實際上,framebuffer的cdev的struct file_operations對象的read函數會調用子類struct fb_info對象中同名fb_read函數(如果子類未實現該函數,則不調用),從而模擬了繼承關系中基類同名函數通過多態的方法調用子類同名函數的行為。
Figure 8 framebuffer driver 中read函數的多態實現
2). 實例化
- 在C語言面向對象編程時,如第3節繼承與接口中在第3段不嚴格的基類與抽象基類,提出的一個關于實例化與繼承的問題,定義一個變量struct cdev mycdev; mycdev到底代表cdev的子類還是cdev的實例,在該章節中,已有規則說明在何種場景下mydev代表cdev的子類。
- 因而,在Linux內核中,關于對象實例化,還需要以下幾條規則:
1. 定義一個類似 struct cdev mycdev;這樣的結構體變量mycdev,雖然mydev會占用內存空間,但是mydev并并不算實例化內核設備驅動,只有mydev真正在/dev/*目錄下創建了設備節點,才是一個內核驅動設備的實例。
2. 大部分情況下,內核設備是通過總線的接口(包括platform虛擬總線,也包括USB、SPI、I2C等真正的總線,只要是繼承structbus_type基類對象的總線都可以)的probe()函數進行實例化的。例如三星的framebuffer驅動s3c-fb.c,就是通過實現struct platform_driver這個接口,在接口的probe()函數中,為識別到的framebuffer設備創建/dev/*下的節點,實現s3c-fb字符設備的實例化。
3. 在不實現總線接口的字符設備中,定義一個struct cdev mycdev;之后,在模塊加載module_init()的時候,也是可以調用構造函數(初始化函數),創建/dev/*下的設備節點,從而完成實例化的。這種情況下,mycdev可以代表一個設備的實例,這種情況下模擬了面向對象設計模型中的單例模式,這也是可行的。
5.聚合(組合)
- 聚合在面向對象中代表一種HAS-A的關系,比如struct cdev對象HAS-A struct file_operations對象,我們就認為structfile_operations對象與struct cdev對象是聚合關系。
- 在面向對象中,聚合關系主要是為了區別于繼承關系,例如V4L2子系統中,structvideo_device 有struct v4l2_file_operations對象,但是實際上structv4l2_file_operations中的函數都是struct file_operations中的同名函數,并且基類的struct file_operations中的同名函數最終會模擬多態函數的方式,調用到struct v4l2_file_operations中的函數。因而我們認為struct video_device對象中的struct v4l2_file_operations對象是繼承自struct cdev對象,而不是聚合關系(HAS-A).但是struct video_device對象中的v4l2_std_id tvnorms對象,在struct cdev基類對象中是不存在的,是structvideo_device對象特有的,struct video_device HAS-A v4l2_std_idtvnorms, 因而我們認為v4l2_std_id tvnorms對象與struct video_device對象是聚合關系。
- 由于C語言沒有嚴格的面向對象關鍵詞標準來支持,所以聚合和繼承的區別,還是需要人為分類維護。如果子類與基類有同名函數,并且子類同名函數被基類同名函數所調用,那么同名函數所在的*_operations對象都認為是從基類繼承過來的,子類所擁有的與基類無關的對象,我們才認為是子類的聚合。
6. 模板與泛型
- Linux內核中為了簡化復雜對象的定義,提供了很多#define宏來模仿面向對象中的模板和泛型機制。
1). 模板
- 典型的模板宏代碼如Linux內核信號量的模板include/linux/semaphore.h,為簡化信號量的定義與初始化,提供了模板函數。
#define DECLARE_MUTEX(name) / structsemaphore name = __SEMAPHORE_INITIALIZER(name, 1)
2). 泛型
- 典型的泛型宏代碼如Linux內核網絡部分的socket地址泛型(include/linux/net.h ),定義一個socket地址,socket地址的具體數據類型在實際定義的時候才由type參數確定。
#define DECLARE_SOCKADDR(type, dst,src) / typedst = ({ __sockaddr_check_size(sizeof(*dst)); (type) src; })7. 開閉原則
- 開閉原則是可復用的面向對象代碼設計的基石。
- 開閉原則是指,代碼要對擴展開放,對修改關閉。
- Linux內核設計中,通過繼承關系,以及用戶態與內核態隔離,限定使用一組API實現用戶態與內核態通信的機制,使得內核代碼對Linux內核態的設備驅動擴展與開發是開放的,而對Linux用戶態應用程序的各種可能的修改關閉。
- 通過開閉原則,也實現了程序員的分工,Linux內核對內核維護的程序員擴展開放,對用戶態應用開發程序員的修改關閉。整個內核設計思路是符合開閉原則的。
8. 設計模式
- 設計模式是在面向對象程序設計中總結出來的一些常用的代碼復用規則。Linux內核設計中,大量地參考了經典的設計模式,這里僅僅舉兩個例子,讀者在閱讀各驅動源碼時,需要自我總結一些設計模式。
1). 觀察者模式(訂閱者/發布者)
- 觀察者模式定義了一種一對多的依賴關系,讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀態發生變化時,會通知所有觀察者對象,使它們能夠自動更新自己。
- Linux內核的通知鏈模型是典型的觀察者模式,其介紹可以參閱本博客另一篇文章《Linux內核重點精要》中通知鏈模型的相關的介紹。
2).橋接模式(handle/body)
- 橋接模式的設計意圖將抽象部分與它的實現部分分離,使它們都可以獨立地變化。
- Linux內核中使用的最重要的橋接模式,在于萬物皆文件的思想。即將用戶態的抽象字符設備文件,與實際的字符設備驅動實現分離,從而使得文件描述符和內核設備驅動可以分別在用戶態和內核態獨立變化,只需要在open的時候將抽象文件與實際的設備驅動關聯起來即可。
- 抽象字符設備文件與實際的內核設備驅動橋接模式的UML簡化圖如圖9所示。
Figure 9 Linux內核設備驅動模型中經典的橋接模式
9. 參考文獻
- 《Linux內核設計與實現》—— Robert.Love
- 《設計模式》——伽馬等(四人組)
- 《JAVA編程思想》——Bruce.Eckel
- 《代碼大全》——SteveMcConnell
- 《C語言面向對象編程》 —— foruok的博客
新聞熱點
疑難解答