【Linux API 揭秘】module_init与module_exit

Linux Version:6.6

Author:Donge

Github:linux-api-insides

1、函数作用

module_initmodule_exit是驱动中最常用的两个接口,主要用来注册、注销设备驱动程序。

并且这两个接口的实现机制是一样的,我们先以module_init为切入点分析。

2、module_init函数解析

2.1 module_init

 #ifndef MODULE
 /**
  * module_init() - driver initialization entry point
  * @x: function to be run at kernel boot time or module insertion
  *
  * module_init() will either be called during do_initcalls() (if
  * builtin) or at module insertion time (if a module). There can only
  * be one per module.
  */
 #define module_init(x) __initcall(x);
 ​
 ......
 ​
 #else /* MODULE */
 ​
 ......
     
 /* 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))); \
     ___ADDRESSABLE(init_module, __initdata);
 ​
 ......
 ​
 #endif

函数名称module_init

文件位置include/linux/module.h

函数解析

Linux内核中,驱动程序可以以两种方式存在:内建(Builtin)和模块(Module)。内建驱动就是在编译时,直接编译进内核镜像中;而模块驱动则是在内核运行过程中动态加载卸载的。

module_init函数的定义位置有两处,使用MODULE宏作为判断依据。MODULE是一个预处理器宏,仅当该驱动作为模块驱动时,编译的时候会加入MODULE的定义。

这里难免会有疑问:为什么会有两套实现呢?

其实,当模块被编译进内核时,代码是存放在内存的.init字段,该字段在内核代码初始化后,就会被释放掉了,所以当可动态加载模块需要加载时,就需要重新定义了。

2.1.1 模块方式

当驱动作为可加载模块时,MODULE宏被定义,我们简单分析一下相关代码

 #define module_init(initfn)                 \
     static inline initcall_t __maybe_unused __inittest(void) \
     { return initfn; } \
     int init_module(void) __copy(initfn) \
         __attribute__((alias(#initfn))); \
     ___ADDRESSABLE(init_module, __initdata);
  • static inline initcall_t __maybe_unused __inittest(void) { return initfn; }:一个内联函数,返回传入的initfn函数。

    • __maybe_unused :编译器指令,用于告诉编译器,该函数可能不会使用,以避免编译器产生警告信息。

  • int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));init_module函数的声明

    • __copy(initfn):编译器指令,也就是将我们的initfn函数代码复制到init_module中,

    • __attribute__((alias(#initfn))):编译器指令,将init_module函数符号的别名设置为initfn

  • ___ADDRESSABLE(init_module, __initdata);:一个宏定义,主要用于将init_module函数的地址放入__initdata段,这样,当模块被加载时,init_module函数的地址就可以被找到并调用。

总的来说,如果是可加载的ko模块,module_init宏主要定义了init_module函数,并且将该函数与initfn函数关联起来,使得当模块被加载时,初始化函数可以被正确地调用。

2.1.2 内建方式

当模块编译进内核时,MODULE宏未被定义,所以走下面流程

 #define module_init(x)  __initcall(x);

2.2 __initcall

 #define __initcall(fn) device_initcall(fn)
 ​
 #define device_initcall(fn) __define_initcall(fn, 6)
 ​
 #define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
 ​
 #define ___define_initcall(fn, id, __sec) \
     __unique_initcall(fn, id, __sec, __initcall_id(fn))
 ​
 #define __unique_initcall(fn, id, __sec, __iid) \
     ____define_initcall(fn, \
         __initcall_stub(fn, __iid, id), \
         __initcall_name(initcall, __iid, id), \
         __initcall_section(__sec, __iid))
 ​
 #define ____define_initcall(fn, __unused, __name, __sec) \
     static initcall_t __name __used \
         __attribute__((__section__(__sec))) = fn;
 ​
 #define __initcall_stub(fn, __iid, id) fn
 ​
 /* Format: <modname>__<counter>_<line>_<fn> */
 #define __initcall_id(fn) \
     __PASTE(__KBUILD_MODNAME, \
     __PASTE(__, \
     __PASTE(__COUNTER__, \
     __PASTE(_, \
     __PASTE(__LINE__, \
     __PASTE(_, fn))))))
 ​
 /* Format: __<prefix>__<iid><id> */
 #define __initcall_name(prefix, __iid, id) \
     __PASTE(__, \
     __PASTE(prefix, \
     __PASTE(__, \
     __PASTE(__iid, id))))
 ​
 #define __initcall_section(__sec, __iid) \
     #__sec ".init"
 ​
 /* Indirect macros required for expanded argument pasting, eg. __LINE__. */
 #define ___PASTE(a,b) a##b
 #define __PASTE(a,b) ___PASTE(a,b)

