Linux驱动之内核模块

一、Linux内核模块简介

1.1 Linux内核模块介绍

Linux内核的整体结构已经非常庞大,而其包含的组件也非常多。我们怎样把需要的部分都包含在内核中呢?

一种方法是把所有需要的功能都编译到Linux内核。这会导致两个问题,一是生成的内核会很大,二是如果我们要在现有的内核中新增或删除功能,将不得不重新编译内核。

有没有一种机制使得编译出的内核本身并不需要包含所有功能,而在这些功能需要被使用的时候,其对应的代码被动态地加载到内核中呢?

Linux提供了这样一种机制,这种机制被称为模块(Module)。模块具有这样的特点:

  • ++模块本身不被编译如内核映像,从而控制了内核的大小++;
  • ++模块一旦被加载,它就和内核中的其他部分完全一样++;

模块化的最大的好处是可以动态扩展应用程序的功能而无需重新编译链接生成一个新的应用程序映像(或内核)。这对应Windows系统上为动态链接库DDL(Dynamic Link Library),对应到Linux系统中为驱动模块和应用程序的共享库so(shared object)

模块的机制使驱动程序与Linux内核结合有两种方式:在编译内核时,静态地链接进内核;++在系统运行时,以模块加载的方式加载进内核++。

1.2 Linux内核模块的构成

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

(1)模块加载函数(一般需要)

当通过insmod或modprobe命令加载内核模块时,模块的加载函数会被内核执行,完成本模块的相关初始化工作。

(2)模块卸载函数(一般需要)

当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与加载函数相反的功能。

(3)模块许可证声明(必须)

许可证(LICENSE)声明描述内核模块的许可权限,如果不声明LICENSE,模块被加载时,将收到内核被污染(kernel tainted)的警告。

在Linux 2.6内核中,可接受的LICENSE包括“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、“Dual MPL/GPL”和“Proprietary”。大多数情况下,内核模块应遵循GPL兼容许可权,Linux 2.6内核模块最常见的是以MODULE_LICENSE(“Dual BSD/GPL”);语句声明模块采用BSD/GPL双LICENSE。

(4)模块参数(可选)

模块参数是模块被加载的时候可以被传递给它的值,它本身对应模块内部的全局变量。

(5)模块导出符号(可选)

内核模块可以导出符号(symbol,对应于函数或变量),这样其他模块就可以使用本模块中的变量或函数。

(6)模块作者等信息声明(可选)

1.3 一个简单的Linux内核模块

一个基本的内核模块只需包含头文件、模块加载函数、模块卸载函数以及许可协议和一些描述信息。如代码清单1.1所示。

代码清单1.1 一个最简单的Linux内核模块

#include <linux/init.h>    /* 头文件 */
#include <linux/module.h>
   
static int hello_init(void)
{
   printk(KERN_INFO”Hello world enter.\n”);
   return 0;
}
   
static void hello_exit(void)
{
   printk(KERN_INFO”Hello world exit.\n”);
}
   
module_init(hello_init);
module_exit(hello_exit);
   
   /* 模块说明信息 */
MODULE_LICENSE(“Dual BSD/GPL”);
MODULE_AUTHOR(“Aaron konishi:zh5202@163.com”);
MODULE_ALIAS(“A simplest module”);
MODULE_DESCRIPTION(“A simple Hello world module”);

编译它会产生一个hello.ko目标文件,通过“insmod ./hello.ko”命令可以加载它,通过“rmmod hello”命令可以卸载它,加载时输出“Hello world enter.”,卸载时输出“Hello world exit”。

注释:内核模块中的输出函数是内核空间的printk()而非用户空间的printf(),printk()用法与printf()基本相似,但前者可以定义输出级别。printk()可作为一种最基本的内核调试手段,在Linux驱动的调试章节中将详细讲解这个函数。

内核中已加载模块的信息存在于/sys/module目录下,加载hello.ko后,内核中将包含/sys/module/hello目录,该目录下又包含一个refcnt文件和一个sections目录,在/sys/module/hello目录下运行“tree -a”得到如下目录树:

# tree -a
.
| -- holders
| -- initstate
| -- notes
|   `-- .note.gnu.build-id
| -- refcnt
| -- sections
|   | -- .bss
|   | -- .data
|   | -- .gnu.linkonce.this_module
|   | -- .note.gnu.build-id
|   | -- .rodata.str1.1
|   | -- .strtab
|   | -- .symtab
|   | -- .text
` -- srcversion
   
3 directories, 12 files

1.4 Linux模块操作常用命令

在编写、调试和使用Linux内核模块的过程中,常常会使用到lsmod、modprobe、rmmod和modinfo命令。

++lsmod命令可以获得系统中加载了的所有模块以及模块间的依赖关系++,例如:

