从本章开始引入所有关于模块和内核编程的基本概念,并编写一个完整模块来实践这些基本的概

念。


3.1 搭建测试环境


    由于所有测测试代码都是基于Ubuntu 14.04.2 Desktop的3.16.0-30-generic内核,所以最好是到

Linux官方网站去下载一份该版本内核的源代码。另外,建议在虚拟机里面安装你的Ubuntu桌面环境,这

样避免因为误操作造成硬件损坏或者重要数据丢失。

    更多测试环境搭建细节可参考博文:Ubuntu 14.04.2 + Vmware搭建Linux驱动开发环境


3.2 Hello World模块


    下面的代码是一个完整的Hello World模块。

/*
 * hello.c
 */

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

MODULE_LICENSE("Dual BSD/GPL");

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

static void hello_exit(void)
{
    printk(KERN_ALERT "Goodbye, cruel world\n");
}

module_init(hello_init);
module_exit(hello_exit);

该模块有两个函数,hello_init()在模块加载到内核时被调用,hello_exit()在模块从内核移除是被调用。

宏module_init和module_exit分别将函数hello_init()和hello_exit()放入两个特定的段(section)内,这样模块在被链接到内核时,内核能够知道它们分别是用于模块加载/移除的函数。而宏MODULE_LICENSE声明

了模块的许可权限,同时,如果不做许可声明,内核会有些抱怨。


3.3 编译和加载


3.3.1 编译模块

    代码编写完成后,接下来我们要编译它们。内核模块的编译有一套其自己定义的系统,先给出一个编译

的Makefile:

ifeq ($(KERNELRELEASE),)

KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

modules:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
    
modules_install:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
    rm -rf *.o .depend .*.cmd *.ko *.mod.c .tmp_versions modules.*

else

obj-m := hello.o

endif

接下来启动终端,运行make命令就可以编译模块了。

    在我们加载模块之前,先简单解释一下上面的Makefile。第一次接触上面的Makefile,会有些困惑,这

个Makefile会被读2遍:第一遍由于KERNELRELEASE变量没有定义,所以先运行前半部分的分支。Makefile的

前半部分定义了变量KERNELDIR、PWD,同时定义了3个目标规则,其中modules_install可以先不管它。直接

运行make命令会触发modules目标规则,切换到KERNELDIR目录,运行该目录下的Makefile。KERNELDIR目录

下的Makefile读取M参数所指定目录下的Makefile,也就是第二遍读取我们hello模块的Makefile,此时因为

KERNELRELEASE变量已定义,所以读取的是该Makefile的后半部分,也就是只有变量obj-m定义的部分,然后

KERNELDIR下的Makefile知道了要编译的模块名称以及目标文件,根据模块编译的一些预定义规则,继续完

