内核模块 简介

Linux内核模块机制简介

内核是单一体系结构的,整个内核是一个单独的非常大的程序,也就是我们常说的宏内核。为了改善单一体系结构的可扩展性、可维护性等,Linux操作系统使用了一种全新的内核模块机制:用户可以根据需要,在不需要对内核重新编译的情况下,模块能动态地装入内核或从内核移出。

模块是内核态运行的程序,实际上是一种目标对象文件。其没有链接,不能独立运行。但代码可在运行时链接到系统,作为内核的一部分运行。或从内核中取下,动态扩充内核的功能。模块通常由一组函数和数据结构组成,用来实现一种文件系统,一个驱动程序,或其它内核上层的功能。模块机制的完整叫法应该是动态可加载内核模块(Loadable Kernel Module)或 LKM,一般就简称为模块。
模块像静态连接的内核函数一样,它在内核态代表当前进程执行。

模块的加载流程

驱动程序注册设备,创建系统信息 (/sys/class/xxx)
注册设备使用的是模块文件名,要带 .ko 后缀。
当操作已插入内核的模块是,只需使用模块名(无后缀)。
udev|mdev 根据注册的设备信息,创建设备节点 (/dev/xxx)
所有设备节点信息存储在 /proc/devices
加载后会输出模块内的加载信息,通过 dmesg 查看。

init_module 系统调用流程

不依赖 C 库。链接、重定位过程自己完成
Kernel/module.c/init_module
拷贝到内核:copy_module_from_user
地址空间分配:layout_and_allocate
符号解析:simplify_symbols
重定位:apply_relocations
执行:complete_formation

内核驱动的两种执行方式

内核启动阶段:

    链接脚本:/arch/arm/kernel/vmlinux.lds.S
    链接段名:.initcall##level##.init
    内核首个函数:/init/main.c -> start_kernel()
    驱动入口解析:pid = kernel_thread(kernel_init, NULL, CLONE_FS);
      -> kernel_init_freeable
        -> do_basic_setup
          -> do_initcalls
            -> do_initcall_level
    for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
        do_one_initcall(initcall_from_entry(fn));
    功能:获取段中注册的驱动函数入口,依次执行。
    initcall_levels:存储链接脚本中 init 段的首地址

模块加载:在模块内调用 dump_stack 追踪过程

    insmod -> init_module -> sys_init_module
    -> load_module:将模块文件加载到内核空间的两个段中。.text 段会一直存在于内存;.init.text 则只用于初始化
        module_sig_check:签名验证。
        layout_and_allocate:分配申请空间。
        check_module_license_and_versions:内核 license 和版本验证。
        simplify_symbols、apply_relocations、post_relocation:模块符号表重定位。
        -> do_init_module:初始化 & 运行模块。
            freeinit->module_init = mod->init_layout.base;:记住 .init.text 首地址。用完之后需要释放。
            do_one_initcall:执行模块入口函数用于初始化(module_init 修饰的 API)
Linux内核模块编译、加载、运行机制分析、版本控制、许可声明、内核污染、模块传参、模块签名机制、out-of-tree动态模块编译及Makefile模板编写,尽在《Linux内核编程》,详情点击:王利涛老师个人淘宝店:Linux内核编程