Module          Size   Used   by
hello            9      472    0
nls_iso8859_1    12     032    1
nls_cp437        13     696    1
vfat             18     816    1
fat              57     376    1  vfat
…

lsmod命令实际上是读取并分析“/proc/modules”文件,与上述lsmod命令结果对应的“/proc/modules”文件如下:

# cat /proc/modules
hello 9472 0 - Live 0xf953b000
nls_iso8859_1 12032 1 - Live 0xf950c000
nls_cp437 13696 1 - Live 0xf9561000
vfat 18816 1 - Live 0xf94f3000
…

++modprobe命令比insmod命令要强大,它在加载某模块时,会同时加载该模块所依赖的其他模块。使用modprobe命令加载的模块若以“modprobe -r filename”的方式卸载将同时卸载其依赖的模块。++

++使用modinfo<模块名>命令可以获得模块的信息,包括模块作者、模块说明、模块所支持的参数以及vermagic++。

# modinfo hello.ko
   filename:      hello.ko
   alias:         A simplest module
   description:    A simple Hello world module
   license:       Dual BSD/GPL
   author:        Aaron konishi:zh5202@163.com
   srcversion:     3FE9B0FBAFDD565399B9C05
   depends:
   vermagic:      2.6.15-11-generic SMP mod_unload modversions 586

二、Linux内核模块详解

2.1 模块加载函数

++Linux内核模块加载函数一般以__init标识声明++,典型的模块加载函数的形式如代码清单1.2所示。

代码清单1.2 内核模块加载函数

static int __init initialization_function(void)
{
    /* 初始化代码 */
}
module_init(initialization_function);

==模块加载函数必须以“module_init(函数名)”的形式被指定==。它返回整型值,若初始化成功则返回0;初始化失败则返回错误编码。在Linux内核里,错误编码是一个负值,在<linux/errno.h>中定义,包含-ENODEV、-ENOMEM之类的符号值。总是返回相应的错误编码是种非常好的习惯,因为只有这样,用户程序才可以利用perror等方法把它们转换成有意义的错误信息字符串。

在Linux 2.6内核中,可以++使用request_module(const char *fmt,…)函数加载内核模块++,驱动开发人员可以通过调用:

request_module(module_name);

或:

request_module(“char-major-%d-%d”, MAJOR(dev), MINOR(dev));

这种灵活的方式加载其他内核模块。

在Linux中,所有标识为__init的函数在链接的时候都放在.init.text这个区段内,此外,所有的__init函数在区段.initcall.init中还保存了一份函数指针,在初始化时内核会通过这些函数指针调用这些__init函数,并在初始化完成后,释放init区段(包括.init.text、.initcall.init等)。

2.2 模块卸载函数

++Linux内核模块卸载函数一般以__exit标识声明++,典型的模块卸载函数的形式如代码清单1.3所示。

代码清单1.3 内核模块卸载函数

static void __exit cleanup_function(void)
{
    /* 释放代码 */
}
module_exit(cleanup_function);

模块卸载函数在模块卸载的时候执行,不返回任何值,++必须以“module_exit(函数名)”的形式来指定++。

通常来说,模块卸载函数要完成与模块加载函数相反的功能,如下所示:

  • 若模块加载函数注册了xxx,则模块卸载函数应该注销xxx;
  • 若模块加载函数动态申请了内存,则模块卸载函数应释放该内存;
  • 若模块加载函数开启了硬件,则模块卸载函数中一般要关闭该硬件;
  • 若模块加载函数申请了硬件资源(中断、DMA通道、I/O端口和I/O内存等)的占用,则模块卸载函数应释放这些硬件资源;

和__init一样,__exit也可以使对应的函数在运行完成后自动回收内存。实际上,__init和__exit都是宏,其定义分别为:

#define __init  __attribute__ ((__section__(“.init.text”)))
#define MODULE
#define __exit  __attribute__ ((__section__(“.exit.text”)))
#else
#define __exit  __attribute_used__attribute__ ((__section__(“.exit.text”)))
#endif

数据也可以被定义为__initdata和__exitdata,这两个宏分别为:

#define __initdata  __attribute__ ((__section__(“.init.data”)))
#define __exitdata  __attribute__ ((__section__(“.exit.data”)))

2.3 模块参数

我们可以++用“module_param(参数名,参数类型,参数读/写权限)”为模块定义一个参数++,例如下列代码定义了1个整型参数和1个字符指针参数:

static char *book_name = “dissecting Linux Device Driver”;
static int num = 4000;
module_param(num, int, S_IRUGO);
module_param(book_name, charp, S_IRUGO);

