Linux内核模块开发

时间:2022-04-07 17:56:44

 模块功能

1.什么是内核模块

Linux内核的整体结构非常庞大,其包含的组件也非常多,如何使用需要的组件呢:

        方法一:把所有的组件都编译进内核文件,即zImage或bzImage。但这样会导致两个问题:一是生成的内核文件过大;二是要添加或删除某个组件,需要重新编译整个内核。

        有没有一种机制能让内核文件(zImage或bzImage)本身并不包含某组件,而是在该组件需要被使用的时候,动态的添加到正在运行的内核中呢?

        有,Linux提供了一种叫做"内核模块"的机制,就可以实现以上效果。

模块特点

内核模块具有如下特点:

(1)内核本身并不被编译进内核文件(zImage或bzImage);

(2)可以根据需要,在内核运行期间动态的安装或卸载;

范例(hello world):

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

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

static void __exit hello_exit()
{
 printk("<7>hello <0>exit\n");
}

module_init(hello_init);
module_exit(hello_exit);

程序结构

1.模块安装函数(必需)

       安装模块时被系统自动调用的函数,通过module_init宏来指定,如上hello world程序中module_init(hello_init);
为模块加载函数。

2.模块卸载函数(必需)

       卸载模块时被系统自动调用的函数,通过module_exit宏来指定,如上hello world程序中module_exit(hello_exit);为模块卸载函数。

模块的编译

       在Linux2.6下编译内核模块,通常使用makefile

       内核文件由一个源文件构成,该如何编写makefile呢?下面是上述hello world源程序的makefile程序代码:

 

ifneq ($(KERNELRELEASE),)

obj-m := hello.o

else

KDIR := /lib/modules/2.6.27.59/build
all:
 make -C $(KDIR) M=$(PWD) modules
clean:
 rm -f *.ko *.o *.mod.o *.mod.c *.symvers

endif

如果内核模块由多个源文件构成,该如何编写makefile。如一个内核模块由add.c和main.c两个源文件构成

add.c如下:

int add(int a, int b)
{
 return a+b;
}

main.c如下:

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

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Davide Yuan");
MODULE_DESCRIPTION("Hello World Module!");
MODULE_ALIAS("A simplest module");

extern int add(int a, int b);
static int __init hello_init()
{
 printk("Hello world!\n");
 add(1, 2);
 return 0;
}

static void __exit hello_exit()
{
 printk("<7>hello <0>exit\n");
}

module_init(hello_init);
module_exit(hello_exit);

则其makefile可以写为:

ifneq ($(KERNELRELEASE),)

obj-m := hello.o
hello-objs := main.o add.o

else

KDIR := /lib/modules/2.6.27.59/build
all:
 make -C $(KDIR) M=$(PWD) modules
clean:
 rm -f *.ko *.o *.mod.o *.mod.c *.symvers

endif

安装与卸载内核模块

1.加载insmod(如:insmod hello.ko)

2.卸载rmmod(如:rmmod hello)

3.查看lsmod

4.加载modprobe(如:modprobe hello):modprobe如同insmod,也是加载一个模块到内核。他的不同之处在于他会根据文件/lib/modules/$<version>/modules.dep来查看要加载的模块,看他是否还依赖于其他模块,如果是,modprobe会首先找到这些模块,把他们先加载进内核。

模块可选信息

1.许可证申明:宏MODULE_LISENSE用来告知内核,该模块带有一个许可证,没有这样的说明,加载模块时内核会抱怨。有效的许可证有:"GPL"、"GPLv2"、"GPL and additional rights"、"Dual BSD/GPL"、"Dual MPL/GPL"和"Proprietary"。

2.作者申明(可选):如MODULE_AUTHOR("Simon li");

3.模块描述(可选):如MODULE_DESCRIPTION("Hello World Module");

4.模块版本(可选):如MODULE_VERTION("V1.0");

5.模块别名(可选):如MODULE_ALIAS("a simple module");