函数名称__initcall

文件位置include/linux/init.h

函数解析:设备驱动初始化函数

2.2.1 代码调用流程

 module_init(fn)
     |--> __initcall(fn)
         |--> device_initcall(fn)
             |--> __define_initcall(fn, 6)
                 |--> ___define_initcall(fn, id, __sec)
                     |--> __initcall_id(fn)
                     |--> __unique_initcall(fn, id, __sec, __iid)
                         |--> ____define_initcall(fn, __unused, __name, __sec)
                             |--> __initcall_stub(fn, __iid, id)
                             |--> __initcall_name(prefix, __iid, id)
                             |--> __initcall_section(__sec, __iid)
                         |--> ____define_initcall(fn, __unused, __name, __sec)

进行函数分析前,我们先要明白###的概念

2.2.2 #和##的作用

符号 作用 举例
## ##符号 可以是连接的意思 例如 __initcall_##fn##id__initcall_fnid那么,fn = test_initid = 6时,__initcall##fn##id__initcall_test_init6
# #符号 可以是字符串化的意思 例如 #id"id"id=6 时,#id"6"

更多干货可见:高级工程师聚集地,助力大家更上一层楼!

2.2.3 函数解析

下面分析理解比较有难度的函数

 #define device_initcall(fn)     __define_initcall(fn, 6)
 #define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
  • .initcall##id:通过##来拼接两个字符串:.initcall6

 #define ___define_initcall(fn, id, __sec)           \
     __unique_initcall(fn, id, __sec, __initcall_id(fn))
 ​
 /* Format: <modname>__<counter>_<line>_<fn> */
 #define __initcall_id(fn) \
     __PASTE(__KBUILD_MODNAME, \
     __PASTE(__, \
     __PASTE(__COUNTER__, \
     __PASTE(_, \
     __PASTE(__LINE__, \
     __PASTE(_, fn))))))
 ​
 /* Indirect macros required for expanded argument pasting, eg. __LINE__. */
 #define ___PASTE(a,b) a##b
 #define __PASTE(a,b) ___PASTE(a,b)
  • ___PASTE:拼接两个字符串

  • __initcall_id它用于生成一个唯一的标识符,这个标识符用于标记初始化函数

    • __KBUILD_MODNAME:当前正在编译的模块的名称

    • __COUNTER__:一个每次使用都会递增计数器,用于确保生成名称的唯一性

    • __LINE__:当前代码的行号

 #define __unique_initcall(fn, id, __sec, __iid)         \
     ____define_initcall(fn, \
         __initcall_stub(fn, __iid, id), \
         __initcall_name(initcall, __iid, id), \
         __initcall_section(__sec, __iid))
 ​
 #define ____define_initcall(fn, __unused, __name, __sec) \
     static initcall_t __name __used \
         __attribute__((__section__(__sec))) = fn;
 ​
 #define __initcall_stub(fn, __iid, id) fn
 ​
 /* Format: __<prefix>__<iid><id> */
 #define __initcall_name(prefix, __iid, id) \
     __PASTE(__, \
     __PASTE(prefix, \
     __PASTE(__, \
     __PASTE(__iid, id))))
 ​
 #define __initcall_section(__sec, __iid) \
     #__sec ".init"

