【Linux】Linux基础知识(Linux模块)

Linux的内核模块机制允许开发者动态的向内核添加功能,我们常见的文件系统、驱动程序等都可以通过模块的方式添加到内核而无需对内核重新编译,这在很大程度上减少了操作的复杂度。模块机制使内核预编译时不必包含很多无关功能,把内核做到最精简,后期可以根据需要进行添加。

而针对驱动程序,因为涉及到具体的硬件,很难使通用的,且其中可能包含了各个厂商的私密接口,厂商几乎不会允许开发者把源代码公开,这就和linux的许可相悖,模块机制很好的解决了这个冲突,允许驱动程序后期进行添加而不合并到内核。OK,下面结合源代码讨论下模块机制的实现。

类似于普通的可执行文件,模块经过编译后得到.ko文件,其本身也是可重定位目标文件,类似于gcc -c 得到的.o目标文件。

关于可重定位目标文件的定义:可重定位目标文件(多位高手回答版,未综合)

既然是重定位文件,在把模块加载到内核的时候就需要进行重定位,回想下用户可执行文件的重定位,一般如果一个程序的可执行文件总能加载到自己的理想位置,所以对于用户可执行文件,一般不怎么需要重定位;而对于动态库文件就不同了,库文件格式是一致的,但是可能需要加载多个库文件,那么有些库文件必然无法加载到自己的理想位置,就需要进行重定位。而内核模块由于和内核共享同一个内核地址空间,更不能保证自己的理想地址不被占用,所以一般情况内核模块也需要进行重定位。在加载到内核时,还有一个重要的工作即使解决模块之间的依赖,模块A中引用了其他模块的函数,那么在加载到内核之前其实模块A并不知道所引用的函数地址,因此只能做一个标记,在加载到内核的时候在根据符号表解决引用问题!这些都是在加载内核的核心系统调用sys_init_module完成。


Linux内核模块

Linux模块是一些可以作为独立程序来编译的函数和数据类型的集合。之所以提供模块机制,是因为Linux本身是一个单内核。单内核由于所有内容都集成在一起,效率很高,但可扩展性和可维护性相对较差,模块机制可弥补这一缺陷。

Linux模块可以通过静态或动态的方法加载到内核空间,静态加载是指在内核启动过程中加载;动态加载是指在内核运行的过程中随时加载。一个模块被加载到内核中时,就成为内核代码的一部分。模块加载入系统时,系统修改内核中的符号表,将新加载的模块提供的资源和符号添加到内核符号表中,以便模块间的通信。

从代码的特征上来看,模块就是可以完成一个独立功能的一组函数的集合,但以特殊的方法来编译,从而使之可以在需要时随时安装,在不需要时随时卸载。它们扩展了操作系统内核功能却不需要重新编译内核、启动系统。

准确的说,模块就是一个已经编译但未经连接的可执行文件。

一个Linux 内核模块主要由以下几个部分组成: 

  • 模块加载函数(必须):当通过insmod命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块相关初始化工作;
  • 模块卸载函数(必须):当通过rmmod命令卸载模块时,模块的卸载函数会自动被内核执行,完成与模块加载函数相反的功能;
  • 模块许可证声明(必须):模块许可证(LICENCE)声明描述内核模块的许可权限,如果不声明LICENCE,模块被加载时将收到内核被污染的警告。大多数情况下,内核模块应遵循GPL 兼容许可权。Linux2.6 内核模块最常见的是以MODULE_LICENSE(“Dual BSD/GPL”)语句声明模块采用BSD/GPL 双LICENSE;
  • 模块参数(可选):模块参数是模块被加载的时候可以被传递给他的值,它本身对应模块内部的全局变量;
  • 模块导出符号(可选):内核模块可以导出符号(symbol,对应于函数或变量),这样其他模块可以使用本模块中的变量或函数;
  • 模块作者等信息声明(可选)。

一个内核模块至少包含两个函数,模块被加载时执行的初始化函数init_module()和模块被卸载时执行的析构函数delete_module()。前者为内核工作进行必要的硬件及软件的初始化工作;后者用来对内核的卸载做内存的释放等一些扫尾工作。在最新内核稳定版本2.6 中,两个函数可以起任意的名字,通过宏module_init()和module_exit()注册调用要编译内核模块,把代码嵌进内核空间,首先要获取内核源代码,且版本必需与当前正在运行的版本一致。


模块的内核描述

每一个内核模块在内核中都对应一个数据结构module,所有的模块通过一个链表维护。所以有些恶意模块企图通过从链表摘除结构来达到隐藏模块的目的。部分成员列举如下:

struct module
{
    enum module_state state;                                //状态

    /* Member of list of modules */
    struct list_head list;                                //所有的模块构成双链表,包头为全局变量modules

