溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

讓天堂的歸天堂,讓塵土的歸塵土——談Linux的總線、設(shè)備、驅(qū)動模型

發(fā)布時間:2020-06-09 05:43:47 來源:網(wǎng)絡(luò) 閱讀:18419 作者:21cnbao 欄目:系統(tǒng)運(yùn)維

公元1951年5月15日的國會聽證上,美國陸軍五星上將麥克阿瑟建議把朝鮮戰(zhàn)爭擴(kuò)大至中國,布萊德利隨后發(fā)言:“如果我們把戰(zhàn)爭擴(kuò)大到×××中國,那么我們會被卷入到一場錯誤的時間,錯誤的地點(diǎn)同錯誤的對手打的一場錯誤的戰(zhàn)爭中?!?/span>


寫代碼,適用于同樣的原則,那就是把正確的代碼放到正確的位置而不是相反。同樣的一個代碼,可以出現(xiàn)在多個可能的位置,它究竟應(yīng)該出現(xiàn)在哪里,是軟件架構(gòu)設(shè)計(jì)的結(jié)果,說白了一切都是為了高內(nèi)核和低耦合。


1.   陷入絕境

下面我們設(shè)想一個名字叫做ABC的簡單的網(wǎng)卡,它需要接在一個CPU(假設(shè)CPU為X)的內(nèi)存總線上,需要地址、數(shù)據(jù)和控制總線(以及中斷pin腳等)。


讓天堂的歸天堂,讓塵土的歸塵土——談Linux的總線、設(shè)備、驅(qū)動模型


那么在ABC的網(wǎng)卡驅(qū)動里面,我們需要定義ABC的基地址、中斷號等信息。假設(shè)在CPU X的電路板上面,ABC的地址為0x100000,中斷號為10。假設(shè)我們是這樣定義的宏:

#define ABC_BASE 0x100000  
#define ABC_INTERRUPT 10


并且這樣寫代碼完成發(fā)送報(bào)文和初始化申請中斷:

#define ABC_BASE 0x100000  
#define ABC_IRQ 10  
  
int abc_send(...)  
{  
        writel(ABC_BASE + REG_X, 1);  
        writel(ABC_BASE + REG_Y, 0x3);  
        ...  
}  
  
int abc_init(...)  
{  
        request_irq(ABC_IRQ,...);  
}


這個代碼的問題在于,一旦重新?lián)Q板子,ABC_BASE和ABC_IRQ就不再一樣,代碼也需要隨之變更。

有的程序員說我可以這么干:

#ifdef BOARD_A  
#define ABC_BASE 0x100000  
#define ABC_IRQ 10  
  
#elif defined(BOARD_B)  
#define ABC_BASE 0x110000  
#define ABC_IRQ 20  
  
#elif defined(BOARD_C)  
#define ABC_BASE 0x120000  
#define ABC_IRQ 10  
...  
#endif


這么干固然是可以,但是如果你有1萬個不同的板子,你就要ifdef一萬次,這樣寫代碼,找到了一種明顯的砌墻的感覺(你感覺寫代碼,就跟砌墻似的,一塊塊磚頭一樣放進(jìn)去的時候,簡單重復(fù)機(jī)械,這個時候,就很危險(xiǎn)了,可能代碼里面就已經(jīng)出現(xiàn)了不好的“味道”)??紤]到Linux向全世界各個產(chǎn)品適配,各種硬件適配的特點(diǎn),究竟有多少個板子用ABC,還真的誰也說不清楚。


那么,是不是真的#ifdef走一萬次,就一定能解決問題呢?還真的是不能。假設(shè)有一個電路板有2個ABC網(wǎng)卡,就徹底傻眼了。難道這樣定義?

#ifdef BOARD_A  
#define ABC1_BASE 0x100000  
#define ABC1_IRQ 10  
#define ABC2_BASE 0x101000  
#define ABC2_IRQ 11  
  
#elif defined(BOARD_B)  
#define ABC1_BASE 0x110000  
#define ABC1_IRQ 20  
...  
#endif


如果這樣做,abc_send()和abc_init()又該如何改?難道這樣:

