Linux核心驅動程式初始化順序的調整 ,當在做一個驅動的時候要用到另一個驅動提供的API時,此時核心初始化時便碰到了一個依賴問題。
而這也是面試老生常談的問題:
經常讓你說下你做的驅動,然後你說XXX驅動後,很可能問下驅動細節後,會有另外一個問題: 1.你寫了一個XXX驅動,那麼它是怎麼被核心執行的呢? 2.我有a,b,c三個裝置的驅動,怎麼讓他們按照b,a,c的順序載入 |
那麼首先我們就得搞清楚驅動程式是如何被核心初始化的,這個問題很明顯就得追溯到核心啟動流程上了。簡單的講Linux的啟動過程可以分為兩個部分:架構/開發板相關的引導過程、後續的通用啟動過程。在這裡我們不多談核心啟動,為了使問題能夠解釋清楚,我們直接跳到第二階段init/main.c的start_kernel()函數(核心啟動流程的第一個C函數)說起。
而驅動程式的初始化正是在這個階段的rest_init()函數中完成的,rest_init()函數主要目標有:載入驅動程式,掛載根檔案系統,執行使用者空間第一個進程init進程(pid=1)。
讓我們看看rest_init()主要流程:
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
static void noinline __init_refok rest_init(void) __releases(kernel_lock)
{
int pid;
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
numa_default_policy();
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
kthreadd_task = find_task_by_pid(pid);
unlock_kernel();
/*
* The boot idle thread must execute schedule()
* at least one to get things moving:
*/
preempt_enable_no_resched();
schedule();
preempt_disable();
/* Call into cpu_idle with preempt disabled */
cpu_idle();
}
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
從上面看出,rest_init()函數建立兩個核心線程:1)kernel_init;2)kthreadd。
kernel_init線程主要就用來載入驅動程式、掛載根檔案系統、由核心線程蛻變成第一個使用者進程init(pid=1)
kthreadd線程將是所有其它核心線程的父線程(ps -aux 命令查看,所有由[]括起的那些)
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
static int __init kernel_init(void * unused)
{
lock_kernel();
。。。。。。
do_basic_setup(); //載入驅動程式工作的地方
/*
* check if there is an early userspace init. If yes, let it do all
* the work
*/
if (!ramdisk_execute_command)
ramdisk_execute_command = "/init";
if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace(); //掛載根檔案系統
}
init_post(); //由核心線程蛻變成第一個使用者進程init
return 0;
}
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
讓我們來看看do_basic_setup()函數是如何載入驅動的:
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
static void __init do_basic_setup(void)
{
/* drivers will send hotplug events */
init_workqueues(); //初始化工作隊列
usermodehelper_init(); //使用者態的khelper守護進程
driver_init(); //裝置模型中一些基礎結構體的初始化和裝置的註冊
init_irq_proc(); //irq程式的初始化
do_initcalls(); //(*call)()* 具體的initcall函數調用
}
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
現在開始我們有點眉目了。等不及了,進來看看吧!
----------------------------------------------------------------------------------------------------------------------------------------------------------------
static void __init do_initcalls(void)
{
initcall_t *call;
int count = preempt_count();
for (call = __initcall_start; call <
__initcall_end; call++) {
ktime_t t0, t1, delta;
char *msg = NULL;
char msgbuf[40];
int result;
。。。。。。
result = (*call)();
。。。。。。
}
/* Make sure there is no pending stuff from the initcall sequence */
flush_scheduled_work();
}
-----------------------------------------------------------------------------------------------------------------------------------------------------------
這個__initcall_start是在檔案 arch/arm/kernel/vmlinux.lds
這個檔案是核心ld的時候使用的.其中定義了各個sectioin,看看就明白了。
在這個檔案中有個.initcall.init, 代碼如下:
----------------------------------------------------------------------------------------------------------------------------------------------------------
SECTIONS
{
.init : { /* Init code and data */
*(.init.text)
_einittext = .;
__proc_info_begin = .;
*(.proc.info.init)
__proc_info_end = .;
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
__tagtable_begin = .;
*(.taglist.init)
__tagtable_end = .;
. = ALIGN(16);
__setup_start = .;
*(.init.setup)
__setup_end = .;
__early_begin = .;
*(.early_param.init)
__early_end = .;
__initcall_start = .;
*(.initcall1.init)
*(.initcall2.init)
*(.initcall3.init)
*(.initcall4.init)
*(.initcall5.init)
*(.initcall6.init)
*(.initcall7.init)
__initcall_end = .;
__con_initcall_start = .;
*(.con_initcall.init)
__con_initcall_end = .;
。。。。。。
}
。。。。。。
}
--------------------------------------------------------------------------------------------------------------------------------------------------------
這裡有7個初始化的優先順序,核心會按照這個優先順序的順序依次載入.
這些優先順序是在檔案include/linux/init.h 中定義的. 你注意一下宏 __define_initcall的實現就明白了.
相關代碼如下:
#define __define_initcall(level,fn) \
static initcall_t __initcall_##fn __attribute_used__ \
__attribute__((__section__(".initcall" level ".init"))) = fn
#define core_initcall(fn) __define_initcall("1",fn)
#define postcore_initcall(fn) __define_initcall("2",fn)
#define arch_initcall(fn) __define_initcall("3",fn)
#define subsys_initcall(fn) __define_initcall("4",fn)
#define fs_initcall(fn) __define_initcall("5",fn)
#define device_initcall(fn) __define_initcall("6",fn)
#define late_initcall(fn) __define_initcall("7",fn)
---------------------------------------------------------------------------------------------------------------------------------------------------------
而我們經常寫的裝置驅動程式中就常用的module_init進行修飾,其實就是對應了優先順序 6
#define __initcall(fn) device_initcall(fn)
#define module_init(x) __initcall(x);
正是這樣系統中的驅動程式會在核心啟動過程中,被連結在一個段中統一被載入。
----------------------------------------------------------------------------------------------------------------------------------------------------------
其實所以的__init函數在區段.initcall.init中儲存了一份函數指標,在初始化時核心會通過這些函數指標調用這些__init函數指標,並在整個初始化完成後,釋放整個init區段(包括.init.text,.initcall.init等)。
注意,這些函數在核心初始化過程中的調用順序只和這裡的函數指標的順序有關,各個子區段之間的順序是確定的,即先調用. initcall1.init中的函數指標,再調用.initcall2.init中的函數指標,等等。而在每個子區段中的函數指標的順序是和連結順序相關的,是不確定的。
未完待續……