Linux设备驱动probe过程(一)
Linux设备驱动probe系列文章:
本章介绍以built-in的方式进行打包的标准设备初始化,在系统上使用固定硬件连接方式,启动后kernel初始化阶段进行probe。
非热插拔类型设备驱动probe过程
本周比较忙,只整理了设备驱动的一些流程,记录从kernel加载到driver probe的完整过程。先贴下流程图,然后各章节描述下比较重要和晦涩的几个步骤,用数组图标①->⑦标记。
1. primary_entry
函数位于arch/arm64/kernel/head.S,汇编实现。在bl31跳转到kernel __HEAD后被调用,位于非常早期的启动阶段:
/*
* Kernel startup entry point.
...
*/
__HEAD
_head:
/*
* DO NOT MODIFY. Image header expected by Linux boot-loaders.
*/
#ifdef CONFIG_EFI
/*
* This add instruction has no meaningful effect except that
* its opcode forms the magic "MZ" signature required by UEFI.
*/
add x13, x18, #0x16
b primary_entry
#else
b primary_entry // branch to kernel start, magic
.long 0 // reserved
#endif
...
primary_entry函数主要2个作用:初始化cpu,调用__primary_switch进一步初始化kernel。
SYM_CODE_START(primary_entry)
bl preserve_boot_args
bl init_kernel_el // w0=cpu_boot_mode
adrp x23, __PHYS_OFFSET
and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0
bl set_cpu_boot_mode_flag
bl __create_page_tables
/*
* The following calls CPU setup code, see arch/arm64/mm/proc.S for
* details.
* On return, the CPU will be ready for the MMU to be turned on and
* the TCR will have been set.
*/
bl __cpu_setup // initialise processor
b __primary_switch
SYM_CODE_END(primary_entry)
1.1 __primary_switch
__primary_switch函数也为汇编,进一步调用__primary_switched。这里有个相对重要一些的步骤,将init_task(全局变量,位于init/init_task.c)保存到sp_el0寄存器中。后续访问current->xxx()初始化kernel时会用到,用于进程创建。
/*
* We don't use read_sysreg() as we want the compiler to cache the value where
* possible.
*/
static __always_inline struct task_struct *get_current(void)
{
unsigned long sp_el0;
asm ("mrs %0, sp_el0" : "=r" (sp_el0));
return (struct task_struct *)sp_el0;
}
#define current get_current()
在一系列寄存器和地址段配置完成后,调用start_kernel,转为C语言函数,进一步初始化。
SYM_FUNC_START_LOCAL(__primary_switched)
adrp x4, init_thread_union
add sp, x4, #THREAD_SIZE
adr_l x5, init_task
msr sp_el0, x5 // Save thread_info,将init_task保存到sp_el0,后续初始化时会调用current->xxx()获取task信息
...
// Clear BSS
adr_l x0, __bss_start
mov x1, xzr
adr_l x2, __bss_stop
...
add sp, sp, #16
mov x29, #0
mov x30, #0
b start_kernel // 进入到start_kernel
SYM_FUNC_END(__primary_switched)
1.2 rest_init
start_kernel执行了大部分初始化功能,详细又繁琐,这次主要聚焦设备驱动的加载过程,就不在此展开了。
由start_kernel逐级调用后,来到reset_init函数。里面最重要的一步就是通过kernel_thread来创建kernel_init线程。创建线程过程中有几个步骤涉及到上下文的配置,集中在copy_thread函数中,略微有点晦涩,简单展开聊一下,有助于对于新线程创建和上下文切换的理解。
- 首先在将kernel_init函数配置到args.stack,作为传参送入kernel_clone。
noinline void __ref rest_init(void)
{
...
/*
* We need to spawn init first so that it obtains pid 1, however
* the init task will end up wanting to create kthreads, which, if
* we schedule it before we create kthreadd, will OOPS.
*/
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
...
}
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
struct kernel_clone_args args = {
.flags = ((lower_32_bits(flags) | CLONE_VM |
CLONE_UNTRACED) & ~CSIGNAL),
.exit_signal = (lower_32_bits(flags) & CSIGNAL),
.stack = (unsigned long)fn,
.stack_size = (unsigned long)arg,
};
// 如此,kernel_init被放到了stack上
return kernel_clone(&args);
}
将记录kernel_init的args.stack传递到copy_thread函数参数stack_start中,并记录到arm寄存器x19中,如此完成了kernel_init函数到x19寄存器的传递。copy_thread函数还有一个步骤比较重要,将ret_from_fork赋值到task pc位置,作为新创建线程的入口。
/*
* This creates a new process as a copy of the old one,
* but does not actually start it yet.
*
* It copies the registers, and all the appropriate
* parts of the process environment (as per the clone
* flags). The actual kick-off is left to the caller.
*/
static __latent_entropy struct task_struct *copy_process(
struct pid *pid,
int trace,
int node,
struct kernel_clone_args *args)
{
...
retval = copy_thread(clone_flags, args->stack, args->stack_size, p, args->tls);
...
}
int copy_thread(unsigned long clone_flags, unsigned long stack_start,
unsigned long stk_sz, struct task_struct *p, unsigned long tls)
{
...
p->thread.cpu_context.x19 = stack_start; // 此处stack_start等于kernel_init
p->thread.cpu_context.x20 = stk_sz;
...
p->thread.cpu_context.pc = (unsigned long)ret_from_fork; // ref_from_fork函数赋值给pc
p->thread.cpu_context.sp = (unsigned long)childregs;
}
至此步骤①的节点描述完毕,如此划分主要是突出主角kernel_init线程如何在kernel加载后被创建。接下来继续讲讲primary_entry任务创建创建完kernel_init后,如何将task放到任务列表,使得schedule可以调度到。
2. __enqueue_entity
kernel_clone函数在创建完kernel_init线程上下文后,需要将他放到调度列表中,才可以被调度到,整个流程是这样的:kernel_clone -> wake_up_new_task -> active_task -> enqueue_task -> enqueue_task_fair -> enqueue_entity -> __enqueue_entity。
至此primary_entry任务对于kernel_init的有关操作就完成了,primary_entry调度本身也会结束。
3. schedule
接下来进入新进程的生命周期,schedule被如何被调度其实有很多时机,中断啊、高优先级任务抢占、主动结束什么的。初始化阶段时,primary_entry所属进程结束,就会触发schedul调度。经过一系列函数调用,最终进入到pc指针指向的ret_from_fork函数:
schedule -> __schedule -> context_switch -> switch_to -> __switch_to -> cpu_switch_to -> msr sp_el0, x1 -> pc -> ret_from_fork
其中"msr sp_el0, x1"属于隐式的调用task pc指针。
4. ret_from_fork
ret_from_fork为汇编实现,主要作用为:清理调度环境,调用x19寄存器保存的函数指针,当然就是kernel_init了,1.2里面讲过。
SYM_CODE_START(ret_from_fork)
bl schedule_tail
cbz x19, 1f // not a kernel thread
mov x0, x20
blr x19 // 1.2里面讲过,这个流程:kernel_init->args.stack->start_stack->x19
1: get_current_task tsk
b ret_to_user
SYM_CODE_END(ret_from_fork)
NOKPROBE(ret_from_fork)
接下来终于进入kernel_init函数执行了,回到设备驱动probe正题了。
5. kernel_init
kernel_init上来就会调用kernel_init_freeable函数初始化各种__init前缀标注的驱动和文件系统相关的初始化函数。多提一嘴,kernel_init结束后会初始化用户空间init环境,然后退出并跳转到用户空间执行初始化。
我们非热插拔的设备驱动是通过platform_driver_probe注册的,会需要声明一个初始化函数:
static int __init xxx_module_init( void )
{
int32_t rc = 0;
LOG( LOG_INFO, "XXX %s", __FUNCTION__ );
rc = platform_driver_probe( &xxx_platform_driver,
xxx_platform_probe );
return rc;
}
各模块的初始化函数由do_one_initcall调用,和kernel_init调用关系为:kernel_init -> kernel_init_freeable -> do_basic_setup -> do_initcall_level -> do_one_initcall -> fn() -> xxx_module_init
6. xxx_module_init
如此进入自定义初始化函数中,进一步调用到probe过程:
xxx_module_init -> platform_driver_probe -> __platform_driver_probe -> __platform_driver_register -> driver_register -> bus_add_driver -> bus_add_driver -> driver_attach -> __driver_attach -> device_driver_attach -> driver_probe_device -> really_probe -> drv->probe(dev) - > platform_drv_probe
如此,调用到了platform_drv_probe函数,开始了platform初始化。
7. xxx_platform_probe
最终到了自定义driver的probe,这个就比较简单了:platform_drv_probe -> “drv->probe(dev)” -> xxx_platform_probe。
之后自定义函数的probe了,一般包含:
- dts parse
- interrupt register
- apb register addr remap
- clock & reset init
- iommu init
- sysfs init
- work thread(state machine) create
- …
本章结束,整体有些粗糙,一些没有涉及的初始化步骤以拓展阅读再补充进来,也可能再起一篇文章。