1. 程式人生 > >Linux那些事兒 之 戲說USB(12)從這裡開始

Linux那些事兒 之 戲說USB(12)從這裡開始

任小強們說房價高漲從現在開始,股評家們說牛市從5000點開始。他們的開始需要我們的錢袋,我的開始只需要一臺電腦,最好再有一杯茶,伴著幾支小曲兒,不盯著錢總是會比較愜意的。生容易,活容易,生活不容易,因為要盯著錢。 USB core從USB子系統的初始化開始,我們也需要從那裡開始,它們在檔案drivers/usb/core/usb.c 我們看到一個subsys_initcall,復旦人甲說它也是一個巨集,我們可以把它理解為module_init,只不過因為這部分程式碼比較核心,開發者們把它看作一個子系統,而不僅僅是一個模組,這也很好理解,usbcore這個模組它代表的不是某一個裝置,而是所有usb裝置賴以生存的模組,Linux中,像這樣一個類別的裝置驅動被歸結為一個子系統。比如pci子系統,比如scsi子系統,基本上,drivers/目錄下面第一層的每個目錄都算一個子系統,因為它們代表了一類裝置。subsys_initcall(usb_init)的意思就是告訴我們usb_init是我們真正的初始化函式,而usb_exit()將是整個usb子系統的結束時的清理函式,於是我們就從usb_init開始看起。
既然復旦人甲都這麼說了,那咱們就從usb_init開始看起吧。至於子系統在核心裡具體的描述,牽涉到linux裝置模型了,可以去看ldd3,或者更詳細的。目前來說,我們只需要知道子系統通常顯示在sysfs分層結構中的頂層,比如塊裝置子系統對應/sys/block,當然也不一定,usb子系統對應的就是/sys/bus/usb。 860 /* 861  * Init 862  */ 864 { 865         int retval; 866         if (nousb) { 867pr_info("%s: USB support disabled/n", usbcore_name); 868                 return 0;
869         } 870 872         if (retval) 873                 goto out; 875         if (retval) 876                 goto bus_register_failed; 878         if (retval) 879                 goto host_init_failed; 881         if (retval) 882                 goto major_init_failed; 884         if (retval)
885                 goto driver_register_failed; 887         if (retval) 888                 goto usb_devio_init_failed; 890         if (retval) 891                 goto fs_init_failed; 893         if (retval) 894                 goto hub_init_failed; 896         if (!retval) 897                 goto out; 898 900 hub_init_failed: 902 fs_init_failed: 904 usb_devio_init_failed: 906 driver_register_failed: 908 major_init_failed: 910 host_init_failed: 912 bus_register_failed: 914out: 915         return retval; 916 } 看到上面定義裡的__init標記沒,寫過驅動的應該不會陌生,它對核心來說就是一種暗示,表明這個函式僅在初始化期間使用,在模組被裝載之後,它佔用的資源就會釋放掉用作它處。它的暗示你懂,可你的暗示,她卻不懂或者懂裝不懂,多麼讓人感傷。它在自己短暫的一生中一直從事繁重的工作,吃的是草吐出的是牛奶,留下的是整個USB子系統的繁榮。 受這種精神所感染,我覺得還是有必要為它說的更多些,21世紀多的是任小強,缺的是知恩圖報的人。對__init的定義在include/linux/init.h裡 好像這裡的疑問要更多,不過與__init相比,這點辛苦算什麼,我會在它強大的精神支援下儘量說清楚的。那麼__attribute__是什麼?Linux核心程式碼使用了大量的GNU C擴充套件,以至於GNU C成為能夠編譯核心的唯一編譯器,GNU C的這些擴充套件對程式碼優化、目的碼佈局、安全檢查等方面也提供了很強的支援。而__attribute__就是這些擴充套件中的一個,它主要被用來宣告一些特殊的屬性,這些屬性主要被用來指示編譯器進行特定方面的優化和更仔細的程式碼檢查。GNU C支援十幾個屬性,section是其中的一個,我們檢視gcc的手冊可以看到下面的描述 ‘section ("section-name")'   Normally, the compiler places the code it generates in the `text'      section. Sometimes, however, you need additional sections, or you need certain particular functions to appear in special sections.
The `section' attribute specifies that a function lives in a
   particular section. For example, the declaration:
      extern void foobar (void) __attribute__ ((section ("bar")));   puts the function ‘foobar' in the ‘bar' section.        Some file formats do not support arbitrary sections so the
   ‘section' attribute is not available on all platforms. 
If you
  
need to map the entire contents of a module to a particular
   section, consider using the facilities of the linker instead.
通常編譯器將函式放在.text節,變數放在.data 或 .bss 節,使用section屬性,可以讓編譯器將函式或變數放在指定的節中。那麼前面對__init的定義便表示將它修飾的程式碼放在.init.text節。聯結器可以把相同節的程式碼或資料安排在一起,比如__init修飾的所有程式碼都會被放在.init.text節裡,初始化結束後就可以釋放這部分記憶體。 那核心又是如何呼叫到這些__init修飾的初始化函式那?好奇心是科學的原動力,茶葉蛋就是這麼煮出來的,原子彈也是這麼造出來的,__init背後的哲學總不會比它們還難,芙蓉姐姐說了,我挑戰,我喜歡。好像越聊越遠了,不過想想多少年前當自己還是青澀少年的時候就對它極度的好奇過,這裡還是儘量將它說一下,也順便積攢下rp,稍後的冠軍盃裡俺的米蘭也好有個開門紅。 要回答這個問題,還需要回顧一下上面938行的程式碼,那裡已經提到subsys_initcall也是一個巨集,它也在include/linux/init.h裡定義

這裡又出現了一個巨集__define_initcall,它是用來將指定的函式指標fn放到initcall.init節裡,也在include/linux/init.h檔案裡定義,這裡就不多說了,有那點意思就可以了。而對於具體的subsys_initcall巨集,則是把fn放到.initcall.init的子節.initcall4.init裡。要弄清楚.initcall.init、.init.text和.initcall4.init這樣的東東,我們還需要了解一點核心可執行檔案相關的概念。

核心可執行檔案由許多連結在一起的物件檔案組成。物件檔案有許多節,如文字、資料、init 資料、bass 等等。這些物件檔案都是由一個稱為連結器指令碼的檔案連結並裝入的。這個連結器指令碼的功能是將輸入物件檔案的各節對映到輸出檔案中;換句話說,它將所有輸入物件檔案都連結到單一的可執行檔案中,將該可執行檔案的各節裝入到指定地址處。 vmlinux.lds是存在於 arch/<target>/ 目錄中的核心連結器指令碼,它負責連結核心的各個節並將它們裝入記憶體中特定偏移量處。

涉及到的東西越來越多了是吧,先深呼吸,平靜一下,堅定而又勇敢的開啟arch/i386/kernel/vmlinux.lds檔案,你就會見到前所未見的景象。我可以負責任的說,要看懂這個檔案是需要一番功夫的,不過大家都是聰明人,聰明人做聰明事,所以你需要做的只是搜尋initcall.init,然後便會看到似曾相識的內容

__inicall_start = .; .initcall.init : AT(ADDR(.initcall.init) – 0xC0000000) { *(.initcall1.init) *(.initcall2.init) *(.initcall3.init) *(.initcall4.init) *(.initcall5.init) *(.initcall6.init) *(.initcall7.init) } __initcall_end = .;

這裡的__initcall_start指向.initcall.init節的開始,__initcall_end指向它的結尾。而.initcall.init節又被分為了7個子節,分別是

.initcall1.init .initcall2.init .initcall3.init .initcall4.init .initcall5.init .initcall6.init .initcall7.init

我們的subsys_initcall巨集便是將指定的函式指標放在了.initcall4.init子節。其它的比如core_initcall將函式指標放在.initcall1.init子節,device_initcall將函式指標放在了.initcall6.init子節等等,都可以從include/linux/init.h檔案找到它們的定義。各個位元組的順序是確定的,即先呼叫.initcall1.init中的函式指標再呼叫.initcall2.init中的函式指標,等等。__init修飾的初始化函式在核心初始化過程中呼叫的順序和.initcall.init節裡函式指標的順序有關,不同的初始化函式被放在不同的子節中,因此也就決定了它們的呼叫順序。

至於實際執行函式呼叫的地方,就在/init/main.c檔案裡,核心的初始化麼,不在那裡還能在哪裡,裡面的do_initcalls函式會直接用到這裡的__initcall_start、__initcall_end來進行判斷,不多說了。我們的思念已經入滔滔江水氾濫成災了,還是回到久違的usb_init函式吧。