Linux内核模块

Author Avatar
thychan Dec 03, 2016
  • Read this article on other devices

主要内容

  • 内核模块概述
  • 内核模块编程
  • 内核模块机制的实现
  • 实验
    • 通过内核模块显示进程控制块信息

内核模块概述

内核模块

  • 核心空间运行的一种目标文件,不能单独执行但其代码可在运行时链接到系统中作为内核的一部分执行或卸载。

    –内核模块是一种特有的机制,它由一组函数和数据结构组成,可作为独立程序来编译。

  • 当模块被安装时,它被链接到内核中。

    –可在系统启动时进行模块安装,称静态加载;也可在系统运行时进行模块安装,称动态加载。

内核模块的主要作用

  • 动态地增加或减少内核功能。

    – Loadable Kernel Modules(LKM)机制实现系统运行时对内核功能的动态扩充。

    –许多情况下用户需要增加内核态程序。

  • 提高单内核操作系统的灵活性与可扩展性。

Linux内核模块

  • 是一个编译好的、具有特定格式的独立目标文件,用户可通过系统提供的一组与模块相关的命令将内核模块加载进内核,当内核模块被加载后,它有以下特点:

    – 与内核一起运行在相同的内核态和内核地址空间;

    – 运行时具有与内核同样的特权级;

    – 可方便地访问内核中的各种数据结构。

  • 被载入内核的内核模块代码与静态编译进内核的代码没有区别,内核模块与内核中的其他模块交互只需采用函数调用。

  • 内核模块可以很容易地被移出内核,当用户不再需要某功能模块时,可以将它从内核卸载以节省系统主存开销,配置十分灵活。

  • Linux内核需要对载入的内核模块进行管理,管理内核模块主要有两项任务:

    –内核符号表管理;

    –维护内核模块的引用计数。

  • 内核将资源登记在符号表中,当内核模块被加载后,模块可以通过符号表使用内核中的资源。

    –新模块载入内核时,系统把新模块提供的符号加进符号表中,这样新载入模块就可访问已装载模块提供的资源;

  • 在卸载一个模块时,系统释放分配给该模块的所有系统资源,如内核主存区等, 同时将该模块提供的符号从符号表中删除。

  • 由于所有内核模块在加载后都在同一地址空间中,内核模块之间可相互引用各自导出的符号,因此内核模块之间会产生依赖性:

    – 如果A模块需要用到B模块导出的符号,而B模块没有被载入内核的话,A模块的加载就会出错;

    – 如果有其他内核模块引用一个内核模块导出的符号,内核不允许该内核模块被卸载,内核模块的引用计数器便用来管理内核模块之间的依赖性。

    – 如果一个内核模块被依赖,它的引用计数就会增加;当依赖减少时,相应的引用计数也会减少;一个内核模块只有在引用计数为0时候才允许被卸载。

内核模块编程

内核模块的结构

  • 编写“Hello,world!”内核模块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <linux/init.h>
    #include <linux/module.h>
    MODULE LICENSE("GPL");
    static int hello_init(void)
    {
    printk(KERN_ALERT "Hello, world!\n");
    return 0;
    }
    static void hello_exit(void)
    {
    printk(KERN_ALERT "Goodbye, world!\n");
    }
    module_init(hello_init);
    module_exit(hello_exit);

编译与加载

  • 在v2.6中,编译、链接后生成的内核模块后缀为.ko,

  • 编译内核模块的makefile只需要下面一行:

    1
    obj-m:=hello.o
  • 生成的内核模块为hello.ko

  • 如果需要生成一个名为mymodule.ko的内核模块,并且该内核模块的源代码来源于modulesrc1.c和modulesrc2.c两个文件,makefile应该写成如下形式:

    1
    2
    obj-m:=mymodule.o
    module-objs:=modulesrc1.omodulesrc2.o
  • 如果用户采用makefile,在调用make命令时,需要将内核源代码所在目录作为一个参数传递给make命令。

    –例如,如果v2.6的内核源代码位于/usr/src/linux-2.6下,用户模块源代码所在目录应该使用的make命令为:

    1
    Make -C /usr/src/linux-2.6 M=$(shell pwd) modules
  • 当编译好内核模块后,用户以超级用户身份就可将内核模块加载到内核中。

  • 内核提供modutils软件包供用户对内核模块进行管理,该软件包安装后会在/sbin目录下安装insmod、rmmod、ksyms、lsmod、modprobe等实用程序。

  • insmod命令:把需要载入的模块以目标代码形式加载进内核中,insmod自动调用modules_init( )函数中定义的过程运行,超级用户使用这个命令,其格式为:

    1
    #insmod [path]–# modulename
  • rmmod命令:将已经载入内核的模块从内核中卸载,自动调用modules_exit()函数中定义的过程运行,命令格式为

    1
    #rmmod [path]modulename
  • ksyms命令:用来显示内核符号和模块符号信息

  • lsmod命令:显示已经载入内核的所有模块信息,包括被载入模块的模块名、大小和引用计数等,命令格式为:

    1
    #lsmod
  • 对于本节中的hello模块,超级用户可用以下命令加载模块:

    1
    #insmod hello.ko

