咱們都知道,linux對驅動程序提供靜態編譯進內核和動態加載兩種方式,當咱們試圖將一個驅動程序編譯進內核時,開發者一般提供一個xxx_init()函數接口以啓動這個驅動程序同時提供某些服務。html
那麼,根據常識來講,這個xxx_init()函數確定是要在系統啓動的某個時候被調用,才能啓動這個驅動程序。linux
最簡單直觀地作法就是:開發者試圖添加一個驅動程序時,在內核啓動init程序的某個地方直接添加調用本身驅動程序的xxx_init()函數,在內核啓動時天然會調用到這個程序。數組
可是,回頭一想,這種作法在單人開發的小系統中或許能夠,可是在linux中,若是驅動程序是這麼個添加法,那就是一場災難,這個道理我想不用我多說。函數
不難想到另外一種方式,就是集中提供一個地方,若是你要添加你的驅動程序,你就將你的初始化函數在這個地方進行添加,在內核啓動的時候統一掃描這個地方,再執行這一部分的全部被添加的驅動程序。post
那到底怎麼添加呢?直接在C文件中做一個列表,在裏面添加初始化函數?我想隨着驅動程序數量的增長,這個列表會讓人頭昏眼花。優化
固然,對於linus大神而言,這些都不是事,linux的作法是:ui
底層實現上,在內核鏡像文件中,自定義一個段,這個段裏面專門用來存放這些初始化函數的地址,內核啓動時,只須要在這個段地址處取出函數指針,一個個執行便可。操作系統
對上層而言,linux內核提供xxx_init(init_func)宏定義接口,驅動開發者只須要將驅動程序的init_func使用xxx_init()來修飾,這個函數就被自動添加到了上述的段中,開發者徹底不須要關心實現細節。
對於各類各樣的驅動而言,可能存在必定的依賴關係,須要遵循前後順序來進行初始化,考慮到這個,linux也對這一部分作了分級處理。debug
在平臺對應的init.h文件中,能夠找到xxx_initcall的定義:指針
/*Only for built-in code, not modules.*/ #define early_initcall(fn) __define_initcall(fn, early) #define pure_initcall(fn) __define_initcall(fn, 0) #define core_initcall(fn) __define_initcall(fn, 1) #define core_initcall_sync(fn) __define_initcall(fn, 1s) #define postcore_initcall(fn) __define_initcall(fn, 2) #define postcore_initcall_sync(fn) __define_initcall(fn, 2s) #define arch_initcall(fn) __define_initcall(fn, 3) #define arch_initcall_sync(fn) __define_initcall(fn, 3s) #define subsys_initcall(fn) __define_initcall(fn, 4) #define subsys_initcall_sync(fn) __define_initcall(fn, 4s) #define fs_initcall(fn) __define_initcall(fn, 5) #define fs_initcall_sync(fn) __define_initcall(fn, 5s) #define rootfs_initcall(fn) __define_initcall(fn, rootfs) #define device_initcall(fn) __define_initcall(fn, 6) #define device_initcall_sync(fn) __define_initcall(fn, 6s) #define late_initcall(fn) __define_initcall(fn, 7) #define late_initcall_sync(fn) __define_initcall(fn, 7s)
xxx_init_call(fn)的原型實際上是__define_initcall(fn, n),n是一個數字或者是數字+s,這個數字表明這個fn執行的優先級,數字越小,優先級越高,帶s的fn優先級低於不帶s的fn優先級。
繼續跟蹤代碼,看看__define_initcall(fn,n):
#define __define_initcall(fn, id) \ static initcall_t __initcall_##fn##id __used \ __attribute__((__section__(".initcall" #id ".init"))) = fn;
值得注意的是,__attribute__()是gnu C中的擴展語法,它能夠用來實現不少靈活的定義行爲,這裏不細究。
__attribute__((__section__(".initcall" #id ".init")))表示編譯時將目標符號放置在括號指定的段中。
而#在宏定義中的做用是將目標字符串化,##在宏定義中的做用是符號鏈接,將多個符號鏈接成一個符號,並不將其字符串化。
__used是一個宏定義,
#define __used __attribute__((__used__))
使用前提是在編譯器編譯過程當中,若是定義的符號沒有被引用,編譯器就會對其進行優化,不保留這個符號,而__attribute__((__used__))的做用是告訴編譯器這個靜態符號在編譯的時候即便沒有使用到也要保留這個符號。
爲了更方便地理解,咱們拿舉個例子來講明,開發者聲明瞭這樣一個函數:pure_initcall(test_init);
因此pure_initcall(test_init)的解讀就是:
首先宏展開成:__define_initcall(test_init, 0) 而後接着展開:static initcall_t __initcall_test_init0 = test_init;這就是一個簡單的變量定義。 同時聲明__initcall_test_init0這個變量即便沒被引用也保留符號,且將其放置在內核鏡像的.initcall0.init段處。
須要注意的是,根據官方註釋能夠看到early_initcall(fn)只針對內置的核心代碼,不能描述模塊。
既然咱們知道了xxx_initcall是怎麼定義並且目標函數的放置位置,那麼使用xxx_initcall()修飾的函數是怎麼被調用的呢?
咱們就從內核C函數起始部分也就是start_kernel開始往下挖,這裏的調用順序爲:
start_kernel -> rest_init(); -> kernel_thread(kernel_init, NULL, CLONE_FS); -> kernel_init() -> kernel_init_freeable(); -> do_basic_setup(); -> do_initcalls();
這個do_initcalls()就是咱們須要尋找的函數了,在這個函數中執行全部使用xxx_initcall()聲明的函數,接下來咱們再來看看它是怎麼執行的:
static initcall_t *initcall_levels[] __initdata = { __initcall0_start, __initcall1_start, __initcall2_start, __initcall3_start, __initcall4_start, __initcall5_start, __initcall6_start, __initcall7_start, __initcall_end, }; int __init_or_module do_one_initcall(initcall_t fn) { ... if (initcall_debug) ret = do_one_initcall_debug(fn); else ret = fn(); ... return ret; } static void __init do_initcall_level(int level) { initcall_t *fn; ... for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++) do_one_initcall(*fn); } static void __init do_initcalls(void) { int level; for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) do_initcall_level(level); }
在上述代碼中,定義了一個靜態的initcall_levels數組,這是一個指針數組,數組的每一個元素都是一個指針.
do_initcalls()循環調用do_initcall_level(level),level就是initcall的優先級數字,由for循環的終止條件ARRAY_SIZE(initcall_levels) - 1可知,總共會調用do_initcall_level(0)~do_initcall_level(7),一共七次。
而do_initcall_level(level)中則會遍歷initcall_levels[level]中的每一個函數指針,initcall_levels[level]其實是對應的__initcall##level##_start指針變量,而後依次取出__initcall##level##_start指向地址存儲的每一個函數指針,並調用do_one_initcall(*fn),實際上就是執行當前函數。
能夠猜到的是,這個__initcall##level##start所存儲的函數指針就是開發者用xxx_initcall()宏添加的函數,對應".initcall##level##.init"段。
do_one_initcall(*fn)的執行:判斷initcall_debug的值,若是爲真,則調用do_one_initcall_debug(fn);若是爲假,則直接調用fn。事實上,調用do_one_initcall_debug(fn)只是在調用fn的基礎上添加一些額外的打印信息,能夠直接當作是調用fn。
那麼,在initcall源碼部分有提到,在開發者添加xxx_initcall(fn)時,事實上是將fn放置到了".initcall##level##.init"的段中,可是在do_initcall()的源碼部分,倒是從initcall_levelslevel取出,initcall_levels[level]是怎麼關聯到".initcall##level##.init"段的呢?
答案在vmlinux.lds.h中:
#define INIT_CALLS_LEVEL(level) \ VMLINUX_SYMBOL(__initcall##level##_start) = .; \ KEEP(*(.initcall##level##.init)) \ KEEP(*(.initcall##level##s.init)) \ #define INIT_CALLS \ VMLINUX_SYMBOL(__initcall_start) = .; \ KEEP(*(.initcallearly.init)) \ INIT_CALLS_LEVEL(0) \ INIT_CALLS_LEVEL(1) \ INIT_CALLS_LEVEL(2) \ INIT_CALLS_LEVEL(3) \ INIT_CALLS_LEVEL(4) \ INIT_CALLS_LEVEL(5) \ INIT_CALLS_LEVEL(rootfs) \ INIT_CALLS_LEVEL(6) \ INIT_CALLS_LEVEL(7) \ VMLINUX_SYMBOL(__initcall_end) = .;
在這裏首先定義了__initcall_start,將其關聯到".initcallearly.init"段。
而後對每一個level定義了INIT_CALLS_LEVEL(level),將INIT_CALLS_LEVEL(level)展開以後的結果是定義__initcall##level##_start,並將
__initcall##level##_start關聯到".initcall##level##.init"段和".initcall##level##s.init"段。
到這裏,__initcall##level##_start和".initcall##level##.init"段的對應就比較清晰了,因此,從initcall_levels[level]部分一個個取出函數指針並執行函數就是執行xxx_init_call()定義的函數。
便於理解,咱們須要一個示例來梳理整個流程,假設我是一個驅動開發者,開發一個名爲beagle的驅動,在系統啓動時須要調用beagle_init()函數來啓動啓動服務。
我須要先將其添加到系統中:
core_initcall(beagle_init)
core_initcall(beagle_init)宏展開爲__define_initcall(beagle_init, 1),因此beagle_init()這個函數被放置在".initcall1.init"段處。
在內核啓動時,系統會調用到do_initcall()函數。 根據指針數組initcall_levels[1]找到__initcall1_start指針,在vmlinux.lds.h能夠查到:__initcall1_start對應".initcall1.init"段的起始地址,依次取出段中的每一個函數指針,並執行函數。
添加的服務就實現了啓動。
可能有些C語言基礎不太好的朋友不太理解do_initcall_level()函數中依次取出地址並執行的函數執行邏輯:
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++) do_one_initcall(*fn);
fn爲函數指針,fn++至關於函數指針+1,至關於:內存地址+sizeof(fn),sizeof(fn)根據平臺不一樣而不一樣,通常來講,32位機上是4字節,64位機則是8字節(關於指針在操做系統中的大小能夠參考另外一篇博客:不一樣平臺下指針大小 )。
而initcall_levels[level]指向當前".initcall##level##s.init"段,initcall_levels[level+1]指向".initcall##(level+1)##s.init"段,兩個段之間的內存就是存放全部添加的函數指針。
也就是從".initcall##level##s.init"段開始,每次取一個函數出來執行,並累加指針,直到取完。
好了,關於linux中initcall系統的討論就到此爲止啦,若是朋友們對於這個有什麼疑問或者發現有文章中有什麼錯誤,歡迎留言
原創博客,轉載請註明出處!
祝各位早日實現項目叢中過,bug不沾身.