下面的分析,米盧教練說(shuō)了,內(nèi)容不重要,重要的是態(tài)度。就像韓局長(zhǎng)對(duì)待日記的態(tài)度那樣,嚴(yán)謹(jǐn)而細(xì)致。
只要你使用這樣的態(tài)度開(kāi)始分析內(nèi)核,那么無(wú)論你選擇內(nèi)核的哪個(gè)部分作為切入點(diǎn),比如USB,比如進(jìn)程管理,在花費(fèi)相對(duì)不算很多的時(shí)間之后,你就會(huì)發(fā)現(xiàn)你對(duì)內(nèi)核的理解會(huì)上升到另外一個(gè)高度,一個(gè)抱著情景分析,抱著0.1內(nèi)核完全注釋,抱著各種各樣的內(nèi)核書(shū)籍翻來(lái)覆去的看很多遍又忘很多遍都無(wú)法達(dá)到的高度。請(qǐng)相信我!
讓我們?cè)?a href="" target="_self" style="word-break: break-all; color: #333333; text-decoration: none; line-height: normal !important; ">Linux社區(qū)里發(fā)出號(hào)召:學(xué)習(xí)內(nèi)核源碼,從學(xué)習(xí)韓局長(zhǎng)開(kāi)始!
任小強(qiáng)們說(shuō)房?jī)r(jià)高漲從現(xiàn)在開(kāi)始,股評(píng)家們說(shuō)牛市從5000點(diǎn)開(kāi)始。他們的開(kāi)始需要我們的錢袋,我們的開(kāi)始只需要一臺(tái)電腦,最好再有一杯茶,伴著幾支小曲兒,不盯著錢總是會(huì)比較愜意的。生容易,活容易,生活不容易,因?yàn)榭傄⒅X。
有了地圖Kconfig和Makefile,我們可以在龐大復(fù)雜的內(nèi)核代碼中定位以及縮小了目標(biāo)代碼的范圍。那么現(xiàn)在,為了研究?jī)?nèi)核對(duì)USB子系統(tǒng)的實(shí)現(xiàn),我們還需要在目標(biāo)代碼中找到一個(gè)突破口,這個(gè)突破口就是USB子系統(tǒng)的初始化代碼。
針對(duì)某個(gè)子系統(tǒng)或某個(gè)驅(qū)動(dòng),內(nèi)核使用subsys_initcall或module_init宏指定初始化函數(shù)。在drivers/usb/core/usb.c文件中,我們可以發(fā)現(xiàn)下面的代碼。
我們看到一個(gè)subsys_initcall,它也是一個(gè)宏,我們可以把它理解為module_init,只不過(guò)因?yàn)檫@部分代碼比較核心,開(kāi)發(fā)者們把它看作一個(gè)子系統(tǒng),而不僅僅是一個(gè)模塊。這也很好理解,usbcore這個(gè)模塊它代表的不是某一個(gè)設(shè)備,而是所有USB設(shè)備賴以生存的模塊,Linux中,像這樣一個(gè)類別的設(shè)備驅(qū)動(dòng)被歸結(jié)為一個(gè)子系統(tǒng)。比如PCI子系統(tǒng),比如SCSI子系統(tǒng),基本上,drivers/目錄下面第一層的每個(gè)目錄都算一個(gè)子系統(tǒng),因?yàn)樗鼈兇砹艘活愒O(shè)備。
subsys_initcall(usb_init)的意思就是告訴我們usb_init是USB子系統(tǒng)真正的初始化函數(shù),而usb_exit()將是整個(gè)USB子系統(tǒng)的結(jié)束時(shí)的清理函數(shù)。于是為了研究USB子系統(tǒng)在內(nèi)核中的實(shí)現(xiàn),我們需要從usb_init函數(shù)開(kāi)始看起。
?。?)__init標(biāo)記。
關(guān)于usb_init,第一個(gè)問(wèn)題是,第865行的__init標(biāo)記具有什么意義?
寫(xiě)過(guò)驅(qū)動(dòng)的應(yīng)該不會(huì)陌生,它對(duì)內(nèi)核來(lái)說(shuō)就是一種暗示,表明這個(gè)函數(shù)僅在初始化期間使用,在模塊被裝載之后,它占用的資源就會(huì)釋放掉用作它處。它的暗示你懂,可你的暗示,她卻不懂或者懂裝不懂,多么讓人感傷。它在自己短暫的一生中一直從事繁重的工作,吃的是草吐出的是牛奶,留下的是整個(gè)USB子系統(tǒng)的繁榮。
受這種精神所感染,我覺(jué)得有必要為它說(shuō)的更多些。__init的定義在include/linux/init.h文件里
43 #define __init __attribute__ ((__section__ (".init.text"))) |
好像這里引出了更多的疑問(wèn),__attribute__是什么?Linux內(nèi)核代碼使用了大量的GNU C擴(kuò)展,以至于GNU C成為能夠編譯內(nèi)核的唯一編譯器,GNU C的這些擴(kuò)展對(duì)代碼優(yōu)化、目標(biāo)代碼布局、安全檢查等方面也提供了很強(qiáng)的支持。而__attribute__就是這些擴(kuò)展中的一個(gè),它主要被用來(lái)聲明一些特殊的屬性,這些屬性主要被用來(lái)指示編譯器進(jìn)行特定方面的優(yōu)化和更仔細(xì)的代碼檢查。GNU C支持十幾個(gè)屬性,section是其中的一個(gè),我們查看GCC的手冊(cè)可以看到下面的描述
‘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. |
通常編譯器將函數(shù)放在.text節(jié),變量放在.data或.bss節(jié),使用section屬性,可以讓編譯器將函數(shù)或變量放在指定的節(jié)中。那么前面對(duì)__init的定義便表示將它修飾的代碼放在.init.text節(jié)。連接器可以把相同節(jié)的代碼或數(shù)據(jù)安排在一起,比如__init修飾的所有代碼都會(huì)被放在.init.text節(jié)里,初始化結(jié)束后就可以釋放這部分內(nèi)存。
問(wèn)題可以到此為止,也可以更深入,即內(nèi)核又是如何調(diào)用到這些__init修飾的初始化函數(shù)?要回答這個(gè)問(wèn)題,還需要回顧一下subsys_initcall宏,它也在include/linux/init.h里定義
125 #define subsys_initcall(fn) __define_initcall("4",fn,4) |
這里又出現(xiàn)了一個(gè)宏__define_initcall,它用于將指定的函數(shù)指針fn放到initcall.init節(jié)里 而對(duì)于具體的subsys_initcall宏,則是把fn放到.initcall.init的子節(jié).initcall4.init里。要弄清楚.initcall.init、.init.text和.initcall4.init這樣的東東,我們還需要了解一點(diǎn)內(nèi)核可執(zhí)行文件相關(guān)的概念。
內(nèi)核可執(zhí)行文件由許多鏈接在一起的對(duì)象文件組成。對(duì)象文件有許多節(jié),如文本、數(shù)據(jù)、init數(shù)據(jù)、bass等等。這些對(duì)象文件都是由一個(gè)稱為鏈接器腳本的文件鏈接并裝入的。這個(gè)鏈接器腳本的功能是將輸入對(duì)象文件的各節(jié)映射到輸出文件中;換句話說(shuō),它將所有輸入對(duì)象文件都鏈接到單一的可執(zhí)行文件中,將該可執(zhí)行文件的各節(jié)裝入到指定地址處。 vmlinux.lds是存在于arch/<target>/ 目錄中的內(nèi)核鏈接器腳本,它負(fù)責(zé)鏈接內(nèi)核的各個(gè)節(jié)并將它們裝入內(nèi)存中特定偏移量處。
我可以負(fù)責(zé)任的告訴你,要看懂vmlinux.lds這個(gè)文件是需要一番功夫的,不過(guò)大家都是聰明人,聰明人做聰明事,所以你需要做的只是搜索initcall.init,然后便會(huì)看到似曾相識(shí)的內(nèi)容
__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節(jié)的開(kāi)始,__initcall_end指向它的結(jié)尾。而.initcall.init節(jié)又被分為了7個(gè)子節(jié),分別是
.initcall1.init .initcall2.init .initcall3.init .initcall4.init .initcall5.init .initcall6.init .initcall7.init |
我們的subsys_initcall宏便是將指定的函數(shù)指針?lè)旁诹?initcall4.init子節(jié)。其它的比如core_initcall將函數(shù)指針?lè)旁?initcall1.init子節(jié),device_initcall將函數(shù)指針?lè)旁诹?initcall6.init子節(jié)等等,都可以從include/linux/init.h文件找到它們的定義。各個(gè)字節(jié)的順序是確定的,即先調(diào)用.initcall1.init中的函數(shù)指針再調(diào)用.initcall2.init中的函數(shù)指針,等等。__init修飾的初始化函數(shù)在內(nèi)核初始化過(guò)程中調(diào)用的順序和.initcall.init節(jié)里函數(shù)指針的順序有關(guān),不同的初始化函數(shù)被放在不同的子節(jié)中,因此也就決定了它們的調(diào)用順序。
至于實(shí)際執(zhí)行函數(shù)調(diào)用的地方,就在/init/main.c文件里,內(nèi)核的初始化么,不在那里還能在哪里,里面的do_initcalls函數(shù)會(huì)直接用到這里的__initcall_start、__initcall_end來(lái)進(jìn)行判斷。
(2)模塊參數(shù)。
關(guān)于usb_init函數(shù),第二個(gè)問(wèn)題是,第868行的nousb表示什么?
知道C語(yǔ)言的人都會(huì)知道nousb是一個(gè)標(biāo)志,只是不同的標(biāo)志有不一樣的精彩,這里的nousb是用來(lái)讓我們?cè)趩?dòng)內(nèi)核的時(shí)候通過(guò)內(nèi)核參數(shù)去掉USB子系統(tǒng)的,Linux社會(huì)是一個(gè)很人性化的世界,它不會(huì)去逼迫我們接受USB,一切都只關(guān)乎我們自己的需要。不過(guò)我想我們一般來(lái)說(shuō)是不會(huì)去指定nousb的吧。如果你真的指定了nousb,那它就只會(huì)幽怨的說(shuō)一句“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是個(gè)模塊參數(shù)。關(guān)于模塊參數(shù),我們都知道可以在加載模塊的時(shí)候可以指定,但是如何在內(nèi)核啟動(dòng)的時(shí)候指定?
打開(kāi)系統(tǒng)的grub文件,然后找到kernel行,比如:
kernel /boot/vmlinuz-2.6.18-kdb root=/dev/sda1 ro splash=silent vga=0x314 |
其中的root,splash,vga等都表示內(nèi)核參數(shù)。當(dāng)某一模塊被編譯進(jìn)內(nèi)核的時(shí)候,它的模塊參數(shù)便需要在kernel行來(lái)指定,格式為“模塊名.參數(shù)=值”,比如:
modprobe usbcore autosuspend=2 |
對(duì)應(yīng)到kernel行,即為:
通過(guò)命令“modinfo -p ${modulename}”可以得知一個(gè)模塊有哪些參數(shù)可以使用。同時(shí),對(duì)于已經(jīng)加載到內(nèi)核里的模塊,它們的模塊參數(shù)會(huì)列舉在/sys/module/${modulename}/parameters/目錄下面,可以使用“echo -n ${value} > /sys/module/${modulename}/parameters/${parm}”這樣的命令去修改。
?。?)可變參數(shù)宏。
關(guān)于usb_init函數(shù),第三個(gè)問(wèn)題是,pr_info如何實(shí)現(xiàn)與使用?
pr_info只是一個(gè)打印信息的可辨參數(shù)宏,printk的變體,在include/linux/kernel.h里定義:
242 #define pr_info(fmt,arg...) / 243 printk(KERN_INFO fmt,##arg) |
99年的ISO C標(biāo)準(zhǔn)里規(guī)定了可變參數(shù)宏,和函數(shù)語(yǔ)法類似,比如
#define debug(format, ...) fprintf (stderr, format, __VA_ARGS__) |
里面的“…”就表示可變參數(shù),調(diào)用時(shí),它們就會(huì)替代宏體里的__VA_ARGS__。GCC總是會(huì)顯得特立獨(dú)行一些,它支持更復(fù)雜的形式,可以給可變參數(shù)取個(gè)名字,比如
#define debug(format, args...) fprintf (stderr, format, args) |
有了名字總是會(huì)容易交流一些。是不是與pr_info比較接近了?除了‘##’,它主要是針對(duì)空參數(shù)的情況。既然說(shuō)是可變參數(shù),那傳遞空參數(shù)也總是可以的,空即是多,多即是空,股市里的哲理這里同樣也是適合的。如果沒(méi)有‘##’,傳遞空參數(shù)的時(shí)候,比如
展開(kāi)后,里面的字符串后面會(huì)多個(gè)多余的逗號(hào)。這個(gè)逗號(hào)你應(yīng)該不會(huì)喜歡,而‘##’則會(huì)使預(yù)處理器去掉這個(gè)多余的逗號(hào)。
關(guān)于usb_init函數(shù),上面的三個(gè)問(wèn)題之外,余下的代碼分別完成usb各部分的初始化,接下來(lái)就需要圍繞它們分別進(jìn)行深入分析。因?yàn)檫@里只是演示如何入手分析,展示的只是一種態(tài)度,所以具體的深入分析就免了吧。