Linux内核模块简单介绍

Linux内核的总体结构已经很庞大,而其包括的组件或许多。我们如何把须要的部分都包括在内核中呢?
一种方法是把全部须要的功能都编译到Linux内核。这会导致两个问题。一是生成的内核会很大,二是假设我们要在现有的内核中新增或删除功能,将不得不又一次编译内核。

有没有一种机制使得编译出的内核本身并不须要包括全部功能,而在这些功能须要被使用的时候,其相应的代码被动态地载入到内核中呢?
Linux提供了这样的一种机制,这样的机制被称为模块(Module)。模块具有这样的特点。

  1. 模块本身不被编译入内核映像,从而控制了内核的大小。

  2. 模块一旦被载入,它就和内核中的其它部分全然一样。

为了使读者建立对模块的初步感性认识,我们先来看一个最简单的内核模块“Hello World”,如代码清单4.1所看到的。

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

01 /*
02 * a simple kernel module: hello
03 *
04 * Copyright (C) 2014 Barry Song (baohua@kernel.org)
05 *
06 * Licensed under GPLv2 or later.
07 */
08
09 #include <linux/init.h>
10 #include <linux/module.h>
11
12 static int __init hello_init(void)
13 {
14 printk(KERN_INFO "Hello World enter\n");
15 return 0;
16 }
17 module_init(hello_init);
18
19 static void __exit hello_exit(void)
20 {
21 printk(KERN_INFO "Hello World exit\n ");
22 }
23 module_exit(hello_exit);
24
25 MODULE_AUTHOR("Barry Song <21cnbao@gmail.com>");
26 MODULE_LICENSE("GPL v2");
27 MODULE_DESCRIPTION("A simple Hello World Module");
28 MODULE_ALIAS("a simplest module");

这个最简单的内核模块仅仅包括内核模块载入函数、卸载函数和对GPL v2许可权限的声明以及一些描写叙述信息。位于本书配套源码的/kernel/drivers/hello文件夹。编译它会产生hello.ko目标文件,通过“insmod ./hello.ko”命令能够载入它,通过“rmmod hello”命令能够卸载它,载入时输出“Hello World enter”,卸载时输出“Hello World exit”。

内核模块中用于输出的函数是内核空间的printk()而非用户空间的printf()。printk()的使用方法和printf()基本类似,但前者可定义输出级别。printk()可作为一种最主要的内核调试手段,在Linux驱动的调试章节中将具体解说这个函数。
在Linux中。使用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 12393 0 - Live 0xe67a2000 (OF)
nls_utf8 12493 1 - Live 0xe678e000
isofs 39596 1 - Live 0xe677f000
vboxsf 42561 2 - Live 0xe6767000 (OF)
...

内核中已载入模块的信息也存在于/sys/module文件夹下。载入hello.ko后,内核中将包括/sys/module/hello文件夹。该文件夹下又包括一个refcnt文件和一个sections文件夹,在/sys/module/hello文件夹下执行“tree –a”得到例如以下文件夹树:

root@barry-VirtualBox:/sys/module/hello# tree -a
.
├── coresize
├── holders
├── initsize
├── initstate
├── notes
│ └── .note.gnu.build-id
├── refcnt
├── sections
│ ├── .exit.text
│ ├── .gnu.linkonce.this_module
│ ├── .init.text
│ ├── .note.gnu.build-id
│ ├── .rodata.str1.1
│ ├── .strtab
│ └── .symtab
├── srcversion
├── taint
└── uevent 3 directories, 15 files

modprobe命令比insmod命令要强大。它在载入某模块时,会同一时候载入该模块所依赖的其它模块。

使用modprobe命令载入的模块若以“modprobe -r filename”的方式卸载将同一时候卸载其依赖的模块。模块之间的依赖关系上存放在根文件系统的/lib/modules/<kernel-version>/modules.dep文件里,实际上是在总体编译内核的时候由depmod工具生成的,它的格式很easy:

kernel/lib/cpu-notifier-error-inject.ko: kernel/lib/notifier-error-inject.ko
kernel/lib/pm-notifier-error-inject.ko: kernel/lib/notifier-error-inject.ko
kernel/lib/lru_cache.ko:
kernel/lib/cordic.ko:
kernel/lib/rbtree_test.ko:
kernel/lib/interval_tree_test.ko:
updates/dkms/vboxvideo.ko: kernel/drivers/gpu/drm/drm.ko

