而这也是面试老生常谈的问题:
经常让你说下你做的驱动,然后你说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中的函数指针,等等。而在每个子区段中的函数指针的顺序是和链接顺序相关的,是不确定的。
未完待续……