【转】module_init机制的理解

本文详细剖析了Linux内核中的模块机制,包括module的两种运行方式:静态编译连接和动态加载。通过具体代码示例,讲解了module_init宏在不同编译方式下的实现原理,以及动态加载模块的加载过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

分析的很到位,对linux内核机制其中的module 的实现很详细的剖析。值的一看

我们在学习Linux驱动开发时,首先需要了解Linux的模块化机制(module),但是module并不仅仅用于支撑驱动的加载和卸载。一个最简单的模块例子如下:


 
  1. // filename: HelloWorld.c

  2.  
  3. #include <linux/module.h>

  4. #include <linux/init.h>

  5.  
  6. static int hello_init(void)

  7. {

  8. printk(KERN_ALERT "Hello World\n");

  9. return 0;

  10. }

  11.  
  12. static void hello_exit(void)

  13. {

  14. printk(KERN_ALERT "Bye Bye World\n");

  15. }

  16.  
  17. module_init(hello_init);

  18. module_exit(hello_exit);

  19. MODULE_LICENSE("Dual BSD/GPL");

  模块代码有两种运行方式,一是静态编译连接进内核,在系统启动过程中进行初始化;一是编译成可动态加载的module,通过insmod动态加载重定位到内核。这两种方式可以在Makefile中通过obj-y或obj-m选项进行选择。 
   
  而一旦可动态加载的模块目标代码(.ko)被加载重定位到内核,其作用域和静态链接的代码是完全等价的。所以这种运行方式的优点显而易见:

  1. 可根据系统需要运行动态加载模块,以扩充内核功能,不需要时将其卸载,以释放内存空间;
  2. 当需要修改内核功能时,只需编译相应模块,而不必重新编译整个内核。

