【Leo的手記】Linux設(shè)備驅(qū)動(dòng)程序手記3-分層抽象與次設(shè)備號(hào)

1.分層抽象,接口與封裝
對(duì)于設(shè)備驅(qū)動(dòng)程序而言,分離設(shè)備業(yè)務(wù)邏輯和底層硬件操作有利于驅(qū)動(dòng)程序的擴(kuò)展。而利用面向?qū)ο蟮乃枷雽?duì)驅(qū)動(dòng)程序進(jìn)行抽象是一個(gè)很好的方法。其主要的方法就是將驅(qū)動(dòng)程序的業(yè)務(wù)邏輯中關(guān)于底層硬件操作的部分通過(guò)某種接口加以規(guī)范。如同file_operations,由此便可以使用統(tǒng)一的接口進(jìn)行業(yè)務(wù)邏輯相關(guān)操作而無(wú)需關(guān)注硬件底層差異。在系統(tǒng)結(jié)構(gòu)圖上來(lái)看相當(dāng)于將業(yè)務(wù)邏輯作為上層實(shí)現(xiàn),而具體的硬件操作作為下層依賴,形成不同層級(jí)的系統(tǒng)結(jié)構(gòu)。而關(guān)于相關(guān)接口的實(shí)現(xiàn)則交由單獨(dú)的平臺(tái)相關(guān)的程序部分加以實(shí)現(xiàn)。此外還可以通過(guò)使用簡(jiǎn)易工廠模式對(duì)提供給業(yè)務(wù)邏輯對(duì)應(yīng)接口的部分代碼加以進(jìn)一步封裝。以此實(shí)現(xiàn)對(duì)于驅(qū)動(dòng)程序代碼的高效封裝,復(fù)用和拓展。
2.次設(shè)備號(hào)與一類設(shè)備
諸如tty設(shè)備與磁盤(pán)設(shè)備等大多數(shù)的設(shè)備,其硬件實(shí)體在系統(tǒng)中往往存在不僅一個(gè)實(shí)例。驅(qū)動(dòng)程序往往需要維護(hù)多個(gè)設(shè)備,或者說(shuō),驅(qū)動(dòng)程序應(yīng)該維護(hù)驅(qū)動(dòng)程序所支持的一類設(shè)備。因此,如何在系統(tǒng)中創(chuàng)建一類設(shè)備的多個(gè)實(shí)例并在驅(qū)動(dòng)程序中加以辨識(shí)和管理,則是一個(gè)必須納入考量的事情。
對(duì)于Linux系統(tǒng)而言,每一個(gè)設(shè)備都有一個(gè)主設(shè)備號(hào)與次設(shè)備號(hào),對(duì)于一個(gè)驅(qū)動(dòng)程序而言,通常會(huì)在驅(qū)動(dòng)程序加載的時(shí)候注冊(cè)設(shè)備并獲取主設(shè)備號(hào),主設(shè)備號(hào)用于關(guān)聯(lián)一組文件IO操作。因此,主設(shè)備號(hào)便可以用于區(qū)分一類設(shè)備,而次設(shè)備號(hào)便可以用于區(qū)分一類設(shè)備中的不同實(shí)例。
例如一個(gè)字符設(shè)備包含有多個(gè)設(shè)備實(shí)例,可以在注冊(cè)設(shè)備后,創(chuàng)建多個(gè)設(shè)備實(shí)例。實(shí)例代碼如下
3.驅(qū)動(dòng)程序中獲取次設(shè)備號(hào)
注冊(cè)設(shè)備時(shí)所綁定的一系列文件IO操作是服務(wù)于一類設(shè)備的,其包含了通用的業(yè)務(wù)方法抽象。上文指出可以通過(guò)次設(shè)備號(hào)來(lái)區(qū)分不同的設(shè)備實(shí)例,因此在接口中如何獲取次設(shè)備號(hào)則是需要討論的。
在初始化時(shí),通過(guò)device_create創(chuàng)建設(shè)備時(shí),我們通過(guò)MKDEV組合了主設(shè)備號(hào)與次設(shè)備號(hào)并用其創(chuàng)建了設(shè)備,在/dev目錄中我們也可以發(fā)現(xiàn)對(duì)于不同的設(shè)備,在使用ls打印時(shí)會(huì)顯示相關(guān)的主設(shè)備號(hào)和次設(shè)備號(hào)。我們可以發(fā)現(xiàn),設(shè)備號(hào)信息與設(shè)備對(duì)象是關(guān)聯(lián)的。而我們?cè)诓僮髟O(shè)備的不同實(shí)例時(shí)恰好就是訪問(wèn)了不同的設(shè)備文件,因此我們來(lái)看文件IO接口的原型。
我們可以看到,其每一個(gè)接口都至少指定了一個(gè)文件作為形參。事實(shí)上,Linux內(nèi)核提供了一個(gè)iminor方法用于獲取inode的次設(shè)備號(hào)。獲取方法如下
而對(duì)于諸如open和release方法,其形參列表中存在單獨(dú)的inode因此可以直接使用,而對(duì)于read,write等方法,則需要從其傳入的file參數(shù)中獲取inode信息。Linux內(nèi)核提供了一個(gè)file_inode方法用于提取struct file結(jié)構(gòu)體的inode。
由此便可獲取當(dāng)前所進(jìn)行系統(tǒng)調(diào)用所訪問(wèn)的設(shè)備文件的次設(shè)備號(hào)。
拓展
file_inode方法實(shí)際上返回了struct file結(jié)構(gòu)體中的f_inode成員。
iminor方法返回了struct inode結(jié)構(gòu)體中的i_rdev成員。
4.驅(qū)動(dòng)程序的分層抽象
我們可以看到,由于多個(gè)設(shè)備實(shí)例的存在,驅(qū)動(dòng)程序需要考慮的情況變得龐大。如果將對(duì)于不同設(shè)備的底層差異的判斷都交由接口所指定的對(duì)應(yīng)函數(shù)實(shí)現(xiàn), 則程序體量會(huì)膨脹。并且一旦對(duì)硬件進(jìn)行擴(kuò)展,便要修改驅(qū)動(dòng)程序的對(duì)應(yīng)源碼。這使得驅(qū)動(dòng)程序的開(kāi)發(fā)維護(hù)變得很爛。
一個(gè)好的想法在上文中已經(jīng)提到,我們可以將文件IO接口實(shí)現(xiàn)中對(duì)于底層的操作封裝為抽象的接口,而由負(fù)責(zé)硬件的程序部分實(shí)現(xiàn)該接口,由此便完成了抽象。
因此,在驅(qū)動(dòng)程序中我們可以將次設(shè)備號(hào)作為參數(shù)傳遞給相應(yīng)的底層接口,只需要考慮對(duì)于這一類設(shè)備的業(yè)務(wù)邏輯即可。大大提高了代碼的復(fù)用與易擴(kuò)展性。
例如,如果我們抽象一個(gè)LED的驅(qū)動(dòng)程序,我們可以將LED的操作和具體的硬件實(shí)現(xiàn)分離。在對(duì)應(yīng)的IO接口中僅僅實(shí)現(xiàn)對(duì)業(yè)務(wù)邏輯的包裝。在接口的實(shí)現(xiàn)中實(shí)現(xiàn)對(duì)業(yè)務(wù)邏輯的具體實(shí)現(xiàn)。
創(chuàng)建兩個(gè)文件,led_drv.c和led_suniv.c,編寫(xiě)Makefile。想要實(shí)現(xiàn)將多個(gè)c源碼文件編譯成一個(gè)內(nèi)核模塊文件,Makefile編寫(xiě)如下。
led_drv.c負(fù)責(zé)對(duì)字符設(shè)備進(jìn)行注冊(cè),并且實(shí)現(xiàn)業(yè)務(wù)邏輯。
led_suniv.c實(shí)現(xiàn)了f1c100s對(duì)于字符設(shè)備的底層實(shí)現(xiàn)。
想要實(shí)現(xiàn)對(duì)接口的定義,需要額外編寫(xiě)一個(gè)頭文件led_drv.h,用于規(guī)范相關(guān)的接口。其實(shí)現(xiàn)如下
led_drv.h
led_drv.c
實(shí)現(xiàn)如下
led_suniv.c
該文件主要實(shí)現(xiàn)底層的初始化,實(shí)現(xiàn)如下
5.測(cè)試程序