1. linux的initcall机制(针对编译进内核的驱动)
static int __init hello_init(void)
宏定义__init,用于告诉编译器相关函数或变量仅用于初始化。编译器将标__init的所有代码存在特殊的内存段中,初始化结束后就释放这段内存。
下面是其定义:
file:/include/linux/init.h
#define __init __section(.init.text) __cold __latent_entropy __noinitretpoline __nocfi
#define __initdata __section(.init.data)
#define __initconst __section(.init.rodata)
#define __exitdata __section(.exit.data)
#define __exit_call __used __section(.exitcall.exit)
其典型用法如下:
static int __init xxx_drv_init(void)
{
return pci_register_driver(&xxx_driver);
}
(1)根据上面的定义与用法,xxx_drv_init()函数将会被link到.init.text段。
1) 所有标识为__init的函数,在链接的时候,都放在.init.text这个区域中。在这个区域中,函数的摆放顺序是和链接顺序有关的,是不确定的。
2)所有的__init函数在区域.initcall.init中还保存了一份函数指针。在初始化时,内核会通过这些函数指针调用这些__init函数,并在整个初始化完成后,释放整个init区域 (包括.init.text, .initcall.init...)
注:这些函数在内核初始化过程中的调用顺序只和这里的函数指针顺序有关,和1)中所述的这些函数代码本身在.init.text区域中的顺序无关。
在2.4内核中,这些函数指针的顺序也是和链接顺序有关的,是不确定的。
在2.6内核中,.initcall.init区域又分成了7个子区域,分别是:
.initcall1.init
.initcall2.init
.initcall3.init
.initcall4.init
.initcall5.init
.initcall6.init
.initcall7.init
当需要把函数fn放到.initcall1.init区域时,只要声明core_initcall(fn);即可。
其他的各个区域的定义方法分别是:
core_initcall(fn) --> .initcall1.init
postcore_initcall(fn) --> .initcall2.init
arch_initcall(fn) --> .initcall3.init
subsys_initcall(fn) --> .initcall4.init
fs_initcall(fn) --> .initcall5.init
device_initcall(fn) --> .initcall6.init
late_initcall(fn) --> .initcall7.init
而与2.4兼容的initcall(fn)则等价于device_initcall(fn).
各个子区域之间的顺序是确定的,即先调用.initcall1.init中的函数指针,再调用.initcall2.init中的函数指针,等等。而在每个子区域中的函数指针的顺序是和链接顺序相关的,是不确定的。
在内核中,不同的init函数被放在不同的子区域中,因此也就决定了他们的调用顺序。这样也就解决了一些init函数之间必须保证一定的调用顺序问题。
之所以加入这样的宏,原因有二:
①一部分内核初始化机制依赖与它。如kernel将初始化要执行的init函数,分为7个级别,core_initcall, postcore_initcall, arch_initcall, subsys_initcall, fs_iitcall, device_initcall, late_initcall。这7个级别优先级递减,即先执行core_initcall, 最后执行late_initcall。通过使用文中提到的宏,gcc会将初始化代码按下面的结构安排:
在内核初始化时,从__initcall_start到__initcall_end之间的initcall被一次执行。
②提高系统效率
初始化代码的特点是,在系统启动时运行,且一旦运行后马上推出内存,不再占用内存。
(2)initcall的源码
追踪源码来理解上文的意思
file:/include/linux/init.h
/*
* Early initcalls run before initializing SMP.
*
* Only for built-in code, not modules.
*/
#define early_initcall(fn) __define_initcall(fn, early)
/*
* A "pure" initcall has no dependencies on anything else, and purely
* initializes variables that couldn't be statically initialized.
*
* This only exists for built-in code, not for modules.
* Keep main.c:initcall_level_names[] in sync.
*/
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
#define __initcall(fn) device_initcall(fn)
① xxx_initcall(fn)的原型其实是__define_initcall(fn, n),n是一个数字或者是数字+s,这个数字代表这个fn执行的优先级,数字越小,优先级越高,带s的fn优先级低于不带s的fn优先级。
继续跟踪代码,看看__define_initcall(fn,n):
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
#define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(#__sec ".init"))) = fn;
值得注意的是,__attribute__()是gnu C中的扩展语法,它可以用来实现很多灵活的定义行为,这里不细究。
__attribute__((__section__(#__sec ".init")))表示编译时将目标符号放置在括号指定的段中。
而#在宏定义中的作用是将目标字符串化,##在宏定义中的作用是符号连接,将多个符号连接成一个符号,并不将其字符串化。
__used是一个宏定义,
file:\include\linux\compiler_attributes.h
#define __used __attribute__((__used__))
使用前提是在编译器编译过程中,如果定义的符号没有被引用,编译器就会对其进行优化,不保留这个符号,而__attribute__((__used__))的作用是告诉编译器这个静态符号在编译的时候即使没有使用到也要保留这个符号。
为了更方便地理解,我们拿举个例子来说明,开发者声明了这样一个函数:pure_initcall(test_init);
所以pure_initcall(test_init)的解读就是:
首先宏展开成:__define_initcall(test_init, 0)
然后接着展开:___define_initcall(test_init, 0, .initcall0 )
static initcall_t __initcall_test_init0 = test_init; 这就是一个简单的变量定义。
同时声明__initcall_test_init0这个变量即使没被引用也保留符号,且将其放置在内核镜像的.initcall0.init段处。__attribute__((__section__(.initcall0 ".init"))) = test_init;
需要注意的是,根据官方注释可以看到early_initcall(fn)只针对内置的核心代码,不能描述模块。
② xxx_initcall()的调用顺序
从内核C函数起始部分也就是start_kernel开始往下挖,这里的调用顺序为:
start_kernel
-> rest_init();
-> kernel_thread(kernel_init, NULL, CLONE_FS);
-> kernel_init()
-> kernel_init_freeable();
-> do_basic_setup();
-> do_initcalls();
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
static void __init do_initcall_level(int level)
{
initcall_entry_t *fn;
strcpy(initcall_command_line, saved_command_line);
parse_args(initcall_level_names[level],
initcall_command_line, __start___param,
__stop___param - __start___param,
level, level,
NULL, &repair_env_string);
trace_initcall_level(initcall_level_names[level]);
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(initcall_from_entry(fn));
}
static initcall_entry_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
int __init_or_module do_one_initcall(initcall_t fn)
{
...
do_trace_initcall_start(fn);
ret = fn();
do_trace_initcall_finish(fn, ret);
...
return ret;
}
在上述代码中,定义了一个静态的initcall_levels数组,这是一个指针数组,数组的每个元素都是一个指针。
①do_initcalls()循环调用do_initcall_level(level),level就是initcall的优先级数字,由for循环的终止条件ARRAY_SIZE(initcall_levels) - 1可知,总共会调用do_initcall_level(0)~do_initcall_level(7),一共八次。
②而do_initcall_level(level)中则会遍历initcall_levels[level]中的每个函数指针,initcall_levels[level]实际上是对应的__initcall##level##_start指针变量,然后依次取出__initcall##level##_start指向地址存储的每个函数指针,并调用do_one_initcall(initcall_from_entry(fn))。
注意:
- fn为函数指针,fn++相当于函数指针+1,相当于:内存地址+sizeof(fn),sizeof(fn)根据平台不同而不同,一般来说,32位机上是4字节,64位机则是8字节。
- initcall_levels[level]指向当前".initcall##level.init"段,initcall_levels[level+1]指向".initcall##(level+1).init"段,两个段之间的内存就是存放所有添加的具有相同段属性的函数指针。
也就是从".initcall##level.init"段开始,每次取一个函数出来执行,并累加指针,直到取完。
③do_one_initcall(initcall_from_entry(fn))的执行:initcall_from_entry(fn)是得到当前fn在这个段中的偏移。然后调用fn。
那么,在initcall源码部分有提到,在开发者添加xxx_initcall(fn)时,事实上是将fn放置到了".initcall##level##.init"的段中,但是在do_initcall()的源码部分,却是从initcall_levels[level]取出,initcall_levels[level]是怎么关联到".initcall##level##.init"段的呢?
file:include/asm-generic/vmlinux.lds.h
#define INIT_CALLS_LEVEL(level) \
__initcall##level##_start = .; \
KEEP(*(.initcall##level##.init)) \
KEEP(*(.initcall##level##s.init)) \
#define INIT_CALLS \
__initcall_start = .; \
KEEP(*(.initcallearly.init)) \
INIT_CALLS_LEVEL(0) \
INIT_CALLS_LEVEL(1) \
INIT_CALLS_LEVEL(2) \
INIT_CALLS_LEVEL(3) \
INIT_CALLS_LEVEL(4) \
INIT_CALLS_LEVEL(5) \
INIT_CALLS_LEVEL(rootfs) \
INIT_CALLS_LEVEL(6) \
INIT_CALLS_LEVEL(7) \
__initcall_end = .;
在这里首先定义了__initcall_start,将其关联到".initcallearly.init"段。
然后对每个level定义了INIT_CALLS_LEVEL(level),将INIT_CALLS_LEVEL(level)展开之后的结果是定义__initcall##level##_start,并将__initcall##level##_start关联到".initcall##level##.init"段和".initcall##level##s.init"段。
到这里,__initcall##level##_start和".initcall##level##.init"段的对应就比较清晰了,所以,从initcall_levels[level]部分一个个取出函数指针,并执行每一个函数,其中有一个fn就是执行xxx_initcall()定义的函数。
总结:
便于理解,我们需要一个示例来梳理整个流程,假设我是一个驱动开发者,开发一个名为beagle的驱动,在系统启动时需要调用beagle_init()函数来启动启动服务。
我需要先将其添加到系统中:
core_initcall(beagle_init)
core_initcall(beagle_init)宏展开为__define_initcall(beagle_init, 1),所以beagle_init()这个函数被放置在".initcall1.init"段处。
在内核启动时,系统会调用到do_initcall()函数。 根据指针数组initcall_levels[1]找到__initcall1_start指针,在vmlinux.lds.h可以查到:__initcall1_start对应".initcall1.init"段的起始地址,依次取出段中的每个函数指针,并执行函数。
添加的服务就实现了启动。
(3)常用的宏
__init,标记内核启动时所用的初始化代码,内核启动完成后就不再使用。其所修饰的内容被放到.init.text section中。
__exit,标记模块退出代码,对非模块无效
__initdata,标记内核启动时所用的初始化数据结构,内核启动完成后不再使用。其所修饰的内容被放到.init.data section中。
__devinit,标记设备初始化所用的代码
__devinitdata,标记设备初始化所用的数据结构
__devexit,标记设备移除时所用的代码
xxx_initcall,7个级别的初始化函数
(4)driver中的使用
module_init, module_exit函数所调用的函数,需要分别用__init和__exit来标记
pci_driver数据结构不需要标记
probe和remove函数用__devinit和__devexit来标记
如果remove使用__devexit标记,则在pci_drvier结构中要用__devexit_p(remove)来引用remove函数
如果不确定需不需要添加宏,则不要添加
(5)Init函数的优先级
· 不同级别的init函数:
由上可知不同级别的init函数按照对应initial_call的对应数字来区分,数字0是最高优先级执行的,7是最低优先级执行。
· 同一级别的init函数:
同级别module_init,看makefile加载顺序(从上往下),.o文件先生成的在前(obj -y += xxx.o在前面的就先启动)
总结:修改设备加载的顺序可以有两种方法,一种是修改设备加载的初始化等级,另外就是修改Makefile中的顺序。
2. makefile语法(Kbuild Makefile)
(1)目标定义
目标定义是Kbuild Makefile的主要部分,也是核心部分。主要是定义了要编译的文件,所有的选项,以及到哪些子目录去执行递归操作。
这个功能是通过下面两个对象指定的obj-?和xxx-objs:
· obj-?
obj-?指定编译什么,怎么编译?其中的“?”可能是“y”或“m”,“y”指定把对象编译进内核中,“m”指定把对象编译为模块。语法如下:
obj-? = $(target).o
target为编译对象的名字。如果没有指定xxx-objs,这编译这个对象需要的源文件就是$(target).c或$(target).s。如果指定了$(target)-objs,则编译这个对象需要的源文件由$(target)-objs指定,并且不能有$(target).c或$(target).s文件。
例:
obj-y += dilink_touchs.o
告诉kbuild,在文件夹中又一个叫做dilink_touchs.o的object。dilink_touchs.o将会被从dilink_touchs.c或者dilink_touchs.s被构建。
如果dilink_touchs.o被构建成一个模块,则将使用变量obj-m。
· xxx-objs
xxx-objs指定了编译对象需要的文件,一般只有在源文件是多个时才需要它。
如果某个目标由多个源文件编译得到,那么可以通过$(<module_name>-objs)或$(<module_name>-y)把这些源文件告诉kbuild。Kbuild能够识别后缀-objs和-y,例:
obj-$(CONFIG_ISDN) += isdn.o
isdn-objs := isdn_net_lib.o isdn_v110.o isdn_common.o
Kbuild会编译所有$(isdn-objs)中的对象,并调用"$(LD) -r"把它们链接成isdn.o文件。
只要包含了这两行,Kbuild Makefile就应该可以工作了。
· obj-y生成built-in.o
Kbuild编译所有的$(obj-y)文件,并调用”$(LD) -r”把所有这些文件合并到built-in.o文件。这个built-in.o会被上一级目录的Makefile使用,最终链接到vmlinux中。
$(obj-y)中的文件是有顺序的。联接也是有顺序的,那是因为有些函数(module_init()/__initcall)将会在启动时按照他们出现的顺序进行调用。
Kbuild能够识别用于组成目标文件的后缀-objs和后缀-y。这就让Kbuild Makefile可以通过使用 CONFIG_ 符号来判断该对象是否是用来组合对象的。
-objs后面是目标文件,而-y针对复合对象。
下面是一个使用后缀-y的例子。后缀-y的好处是,可以使用CONFIG_XXX符号来决定是否加入某些源文件(.o从对应的.c或.S编译得到):
obj-$(CONFIG_EXT2_FS) += ext2.o
ext2-y := balloc.o bitmap.o
ext2-$(CONFIG_EXT2_FS_XATTR) += xattr.o
在这个例子中,如果 $(CONFIG_EXT2_FS_XATTR) 是'y', xattr.o将是复合对象 ext2.o的一部分。所以,如果CONFIG_EXT2_FS=y,那Kbuild会按你所期望的那样,生成 ext2.o文件,然后将其联接到 built-in.o中,最终连接进内核中。
如果CONFIG_ETX2_FS=m,由balloc.o和bitmap.o两个目标文件最终链接生成ext2.o,直至ext2.ko文件,保存在/lib/modules目录下方便加载。
总结:
obj-$(...)+=xxx.o 表示是否编译xxx.o目标文件
xxx.o目标依赖多个源文件的情况就下面两种:
xxx-y+=xxx1.o 复合对象xxx.o目标依赖哪些文件
xxx-$(...)+=xxx2.o xxx3.o 复合对象xxx.o目标依赖哪些文件
或者xxx-objs+=xxx1.o xxx2.o xxx3.o 目标文件xxx.o依赖哪些文件
(2)嵌套编译(调用子目录)
有时一个对象可能嵌入到另一个对象的目录下,那个如何编译子目录下的对象呢?其实很简单,只要指定obj-?的对象为子目录的名字就可以了:
obj-? = $(sub_target)/
其中“?”可以是“y”或“m”,$(sub_target)是子目录名字。
例:
obj-$(CONFIG_TOUCHSCREEN_HIMAX_CHIPSET) += himax_tp/
$(CONFIG_TOUCHSCREEN_HIMAX_CHIPSET)要么是y(built-in)要么是m(module)。如果CONFIG_TOUCHSCREEN_HIMAX_CHIPSET既不是y也不是m,那么文件将不会被编译也不会被连接。
(3)= := += ?=的区别
①=并不会马上就行赋值,而是在Makefile中将等号右边的变量展开之后,将最后的值赋给等号左边的变量。
x = sb
y = u$(x)
x = all
上例输出为:x = all, y = uall
这里=的话算$(x)的时候会一直搜索到文件最后,最后再赋值。
②:=会马上进行赋值,并不会展开,这种可以理解为最普通的情况
x := sb
y := u$(x)
x := all
上例输出为:x = all, y = usb
这里:=不会搜索后面的x值。
③+=相当于append,后面继续追加值
x = u
x += sb
上例输出为:x = usb
④?=判断原变量是否为空,为空赋值,不为空则不会赋值
3. Kconfig语法
基本构成包括五种,menu/endmenu,menuconfig,config,choice/endchoice,source。
(1)menu/endmenu
menu的作用,可以理解成一个目录,menu可以把其中一部分配置项包含到一个menu中,这样有利于配置的分类显示。menu与endmenu是一组指令,必须同时出现。menu和endmenu中包含的部分就是子目录中的配置项。
比如,在init/Kconfig中显示为:
menu "General setup"
这样,就会生成一个目录,特征就是右侧会出现一个箭头,如图1中第一行。当点击确认键时,会进入这个菜单项
(2)menuconfig
menuconfig有点类似menu,但区别就在于menu后面多了一个config,这个menu是可以配置的,如上图中的第二行,前面比menu类型多了一个方框,通过空格可以修改这个配置项的选中状态。而且从格式上来看,也是有区别的。格式如:
menuconfig MODULES
bool "Enable loadable modulesupport"config
if MODULES
xx
endif
也就是说,配置项是位于if和endif中。其中的部分就是MODULES子目录显示的内容。如果选中了MODULE,那么if和endif中的内容可以显示。如果没有定义,就只能进入一个空目录。
(3)config
config是构成Kconfig的最基本单元,其中定义了配置项的详细信息。定义的格式如下:
config CGROUP_HUGETLB
bool "HugeTLB controller"
depends on HUGETLB_PAGE
select PAGE_COUNTER
default n
help
Provides a cgroup controller for HugeTLB pages.....
可知,config需要定义名称,与menuconfig相同。这个名称不但用于裁剪内核中,还用于配置项之间的相互依赖关系中。
config的类型有5种,分别是bool(y/n),tristate(y/m/n),string(字符串),hex(十六进制),integer(整数)。其中,需要特别介绍一下bool和tristate,bool只能表示选中和不选,而tristate还可以配置成模块(m),特别对于驱动程序的开发非常有用。
其他语法如下:
1) prompt:提示,显示在make menuconfig中的名称,一般省略。下面两种写法相同。
①. bool “Networking Support”
②. bool
prompt “NetworkingSupport”
2) default:默认值
一个配置项可以有多个默认值,但是只有第一个被定义的值是有效的。
3) depends on/requires:依赖关系
如果依赖的配置项没有选中,那么就当前项也无法选中。
4) select:反向依赖
如果当前项选中,那么也选中select后的选项。
5) range:范围,用于hex和integer
range A B表示当前值不小于A,不大于B
6) comment:注释
(4)choice/endchoice
choice的作用,多选一,有点像MFC中的Radio控件。如:
choice
prompt "Compression algorithm"
depends on MODULE_COMPRESS
default MODULE_COMPRESS_GZIP
help
This determines which sort of compression will be used during
'make modules_install'.
GZIP (default) and XZ are supported.
config MODULE_COMPRESS_GZIP
bool "GZIP"
config MODULE_COMPRESS_XZ
bool "XZ"
Endchoice
choice有点类似于menu,是在子窗口里选择,但是不同的是子窗口中只能选择一项。在prompt后会显示当前选择项的名称。
(5) source
source只是将另外一个Kconfig文件直接复制到当前位置而已。但它的作用也是明显的,可以将这个系统贯穿在一起。从开始位置arch/arm/Kconfig,来将整个系统都作为配置型。这个文件会通过source指令来调用其他目录下的Kconfig文件,从而完成整体配置。这样,arch/arm/Kconfig就可以理解成main函数,而source指令就有点类似于include。可以按照上面的语法,来分析Kconfig文件。如:
source "drivers/input/touchscreen/himax_tp/Kconfig"
4. Kconfig和.config文件和Makefile三者的关联
经过menuconfig配置之后保存,就会在内核顶层目录下生成.config文件。
=y表示该配置将会被编译进内核,=m表示该配置需要单独编译成模块。
内核顶层makefile会调用.config文件,引用.config里的配置,进而选择性的编译内核驱动模块。
配置项被配置成Y、N、M会影响“.config”文件中的CONFIG_XXX变量的配置值。
“.config”中的配置值(=y、=m、没有)会影响最终的编译链接过程。
如果=y则会被编入(built-in),如果=m会被单独连接成一个”.ko”模块,如果没有则对应的代码不会被编译。
那么这是怎么实现的?都是通过makefile实现的。
如makefile中:obj-$(CONFIG_DM9000) += dm9000.o,
如果CONFIG_DM9000变量值为y,则obj += dm9000.o,因此dm9000.c会被编译;
如果CONFIG_DM9000变量未定义,则dm9000.c不会被编译。
如果CONFIG_DM9000变量的值为m则会被连接成“.ko”模块。