module_init機制的理解

我們在學習Linux驅動開發時,首先需要了解Linux的模塊化機制(module),但是module並不僅僅用於支撐驅動的加載和卸載。一個最簡單的模塊例子如下:

// filename: HelloWorld.c
 
#include <linux/module.h>
#include <linux/init.h>
 
static int hello_init(void)
{
    printk(KERN_ALERT "Hello World\n");
    return 0;
}
 
static void hello_exit(void)
{
    printk(KERN_ALERT "Bye Bye World\n");
}
 
module_init(hello_init);
module_exit(hello_exit);
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,可以看到有如下代碼:

#ifndef MODULE
// 省略
#define module_init(x)  __initcall(x);
// 省略
#else
 
#define module_init(initfn) \
    int init_module(void) __attribute__((alias(#initfn)));
// 省略
#endif

 顯然,MODULE 是由Makefile控制的。上面部分用於將模塊靜態編譯連接進內核,下面部分用於編譯可動態加載的模塊。接下來我們對這兩種情況進行分析。

方式一:#ifndef MODULE

代碼梳理:

#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 段。
  可以看到,.init段中包含 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 函數調用。

方式二:#else

相關代碼:

#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_modulecleanup_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 函數。

模塊釋放

init/main.c中
 
start_kernel
|
--> rest_init
    |
    --> kernel_init
        |
        -->free_initmem

 

 

發佈了9 篇原創文章 · 獲贊 1 · 訪問量 1094
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章