文档章节

linux内核之模块moudle_init与moudle_exit原理

黑客画家
 黑客画家
发布于 2017/05/31 10:15
字数 2187
阅读 179
收藏 0

前言

      我们在开发内核驱动时,其实就是在开发一个内核的模块(module),任何一个模块都会存在一个初始化加载函数和卸载函数,那么它们是在什么时候被调用的呢?模块在静态链接形式和动态加载形式时它们的调用又有什么区别呢?本文就来解答这些问题。

      任何一个内核模块都至少会存在以下结构:

#include <linux/module.h>
#include <linux/init.h>

static int __init hello_init(void)
{
    printk(KERN_ALERT "Hello World\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_ALERT "Bye Bye World\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");

      在此模块中,hello_init和hello_exit是模块的加载和卸载函数。其中,__init和__exit都是宏,它们封装了内核使用的编译器属性,被它们修饰的函数都会被编译器放在特定的段中,比如__init修饰的函数会被放在.init.text段中,这个段中的函数在初始化阶段完成之后会被清除,以减少不必要的内存占用。他们的定义在/include/linux/init.h

/* These are for everybody (although not all archs will actually
   discard it in modules) */
#define __init		__section(.init.text) __cold notrace
#define __initdata	__section(.init.data)
#define __initconst	__constsection(.init.rodata)
#define __exitdata	__section(.exit.data)
#define __exit_call	__used __section(.exitcall.exit)

      __init和__exit都是可选的,但是module_init与module_exit是必须添加的,只有它们才能最终导致加载函数和卸载函数被调用。

模块

      模块代码有两种运行方式,一是静态编译链接进内核,在系统启动过程中进行加载初始化;一是编译成可动态加载的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,可以看到有如下代码:

#ifndef MODULE
// 省略
#define module_init(x)  __initcall(x);
// 省略
#else

#define module_init(initfn) \
    int init_module(void) __attribute__((alias(#initfn)));
// 省略
#endif

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

静态链接模式

#define module_init(x)  __initcall(x);
|
--> #define __initcall(fn) device_initcall(fn)
    |
    --> #define device_initcall(fn)     __define_initcall(fn, 6)
        |
        --> #define __define_initcall(fn, id) \
                static initcall_t __initcall_##fn##id __used \
                __attribute__((__section__(".initcall" #id ".init"))) = fn

      即 module_init(hello_init) 展开为:

static initcall_t __initcall_hello_init6 __used \
    __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 段。 以/arch/arm/kernel/vmlinux.lds.S为例,存在一个.init.data段:

	.init.data : {
		INIT_DATA
		INIT_SETUP(16)
		INIT_CALLS
		CON_INITCALL
		SECURITY_INITCALL
		INIT_RAM_FS
	}

   可以看到,.init.data段中包含 INIT_CALLS,它定义在/include/asm-generic/vmlinux.lds.h。INIT_CALLS 展开后可得:

#define INIT_CALLS							\
		VMLINUX_SYMBOL(__initcall_start) = .;			\
		*(.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)					\
		VMLINUX_SYMBOL(__initcall_end) = .;

     进一步展开为:

        __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中按先后顺序组织,也就决定了位于其中的一些函数的执行先后顺序(__initcall_hello_init6 位于 .initcall6.init 段中)。.init 或者 .initcalls 段的特点就是,当内核启动完毕后,这个段中的内存会被释放掉。这一点从内核启动信息可以看到:

Freeing unused kernel memory: 124K (80312000 - 80331000)

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

start_kernel
|
--> rest_init
    |
    --> kernel_thread
        |
        --> kernel_init
            |
            --> kernel_init_freeable
                |
                --> do_basic_setup
                    |
                    --> do_initcalls
                        |
                        --> do_initcall_level(level)
                            |
                            --> do_one_initcall(initcall_t fn)

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

static void __init do_initcalls(void)
{
    int level;

    for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
        do_initcall_level(level);
}

      函数 do_initcall_level 如下: 

static void __init do_initcall_level(int level)
{
    // 省略
    for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
        do_one_initcall(*fn);
}

      函数 do_one_initcall 如下:

int __init_or_module do_one_initcall(initcall_t fn)
{
    int ret;
    // 省略
    ret = fn();
    return ret;
}

      initcall_levels 的定义如下:

static initcall_t *initcall_levels[] __initdata = {
    __initcall0_start,
    __initcall1_start,
    __initcall2_start,
    __initcall3_start,
    __initcall4_start,
    __initcall5_start,
    __initcall6_start,
    __initcall7_start,
    __initcall_end,
};

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

extern initcall_t __initcall_start[];
extern initcall_t __initcall0_start[];
extern initcall_t __initcall1_start[];
extern initcall_t __initcall2_start[];
extern initcall_t __initcall3_start[];
extern initcall_t __initcall4_start[];
extern initcall_t __initcall5_start[];
extern initcall_t __initcall6_start[];
extern initcall_t __initcall7_start[];
extern initcall_t __initcall_end[];

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

动态加载模式

#define module_init(initfn)                 \
    static inline initcall_t __inittest(void)       \
    { return initfn; }                  \
    int init_module(void) __attribute__((alias(#initfn)));

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

#define module_init(initfn)                 \
    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 文件,内容如下:

#include <linux/module.h>
#include <linux/vermagic.h>
#include <linux/compiler.h>

MODULE_INFO(vermagic, VERMAGIC_STRING);

struct module __this_module
__attribute__((section(".gnu.linkonce.this_module"))) = {
    .name = KBUILD_MODNAME,
    .init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
    .exit = cleanup_module,
#endif
    .arch = MODULE_ARCH_INIT,
};

static const char __module_depends[]
__used
__attribute__((section(".modinfo"))) =
"depends=";

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

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

insmod_main
|
--> bb_init_module
    |
    --> 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),代码梳理:

SYSCALL_DEFINE3(init_module, ...)
|
-->load_module
    |
    --> do_init_module(mod)
        |
        --> do_one_initcall(mod->init);

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

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

      从而形成 sys_init_module 函数。

相关文章

从glibc源码看系统调用原理

© 著作权归作者所有

共有 人打赏支持
黑客画家
粉丝 133
博文 199
码字总数 515002
作品 0
杭州
高级程序员
私信 提问
高通平台怎样不用设备树 强制设置一个gpio端口

高通平台怎样不用设备树 强制设置一个gpio端口 ---------------------------------------------------------------------- #define VIBCONTROLGPIO 46 gpiodirectionoutput(VIBCONTROLGPIO,......

qq_34040053
04/23
0
0
proc文件系统

今天是端午节,本来想些点东西,可最近压力太大了,连msn 的blog都没有时间写,学习的时候,可以坐在那里一天不站起来,可以熬几个通宵,可是代码隔上四五天不写,就会颓废掉,再写的话感觉要...

晨曦之光
2012/04/13
176
0
WEB服务器-Nginx之虚拟主机、日志、认证及优化

WEB服务器-Nginx之虚拟主机、日志、认证及优化 概述 Nginx ("engine x") 是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP服务器。Nginx是由Igor Sysoev为俄罗斯访问量第二的Ram...

于学康
2017/05/25
0
0
Linux内核模块开发之Hello world

今年打算好好学学Linux下内核的开发,不过万事开头难,第一步当然是学着编译内核,试验了好多次都是失败告终,昨天终于第一次在虚拟机上升级内核成功,今天就先来试着写个hello world的程序,...

红薯
2010/05/04
2.6K
4
module_init和module_exit

转自:http://blog.csdn.net/hudashi/article/details/7080071 本文参照:http://blog.163.com/xinbuqianjin@126/blog/static/167563447201010221231507/ http://www.360doc.com/content/11......

chengyang
2012/03/12
0
0

没有更多内容

加载失败,请刷新页面

加载更多

开源软件会被云杀死吗 ?

本文转载云头条,原作者:Michael Stiefel是Reliable Software公司的负责人,是一名软件架构和开发顾问。 文章要点 虽然开源开发不会消失,但商业开源厂商的未来不是很有希望。随着全面管理的...

linuxCool
26分钟前
1
0
OSChina 周三乱弹 —— 谈什么对象?睡什么觉?

Osc乱弹歌单(2018)请戳(这里) 【今日歌曲】 @胖达panda :最肯忘却古人诗,最不屑一顾是相思。分享童丽的单曲《红豆生南国》: 《红豆生南国》- 童丽 手机党少年们想听歌,请使劲儿戳(这...

小小编辑
30分钟前
81
5
stylus

stylus基础教程,stylus实例教程,stylus语法总结

miaojiangmin
今天
3
0
PHP生成CSV之内部换行

当我们使用PHP将采集到的文件内容保存到csv文件时,往往需要将采集内容进行二次过滤处理才能得到需要的内容。比如网页中的换行符,空格符等等。 对于空格等处理起来都比较简单,这里我们单独...

豆花饭烧土豆
今天
2
0
使用 mjml 生成 thymeleaf 邮件框架模板

发邮件算是系统开发的一个基本需求了,不过搞邮件模板实在是件恶心事,估计搞过的同仁都有体会。 得支持多种客户端 支持响应式 疼彻心扉的 outlook 多数客户端只支持 inline 形式的 css 布局...

郁也风
今天
8
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部