__unique_initcall:调用____define_initcall,关键实现部分

____define_initcall:定义一个名为 __nameinitcall_t 类型的静态变量,并将其初始化为 fn,并放入特定的__sec段中。

  • __initcall_stub:表示唯一的函数名fn

  • __initcall_name:表示一个唯一的变量名

  • __initcall_section: 生成一个唯一的段名。

  • #__sec ".init":将两个字符串拼接起来,比如:__sec=.initcall6,拼接后的段为:.initcall6.init,该段为最终存储的段。

字段通过链接器链接起来,形成一个列表进行统一管理。

这些字段我们可以在arch/arm/kernel/vmlinux.lds中查看。

......
__initcall6_start = .; KEEP(*(.initcall6.init)) KEEP(*(.initcall6s.init))
......

3、module_exit函数解析

module_exitmodule_init的实现机制几乎没有差别,下面就简单介绍一下。

3.1 module_exit

#ifndef MODULE

/**
* module_exit() - driver exit entry point
* @x: function to be run when driver is removed
*
* module_exit() will wrap the driver clean-up code
* with cleanup_module() when used with rmmod when
* the driver is a module. If the driver is statically
* compiled into the kernel, module_exit() has no effect.
* There can only be one per module.
*/
#define module_exit(x) __exitcall(x);

......

#else /* MODULE */

......