因为这样的优点,在进行设备驱动开发时,基本上都是将其编译成可动态加载的模块。但是需要注意,有些模块必须要编译到内核,随内核一起运行,从不卸载,如 vfs、platform_bus等。

  那么同样一份C代码如何实现这两种方式的呢? 
  答案就在于module_init宏!下面我们一起来分析module_init宏。(这里所用的Linux内核版本为3.10.10) 
  定位到Linux内核源码中的 include/linux/init.h,可以看到有如下代码:


 
  1. #ifndef MODULE

  2. // 省略

  3. #define module_init(x) __initcall(x);

  4. // 省略

  5. #else

  6.  
  7. #define module_init(initfn) \

  8. int init_module(void) __attribute__((alias(#initfn)));

  9. // 省略

  10. #endif

  显然,MODULE 是由Makefile控制的。上面部分用于将模块静态编译连接进内核,下面部分用于编译可动态加载的模块。接下来我们对这两种情况进行分析。

方式一:#ifndef MODULE

代码梳理:


 
  1. #define module_init(x) __initcall(x);

  2. |

  3. --> #define __initcall(fn) device_initcall(fn)

  4. |

  5. --> #define device_initcall(fn) __define_initcall(fn, 6)

  6. |

  7. --> #define __define_initcall(fn, id) \

  8. static initcall_t __initcall_##fn##id __used \

  9. __attribute__((__section__(".initcall" #id ".init"))) = fn

  即 module_init(hello_init) 展开为:


 
  1. static initcall_t __initcall_hello_init6 __used \

  2. __attribute__((__section__(".initcall6.init"))) = hello_init

  这里的 initcall_t 是函数指针类型,如下:  

typedef int (*initcall_t)(void);

  GNU编译工具链支持用户自定义section,所以我们阅读Linux源码时,会发现大量使用如下一类用法:

__attribute__((__section__("section-name"))) 

  __attribute__用来指定变量或结构位域的特殊属性,其后的双括弧中的内容是属性说明,它的语法格式为:__attribute__ ((attribute-list))。它有位置的约束,通常放于声明的尾部且“ ;” 之前。 
  这里的attribute-list为__section__(“.initcall6.init”)。通常,编译器将生成的代码存放在.text段中。但有时可能需要其他的段,或者需要将某些函数、变量存放在特殊的段中,section属性就是用来指定将一个函数、变量存放在特定的段中。

  所以这里的意思就是:定义一个名为 __initcall_hello_init6 的函数指针变量,并初始化为 hello_init(指向hello_init);并且该函数指针变量存放于 .initcall6.init 代码段中。


  接下来,我们通过查看链接脚本( arch/$(ARCH)/kernel/vmlinux.lds.S)来了解 .initcall6.init 段。 
  可以看到,.init段中包含 INIT_CALLS,它定义在include/asm-generic/vmlinux.lds.h。INIT_CALLS 展开后可得:


 
  1. #define INIT_CALLS \

  2. VMLINUX_SYMBOL(__initcall_start) = .; \

  3. *(.initcallearly.init) \

  4. INIT_CALLS_LEVEL(0) \

  5. INIT_CALLS_LEVEL(1) \

  6. INIT_CALLS_LEVEL(2) \

  7. INIT_CALLS_LEVEL(3) \

  8. INIT_CALLS_LEVEL(4) \

  9. INIT_CALLS_LEVEL(5) \

  10. INIT_CALLS_LEVEL(rootfs) \

  11. INIT_CALLS_LEVEL(6) \

  12. INIT_CALLS_LEVEL(7) \

  13. VMLINUX_SYMBOL(__initcall_end) = .;

进一步展开为:


 
  1. __initcall_start = .; \

  2. *(.initcallearly.init) \

  3. __initcall0_start = .; \

  4. *(.initcall0.init) \

  5. *(.initcall0s.init) \

  6. // 省略1、2、3、4、5

  7. __initcallrootfs_start = .; \

  8. *(.initcallrootfs.init) \

  9. *(.initcallrootfss.init) \

  10. __initcall6_start = .; \

  11. *(.initcall6.init) \

  12. *(.initcall6s.init) \

  13. __initcall7_start = .; \

  14. *(.initcall7.init) \

  15. *(.initcall7s.init) \

  16. __initcall_end = .;

  上面这些代码段最终在kernel.img中按先后顺序组织,也就决定了位于其中的一些函数的执行先后顺序(__initcall_hello_init6 位于 .initcall6.init 段中)。.init 或者 .initcalls 段的特点就是,当内核启动完毕后,这个段中的内存会被释放掉。这一点从内核启动信息可以看到:
Freeing unused kernel memory: 124K (80312000 - 80331000)

  那么存放于 .initcall6.init 段中的 __initcall_hello_init6 是怎么样被调用的呢?我们看文件 init/main.c,代码梳理如下:


 
  1. start_kernel

  2. |

  3. --> rest_init

  4. |

  5. --> kernel_thread

  6. |

  7. --> kernel_init

  8. |

  9. --> kernel_init_freeable

  10. |

  11. --> do_basic_setup

  12. |

  13. --> do_initcalls

  14. |

  15. --> do_initcall_level(level)

  16. |

  17. --> do_one_initcall(initcall_t fn)

  kernel_init 这个函数是作为一个内核线程被调用的(该线程最后会启动第一个用户进程init)。 
  我们着重关注 do_initcalls 函数,如下:


 
  1. static void __init do_initcalls(void)

  2. {

  3. int level;

  4.  
  5. for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)

  6. do_initcall_level(level);

  7. }

函数 do_initcall_level 如下:


 
  1. static void __init do_initcall_level(int level)

  2. {

  3. // 省略

  4. for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)

  5. do_one_initcall(*fn);

  6. }

函数 do_one_initcall 如下:


 
  1. int __init_or_module do_one_initcall(initcall_t fn)

  2. {

  3. int ret;

  4. // 省略

  5. ret = fn();

  6. return ret;

  7. }

initcall_levels 的定义如下:


 
  1. static initcall_t *initcall_levels[] __initdata = {

  2. __initcall0_start,

  3. __initcall1_start,

  4. __initcall2_start,

  5. __initcall3_start,

  6. __initcall4_start,

  7. __initcall5_start,

  8. __initcall6_start,

  9. __initcall7_start,

  10. __initcall_end,

  11. };

  initcall_levels[] 中的成员来自于 INIT_CALLS 的展开,如“__initcall0_start = .;”,这里的 __initcall0_start是一个变量,它跟代码里面定义的变量的作用是一样的,所以代码里面能够使用__initcall0_start。因此在 init/main.c 中可以通过 extern 的方法将这些变量引入,如下:


 
  1. extern initcall_t __initcall_start[];

  2. extern initcall_t __initcall0_start[];

  3. extern initcall_t __initcall1_start[];

  4. extern initcall_t __initcall2_start[];

  5. extern initcall_t __initcall3_start[];

  6. extern initcall_t __initcall4_start[];

  7. extern initcall_t __initcall5_start[];

  8. extern initcall_t __initcall6_start[];

  9. extern initcall_t __initcall7_start[];

  10. extern initcall_t __initcall_end[];

  到这里基本上就明白了,在 do_initcalls 函数中会遍历 initcalls 段中的每一个函数指针,然后执行这个函数指针。因为编译器根据链接脚本的要求将各个函数指针链接到了指定的位置,所以可以放心地用 do_one_initcall(*fn) 来执行相关初始化函数。 
   
  我们例子中的 module_init(hello_init) 是 level6 的 initcalls 段,比较靠后调用,很多外设驱动都调用 module_init 宏,如果是静态编译连接进内核,则这些函数指针会按照编译先后顺序插入到 initcall6.init 段中,然后等待 do_initcalls 函数调用。

方式二:#else

相关代码:


 
  1. #define module_init(initfn) \

  2. static inline initcall_t __inittest(void) \

  3. { return initfn; } \

  4. int init_module(void) __attribute__((alias(#initfn)));

  __inittest 仅仅是为了检测定义的函数是否符合 initcall_t 类型,如果不是 __inittest 类型在编译时将会报错。所以真正的宏定义是:


 
  1. #define module_init(initfn) \

  2. int init_module(void) __attribute__((alias(#initfn)));

  因此,用动态加载方式时,可以不使用 module_init 和 module_exit 宏,而直接定义 init_module 和 cleanup_module 函数,效果是一样的。 
   
  alias 属性是 gcc 的特有属性,将定义 init_module 为函数 initfn 的别名。所以 module_init(hello_init) 的作用就是定义一个变量名 init_module,其地址和 hello_init 是一样的。 
   
  上述例子编译可动态加载模块过程中,会自动产生 HelloWorld.mod.c 文件,内容如下:


 
  1. #include <linux/module.h>

  2. #include <linux/vermagic.h>

  3. #include <linux/compiler.h>

  4.  
  5. MODULE_INFO(vermagic, VERMAGIC_STRING);

  6.  
  7. struct module __this_module

  8. __attribute__((section(".gnu.linkonce.this_module"))) = {

  9. .name = KBUILD_MODNAME,

  10. .init = init_module,

  11. #ifdef CONFIG_MODULE_UNLOAD

  12. .exit = cleanup_module,

  13. #endif

  14. .arch = MODULE_ARCH_INIT,

  15. };

  16.  
  17. static const char __module_depends[]

  18. __used

  19. __attribute__((section(".modinfo"))) =

  20. "depends=";

  可知,其定义了一个类型为 module 的全局变量 __this_module,成员 init 为 init_module(即 hello_init),且该变量链接到.gnu.linkonce.this_module 段中。


  编译后所得的 HelloWorld.ko 需要通过 insmod 将其加载进内核,由于 insmod 是 busybox 提供的用户层命令,所以我们需要阅读 busybox 源码。代码梳理如下:(文件 busybox/modutils/ insmod.c


 
  1. insmod_main

  2. |

  3. --> bb_init_module

  4. |

  5. --> init_module

  而 init_module 定义如下:(文件 busybox/modutils/modutils.c

#define init_module(mod, len, opts) syscall(__NR_init_module, mod, len, opts)

  因此,该系统调用对应内核层的 sys_init_module 函数。


  回到Linux内核源代码(kernel/module.c),代码梳理:


 
  1. SYSCALL_DEFINE3(init_module, ...)

  2. |

  3. -->load_module

  4. |

  5. --> do_init_module(mod)

  6. |

  7. --> do_one_initcall(mod->init);

  文件(include/linux/syscalls.h)中,有:

#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)

  从而形成 sys_init_module 函数。

<think>好的,我需要回答用户关于Linux内核模块开发中module_platform_driver和module_init的区别的问题。首先,我得回忆一下这两个宏的用途和它们在内核中的作用。 用户之前问过内核模块中局部变量大小的限制,现在向驱动初始化的宏,可能需要更深入的驱动开发知识。用户可能是在学习编写设备驱动,遇到了这两个宏,不清楚何时使用哪一个,或者它们的内部机制有何不同。 首先,module_init是一个常见的宏,用于指定模块的初始化函数。当模块被加载时(比如通过insmod),这个函数会被调用。而module_platform_driver似乎与平台设备驱动相关,可能用于简化平台驱动的注册流程。 接下来,我需要详细对比这两个宏的定义和用途。module_init是通用的,适用于任何类型的模块初始化,而module_platform_driver可能特定于平台设备驱动,可能封装了更多的步骤,比如注册platform_driver结构体,自动处理probe和remove函数。 可能需要查看内核源码中的宏定义。比如,module_platform_driver可能展开后包含了module_initmodule_exit,同时自动注册和注销platform_driver。而使用module_init的话,开发者需要手动调用platform_driver_register和platform_driver_unregister。 另外,需要说明使用module_platform_driver的好处,比如代码更简洁,减少样板代码,避免手动编写初始化和退出函数。而module_init则更灵活,适用于非平台驱动的情况,或者需要自定义初始化流程的场景。 还要注意两者的参数不同。module_init的参数是一个函数指针,而module_platform_driver的参数是一个platform_driver结构体。这样,module_platform_driver可能在内部处理了这个结构体的注册,而module_init需要开发者自己处理。 此外,需要提到编译阶段的影响,比如使用module_platform_driver是否会影响模块的依赖关系或初始化的顺序。可能还要指出在内核文档或源码中的具体位置,方便用户进一步查阅。 可能会有一些例子,比如用module_init时需要自己写init和exit函数,而用module_platform_driver则可以简化代码。例如,使用module_platform_driver时,只需要定义platform_driver结构体,并调用宏,而无需编写注册和注销的代码。 需要确保回答的结构清晰,先分别解释两者的定义和作用,再对比它们的区别,最后给出使用场景的建议。同时,使用代码示例来说明差异,这样用户更容易理解。 最后,检查是否有遗漏点,比如热插拔支持、设备树匹配等是否与这两个宏有关联。可能需要提到module_platform_driver更适合与设备树配合使用的平台驱动,而module_init更通用,适用于各种驱动类型。 总结时,强调module_platform_driver是对module_init的封装,专为平台驱动设计,简化代码,而module_init则是基础机制,适用于所有模块初始化。</think>--- ### 核心区别 `module_platform_driver` 和 `module_init` 是 Linux 内核驱动开发中用于模块初始化的两种不同机制,主要差异体现在 **设计层级** 和 **自动化程度** 上。 --- ### 1. `module_init`:基础初始化机制 #### 功能特性 - **通用模块入口**:适用于所有内核模块(驱动、文件系统等) - **手动管理**:需要开发者显式编写注册/注销逻辑 - **灵活性强**:可处理任意类型的初始化操作 #### 典型用法 ```c static int __init my_driver_init(void) { // 手动注册驱动 platform_driver_register(&my_platform_driver); return 0; } static void __exit my_driver_exit(void) { // 手动注销驱动 platform_driver_unregister(&my_platform_driver); } module_init(my_driver_init); module_exit(my_driver_exit); ``` --- ### 2. `module_platform_driver`:平台驱动专用宏 #### 功能特性 - **平台驱动专用**:专为 `platform_driver` 结构体设计 - **自动封装**:隐式处理注册/注销操作 - **代码简化**:通过宏展开生成标准初始化代码 #### 展开原理(以 Linux 5.x 内核为例) ```c #define module_platform_driver(__platform_driver) \ module_driver(__platform_driver, platform_driver_register, \ platform_driver_unregister) ``` 等价于自动生成: ```c static int __init my_driver_init(void) { return platform_driver_register(&my_platform_driver); } module_init(my_driver_init); static void __exit my_driver_exit(void) { platform_driver_unregister(&my_platform_driver); } module_exit(my_driver_exit); ``` --- ### 关键对比表 | 特性 | `module_init` | `module_platform_driver` | |---------------------|--------------------------------|------------------------------------| | **适用范围** | 所有内核模块 | 仅限平台驱动(`platform_driver`) | | **代码量** | 需要手动编写注册/注销代码 | 自动生成注册/注销逻辑 | | **错误处理** | 可自定义错误处理流程 | 使用标准错误返回机制 | | **维护性** | 需手动维护初始化顺序 | 自动保证初始化的正确性 | | **典型使用场景** | 非平台驱动、复杂初始化需求 | 标准平台驱动快速开发 | --- ### 使用建议 1. **优先使用 `module_platform_driver`** 当满足以下条件时: - 驱动类型为平台设备驱动 - 不需要特殊的初始化顺序控制 - 无需在注册前后执行额外操作 2. **选择 `module_init`** 当需要: ```c // 示例:需要初始化前配置硬件 static int __init custom_init(void) { hw_preconfigure(); int ret = platform_driver_register(&drv); if(ret) { hw_rollback(); return ret; } init_debugfs(); // 附加初始化 return 0; } ``` --- ### 底层实现分析 #### `module_platform_driver` 展开流程 ```c module_platform_driver(my_driver) ↓ 展开为 module_driver(my_driver, platform_driver_register, platform_driver_unregister) ↓ 进一步展开 static int __init my_driver_init(void) { \ return platform_driver_register(&my_driver); \ } \ module_init(my_driver_init); \ static void __exit my_driver_exit(void) { \ platform_driver_unregister(&my_driver); \ } \ module_exit(my_driver_exit); ``` #### 初始化顺序控制 - 通过 `module_init` 的 `__initcall` 层级: ```c #define module_init(x) __initcall(x, 6) ``` 内核启动时按优先级顺序执行初始化函数(1-7级,6为设备驱动级) --- ### 调试技巧 1. 查看初始化顺序: ```bash # 显示所有 __initcall 函数 cat /proc/kallsyms | grep __initcall ``` 2. 动态调试驱动加载: ```bash # 加载时显示初始化函数地址 insmod my_driver.ko dyndbg=+p ``` --- ### 兼容性注意事项 | 内核版本 | `module_platform_driver` 可用性 | |------------------|--------------------------------| | < Linux 2.6.31 | 不可用 | | ≥ Linux 2.6.31 | 可用(推荐使用) | --- 通过合理选择这两种机制,开发者可以在代码简洁性和控制灵活性之间取得最佳平衡。对于标准平台驱动开发,`module_platform_driver` 能显著减少样板代码量并降低错误概率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值