成模块的编译,编译成功会生成一个名为hello.ko的文件,就是我们模块文件了。

    关于更多模块编译系统的细节,可以参考Linux内核源码文档目录Documentation/kbuild/*.*。内容并

不多,每个做模块开发的初学者,都有必要读一读。


3.3.2 模块加载和卸载

    使用insmod加载模块,在命令行中运行:insmod ./hello.ko,模块将被加载。此时可以用dmesg查看输

出,可以看到"Hello, world"的输出,同时可以使用lsmod查看检验模块是否已经加载。当模块使用完成

后,使用rmmod卸载模块,运行命令:rmmod hello完成模块的卸载,此时用dmesg查看输出,可以看到

"Goodbye, cruel world"的输出。

    模块加载的工具除insmod外,还有modprobe。

    模块的加载细节可参考kernel/module.c,sys_init_module函数调用vmalloc分配内存,将模块加载到

内存中,并根据内核全局符号表解析模块未定义的符号引用,最终会调用模块的初始化函数。

    注意,在Ubuntu系统下加载/卸载模块需要超级用户权限,可在命令前加上sudo来保证命令成功执行,

否则系统不会加载/卸载模块,并伴有权限不足的提示信息。


3.3.3 版本平台依赖

    模块需要为每一个目标运行平台重新编译,不同的内核版本,编译模块的编译器版本,硬件运行平台都

能导致模块不能正常加载和运行。

    Linux在linux/version.h中定义了一些宏来帮助解决版本依赖问题,linux/module.h会自动包含这个文

件。linux/version.h定义的关于版本的宏有:

    UTS_RELEASE:内核版本的字符串描述,如"2.6.10"。

    LINUX_VERSION_CODE:内核版本的十六进制描述,如0x02060a,即表示内核2.6.10。

    KERNEL_VERSION(major,minor,release):用来建立内核版本号,如KERNEL_VERSION(2,6,10)。

    另外,在编译模块时,会链接当前内核树中的vermagic.o文件。这个文件包含了目标内核版本,编译器

版本,以及其他重要配置信息。当内核加载模块时,会对这些信息做检验,如果不匹配,模块不会被加载,

并伴有错误提示信息。


3.4 内核符号表


    内核符号表包含全局内核函数和变量的地址,加载模块时利用这些信息解析未定义符号的引用。当模块

加载时,该模块导出全局符号(包括函数和变量)会被扩展到内核符号表,届时其他模块可以引用这些符

号。

    Linux内核提供一下两个宏来导出全局符号:

    EXPORT_SYMBOL(name);

    EXPORT_SYMBOL_GPL(name);

这两个宏的差别是GPL版本的宏导出的符号只能被GPL许可声明的模块引用。这两个宏导出的符号都会被放入

一个特殊的ELF段中。


3.5 预备知识


    模块许可:MODULE_LICENSE(license); 

              模块许可有GPL、GPL v2、Dual BSD/GPL、Dual MPL/GPL、Proprietary。

    模块作者:MODULE_AUTHOR(author);

    模块描述:MODULE_DESCRIPTION(desc);

    模块支持设备列表:MODULE_DEVICE_TABLE();


3.6 模块的初始化和退出


3.6.1 模块的启停

    模块的初始化和退出函数应当声明为static,因为它们不会被外部引用,但这不是必须的。初始化和退

出函数最好(但不是必须)加上__init和__exit修饰,如下:

static int __init init_func(void)
{
    ...
}

static void __exit exit_func(void)
{
    ...
}

在加上__init修饰告诉系统在模块加载时完成该函数调用后可丢弃该函数以节省内存空间,而__exit修饰告

诉系统函数只在模块卸载是调用。另外,如果内核配置成模块不能被卸载,模块的退出函数被简单忽略。


3.6.2 模块加载竞态

    一旦你对内核注册了你的模块(如cdev_add调用完成),模块的功能就可能开始被别的程序调用,这就

意味着你必须在对内核注册模块完成所有必须的初始化。


3.7 模块参数


    Linux模块机制允许你在模块加载时向模块传递参数。先看一个例子:

/*
 * hellop.c
 */

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

MODULE_LICENSE("Dual BSD/GPL");

static char *whom = "world";
static int howmany = 1;

static int hello_init(void)
{
    int i;
    
    for (i = 0; i < howmany; i++)
        printk(KERN_ALERT "Hello, %s\n", whom);
    return 0;
}

static void hello_exit(void)
{
    printk(KERN_ALERT "Goodbye, %s\n", whom);
}

module_param(whom, charp, S_IRUGO);
module_param(howmany , int, S_IRUGO);

module_init(hello_init);
module_exit(hello_exit);

Makefile如下:

ifeq ($(KERNELRELEASE),)

KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

modules:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
    
modules_install:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
    rm -rf *.o .depend .*.cmd *.ko *.mod.c .tmp_versions modules.*

else

obj-m := hellop.o

endif

在命令行运行make完成编译,成功后生成hellop.ko。此时模块的加载命令稍有不同,在命令行下输入:

insmod ./hellop.ko whom="XXX" howmany=3

此后用dmesg查看输出,可以看到3个同样的字串"Hello, XXX"。最后用rmmod hellop移除模块,用dmesg可

以看到输出信息"Goodbye, XXX"。

    从上面可以看到,模块参数实际上利用宏module_param(name, type, perm),该宏的参数解释如下:

    name:模块参数的名称,如上例中的whom,howmany;

    type:模块参数的类型,有bool,invbool,charp,int,uint,long,ulong,short,ushort;

    perm:参数访问权限,定义在linux/stat.h中,有S_IRUGO,S_IWUSR等。

    module_param宏是用于单个模块参数,另外还有宏module_array_param(name, type, num, perm);可用

于模块数组参数,其中除参数num表示数组元素个数外,其他3个参数含义同宏module_param。


3.8 用户空间模块


    除可在内核态下编写模块外,还可以编写用户空间编写模块,这样的模块通常是驱动程序,利用mmap之

类的系统调用完成设备地址映射,使得在用户空间可以直接访问设备寄存器,设备内存以达到驱动设备的目

的。

 

参考资料:

《Linux Device Drivers》第三版。