使用modinfo <模块名>命令能够获得模块的信息,包括模块作者、模块的说明、模块所支持的參数以及vermagic:

# modinfo hello.ko
filename: hello.ko
alias: a simplest module
description: A simple Hello World Module
license: GPL v2
author: Barry Song <21cnbao@gmail.com>
srcversion: 081230411494509792BD4A3
depends:
vermagic: 3.8.0-39-generic SMP mod_unload modversions 686

Linux内核模块程序结构

一个Linux内核模块主要由例如以下几个部分组成。
(1)模块载入函数
当通过insmod或modprobe命令载入内核模块时。模块的载入函数会自己主动被内核执行,完毕本模块的相关初始化工作。
(2)模块卸载函数
当通过rmmod命令卸载某模块时,模块的卸载函数会自己主动被内核执行,完毕与模块卸载函数相反的功能。
(3)模块许可证声明
许可证(LICENSE)声明描写叙述内核模块的许可权限,假设不声明LICENSE,模块被载入时,将收到内核被污染 (kernel tainted)的警告。
在Linux内核模块领域。可接受的LICENSE包括“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、“Dual MPL/GPL”和“Proprietary”(关于模块能否够採用非GPL许可权如“Proprietary”,这个在学术界和法律界都有争议)。
大多数情况下,内核模块应遵循GPL兼容许可权。

Linux内核模块最常见的是以MODULE_LICENSE( "GPL v2" )语句声明模块採用GPL v2。
(4)模块參数(可选)。
模块參数是模块被载入的时候能够被传递给它的值,它本身相应模块内部的全局变量。
(5)模块导出符号(可选)。
内核模块能够导出符号(symbol。相应于函数或变量),这样其它模块能够使用本模块中的变量或函数。
(6)模块作者等信息声明(可选)。

模块载入函数

Linux内核模块载入函数一般以_ _init标识声明,典型的模块载入函数的形式如代码清单4.2所看到的。

代码清单4.2  内核模块载入函数

1     static int _ _init initialization_function(void)
2 {
3 /* 初始化代码 */
4 }
5 module_init(initialization_function);

模块载入函数则以“module_init(函数名)”的形式被指定。它返回整型值,若初始化成功,应返回0。而在初始化失败时,应该返回错误编码。

在Linux内核里,错误编码是一个接近于0的负值,在<linux/errno.h>中定义。包括-ENODEV、-ENOMEM之类的符号值。

总是返回相应的错误编码是种很好的习惯。由于仅仅有这样,用户程序才干够利用perror等方法把它们转换成有意义的错误信息字符串。
在Linux内核中,能够使用request_module(const char *fmt, …)函数载入内核模块,驱动开发者能够通过调用
request_module(module_name);
这样的灵活的方式载入其它内核模块。

在Linux中,全部标识为_ _init的函数假设直接编译进入内核,成为内核镜像的一部分,在连接的时候都放在.init.text这个区段内。
#define _ _init        _ _attribute_ _ ((_ _section_ _ (".init.text")))
全部的_ _init函数在区段.initcall.init中还保存了一份函数指针,在初始化时内核会通过这些函数指针调用这些_ _init函数,并在初始化完毕后,释放init区段(包括.init.text、.initcall.init等)的内存。
除了函数以外,数据也能够被定义为_ _initdata,对于仅仅是初始化阶段须要的数据,内核在初始化完后,也能够释放它们占用的内存。比如,以下的代码中将hello_data定义为__initdata。

static int hello_data __initdata = 1;

static int __init hello_init(void)
{
printk(KERN_INFO "Hello, world %d\n", hello_data);
return 0;
}
module_init(hello_init); static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, world\n");
}
module_exit(hello_exit);

模块卸载函数

Linux内核模块载入函数一般以_ _exit标识声明,典型的模块卸载函数的形式如代码清单4.3所看到的。
代码清单4.3  内核模块卸载函数

1    static void _ _exit cleanup_function(void)
2 {
3 /* 释放代码 */
4 }
5 module_exit(cleanup_function);

