公元1951年5月15日的國會聽證上,美國陸軍五星上將麥克阿瑟建議把朝鮮戰爭擴大至中國,布萊德利隨後發言:「若是咱們把戰爭擴大到xxx中國,那麼咱們會被捲入到一場錯誤的時間,錯誤的地點同錯誤的對手打的一場錯誤的戰爭中。」
css

 

寫代碼,適用於一樣的原則,那就是把正確的代碼放到正確的位置而不是相反。一樣的一個代碼,能夠出如今多個可能的位置,它究竟應該出如今哪裏,是軟件架構設計的結果,說白了一切都是爲了高內核和低耦合。html

 

1.   陷入絕境

下面咱們設想一個名字叫作ABC的簡單的網卡,它須要接在一個CPU(假設CPU爲X)的內存總線上,須要地址、數據和控制總線(以及中斷pin腳等)。mysql

 

wKioL1lkgSazKttEAAA-BNNDYEc112.png

 

那麼在ABC的網卡驅動裏面,咱們須要定義ABC的基地址、中斷號等信息。假設在CPU X的電路板上面,ABC的地址爲0x100000,中斷號爲10。假設咱們是這樣定義的宏:linux

#define ABC_BASE 0x100000  
#define ABC_INTERRUPT 10
 

 

而且這樣寫代碼完成發送報文和初始化申請中斷:程序員

#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,...);  
}
 

 

這個代碼的問題在於,一旦從新換板子,ABC_BASE和ABC_IRQ就再也不同樣,代碼也須要隨之變動。sql

有的程序員說我能夠這麼幹:數據庫

#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一萬次,這樣寫代碼,找到了一種明顯的砌牆的感受(你感受寫代碼,就跟砌牆似的,一塊塊磚頭同樣放進去的時候,簡單重複機械,這個時候,就很危險了,可能代碼裏面就已經出現了很差的「味道」)。考慮到Linux向全世界各個產品適配,各類硬件適配的特色,究竟有多少個板子用ABC,還真的誰也說不清楚。架構

 

那麼,是否是真的#ifdef走一萬次,就必定能解決問題呢?還真的是不能。假設有一個電路板有2個ABC網卡,就完全傻眼了。難道這樣定義?ide

#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 style="white-space:pre">  </span>} else if (id == 1) {  
            writel(ABC2_BASE + REG_X, 1);  
            writel(ABC2_BASE + REG_Y, 0x3);  
    }  
    ...  
}
 

 

不管你怎麼改,這個代碼實在都已是慘不忍睹了,連本身都看不下去了。咱們爲何會陷入這樣的困境,是由於咱們犯了未能「把正確的代碼,放入正確的位置的錯誤」,這樣引入了極大的耦合。

 

2.   迷途反思

 

咱們犯的致命的錯誤,在於把板級互連信息,耦合進了驅動的代碼,致使驅動沒法跨平臺。

 

wKiom1lkgVXyJkwTAACc4ZRuTjY025.png

 

咱們轉念想想,ABC的驅動的真正職責是完成ABC網卡的收發流程,試問,這個流程,真的與它接在什麼CPU(TI、三星、Broad、Allwinner等)有半毛錢關係嗎?又和接在哪一個板子上有半毛錢關係嗎?

答案是真的沒有什麼關係!ABC網卡,不會由於你是TI的ARM,你是龍芯,仍是你是Blackfin有什麼不一樣。任你外面什麼板子排山倒海,狗急跳牆,ABC本身都是巋然不動。

 

既然沒有什麼關係,那麼這些板子級別的互連信息,又爲何要放在驅動的代碼裏面呢?基本上,咱們能夠認爲,ABC不會因誰而變,因此它的代碼應該是自然跨平臺的。故此,咱們認爲「#defineABC_BASE 0x100000, #define ABC_IRQ 10」這樣的代碼,出如今驅動裏面,屬於「在錯誤的地點,和錯誤的敵人,打一場錯誤的戰爭」。它沒有被放在正確的位置上,而咱們寫代碼,必定「讓天堂的歸天堂, 讓塵土的歸塵土」。咱們真實的期待,恐怕是這個樣子:

 

wKiom1lkgWzALCliAACItvoPv_E914.png

 

軟件工程強調高內聚、低耦合。若一個模塊內各元素聯繫的越緊密,則它的內聚性就越高;模塊之間聯繫越不緊密,其耦合性就越低。因此高內聚、低耦合強調,內部的要牢牢抱團,外面的給我滾蛋。對於驅動而言,板級互連信息,顯然屬於應該滾蛋的。每一個軟件模塊最好是一個宅男,不談戀愛,不看電影,不吃大餐,不踢足夠,和外界惟一的聯繫就是「餓了嗎」,這樣的軟件,顯然是又高內聚、又低耦合。

 

 有一次我在一個德國外企,問到工程師們「高內聚和低耦合是什麼關係」,有一個工程師很是積極地回答,「高內聚和低耦合是一對矛盾」。我以爲他的腦子好亂,若是必定要用一個關係來描述高內聚和低耦合的關係,我認爲他們符合馬列主義,毛xx思想強調的「高內聚和低耦合,相互依存,缺一不可,相輔相成,共同促進」,它其實反映了同一個事物兩個不一樣的側面,總之,把政治課本背一遍就對了。

 

你寫個串口的代碼,裏面從頭至尾都是串口相關的東西,聚地緊,它也天然不會滿世界亂跑到SPI裏面去耦合。SPI要和串口低耦合,它也勢必要求UART內部代碼把串口的東東所有聚一塊兒,不要亂竄,沒有SPI的戶口,居住證也不發給你,就給我滾回老家去。

 

