《Linux內核修煉之道》——分析內核源碼如何入手?(下)
下面的分析,米盧教練說了,內容不重要,重要的是態度。就像韓局長對待日記的態度那樣,嚴謹而細致。
只要你使用這樣的態度開始分析內核,那么無論你選擇內核的哪個部分作為切入點,比如USB,比如進程管理,在花費相對不算很多的時間之后,你就會發現你對內核的理解會上升到另外一個高度,一個抱著情景分析,抱著0.1內核完全注釋,抱著各種各樣的內核書籍翻來覆去的看很多遍又忘很多遍都無法達到的高度。請相信我!
讓我們在Linux社區里發出號召:學習內核源碼,從學習韓局長開始!
態度決定一切:從初始化函數開始
任小強們說房價高漲從現在開始,股評家們說牛市從5000點開始。他們的開始需要我們的錢袋,我們的開始只需要一臺電腦,最好再有一杯茶,伴著幾支小曲兒,不盯著錢總是會比較愜意的。生容易,活容易,生活不容易,因為總要盯著錢。
有了地圖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")' 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 |
通常編譯器將函數放在.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 */ |
從中可知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各部分的初始化,接下來就需要圍繞它們分別進行深入分析。因為這里只是演示如何入手分析,展示的只是一種態度,所以具體的深入分析就免了吧。
posted on 2011-11-10 11:10 順其自然EVO 閱讀(322) 評論(0) 編輯 收藏 所屬分類: 測試學習專欄