内核符号表

  • 是一个用来存放所有模块可以访问的符号,以及对应地址的特殊数据结构,模块的链接是将模块插入到内核的过程,模块所导出的符号都将成为内核符号表的一部分。

  • 模块根据符号表从核心空间获取主存地址,从而确保在核心空间中正确地运行

  • 在v2.6内核中,用户可从/proc/kallsyms中以文本方式读取内核符号表。

  • 在v2.4内核中,缺省情况下,模块中的非静态全局变量及函数在模块加载后会输出到内核符号表,而在v2.6内核中,缺省情况下,这些符号不会被输出到内核符号表中。如果模块需要导出符号供其他模块使用,应该使用下面定义的两个宏:

    1
    2
    EXPORT_SYMBOL(name)
    EXPORT_SYMBOL_GPL(name)

初始化与清理函数

  • 内核模块必须调用宏module_init与module_exit去注册初始化与清理函数。初始化函数通常定义为:

    1
    2
    3
    4
    5
    staticint __init init_func(void)
    {
    /* 初始化代码*/
    }
    module_init(init_func);
  • 大部分模块都需要设置清理函数,该函数在模块卸载时被调用;如果一个模块没有定义清理函数,内核将不会允许它被卸载

  • 模块在清理函数中需要将已申请的资源归还给系统。清理函数的定义为:

    1
    2
    3
    4
    5
    staticvoid__exit exit_func(void)
    {
    /* 清理代码*/
    }
    module_exit(exit_func);

模块参数

  • 用户在加载内核模块之前,可能需要传递参数给内核模块,与内核模块进行简单的交互

  • 用户在insmod、modprobe命令中直接制定参数,命令形式为:

    1
    2
    #modprobe modname var=value
    //modname是要加载的模块名,var是要传递的变量名,value是传递的参数值。
  • 如果用户每次加载模块时传递的参数都相同,可以在/etc/modprobe.conf配置文件中预先写入参数,每次当模块被加载时,该配置文件中的参数就会被自动传递给模块。

  • 模块要使用用户传递的参数,应该采用以下定义的宏:

    1
    2
    module_param(name,type,perm)
    module_param_array(name,type,nump,perm)
  • type指明参数类型,模块直接支持的参数类型有:

    – bool、invbool。bool为布尔型,invbool类型是颠倒的布尔类型,真值为false,假值为true。

    – charp。字符串指针,指针指向用户传入的字符串。

    – int、long、short、uint、ulong、ushort。基本变长整型值,以u开头的是无符号值。

  • perm为参数在sysfs文件系统中所对应的文件节点的属性。

    模块被加载后,在/sys/module/目录下将出现以模块名命名的目录,如果此模块存在perm不为0的命令行参数则在此模块的目录下将出现parameters目录,包含一系列以参数名命名的文件节点,这些文件的权限值等于perm,文件的内容为参数的值。

  • nump为一个指针

    该指针所指的变量保存输入的数组元素个数,当不需保存实际输入的数组元素个数时,该指针可设置为NULL。用户传递数组参数给模块时,使用逗号分隔输入的数组元素设定好nump后,如果用户传递的数组大小大于指定的nump,模块加载者会拒绝加载更多的数组元素。

内核模块机制的实现

模块在内核中的表示

  • 内核在管理模块时使用的管理数据结构为structmodule,每一个内核模块被载入时,都要为其分配一个module对象,用一个双向链表把所有module对象组织起来,该链表的第1个元素为modules,开发者能够通过该元素依次访问内核中所有的module对象。

  • 内核通过module对象主要是为了记录模块的依赖,并进行模块导出符号的管理。

    | 类型 | 成员名 | 作用 |
    | :————————–: | :———-: | :————-: |
    | enum module_stat | stat | 模块当前状态 |
    | char [60] | nam | 模块名 |
    | const struct kernel_symbol | syms | 导出符号数组 |
    | unsigned int | num_syms | 导出符号数组大小 |
    | const struct kernel_symbol
    | gpl_syms | GPL导出符号数组 |
    | unsigned int | num_gpl_syms | GPL导出符号数组大小 |
    | struct module_ref | ref[NR_CPUS] | 每个CPU上对该模块的引用计数 |

  • stat成员表明当前模块的状态:MODULE_STATE_LIVE(处于激活状态), MODULE STATE COMING(正在被初始化状态), MODULE_STATE_GOING(正在被卸载状态)。

  • 每个module对象都包含有多个引用计数,每个CPU都有一个引用计数。

    – 每当模块被使用时,模块的引用计数加1;当模块不被使用时,模块的引用计数就会相应减少,仅当引用计数为0时,该模块才能被内核卸载。

    – 例如,假设一个MS-DOS的文件系统被编译成为模块,当模块被加载后,模块的引用计数为0;当用户用mount命令挂上一个MS-DOS的分区后,模块引用计数就变成1;只有在用户umount后,引用计数变成0,该模块才能被卸载。

  • 在内核代码段中,有3个段保存导出符号相关信息:

    kstrtab保存导出符号的名字;

    – ksymtab保存供所有模块使用的符号的地址;

    – __ksymtabgpl保存仅供GPL协议模块使用的符号的地址。

    – 只有被EXPORT_SYMBOL和EXPORTSYMBOL GPL宏导出的符号才会被C编译器写入内核代码的相应段中,在加载内核时,会根据代码段中的符号信息创建符号表。