    /* Unique handle for this module */
    char name[MODULE_NAME_LEN];                        //模块名字,唯一,一般存储去掉.ko的部分

    /* Sysfs stuff. */
    struct module_kobject mkobj;
    struct module_attribute *modinfo_attrs;
    const char *version;
    const char *srcversion;
    struct kobject *holders_dir;

    /* Exported symbols *//**/
    const struct kernel_symbol *syms;                    //导出符号信息,指向一个kernel_symbol的数组,有num_syms个表项。
    const unsigned long *crcs;                        //同样有num_syms个表项,不过存储的是符号的校验和
    unsigned int num_syms;

    /* Kernel parameters. */
    struct kernel_param *kp;
    unsigned int num_kp;

    /* GPL-only exported symbols. */
    unsigned int num_gpl_syms;/                        /具体意义同上面符号,但是这里只适用于GPL兼容的模块
    const struct kernel_symbol *gpl_syms;
    const unsigned long *gpl_crcs;

#ifdef CONFIG_UNUSED_SYMBOLS
    /* unused exported symbols. */
    const struct kernel_symbol *unused_syms;
    const unsigned long *unused_crcs;
    unsigned int num_unused_syms;

    /* GPL-only, unused exported symbols. */
    unsigned int num_unused_gpl_syms;
    const struct kernel_symbol *unused_gpl_syms;
    const unsigned long *unused_gpl_crcs;
#endif

#ifdef CONFIG_MODULE_SIG
    /* Signature was verified. */
    bool sig_ok;
#endif

    /* symbols that will be GPL-only in the near future. */
    const struct kernel_symbol *gpl_future_syms;
    const unsigned long *gpl_future_crcs;
    unsigned int num_gpl_future_syms;

    /* Exception table */
    unsigned int num_exentries;
    struct exception_table_entry *extable;

    /* Startup function. */
    int (*init)(void);                            //模块初始化函数指针

    /* If this is non-NULL, vfree after init() returns */
    void *module_init;                            /如果该函数不为空,则init结束后就可以调用进行适当释放

    /* Here is the actual code + data, vfree'd on unload. */
    void *module_core;                            //核心数据和代码部分,在卸载的时候会调用

    /* Here are the sizes of the init and core sections */
    unsigned int init_size, core_size;            //对应于上面的init和core函数,决定各自占用的大小

    /* The size of the executable code in each section.  */
    unsigned int init_text_size, core_text_size;

    /* Size of RO sections of the module (text+rodata) */
    unsigned int init_ro_size, core_ro_size;
    ......

#ifdef CONFIG_MODULE_UNLOAD
    /*模块间的依赖关系记录*/
    /* What modules depend on me? */
    struct list_head source_list;
    /* What modules do I depend on? */
    struct list_head target_list;

    /* Who is waiting for us to be unloaded */
    struct task_struct *waiter;                    //等待队列,记录那些进程等待模块被卸载

    /* Destruction function. */
    void (*exit)(void);                            //卸载退出函数,模块中定义的exit函数

    ......
};

依赖关系

模块间的依赖关系通过两个节点source_list和target_list记录,前者记录那些模块依赖于本模块,后者记录本模块依赖于那些模块。节点通过module_use记录,module_use如下(定义在include/linux/module.h中):

struct module_use {
    struct list_head source_list;
    struct list_head target_list;
    struct module *source, *target;
};

每个module_use记录一个映射关系,注意这里把source和target放在一个一个结构里,因为一个关系需要在源模块和目标模块都做记录。如果模块A依赖于模块B,则生成一个module_use结构,其中source_list字段链入模块B的module结构的source_list链表,而source指针指向模块A的module结构。而target_list加入到模块A中的target_list链表,target指针指向模块B的模块结构,参考下面代码:

static int add_module_usage(struct module *a, struct module *b)
{
    struct module_use *use;

    pr_debug("Allocating new usage for %s.\n", a->name);
    use = kmalloc(sizeof(*use), GFP_ATOMIC);
    if (!use) {
        printk(KERN_WARNING "%s: out of memory loading\n", a->name);
        return -ENOMEM;
    }

    use->source = a;
    use->target = b;
    list_add(&use->source_list, &b->source_list);
    list_add(&use->target_list, &a->target_list);
    return 0;
}

符号信息

内核模块几乎不会作为完全独立的存在,均需要引用其他模块的函数,而这一机制就是由符号机制保证的。参考前面的module数据结构,在module结构体中:

const struct kernel_symbol *syms;                //导出符号信息,指向一个kernel_symbol的数组,有num_syms个表项。 
const unsigned long *crcs;                        //同样有num_syms个表项,不过存储的是符号的校验和 
unsigned int num_syms;

syms指针指向一个符号数组,也可以称之为符号表,不过是局部的符号表。看下kernel_symbol结构:

