瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网网关、NVR 存储、工控平板、工业检测、工控盒、卡拉 OK、云终端、车载中控等行业。
动基础-进阶篇
进阶1 编译进内核的驱动系统是如何运行的?
在经过前面章节的学习后,相信大家已经对驱动有了一些自己的认识和理解,从本章开始将对一些驱动相关的进阶知识进行讲解。本章要研究的内容为编译进内核的驱动系统是如何运行的?
在驱动程序中,module_init 宏定义了驱动的入口函数,在模块加载时被内核自动调用,该宏定义在内核源码目录下的“include/linux/module.h”文件中,具体内容如下所示:
module_init的具体内容由MODULE宏定义来决定,该宏定义在内核源码的顶层Makefile中,具体为KBUILD_CFLAGS_KERNEL和KBUILD_CFLAGS_MODULE两个宏,如下所示:
由于本章节探究的是编译进内核的驱动,所以要看KBUILD_CFLAGS_KERNEL宏定义,该宏为空,那module_init 的宏定义具体内容如下所示:
注意:因为静态编译的驱动无法卸载,所以module_exit在编译进内核的驱动中并不会被执行!所以这里只是分析module_init。
然后继续向下查找__initcall的定义路径,该宏定义在内核源码目录下的“include/linux/init.h”文件中,具体内容如下所示:
#define __initcall(fn) device_initcall(fn)
接下来会发现该宏定义仍会套很多层宏定义,这些宏都在内核源码目录下的“include/linux/init.h”文件中,具体后续嵌套内容如下所示:
由于嵌套关系较为复杂,这里以module_init(helloworld)为例绘制了调用关系,具体内容如下所示:
注意:##代表强制连接,#表示对这个变量替换后,用双引号引起来。
而宏定义展开到最后的initcall_t是一个函数指针,它的原型如下所示:
typedef int (*initcall_t)(void);
所以,当使用module_init(helloworld)宏定义模块的入口函数后,会创建一个 __initcall_hello_world6函数指针变量,并将其初始化为hello_world函数,这个__initcall_hello_world6函数指针变量的目的是将模块的入口函数放置在内核的初始化调用链中,以便在系统引导期间自动执行。
在编译过程中,这个函数指针会被放置在.initcall6.init段中。这个段是内核初始化调用链的一部分,用于在系统引导期间按顺序调用所有位于该段中的函数。通过将模块的入口函数放置在.initcall6.init段中,可以确保在系统引导期间自动调用该函数,从而初始化模块并注册模块的功能。
而在内核源码中除了module_init,还有其他的宏定义接口用来完成初始化模块并注册模块的功能,他们的原型都是define_initcall,只是相应的优先级不同,而优先级的不同就导致了系统启动时驱动模块的加载先后顺序不一样,module_init的优先级是6,其他的宏定义在include/linux/init.h 文件中,具体内容如下所示:
而在include/asm-generic/vmlinux.lds.h 链接脚本(linker script)中定义初始化调用函数的布局和顺序,具体内容如下所示:
NIT_CALLS_LEVEL(level) 宏用于定义特定优先级(level)的初始化调用函数的布局。它会创建以下两个符号:
__initcall[level]_start:表示该优先级初始化调用函数段的起始位置。
__initcall[level]s_start:表示该优先级初始化调用函数段的起始位置(用于静态初始化)。
接着,INIT_CALLS宏用于定义整个初始化调用函数的布局。它按照一定的顺序将不同优先级的初始化调用函数放置在链接器脚本的相应位置。具体的步骤如下:
1.定义 __initcall_start符号,表示初始化调用函数段的起始位置。
2.使用KEEP命令保留所有.initcallearly.init段中的内容。这个段包含了一些早期的初始化调用函数,它们会在其他优先级之前被调用。
3.依次调用 INIT_CALLS_LEVEL 宏,传入不同的优先级参数,将相应优先级的初始化调用函数放置在链接器脚本中的正确位置。
4.定义 __initcall_end 符号,表示初始化调用函数段的结束位置。
链接器在链接过程中会根据这些符号的位置信息,将初始化调用函数按照优先级顺序放置在对应的段中。这样,当系统启动时,初始化调用函数将按照定义的顺序被调用,实现系统的初始化和功能注册。
展开之后的INIT_CALLS宏内容如下所示:
_initcall0_start等以_start结尾的相关变量记录了.initcall0.init等段的首地址,这些变量在 init/main.c中通过extern关键字进行引用,并将这些首地址放置在数组initcall_levels中,具体内容如下所示:
在1-10行声明了一系列__initcall0_start相关变量,在第12行定义了一个名为initcall_levels 的静态指针数组。该数组用于存储不同优先级的初始化调用函数段的起始地址。数组的元素对应不同的优先级,按照顺序存储了对应优先级的起始地址。该数组最终会在do_one_initcall函数中执行,由于调用关系较为复杂,所以这里直接绘制出了相应的调用关系图,具体内容如下所示:
首先来看do_initcalls函数,该函数定义在内核源码的init/main.c目录下,具体内容如下所示:
在第9行,循环遍历了initcall_levels数组,其中ARRAY_SIZE(initcall_levels)表示 initcall_levels数组的大小,do_initcalls函数的循环将执行7次do_initcall_level。在每次循环中,do_initcall_level函数被调用,并传递当前迭代的level值作为参数,数字越小,优先级越高,带s段的优先级要小于不带 "s" 段的优先级,然后我们继续来看do_initcall_level函数,该函数的具体内容如下所示:
在该函数中最重要的内容为24、25行的for循环,关于for循环内容的具体解释如下所示:
(1)fn 是一个指向initcall_entry_t类型的指针,用于迭代遍历当前级别的初始化调用函数数组。
(2)initcall_levels[level] 表示当前级别的初始化调用函数数组的起始地址。
(3)initcall_levels[level+1] 表示下一个级别的初始化调用函数数组的起始地址。由于数组是连续存储的,因此通过比较fn和initcall_levels[level+1]的值,可以确定循环的终止条件。
(4)do_one_initcall是一个函数,用于执行单个初始化调用函数。它接受一个函数指针作为参数,并调用该函数。
(5)initcall_from_entry是一个宏,用于从函数指针fn中获取实际的初始化调用函数。
因此,循环的作用是遍历当前级别的初始化调用函数数组,并依次将每个函数指针传递给 do_one_initcall 函数执行初始化调用。通过这个循环,可以按照预定义的顺序执行每个初始化调用函数,完成系统的初始化过程。do_one_initcall函数的具体内容如下所示:
该函数的作用是执行单个初始化调用函数并处理相关逻辑,至此一系列的调用关系就解释完成了。
最后对本章节内容进行一下简单的总结,在使用module_init(hello_world)时,hello_world()函数指针会被放置在.initcall6.init段处。内核启动时,会执行do_initcall()函数,该函数根据指针数组initcall_levels[6]找到_initcall6_start,在include/asm-generic/vmlinux.lds.h文件中可以查到_initcall6_start对应.initcall6.init段的起始地址。然后,依次取出该段中的函数指针,并执行这些函数。
至此,关于编译进内核的驱动系统是如何运行的这一问题就讲解完成了,最后布置一个课程作业,利用本章节学习到的知识来让驱动可以更快的被加载,会在下一章中对该作业进行讲解。
|