模块的加载与卸载

  • 模块的加载

    – 用户通过insmod命令将模块载入内核,该命令的主要操作如下:

    ​ –从命令行读入要被载入的模块名。

    ​ –获得模块代码,它通常放在/lib/modules目录下。

    ​ –调用init_module( )函数,将包含模块代码缓存的指针、模块代码长度和用户参数传递给函数,该函 数将完成模块的加载工作。

  • 模块的卸载

    –用户可以通过rmmod命令将内核模块卸载,该命令所做的操作是:

    ​ 1)读取要被卸载的模块名。

    ​ 2)打开/proc/modules文件,查看该模块是否已经被卸载。

    ​ 3)调用delete_module( ),把模块名传递给该函数,该函数将完成模块的卸载工作。

实验:内核模块显示进程控制块信息

实验说明

  • 在内核中,所有进程控制块都被一个双向链表连接起来,该链表中的第1个进程控制块为init_task。
  • 编写一个内核模块,模块接收用户传递的一个参数num,num指定要打印的进程控制块的数量;若用户不指定num或者num<0,模块则打印所有进程控制块的信息。需要打印的进程控制块信息有:进程PID和进程的可执行文件名。

解决方案

  • 定义模块参数

    – 该模块需要接受用户传递的参数,在使用该参数之前,需要在代码中预先定义好该参数,该参数的类型设置为整型,在sysfs中的权限是只读的。定义的方法为:

    1
    2
    staticintnum=-1;
    module_param(num,int,S_IRUGO);

    – 该参数的初始值被设置为-1。-1将作为打印所有进程控制块的标记,默认值为-1,意味着当用户不传入任何参数时,模块将打印所有的进程的信息。

  • 访问进程控制块链表

    – 在内核中,进程控制块被组织成多个双向链表,其中有一个双向链表包含所有的进程控制块,只需要访问该双向链表,就可以访问到所有进程控制块。Linux内核中几乎所有双向链表都采用相同的数据结构来实现,内核中定义list_head通用数据结构,其定义如下:

    1
    2
    3
    Struct  list_head{
            structlist_head*next, * prev;
    };
  • 访问进程控制块链表

    – list head中,开发者可以通过内核提供的一组宏创建并操作一个双向链表,而该链表中元素的类型为该数据结构.

  • 访问进程控制块链表

    – 在进程控制块task struct中,包含一个名为tasks的成员,该成员的类型为list_head,通过该成员将进程控制块串成一个双向链表。Linux内核通过该成员将所有的进程都放入同一个双向链表。

    – 为了访问包含list_head的数据结构,内核提供一个宏:

    1
    list_entry(ptr,type,member);
  • 访问进程控制块链表

    – 在该宏中,ptr是一个指向list head的指针,type是包含list_head的数据结构类型,而member是list_head在该数据结构中的成员名。例如,若一个进程控制块中的tasks的地址为p,为了访问该进程控制块,可以采用:

    1
    2
    list_entry(p, structtask_struct,tasks) ;
    //该宏便会返回该进程控制块的地址。
  • 访问进程控制块链表

    – 如果得到一个进程控制块的地址p,开发者可以通过:

    1
    2
    list_entry(p->tasks.next,structtask_struct,tasks);
    //访问该双向链表中的下一个进程控制块。

    – 第1个进程控制块为init_task,如果开发者发现下一个进程控制块为init_task 时,说明已经完整地遍历过所有进程控制块。

  • 访问进程控制块链表

    – 内核定义宏for_each_process用于遍历所有的进程控制块,开发者通过该宏就能将所有的进程控制块访问一遍,该宏展开的形式为:

    1
    for(p = &init_task; (p = list_entry((p)->tasks.next,structtask_struct,tasks) !=&init_task; )
  • 输出进程控制块信息

    – 进程控制块中包含进程大部分信息,根据实验要求,模块需要打印进程的pid和可执行文件名,在进程控制块的数据结构中,成员pid为进程的PID,而成员comm包含进程的可执行文件名。在内核中,模块可以通过printk()内核函数将这些信息打印到系统日志中。