天天看點

《Linux核心修煉之道》——分析核心源碼如何入手?(下)

下面的分析,米盧教練說了,内容不重要,重要的是态度。就像韓局長對待日記的态度那樣,嚴謹而細緻。

  隻要你使用這樣的态度開始分析核心,那麼無論你選擇核心的哪個部分作為切入點,比如usb,比如程序管理,在花費相對不算很多的時間之後,你就會發現你對核心的了解會上升到另外一個高度,一個抱着情景分析,抱着0.1核心完全注釋,抱着各種各樣的核心書籍翻來覆去的看很多遍又忘很多遍都無法達到的高度。請相信我!

  态度決定一切:從初始化函數開始

  有了地圖kconfig和makefile,我們可以在龐大複雜的核心代碼中定位以及縮小了目标代碼的範圍。那麼現在,為了研究核心對usb子系統的實作,我們還需要在目标代碼中找到一個突破口,這個突破口就是usb子系統的初始化代碼。

  針對某個子系統或某個驅動,核心使用subsys_initcall或module_init宏指定初始化函數。在drivers/usb/core/usb.c檔案中,我們可以發現下面的代碼。

940 subsys_initcall(usb_init);

941 module_exit(usb_exit);

  我們看到一個subsys_initcall,它也是一個宏,我們可以把它了解為module_init,隻不過因為這部分代碼比較核心,開發者們把它看作一個子系統,而不僅僅是一個子產品。這也很好了解,usbcore這個子產品它代表的不是某一個裝置,而是所有usb裝置賴以生存的子產品,linux中,像這樣一個類别的裝置驅動被歸結為一個子系統。比如pci子系統,比如scsi子系統,基本上,drivers/目錄下面第一層的每個目錄都算一個子系統,因為它們代表了一類裝置。

  subsys_initcall(usb_init)的意思就是告訴我們usb_init是usb子系統真正的初始化函數,而usb_exit()将是整個usb子系統的結束時的清理函數。于是為了研究usb子系統在核心中的實作,我們需要從usb_init函數開始看起。

865 static int __init usb_init(void)

866 {

867   int retval;

868   if (nousb) {

869    pr_info("%s: usb support disabled/n", usbcore_name);

870    return 0;

871   }

872

873   retval = ksuspend_usb_init();

874   if (retval)

875    goto out;

876   retval = bus_register(&usb_bus_type);

877   if (retval)

878    goto bus_register_failed;

879   retval = usb_host_init();

880   if (retval)

881    goto host_init_failed;

882   retval = usb_major_init();

883   if (retval)

884    goto major_init_failed;

885   retval = usb_register(&usbfs_driver);

886   if (retval)

887    goto driver_register_failed;

888   retval = usb_devio_init();

889   if (retval)

890    goto usb_devio_init_failed;

891   retval = usbfs_init();

892   if (retval)

893    goto fs_init_failed;

894   retval = usb_hub_init();

895   if (retval)

896    goto hub_init_failed;

897   retval = usb_register_device_driver(&usb_generic_driver, this_module);

898   if (!retval)

899    goto out;

900

901   usb_hub_cleanup();

902  hub_init_failed:

903   usbfs_cleanup();

904 fs_init_failed:

905   usb_devio_cleanup();

906 usb_devio_init_failed:

907   usb_deregister(&usbfs_driver);

908 driver_register_failed:

909   usb_major_cleanup();

910 major_init_failed:

911   usb_host_cleanup();

912 host_init_failed:

913   bus_unregister(&usb_bus_type);

914 bus_register_failed:

915   ksuspend_usb_cleanup();

916 out:

917   return retval;

918 }

 (1)__init标記。

  關于usb_init,第一個問題是,第865行的__init标記具有什麼意義?

  寫過驅動的應該不會陌生,它對核心來說就是一種暗示,表明這個函數僅在初始化期間使用,在子產品被裝載之後,它占用的資源就會釋放掉用作它處。它的暗示你懂,可你的暗示,她卻不懂或者懂裝不懂,多麼讓人感傷。它在自己短暫的一生中一直從事繁重的工作,吃的是草吐出的是牛奶,留下的是整個usb子系統的繁榮。

  受這種精神所感染,我覺得有必要為它說的更多些。__init的定義在include/linux/init.h檔案裡