模块卸载函数在模块卸载的时候执行,不返回不论什么值,必须以“module_exit(函数名)”的形式来指定。通常来说,模块卸载函数要完毕与模块载入函数相反的功能。

我们用__exit来修饰模块卸载函数,能够告诉内核假设相关的模块被直接编译进内核(即built-in),则cleanup_function() 函数会被省略直接不连接进最后的镜像。既然模块被built-in了。就不可能卸载它了。卸载函数也就没有存在的必要了。除了函数以外,仅仅是退出阶段採用的数据也能够用__exitdata来形容。

模块參数

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

static char *book_name = "dissecting Linux Device Driver";
module_param(book_name, charp, S_IRUGO); static int book_num = 4000;
module_param(book_num, int, S_IRUGO);

在装载内核模块时,用户能够向模块传递參数。形式为“insmode(或modprobe)模块名 參数名=參数值”,假设不传递,參数将使用模块内定义的缺省值。假设模块被built-in。就无法insmod了,可是bootloader能够通过在bootargs里设置“模块名.參数名=值”的形式给该built-in的模块传递參数。
參数类型能够是byte、short、ushort、int、uint、long、ulong、charp(字符指针)、bool或invbool(布尔的反),在模块被编译时会将module_param中声明的类型与变量定义的类型进行比較。推断是否一致。
除此之外,模块也能够拥有參数数组,形式为“module_param_array(数组名,数组类型,数组长,參数读/写权限)”。
模块被载入后,在/sys/module/文件夹下将出现以此模块名命名的文件夹。当“參数读/写权限”为0时。表示此參数不存在sysfs文件系统下相应的文件节点。假设此模块存在“參数读/写权限”不为0的命令行參数,在此模块的文件夹下还将出现parameters文件夹。包括一系列以參数名命名的文件节点,这些文件的权限值就是传入module_param()的“參数读/写权限”,而文件的内容为參数的值。
执行insmod或modprobe命令时,应使用逗号分隔输入的数组元素。
如今我们定义一个包括两个參数的模块(如代码清单4.4。位于本书源码/kernel/drivers/param文件夹),并观察模块载入时被传递參数和不传递參数时的输出。

代码清单4.4  带參数的内核模块

01 #include <linux/init.h>
02 #include <linux/module.h>
03
04 static char *book_name = "dissecting Linux Device Driver";
05 module_param(book_name, charp, S_IRUGO);
06
07 static int book_num = 4000;
08 module_param(book_num, int, S_IRUGO);
09
10 static int __init book_init(void)
11 {
12 printk(KERN_INFO "book name:%s\n", book_name);
13 printk(KERN_INFO "book num:%d\n", book_num);
14 return 0;
15 }
16 module_init(book_init);
17
18 static void __exit book_exit(void)
19 {
20 printk(KERN_INFO "book module exit\n ");
21 }
22 module_exit(book_exit);
23
24 MODULE_AUTHOR("Barry Song <baohua@kernel.org>");
25 MODULE_LICENSE("GPL v2");
26 MODULE_DESCRIPTION("A simple Module for testing module params");
27 MODULE_VERSION("V1.0");

对上述模块执行“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: book num:4000

当用户执行“insmod book.ko book_name='GoodBook' book_num=5000”命令时。输出的是用户传递的參数:

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

另外,在/sys文件夹下,也能够看到book模块的參数:

barry@barry-VirtualBox:/sys/module/book/parameters$ tree
.
├── book_name
└── book_num

导出符号

Linux的“/proc/kallsyms”文件相应着内核符号表,它记录了符号以及符号所在的内存地址。
模块能够使用例如以下宏导出符号到内核符号表:
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名);
    导出的符号将能够被其它模块使用。使用前声明一下就可以。EXPORT_SYMBOL_GPL()仅仅适用于包括GPL许可权的模块。代码清单4.5给出了一个导出整数加、减运算函数符号的内核模块的样例。
代码清单4.5  内核模块中的符号导出

01 #include <linux/init.h>
02 #include <linux/module.h>
03
04 int add_integar(int a, int b)
05 {
06 return a + b;
07 }
08 EXPORT_SYMBOL_GPL(add_integar);
09
10 int sub_integar(int a, int b)
11 {
12 return a - b;
13 }
14 EXPORT_SYMBOL_GPL(sub_integar);
15
16 MODULE_LICENSE("GPL v2");