++在装载内核模块时,用户可以向模块传递参数,形式为“insmod(或modprobe) 模块名 参数名=参数值”,如果不传递,参数将使用模块内定义的缺省值++。

参数类型可以是byte、short、ushort、int、uint、long、ulong、charp(字符指针)、bool或invbool(布尔的反),在模块被编译时会将module_param中声明的类型与变量定义的类型进行比较,判断是否一致。

模块被加载后,在/sys/module/目录下将出现以此模块名命名的目录。当“参数读/写权限”为0时,表示此参数不存在sysfs文件系统下对应的文件节点,如果此模块存在“参数读/写权限”不为0的时,在此模块的目录下将会出现parameters目录,包含一系列以参数名命名的文件节点,这些文件的权限值就是传入module_param()的“参数读/写权限”,而文件的内容为参数的值。

除此之外,++模块也可以拥有参数数组,形式为“module_param_array(数组名, 数组类型, 数组长度, 参数读/写权限)”++。从2.6.0~2.6.10版本,需将数组长变量名赋给“数组长”,从2.6.10版本开始,需将数组长变量的指针赋给“数组长”,当不需要保存实际输入的数组元素个数时,可以设置“数组长”为NULL。

运行insmod或modprobe命令时,应使用逗号分隔输入的数组元素。

现在我们定义一个包含两个参数的模块(如代码清单1.4),并观察模块加载时被传递参数和不传递参数时的输出。

代码清单1.4 带参数的内核模块

#include <linux/init.h>
#include <linux/module.h>
static char *book_name = “dissecting Linux Device Driver”;
static int num = 4000;
static int book_init(void)
{
    printk(KERN_INFO”book name:%s\n”, book_name);
    printk(KERN_INFO”book num:%d\n”, num);
    return 0;
}
static void book_exit(void)
{
    printk(KERN_INFO”book module exit\n”);
}
module_init(book_init);
module_exit(book_exit);
module_param(num, int, S_IRUGO);
module_param(book_name, charp, S_IRUGO);
MODULE_AUTHOR(“Aaron Konishi<zh5202@163.com>”);
MODULE_DESCRIPTION(“A simple Module for testing module params”);
MODULE_VERSION(“V1.0”);
MODULE_LICENSE(“Dual BSD/GPL”);

对上述模块运行“insmod book.ko”命令加载,相应输出都为模块内的默认值,通过查看“/var/log/messages”日志文件可以看到内核的输出:

# tail -n 2 /var/log/messages
Jul 2 01:03:10 localhost kernel: <6> book name:dissecting Linux Device Driver
Jul 2 01:03:10 localhost kernel: <6> book num:4000

当用户运行“insmod book.ko book_name=’GoodBook’ num=5000”命令时,输出的是用户传递的参数:

# tail -n 2 /var/log/messages
Jul 2 01:03:10 localhost kernel: <6> book name:GoodBook
Jul 2 01:03:10 localhost kernel: <6> book num:5000

2.4 模块符号导出

Linux 2.6的“/proc/kallsyms”文件对应着内核符号表,它记录了符号以及符号所在的内存地址。模块可以使用如下宏导出符号到内核符号表:

EXPORT_SYMBOL (符号名);
EXPORT_SYMBOL_GPL(符号名);

导出的符号将可以被其他模块使用,使用前声明一下即可。EXPORT_SYMBOL_GPL()只适用于包含GPL许可权的模块。代码清单1.5给出了一个导出整数加、减运算函数符号的内核模块的例子(这里仅仅是为了演示)。

代码清单1.5 内核模块中的符号导出

#include <linux/init.h>
#include <linux/module.h>
int add_integar(int a,int b);
{
    return a+b;
}
int sub_integar(int a,int b)
{
    return a-b;
}
EXPORT_SYMBOL(add_integar);
EXPORT_SYMBOL(add_integar);
MODULE_LICENSE(“Dual BSD/GPL”);

加载该模块之后,从“/proc/kallsyms”文件中找出add_integar、sub_integar相关信息:

# cat /proc/kallsyms | grep integar
…..
c886f00 T add_integar [export]
c886f0b T sub_integar [export]
……

2.5 模块的声明与描述

在Linux内核模块中,我们可以用MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_DEVICE_TABLE、MODULE_ALIAS分别声明模块的作者、描述、版本、设备表和别名,例如:

MODULE_AUTHOR(author);
MODULE_DESCRIPTION(description);
MODULE_VERSION(version_string);
MODULE_DEVICE_TABLE(table_info);
MODULE_ALIAS(alternate_name);

对于USB、PCI等设备驱动,通常会建立一个MODULE_DEVICE_TABLE,表明该驱动模块所支持的设备,如代码清单1.6所示。