6.模块参数:通过宏module_param指定模块参数,模块参数用于在加载模块时传递参数给模块。module_param(name,type,perm);name是模块参数的名称type是这个参数的类型,perm是模块参数的访问权限。其中type常见值:bool,int,charp(字符串型)。perm常见值:S_IRUGO,任何用户都对/sys/module中出现的该参数具有读权限;S_IWUSR,允许root用户修改/sys/module中出现的该参数。例如:

int a = 3;

char *st;

module_param(a,int,S_IRUGO);

module_param(st,charp,S_IRUGO);

内核符号导出

/proc/kallsyms记录了内核中所有导出的符号的名字与地址。如下两个源文件hello.c、calculate.c:

hello.c如下:

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

MODULE_LICENSE("GPL");
MODULE_AUTHOR("David Yuan");
MODULE_DESCRIPTION("Hello World Module");
MODULE_ALIAS("A simplest module");

extern int add_integar(int a, int b);
extern int sub_integar(int a, int b);

static int __init hello_init()
{
 int res = add_integar(1, 2);
 return 0;
}

static void __exit hello_exit()
{
 int res = sub_integar(2, 1);
}

module_init(hello_init);
module_exit(hello_exit);

calculate.c如下:

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

MODULE_LICENSE("GPL");

int add_integar(int a, int b)
{
 return a+b;
}

int sub_integar(int a, int b)
{
 return a-b;
}

static int __init sym_init()
{
 return 0;
}

static void __exit sym_exit()
{
 
}

module_init(sym_init);
module_exit(sym_exit);

EXPORT_SYMBOL(add_integar);
EXPORT_SYMBOL(sub_integar);

内核符号的导出使用:EXPORT_SYMBOL(符号名)、EXPORT_SYMBOL_GPL(符号名),其中EXPORT_SYMBOL_GPL只能用于包含GPL许可证的模块。

常见问题:版本不匹配

内核模块的版本由其所依赖的内核代码版本所决定,在加载内核模块时,insmod程序会将内核模块版本与当前正在运行的内核版本比较,如果不一致时,就会出现类似下面的错误:

insmod hello.ko

disagrees about version of symbol struct_module

insmod:error inserting 'hello.ko':-1 Invalid module format。

解决方法:

1.使用modprobe --force-modversion强行插入。

2.确保编译内核模块时,所依赖的内核代码版本等同于当前正在运行的内核。

注:可通过uname -r查看当前运行的内核版本。

2.4与2.6内核模块对比

详细参看网络资料。

总结——对比应用

对比应用程序,内核模块具有以下不同:应用程序是从头(main)到尾执行任务,执行结束后从内存中消失。内核模块则是先在内核中注册自己以便服务于将来的某个请求,然后他的初始化函数结束,此时模块仍然存在于内核中,直到卸载函数被调用,模块才从内核中消失。

内核打印

printk是内核中出现最频繁的函数之一,通过将printk与printf对比,将有助于理解。

相同点:打印信息。

不同点:printk在内核中使用,printf在应用程序中使用;printk允许根据严重程度,通过附加不同的"优先级"来对消息分类。

关于优先级,在<linux/kernel.h>中定义了8种记录级别。按照优先级递减的顺序分别是:

KERN_EMERG      "<0>"  :用于紧急消息,常常是那些崩溃前的消息。

KERN_ALERT       "<1>":需要立刻行动的消息。

KERN_CRIT          "<2>":严重情况。

KERN_ERR           "<3>":错误情况。

KERN_WARNING "<4>":有问题的警告。

KERN_NOTICE    "<5>":正常情况,但是仍然值得注意。

KERN_INFO         "<6>":信息型消息。

KERN_DEBUG    "<7>":用作调试消息。

没有指定优先级的printk默认使用DEFAULT_MESSAGE_LOGLEVEL优先级,他是一个在kernel/printk.c中定义的整数。

在2.6.29的内核中

#define DEFAULT_MESSAGE_LOGLEVEL。

控制台优先级配置:/proc/sys/kernel/printk       6  4  1  7

console_loglevel

Default_message_loglevel

Minimum_console_level

Default_console_loglevel

(完)

注:本文参看国嵌教育相关内容。