从“/proc/kallsyms”文件里找出add_integar、sub_integar相关信息:

# grep integar /proc/kallsyms
e679402c r __ksymtab_sub_integar [export_symb]
e679403c r __kstrtab_sub_integar [export_symb]
e6794038 r __kcrctab_sub_integar [export_symb]
e6794024 r __ksymtab_add_integar [export_symb]
e6794048 r __kstrtab_add_integar [export_symb]
e6794034 r __kcrctab_add_integar [export_symb]
e6793000 t add_integar [export_symb]
e6793010 t sub_integar [export_symb]

模块声明与描写叙述

在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。表明该驱动模块所支持的设备,如代码清单4.6所看到的。
代码清单4.6  驱动所支持的设备列表

1 /* 相应此驱动的设备表 */
2 static struct usb_device_id skel_table [] = {
3 { USB_DEVICE(USB_SKEL_VENDOR_ID,
4 USB_SKEL_PRODUCT_ID) },
5 { } /* 表结束 */
6 };
7
8 MODULE_DEVICE_TABLE (usb, skel_table);

此时。并不须要读者理解MODULE_DEVICE_TABLE的作用,兴许相关章节会有具体介绍。

模块的使用计数

Linux 2.4内核中。模块自身通过MOD_INC_USE_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模块的计数管理由内核里更底层的代码如总线驱动或是此类设备共用的核心模块来实现。从而简化了设备驱动开发。

模块的编译

我们能够为代码清单4.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

模块与GPL

Linux内核有2种方法导出符号给模块使用,一种方法是EXPORT_SYMBOL()。第二种EXPORT_SYMBOL_GPL()。这一点和模块A导出符号给模块B用是一致的。
内核的Documentation/DocBook/kernel-hacking.tmpl明白表明“the symbols exported by EXPORT_SYMBOL_GPL()can only be seen by modules with a MODULE_LICENSE() that specifies a GPL compatible license.”由此可见内核用EXPORT_SYMBOL_GPL()导出的符号是不能够被非GPL模块引用的。
由于相当多的内核符号都是以EXPORT_SYMBOL_GPL()导出的,所以历史上以前有一些公司的做法是把内核的EXPORT_SYMBOL_GPL()直接改为EXPORT_SYMBOL()。然后将改动后的内核以GPL形式公布。这样改动内核之后,模块不再使用内核的EXPORT_SYMBOL_GPL()符号,因此模块不再须要GPL。对此Linus的回复是:“I think both them said that anybody who were to change a xyz_GPL to the non-GPL one in order to use it with a non-GPL module would almost immediately fall under the "willful infringement" thing, and that it would make it MUCH easier to get triple damages and/or injunctions, since they clearly knew about it”。

因此,这样的做法可能构成“蓄意侵权(willful infringement)”。
    第二种做法是写一个wrapper内核模块(这个模块遵循GPL)。把EXPORT_SYMBOL_GPL()导出的符号封装一次再次以EXPORT_SYMBOL()形式导出,而其它的模块不直接调用内核而是调用wrapper函数。如图4.1所看到的。这样的做法也具有争议。
 
图4.1将EXPORT_SYMBOL_GPL又一次以EXPORT_SYMBOL导出
一般觉得。保守的做法是Linux内核不能使用非GPL许可权。