wKioL1lkgYWhpkh1AABflHYrv0s969.png

 

 

3.   柳岸花明

如今板級互連信息已經和驅動分離開來了,讓它們彼此出如今不一樣的軟件模塊。可是,最終它們仍然有必定的聯繫,由於,驅動最終仍是要取出基地址、中斷號等板級信息的。怎麼取,這是個大問題。

一種方法是ABC的驅動滿世界詢問各個板子,「請問你的基地址,中斷號是幾?」,「你媽貴姓?」這仍然是一個嚴重的耦合。由於,驅動仍是得知道板子上有沒有ABC,哪一個板子有,怎麼個有法。它仍是在和板子直接耦合。

wKiom1lkgaKBkQIiAACp9W4f6yY648.png-wh_50

可不能夠有另一種方法,咱們維護一個共同的相似數據庫的東西,板子上有什麼網卡,基地址中斷號是什麼,都統一在一個地方維護。而後,驅動問一個統一的地方,經過一個統一的API來獲取即好?

wKioL1lkgcfCicU5AAC3R-fWQAw151.png

基於這樣的想法,linux把設備驅動分爲了總線、設備和驅動三個實體,總線是上圖中的統一紐帶,設備是上圖中的板級互連信息,這三個實體完成的職責分別以下:

實體

功能

代碼

設備

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

arch/arm

arch/blackfin

arch/xxx

等目錄

驅動

完成外設的功能,如網卡收發包,聲卡錄放,SD卡讀寫…

drivers/net

sound

drivers/mmc

等目錄

總線

完成設備和驅動的關聯

drivers/base/platform.c

drivers/pci/pci-driver.c

咱們把全部的板子互連信息填入設備端,而後讓設備端向總線註冊告知總線本身的存在,總線上面天然關聯了這些設備,並進一步間接關聯了設備的板級鏈接信息。好比arch/blackfin/mach-bf533/boards/ip0x.c這塊板子有2個DM9000的網卡,它是這樣註冊的:

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的總線這個統一紐帶上,天然就知道板子上面有2個DM9000的網卡。一旦DM9000的驅動也被註冊,因爲platform總線已經關聯了設備,驅動天然能夠根據已經存在的DM9000設備信息,獲知以下的內存基地址、中斷等信息了:

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  
    }  
};
 

 

總線存在的目的,則是把這些驅動和這些設備,一一配對的匹配在一塊兒。以下圖,某個電路板子上有2個ABC,1個DEF,1個HIJ設備,以及分別1個的ABC、DEF、HIJ驅動,那麼總線,就是讓2個ABC設備和1個ABC驅動匹配,DEF設備和驅動一對一匹配,HIJ設備和驅動一對一匹配。

wKiom1lkgeLgaj7JAAC_7Plfo9E227.png

驅動自己,則能夠用最簡單的API取出設備端填入的互連信息,看一下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);  
…  
}
 

 

這樣,板級互連信息,不再會闖入驅動,而驅動,看起來也沒有和設備之間直接耦合,由於它調用的都是總線級別的標準API:platform_get_resource()。總線裏面有個match()函數,來完成哪一個設備由哪一個驅動來服務的職責,好比對於掛在內存上的platform總線而言,它的匹配相似(最簡單的匹配方法就是設備和驅動的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是風河公司新的設備驅動程序架構,它是在VxWorks 6.2及之後版本被增長到VxWorks中的,直至VxWorks 6.9,基本都已經VxBus化了。可是,這個VxBus,能夠說和Linux的總線、設備、驅動模型是極大地雷同的。可是,請問,你爲何要叫VxBus呢,它很是地Vx嗎?

 

因此,這個時候咱們看到的代碼會是這樣,不管是哪一個板子的ABC設備,都統一使用了一個不變的drivers/net/ethernet/abc.c驅動,而arch/arm/mach-yyy/board-a.c這樣的代碼,則有不少不少份。

 

wKioL1lkgfuwbLb-AACITDA46_I846.png

 

4. 更上層樓

咱們仍然看到大量的arch/arm/mach-yyy/board-a.c這樣的代碼,衝刺着描述板級信息的細節代碼,儘管它自己已經和驅動解耦了。這些代碼的存在,簡直是對Linux內核的污染和對Linus Torvalds的無情藐視,由於,太木有技術含量了!

 

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

 

設備樹,是一種dts文件,它用最簡單的語法描述每一個板子上的全部設備,以及這些設備的鏈接信息。好比arch/arm/boot/dts/ imx1-apf9328.dts下面的DM9000就是這樣的腳本,基地址、中斷號都成爲了DM9000設備節點的一個屬性:

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這樣的文件永遠地進入了歷史的故紙堆,代碼就變成這樣的架構,換個板子,只要換個Device Tree就好。「讓天堂的歸天堂, 讓塵土的歸塵土」,讓驅動的歸驅動C代碼,讓設備的歸設備樹腳本。

 

wKiom1lkghPDSZu0AABgYsXrszY057.png

 

咱們很高興也很悲痛地看到,VxWorks 7的新版,也採用Device Tree了。咱們高興的是,它終於來了;咱們悲痛的是,它終於又來晚了。Linux的車輪滾滾向前,無情碾壓一切。人類的千年軌跡,滄海桑田,斗轉星移,重複地進行着歷史的歸於歷史,將來仍是歸於歷史的過程。這是現實的悲愴,也是歷史的豪邁。

 

 《孫子兵法》曰:「水因地而制流,兵因敵而制勝。故兵無常勢,水無常形;能因敵變化而取勝者,謂之神。」一切不過是順勢而爲,把正確的代碼,安放到正確的位置。