int abc1_send(...)  
{  
        writel(ABC1_BASE + REG_X, 1);  
        writel(ABC1_BASE + REG_Y, 0x3);  
        ...  
}  
  
int abc1_init(...)  
{  
        request_irq(ABC1_IRQ,...);  
}  
  
int abc2_send(...)  
{  
        writel(ABC2_BASE + REG_X, 1);  
        writel(ABC2_BASE + REG_Y, 0x3);  
        ...  
}  
  
int abc2_init(...)  
{  
        request_irq(ABC2_IRQ,...);  
}  
…


還是這樣?

int abc_send(int id, ...)  
{  
    if (id == 0) {  
            writel(ABC1_BASE + REG_X, 1);  
            writel(ABC1_BASE + REG_Y, 0x3);  
<span >  </span>} else if (id == 1) {  
            writel(ABC2_BASE + REG_X, 1);  
            writel(ABC2_BASE + REG_Y, 0x3);  
    }  
    ...  
}


無論你怎么改,這個代碼實(shí)在都已經(jīng)是慘不忍睹了,連自己都看不下去了。我們?yōu)槭裁磿萑脒@樣的困境,是因?yàn)槲覀兎噶宋茨堋鞍颜_的代碼,放入正確的位置的錯誤”,這樣引入了極大的耦合。


2.   迷途反思


我們犯的致命的錯誤,在于把板級互連信息,耦合進(jìn)了驅(qū)動的代碼,導(dǎo)致驅(qū)動無法跨平臺。


讓天堂的歸天堂,讓塵土的歸塵土——談Linux的總線、設(shè)備、驅(qū)動模型


我們轉(zhuǎn)念想一想,ABC的驅(qū)動的真正職責(zé)是完成ABC網(wǎng)卡的收發(fā)流程,試問,這個流程,真的與它接在什么CPU(TI、三星、Broad、Allwinner等)有半毛錢關(guān)系嗎?又和接在哪個板子上有半毛錢關(guān)系嗎?

答案是真的沒有什么關(guān)系!ABC網(wǎng)卡,不會因?yàn)槟闶荰I的ARM,你是龍芯,還是你是Blackfin有什么不同。任你外面什么板子排山倒海,狗急跳墻,ABC自己都是巋然不動。


既然沒有什么關(guān)系,那么這些板子級別的互連信息,又為什么要放在驅(qū)動的代碼里面呢?基本上,我們可以認(rèn)為,ABC不會因誰而變,所以它的代碼應(yīng)該是天然跨平臺的。故此,我們認(rèn)為“#defineABC_BASE 0x100000, #define ABC_IRQ 10”這樣的代碼,出現(xiàn)在驅(qū)動里面,屬于“在錯誤的地點(diǎn),和錯誤的敵人,打一場錯誤的戰(zhàn)爭”。它沒有被放在正確的位置上,而我們寫代碼,一定“讓天堂的歸天堂, 讓塵土的歸塵土”。我們真實(shí)的期待,恐怕是這個樣子:


讓天堂的歸天堂,讓塵土的歸塵土——談Linux的總線、設(shè)備、驅(qū)動模型


軟件工程強(qiáng)調(diào)高內(nèi)聚、低耦合。若一個模塊內(nèi)各元素聯(lián)系的越緊密,則它的內(nèi)聚性就越高;模塊之間聯(lián)系越不緊密,其耦合性就越低。所以高內(nèi)聚、低耦合強(qiáng)調(diào),內(nèi)部的要緊緊抱團(tuán),外面的給我滾蛋。對于驅(qū)動而言,板級互連信息,顯然屬于應(yīng)該滾蛋的。每個軟件模塊最好是一個宅男,不談戀愛,不看電影,不吃大餐,不踢足夠,和外界唯一的聯(lián)系就是“餓了嗎”,這樣的軟件,顯然是又高內(nèi)聚、又低耦合。


 有一次我在一個德國外企,問到工程師們“高內(nèi)聚和低耦合是什么關(guān)系”,有一個工程師非常積極地回答,“高內(nèi)聚和低耦合是一對矛盾”。我覺得他的腦子好亂,如果一定要用一個關(guān)系來描述高內(nèi)聚和低耦合的關(guān)系,我認(rèn)為他們符合馬列主義,×××思想強(qiáng)調(diào)的“高內(nèi)聚和低耦合,相互依存,缺一不可,相輔相成,共同促進(jìn)”,它其實(shí)反映了同一個事物兩個不同的側(cè)面,總之,把政治課本背一遍就對了。