Linux内核模块编程与内核模块LICENSE -《具体解释(第3版)》预读的更多相关文章

  1. Linux系统编程【3.2】——ls命令优化版和ls -l实现

    前情提要 在笔者的上一篇博客Linux系统编程[3.1]--编写ls命令中,实现了初级版的ls命令,但是与原版ls命令相比,还存在着显示格式和无颜色标记的不同.经过笔者近两天的学习,基本解决了这两个问 ...

  2. Linux内核模块编程——Hello World模块

    Linux内核模块编程 编程环境 Ubuntu 16.04 LTS 什么是模块 内核模块的全称是动态可加载内核模块(Loadable Kernel Modul,KLM),可以动态载入内核,让它成为内核 ...

  3. linux内核模块编程实例

    linux内核模块编程实例 学号:201400814125 班级:计科141 姓名:刘建伟 1.确定本机虚拟机中的Ubuntu下Linux的版本 通过使用命令uname -a/uname -r/una ...

  4. 《linux内核设计与分析》内核模块编程

    内核模块编程 一.准备工作 虚拟机:VMware Workstation 12操作系统:ubuntu当前内核版本:linux-headers-4.4.0-22-generic 二.有关于内核模块的知识 ...

  5. inux内核模块编程入门

    linux内核模块编程入门 2013-07-06 23:59:54 分类: LINUX 原文地址:linux内核模块编程入门 作者:s270768095 模块编程属于内核编程,因此,除了对内核相关知识 ...

  6. Linux内核分析(二)----内核模块简介|简单内核模块实现

    原文:Linux内核分析(二)----内核模块简介|简单内核模块实现 Linux内核分析(二) 昨天我们开始了内核的分析,网上有很多人是用用源码直接分析,这样造成的问题是,大家觉得很枯燥很难理解,从某 ...

  7. 《Linux设备驱动开发具体解释(第3版)》进展同步更新

    本博实时更新<Linux设备驱动开发具体解释(第3版)>的最新进展. 2015.2.26 差点儿完毕初稿. 本书已经rebase到开发中的Linux 4.0内核,案例多数基于多核CORTE ...

  8. Linux网络编程入门 (转载)

    (一)Linux网络编程--网络知识介绍 Linux网络编程--网络知识介绍客户端和服务端         网络程序和普通的程序有一个最大的区别是网络程序是由两个部分组成的--客户端和服务器端. 客户 ...

  9. [转] - Linux网络编程 -- 网络知识介绍

    (一)Linux网络编程--网络知识介绍 Linux网络编程--网络知识介绍客户端和服务端         网络程序和普通的程序有一个最大的区别是网络程序是由两个部分组成的--客户端和服务器端. 客户 ...

随机推荐

  1. linux kernel如何处理大端小端字节序

    (转)http://blog.csdn.net/skyflying2012/article/details/43771179 最近在做将kernel由小端处理器(arm)向大端处理器(ppc)的移植的 ...

  2. 数据结构( Pyhon 语言描述 ) — — 第8章:队列

    队列概览 队列是线性的集合 队列的插入限制在队尾,删除限制在队头.支持先进先出协议( FIFIO, first-in first-out ) 两个基本操作 add:在队尾添加一项 pop:从队头弹出一 ...

  3. Java集合之PriorityQueue

    PriorityQueue 定义 C++:priority_queue Java:PriorityQueue 创建与其基本操作 创建: PriorityQueue<Integer>=new ...

  4. kendo Grid 列添加自定义模板

    columns: [ {field: "行为",template: "<a href='#= 行为#'>#= 行为#</a>"}, {f ...

  5. BNUOJ 1207 滑雪

    滑雪 Time Limit: 1000ms Memory Limit: 65536KB   This problem will be judged on PKU. Original ID: 10886 ...

  6. java紧耦合与松耦合关系

    请先看下这个关于松耦合的回答 举个简单的例子啦 有一百人分成10个团队做开发 你写了一个类A,供其他人调用,怎么办? 简单的方法就是把这个类打成jar包,然后给他们 他们就A a = new A(); ...

  7. 【转】Python + Android + Uiautomator自动化测试

    1.首先来介绍下UIAutomator工具 UIAutomator是Android官方推出的安卓应用界面自动化测试工具,是最理想的针对APK进行自动化功能回归测试的利器. 2.UIAutomator测 ...

  8. Spoj-BOKAM143SOU Checking cubes.

    Given a integer N. Find number of possible ways to represent N as a sum of at most five cubes. Input ...

  9. 仓库建设(bzoj 1096)

    Description L公司有N个工厂,由高到底分布在一座山上.如图所示,工厂1在山顶,工厂N在山脚.由于这座山处于高原内陆地区(干燥少雨),L公司一般把产品直接堆放在露天,以节省费用.突然有一天, ...

  10. Yii 之视图布局

    控制器代码: //设置的布局文件 public $layout = 'common'; public function actionAbout(){ $data = array('page_name' ...