自己做网站制作教程,做家庭影院的有哪些网站,升级网页,wordpress客户端开发源码基于#xff1a;Linux 5.4 0. 前言
Linux 对驱动程序提供静态编译进内核和动态加载两种方式#xff0c;当采用静态方式时#xff0c;开发者如果想要在系统中启动这个驱动通常调用类似 xxx_init() 接口。
最直观的做法#xff1a;开发者试图添加一个驱动初始化程序时Linux 5.4 0. 前言
Linux 对驱动程序提供静态编译进内核和动态加载两种方式当采用静态方式时开发者如果想要在系统中启动这个驱动通常调用类似 xxx_init() 接口。
最直观的做法开发者试图添加一个驱动初始化程序时在内核启动 init 程序的某个地方直接添加调用自己驱动程序的 xxx_init() 接口函数在内核启动时就自然会启动这个驱动程序类似
void kernel_init()
{a_init();b_init();...m_init();
}
但是这种做法在小系统中或许可以对于 linux 庞大的系统来说驱动很多不可能每添加一个驱动就会改动一下 kernel_init() 代码这将会是一场灾难。
Linux 内核提供了解决方案
在编译的时候通过使用告知编译器连接自定义一个专门用来存放这些初始化函数的地址段将对应的函数入口统一放在一起驱动程序中调用linux 内核提供的专门的 xxx_init() 接口由编译器来收集这些入口函数集中存放在一个地方内核启动时统一扫描这段的开始地址按照顺序执行被添加的驱动初始化程序init 初始化代码基本上只会执行一次因此在这类 xxx_init() 代码所在的特殊段在初始化 完成之后会被内存管理器回收同时节省了这部分的内存 1. initcall 源码
上文提到过 Linux 对驱动程序提供静态编译进内核和动态加载两种方式Linux 的 initcall 机制也是根据静态编译和动态加载的两种方式选择不同的编译、运行流程。
include/linux/init.h#ifndef MODULE... //静态加载#else... //动态加载#endif
MODULE 是在编译的时候通过编译器参数来传入。例如在编译 ko 时会使用如下两个编译选项如果是链接到内核则不会使用
//MakefileKBUILD_AFLAGS_MODULE : -DMODULE
KBUILD_CFLAGS_MODULE : -DMODULE
通过 MODULE 的配置选择静态编译还是动态加载。
本文将分开单独剖析这两种情况下的 initcall 机制。 2. 静态编译
2.1 initcall 接口
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 couldnt 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)#define __exitcall(fn) \static exitcall_t __exitcall_##fn __exit_call fn#define console_initcall(fn) ___define_initcall(fn, con, .con_initcall)
对于静态编译 initcall 接口如上其中 pure_initcall() 只能在静态编译中存在。
当然对于静态编译的驱动也可以调佣 module_init() 接口
include/linux/module.h#define module_init(x) __initcall(x);#define module_exit(x) __exitcall(x);
此时的 module_init() 就是 device_initcall()。 2.2 initcall 级别 2.3 __define_initcall()
include/linux/init.h#ifdef CONFIG_LTO_CLANG/** With LTO, the compiler doesnt necessarily obey link order for* initcalls, and the initcall variable needs to be globally unique* to avoid naming collisions. In order to preserve the correct* order, we add each variable into its own section and generate a* linker script (in scripts/link-vmlinux.sh) to ensure the order* remains correct. We also add a __COUNTER__ prefix to the name,* so we can retain the order of initcalls within each compilation* unit, and __LINE__ to make the names more unique.*/#define ___lto_initcall(c, l, fn, id, __sec) \static initcall_t __initcall_##c##_##l##_##fn##id __used \__attribute__((__section__( #__sec \__stringify(.init..##c##_##l##_##fn)))) fn;#define __lto_initcall(c, l, fn, id, __sec) \___lto_initcall(c, l, fn, id, __sec)#define ___define_initcall(fn, id, __sec) \__lto_initcall(__COUNTER__, __LINE__, fn, id, __sec)
#else#define ___define_initcall(fn, id, __sec) \static initcall_t __initcall_##fn##id __used \__attribute__((__section__(#__sec .init))) fn;
#endif
#endif#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
下文会继续细化分析这里提前提示
__define_initcall() 其实就是定义了一个 static initcall_t 的函数指针
include/linux/init.htypedef int (*initcall_t)(void);
typedef void (*exitcall_t)(void); 2.3.1 __used
include/linux/compiler_attributes.h#define __used __attribute__((__used__))
这是一种 attribute 修饰属性的一种意思是告诉编译器这个静态符号在编译的时候即使没有使用也要保留不能优化掉。
详细可以查看《__attribute__机制详解》一文。 2.3.1 __attribute__ ((__section__(...)))
__attribute__ 是 GNU C 的一大特色可以用来修饰对象、函数、结构体类型等等。
这里用来修改 section意思是将作用的函数放入指定的 section name 对应的段中。
详细可以查看《__attribute__机制详解》一文。 2.3.2 __stringify()
include/linux/stringify.h#define __stringify_1(x...) #x
#define __stringify(x...) __stringify_1(x)
将 __stringify() 中内容字符串化。 2.4 举例理解initcall接口
上面initcall 接口最终有各种宏转换可能看着还是一头雾水。本小节用实例来剖析这个接口。
假如我们在驱动使用如下接口
module_init(hello_init);
那么在编译的时候编译器会通过 initcall 接口产生
static initcall_t __initcall_1_23_hello_init6 __attribute__(__used) \__attribute__((__section__(.initcall6.init..1_23_hello_init))) hello_init; 2.5 linux 编译后的initcall 函数
通过 arch64-linux-gnu-nm 或者 aarch64-linux-gnu/bin/nm 命令
aarch64-linux-gnu/bin/nm -n vmlinux | grep -E -C 2 _initcall.*(_start|_end)$
会显示编译之后的 initcall 函数
System.map:282341:ffffffc012032ee0 D __initcall_start
System.map-282342-ffffffc012032ee0 D __setup_end
System.map-282343-ffffffc012032ee8 d __initcall_224_66_trace_init_flags_sys_exitearly
--
System.map-282363-ffffffc012032f88 d __initcall_131_37_dummy_timer_registerearly
System.map-282364-ffffffc012032f90 d __initcall_312_768_initialize_ptr_randomearly
System.map:282365:ffffffc012032f98 D __initcall0_start
System.map-282366-ffffffc012032f98 d __initcall_241_771_bpf_jit_charge_init0
System.map-282367-ffffffc012032fa0 d __initcall_141_53_init_mmap_min_addr0
System.map-282368-ffffffc012032fa8 d __initcall_209_6528_pci_realloc_setup_params0
System.map-282369-ffffffc012032fb0 d __initcall_339_1143_net_ns_init0
System.map:282370:ffffffc012032fb8 D __initcall1_start
System.map-282371-ffffffc012032fb8 d __initcall_160_1437_fpsimd_init1
System.map-282372-ffffffc012032fc0 d __initcall_181_669_tagged_addr_init1
--
System.map-282427-ffffffc012033178 d __initcall_347_1788_init_default_flow_dissectors1
System.map-282428-ffffffc012033180 d __initcall_360_2821_netlink_proto_init1
System.map:282429:ffffffc012033188 D __initcall2_start
...
__initcall 后面跟 __COUNTER__ 和 __LINE__接着加上初始化函数 fun最后是 initcall 的级别。 当然通过命令 readelf 或者 objdump (objdump -h vmlinux.o)都能看到字段
Sections:
Idx Name Size VMA LMA File off Algn0 .initcall0.init 00000020 0000000000000000 0000000000000000 00000040 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA1 .initcall1.init 000001d0 0000000000000000 0000000000000000 00000060 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA2 .initcall2.init 00000138 0000000000000000 0000000000000000 00000230 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA3 .initcall2s.init 00000008 0000000000000000 0000000000000000 00000368 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA4 .initcall3.init 000000b0 0000000000000000 0000000000000000 00000370 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA5 .initcall3s.init 00000008 0000000000000000 0000000000000000 00000420 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA6 .initcall4.init 000004f0 0000000000000000 0000000000000000 00000428 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA7 .initcall4s.init 00000008 0000000000000000 0000000000000000 00000918 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA8 .initcall5.init 00000168 0000000000000000 0000000000000000 00000920 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA9 .initcall5s.init 00000008 0000000000000000 0000000000000000 00000a88 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA10 .initcall6.init 00001140 0000000000000000 0000000000000000 00000a90 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA11 .initcall7.init 00000140 0000000000000000 0000000000000000 00001bd0 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA12 .initcall7s.init 00000028 0000000000000000 0000000000000000 00001d10 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA13 .con_initcall.init 00000008 0000000000000000 0000000000000000 00001d38 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA14 .initcallearly.init 000000b8 0000000000000000 0000000000000000 00001d40 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA15 .initcallrootfs.init 00000008 0000000000000000 0000000000000000 00001df8 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA 2.6 initcall 的函数如何被调用
init/main.cstart_kernel()----arch_call_rest_init()----rest_init()----kernel_init()----kernel_init_freeable()----do_basic_setup()----do_initcalls()
init/main.cstatic initcall_entry_t *initcall_levels[] __initdata {__initcall0_start,__initcall1_start,__initcall2_start,__initcall3_start,__initcall4_start,__initcall5_start,__initcall6_start,__initcall7_start,__initcall_end,
};static void __init do_initcalls(void)
{int level;for (level 0; level ARRAY_SIZE(initcall_levels) - 1; level)do_initcall_level(level);
}
for 循环是一个指针数组该数组会被存放在 __initdata 段。
另外这个指针数组的类型为 initcall_entry_t其实就是在上文第 2.3 节提到的 initcall_t 函数指针类型。
继续来看下这个指针数组中的元素__initcall0_start ~ __initcall_end而这些元素的值在本 c 文件中已经声明
init/main.cextern initcall_entry_t __initcall_start[];
extern initcall_entry_t __initcall0_start[];
extern initcall_entry_t __initcall1_start[];
extern initcall_entry_t __initcall2_start[];
extern initcall_entry_t __initcall3_start[];
extern initcall_entry_t __initcall4_start[];
extern initcall_entry_t __initcall5_start[];
extern initcall_entry_t __initcall6_start[];
extern initcall_entry_t __initcall7_start[];
extern initcall_entry_t __initcall_end[];
不难看出initcall_levels 中存放的是这些函数指针数组的首地址。
那么这些实际的指针数组是在哪里呢从上文initcall 函数都会被定义成static initcall_t 类型并且保存在 .initcall##level##.init 段中那么 initcall_levels 与其是怎么关联的呢
答案在 vmlinux.lds.h 中。 2.6.1 vmlinux.lds.h
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_start .; \*(.initcallearly.init) \__initcall0_start .; \*(.initcall0.init) \*(.initcall0s.init) \// 省略1、2、3、4、5__initcallrootfs_start .; \*(.initcallrootfs.init) \*(.initcallrootfss.init) \__initcall6_start .; \*(.initcall6.init) \*(.initcall6s.init) \__initcall7_start .; \*(.initcall7.init) \*(.initcall7s.init) \__initcall_end .;上面这些代码段最终在kernel.img中按先后顺序组织也就决定了位于其中的一些函数的执行先后顺序。
.init 或者 .initcalls 段的特点就是当内核启动完毕后这个段中的内存会被释放掉。 2.6.2 do_initcall_level()
init/main.cstatic 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[level1]; fn)do_one_initcall(initcall_from_entry(fn));
}
do_one_initcall() 的参数就是获取到函数的函数指针。
init/main.cint __init_or_module do_one_initcall(initcall_t fn)
{...ret fn();...return ret;
} 3. 动态加载
当模块以 ko 的形式存在并被加载重定位到内核其作用域和静态连接的代码是完全等价的。这种运行方式的优点
可根据系统需要运行动态加载模块以扩充内核功能不需要时可以将其卸载以释放内存空间当需要修改内核功能时只需要编译模块而不必重新编译整个内核
当然有些模块是必须要编译到内核随内核一起运行从不卸载例如 vfs、platform_bus 等。 3.1 initcall 接口
当动态加载时会在Makefile中添加上 MODULE 的定义
//MakefileKBUILD_AFLAGS_MODULE : -DMODULE
KBUILD_CFLAGS_MODULE : -DMODULE
而initcall 代码也将从 init.h 转换到 module.h
include/module.h#ifndef MODULE...#else#define early_initcall(fn) module_init(fn)
#define core_initcall(fn) module_init(fn)
#define core_initcall_sync(fn) module_init(fn)
#define postcore_initcall(fn) module_init(fn)
#define postcore_initcall_sync(fn) module_init(fn)
#define arch_initcall(fn) module_init(fn)
#define subsys_initcall(fn) module_init(fn)
#define subsys_initcall_sync(fn) module_init(fn)
#define fs_initcall(fn) module_init(fn)
#define fs_initcall_sync(fn) module_init(fn)
#define rootfs_initcall(fn) module_init(fn)
#define device_initcall(fn) module_init(fn)
#define device_initcall_sync(fn) module_init(fn)
#define late_initcall(fn) module_init(fn)
#define late_initcall_sync(fn) module_init(fn)#define console_initcall(fn) module_init(fn)/* Each module must use one module_init(). */
#define module_init(initfn) \static inline initcall_t __maybe_unused __inittest(void) \{ return initfn; } \int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));/* This is only required if you want to be unloadable. */
#define module_exit(exitfn) \static inline exitcall_t __maybe_unused __exittest(void) \{ return exitfn; } \void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));#endif
moudle_init() 共做了两件事情
定义 static initcall_t __inittest() { ... }声明 int init_module()
__inittest 仅仅是为了检测定义的函数是否符合 initcall_t 类型不过不是 __inittest 类型在编译的时候会报错。所以真正使用的是 init_module() 函数的声明。 注意
__copy(initfn)从 initfn 赋值函数属性从 gcc-9 开始支持__attribute__((alias(#initfn)))为 init_module 创建别名指向原来的 initfn
这里alias 是 gcc 的特有属性将定义 init_module 为函数initfn 的别名。即对于module_init() 作用就是定义一个变量名 init_module其地址与 initfn 是一样的。 3.2 insmod
编译后的模块 xxx.ko 需要通过 insmod 或 modprobe 将其加载到内核由于 insmod 是bubybox 提供的用户层命令所以需要阅读 busybox 源码
modutils/insmod.cint insmod_main(int argc UNUSED_PARAM, char **argv)
{char *filename;int rc;/* Compat note:* 2.6 style insmod has no options and required filename* (not module name - .ko cant be omitted).* 2.4 style insmod can take module name without .o* and performs module search in default directories* or in $MODPATH.*/IF_FEATURE_2_4_MODULES(getopt32(argv, INSMOD_OPTS INSMOD_ARGS);argv optind - 1;);filename *argv;if (!filename)bb_show_usage();rc bb_init_module(filename, parse_cmdline_module_options(argv, /*quote_spaces:*/ 0));if (rc)bb_error_msg(cant insert %s: %s, filename, moderror(rc));return rc;
}
bb_init_module()
modutils/modutils.cint FAST_FUNC bb_init_module(const char *filename, const char *options)
{size_t image_size;char *image;int rc;bool mmaped;if (!options)options ;//TODO: audit bb_init_module_24 to match error code convention
#if ENABLE_FEATURE_2_4_MODULESif (get_linux_version_code() KERNEL_VERSION(2,6,0))return bb_init_module_24(filename, options);
#endif/** First we try finit_module if available. Some kernels are configured* to only allow loading of modules off of secure storage (like a read-* only rootfs) which needs the finit_module call. If it fails, we fall* back to normal module loading to support compressed modules.*/
# ifdef __NR_finit_module{int fd open(filename, O_RDONLY | O_CLOEXEC);if (fd 0) {rc finit_module(fd, options, 0) ! 0;close(fd);if (rc 0)return rc;}}
# endifimage_size INT_MAX - 4095;mmaped 0;image try_to_mmap_module(filename, image_size);if (image) {mmaped 1;} else {errno ENOMEM; /* may be changed by e.g. open errors below */image xmalloc_open_zipped_read_close(filename, image_size);if (!image)return -errno;}errno 0;init_module(image, image_size, options);rc errno;if (mmaped)munmap(image, image_size);elsefree(image);return rc;
}init_module() 定义如下
modutils/modutils.c
#define init_module(mod, len, opts) syscall(__NR_init_module, mod, len, opts)最终进行syscall 的系统调用
kernel/module.cSYSCALL_DEFINE3(init_module, void __user *, umod,unsigned long, len, const char __user *, uargs)
{int err;struct load_info info { };err may_init_module();if (err)return err;pr_debug(init_module: umod%p, len%lu, uargs%p\n,umod, len, uargs);err copy_module_from_user(umod, len, info);if (err)return err;return load_module(info, uargs, 0);
}
其实无论是 insmod 或者是 modprobe最终都是调用到内核的 load_module()。
下面的流程为
kernel/module.cload_module()----do_init_module()----do_one_initcall()
最终 do_one_initcall() 同静态编译
init/main.cint __init_or_module do_one_initcall(initcall_t fn)
{...ret fn();...return ret;
}