你寫個串口的代碼,里面從頭到尾都是串口相關(guān)的東西,聚地緊,它也自然不會滿世界亂跑到SPI里面去耦合。SPI要和串口低耦合,它也勢必要求UART內(nèi)部代碼把串口的東東全部聚一起,不要亂竄,沒有SPI的戶口,居住證也不發(fā)給你,就給我滾回老家去。


讓天堂的歸天堂,讓塵土的歸塵土——談Linux的總線、設(shè)備、驅(qū)動模型



3.   柳岸花明

現(xiàn)在板級互連信息已經(jīng)和驅(qū)動分離開來了,讓它們彼此出現(xiàn)在不同的軟件模塊。但是,最終它們?nèi)匀挥幸欢ǖ穆?lián)系,因?yàn)椋?qū)動最終還是要取出基地址、中斷號等板級信息的。怎么取,這是個大問題。

一種方法是ABC的驅(qū)動滿世界詢問各個板子,“請問你的基地址,中斷號是幾?”,“你媽貴姓?”這仍然是一個嚴(yán)重的耦合。因?yàn)?,?qū)動還是得知道板子上有沒有ABC,哪個板子有,怎么個有法。它還是在和板子直接耦合。

讓天堂的歸天堂,讓塵土的歸塵土——談Linux的總線、設(shè)備、驅(qū)動模型

可不可以有另外一種方法,我們維護(hù)一個共同的類似數(shù)據(jù)庫的東西,板子上有什么網(wǎng)卡,基地址中斷號是什么,都統(tǒng)一在一個地方維護(hù)。然后,驅(qū)動問一個統(tǒng)一的地方,通過一個統(tǒng)一的API來獲取即好?

讓天堂的歸天堂,讓塵土的歸塵土——談Linux的總線、設(shè)備、驅(qū)動模型

基于這樣的想法,linux把設(shè)備驅(qū)動分為了總線、設(shè)備和驅(qū)動三個實(shí)體,總線是上圖中的統(tǒng)一紐帶,設(shè)備是上圖中的板級互連信息,這三個實(shí)體完成的職責(zé)分別如下:

實(shí)體

功能

代碼

設(shè)備

描述基地址、中斷號、時鐘、DMA、復(fù)位等信息

arch/arm

arch/blackfin

arch/xxx

等目錄

驅(qū)動

完成外設(shè)的功能,如網(wǎng)卡收發(fā)包,聲卡錄放,SD卡讀寫…

drivers/net

sound

drivers/mmc

等目錄

總線

完成設(shè)備和驅(qū)動的關(guān)聯(lián)

drivers/base/platform.c

drivers/pci/pci-driver.c

我們把所有的板子互連信息填入設(shè)備端,然后讓設(shè)備端向總線注冊告知總線自己的存在,總線上面自然關(guān)聯(lián)了這些設(shè)備,并進(jìn)一步間接關(guān)聯(lián)了設(shè)備的板級連接信息。比如arch/blackfin/mach-bf533/boards/ip0x.c這塊板子有2個DM9000的網(wǎng)卡,它是這樣注冊的:

static struct resource dm9000_resource1[] = {  
    {  
        .start = 0x20100000,  
        .end   = 0x20100000 + 1,  
        .flags = IORESOURCE_MEM  
    },{  
        .start = 0x20100000 + 2,  
        .end   = 0x20100000 + 3,  
        .flags = IORESOURCE_MEM  
    },{  
        .start = IRQ_PF15,  
        .end   = IRQ_PF15,  
        .flags = IORESOURCE_IRQ | IORESOURCE_IRQ_HIGHEDGE  
    }  
};  
  
static struct resource dm9000_resource2[] = {  
    {  
        .start = 0x20200000,  
        .end   = 0x20200000 + 1,  
        .flags = IORESOURCE_MEM  
    }…  
};  
  
