【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. 记一次 .NET某股票交易软件 灵异崩溃分析

    一:背景 1. 讲故事 在dump分析的旅程中也会碰到一些让我无法解释的灵异现象,追过这个系列的朋友应该知道,上一篇我聊过 宇宙射线 导致的程序崩溃,后来我又发现了一例,而这一例恰恰是高铁的 列控连锁 ...

  2. 你是否想知道如何应对高并发?Go语言为你提供了答案!

    并发编程是当前软件领域中不可忽视的一个关键概念.随着CPU等硬件的不断发展,我们都渴望让我们的程序运行速度更快.更快.而Go语言在语言层面天生支持并发,充分利用现代CPU的多核优势,这也是Go语言能够 ...

  3. MinIO客户端之alias

    MinIO提供了一个命令行程序mc用于协助用户完成日常的维护.管理类工作. 官方资料 mc alias mc alias list mc alias remove mc alias set mc al ...

  4. 国家专用加密数据传输之rsa,3des加密方法-rasor3desc

    title: 国家专用加密数据传输之rsa,3des加密方法 date: 2021-12-20 13:08:21.646 updated: 2022-03-10 16:00:58.65 url: ht ...

  5. Java 并发编程(三)锁与 AQS

    本文 JDK 对应的版本为 JDK 13 由于传统的 synchronized 关键字提供的内置锁存在的一些缺点,自 JDK 1.5 开始提供了 Lock 接口来提供内置锁不具备的功能.显式锁的出现不 ...

  6. poj 3987 Computer Virus on Planet Pandora —— ac自动机复习

    poj 3987 Computer Virus on Planet Pandora ac自动机复习 题意如下 给出多个模式串,最后给出一个文本串,求有多少个模式串被文本串包含或者被反序的文本串包含 几 ...

  7. php +libcurl+nghttp2 实现高性能微服务架构

    1.server端nginx编译时增加参数configure --with-http_v2_module     server {        listen       80 http2;      ...

  8. 看完这篇,DWS故障修复不再愁

    摘要:本文详细梳理分析了DWS服务面临软硬件故障场景和对应的修复原理,希望借此能够让你对DWS的集群故障修复有个全面深入的了解. 本文分享自华为云社区<GaussDB(DWS)故障修复系统性介绍 ...

  9. PPT 版面的规则和精髓

    排版四原则 对齐 对比 亲密 重复 背景图 案例

  10. Hugging News #0918: Hub 加入分类整理功能、科普文本生成中的流式传输

    每一周,我们的同事都会向社区的成员们发布一些关于 Hugging Face 相关的更新,包括我们的产品和平台更新.社区活动.学习资源和内容更新.开源库和模型更新等,我们将其称之为「Hugging Ne ...