Linux设备驱动probe过程(一)


Linux设备驱动probe系列文章:

  1. Linux设备驱动probe过程(二)
  2. Linux设备驱动probe过程(三)
  3. 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了,一般包含:

  1. dts parse
  2. interrupt register
  3. apb register addr remap
  4. clock & reset init
  5. iommu init
  6. sysfs init
  7. work thread(state machine) create

本章结束,整体有些粗糙,一些没有涉及的初始化步骤以拓展阅读再补充进来,也可能再起一篇文章。

  • 15
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值