…  
static struct platform_device dm9000_device1 = {  
    .name           = "dm9000",  
    .id             = 0,  
    .num_resources  = ARRAY_SIZE(dm9000_resource1),  
    .resource       = dm9000_resource1,  
};  
  
…  
static struct platform_device dm9000_device2 = {  
    .name           = "dm9000",  
    .id             = 1,  
    .num_resources  = ARRAY_SIZE(dm9000_resource2),  
    .resource       = dm9000_resource2,  
};  
  
static struct platform_device *ip0x_devices[] __initdata = {  
    &dm9000_device1,  
    &dm9000_device2,  
…  
};  
  
static int __init ip0x_init(void)  
{  
    platform_add_devices(ip0x_devices, ARRAY_SIZE(ip0x_devices));  
    …  
}


這樣platform的總線這個統(tǒng)一紐帶上,自然就知道板子上面有2個DM9000的網(wǎng)卡。一旦DM9000的驅(qū)動也被注冊,由于platform總線已經(jīng)關(guān)聯(lián)了設(shè)備,驅(qū)動自然可以根據(jù)已經(jīng)存在的DM9000設(shè)備信息,獲知如下的內(nèi)存基地址、中斷等信息了:

static struct resource dm9000_resource1[] = {  
    {  
        .start = 0x20100000,  
        .end   = 0x20100000 + 1,  
        .flags = IORESOURCE_MEM  
    },{  
        .start = 0x20100000 + 2,  
        .end   = 0x20100000 + 3,  
        .flags = IORESOURCE_MEM  
    },{  
        .start = IRQ_PF15,  
        .end   = IRQ_PF15,  
        .flags = IORESOURCE_IRQ | IORESOURCE_IRQ_HIGHEDGE  
    }  
};


總線存在的目的,則是把這些驅(qū)動和這些設(shè)備,一一配對的匹配在一起。如下圖,某個電路板子上有2個ABC,1個DEF,1個HIJ設(shè)備,以及分別1個的ABC、DEF、HIJ驅(qū)動,那么總線,就是讓2個ABC設(shè)備和1個ABC驅(qū)動匹配,DEF設(shè)備和驅(qū)動一對一匹配,HIJ設(shè)備和驅(qū)動一對一匹配。

讓天堂的歸天堂,讓塵土的歸塵土——談Linux的總線、設(shè)備、驅(qū)動模型

驅(qū)動本身,則可以用最簡單的API取出設(shè)備端填入的互連信息,看一下drivers/net/ethernet/davicom/dm9000.c的dm9000_probe()代碼:

static int dm9000_probe(struct platform_device *pdev)  
{  
    …  
db->addr_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);  
db->data_res = platform_get_resource(pdev, IORESOURCE_MEM, 1);  
db->irq_res  = platform_get_resource(pdev, IORESOURCE_IRQ, 0);  
…  
}