/* 对应此驱动的设备表 */
static struct usb_device_id skel_table[] = {
    { USB_DEVICE(USB_SKEL_VENDOR_ID,
     USB_SKEL_PRODUCT_ID)  }
    { } /* 表结束 */
};
MODULE_DEVICE_TABLE(usb, skel_table);

此时,并不需要读者理解MODULE_DEVICE_TABLE的作用,后续相关章节会有详细介绍。

2.6 模块的使用计数

Linux 2.4内核中,模块自身通过MOD_INC_COUNT、MOD_DEC_USE_COUNT宏来管理自己被使用的计数。

Linux 2.6内核提供了模块计数管理接口try_module_get(&module)和module_put(&module),从而取代Linux 2.4内核中的模块使用计数管理宏。模块的使用计数一般不必由模块自身管理,而且模块计数管理还考虑了SMP与PREEMPT机制的映像。

int try_module_get(struct module *module);

该函数用于增加模块使用计数,若返回0,表示调用失败,希望使用的模块没有被加载或正在被卸载中。

void module_put(struct module *module);

该函数用于减少模块使用计数。

try_module_get()与module_put()的引入与使用与Linux 2.6内核下的设备模型密切相关。Linux 2.6内核为不同类型的设备定义了struct module *owner域,用来指向管理此设备的模块。当开始使用某个设备时,内核使用try_module_get(dev->owner)去增加管理此设备的owner模块的使用计数;当不再使用此设备时,内核使用module_put(dev->owner)减少对管理此设备的owner模块的使用计数。这样,当设备在使用时,管理此设备的模块将不能被卸载。只有当设备不再被使用时,模块才允许被卸载。

在Linux 2.6内核下,对于设备驱动工程师而言,很少需要亲自调用try_module_get()与module_put(),因为此时开发人员所写的驱动通常与支持某具体设备的owner模块,对此设备owner模块的计数管理由内核里更底层的代码如总线驱动或是此类设备共用的核心模块来实现,从而简化了设备驱动的开发。

2.7 模块的编译

我们可以为代码清单1.1的模板编写一个简单的Makefile:

KVERS = $(shell uname -r)
# Kernel modules
obj-m += hello.o
# Specify flags for the module compilation.
#EXTRA_CFLAGS=-g -O0
build: kernel_modules
kernel_modules:
    make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
clean:
    make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

该Makefile文件应该与源代码hello.c位于同一目录,开启其中的EXTRA_CFLAGS=-g -O0可以得到包含调试信息的hello.ko模块。运行make命令得到的模块可直接在PC上运行。

如果一个模块包括多个.c文件(如file1.c、file2.c),则应该以如下方式编写Makefile:

obj-m := modulename.o
modulename-objs := file1.o file2.o

2.8 使模块绕开GPL

对于企业自己编写的驱动等内核代码,如果不编译为模块则无法绕开GPL,编译为模块后企业在产品中使用模块,则公司对外不再需要提供对应的源代码,为了使公司产品所使用的Linux操作系统支持模块,需要完成如下工作:

  • 在内核编译时应该选上“可以加载模块”,嵌入式产品一般不需要动态卸载模块,所以“可以卸载模块”不用选;
  • 将我们编译的内核模块.ko文件放在目标文件系统的相关目录中;
    产品的文件系统中应该包含了支持新内核的insmod、lsmod、rmmod等工具,由于嵌入式产品中一般不需要建立模块间依赖关系,所以modprobe可以不要,一般也不需要卸载模块,所以rmmod也可以不要;
  • 在使用中用户可使用insmod命令手动加载模块,如insmod xxx.ko;
  • 但是一般而言,产品在启动过程中应该加载模块,在嵌入式产品Linux的启动过程中,加载企业自己的模块的最简单的方法是修改启动过程的rc脚本,增加insmod /…/xxx.ko这样的命令。用busybox做出的文件系统,通常修改/etc/init.d/rcS文件。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 周五抽了一张问题卡,问题是“有什么是你一直想做却多年未能着手进行的事”,这个问题是个好问题,而我却一下没有答案,我...
    祉莉翔妈阅读 650评论 0 1
  • 父母的日渐衰老是我不得不察觉到的实事,今天冒雨赶往学校了,一种说不出口的复杂情感堵在胸口,有对家人的不舍,对未来的...
    伊古董阅读 193评论 0 1
  • 我的驾照拿到快一年了,每当我放着我喜欢的电子音乐,在高速上飞奔的时候,我就会想起这一年的生活,我的大学生活。其实并...
    tonytonie阅读 459评论 0 0
  • 纯白的爱人天各一方, 默守陈规到夕阳, 与雏鸟交谈同顽石幻想, 逃出圆形立方的不只我一个模样。 晨曦清唱, 半夜哽...
    果间房的晚餐阅读 295评论 1 7