43 #define __init          __attribute__ ((__section__ (".init.text")))

  好像這裡引出了更多的疑問,__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修飾的初始化函數?要回答這個問題,還需要回顧一下subsys_initcall宏,它也在include/linux/init.h裡定義

125 #define subsys_initcall(fn)             __define_initcall("4",fn,4)

  這裡又出現了一個宏__define_initcall,它用于将指定的函數指針fn放到initcall.init節裡 而對于具體的subsys_initcall宏,則是把fn放到.initcall.init的子節.initcall4.init裡。要弄清楚.initcall.init、.init.text和.initcall4.init這樣的東東,我們還需要了解一點核心可執行檔案相關的概念。

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

  我可以負責任的告訴你,要看懂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來進行判斷。

  (2)子產品參數。

  關于usb_init函數,第二個問題是,第868行的nousb表示什麼?

  知道c語言的人都會知道nousb是一個标志,隻是不同的标志有不一樣的精彩,這裡的nousb是用來讓我們在啟動核心的時候通過核心參數去掉usb子系統的,linux社會是一個很人性化的世界,它不會去逼迫我們接受usb,一切都隻關乎我們自己的需要。不過我想我們一般來說是不會去指定nousb的吧。如果你真的指定了nousb,那它就隻會幽怨的說一句“usb support disabled”,然後退出usb_init。

  nousb在drivers/usb/core/usb.c檔案中定義為:

static int nousb; /* disable usb when built into kernel image */

module_param_named(autosuspend, usb_autosuspend_delay, int, 0644);

module_parm_desc(autosuspend, "default autosuspend delay");

  從中可知nousb是個子產品參數。關于子產品參數,我們都知道可以在加載子產品的時候可以指定,但是如何在核心啟動的時候指定?

  打開系統的grub檔案,然後找到kernel行,比如:

kernel  /boot/vmlinuz-2.6.18-kdb root=/dev/sda1 ro splash=silent vga=0x314

  其中的root,splash,vga等都表示核心參數。當某一子產品被編譯進核心的時候,它的子產品參數便需要在kernel行來指定,格式為“子產品名.參數=值”,比如:

modprobe usbcore autosuspend=2

  對應到kernel行,即為:

usbcore.autosuspend=2

  通過指令“modinfo -p ${modulename}”可以得知一個子產品有哪些參數可以使用。同時,對于已經加載到核心裡的子產品,它們的子產品參數會列舉在/sys/module/${modulename}/parameters/目錄下面,可以使用“echo -n ${value} > /sys/module/${modulename}/parameters/${parm}”這樣的指令去修改。

  (3)可變參數宏。

  關于usb_init函數,第三個問題是,pr_info如何實作與使用?

  pr_info隻是一個列印資訊的可辨參數宏,printk的變體,在include/linux/kernel.h裡定義:

242 #define pr_info(fmt,arg...) /

243         printk(kern_info fmt,##arg)

  99年的iso c标準裡規定了可變參數宏,和函數文法類似,比如

#define debug(format, ...) fprintf (stderr, format, __va_args__)

  裡面的“…”就表示可變參數,調用時,它們就會替代宏體裡的__va_args__。gcc總是會顯得特立獨行一些,它支援更複雜的形式,可以給可變參數取個名字,比如

#define debug(format, args...) fprintf (stderr, format, args)

  有了名字總是會容易交流一些。是不是與pr_info比較接近了?除了‘##’,它主要是針對空參數的情況。既然說是可變參數,那傳遞空參數也總是可以的,空即是多,多即是空,股市裡的哲理這裡同樣也是适合的。如果沒有‘##’,傳遞空參數的時候,比如

debug ("a message");

  展開後,裡面的字元串後面會多個多餘的逗号。這個逗号你應該不會喜歡,而‘##’則會使預處理器去掉這個多餘的逗号。

  關于usb_init函數,上面的三個問題之外,餘下的代碼分别完成usb各部分的初始化,接下來就需要圍繞它們分别進行深入分析。因為這裡隻是示範如何入手分析,展示的隻是一種态度,是以具體的深入分析就免了吧。

本文出自seven的測試人生公衆号最新内容請見作者的github頁:http://qaseven.github.io/

繼續閱讀