這樣,板級互連信息,再也不會闖入驅(qū)動,而驅(qū)動,看起來也沒有和設(shè)備之間直接耦合,因?yàn)樗{(diào)用的都是總線級別的標(biāo)準(zhǔn)API:platform_get_resource()??偩€里面有個match()函數(shù),來完成哪個設(shè)備由哪個驅(qū)動來服務(wù)的職責(zé),比如對于掛在內(nèi)存上的platform總線而言,它的匹配類似(最簡單的匹配方法就是設(shè)備和驅(qū)動的name字段一樣):

static int platform_match(struct device *dev, struct device_driver *drv)  
{  
        struct platform_device *pdev = to_platform_device(dev);  
        struct platform_driver *pdrv = to_platform_driver(drv);  
  
        /* When driver_override is set, only bind to the matching driver */  
        if (pdev->driver_override)   
                return !strcmp(pdev->driver_override, drv->name);  
  
        /* Attempt an OF style match first */  
        if (of_driver_match_device(dev, drv))  
                return 1;  
  
        /* Then try ACPI style match */  
        if (acpi_driver_match_device(dev, drv))  
                return 1;  
  
        /* Then try to match against the id table */  
        if (pdrv->id_table)  
                return platform_match_id(pdrv->id_table, pdev) != NULL;  
  
        /* fall-back to driver name match */  
        return (strcmp(pdev->name, drv->name) == 0);  
}


VxBus是風(fēng)河公司新的設(shè)備驅(qū)動程序架構(gòu),它是在VxWorks 6.2及以后版本被增加到VxWorks中的,直至VxWorks 6.9,基本都已經(jīng)VxBus化了。但是,這個VxBus,可以說和Linux的總線、設(shè)備、驅(qū)動模型是極大地雷同的。但是,請問,你為什么要叫VxBus呢,它非常地Vx嗎?


所以,這個時候我們看到的代碼會是這樣,無論是哪個板子的ABC設(shè)備,都統(tǒng)一使用了一個不變的drivers/net/ethernet/abc.c驅(qū)動,而arch/arm/mach-yyy/board-a.c這樣的代碼,則有很多很多份。


讓天堂的歸天堂,讓塵土的歸塵土——談Linux的總線、設(shè)備、驅(qū)動模型


4. 更上層樓

我們?nèi)匀豢吹酱罅康腶rch/arm/mach-yyy/board-a.c這樣的代碼,沖刺著描述板級信息的細(xì)節(jié)代碼,盡管它本身已經(jīng)和驅(qū)動解耦了。這些代碼的存在,簡直是對Linux內(nèi)核的污染和對Linus Torvalds的無情藐視,因?yàn)?,太木有技術(shù)含量了!


我們有理由,把這些設(shè)備端的信息,用一個非C的腳本語言來描述,這個腳本文件,就是傳說中的Device Tree(設(shè)備樹)。


設(shè)備樹,是一種dts文件,它用最簡單的語法描述每個板子上的所有設(shè)備,以及這些設(shè)備的連接信息。比如arch/arm/boot/dts/ imx1-apf9328.dts下面的DM9000就是這樣的腳本,基地址、中斷號都成為了DM9000設(shè)備節(jié)點(diǎn)的一個屬性:

eth: eth@4,c00000 {  
        compatible = "davicom,dm9000";  
        reg = <   
                4 0x00c00000 0x2  
                4 0x00c00002 0x2  
        >;  
        interrupt-parent = <&gpio2>;  
        interrupts = <14 IRQ_TYPE_LEVEL_LOW>;  
        …  
};


之后,C代碼被剔除,arch/arm/mach-xxx/board-a.c這樣的文件永遠(yuǎn)地進(jìn)入了歷史的故紙堆,代碼就變成這樣的架構(gòu),換個板子,只要換個Device Tree就好?!白屘焯玫臍w天堂, 讓塵土的歸塵土”,讓驅(qū)動的歸驅(qū)動C代碼,讓設(shè)備的歸設(shè)備樹腳本。


讓天堂的歸天堂,讓塵土的歸塵土——談Linux的總線、設(shè)備、驅(qū)動模型


我們很高興也很悲痛地看到,VxWorks 7的新版,也采用Device Tree了。我們高興的是,它終于來了;我們悲痛的是,它終于又來晚了。Linux的車輪滾滾向前,無情碾壓一切。人類的千年軌跡,滄海桑田,斗轉(zhuǎn)星移,重復(fù)地進(jìn)行著歷史的歸于歷史,未來還是歸于歷史的過程。這是現(xiàn)實(shí)的悲愴,也是歷史的豪邁。


 《孫子兵法》曰:“水因地而制流,兵因敵而制勝。故兵無常勢,水無常形;能因敵變化而取勝者,謂之神?!币磺胁贿^是順勢而為,把正確的代碼,安放到正確的位置。

為了更進(jìn)一步深入地探討這個話題,CSDN學(xué)院聯(lián)合博主組織了2017年7月5日8PM~9PM的關(guān)于《探究Linux的總線、設(shè)備、驅(qū)動模型》直播活動,有314人參與了在線直播,活動已經(jīng)結(jié)束,想觀看錄播視頻的讀者可以進(jìn)入:

http://edu.csdn.net/huiyiCourse/detail/426?ref=0


向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI