http://www.luyuncheng.com
Peter Jay Salzman
Michael Burian
Ori Pomerantz
Copyright © 2001 Peter Jay Salzman
2007-05-18 ver 2.6.4
Linux 内核模块编程指南是一本免费的书;你可以根据开源软件许可条例1.1下复制或者修改。你可以在http://opensource.org/licenses/osl.php查看条例副本。
这本书希望对您有帮助,但是不是绝对的,这本书并没有特定人群或者以商业为目的。
作者鼓励书本作为个人或者商业目的的广泛运用,并提供版权信息。总的来说,你能拷贝或者发布这本书在任何媒体上。
衍生作品和翻译文件必须放置于Open Software License中,并且原始版权必须完整保留。如果你对这本书有贡献新的素材,你肯定能让材料和源码为你所用。请将修改和更新直接給文件保存者:Peter Jay Salzman [email protected]。这将可能合并更新到主文件中并将为Linux社区在提供持久的修订版本。
如果你出于商业性,捐赠性,专利版税或者复印地出版或者发布这本书,请由衷的感激作者和Linux Documentation Project (LDP).以这种方式展示贡献表示你支持开源软件和LDP。如果你疑问或者问题请联系上面地址。
内容
前言
作家介绍
版本和注意事项
答谢名单
引言
1.1 什么是内核模块
1.2 怎么使得模块进入内核Hello World
2.1 Hello,World(part 1):最简单模块
2.2 编译内核模块
2.3 Hello World(part 2)
2.4 Hello World(part 3):The __init and __exit
宏指令
2.5 Hello World(part 4):证书和模块文件
2.6 传命令行参数到内核
2.7 多文件内核模块
2.8 为与编译内核建立模块初步
3.1 模块vs程序字符设备文件
4.1 字符设备驱动/proc文件系统
5.1 /proc文件系统
5.2 读写一个/proc文件
5.3 用标准文件系统管理/proc文件
5.4 用seq_file管理/proc文件用/proc来输入
6.1 写一章sysfs与设备文件对话
7.1 与设备文件对话(写 和 IOCTLs)系统调用
8.1 系统调用阻塞进程
9.1 阻塞进程替换Printks
10.1 替换printk
10.2 点亮键盘LED灯任务调度
11.1 任务调度中断处理
12.1 中断处理对称多处理机
13.1 对称多处理机常见错误
14.1 常见错误A. 从2.0到2.2的改变
B. 展望未来
引用
例子列表:
List of Examples
2-1. hello-1.c
2-2. Makefile for a basic kernel module
2-3. hello-2.c
2-4. Makefile for both our modules
2-5. hello-3.c
2-6. hello-4.c
2-7. hello-5.c
2-8. start.c
2-9. stop.c
2-10. Makefile
4-1. chardev.c
5-1. procfs1.c
5-2. procfs2.c
5-3. procfs3.c
5-4. procfs4.c
7-1. chardev.c
7-2. chardev.h
7-3. ioctl.c
8-1. syscall.c
9-1. sleep.c
9-2. cat_noblock.c
10-1. print_string.c
10-2. kbleds.c
11-1. sched.c
12-1. intrpt.c
前言
略
第一章:引入
1.1什么是内核
当你想写一个内核模块,你知道C语言,你已经写过一些内核程序,现在你想得到真正的跑起来那就只能重启计算机:让一个野指针清除你的文件系统和存储器内容!
什么是真正的内核模块?模块是代码的片段,它们能按需求装载和卸载到内核。它们在不需要重启系统的情况下拓展了内核的功能。例如,一模块类型是设备驱动,它允许内核连接到硬件系统。如果没有模块,我们必须build庞大的内核并且加入在内核镜像中直接加入新的函数,除了有大量内核外,它的缺点还在于每次加入函数都需要我们rebuild和重启内核。1.2模块如何进入内核
你能通过运行
lsmod
看到什么模块已经装载到了内核,它是通过读proc/modules
文件获得的信息。
这么模块如何找到他们自己的方式进入内核呢?当内核需要一个部件但是并没有常驻内核时候,内核模块的后台驻留程序kmod执行modprobe来把模块装进来。modprobe是用下述两个中的一种格式传一个字符串:
1. 一个模块名字如:softdog或者ppp
2. 更通用标识符如:char-major-10-30
如果modprobe提交了一个通用标识符,它首先会在/etc/modprobe.conf
文件中招字符串。如果它找到了一个别名引用(alias link) 像是:
alias char-major-10-30 softdog
这个可以知道这个通用标识符指向模块softdog.ko。
接下来,modprobe 查找文件/lib/modules/version/modules.dep
,来看是否在需求的模块已经装载之前其他模块已经装载了(to see if other modules must be loaded before the requested module may be loaded)。这个文件由depmod -a
创建并且依赖于所包含的模块。例如msdos.ko
需要fat.ko
模块已经装载进内核了,如果其他模块定义标识(变量或者函数)来依赖模块使用,那么说所需求的模块依赖于另一个模块。
最后,modprobe使用insmod来首先装载所有的先决必须模块到内核中然后要求的模块。modprobe引导insmod到/lib/modules/version/
模块标准目录,insmod的目标是相当非智能的定位这些模块,然后modprobe能意识到这个默认模块定位,知道用正确的顺序如何解决依赖和装载模块。例如:如果你想要装载msdos模块,你必须既运行
insmod /lib/modules/2.6.11/kernel/fs/fat/fat.ko
insmod /lib/modules/2.6.11/kernel/fs/msdos/msdos.ko
也要运行
modprobe msdos
这里我们看到:insmod需要你传递全的路径名字且要用正确的顺序注入模块,然后modprobe仅仅用名字,不需要任何其他操作仅仅通过语句/lib/modules/version/modules.dep
就能解决它全部所需要的。
Linux发行版提供了modprobe,insmod和depmod当作一个包叫做module-init-tools
.在早期版本中这个包叫做modutils。一些发行版也设定一些封装来允许两个包平行的装入,且在顺序都做正确的事情,以此来兼容处理2.4和2.6的内核。用户不需要关注这些细节,只要他们能在最近版本中运行这些工具就行。
现在你能知道模块如何进入内核。如果你想要写你自己的内核模块,并且这些模块依赖于其他模块(我们称这些为“栈模块”)这还有一些其他的故事。但是这个奖不得不等到以后的章节中。我们在针对相对高等级问题之前,我们还有许多需要解决。1.2.1开始之前
略
1.2.1.3 编译问题和内核版本
通常,打了补丁的Linux发行版会通过很多非标准的方式分配kernel源码,这可能会造成一些问题。
一个更常见的问题是一些Linux发行版本分配不完整的内核头。你需要从内核用到各种Linux头文件编译你的代码。墨菲法则说过这些缺失的头文件恰恰就是你模块工作所需要的
为了避免这两种情况,我们高度推荐你下载,编译且弄一个新的,存有Linux内核的,这些可以从任何Linux镜像站下载得到。可以看 Linux Kernel HOWTO 得到更多细节
讽刺迪说,这个也会导致一个问题。默认情况下,你系统的gcc系统可能在默认位置下找寻内核头文件而不是你安装的新版本副本(通常在/usr/src/)这个能通过用gcc的-I
切换第二章Hello World
2.1 Hello,World(part1),最简模块
当“野人“程序猿第一次在墙上”雕琢“洞穴计算程序时,那时第一个程序就是在羊皮纸上画一个”Hello,World“。罗马编程本开始于“Salut,Mundi”程序。我不知道打破这个传统的人们发生了什么,但是我认为这个还是不要发现的好。我们将开始一系列hello world 程序来阐述写内核模块的基础的不同的方面
这个可能是最简单的内核模块了。暂时还不要编译;我们将解决模块编译问题在下一节
例子:2-1.hello-1.c/* * hello-1.c - The simplest kernel module. */ #include
/* Needed by all modules */ #include /* Needed for KERN_INFO */ int init_module(void) { printk(KERN_INFO "Hello world 1.\n"); /* * A non 0 return means init_module failed; module can't be loaded. */ return 0; } void cleanup_module(void) { printk(KERN_INFO "Goodbye world 1.\n"); } 内核模块必须有至少两个函数:
一个“start”(初始化)函数叫做:init_modue()
这个函数当模块是被insmod调入内核时候来调用的。
第二个是“end”(清除)函数叫做cleanup_module()
这个函数是当被移除模块之前调用的。事实上,事情从内核2.3.13开始就发生改变了。你看在能用任何你喜欢的名字来作为一个模块的开始和结束函数,你将在2.3节学习如何做到这些。事实上,这个新的方法是优先方法。然而许多人仍然用init_module()
和cleanup_module()
来作为他们的开始和结束函数
init_module()
一般来说,不是 对于一些内核事务注册一个处理的方法 就是 用一个自己的代码(通常做一些事然后调用原有函数)代替一个内核函数。
clean_module()
函数用来撤销init_module()
做过的。所以模块能安全的卸载。
最后,每一个内核模块需要包含linux/module.h
我们需要包含linux/kernel.h
为了宏指令拓展到printk()
log层 ,KERN_ALERT
,这些你能在2.1.1章节学到。2.1.1引入printk()
尽管你可能会认为pintk()不是用来給用户传递信息的,即使我们用它来仅仅展示hello!这个恰巧是内核的日志机制,并且用来记录信息或者给出提示。因此每一个printk()申明伴随着一个优先级,那就是你看到的<1>和
KERN_ALERT
。这里有8个优先级,且内核有为他们的宏指令,所以你不必用神秘的数字,且你能在linux/kernel.h
中看到他们并且他们表示的意义。如果你不能区分优先等级,那默认优先级,DEFAULT_MESSAGE_LOGLEVEL
可以用到。
花时间读优先级宏指令。头文件同样描述了每个优先级的意思。实际上不要用任何数字像<4>总是用宏定义,如:KERN_WARNING
如果优先级小于整形console_loglevel
,这个消息被打印到你当前的终端上,如果有syslogd 和 klogd 在运行,,无论是否让它打印到控制台。这个消息都能由/var/log/messages
得到。我们用一个高优先级像KERN_ALERT
来确认printk()消息能打印到你的控制台而不是仅仅在日志文件中记录。当你写真实模块的时候,你将想要用这些优先级,这将对于手上的情况更有益。2.2编译内核模块
内核模块与通常的用户态的软件编译起来有一点不同。以前的内核版本需要我们更多的关注他们的设置,这通常存在Makefiles中。即使分层组织,许多冗余的设置累积起来在次级Makefiles 也能使他们很大且很难维护。幸运的是,现在有一种新的方式来做这做这些工作,叫做:kbuild,这个build的过程对于拓展的可装载的模块目前是完全整合到了标准内核build机制。要学更多的关于如何编译那些不是官方模块部分的模块(例如所有本文章中的例子)请看文件:
linux/Documentation/kbuild/modules.txt
。那么,让我们看看对于编译一个模块叫做
hello-1.c
的简单的Makefile:
例子 2-2. Makefile for basic kernel module (这里记得makefile的空格是8个)obj-m += hello-1.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
从技术的观点来看,仅仅第一行是真的必须的,“all”和“clean”目标单纯为了方便而加。
现在你能通过make命令来编译模块。你应该得到如下类似的输出:hostname:~/lkmpg-examples/02-HelloWorld# make make -C /lib/modules/2.6.11/build M=/root/lkmpg-examples/02-HelloWorld modules make[1]: Entering directory `/usr/src/linux-2.6.11' CC [M] /root/lkmpg-examples/02-HelloWorld/hello-1.o Building modules, stage 2. MODPOST CC /root/lkmpg-examples/02-HelloWorld/hello-1.mod.o LD [M] /root/lkmpg-examples/02-HelloWorld/hello-1.ko make[1]: Leaving directory `/usr/src/linux-2.6.11' hostname:~/lkmpg-examples/02-HelloWorld#
注意在内核2.6引入了一个新的文件命名转换:内核模块现在有
a.ko
后缀(取代原来老版本的.o后缀)这个后缀更容易与传统的对象文件区分。原因在于他们包含一个额外的.modinfo
选择,这个额外的关于模块的信息保留了下来。我们将很快看到这种信息的好处。
用modinfohello-*.ki
来看到的信息是:hostname:~/lkmpg-examples/02-HelloWorld# modinfo hello-1.ko filename: hello-1.ko vermagic: 2.6.11 preempt PENTIUMII 4KSTACKS gcc-3.3 depends:
到目前为止貌似没什么特别的。这个将在后面例子:
hello-5.ko
中对于曾经我们用的modinfo作的改变看到。hostname:~/lkmpg-examples/02-HelloWorld# modinfo hello-5.ko filename: hello-5.ko license: GPL author: Peter Jay Salzman vermagic: 2.6.11 preempt PENTIUMII 4KSTACKS gcc-3.3 depends: parm: myintArray:An array of integers (array of int) parm: mystring:A character string (charp) parm: mylong:A long integer (long) parm: myint:An integer (int) parm: myshort:A short integer (short) hostname:~/lkmpg-examples/02-HelloWorld#
你能看到许多有用的信息。
其他关于Makefiles对于内核模块的细节可以在linux/Documentation/kbuild/makefiles.txt
得到。在开始应付Makefiles之前确定读了这个文件以及一些相关文件能帮助你节约大量时间。
现在是时候用insmod ./hello-1.ko
注入你的新鲜的编译完了的模块到内核了(忽略任何你看到的关于内核的错误信息,我们将马上解决)
所有的模块装载到内核都列在了/proc/modules
直接去并显示文件就会看到你的模块已经是内核的一部分了。恭喜你,你现在是一名Linux内核代码的作者了!当这新鲜体验没了后,通过用rmmod hello-1
从内核移除你的模块.来看看/var/log/messages
可以看到纪录了你系统的日志。
(译注:)
我的ubuntu12的系统上的日志在/var/log/kern.log
中看到
我的Makefile是:
>
obj-m +=test.o
KDIR =//usr/src/linux-headers-3.13.0-63-generic
all:
(MAKE)−C (KDIR) SUBDIRS=$(PWD) modules
clean:
rm -rf .o .ko .mod. .symvers .order
这里对于读者有另外的练习。看到在init_module()
关于返回语句的注释?改变对于一些不好情况的返回值,重新编译然后再一次加载到模块,发生了什么?
2.3HelloWorld(part2)
在Linux2.4,你能重命名你自己模块的init 和 cleanup 函数,他们不再是固定的
init_module()
和cleanup_module()
这个可以在宏定义中用module_init()
和module_exit()
实现。这个宏定义在linux/init.h
中定义。唯一要注意的是(The only caveat is that)init 和 cleanup函数必须在调用宏定义之前,否则你编译就会报错。这个方法的例子如下:
例子2-3.hello-2.c/* * hello-2.c - Demonstrating the module_init() and module_exit() macros. * This is preferred over using init_module() and cleanup_module(). */ #include
/* Needed by all modules */ #include /* Needed for KERN_INFO */ #include /* Needed for the macros */ static int __init hello_2_init(void) { printk(KERN_INFO "Hello, world 2\n"); return 0; } static void __exit hello_2_exit(void) { printk(KERN_INFO "Goodbye, world 2\n"); } module_init(hello_2_init); module_exit(hello_2_exit); 所以现在我们有两个内核模块了,加入另一个模块就像如下方式一样简单:
例子2-4 Makefile 两个模块obj-m += hello-1.o obj-m += hello-2.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
现在看看
linux/drivers/char/Makefile
一个真实世界的例子,你能发现,一些已经融入到内核了(obj-y)但是那些obj-m去哪了?那些熟悉的shell脚本将很容易认出他们。对于那些没有的,obj-$(CONFIG_FOO)
入口,你能看到每个地方都拓展了obj-y活着obj-m,取决于CONFIG_FOO
变量是否设定为y或者m。在最后一次我当我们说make菜单配置或者类似的事情的时候当我们在这种模式下,那些你已经设定在了linux/.config
文件中的变量就恰好可以了。2.4Hello World (part 3): The __init and __exit Macros
这个表明了内核2.2以及以后版本的特性。注意到在定义init和cleanup函数的改变。
__init
宏使得init函数被丢弃,而且它的内存一旦初始化函数在built-in
驱动完成后也就释放了。如果你考虑何时初始化函数被唤起,那你有不错的感觉了!
这同样有一个__initdata
这个与__init
有同样的工作,但是这个用于初始化变量而不是函数
__exit
宏定义当模块被build到内核时候会造成产生一个函数排除,而且,如同__exit
对于可装载的模块没有任何作用。再一次的说,如果你考虑何时cleanup函数执行,这个完全没有意义;当可装载模块执行的时候,built-in
驱动不需要cleanup函数。
这些宏定义定义在linux/init.h
是为了释放内核内存的。当你引导你的内核且看到一些像这种:Freeing unused kernel memory: 236k freed
提示时,这个就是内核正在释放空间
例子:2-5 hello-3.c/* * hello-3.c - Illustrating the __init, __initdata and __exit macros. */ #include
/* Needed by all modules */ #include /* Needed for KERN_INFO */ #include /* Needed for the macros */ static int hello3_data __initdata = 3; static int __init hello_3_init(void) { printk(KERN_INFO "Hello, world %d\n", hello3_data); return 0; } static void __exit hello_3_exit(void) { printk(KERN_INFO "Goodbye, world 3\n"); } module_init(hello_3_init); module_exit(hello_3_exit); 2.5 Hello World (part 4): 许可证和模块文件
如果你用的是2.4的内核或者更高版本,你可以发现一些如下的信息装载到模块中
# insmod xxxxxx.o Warning: loading xxxxxx.ko will taint the kernel: no license See http://www.tux.org/lkml/#export-tainted for information about tainted modules Module xxxxxx loaded, with warnings
在2.4内核或者更高版本中,有一种机制在GPL下来区分代码许可证,所以人们会收到提醒,这个代码已经不是开源的了。这个由
MODULE_LICENSE()
宏定义来完成,这个用来声明明在后面的代码中。通过设置许可证到GPL,你能时刻得到来自屏幕的提醒,这个许可证机制定义在linux/module.h
文件中/* * The following license idents are currently accepted as indicating free * software modules * * "GPL" [GNU Public License v2 or later] * "GPL v2" [GNU Public License v2] * "GPL and additional rights" [GNU Public License v2 rights and more] * "Dual BSD/GPL" [GNU Public License v2 * or BSD license choice] * "Dual MIT/GPL" [GNU Public License v2 * or MIT license choice] * "Dual MPL/GPL" [GNU Public License v2 * or Mozilla license choice] * * The following other idents are available * * "Proprietary" [Non free products] * * There are dual licensed components, but when running with Linux it is the * GPL that is relevant so this is a non issue. Similarly LGPL linked with GPL * is a GPL combined work. * * This exists for several reasons * 1. So modinfo can show license info for users wanting to vet their setup * is free * 2. So the community can ignore bug reports including proprietary modules * 3. So vendors can do likewise based on their own policies */
同样的,
MODULE_DESCRIPTION()
用来描述这个模块做什么用的。MODOULE_AUTHOR()
用来描述模块的作者。MODULE_SUPPORTED_DEVICE()
申明模块支持什么样的操作。
这些宏定义都在linux/module.h
中,并且不被内核本身所用。他们仅仅是文件且能被视作对象收集处。给读者一个练习,尝试搜索在linux/drivers
搜索这些宏定义,看看模块作者如何使用这些宏定义来编撰他们的模块。
我建议使用类似于grep -inr MODULE_AUTHOR * in /usr/src/linux-2.6.x/
的操作。人们不熟悉用命令行工具,可能就用web的解决方案,搜索提供了内核树的站点来用LXR得到索引(或者安装到你自己本机)。
传统Unix的用户,例如emacs或者vi也可以发现有用的tag文件。他们能通过制作tags产生或者是在/usr/srclinux-2.6.x/
制作TAGS。一旦你在内核树得到这个标签文件你就能将光标移到某个函数调用上,然后用关键字匹配直接跳到定义的函数上。
例子:2-6.hello-4.c/* * hello-4.c - Demonstrates module documentation. */ #include
/* Needed by all modules */ #include /* Needed for KERN_INFO */ #include /* Needed for the macros */ #define DRIVER_AUTHOR "Peter Jay Salzman " #define DRIVER_DESC "A sample driver" static int __init init_hello_4(void) { printk(KERN_INFO "Hello, world 4\n"); return 0; } static void __exit cleanup_hello_4(void) { printk(KERN_INFO "Goodbye, world 4\n"); } module_init(init_hello_4); module_exit(cleanup_hello_4); /* * You can use strings, like this: */ /* * Get rid of taint message by declaring code as GPL. */ MODULE_LICENSE("GPL"); /* * Or with defines, like this: */ MODULE_AUTHOR(DRIVER_AUTHOR); /* Who wrote this module? */ MODULE_DESCRIPTION(DRIVER_DESC); /* What does this module do */ /* * This module uses /dev/testdevice. The MODULE_SUPPORTED_DEVICE macro might * be used in the future to help automatic configuration of modules, but is * currently unused other than for documentation purposes. */ MODULE_SUPPORTED_DEVICE("testdevice");
2.6 传递命令行参数到模块
模块能得到命令行参数,但是不是用你曾经用过到的
argc/argv
。
为了允许参数能传递到你的模块,申明一个变量来接收命令行值作为全局参数,然后这些使用module_param()
宏定义,(定义在linux/moduleparam.h
)来把机制开起来。在运行时候,insmod会用任意命令行提供的参数匹配变量,比方说:./insmod mymodule.ko myvariable=5.
这个变量申明和宏定义应该放在模式的开头来申明。范例代码应该清除我的糟糕的解释(my admittedly lousy explanation.)
module_param()
宏定义用3个参数:变量名,类型和在sysfs
中的相关的权限。(permissions for the corresponding file in sysfs. )。整数类型能被看作是有符号的或者无符号的。如果你想用整数数组或者字符串就看:module_param_array()
和module_param_string()
int myint = 3; module_param(myint,int,0);
数组也是支持的。但是现在与2.4时代有点不同。为了追踪参数的个数,你需要传一个指针变量作为第三个参数。随便你选择,你也可以无视这个直接传NULL。我们这里将两种可能:
int myintarray[2]; module_param_array(myintarray, int, NULL, 0); /* not interested in count */ int myshortarray[4]; int count; module_parm_array(myshortarray, short, , 0); /* put count into "count" variable */
设定默认值的模块变量,如端口活着IO地址,是好的做法。若果变量包含默认值,那么执行自动监测(在任何地方解释)。否则,保持当前的值。这个将后面讲清楚。
最后,这里有一个宏定义函数,MODULE_PARM_DESC()
,这个用来记录模块得到的参数。它有两个参数:一个变量名和一个字符串形式描述的变量。
例子2-7 hello-5.c/* * hello-5.c - Demonstrates command line argument passing to a module. */ #include
#include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Peter Jay Salzman"); static short int myshort = 1; static int myint = 420; static long int mylong = 9999; static char *mystring = "blah"; static int myintArray[2] = { -1, -1 }; static int arr_argc = 0; /* * module_param(foo, int, 0000) * The first param is the parameters name * The second param is it's data type * The final argument is the permissions bits, * for exposing parameters in sysfs (if non-zero) at a later stage. */ module_param(myshort, short, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); MODULE_PARM_DESC(myshort, "A short integer"); module_param(myint, int, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); MODULE_PARM_DESC(myint, "An integer"); module_param(mylong, long, S_IRUSR); MODULE_PARM_DESC(mylong, "A long integer"); module_param(mystring, charp, 0000); MODULE_PARM_DESC(mystring, "A character string"); /* * module_param_array(name, type, num, perm); * The first param is the parameter's (in this case the array's) name * The second param is the data type of the elements of the array * The third argument is a pointer to the variable that will store the number * of elements of the array initialized by the user at module loading time * The fourth argument is the permission bits * MODULE_PARM_DESC(参数名,“描述参数”) * module_param(参数名,类型,permission bits) */ module_param_array(myintArray, int, &arr_argc, 0000); MODULE_PARM_DESC(myintArray, "An array of integers"); static int __init hello_5_init(void) { int i; printk(KERN_INFO "Hello, world 5\n=============\n"); printk(KERN_INFO "myshort is a short integer: %hd\n", myshort); printk(KERN_INFO "myint is an integer: %d\n", myint); printk(KERN_INFO "mylong is a long integer: %ld\n", mylong); printk(KERN_INFO "mystring is a string: %s\n", mystring); for (i = 0; i < (sizeof myintArray / sizeof (int)); i++) { printk(KERN_INFO "myintArray[%d] = %d\n", i, myintArray[i]); } printk(KERN_INFO "got %d arguments for myintArray.\n", arr_argc); return 0; } static void __exit hello_5_exit(void) { printk(KERN_INFO "Goodbye, world 5\n"); } module_init(hello_5_init); module_exit(hello_5_exit); 我会建议用这个代码来玩玩:
satan# insmod hello-5.ko mystring="bebop" mybyte=255 myintArray=-1 mybyte is an 8 bit integer: 255 myshort is a short integer: 1 myint is an integer: 20 mylong is a long integer: 9999 mystring is a string: bebop myintArray is -1 and 420 satan# rmmod hello-5 Goodbye, world 5 satan# insmod hello-5.ko mystring="supercalifragilisticexpialidocious" \
mybyte=256 myintArray=-1,-1
mybyte is an 8 bit integer: 0
myshort is a short integer: 1
myint is an integer: 20
mylong is a long integer: 9999
mystring is a string: supercalifragilisticexpialidocious
myintArray is -1 and -1satan# rmmod hello-5 Goodbye, world 5 satan# insmod hello-5.ko mylong=hello hello-5.o: invalid argument syntax for mylong: 'h'
2.7多文件内核模块
有时候会将内核模块分成几个不同的源文件。这里就有个这样的例子:
例子2-8. start.c/* * start.c - Illustration of multi filed modules */ #include
/* We're doing kernel work */ #include /* Specifically, a module */ int init_module(void) { printk(KERN_INFO "Hello, world - this is the kernel speaking\n"); return 0; } 然后,makefile:
例子2-10 Makefileobj-m += hello-1.o obj-m += hello-2.o obj-m += hello-3.o obj-m += hello-4.o obj-m += hello-5.o obj-m += startstop.o startstop-objs := start.o stop.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
这个完整的makefile包含所有的至今我们看到过的例子。第5行没什么特别,就是最火一个例子,我们需要两行。第一行我们创造了一个对应object名字的模块,第二行我们告诉make 有哪些内核部分是这个object文件的。
2.8为预编译的内核Building模块
很显然,我们强烈支持你重编译你的内核,那么你能够开启一系列有用的debugging特性,例如
forced module unloading
即(MODULE_FORCE_UNLOAD
):当这个选项开启的时候,即使它认为不安全操作,你也能强制内核卸载一个模块。
尽管如此,当你可能想要装载你的模块到预编译的正在运行的模块时候也会伴随一些问题,例如你正装载的模块正在与一个普通版的Linux的程序同时运行,或者一个你以前编译过的内核。在这种情况,你可能需要编译和插入一个模块到正在运行的 不允许你重新编译或者你不想重启 的内核。如果你不能想到一个会迫使你对于一个预编译内核使用模块的情况,那么你可能想要跳过这节,然后将其他章节当作一个大注脚吧。
现在,如果你仅仅安装了一个内核源码树,用它来编译你的内核模块,然后你尝试插入你的模块到内核,大多数情况你会得到如下错误:
insmod: error inserting 'poet_atkm.ko': -1 Invalid module format
还有隐藏信息记录在:/var/log/messages
:
Jun 4 22:07:54 localhost kernel: poet_atkm: version magic '2.6.5-1.358custom 686
REGPARM 4KSTACKS gcc-3.3' should be '2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3'
也就是说,你的内核拒绝接受你的模块,因为版本串(更恰当的说是神奇的版本)不匹配。附带说,神奇的版本以一个静态字符串的形式存在模块对象中。开始于:vermagic:.
当模块连接到init/vermagic.o
文件时候,版本数据就会插入你的模块。为了检查神奇版本和其他存在你模块中的字符串,执行如下的modinfo命令:[root@pcsenonsrv 02-HelloWorld]# modinfo hello-4.ko license: GPL author: Peter Jay Salzman
description: A sample driver vermagic: 2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3 depends:
为了克服这个问题,我们能求助于
--force-vermagic
选项,但是这个解决方法有潜在的不安全性,毫无疑问的在产生模块的时候不可接受。因此,我们想要在一个与我们预编译内核相同的环境下编译我们自己的模块。如何做呢,那就是这章节剩下的主要内容了。
首先:确保一个内核源码树是可以用的。与你现在的内核拥有同样的版本。然后,找到用来编译你预编译内核的配置文件。通常,这个可以在/boot
的目录下得到。名字类似于config-2.6.x
。你可能想要拷贝它到你的内核源码树用:cp /boot/config-`uname -r` /usr/src/linux-`uname -r`/.config.
让我们重新关注前面的错误文件(message):一个看着类似的版本串表示即使用两个完全一样的配置文件,在版本上一点点的不同也有可能,这就足以阻止它插入模块到内核。这轻微的不同,叫做
custom
串,这个出现在模块版本中,而不是在内核中那个版本中,因为对于原版的尊敬的修改版,在makefile中,有一些分支包含了。然后,检验你的/usr/src/linux/Makefile
然后确认具体的版本信息完全匹配你现在的内核。例如你的makefile可能开始的描述如下:VERSION = 2 PATCHLEVEL = 6 SUBLEVEL = 5 EXTRAVERSION = -1.358custom ...
在这种情况下,你需要修复标识
EXTRAVERSION
的值到-1.358
,我们建议保留用来编译内核的makefile备份件,可以在/lib/modules/2.6.5-1.358/build
得到。简单点就是:cp /lib/modules/`uname -r`/build/Makefile /usr/src/linux-`uname -r`
这样应该足够了。还有就是,如果你已经用之前错误的Makefile开始build内核了,你也要重新运行make,或者根据文件
/lib/modules/2.6.x/build/include/linux/version.h
的内容直接修改标识符:UTS_RELEASE
这个文件在:/usr/src/linux-2.6.x/include/linux/version.h
。或者用前者覆盖后者。
现在请运行make来更新配置和版本头和对象:[root@pcsenonsrv linux-2.6.x]# make CHK include/linux/version.h UPD include/linux/version.h SYMLINK include/asm -> include/asm-i386 SPLIT include/linux/autoconf.h -> include/config/* HOSTCC scripts/basic/fixdep HOSTCC scripts/basic/split-include HOSTCC scripts/basic/docproc HOSTCC scripts/conmakehash HOSTCC scripts/kallsyms CC scripts/empty.o ...
如果你不想真实的编译内核,你在SPLIT那一行就打断build过程(CTRL-C),因为那个时候,你需要的文件已经准备好了。现在你能返回到你的内核目录了,然后编译它,它就能根据你现在的内核设置build了,然后装载进去不会有错误了。
第三章开场白
模块vs程序
3.1.1. 模块如何开始结束
一个程序通常开始于一个main()函数,执行一系列的指令分支,然后终结于这些指令。内核模块工作起来有点不同。一个模块总是开始于不是
init_module
就是你定义的特定用module_init
调用的函数。这个入口函数,它告诉内核这个模块所提供的功能并且让内核来在需要的时候来运行模块函数。一旦做了这些,入口函数返回 然后 模块直到内核想要用模块提供的代码做一些事情之前什么事情都不做。
所有的模块通过调用要么是cleanup_module
或者你用module_exit
调用的特定函数来结束。这个用来模块结束的函数;它撤销所有入口函数做的事情。它会撤销注册入口函数登记过的功能函数。
每一个模块必须有一个入口函数和一个退出函数。因为有不止一只一种方式来指定入口和退出函数,我将尽我努力来使用这些入口和退出函数,但是如果我遗漏了或者仅仅引用了他们像init_module
和cleanup_module
我想你会知道我想说什么的。3.1.2对于模块可用的函数
程序使用函数不会总是定义了的。一个很好的例子就是
printf()
。你用这些库函数不会真实地进入你的程序,直到你到连接(link)这一步才会,这一步用来保障代码(例如printf()
)可以用。,并且安装这些调用的指令去指向那些库中的代码。
内核模块也就是这里不同。在hello world例子中,你可能注意到我们用了函数printk()
但是不包含标准的I/O库。那是因为模块是object文件,它的标识直到insmod
过程中才决定。这些标识的定义来自于内核本身;你能使用的外置函数都是由内核提供的。如果你对此还有疑问关于你的内核给了什么样的标识,可以看看/proc/kallsyms
要记住的一点就是库函数和系统调用的不同。库函数有更高的等级,完全运行在用户态,且为程序到系统调用函数提供一个更方便的接口。系统调用运行在内核模式,由内核自身提供給用户的利益。库函数printf()
可能看起来像一个更通用的输出函数,但是他所有真正做的只是将数据格式化变成字符串,并且写字符串数据,用低等级系统调用write()
,这个write()
函数是发送数据到标准输出。
你想要看由系统调用printf()
做了什么吗?很容易,编译如下程序:#include
int main(void) { printf("hello"); return 0; } 用
gcc-Wall-o hello hello.c
用监控调试命令运行可执行文件(:strace ./hello
),有印象吗?每一行你看到的都对应一个系统调用。strace[4]是一个很好用的程序,它给了你很多关于系统调用一个程序来运行的细节,包含它做了什么调用,返回了什么样的参数。这是一个非常宝贵的工具用来解决像程序尝试进入一个什么样的文件的问题。到了最后,你会看到一行像:write(1, "hello", 5hello).
就是这个,这个就是printf()
背后的面具。你可能不熟悉write。因为大多数人用库函数来文件I/O(例如fopen,fputs,fclose)。如果那种情况,尝试看到操纵两个write,第二个操作部分是专用语系统调用(像kill()
和read()
)第3个操作部分是用来库函数调用,你可能更熟悉(例如cosh()
和random()
).
你甚至能写模块来替代内核的系统调用,我们后面会说。厉害的人总是利用这些来做后门和特洛伊木马,但是你能写你自己模块做更多有益的事,例如有内核写了Teehee,太好了!每次有人尝试删除你系统文件时候。(-_-!!)3.1.3用户空间vs内核空间
一个内核是一个讲进入到资源的,这个资源恰巧碰到一个显卡,一个硬件或者内存是否有问题。程序经常竞争同一个资源。当我存了一个文件,updatedb开始更新数据库中的文件。我的vim和updatedb同时使用设备。内核需要保持事务有序进行,且不能給用户进入资源无论何时他们想的时候。最后,CPU能运行不同模式。每一个模式给了一个不同的自由度等级来做他们能对系统做的。Intel80386结构有4种模式,叫做rings(环?):Unix仅仅使用2个rings,最高的ring(ring0,也就是我们所知道的
supervisor mode
这个模式所有事物都允许发生)还有个最低的ring叫做user mode
。
回到我们关于库函数vs系统调用讨论的来,典型的是,你用一个库函数是在用户模式。库函数调用一个或者多个系统调用,并且这些系统调用在库函数自己代表的下面执行,但是系统调用这是在supervisor模式下,因为他们是内核自己本身。一旦系统调用完成它的任务,它返回然后执行的转回到用户模式。3.1.4命名空间
当你写一个小C程序时候,你用的变量是很方便并且为用户所用。如果,在另一方面,你写的是一个大程序中的一个常规程序,然后你用的全局变量是其他人的变量;一些变量名字就会冲突。当一个程序有许多全局变量,并且有太多意思不好区分,你需要用命名空间。在大项目中,效果必须用来记住预留的名字,然后找到方法用独一无二的方法命名和标识来发展结构。
当系一个内核代码,即使最小的模块也会连接到整个内核,这就是个定义问题了。最好的方式来解决就是申明所有的你用的变量为一个静态的,然后用一个好的定义的前缀来标识你的变量。按照惯例说,所有的内核前缀是小写。如果你不想申明所有的变量为静态的,另一种可选的申明是:a symbol table
然后注册它到内核,我们将在下面介绍它。
文件/proc/kallsyms
有所有的内核知道的标识符,因此内核进入到你的模块是因为他们共享了内核的代码空间。3.1.5代码空间
内存管理是一个非常复杂的工程–O’Reilly的《Understanding The Linux Kernel》的大部分就是关于内存管理!我们不在内存管理上设定成为专家,但是,即使开始担心写真实的模块,我们也需要知道一些事情。
如果你没有想过关于段错误的真正含义,你可能会惊讶于听到指针不会真实指导一个内存空间。不是真实的空间,不管怎么说,当一个进程被创建,内核留出一部分真实物理空间然后提交给进程用来执行代码,变量,栈,堆和其他的只有电脑科学家才知道的东西。这个内存开始于0x00000000 然后 延伸到需要到达的地方。因为对于两个进程的内存空间不会重叠,每一个进程能进入一个内存地址,如:0xbffff978
,在真实物理内存中就会进入一个不同的地方!这个进程户进入一个索引,查找0xbffff978
指向一些offset,进入到内存的预留的原始区域,来执行特定进程。大部分来说,像我们的hello world程序这样的一个进程,不能进入到另一个进程的空间,即使我们后面会说有方法实现。
内核也有它自己的内存空间,因为一个模块代码能动态的插入和一处内核(对立面就是半自动化的对象),它共享了内核的代码空间而不是占为己用。因此,如果你的模块段错误,内核也会段错误。如果你开始因为差一错误来写数据,那么你会侵犯到内核数据(或者代码)。这个比听起来会更糟糕,所以请尽可能的小心。
顺便说,我想要指出上述讨论在任何使用巨大内核的操作系统都是真实的。还有叫微内核的,他们有模块就是自己拥有自己的代码段。GNU Hurd 和 QNX Neutrino就是两个微内核的例子。3.1.6设备驱动
模块的一个类别是一个设备的驱动,它提供了对于这个硬件的各个驱动,比方说显卡,串口。在Unix,硬件的每一块都由一个文件(在:
/dev
叫做a device file
)代表,文件提供了代表一个用户程序的通信手段。所以es1370.o
声卡驱动可能连接了/dev/sound
设备文件来驱动声卡。一个用户空间的程序例如mp3blaster能使用/dev/sound
但不需要知道装的是什么类型的声卡。3.1.6.1 主号辅号(驱动程序关联码和设备号)
让我们看一些设备文件,下面是在主要的masterIDE硬件驱动设备文件的头三个
# ls -l /dev/hda[1-3] brw-rw---- 1 root disk 3, 1 Jul 5 2000 /dev/hda1 brw-rw---- 1 root disk 3, 2 Jul 5 2000 /dev/hda2 brw-rw---- 1 root disk 3, 3 Jul 5 2000 /dev/hda3
注意到这些列由逗号分开的数字吗?第一个数字叫做设备主号,第二个数字叫做辅号。主号告诉你哪个设备用来进入硬件。每一个设备分配了一个独一无二的主号;所有的设备文件都用同样的主号去控制对应的设备。以上的主号是3,因为他们由同一个驱动控制。
辅号是驱动用来区分他控制的两个不同的硬件的。对于上述例子返回的,虽然三个设备都由同一个驱动处理,但是他们有独一无二的辅号,因为驱动看到他们是硬件的不同部分。
设备被分成两个部分:字符设备和块设备。两者不同的是,块设备有一个请求的缓存空间,那么他们能选择最好的顺序来应对请求。这对于在存储设备上是很重要的,因为它能更快在相近的扇区的读和写。而不要分散数据。另一个不同就是,那个块设备仅仅能以块的形式接收输入和返回输出(大小由设备决定)。然而,字符设备允许使用尽可能多的小字节。大多数设备都是字符设备,因为他们并不需要这种缓存类型,并且他们不需要操作限定大小的块。你能告诉一个设备文件是否是块设备还是字符设备通过看在输出文件用ls -l
的第一个字符,如果是b那就是块设备,如果是c那就是字符设备。上述的设备我们看到的都是块设备。这里有一些字符设备(串口:):crw-rw---- 1 root dial 4, 64 Feb 18 23:34 /dev/ttyS0 crw-r----- 1 root dial 4, 65 Nov 17 10:26 /dev/ttyS1 crw-rw---- 1 root dial 4, 66 Jul 5 2000 /dev/ttyS2 crw-rw---- 1 root dial 4, 67 Jul 5 2000 /dev/ttyS3
如果你想看到哪个是被分配的主号,你能看到:
/usr/src/linux/Documentation/devices.txt.
当系统被装好了后,所有那些设备文件就会由mknod命令创建。问了创建一个新的字符设备叫做:coffee
,并用主辅号12和2,仅仅需要:do mknod /dev/coffee c 12 2
你不必要将你的设备文件放到/dev
但是一般情况下会。Linux把它的设备文件放到/dev
然后就看你了。然后当创建一个设备文件来测试时候,它可能放到你自己的那个编译内核的工作目录下OK。仅仅确定当你正在写设备驱动的时候是放到了正确地方。
我想要造一些由上述讨论的隐藏的last point,但是我还是做了详述来以防万一。当一个设备文件被进入了,内核使用文件的主号来决定那个驱动应该被使用来处理入口。这个就表示内核不是真正使用或者甚至知道辅号(设备号)。驱动自己本身就是唯一关心设备号的。它使用设备号来在硬件的不同地方做区分。
同时,你要记住,当我说`硬件的时候,我表示的是比PCI卡还要抽象点的东西。看看下面两个设备文件:% ls -l /dev/fd0 /dev/fd0u1680 brwxrwxrwx 1 root floppy 2, 0 Jul 5 2000 /dev/fd0 brw-rw---- 1 root floppy 2, 44 Jul 5 2000 /dev/fd0u1680
到目前为止,你能看到这两个设备文件并且马上知道他们是块设备,并且由同一个驱动处理(块 主号:2)你甚至可以知道这些都代表了你的软驱设备,即使你只有一个软驱设备。为什么2个文件?一个代表软盘驱动有1.44MB的存储空间。另一个是同样的软盘驱动有1.68MB的存储空间。然后对应有的调用一个超级格式的磁盘。这种磁盘比标准格式的软盘携带更多的数据。所以,现在有一种情况,两个设备文件用同一个设备号,事实上代表同一片物理硬件。所以只要知道我们所讨论的硬件能表示非常抽象的东西。
第四章,字符设备文件
4.1字符设备驱动