struct kernel_symbol
{
    unsigned long value;
    const char *name;
};

结构很简单,value记录符号地址,而name自然就是符号名字了。

参考文章:Linux下的内核模块机制

内核描述

每当内核需要使用这个模块提供的功能,就会到链表modules中寻找这个模块,并调用模块使用export修饰的功能函数。

module中,成员state为模块当前的状态。它是一个枚举类型的变量,可取的值为MODULE_STATE_LIVE、MODULE_STATE_COMING、MODULE_STATE_GOING,分为当前正常使用中(存活状态)、模块当前正在被加载和模块当前正在被卸载三种状态。

  • 当模块向内核加载时,insmod调用内核的模块加载函数,该函数在完成模块的部分创建工作后,将模块的状态置为MODULE_STATE_COMING。接着内核将调用内核模块初始化函数,并在完成所有的初始化工作之后(包括将模块加入模块注册表,调用模块本身的初始化函数),将模块状态设置为MODULE_STATE_LIVE。
  • 当使用rmmod命令卸载模块时,内核将调用系统调用delete_module,并将模块的状态置为MODULE_STATE_GOING。


Linux模块的实现机制及其管理

由前面的内容可知,从代码的特征上来看,模块是可完成一项独立功能的一组函数的集合;从使用特征上来看,它在需要时可以随时被安装,而在不需要时又可以随时被卸载。

从用户的角度上看,模块时内核的一个外挂的配件:需要时可将其挂接到内核上,以完成用户所要求的任务;不需要时即可将其删除。它给用户提供了扩充内核的手段;从内核的角度上看,模块由在运行时可连接并删除的、包括了至少2个函数的代码块。这个代码块一旦被连接到内核,它就可以是内核的一部分。总之,模块是一个为内核或其他内核模块提供使用功能的代码块。

在某种意义上来说,从可安装模块的角度来看,内核也是一个模块,只不过是大一些。所以说,模块就是一个已编译但未连接的可执行文件。既然把模块安装到了内核这个模块上并向内核提供服务,那么这些可模块就必须有与内核交互的手段。实现两个模块之间交互的最简单的手段就是实现双方的变量和函数的共享。

为了使被模块知道内核的哪些变量和函数是模块可以使用的,linux内核以“可移出”符号的形式提供了可供其他模块共享的变量和函数名称。这里所谓的“可移出”,是指外部可以引用,即是暴露在内核外面的符号。而模块在需要引用内核的一个“可移出”符号时,要把该符号用extern声明为外部引用。

为了使加载的各模块之间也可通过可移出符号进行交互,模块也可声明自己的移出符号,以供其他模块使用。所以这些移出符号可看做是内核与模块以及模块之间的信号通路,模块之间就是通过这些内核或模块的可移出符号实现交互的

但需要注意的是,模块可以引用内核及其他模块的可移出符号,而内核不能引用模块的可移出符号。也就是说,内核与模块之间的互连是一种“单向”的互连。 模块与内核之间连接示意图如图所示:



一个简单的内核模块程序

例程要求:编写一个模块,该模块含有一个初始化函数init_hello_module()和析构函数exit_hello_module()。在两个函数中,分别打印字符串。(注意:内核中打印语句时printk(),而不是printf()!)

hello.c模块代码

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

static int __init init_hello_module(void)                //__init进行注明
{
    printk("***************Start***************\n");
    printk("Hello World! Start of hello world module!\n");
    return 0;
}

static void __exit exit_hello_module(void)              //__exit进行注明
{
    printk("***************End***************\n");
    printk("Hello World! End of hello world module!\n");
}


MODULE_LICENSE("GPL");                             //模块许可证声明(必须要有)
module_init(init_hello_module);                    //module_init()宏,用于初始化
module_exit(exit_hello_module);                    //module_exit()宏,用于析构

Makefile文件

#Makefile**********************************************************
obj-m := hello.o
    KERNELBUILD:=/lib/modules/ $ (shell uname -r) /build 

在Makefile中,在obj-m := hello.o这句中,.o的文件名要与编译的.c文件名一致。 

模块加载和卸载的情况

  • .在Makefile及helloworld.c所在目录下,直接make,成功后查看当前目录下有无helloworld.ko文件产生,有则内核模块生成成功;
  • 使用insmod命令,把此内核模块程序加载到内核中运行。结合lsmod及管道命令,查看内核模块程序在内核中是否正确运行;
  • 查看此内核模块程序打印的信息,另开一个终端,输入tail -n /var/log/messages;
  • 使用rmmod命令把之前加载的内核模块卸载掉,然后再次执行第2步,即可看到此内核模块程序打印的信息。

猜你喜欢

转载自blog.csdn.net/qq_38410730/article/details/81022638