/* 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))); \
___ADDRESSABLE(cleanup_module, __exitdata);

......

#endif

函数名称module_exit

文件位置include/linux/module.h

3.1.1 模块方式

作为模块方式,与module_init的实现方式一样,定义cleanup_moduleexitfn函数相关联,存放在__exitdata段内。

3.1.2 内建方式

当模块编译进内核时,MODULE宏未被定义,所以走下面流程

#define module_exit(x)	__exitcall(x);

3.2 __exitcall

#define __exitcall(fn)						\
static exitcall_t __exitcall_##fn __exit_call = fn

#define __exit_call __used __section(".exitcall.exit")

函数名称__initcall

文件位置include/linux/init.h

函数解析:设备驱动卸载函数

__exitcall_##fn:定义一个新的 exitcall_t 类型的静态变量,并赋值为fn

__exit_call__used __section(".exitcall.exit"),定义该函数存储的段

4、扩展

还记得__define_initcall的定义吗?

#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)

不同的宏定义,被赋予了不同的调用等级,最后将不同的驱动初始化函数统一汇总到__initcallx_start字段统一管理,形成一个有序的列表。

这样,我们在内核中,按照顺序遍历这个列表,最后执行对应的模块初始化函数fn即可实现驱动的初始化。

 

【Linux API 揭秘】module_init与module_exit的更多相关文章

  1. Linux内核驱动将多个C文件编译成一个ko文件的方法——每一个C文件中都有module_init与module_exit

    以两个C文件为例: 将本该被分别编译成adc_device.ko和adc_driver.ko的adc_device.c.adc_driver.c编译成一个ko文件! 採用方法: 第一步.改动C文件 1 ...

  2. 封装获取网络信息Linux—API类

    封装获取网络信息Linux—API类 封装好的库: #ifndef NETINFORMATION_H #define NETINFORMATION_H #include <netdb.h> ...

  3. linux驱动 之 module_init解析 (上)【转】

    转自:https://blog.csdn.net/Richard_LiuJH/article/details/45669207 版权声明:本文为博主原创文章,未经博主允许不得转载. https://b ...

  4. linux内核驱动module_init解析(2)

    本文转载自博客http://blog.csdn.net/u013216061/article/details/72511653 如果了解过Linux操作系统启动流程,那么当bootloader加载完k ...

  5. linux内核驱动module_init解析(1)

    本文转载自博客http://blog.csdn.net/richard_liujh/article/details/45669207 写过linux驱动的程序猿都知道module_init() 这个函 ...

  6. My Linux API

    @图形界面与命令行界面切换 Linux预设提供了六个命令窗口终端机让我们来登录.默认我们登录的就是第一个窗口,也就是tty1,这个六个窗口分别为tty1,tty2 … tty6,你可以按下Ctrl + ...

  7. linux API函数大全

    获取当前执行路径:getcwd1. API之网络函数 WNetAddConnection 创建同一个网络资源的永久性连接 WNetAddConnection2 创建同一个网络资源的连接 WNetAdd ...

  8. Server版Linux命令提示符揭秘

            一直都在Ubuntu12.04和12.10  Desktop下玩.如今要在Centos6.3 Server版下做开发了,感觉还是非常不一样的. 克服一个有一个不顺利后,有那种站在山顶的 ...

  9. Linux API的fork()测试

    现在到docker的实施阶段, 其底层的namespace,cgroup应该深入了解了. 其调用的API也慢慢熟悉起来吧. #include <unistd.h> #include < ...

  10. [DPI][TCP] linux API的接口如何控制urgent包的收发

    做DPI,写协议栈的时候,处理到了urgent数据包.突然好奇应用层是如何控制发出urgent包的呢?而接收端又是如何知道,接受到了urgent包的呢? man 7 tcp,中有如下一段: TCP s ...

随机推荐

  1. GIS系统想要实现Cesium For Unreal的视觉效果是否有捷径可走?

    对于大多数GIS开发人员来说,CesiumJS都是比较熟悉的引擎,但是相比较Cesium For Unreal而言,CesiumJS的视觉效果就显得差强人意了,因此一些GIS开发人员对Cesium F ...

  2. libgdx摄像头的移动

    要知道,做一个游戏,摄像头是必不可少的.接下来,我将讲解libgdx里面摄像头的移动 2d摄像头OrthographicCamera也叫做正交相机 结果展示: 按上下左右是可以移动的 Orthogra ...

  3. ASR项目实战-数据

    使用机器学习方法来训练模型,使用训练得到的模型来预测语音数据,进而得到识别的结果文本,这是实现语音识别产品的一般思路. 本文着重介绍通用语音识别产品对于数据的诉求. 对数据的要求 训练集 相关要求,如 ...

  4. 用c++写 爱心图案

    绘制爱心曲线 现代数学的一个有趣的证明是 Georg Cantor 证明了有理数是可枚举的.在这篇博客中,我们将通过编程绘制一个简单而美丽的数学图形:爱心曲线. 爱心曲线代码 //爱心曲线 (x^2 ...

  5. C++ 观察者模式实现

    观察者模式 主体(被观察者)通知一个或多个观察者状态改变/数据更新/事件发生. 描述 C++ 实现观察者模式有几个要点: 观察者都有一个共同的抽象基类 Listener,定义了一个纯虚接口 OnNot ...

  6. 在Global Mapper中导入点的文本格式

    目录 有时候想在Global Mapper快速显示一个点的具体位置,来不及去创建一个具体的矢量文件.一个最快速的方式就是将这个点写在文本文件中导入: 13149831.629692005 281725 ...

  7. LeetCode 哈希表、映射、集合篇(242、49)

    242. 有效的字母异位词 给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词. 示例 1: 输入: s = "anagram", t = " ...

  8. antd5中文设定

    antd5中文设定 import zhCN from "antd/lib/locale/zh_CN" <ConfigProvider locale={zhCN} theme= ...

  9. 探索开源工作流引擎Azkaban在MRS中的实践

    摘要:本文主要介绍如何在华为云上从0-1搭建azkaban并指导用户如何提交作业至MRS. 本文分享自华为云社区<开源工作流引擎Azkaban在MRS中的实践>,作者:啊喔YeYe. 环境 ...

  10. head/reset/revert/rebase代码回滚全解:git提交记录的背后原理

    多人合作程序开发的过程中,我们有时会出现错误提交的情况,此时我们希望能撤销提交操作,让程序回到提交前的样子,操作有: 回退(reset):reset是彻底回退到指定的commit版本,该commit后 ...