admin管理员组文章数量:1531316
2024年7月24日发(作者:)
3
作者声明...........................................................................................................................3
版本和注意.......................................................................................................................3
感谢...................................................................................................................................3
译者注...............................................................................................................................3
4
什么是内核模块?...........................................................................................................4
内核模块是如何被调入内核工作的?...........................................................................4
7
Hello,World(part1):最简单的内核模块......................................................................7
HelloWorld(part2)..........................................................................................................9
HelloWorld(part3):关于__init和__exit宏................................................................10
HelloWorld(part4):内核模块证书和内核模块文档说明..........................................11
从命令行传递参数给内核模块.....................................................................................13
由多个文件构成的内核模块.........................................................................................16
为已编译的内核编译模块.............................................................................................18
.20
内核模块和用户程序的比较.................................................................................................20
内核模块是如何开始和结束的.....................................................................................20
模块可调用的函数.........................................................................................................20
用户空间和内核空间.....................................................................................................21
命名空间.........................................................................................................................21
代码空间.........................................................................................................................22
22
24
字符设备文件.........................................................................................................................24
关于file_operations结构体.....................................................................................24
/.32
关于/proc文件系统...................................................................................................32
/.36
使用/proc作为输入...................................................................................................36
43
与设备文件对话(writesandIOCTLs)..........................................................................43
.56
系统调用.........................................................................................................................56
62
阻塞进程.........................................................................................................................62
70
替换70
让你的键盘指示灯闪起来.............................................................................................72
76
任务调度.........................................................................................................................76
81
.81
86
对称多线程处理.............................................................................................................86
LINUX内核模块编程[转]
87
注意.................................................................................................................................87
89
为什么这样写?.............................................................................................................89
2
LINUX内核模块编程[转]
Foreword
作者声明
《Linux内核驱动模块编程指南》最初是由OriPomerantz为2.2版本的内核编写的,后来,Ori将
文档维护的任务交给了PeterJaySalzman,Peter完成了2.4内核版本文档的编写,毕竟Linux内
核驱动模块是一个更新很快的内容。现在,Peter也无法腾出足够的时间来完成2.6内核版本文档的编
写,目前该2.6内核版本的文档由合作者MichaelBurian完成。
版本和注意
Linux内核模块是一块不断更新进步的内容,在LKMPG上总有关于是否保留还是历史版本的争论。
Michael和我最终是决定为每个新的稳定版本内核建立一个新的文档分支。也就是说LKMPG2.4.x专
注于2.4的内核,而LKMPG2.6.x将专注于2.6的内核。我们不会在一篇文档中提供对旧版本内核的
支持,对此感兴趣的读者应该寻找相关版本的文档分支。
在文档中的绝大部分源代码和讨论都应该适用于其它平台,但我无法提供任何保证。其中的一个例外就
是Chapter12,中断处理该章的源代码和讨论就只适用于x86平台。
感谢
感谢下列人士为此文档提供了他们宝贵的意见。他们是:IgnacioMartin,DavidPorter,Daniele
Paolo,Scarpazza和DimoVelev。
译者注
我,译者,名叫田竞,目前是一名在北京邮电大学就读的通信专业的大学生。自高中起我就是Linux的
爱好者并追随至今。我喜欢Linux给我带来的自由,我想大家也一样。没有人不向往自由。
我学习内核模块编写时最初阅读的是Orelly出版社的使用2.0版本的内核的书籍,但如同我预料的一样,
书中的许多事例由于内核代码的变化而无法使用。这让想亲自体验内核模块的神秘的我非常苦恼。我在
Linux文档计划在上海的镜像站上找到了这篇文档。
受原作者Ori的鼓励,基于上次完成的LKMPG2.4的,内容有稍许的改变和扩充。应该是目前最新的
了。翻译的方式有所改变,在基于LDP认可的docbook格式上翻译,通过docbook2html转换为附
件中的html文档。由于对docbook不是很熟悉,其中的一些标题尚未翻译,而且可能破坏了原有的
tag,导致html出现一些错误显示,但总体来说很少。修改了很多2.4中的错别字。
学习并分享学习的过程是我翻译的最终目的。
3
LINUX内核模块编程[转]
uction
什么是内核模块?
现在,你是不是想编写内核模块。你应该懂得C语言,写过一些用户程序,那么现在你将要见识一些真
实的东西。在这里,你会看到一个野蛮的指针是如何毁掉你的文件系统的,一次内核崩溃意味着重启动。
什么是内核模块?内核模块是一些可以让操作系统内核在需要时载入和执行的代码,这同样意味着它可
以在不需要时有操作系统卸载。它们扩展了操作系统内核的功能却不需要重新启动系统。举例子来说,
其中一种内核模块时设备驱动程序模块,它们用来让操作系统正确识别,使用安装在系统上的硬件设备。
如果没有内核模块,我们不得不一次又一次重新编译生成单内核操作系统的内核镜像来加入新的功能。
这还意味着一个臃肿的内核。
内核模块是如何被调入内核工作的?
你可以通过执行lsmod命令来查看内核已经加载了哪些内核模块,该命令通过读取/proc/modules文
件的内容来获得所需信息。
这些内核模块是如何被调入内核的?当操作系统内核需要的扩展功能不存在时,内核模块管理守护进程
kmod[1]执行modprobe去加载内核模块。两种类型的参数被传递给modprobe:
一个内核模块的名字像softdog或是ppp。
通用识别符像char-major-10-30。
当传递给modprobe是通用识别符时,modprobe首先在文件/etc/查找该字符串。如
果它发现的一行别名像:
aliaschar-major-10-30softdog
它就明白通用识别符是指向内核模块softdog.o。
然后,modprobe遍历文件/lib/modules/version/来判断是否有其它内核模块需要在
该模块加载前被加载。该文件是由命令depmod-a建立,保存着内核模块的依赖关系。举例来说,
msdos.o依赖于模块fat.o内核模块已经被内核载入。当要加载的内核模块需要使用别的模块提供的符
号链接时(多半是变量或函数),那么那些提供这些所需符号链接的内核模块就被该模块所依赖。
最终,modprobe调用insmod先加载被依赖的模块,然后加载该被内核要求的模块。modprobe将
insmod指向/lib/modules/version/[2]目录,该目录为默认标准存放内核模块的目录。insmod对内
核模块存放位置的处理相当呆板,所以modprobe应该很清楚的知道默认标准的内核模块存放的位置。
所以,当你想要载入一个内核模块时,你可以执行:
4
LINUX内核模块编程[转]
insmod/lib/modules/2.5.1/kernel/fs/fat/fat.o
insmod/lib/modules/2.5.1/kernel/fs/msdos/msdos.o
或只是执行"modprobe-amsdos"。
Linux提供modprobe,insmodanddepmod在一个名为modutils或mod-utils的工具包内。
在结束本章前,让我们来看一个/etc/文件:
#Thisfileisautomaticallygeneratedbyupdate-modules
path[misc]=/lib/modules/2.4.?/local
keep
path[net]=~p/mymodules
optionsmydriverirq=10
aliaseth0eepro
用'#'起始的行为注释。空白行被忽略。
以path[misc]起始的行告诉modprobe用/lib/modules/2.4.?/local替代搜寻misc内核模块的路
径。正如你看到的,命令解释器shell的元字符也可以使用。
以path[net]起始的行告诉modprobe在目录~p/mymodules搜索网络方面的内核模块。但是,在
path[net]指令之前使用的"keep"指令告诉modprobe只是将该路径添加到标准搜索路径中,而不是
像对待misc前面那样进行替换。
以alias起始的的行使modprobe加载eepro.o当kmod以通用识别符'eth0'要求加载相应内核模块
时。
你不会发现像"aliasblock-major-2floppy"这样的别名行在文件/etc/因为
modprobe已经知道在绝大多数系统上安装的标准的设备的驱动模块。
现在你已经知道内核模块是如何被调入的了。当你想写你自己的依赖于其它模块的内核模块时,还有一
些内容没有提供。这个相对高级的问题将在以后的章节中介绍,当我们已经完成前面的学习后。
在开始前
在我们介绍源代码前,有一些事需要注意。系统彼此之间的不同会导致许多困难。顺利的编译并且加载
你的第一个"helloworld"模块有时就会比较困难。但是当你跨过这道坎时,后面会顺利的多。
内核模块和内核的版本问题
为某个版本编译的模块将不能被另一个版本的内核加载如果内核中打开了CONFIG_MODVERSIONS
选项。我们暂时不会讨论与此相关的内容。在我们进入相关内容前,本文档中的范例可能在该选项打开
的情况下无法工作。但是,目前绝大多数的发行版是将该选项打开的。所以如果你遇到和版本相关的
错误时,最好,重新编译一个关闭该选项的内核。
使用X带来的问题
强烈建议你在控制台下输入文档中的范例代码,编译然后加载模块,而不是在X下。
5
LINUX内核模块编程[转]
模块不能像printf()那样输出到屏幕,但它们可以记录信息和警告,当且仅当你在使用控制台时这些信
息才能最终显示在屏幕上。如果你从xterm中insmod一个模块,这些日志信息只会记录在你的日志
文件中。除了查看日志文件你将无法得到输出信息。想要及时的获得这些日志信息,建议所有的工作
都在控制台下进行。
编译相关和内核版本相关的问题
Linux的发行版经常给内核打一些非标准的补丁,这种情况回导致一些问题的发生。
一个更普遍的问题是一些Linux发行版提供的头文件不完整。编译模块时你将需要非常多的内核头文
件。墨菲法则之一就是那些缺少的头文件恰恰是你最需要的。
我强烈建议从Linux镜像站点下载源代码包,编译新内核并用新内核启动系统来避免以上的问题。参阅
"LinuxKernelHOWTO"获得详细内容。
具有讽刺意味的是,这也会导致一些问题。gcc倾向于在缺省的内核源文件路径(通常是/usr/src/)下寻
找源代码文件。这可以通过gcc的-I选项来切换。
Notes
[1]在早期的linux版本中,是一个名为kerneld的守护进程。
[2]如果你在修改内核,为避免覆盖你现在工作的模块,你应该试试使用内核Makefile中的变量
EXTRAVERSION去建立一个独立的模块目录。
6
LINUX内核模块编程[转]
orld
Hello,World(part1):最简单的内核模块
当第一个洞穴程序员在第一台洞穴计算机的墙上上凿写第一个程序时,这是一个在羚羊皮上输出`Hello,
world'的字符串。罗马的编程书籍上是以`Salut,Mundi'这样的程序开始的。我不明白人们为什么要
破坏这个传统,但我认为还是不明白为好。我们将从编写一系列的`Hello,world'模块开始,一步步
展示编写内核模块的基础的方方面面。
这可能是一个最简单的模块了。先别急着编译它。我们将在下章模块编译的章节介绍相关内容。
-1.c
/*
*hello-1.c-Thesimplestkernelmodule.
*/
#include
#include
intinit_module(void)
{
printk("<1>Helloworld1.n");
/*
*Anon0returnmeansinit_modulefailed;modulecan'tbeloaded.
*/
return0;
}
voidcleanup_module(void)
{
printk(KERN_ALERT"Goodbyeworld1.n");
}
一个内核模块应该至少包含两个函数。一个“开始”(初始化)的函数被称为init_module()还有一个“结
束”(干一些收尾清理的工作)的函数被称为cleanup_module(),当内核模块被rmmod卸载时被执行。
实际上,从内核版本2.3.13开始这种情况有些改变。你可以为你的开始和结束函数起任意的名字。你
将在以后学习如何实现这一点theSectioncalledHelloWorld(part2)。实际上,这个新方法时推
荐的实现方法。但是,许多人仍然使init_module()和cleanup_module()作为他们的开始和结束函数。
一般,init_module()要么向内核注册它可以处理的事物,要么用自己的代码替代某个内核函数(代码通
常这样做然后再去调用原先的函数代码)。函数cleanup_module()应该撤消任何init_module()做的
事,从而内核模块可以被安全的卸载。
7
/*Neededbyallmodules*/
/*NeededforKERN_ALERT*/
LINUX内核模块编程[转]
最后,任一个内核模块需要包含linux/module.h。我们仅仅需要包含linux/kernel.h当需要使用
printk()记录级别的宏扩展时KERN_ALERT,相关内容将在theSectioncalled介绍printk()中介绍。
介绍printk()
不管你可能怎么想,printk()并不是设计用来同用户交互的,虽然我们在hello-1就是出于这样的目的
使用它!它实际上是为内核提供日志功能,记录内核信息或用来给出警告。因此,每个printk()声明
都会带一个优先级,就像你看到的<1>和KERN_ALERT那样。内核总共定义了八个优先级的宏,所
以你不必使用晦涩的数字代码,并且你可以从文件linux/kernel.h查看这些宏和它们的意义。如果你不
指明优先级,默认的优先级DEFAULT_MESSAGE_LOGLEVEL将被采用。
阅读一下这些优先级的宏。头文件同时也描述了每个优先级的意义。在实际中,使用宏而不要使用数字,
就像<4>。总是使用宏,就像KERN_WARNING。
当优先级低于intconsole_loglevel,信息将直接打印在你的终端上。如果同时syslogd和klogd都
在运行,信息也同时添加在文件/var/log/messages,而不管是否显示在控制台上与否。我们使用像
KERN_ALERT这样的高优先级,来确保printk()将信息输出到控制台而不是只是添加到日志文件中。
当你编写真正的实用的模块时,你应该针对可能遇到的情况使用合适的优先级。
编译内核模块
内核模块在用gcc编译时需要使用特定的参数。另外,一些宏同样需要定义。这是因为在编译成可执行
文件和内核模块时,内核头文件起的作用是不同的。以往的内核版本需要我们去在Makefile中手动设
置这些设定。尽管这些Makefile是按目录分层次安排的,但是这其中有许多多余的重复并导致代码树
大而难以维护。幸运的是,一种称为kbuild的新方法被引入,现在外部的可加载内核模块的编译的方
法已经同内核编译统一起来。想了解更多的编译非内核代码树中的模块(就像我们将要编写的)请参考帮
助文件linux/Documentation/kbuild/。
现在让我们看一个编译名字叫做hello-1.c的模块的简单的Makefile文件:
Example2-2.一个基本的Makefile
obj-m+=hello-1.o
现在你可以通过执行命令make-C/usr/src/linux-`uname-r`SUBDIRS=$PWDmodules编译
模块。你应该得到同下面类似的屏幕输出:
[root@pcsenonsrvtest_module]#make-C/usr/src/linux-`uname-r`SUBDIRS=$PWD
modules
make:Enteringdirectory`/usr/src/linux-2.6.x
CC[M]/root/test_module/hello-1.o
Buildingmodules,stage2.
MODPOST
CC/root/test_module/.o
LD[M]/root/test_module/
make:Leavingdirectory`/usr/src/linux-2.6.x
8
LINUX内核模块编程[转]
请注意2.6的内核现在引入一种新的内核模块命名规范:内核模块现在使用.ko的文件后缀(代替以往
的.o后缀),这样内核模块就可以同普通的目标文件区别开。更详细的文档请参考
linux/Documentation/kbuild/。在研究Makefile之前请确认你已经参考了这些文档。
现在是使用insmod./命令加载该模块的时候了(忽略任何你看到的关于内核污染的输出显
示,我们将在以后介绍相关内容)。
所有已经被加载的内核模块都罗列在文件/proc/modules中。cat一下这个文件看一下你的模块是否真
的成为内核的一部分了。如果是,祝贺你!你现在已经是内核模块的作者了。当你的新鲜劲过去后,使
用命令rmmodhello-1.卸载模块。再看一下/var/log/messages文件的内容是否有相关的日志内容。
这儿是另一个练习。看到了在声明init_module()上的注释吗?改变返回值非零,重新编译再加载,发
生了什么?
HelloWorld(part2)
在内核Linux2.4中,你可以为你的模块的“开始”和“结束”函数起任意的名字。它们不再必须使用
init_module()和cleanup_module()的名字。这可以通过宏module_init()和module_exit()实现。
这些宏在头文件linux/init.h定义。唯一需要注意的地方是函数必须在宏的使用前定义,否则会有编译错
误。下面就是一个例子。
-2.c
9
LINUX内核模块编程[转]
/*
*hello-2.c-Demonstratingthemodule_init()andmodule_exit()macros.
*Thisispreferredoverusinginit_module()andcleanup_module().
*/
#include
#include
#include
staticint__inithello_2_init(void)
{
printk(KERN_ALERT"Hello,world2n");
return0;
}
staticvoid__exithello_2_exit(void)
{
printk(KERN_ALERT"Goodbye,world2n");
}
module_init(hello_2_init);
module_exit(hello_2_exit);
现在我们已经写过两个真正的模块了。添加编译另一个模块的选项十分简单,如下:
Example2-4.两个内核模块使用的Makefile
obj-m+=hello-1.o
obj-m+=hello-2.o
现在让我们来研究一下linux/drivers/char/Makefile这个实际中的例子。就如同你看到的,一些被编
译进内核(obj-y),但是这些obj-m哪里去了呢?对于熟悉shell脚本的人这不难理解。这些在Makefile
中随处可见的obj-$(CONFIG_FOO)的指令将会在CONFIG_FOO被设置后扩展为你熟悉的obj-y或
obj-m。这其实就是你在使用makemenuconfig编译内核时生成的linux/.config中设置的东西。
/*Neededbyallmodules*/
/*NeededforKERN_ALERT*/
/*Neededforthemacros*/
HelloWorld(part3):关于__init和__exit宏
这里展示了内核2.2以后引入的一个新特性。注意在负责“初始化”和“清理收尾”的函数定义处的变化。
宏__init的使用会在初始化完成后丢弃该函数并收回所占内存,如果该模块被编译进内核,而不是动态
加载。
宏__initdata同__init类似,只不过对变量有效。
宏__exit将忽略“清理收尾”的函数如果该模块被编译进内核。同宏__exit一样,对动态加载模块是无
效的。这很容易理解。编译进内核的模块是没有清理收尾工作的,而动态加载的却需要自己完成这些工
作。
10
LINUX内核模块编程[转]
这些宏在头文件linux/init.h定义,用来释放内核占用的内存。当你在启动时看到这样的Freeing
unusedkernelmemory:236kfreed内核输出,上面的那些正是内核所释放的。
-3.c
/*
*hello-3.c-Illustratingthe__init,__initdataand__exitmacros.
*/
#include
#include
#include
/*Neededbyallmodules*/
/*NeededforKERN_ALERT*/
/*Neededforthemacros*/
staticinthello3_data__initdata=3;
staticint__inithello_3_init(void)
{
printk(KERN_ALERT"Hello,world%dn",hello3_data);
return0;
}
staticvoid__exithello_3_exit(void)
{
printk(KERN_ALERT"Goodbye,world3n");
}
module_init(hello_3_init);
module_exit(hello_3_exit);
HelloWorld(part4):内核模块证书和内核模块文档说明
如果你在使用2.4或更新的内核,当你加载你的模块时,你也许注意到了这些输出信息:
#insmodhello-3.o
Warning:aintthekernel:nolicense
See/lkml/#export-taintedforinformationabouttaintedmodules
Hello,world3
Modulehello-3loaded,withwarnings
在2.4或更新的内核中,一种识别代码是否在GPL许可下发布的机制被引入,因此人们可以在使用非
公开的源代码产品时得到警告。这通过在下一章展示的宏MODULE_LICENSE()当你设置在GPL证书
下发布你的代码时,你可以取消这些警告。这种证书机制在头文件linux/module.h实现,同时还有一
些相关文档信息。
11
LINUX内核模块编程[转]
/*
*Thefollowinglicenseidentsarecurrentlyacceptedasindicatingfree
*softwaremodules
*
*
*
*
*
*
*
*
*
*Thefollowingotheridentsareavailable
*
*
*
*Thereareduallicensedcomponents,butwhenrunningwithLinuxitisthe
*rlyLGPLlinkedwithGPL
*isaGPLcombinedwork.
*
*Thisexistsforseveralreasons
*1.
*
*2.
*3.
*/
类似的,宏MODULE_DESCRIPTION()用来描述模块的用途。宏MODULE_AUTHOR()用来声明模
块的作者。宏MODULE_SUPPORTED_DEVICE()声明模块支持的设备。
这些宏都在头文件linux/module.h定义,并且内核本身并不使用这些宏。它们只是用来提供识别信息,
可用工具程序像objdump查看。作为一个练习,使用grep从目录linux/drivers看一看这些模块的
作者是如何为他们的模块提供识别信息和档案的。
-4.c
Somodinfocanshowlicenseinfoforuserswantingtovettheirsetup
isfree
Sothecommunitycanignorebugreportsincludingproprietarymodules
Sovendorscandolikewisebasedontheirownpolicies
"Proprietary"[Nonfreeproducts]
"DualMPL/GPL"
"GPL"
"GPLv2"
"GPLandadditionalrights"
"DualBSD/GPL"
[GNUPublicLicensev2orlater]
[GNUPublicLicensev2]
[GNUPublicLicensev2rightsandmore]
[GNUPublicLicensev2
orBSDlicensechoice]
[GNUPublicLicensev2
orMozillalicensechoice]
12
LINUX内核模块编程[转]
/*
*hello-4.c-Demonstratesmoduledocumentation.
*/
#include
#include
#include
#defineDRIVER_AUTHOR"PeterJaySalzman
"
#defineDRIVER_DESC"Asampledriver"
staticint__initinit_hello_4(void)
{
printk(KERN_ALERT"Hello,world4n");
return0;
}
staticvoid__exitcleanup_hello_4(void)
{
printk(KERN_ALERT"Goodbye,world4n");
}
module_init(init_hello_4);
module_exit(cleanup_hello_4);
/*
*Youcanusestrings,likethis:
*/
/*
*GetridoftaintmessagebydeclaringcodeasGPL.
*/
MODULE_LICENSE("GPL");
/*
*Orwithdefines,likethis:
*/
MODULE_AUTHOR(DRIVER_AUTHOR);
MODULE_DESCRIPTION(DRIVER_DESC);
/*
*Thismoduleuses/dev/ULE_SUPPORTED_DEVICEmacromight
*beusedinthefuturetohelpautomaticconfigurationofmodules,butis
*currentlyunusedotherthanfordocumentationpurposes.
*/
MODULE_SUPPORTED_DEVICE("testdevice");
/*Whowrotethismodule?*/
/*Whatdoesthismoduledo*/
从命令行传递参数给内核模块
13
LINUX内核模块编程[转]
模块也可以从命令行获取参数。但不是通过以前你习惯的argc/argv。
要传递参数给模块,首先将获取参数值的变量声明为全局变量。然后使用宏MODULE_PARM()(在头文
件linux/module.h)。运行时,insmod将给变量赋予命令行的参数,如同./insmodmymodule.o
myvariable=5。为使代码清晰,变量的声明和宏都应该放在模块代码的开始部分。以下的代码范例也
许将比我公认差劲的解说更好。
宏MODULE_PARM()需要两个参数,变量的名字和其类型。支持的类型有"b":比特型,"h":短整型,
"i":整数型,"l:长整型和"s":字符串型,其中正数型既可为signed也可为unsigned。字符串类型
应该声明为"char*"这样insmod就可以为它们分配内存空间。你应该总是为你的变量赋初值。这是内
核编程,代码要编写的十分谨慎。举个例子:
intmyint=3;
char*mystr;
MODULE_PARM(myint,"i");
MODULE_PARM(mystr,"s");
数组同样被支持。在宏MODULE_PARM中在类型符号前面的整型值意味着一个指定了最大长度的数组。
用'-'隔开的两个数字则分别意味着最小和最大长度。下面的例子中,就声明了一个最小长度为2,最大长
度为4的整形数组。
intmyshortArray[4];
MODULE_PARM(myintArray,"3-9i");
将初始值设为缺省使用的IO端口或IO内存是一个不错的作法。如果这些变量有缺省值,则可以进行自
动设备检测,否则保持当前设置的值。我们将在后续章节解释清楚相关内容。在这里我只是演示如何向
一个模块传递参数。
最后,还有这样一个宏,MODULE_PARM_DESC()被用来注解该模块可以接收的参数。该宏两个参数:
变量名和一个格式自由的对该变量的描述。
-5.c
14
/*
*hello-5.c-Demonstratescommandlineargumentpassingtoamodule.
*/
LINUX内核模块编程[转]
#include
#include
#include
#include
MODULE_LICENSE("GPL");
MODULE_AUTHOR("PeterJaySalzman");
staticshortintmyshort=1;
staticintmyint=420;
staticlongintmylong=9999;
staticchar*mystring="blah";
/*
*module_param(foo,int,0000)
*Thefirstparamistheparametersname
*Thesecondparamisit'sdatatype
*Thefinalargumentisthepermissionsbits,
*forexposingparametersinsysfs(ifnon-zero)atalaterstage.
*/
module_param(myshort,short,S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP);
MODULE_PARM_DESC(myshort,"Ashortinteger");
module_param(myint,int,S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
MODULE_PARM_DESC(myint,"Aninteger");
module_param(mylong,long,S_IRUSR);
MODULE_PARM_DESC(mylong,"Alonginteger");
module_param(mystring,charp,0000);
MODULE_PARM_DESC(mystring,"Acharacterstring");
staticint__inithello_5_init(void)
{
printk(KERN_ALERT"Hello,world5n=============n");
printk(KERN_ALERT"myshortisashortinteger:%hdn",myshort);
printk(KERN_ALERT"myintisaninteger:%dn",myint);
printk(KERN_ALERT"mylongisalonginteger:%ldn",mylong);
printk(KERN_ALERT"mystringisastring:%sn",mystring);
return0;
}
staticvoid__exithello_5_exit(void)
{
printk(KERN_ALERT"Goodbye,world5n");
}
module_init(hello_5_init);
我建议用下面的方法实验你的模块:
module_exit(hello_5_exit);
15
#include
LINUX内核模块编程[转]
satan#ing="bebop"mybyte=255myintArray=-1
mybyteisan8bitinteger:255
myshortisashortinteger:1
myintisaninteger:20
mylongisalonginteger:9999
mystringisastring:bebop
myintArrayis-1and420
satan#rmmodhello-5
Goodbye,world5
satan#ing="supercalifragilisticexpialidocious"
>mybyte=256myintArray=-1,-1
mybyteisan8bitinteger:0
myshortisashortinteger:1
myintisaninteger:20
mylongisalonginteger:9999
mystringisastring:supercalifragilisticexpialidocious
myintArrayis-1and-1
satan#rmmodhello-5
Goodbye,world5
satan#g=hello
hello-5.o:invalidargumentsyntaxformylong:'h'
由多个文件构成的内核模块
有时将模块的源代码分为几个文件是一个明智的选择。在这种情况下,你需要:
只要在一个源文件中添加#define__NO_VERSION__预处理命令。这很重要因为module.h通常包
含kernel_version的定义,此时一个存储着内核版本的全局变量将会被编译。但如果此时你又要包含
头文件version.h,你必须手动包含它,因为module.h不会再包含它如果打开预处理选项
__NO_VERSION__。
像通常一样编译。
将所有的目标文件连接为一个文件。在x86平台下,使用命令ld-melf_i386-r-o
<1stsrcfile.o><2ndsrcfile.o>。
此时Makefile一如既往会帮我们完成编译和连接的脏活。
这里是这样的一个模块范例。
16
LINUX内核模块编程[转]
.c
/*
*start.c-Illustrationofmultifiledmodules
*/
#include
#include
intinit_module(void)
{
printk("Hello,world-thisisthekernelspeakingn");
return0;
}
另一个文件:
.c
/*
*stop.c-Illustrationofmultifiledmodules
*/
#include
#include
voidcleanup_module()
{
printk("<1>Shortisthelifeofakernelmodulen");
}
最后是该模块的Makefile:
le
/*We'redoingkernelwork*/
/*Specifically,amodule*/
/*We'redoingkernelwork*/
/*Specifically,amodule*/
17
LINUX内核模块编程[转]
obj-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:=.o
为已编译的内核编译模块
很显然,我们强烈推荐你编译一个新的内核,这样你就可以打开内核中一些有用的排错功能,像强制卸
载模块(MODULE_FORCE_UNLOAD):当该选项被打开时,你可以rmmod-fmodule强制内核卸
载一个模块,即使内核认为这是不安全的。该选项可以为你节省不少开发时间。
但是,你仍然有许多使用一个正在运行中的已编译的内核的理由。例如,你没有编译和安装新内核的权
限,或者你不希望重启你的机器来运行新内核。如果你可以毫无阻碍的编译和使用一个新的内核,你可
以跳过剩下的内容,权当是一个脚注。
如果你仅仅是安装了一个新的内核代码树并用它来编译你的模块,当你加载你的模块时,你很可能会得
到下面的错误提示:
insmod:errorinserting'poet_':-1Invalidmoduleformat
一些不那么神秘的信息被纪录在文件/var/log/messages中;
Jun422:07:54localhostkernel:poet_atkm:versionmagic'2.6.5-1.358custom686
REGPARM4KSTACKSgcc-3.3'shouldbe'2.6.5-1.358686REGPARM4KSTACKSgcc-3.3'
换句话说,内核拒绝加载你的模块因为记载版本号的字符串不符(更确切的说是版本印戳)。版本印戳作
为一个静态的字符串存在于内核模块中,以vermagic:。版本信息是在连接阶段从文件
init/vermagic.o中获得的。查看版本印戳和其它在模块中的一些字符信息,可以使用下面的命令
:
[root@pcsenonsrv02-HelloWorld]#
license:
author:
description:
vermagic:
depends:
我们可以借助选项--force-vermagic解决该问题,但这种方法有潜在的危险,所以在成熟的模块中也是
不可接受的。解决方法是我们构建一个同我们预先编译好的内核完全相同的编译环境。如何具体实现将
是该章后面的内容。
GPL
PeterJaySalzman
Asampledriver
2.6.5-1.358686REGPARM4KSTACKSgcc-3.3
18
LINUX内核模块编程[转]
首先,准备同你目前的内核版本完全一致的内核代码树。然后,找到你的当前内核的编译配置文件。通
常它可以在路径/boot下找到,使用像config-2.6.x的文件名。你可以直接将它拷贝到内核代码树的
路径下:cp/boot/config-`uname-r`/usr/src/linux-`uname-r`/.config。
让我们再次注意一下先前的错误信息:仔细看的话你会发现,即使使用完全相同的配置文件,版本印戳
还是有细小的差异的,但这足以导致模块加载的失败。这其中的差异就是在模块中出现却不在内核中出
现的custom字符串,是由某些发行版提供的修改过的makefile导致的。检查
/usr/src/linux/Makefile,确保下面这些特定的版本信息同你使用的内核完全一致:
VERSION=2
PATCHLEVEL=6
SUBLEVEL=5
EXTRAVERSION=-1.358custom
...
像上面的情况你就需要将EXTRAVERSION一项改为-1.358。我们的建议是将原始的makefile备份在
/lib/modules/2.6.5-1.358/build
的Makefile
下。一个简单的命令cp/lib/modules/`uname
-r`/build/Makefile/usr/src/linux-`uname-r`即可。另外,如果你已经在运行一个由上面的错误
编译的内核,你应该重新执行
从
make,或直接对应
文件/lib/modules/2.6.x/build/include/linux/version.h
/usr/src/linux-2.6.x/include/linux/version.h修改UTS_RELEASE,或用前者覆盖后者的。
现在,请执行make来更新设置和版本相关的头文件,目标文件:
[root@pcsenonsrvlinux-2.6.x]#make
CHK
UPD
SPLIT
include/linux/version.h
include/linux/version.h
include/linux/autoconf.h->include/config/*
SYMLINKinclude/asm->include/asm-i386
HOSTCCscripts/basic/fixdep
HOSTCCscripts/basic/split-include
HOSTCCscripts/basic/docproc
HOSTCCscripts/conmakehash
HOSTCCscripts/kallsyms
CC
...
如果你不是确实想编译一个内核,你可以在SPLIT后通过按下CTRL-C中止编译过程。因为此时你需要
的文件已经就绪了。现在你可以返回你的模块目录然后编译加载它:此时模块将完全针对你的当前内核
编译,加载时也不会由任何错误提示。
scripts/empty.o
19
LINUX内核模块编程[转]
inaries
内核模块和用户程序的比较
内核模块是如何开始和结束的
用户程序通常从函数main()开始,执行一系列的指令并且当指令执行完成后结束程序。内核模块有一
点不同。内核模块要么从函数init_module或是你用宏module_init指定的函数调用开始。这就是内
核模块的入口函数。它告诉内核模块提供那些功能扩展并且让内核准备好在需要时调用它。当它完成
这些后,该函数就执行结束了。模块在被内核调用前也什么都不做。
所有的模块或是调用cleanup_module或是你用宏module_exit指定的函数。这是模块的退出函数。
它撤消入口函数所做的一切。例如注销入口函数所注册的功能。
所有的模块都必须有入口函数和退出函数。既然我们有不只一种方法去定义这两个函数,我将努力使用
“入口函数”和“退出函数”来描述它们。但是当我只用init_module和cleanup_module时,我希望你
明白我指的是什么。
模块可调用的函数
程序员并不总是自己写所有用到的函数。一个常见的基本的例子就是printf()你使用这些C标准库,libc
提供的库函数。这些函数(像printf())实际上在连接之前并不进入你的程序。在连接时这些函数调用
才会指向你调用的库,从而使你的代码最终可以执行。
内核模块有所不同。在helloworld模块中你也许已经注意到了我们使用的函数printk()却没有包含标
准I/O库。这是因为模块是在insmod加载时才连接的目标文件。那些要用到的函数的符号链接是内核
自己提供的。也就是说,你可以在内核模块中使用的函数只能来自内核本身。如果你对内核提供了哪
些函数符号链接感兴趣,看一看文件/proc/kallsyms。
需要注意的一点是库函数和系统调用的区别。库函数是高层的,完全运行在用户空间,为程序员提供调
用真正的在幕后完成实际事务的系统调用的更方便的接口。系统调用在内核态运行并且由内核自己提
供。标准C库函数printf()可以被看做是一个通用的输出语句,但它实际做的是将数据转化为符合格式
的字符串并且调用系统调用write()输出这些字符串。
是否想看一看printf()究竟使用了哪些系统调用?这很容易,编译下面的代码。
#include
intmain(void)
{printf("hello");return0;}
20
LINUX内核模块编程[转]
使用命令gcc-Wall-ohellohello.c编译。用命令stracehello行该可执行文件。是否很惊讶?每一
行都和一个系统调用相对应。strace[1]是一个非常有用的程序,它可以告诉你程序使用了哪些系统调
用和这些系统调用的参数,返回值。这是一个极有价值的查看程序在干什么的工具。在输出的末尾,你
应该看到这样类似的一行write(1,"hello",5hello)。这就是我们要找的。藏在面具printf()的真实面
目。既然绝大多数人使用库函数来对文件I/O进行操作(像fopen,fputs,fclose)。你可以查看man
说明的第二部分使用命令man2write.。man说明的第二部分专门介绍系统调用(像kill()和read())。
man说明的第三部分则专门介绍你可能更熟悉的库函数,(像cosh()和random())。
你甚至可以编写代码去覆盖系统调用,正如我们不久要做的。骇客常这样做来为系统安装后门或木马。但
你可以用它来完成一些更有益的事,像让内核在每次某人删除文件时输出“Teehee,thattickles!”的
信息。
用户空间和内核空间
内核全权负责对硬件资源的访问,不管被访问的是显示卡,硬盘,还是内存。用户程序常为这些资源竞
争。就如同我在保存这份文档同时本地数据库正在更新。我的编辑器vim进程和数据库更新进程同时
要求访问硬盘。内核必须使这些请求有条不紊的进行,而不是随用户的意愿提供计算机资源。为方便
实现这种机制,CPU可以在不同的状态运行。不同的状态赋予不同的你对系统操作的自由。Intel80836
架构有四种状态。Unix只使用了其中的两种,最高级的状态(操作状态0,即“超级状态”,可以执行任
何操作)和最低级的状态(即“用户状态”)。
回忆以下我们对库函数和系统调用的讨论,一般库函数在用户态执行。库函数调用一个或几个系统调用,
而这些系统调用为库函数完成工作,但是在超级状态。一旦系统调用完成工作后系统调用就返回同时程
序也返回用户态。
命名空间
如果你只是写一些短小的C程序,你可为你的变量起一个方便的和易于理解的变量名。但是,如果你写
的代码只是许多其它人写的代码的一部分,你的全局一些就会与其中的全局变量发生冲突。另一个情
况是一个程序中有太多的难以理解的变量名,这又会导致变量命名空间污染在大型项目中,必须努力
记住保留的变量名,或为独一无二的命名使用一种统一的方法。
当编写内核代码时,即使是最小的模块也会同整个内核连接,所以这的确是个令人头痛的问题。最好的
解决方法是声明你的变量为static静态的并且为你的符号使用一个定义的很好的前缀。传统中,使用小
写字母的内核前缀。如果你不想将所有的东西都声明为static静态的,另一个选择是声明一个symbol
table(符号表)并向内核注册。我们将在以后讨论。
文件/proc/kallsyms保存着内核知道的所有的符号,你可以访问它们,因为它们是内核代码空间的一
部分。
21
LINUX内核模块编程[转]
代码空间
内存管理是一个非常复杂的课题。O'Reilly的《UnderstandingTheLinuxKernel》绝大部分都在讨
论内存管理!我们并不准备专注于内存管理,但有一些东西还是得知道的。
如果你没有认真考虑过内存设计缺陷意味着什么,你也许会惊讶的获知一个指针并不指向一个确切的内
存区域。当一个进程建立时,内核为它分配一部分确切的实际内存空间并把它交给进程,被进程的代码,
变量,堆栈和其它一些计算机学的专家才明白的东西使用[2]。这些内存从$0$开始并可以扩展到需要
的地方。这些内存空间并不重叠,所以即使进程访问同一个内存地址,例如0xbffff978,真实的物理
内存地址其实是不同的。进程实际指向的是一块被分配的内存中以0xbffff978为偏移量的一块内存区
域。绝大多数情况下,一个进程像普通的"Hello,World"不可以访问别的进程的内存空间,尽管有实现
这种机制的方法。我们将在以后讨论。
内核自己也有内存空间。既然一个内核模块可以动态的从内核中加载和卸载,它其实是共享内核的内存
空间而不是自己拥有独立的内存空间。因此,一旦你的模块具有内存设计缺陷,内核就是内存设计缺陷
了。如果你在错误的覆盖数据,那么你就在破坏内核的代码。这比现在听起来的还糟。所以尽量小心
谨慎。
顺便提一下,以上我所指出的对于任何单整体内核的操作系统都是真实的[3]。也存在模块化微内核
的操作系统,如GNUHurd和QNXNeutrino。
DeviceDrivers
一种内核模块是设备驱动程序,为使用硬件设备像电视卡和串口而编写。在Unix中,任何设备都被当
作路径/dev的设备文件处理,并通过这些设备文件提供访问硬件的方法。设备驱动为用户程序访问硬
件设备。举例来说,声卡设备驱动程序es1370.o将会把设备文件/dev/sound同声卡硬件Ensoniq
IS1370联系起来。这样用户程序像mp3blaster就可以通过访问设备文件/dev/sound运行而不必
知道那种声卡硬件安装在系统上。
MajorandMinorNumbers
让我们来研究几个设备文件。这里的几个设备文件代表着一块主IDE硬盘上的头三个分区:
#ls-l/dev/hda[1-3]
brw-rw----1rootdisk3,1Jul52000/dev/hda1
brw-rw----1rootdisk3,2Jul52000/dev/hda2
brw-rw----1rootdisk3,3Jul52000/dev/hda3
注意一下被逗号隔开的两列。第一个数字被叫做主设备号,第二个被叫做从设备号。主设备号决定使用
何种设备驱动程序。每种不同的设备都被分配了不同的主设备号;所有具有相同主设备号的设备文件
都是被同一个驱动程序控制。上面例子中的主设备号都为3,表示它们都被同一个驱动程序控制。
从设备号用来区别驱动程序控制的多个设备。上面例子中的从设备号不相同是因为它们被识别为几个设
备。
22
LINUX内核模块编程[转]
设备被大概的分为两类:字符设备和块设备。区别是块设备有缓冲区,所以它们可以对请求进行优化排
序。这对存储设备尤其重要,因为读写相邻的文件总比读写相隔很远的文件要快。另一个区别是块设
备输入和输出都是以数据块为单位的,但是字符设备就可以自由读写任意量的字节。大部分硬件设备
为字符设备,因为它们不需要缓冲区和数据不是按块来传输的。你可以通过命令ls-l输出的头一个字
母识别一个设备为何种设备。如果是'b'就是块设备,如果是'c'就是字符设备。以上你看到的是块设备。
这儿还有一些字符设备文件(串口):
crw-rw----1rootdial4,64Feb1823:34/dev/ttyS0
crw-r-----1rootdial4,65Nov1710:26/dev/ttyS1
crw-rw----1rootdial4,66Jul52000/dev/ttyS2
crw-rw----1rootdial4,67Jul52000/dev/ttyS3
如果你想看一下已分配的主设备号都是些什么设备可以看一下文件
/usr/src/linux/Documentation/。
系统安装时,所有的这些设备文件都是由命令mknod建立的。去建立一个新的名叫coffee',主设备号
为12和从设备号为2的设备文件,只要简单的执行命令mknod/dev/coffeec122。你并不是必须
将设备文件放在目录/dev中,这只是一个传统。Linus本人是这样做的,所以你最好也不例外。但是,
当你测试一个模块时,在工作目录建立一个设备文件也不错。只要保证完成后将它放在驱动程序找得到
的地方。
我还想声明在以上讨论中隐含的几点。当系统访问一个系统文件时,系统内核只使用主设备号来区别设
备类型和决定使用何种内核模块。系统内核并不需要知道从设备号。内核模块驱动本身才关注从设备号,
并用之来区别其操纵的不同设备。
另外,我这儿提到的硬件是比那种可以握在手里的PCI卡稍微抽象一点的东西。看一下下面的两个设备
文件:
%ls-l/dev/fd0/dev/fd0u1680
brwxrwxrwx
brw-rw----
1rootfloppy
1rootfloppy
2,0Jul52000/dev/fd0
2,44Jul52000/dev/fd0u1680
你现在立即明白这是快设备的设备文件并且它们是有相同的驱动内核模块来操纵(主设备号都为2))。
你也许也意识到它们都是你的软盘驱动器,即使你实际上只有一个软盘驱动器。为什么是两个设备文
件?因为它们其中的一个代表着你的1.44MB容量的软驱,另一个代表着你的1.68MB容量的,被某
些人称为“超级格式化”的软驱。这就是一个不同的从设备号代表着相同硬件设备的例子。请清楚的意识
到我们提到的硬件有时可能是非常抽象的。
Notes
[1]这是一个去跟踪程序究竟在做什么的非常有价值的工具。
[2]我是物理专业的,而不是主修计算机。
[3]这不同于将所有的内核模块编译进内核,但意思确实是一样的。
23
LINUX内核模块编程[转]
terDeviceFiles
字符设备文件
关于file_operations结构体
结构体file_operations在头文件linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种
操作的函数的指针。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的事务的函数的地
址。
举个例子,每个字符设备需要定义一个用来读取设备数据的函数。结构体file_operations中存储着内
structfile_operations{
structmodule*owner;
loff_t(*llseek)(structfile*,loff_t,int);
ssize_t(*read)(structfile*,char__user*,size_t,loff_t*);
ssize_t(*aio_read)(structkiocb*,char__user*,size_t,loff_t);
ssize_t(*write)(structfile*,constchar__user*,size_t,loff_t*);
ssize_t(*aio_write)(structkiocb*,constchar__user*,size_t,
loff_t);
int(*readdir)(structfile*,void*,filldir_t);
unsignedint(*poll)(structfile*,structpoll_table_struct*);
int(*ioctl)(structinode*,structfile*,unsignedint,
unsignedlong);
int(*mmap)(structfile*,structvm_area_struct*);
int(*open)(structinode*,structfile*);
int(*flush)(structfile*);
int(*release)(structinode*,structfile*);
int(*fsync)(structfile*,structdentry*,intdatasync);
int(*aio_fsync)(structkiocb*,intdatasync);
int(*fasync)(int,structfile*,int);
int(*lock)(structfile*,int,structfile_lock*);
ssize_t(*readv)(structfile*,conststructiovec*,unsignedlong,
loff_t*);
ssize_t(*writev)(structfile*,conststructiovec*,unsignedlong,
loff_t*);
ssize_t(*sendfile)(structfile*,loff_t*,size_t,read_actor_t,
void__user*);
ssize_t(*sendpage)(structfile*,structpage*,int,size_t,
loff_t*,int);
unsignedlong(*get_unmapped_area)(structfile*,unsignedlong,
24
unsignedlong,unsignedlong,
unsignedlong);
};
LINUX内核模块编程[转]
核模块中执行这项操作的函数的地址。一下是该结构体在内核2.6.5中看起来的样子:
驱动内核模块是不需要实现每个函数的。像视频卡的驱动就不需要从目录的结构中读取数据。那么,相
对应的file_operations重的项就为NULL。
gcc还有一个方便使用这种结构体的扩展。你会在较现代的驱动内核模块中见到。新的使用这种结构体
的方式如下:
structfile_operationsfops={
read:device_read,
write:device_write,
open:device_open,
release:device_release
};
同样也有C99语法的使用该结构体的方法,并且它比GNU扩展更受推荐。我使用的版本为2.95为了
方便那些想移植你的代码的人,你最好使用这种语法。它将提高代码的兼容性:
structfile_operationsfops={
.read=device_read,
.write=device_write,
.open=device_open,
.release=device_release
};
这种语法很清晰,你也必须清楚的意识到没有显示声明的结构体成员都被gcc初始化为NULL。
指向结构体structfile_operations的指针通常命名为fops。
关于file结构体
每一个设备文件都代表着内核中的一个file结构体。该结构体在头文件linux/fs.h定义。注意,file结
构体是内核空间的结构体,这意味着它不会在用户程序的代码中出现。它绝对不是在glibc中定义的
FILE。FILE自己也从不在内核空间的函数中出现。它的名字确实挺让人迷惑的。它代表着一个抽象的
打开的文件,但不是那种在磁盘上用结构体inode表示的文件。
指向结构体structfile的指针通常命名为filp。你同样可以看到structfilefile的表达方式,但不要被
它诱惑。
去看看结构体file的定义。大部分的函数入口,像结构体structdentry没有被设备驱动模块使用,你
大可忽略它们。这是因为设备驱动模块并不自己直接填充结构体file:它们只是使用在别处建立的结构
体file中的数据。
25
LINUX内核模块编程[转]
注册一个设备
如同先前讨论的,字符设备通常通过在路径/dev[1]设备文件访问。主设备号告诉你哪些驱动模块是用
来操纵哪些硬件设备的。从设备号是驱动模块自己使用来区别它操纵的不同设备,当此驱动模块操纵不
只一个设备时。
将内核驱动模块加载入内核意味着要向内核注册自己。这个工作是和驱动模块获得主设备号时初始化一
同进行的。你可以使用头文件linux/fs.h中的函数register_chrdev来实现。
intregister_chrdev(unsignedintmajor,constchar*name,structfile_operations*fops);
其中unsignedintmajor是你申请的主设备号,constchar*name是将要在文件/proc/devices
structfile_operations*fops是指向你的驱动模块的file_operations表的指针。负的返回值意味着
注册失败。注意注册并不需要提供从设备号。内核本身并不在意从设备号。
现在的问题是你如何申请到一个没有被使用的主设备号?最简单的方法是查看文件
Documentation/从中挑选一个没有被使用的。这不是一劳永逸的方法因为你无法得知该
主设备号在将来会被占用。最终的方法是让内核为你动态分配一个。
如果你向函数register_chrdev传递为0的主设备号,那么返回的就是动态分配的主设备号。副作用
就是既然你无法得知主设备号,你就无法预先建立一个设备文件。有多种解决方法。第一种方法是新
注册的驱动模块会输出自己新分配到的主设备号,所以我们可以手工建立需要的设备文件。第二种是利
用文件/proc/devices新注册的驱动模块的入口,要么手工建立设备文件,要么编一个脚本去自动读
取该文件并且生成设备文件。第三种是在我们的模块中,当注册成功时,使用mknod统调用建立设备
文件并且调用rm删除该设备文件在驱动模块调用函数cleanup_module前。
注销一个设备
即使时root也不能允许随意卸载内核模块。当一个进程已经打开一个设备文件时我们卸载了该设备文
件使用的内核模块,我们此时再对该文件的访问将会导致对已卸载的内核模块代码内存区的访问。幸运
的话我们最多获得一个讨厌的错误警告。如果此时已经在该内存区加载了另一个模块,倒霉的你将会在
内核中跳转执行意料外的代码。结果是无法预料的,而且多半是不那么令人愉快的。
平常,当你不允许某项操作时,你会得到该操作返回的错误值(一般为一负的值)。但对于无返回值的
函数cleanup_module这是不可能的。然而,却有一个计数器跟踪着有多少进程正在使用该模块。你
可以通过查看文件/proc/modules的第三列来获取这些信息。如果该值非零,则卸载就会失败。你不
需要在你模块中的函数cleanup_module中检查该计数器,因为该项检查由头文件linux/module.c
中定义的系统调用sys_delete_module完成。你也不应该直接对该计数器进行操作。你应该使用在
文件linux/modules.h定义的宏来增加,减小和读取该计数器:
try_module_get(THIS_MODULE):Incrementtheusecount.
try_module_put(THIS_MODULE):Decrementtheusecount.
26
LINUX内核模块编程[转]
保持该计数器时刻精确是非常重要的;如果你丢失了正确的计数,你将无法卸载模块,那就只有重启了。
不过这种情况在今后编写内核模块时也是无法避免的。
chardev.c
下面的代码示范了一个叫做chardev的字符设备。你可以用cat输出该设备文件的内容(或用别的程序
打开它)时,驱动模块会将该设备文件被读取的次数显示。目前对设备文件的写操作还不被支持(像echo
"hi">/dev/hello),但会捕捉这些操作并且告诉用户该操作不被支持。不要担心我们对读入缓冲区的
数据做了什么;我们什么都没做。我们只是读入数据并输出我们已经接收到的数据的信息。
v.c
/*
*chardev.c:Createsaread-onlychardevicethatsayshowmanytimes
*you'vereadfromthedevfile
*/
#include
#include
#include
#include
/*
*
*/
intinit_module(void);
voidcleanup_module(void);
staticintdevice_open(structinode*,structfile*);
staticintdevice_release(structinode*,structfile*);
staticssize_tdevice_read(structfile*,char*,size_t,loff_t*);
staticssize_tdevice_write(structfile*,constchar*,size_t,loff_t*);
#defineSUCCESS0
#defineDEVICE_NAME"chardev"
#defineBUF_LEN80
/*
*Globalvariablesaredeclaredasstatic,soareglobalwithinthefile.
*/
staticintMajor;
staticintDevice_Open=0;
staticcharmsg[BUF_LEN];
staticchar*msg_Ptr;
/*Majornumberassignedtoourdevicedriver*/
/*Isdeviceopen?
/*Themsgthedevicewillgivewhenasked*/
*Usedtopreventmultipleaccesstodevice*/
/*Devnameasitappearsin/proc/devices
/*Maxlengthofthemessagefromthedevice*/
*/
/*forput_user*/
27
LINUX内核模块编程[转]
staticstructfile_operationsfops={
.read=device_read,
.write=device_write,
.open=device_open,
.release=device_release
};
/*
*Functions
*/
intinit_module(void)
{
Major=register_chrdev(0,DEVICE_NAME,&fops);
if(Major<0){
printk("Registeringthecharacterdevicefailedwith%dn",
Major);
returnMajor;
}
printk("<1>Iwasassignedmajornumber%ton",Major);
printk("<1>thedriver,createadevfilewithn");
printk("'mknod/dev/helloc%d0'.n",Major);
printk("<1>atandechoton");
printk("thedevicefile.n");
printk("<1>Removethedevicefileandmodulewhendone.n");
return0;
}
voidcleanup_module(void)
{
/*
*Unregisterthedevice
*/
intret=unregister_chrdev(Major,DEVICE_NAME);
if(ret<0)
printk("Errorinunregister_chrdev:%dn",ret);
}
28
LINUX内核模块编程[转]
/*
*Methods
*/
/*
*Calledwhenaprocesstriestoopenthedevicefile,like
*"cat/dev/mycharfile"
*/
staticintdevice_open(structinode*inode,structfile*file)
{
staticintcounter=0;
if(Device_Open)
return-EBUSY;
Device_Open++;
sprintf(msg,"Ialreadytoldyou%dtimesHelloworld!n",counter++);
msg_Ptr=msg;
try_module_get(THIS_MODULE);
returnSUCCESS;
}
/*
*Calledwhenaprocessclosesthedevicefile.
*/
staticintdevice_release(structinode*inode,structfile*file)
{
Device_Open--;/*We'renowreadyforournextcaller*/
/*
*Decrementtheusagecount,orelseonceyouopenedthefile,you'll
*nevergetgetridofthemodule.
*/
module_put(THIS_MODULE);
return0;
}
29
LINUX内核模块编程[转]
/*
*Calledwhenaprocess,whichalreadyopenedthedevfile,attemptsto
*readfromit.
*/
staticssize_tdevice_read(structfile*filp,
char*buffer,
size_tlength,
loff_t*offset)
{
/*
*Numberofbytesactuallywrittentothebuffer
*/
intbytes_read=0;
/*
*Ifwe'reattheendofthemessage,
*return0signifyingendoffile
*/
if(*msg_Ptr==0)
return0;
/*
*Actuallyputthedataintothebuffer
*/
while(length&&*msg_Ptr){
/*
*Thebufferisintheuserdatasegment,notthekernel
*segmentso"*"assignmentwon'touse
*put_userwhichcopiesdatafromthekerneldatasegmentto
*theuserdatasegment.
*/
put_user(*(msg_Ptr++),buffer++);
length--;
bytes_read++;
}
/*
*Mostreadfunctionsreturnthenumberofbytesputintothebuffer
*/
returnbytes_read;
}
/*seeinclude/linux/fs.h
/*buffertofillwithdata*/
/*lengthofthebuffer*/
*/
30
LINUX内核模块编程[转]
/*
*Calledwhenaprocesswritestodevfile:echo"hi">/dev/hello
*/
staticssize_t
device_write(structfile*filp,constchar*buff,size_tlen,loff_t*off)
{
printk("<1>Sorry,thisoperationisn'tsupported.n");
return-EINVAL;
}
为多个版本的内核编写内核模块
系统调用,也就是内核提供给进程的接口,基本上是保持不变的。也许会添入新的系统调用,但那些已
有的不会被改动。这对于向下兼容是非常重要的。在多数情况下,设备文件是保持不变的。但内核的内
部在不同版本之间还是会有区别的。
Linux内核分为稳定版本(版本号中间为偶数)和试验版本(版本号中间为奇数)。试验版本中可以试
验各种各样的新而酷的主意,有些会被证实是一个错误,有些在下一版中会被完善。总之,你不能依赖
这些版本中的接口(这也是我不在本文档中支持它们的原因,它们更新的太快了)。在稳定版本中,我
们可以期望接口保持一致,除了那些修改代码中错误的版本。
如果你要支持多版本的内核,你需要编写为不同内核编译的代码树。可以通过比较宏
LINUX_VERSION_CODE和宏KERNEL_VERSION在版本号为a.b.c的内核中,该宏的值应该为
2^16×a+2^8×b+c
在上一个版本中该文档还保留了详细的如何向后兼容老内核的介绍,现在我们决定打破这个传统。对为
老内核编写驱动感兴趣的读者应该参考对应版本的LKMPG,也就是说,2.4.x版本的LKMPG对应2.4.x
的内核,2.6.x版本的LKMPG对应2.6.x的内核。
Notes
[1]这只是习惯上的。将设备文件放在你的用户目录下是没有问题的。但是当真正提供成熟的驱动模块
时,请保证将设备文件放在/dev下。
31
LINUX内核模块编程[转]
/procFileSystem
关于/proc文件系统
在Linux中有另一种内核和内核模块向进程传递信息的方法,那就是通过/proc文件系统。它原先设计
的目的是为查看进程信息提供一个方便的途径,现在它被用来向用户提供各种内核中被感兴趣的内容。
像文件/proc/modules里是已加载模块的列表,文件/proc/meminfo里是关于内存使用的信息。
使用proc文件系统的方法同使用设备文件很相似。你建立一个包含/proc文件需要的所有信息的结构
体,这其中包括处理各种事务的函数的指针(在我们的例子中,只用到从/proc文件读取信息的函数)。
然后在init_module时向内核注册这个结构体,在cleanup_module时注销这个结构体。
我们使用proc_register_dynamic[1]的原因是我们不用去设置inode,而留给内核去自动分配从而避
免系统冲突错误。普通的文件系统是建立在磁盘上的,而/proc的文件仅仅是建立在内存中的。在前
种情况中,inode的数值是一个指向存储在磁盘某个位置的文件的索引节点(inode就是index-node
的缩写)。该索引节点储存着文件的信息,像文件的权限;同时还有在哪儿能找到文件中的数据。
因为我们无法得知该文件是被打开的或关闭的,我们也无法去使用宏try_module_get和
try_module_put在下面的模块中,我们无法避免该文件被打开而同时模块又被卸载。在下章中我将介
绍一个较难实现,却更灵活,更安全的处理/proc文件的方法。
.c
/*
*procfs.c-createa"file"in/proc
*/
#include
#include
#include
/*Specifically,amodule*/
/*We'redoingkernelwork*/
/*Necessarybecauseweusetheprocfs*/
structproc_dir_entry*Our_Proc_File;
/*Putdataintotheprocfsfile.
*
*Arguments
*=========
*ferwherethedataistobeinserted,if
*
*
*
youdecidetouseit.
usefulifyoudon'twanttousethebuffer
allocatedbythekernel.
*
*rentpositioninthefile
32
LINUX内核模块编程[转]
*eofthebufferinthefirstargument.
*"1"heretoindicateEOF.
*ertodata(usefulincaseonecommon
*readformultiple/proc/...entries)
*
*UsageandReturnValue
*======================
*Areturnvalueofzeromeansyouhavenofurther
*informationatthistime(endoffile).Anegative
*returnvalueisanerrorcondition.
*
*ForMoreInformation
*====================
*ThewayIdiscoveredwhattodowiththisfunction
*wasn'tbyreadingdocumentation,butbyreadingthe
*ookedtoseewhatuses
*theget_infofieldofproc_dir_entrystruct(Iuseda
*combinationoffindandgrep,ifyou'reinterested),
*andIsawthatitisusedin *directory>/fs/ * *Ifsomethingisunknownaboutthekernel,thisis *xwehavethegreat *advantageofhavingthekernelsourcecodefor *free-useit. */ 33 LINUX内核模块编程[转] ssize_t procfile_read(char*buffer, char**buffer_location, off_toffset,intbuffer_length,int*eof,void*data) { printk(KERN_INFO"inside/proc/test:procfile_readn"); intlen=0; staticintcount=1; /* *Wegiveallofourinformationinonego,soiftheuserasksusifwehavemore informationtheanswershouldalwaysbeno. * *Thisisimportantbecausethestandardreadfunctionfromthelibrarywould continuetoissuethereadsystemcalluntilthekernelrepliesthatithasnomore information,oruntilits *bufferisfilled. */ if(offset>0){ printk(KERN_INFO"offset%d:/proc/test:procfile_read, wrote%dBytesn",(int)(offset),len); *eof=1; returnlen; } /* *Fillthebufferandgetitslength */ len=sprintf(buffer, "Forthe%d%stime,goaway!n",count, (count%100>10&&count%100<14)?"th": (count%10==1)?"st": (count%10==2)?"nd": (count%10==3)?"rd":"th"); count++; /* *Returnthelength */ printk(KERN_INFO "leaving/proc/test:procfile_read,wrote%dBytesn",len); returnlen; } /*Thenumberofbytesactuallyused*/ 34 LINUX内核模块编程[转] intinit_module() { intrv=0; Our_Proc_File=create_proc_entry("test",0644,NULL); Our_Proc_File->read_proc=procfile_read; Our_Proc_File->owner=THIS_MODULE; Our_Proc_File->mode=S_IFREG|S_IRUGO; Our_Proc_File->uid=0; Our_Proc_File->gid=0; Our_Proc_File->size=37; printk(KERN_INFO"Tryingtocreate/proc/test:n"); if(Our_Proc_File==NULL){ rv=-ENOMEM; remove_proc_entry("test",&proc_root); printk(KERN_INFO"Error:Couldnotinitialize/proc/testn"); }else{ printk(KERN_INFO"Success!n"); } returnrv; } voidcleanup_module() { remove_proc_entry("test",&proc_root); printk(KERN_INFO"/proc/testremovedn"); } Notes [1]这是在2.0版本中的做法,在版本2.2中,当我们把inode设为0时,就已经这样自动处理了。 35 LINUX内核模块编程[转] /procForInput 使用/proc作为输入 现在我们有两种从内核模块获得输出的方法:我们可以注册一个设备驱动并用mknod生成一个设备文 件,或者我们可以建立一个/proc文件。这样内核就可以告诉我们重要的信息。剩下的唯一问题是我 们没法反馈信息。第一种方法是向/proc文件系统写入信息。 由于/proc文件系统是为内核输出其运行信息而设计的,它并未向内核输入信息提供了任何准备。结 构体structproc_dir_entry并没有指向输入函数的指针,而是指向了一个输出函数。作为替代办法, 向/proc写入信息,我们可以使用标准的文件系统提供的机制。 在Linux中有一种标准的注册文件系统的方法。既然每种文件系统都必须有处理文件索引节点inode 和文件本身的函数[1],那么就一定有种结构体去存放这些函数的指针。这就是结构体struct inode_operations,它其中又包含一个指向结构体structfile_operations的指针。在/proc文件 系统中,当我们需要注册一个新文件时,我们被允许选择哪一个structinode_operations结构体。 这就是我们将使用的机制,用包含结构体structinode_operations指针的结构体struct file_operations来指向我们的module_input和module_output函数。 需要注意的是“读”和“写”的含义在内核中是反过来的。“读”意味着输出,而“写”意味着输入。这是从用 户的角度来看待问题的。如果一个进程只能从内核的“输出”获得输入,而内核也是从进程的输出中得到 “输入”的。 在这儿另一件有趣的事就是module_permission函数了。该函数在每个进程想要对/proc文件系统内 的文件操作时被调用,它来决定是否操作被允许。目前它只是对操作和操作所属用户的UID进行判断, 但它可以也把其它的东西包括进来,像还有哪些别的进程在对该文件进行操作,当前的时间,或是我们 最后接收到的输入。 加入宏put_user和get_user的原因是Linux的内存是使用分页机制的(在Intel架构下是如此,但 其它架构下有可能不同)。这就意味着指针自身并不是指向一个确实的物理内存地址,而知是分页中的 一个地址,而且你必须知道哪些分页将来是可用的。其中内核本身占用一个分页,其它的每个进程都有 自己的分页。 进程能看得到的分页只有属于它自己的,所以当编写用户程序时,不用考虑分页的存在。但是当你编写 内核模块时,你就会访问由系统自动管理的内核所在的分页。当一块内存缓冲区中的内容要在当前运行 中的进程和内核之间传递时,内核的函数就接收指向在进程分页中的该内存缓冲区的指针。宏put_user 和get_user允许你进行这样的访问内存的操作。 .c 36 LINUX内核模块编程[转] /* *procfs.c-createa"file"in/proc,whichallowsbothinputandoutput. */ #include #include #include #include /* *Herewekeepthelastmessagereceived,toprove *thatwecanprocessourinput */ #defineMESSAGE_LENGTH80 staticcharMessage[MESSAGE_LENGTH]; staticstructproc_dir_entry*Our_Proc_File; #definePROC_ENTRY_FILENAME"rw_test" staticssize_tmodule_output(structfile*filp,/*seeinclude/linux/fs.h*/ char*buffer,/*buffertofillwithdata*/ size_tlength,/*lengthofthebuffer*/ loff_t*offset) 37 LINUX内核模块编程[转] { staticintfinished=0; inti; charmessage[MESSAGE_LENGTH+30]; /* *Wereturn0toindicateendoffile,thatwehave *ise,processeswill *continuetoreadfromusinanendlessloop. */ if(finished){ finished=0; return0; } /* *Weuseput_usertocopythestringfromthekernel's *memorysegmenttothememorysegmentoftheprocess *_user,BTW,is *usedforthereverse. */ sprintf(message,"Lastinput:%s",Message); for(i=0;i put_user(message[i],buffer+i); /* *Notice,weassumeherethatthesizeofthemessage *isbelowlen,l *lifesituation,ifthesizeofthemessageisless *thanlenthenwe'dreturnlenandonthesecondcall *startfillingthebufferwiththelen+1'thbyteof *themessage. */ finished=1; returni; } /*Returnthenumberofbytes"read"*/ 38 LINUX内核模块编程[转] staticssize_t module_input(structfile*filp,constchar*buff,size_tlen,loff_t*off) { inti; /* *PuttheinputintoMessage,wheremodule_output *willlaterbeabletouseit */ for(i=0;i get_user(Message[i],buff+i); Message[i]='0';/*wewantastandard,zeroterminatedstring*/ returni; } /* *Thisfunctiondecideswhethertoallowanoperation *(returnzero)ornotallowit(returnanon-zero *whichindicateswhyitisnotallowed). * *Theoperationcanbeoneofthefollowingvalues: *0-Execute(runthe"file"-meaninglessinourcase) *2-Write(inputtothekernelmodule) *4-Read(outputfromthekernelmodule) * *Thisistherealfunctionthatchecksfile *missionsreturnedbyls-lare *forrefereceonly,andcanbeoverriddenhere. */ 39 LINUX内核模块编程[转] staticintmodule_permission(structinode*inode,intop,structnameidata*foo) { /* *Wealloweverybodytoreadfromourmodule,but *onlyroot(uid0)maywritetoit */ if(op==4||(op==2&¤t->euid==0)) return0; /* *Ifit'sanythingelse,accessisdenied */ return-EACCES; } /* *Thefileisopened-wedon'treallycareabout *that,butitdoesmeanweneedtoincrementthe *module'sreferencecount. */ intmodule_open(structinode*inode,structfile*file) { try_module_get(THIS_MODULE); return0; } /* *Thefileisclosed-again,interestingonlybecause *ofthereferencecount. */ intmodule_close(structinode*inode,structfile*file) { module_put(THIS_MODULE); return0; } staticstructfile_operationsFile_Ops_4_Our_Proc_File={ .read=module_output, .write=module_input, .open=module_open, .release=module_close, }; /*success*/ 40 LINUX内核模块编程[转] /* *itso *we'llhavesomeplacetospecifythefileoperations *structurewewanttouse,andthefunctionweusefor *'salsopossibletospecifyfunctions *tobecalledforanythingelsewhichcouldbedoneto *aninode(althoughwedon'tbother,wejustput *NULL). */ staticstructinode_operationsInode_Ops_4_Our_Proc_File={ .permission=module_permission,/*checkforpermissions*/ }; /* *Moduleinitializationandcleanup */ intinit_module() { intrv=0; Our_Proc_File=create_proc_entry(PROC_ENTRY_FILENAME,0644,NULL); Our_Proc_File->owner=THIS_MODULE; Our_Proc_File->proc_iops=&Inode_Ops_4_Our_Proc_File; Our_Proc_File->proc_fops=&File_Ops_4_Our_Proc_File; Our_Proc_File->mode=S_IFREG|S_IRUGO|S_IWUSR; Our_Proc_File->uid=0; Our_Proc_File->gid=0; Our_Proc_File->size=80; if(Our_Proc_File==NULL){ rv=-ENOMEM; remove_proc_entry(PROC_ENTRY_FILENAME,&proc_root); printk(KERN_INFO"Error:Couldnotinitialize/proc/testn"); } returnrv; } voidcleanup_module() { remove_proc_entry(PROC_ENTRY_FILENAME,&proc_root); } 41 LINUX内核模块编程[转] 还需要更多的关于procfs的例子?我要提醒你的是:第一,有消息说也许不久procfs将被sysfs取代; 第二,如果你真的很想多了解些procfs,你可以参考路径linux/Documentation/DocBook/下的那 些技术性的文档。在内核代码树根目录下使用makehelp来获得如何将这些文档转化为你偏好的格式, 例如:makehtmldocs。如果你要为内核加入一些你的文档,你也应该考虑这样做。 Notes [1]两者的区别是文件的操作针对具体的,实在的文件,而文件索引节点的操作是针对文件的引用,像 建立文件的连接等。 42 LINUX内核模块编程[转] gToDeviceFiles 与设备文件对话(writesandIOCTLs) 设备文件是用来代表相对应的硬件设备。绝大多数的硬件设备是用来进行输出和输入操作的,所以在内 核中肯定有内核从进程中获得发送到设备的输出的机制。这是通过打开一个设备文件然后向其中进行写 操作来实现的,如同对普通文件的写操作。在下面的的例子中,这是通过device_write实现的。 但这并不总是够用。设想你有一个通过串口连接的调制解调器(即使你使用的是内置调制解调器,对于 CPU来说同样也是通过连接在串口上来实现工作的)。通常我们通过打开一个设备文件向调制解调器发 送信息(将要通过通信线路传输的指令或数据)或读取信息(从通信线路中返回的响应指令或数据)。但 是,我们如何设置同串口对话的速率,也就是向串口传输数据的速率这个问题仍然没有解决。 解决之道是在Unix系统中的函数ioctl(InputOutputConTroL的简写)。每个设备可以有自己的ioctl 命令,通过读取ioctl's可以从进程中向内核发送信息,或写ioctl's向进程返回信息[1],或者两者都 是,或都不是。函数ioctl调用时需要三个参数:合适的设备文件的文件描述符,ioctl号,和一个可以 被一个任务使用来传递任何东西的long类型的参数[2] ioctl号是反映主设备号,ioctl的种类,对应的命令和参数类型的数字。它通常是通过在头文件中宏调用 (_IO,_IOR,_IOW或_IOWR,取决于其种类)来建立的。该头文件应该被使用ioctl的用户程序包含 (这样它们就可以生成正确的ioctl's)和内核驱动模块包含(这样模块才能理解它)。在下面的例子中, 头文件为chardev.h,源程序为ioctl.c。 即使你只想在自己的模块中使用ioctls,你最好还是接收正式的ioctl标准,这样当你意外的使用别人的 ioctls,或别人使用你的时,你会知道有错误发生。详情参见内核代码目录树下的文件 Documentation/. v.c /* *chardev.c-Createaninput/outputcharacterdevice */ #include #include #include #include #include"chardev.h" #defineSUCCESS0 #defineDEVICE_NAME"char_dev" #defineBUF_LEN80 /*forget_userandput_user*/ /*We'redoingkernelwork*/ /*Specifically,amodule*/ 43 LINUX内核模块编程[转] /* *Isthedeviceopenrightnow?Usedtoprevent *concurentaccessintothesamedevice */ staticintDevice_Open=0; /* *Themessagethedevicewillgivewhenasked */ staticcharMessage[BUF_LEN]; /* *Howfardidtheprocessreadingthemessageget? *Usefulifthemessageislargerthanthesizeofthe *bufferwegettofillindevice_read. */ staticchar*Message_Ptr; /* *Thisiscalledwheneveraprocessattemptstoopenthedevicefile */ staticintdevice_open(structinode*inode,structfile*file) { #ifdefDEBUG printk("device_open(%p)n",file); #endif /* *Wedon'twanttotalktotwoprocessesatthesametime */ if(Device_Open) return-EBUSY; Device_Open++; /* *Initializethemessage */ Message_Ptr=Message; try_module_get(THIS_MODULE); returnSUCCESS; } 44 LINUX内核模块编程[转] staticintdevice_release(structinode*inode,structfile*file) { #ifdefDEBUG printk("device_release(%p,%p)n",inode,file); #endif /* *We'renowreadyforournextcaller */ Device_Open--; module_put(THIS_MODULE); returnSUCCESS; } /* *Thisfunctioniscalledwheneveraprocesswhichhasalreadyopenedthe *devicefileattemptstoreadfromit. */ staticssize_tdevice_read(structfile*file,/*seeinclude/linux/fs.h*/ char__user*buffer,/*buffertobe *filledwithdata*/ size_tlength,/*lengthofthebuffer*/ loff_t*offset) 45 LINUX内核模块编程[转] { /* *Numberofbytesactuallywrittentothebuffer */ intbytes_read=0; #ifdefDEBUG printk("device_read(%p,%p,%d)n",file,buffer,length); #endif /* *Ifwe'reattheendofthemessage,return0 *(whichsignifiesendoffile) */ if(*Message_Ptr==0) return0; /* *Actuallyputthedataintothebuffer */ while(length&&*Message_Ptr){ /* *Becausethebufferisintheuserdatasegment, *notthekerneldatasegment,assignmentwouldn't *d,wehavetouseput_userwhich *copiesdatafromthekerneldatasegmenttothe *userdatasegment. */ put_user(*(Message_Ptr++),buffer++); length--; bytes_read++; } #ifdefDEBUG printk("Read%dbytes,%dleftn",bytes_read,length); #endif 46 LINUX内核模块编程[转] /* *Readfunctionsaresupposedtoreturnthenumber *ofbytesactuallyinsertedintothebuffer */ returnbytes_read; } /* *Thisfunctioniscalledwhensomebodytriesto *writeintoourdevicefile. */ staticssize_t device_write(structfile*file, constchar__user*buffer,size_tlength,loff_t*offset) { inti; #ifdefDEBUG printk("device_write(%p,%s,%d)",file,buffer,length); #endif for(i=0;i get_user(Message[i],buffer+i); Message_Ptr=Message; /* *Again,returnthenumberofinputcharactersused */ returni; } /* *Thisfunctioniscalledwheneveraprocesstriestodoanioctlonour *woextraparameters(additionaltotheinodeandfile *structures,whichalldevicefunctionsget):thenumberoftheioctlcalled *andtheparametergiventotheioctlfunction. * *Iftheioctliswriteorread/write(meaningoutputisreturnedtothe *callingprocess),theioctlcallreturnstheoutputofthisfunction. * */ 47 LINUX内核模块编程[转] intdevice_ioctl(structinode*inode, structfile*file, unsignedintioctl_num, unsignedlongioctl_param) { inti; char*temp; charch; /* *Switchaccordingtotheioctlcalled */ switch(ioctl_num){ caseIOCTL_SET_MSG: /* *Receiveapointertoamessage(inuserspace)andsetthat *tobethedevice'parametergivento *ioctlbytheprocess. */ temp=(char*)ioctl_param; /* *Findthelengthofthemessage */ get_user(ch,temp); for(i=0;ch&&i get_user(ch,temp); device_write(file,(char*)ioctl_param,i,0); break; caseIOCTL_GET_MSG: /* *Givethecurrentmessagetothecallingprocess- *theparameterwegotisapointer,fillit. */ i=device_read(file,(char*)ioctl_param,99,0); /*seeinclude/linux/fs.h*/ /*numberandparamforioctl*/ /*ditto*/ 48 LINUX内核模块编程[转] /* *Putazeroattheendofthebuffer,soitwillbe *properlyterminated */ put_user('0',(char*)ioctl_param+i); break; caseIOCTL_GET_NTH_BYTE: /* *Thisioctlisbothinput(ioctl_param)and *output(thereturnvalueofthisfunction) */ returnMessage[ioctl_param]; break; } returnSUCCESS; } /*ModuleDeclarations*/ /* *Thisstructurewillholdthefunctionstobecalled *whenaprocessdoessomethingtothedevicewe *pointertothisstructureiskeptin *thedevicestable,itcan'tbelocalto *init_forunimplementedfunctions. */ structfile_operationsFops={ .read=device_read, .write=device_write, .ioctl=device_ioctl, .open=device_open, .release=device_release,/**/ }; /* *Initializethemodule-Registerthecharacterdevice */ 49 LINUX内核模块编程[转] intinit_module() { intret_val; /* *Registerthecharacterdevice(atleasttry) */ ret_val=register_chrdev(MAJOR_NUM,DEVICE_NAME,&Fops); /* *Negativevaluessignifyanerror */ if(ret_val<0){ printk("%sfailedwith%dn", "Sorry,registeringthecharacterdevice",ret_val); returnret_val; } printk("%sThemajordevicenumberis%d.n", "Registerationisasuccess",MAJOR_NUM); printk("Ifyouwanttotalktothedevicedriver,n"); printk("you'llhavetocreateadevicefile.n"); printk("Wesuggestyouuse:n"); printk("mknod%sc%d0n",DEVICE_FILE_NAME,MAJOR_NUM); printk("Thedevicefilenameisimportant,becausen"); printk("theioctlprogramassumesthat'sthen"); printk("fileyou'lluse.n"); return0; } 50 LINUX内核模块编程[转] /* *Cleanup-unregistertheappropriatefilefrom/proc */ voidcleanup_module() { intret; /* *Unregisterthedevice */ ret=unregister_chrdev(MAJOR_NUM,DEVICE_NAME); /* *Ifthere'sanerror,reportit */ if(ret<0) printk("Errorinmodule_unregister_chrdev:%dn",ret); } v.h /* *chardev.h-theheaderfilewiththeioctldefinitions. * *Thedeclarationsherehavetobeinaheaderfile,because *theyneedtobeknownbothtothekernelmodule *(inchardev.c)andtheprocesscallingioctl(ioctl.c) */ #ifndefCHARDEV_H #defineCHARDEV_H #include /* *'trelyondynamic *registrationanymore,becauseioctlsneedtoknow *it. */ #defineMAJOR_NUM100 /* *Setthemessageofthedevicedriver */ 51 LINUX内核模块编程[转] #defineIOCTL_SET_MSG_IOR(MAJOR_NUM,0,char*) /* *_IORmeansthatwe'recreatinganioctlcommand *numberforpassinginformationfromauserprocess *tothekernelmodule. * *Thefirstarguments,MAJOR_NUM,isthemajordevice *numberwe'reusing. * *Thesecondargumentisthenumberofthecommand *(therecouldbeseveralwithdifferentmeanings). * *Thethirdargumentisthetypewewanttogetfrom *theprocesstothekernel. */ /* *Getthemessageofthedevicedriver */ #defineIOCTL_GET_MSG_IOR(MAJOR_NUM,1,char*) /* *ThisIOCTLisusedforoutput,togetthemessage *r,westillneedthe *buffertoplacethemessageintobeinput, *asitisallocatedbytheprocess. */ /* *Getthen'thbyteofthemessage */ #defineIOCTL_GET_NTH_BYTE_IOWR(MAJOR_NUM,2,int) /* * *receivesfromtheuseranumber,n,andreturns *Message[n]. */ /* *Thenameofthedevicefile */ #defineDEVICE_FILE_NAME"char_dev" 52 LINUX内核模块编程[转] #endif .c /* *ioctl.c-theprocesstouseioctl'stocontrolthekernelmodule * * *weneedtodoioctl's,whichrequirewritingourownprocess. */ /* *devicespecifics,suchasioctlnumbersandthe *majordevicefile. */ #include"chardev.h" #include #include #include /* *Functionsfortheioctlcalls */ ioctl_set_msg(intfile_desc,char*message) { intret_val; ret_val=ioctl(file_desc,IOCTL_SET_MSG,message); if(ret_val<0){ printf("ioctl_set_msgfailed:%dn",ret_val); exit(-1); } } 53 LINUX内核模块编程[转] ioctl_get_msg(intfile_desc) { intret_val; charmessage[100]; /* *Warning-thisisdangerousbecausewedon'ttell *thekernelhowfarit'sallowedtowrite,soit *lproduction *program,wewouldhaveusedtwoioctls-onetotell *thekernelthebufferlengthandanothertogive *itthebuffertofill */ ret_val=ioctl(file_desc,IOCTL_GET_MSG,message); if(ret_val<0){ printf("ioctl_get_msgfailed:%dn",ret_val); exit(-1); } printf("get_msgmessage:%sn",message); } ioctl_get_nth_byte(intfile_desc) { inti; charc; printf("get_nth_bytemessage:"); i=0; while(c!=0){ c=ioctl(file_desc,IOCTL_GET_NTH_BYTE,i++); if(c<0){ printf ("ioctl_get_nth_bytefailedatthe%d'thbyte:n", i); exit(-1); } putchar(c); } putchar('n'); } 54 LINUX内核模块编程[转] /* *Main-Calltheioctlfunctions */ main() { intfile_desc,ret_val; char*msg="Messagepassedbyioctln"; file_desc=open(DEVICE_FILE_NAME,0); if(file_desc<0){ printf("Can'topendevicefile:%sn",DEVICE_FILE_NAME); exit(-1); } ioctl_get_nth_byte(file_desc); ioctl_get_msg(file_desc); ioctl_set_msg(file_desc,msg); close(file_desc); } Notes [1]注意这儿“读”与“写”的角色再次翻转过来,在ioctl's中读是向内核发送信息,而写是从内核获取信 息。 [2]这样的表述并不准确。例如你不能在ioctl中传递一个结构体,但你可以通过传递指向这个结构体 的指针实现。 55 LINUX内核模块编程[转] Calls 系统调用 到目前为止,我们所做的只是使用完善的内核机制注册/proc文件和处理设备的对象。如果只是想写一 个设备驱动,这些内核程序员设定的方式已经足够了。但是,你不想做一些不寻常的事吗,想使你的 系统看起来不一样吗?当然,这取决你自己。 这里可是一个危险的地方。下面的这个例子中,我关闭了系统调用open()。这意味着我无法打开任何 文件,执行任何程序,连使用shutdown关机都不行,关机只能靠摁电源按钮了。幸运的话,不会有文 件丢失。要保证不丢失文件的话,在insmod和rmmod之前请执行sync命令。 别管什么/proc文件和什么设备文件了,它们只是小的细节问题。所有进程同内核打交道的根本方式是 系统调用。当一个进程需要内核提供某项服务时(像打开一个文件,生成一个新进程,或要求更多的内 存),就会发生系统调用。如果你想你的系统运作方式看起来有意思点,这就是你动手的地方。顺便说 一句,如果你想知道没个程序使用了哪些系统调用,运行strace 总的来说,一个用户进程是不应该也不能够直接访问内核的。它不能访问内核的内存,也不能调用内核 的函数。这是CPU的硬件保护机制决定的(这也是为什么叫做“保护模式”的原因)。 系统调用是这条规则的例外。所发生的事是一个进程用合适的值填充寄存器,然后调用一条跳转到已被 定义过的内核中的位置的指令(当然,这些定义过的位置是对于用户进程可读的,但是显然是不可写的)。 在Intel架构中,这是通过0x80中断完成的。硬件明白一旦你跳转到这个位置,你就不再是在处处受 限的用户态中运行了,而是在无所不能的内核态中。 内核中的进程可以跳转过去的位置叫做系统调用。那儿将检查系统调用的序号,这些序号将告诉内核用 户进程需要什么样的服务。然后,通过查找系统调用表(sys_call_table)找到内核函数的地址,调用该 函数。当函数返回时,再做一些系统检查,接着就返回用户进程(或是另一个进程,如果该进程的时间 用完了)。如果你想阅读一下这方面的源代码,它们就在文件 arch/$<$architecture$>$/kernel/entry.S中ENTRY(system_call)行的下面。 所以,如果我们想改变某个系统调用的运作方式,我们只需要用我们自己的函数去实现它(通常只是加 一点我们自己的代码,然后调用原函数)然后改变系统调用表(sys_call_table)中的指针值使它指向我 们的函数。因为这些模块将在以后卸载,我们不想系统因此而不稳定,所以cleanup_module中恢复 系统调用表是非常重要的。 这就是这样的一个模块。我们可以“监视”一个特定的用户,然后使用printk()输出该用户打开的每个文 件的消息。在结束前,我们用自己的our_sys_open函数替换了打开文件的系统调用。该函数检查当前 进程的用户序号(uid,user'sid),如果匹配我们监视的用户的序号,它调用printk()输出将要打开的 文件的名字。要不然,就用同样的参数调用原始的open()函数,真正的打开文件。 函数init_module改变了系统调用表中的恰当位置的值然后用一个变量保存下来。函数 cleanup_module则使用该变量将所有东西还原。这种处理方法其实是很危险的。想象一下,如果我 们有两个这样的模块,A和B。A用A_open替换了系统的sys_open函数,而B用B_open。现在, 56 LINUX内核模块编程[转] 我们先把模块A加载,那么原先的系统调用被A_open替代了,A_open在完成工作后自身又会调用 原始的sys_open函数。接着,我们加载B模块,它用B_open更改了现在的已更改为A_open(显 然它认为是原始的sys_open系统调用)的系统调用。 现在,如果B先卸载,一切正常。系统调用会还原到A_open,而A_open又会调用原始的sys_open。 但是,一旦A先卸载,系统就会崩溃。A的卸载会将系统调用还原到原始的sys_open,把B从链中切 断。此时再卸载B,B会将系统调用恢复到它认为的初始状态,也就是A_open,但A_open已经不在 内存中了。乍一看来,我们似乎可以通过检测系统调用是否与我们的open函数相同,如果不相同则什 么都不做(这样B就不会尝试在卸载时恢复系统调用表)。但其实这样更糟。当A先被卸载时,它将检 测到系统调用已被更改为B_open,所以A将不会在卸载时恢复系统调用表中相应的项。此时不幸的事 发生了,B_open将仍然调用已经不存在的A_open,这样即使你不卸载B模块,系统也崩溃了。 但是这种替换系统调用的方法是违背正式应用中系统的稳定和可靠原则的。所以,为了防止潜在的对系 统调用表修改带来的危害,系统调用表sys_call_table不再被内核导出。这意味着如果你想顺利的运 行这个例子,你必须为你的内核树打补丁来导出sys_call_table,在example目录内你将找到相关的 补丁和说明。正如同你想像的那样,这可不是儿戏,如果你的系统非常宝贵(例如这不是你的系统,或 系统很难恢复),你最好还是放弃。如果你仍然坚持,我可以告诉你的是打补丁虽然不会有多大问题, 但内核维护者他们肯定有足够的理由在2.6内核中不支持这种hack。详情请参考README。如果你选 择了N,跳过这个例子是一个安全的选择。 l.c /* *syscall.c * *Systemcall"stealing"sample. */ /* *Copyright(C)2001byPeterJaySalzman */ /* *Thenecessaryheaderfiles */ /* *Standardinkernelmodules */ #include #include #include #include /*We'redoingkernelwork*/ /*Specifically,amodule,*/ /*whichwillhaveparams*/ /*Thelistofsystemcalls*/ 57 LINUX内核模块编程[转] /* *Forthecurrent(process)structure,weneed *thistoknowwhothecurrentuseris. */ #include #include /* *Thesystemcalltable(atableoffunctions).We *justdefinethisasexternal,andthekernelwill *fillitupforuswhenweareinsmod'ed * *sys_call_ls. *IfyoureallywanttotrythisDANGEROUSmoduleyouwill *havetoapplythesuppliedpatchagainstyourcurrentkernel *andrecompileit. */ externvoid*sys_call_table[]; /* *UIDwewanttospyon-willbefilledfromthe *commandline */ staticintuid; module_param(uid,int,0644); /* *son *wekeepthis,ratherthancalltheoriginalfunction *(sys_open),isbecausesomebodyelsemighthave *atthis *isnot100%safe,becauseifanothermodule *replacedsys_openbeforeus,thenwhenwe'reinserted *we'llcallthefunctioninthatmodule-andit *mightberemovedbeforeweare. * *Anotherreasonforthisisthatwecan'tgetsys_open. *It'sastaticvariable,soitisnotexported. */ asmlinkageint(*original_call)(constchar*,int,int); 58 LINUX内核模块编程[转] /* *Thefunctionwe'llreplacesys_open(thefunction *calledwhenyoucalltheopensystemcall) *findtheexactprototype,withthenumberandtype *ofarguments,wefindtheoriginalfunctionfirst *(it'satfs/open.c). * *Intheory,thismeansthatwe'retiedtothe *tice,the *systemcallsalmostneverchange(itwouldwreckhavoc *andrequireprogramstoberecompiled,sincethesystem *callsaretheinterfacebetweenthekernelandthe *processes). */ asmlinkageintour_sys_open(constchar*filename,intflags,intmode) { inti=0; charch; /* *Checkifthisistheuserwe'respyingon */ if(uid==current->uid){ /* *Reportthefile,ifrelevant */ printk("Openedfileby%d:",uid); do{ get_user(ch,filename+i); i++; printk("%c",ch); }while(ch!=0); printk("n"); } /* *Calltheoriginalsys_open-otherwise,welose *theabilitytoopenfiles */ returnoriginal_call(filename,flags,mode); } 59 LINUX内核模块编程[转] /* *Initializethemodule-replacethesystemcall */ intinit_module() { /* *Warning-toolateforitnow,butmaybefor * */ printk("I'oudida"); printk("syncbeforeyouinsmod'edme.n"); printk("Mycounterpart,cleanup_module(),iseven"); printk(""); printk("youvalueyourfilesystem,itwill"); printk("be"sync;rmmod"n"); printk("whenyouremovethismodule.n"); /* *Keepapointertotheoriginalfunctionin *original_call,andthenreplacethesystemcall *inthesystemcalltablewithour_sys_open */ original_call=sys_call_table[__NR_open]; sys_call_table[__NR_open]=our_sys_open; /* *Togettheaddressofthefunctionforsystem *callfoo,gotosys_call_table[__NR_foo]. */ printk("SpyingonUID:%dn",uid); return0; } 60 LINUX内核模块编程[转] /* *Cleanup-unregistertheappropriatefilefrom/proc */ voidcleanup_module() { /* *Returnthesystemcallbacktonormal */ if(sys_call_table[__NR_open]!=our_sys_open){ printk("Somebodyelsealsoplayedwiththe"); printk("opensystemcalln"); printk("Thesystemmaybeleftin"); printk("anunstablestate.n"); } sys_call_table[__NR_open]=original_call; } 61 LINUX内核模块编程[转] ngProcesses 阻塞进程 EnterSandman 当别人让你做一件你不能马上去做的事时,你会如何反映?如果你是人类的话,而且对方也是人类的话, 你只会说:“现在不行,我忙着在。闪开!”但是如果你是一个内核模块而且你被一个进程以同样的问题 困扰,你会有另外一个选择。你可以让该进程休眠直到你可以为它服务时。毕竟,这样的情况在内核中 时时刻刻都在发生(这就是系统让多进程在单CPU上同时运行的方法)。 这个内核模块就是一个这样的例子。文件(/proc/sleep))只可以在同一时刻被一个进程打开。如果该 文件已经被打开,内核模块将调用函数wait_event_interruptible[1]。该函数修改task的状态(task 是一个内核中的结构体数据结构,其中保存着对应进程的信息和该进程正在调用的系统调用,如果有的 话)为TASK_INTERRUPTIBLE意味着改进程将不会继续运行直到被唤醒,然后被添加到系统的进程 等待队列WaitQ中,一个等待打开该文件的队列中。然后,该函数调用系统调度器去切换到另一个不同 的但有CPU运算请求的进程。 当一个进程处理完该文件并且关闭了该文件,module_close就被调用执行了。该函数唤醒所有在等待 队列中的进程(还没有只唤醒特定进程的机制)。然后该函数返回,那个刚刚关闭文件的进程得以继续 运行。及时的,进程调度器会判定该进程执行已执行完毕,将CPU转让给别的进程。被提供CPU使用 权的那个进程就恰好从先前系统调用module_interruptible_sleep_on[2]后的地方开始继续执行。 它可以设置一个全局变量去通知别的进程该文件已被打开占用了。当别的请求该文件的进程获得CPU时 间片时,它们将检测该变量然后返回休眠。 更有趣的是,module_close并不垄断唤醒等待中的请求文件的进程的权力。一个信号,像Ctrl+c (SIGINT也能够唤醒别的进程[3]。在这种情况下,我们想立即返回-EINTR。这对用户很重要,举 个例子来说,用户可以在某个进程接受到文件前终止该进程。 还有一点值得注意。有些时候进程并不愿意休眠,它们要么立即执行它们想做的,要么被告知任务无法 进行。这样的进程在打开文件时会使用标志O_NONBLOCK。在别的进程被阻塞时内核应该做出的响 应是返回错误代码-EAGAIN,像在本例中对该文件的请求的进程。程序cat_noblock,在本章的源代 码目录下可以找到,就能够使用标志位O_NONBLOCK打开文件。 .c /* *sleep.c-createa/procfile,andifseveralprocessestrytoopenitat *thesametime,putallbutonetosleep */ #include #include #include #include 62 /*We'redoingkernelwork*/ /*Specifically,amodule*/ /*Necessarybecauseweuseprocfs*/ /*Forputtingprocessestosleepand LINUX内核模块编程[转] wakingthemup*/ #include /* *Themodule'sfilefunctions */ /* *Herewekeepthelastmessagereceived,toprovethatwecanprocessour *input */ #defineMESSAGE_LENGTH80 staticcharMessage[MESSAGE_LENGTH]; staticstructproc_dir_entry*Our_Proc_File; #definePROC_ENTRY_FILENAME"sleep" /* *Sinceweusethefileoperationsstruct,wecan'tusethespecialproc *outputprovisions-wehavetouseastandardreadfunction,whichisthis *function */ staticssize_tmodule_output(structfile*file,/*seeinclude/linux/fs.h*/ char*buf,/*Thebuffertoputdatato (intheusersegment)*/ size_tlen,/*Thelengthofthebuffer*/ loff_t*offset) { staticintfinished=0; inti; charmessage[MESSAGE_LENGTH+30]; /* *Return0tosignifyendoffile-thatwehavenothing *moretosayatthispoint. */ if(finished){ finished=0; return0; } /* *Ifyoudon'tunderstandthisbynow,you'rehopelessasakernel *programmer. */ 63 LINUX内核模块编程[转] sprintf(message,"Lastinput:%sn",Message); for(i=0;i put_user(message[i],buf+i); finished=1; returni; } /* *Thisfunctionreceivesinputfromtheuserwhentheuserwritestothe/proc *file. */ staticssize_tmodule_input(structfile*file, constchar*buf, size_tlength, loff_t*offset) { inti; /* *PuttheinputintoMessage,wheremodule_outputwilllaterbe *abletouseit */ for(i=0;i get_user(Message[i],buf+i); /* *wewantastandard,zeroterminatedstring */ Message[i]='0'; /* *Weneedtoreturnthenumberofinputcharactersused */ returni; } /* *1ifthefileiscurrentlyopenbysomebody */ intAlready_Open=0; /* *Queueofprocesseswhowantourfile */ DECLARE_WAIT_QUEUE_HEAD(WaitQ); 64 /*Returnthenumberofbytes"read"*/ /*Thefileitself*/ /*Thebufferwithinput*/ /*Thebuffer'slength*/ /*offsettofile-ignore*/ LINUX内核模块编程[转] /* *Calledwhenthe/procfileisopened */ staticintmodule_open(structinode*inode,structfile*file) { /* *Ifthefile'sflagsincludeO_NONBLOCK,itmeanstheprocessdoesn't *case,ifthefileisalready *open,weshouldfailwith-EAGAIN,meaning"you'llhavetotry *again",insteadofblockingaprocesswhichwouldratherstayawake. */ if((file->f_flags&O_NONBLOCK)&&Already_Open) return-EAGAIN; /* *Thisisthecorrectplacefortry_module_get(THIS_MODULE)because *ifaprocessisintheloop,whichiswithinthekernelmodule, *thekernelmodulemustnotberemoved. */ try_module_get(THIS_MODULE); /* *Ifthefileisalreadyopen,waituntilitisn't */ while(Already_Open){ inti,is_sig=0; /* *Thisfunctionputsthecurrentprocess,includinganysystem *calls,suchasus,ionwillberesumedright *afterthefunctioncall,eitherbecausesomebodycalled *wake_up(&WaitQ)(onlymodule_closedoesthat,whenthefile *isclosed)orwhenasignal,suchasCtrl-C,issent *totheprocess */ wait_event_interruptible(WaitQ,!Already_Open); /* *Ifwewokeupbecausewegotasignalwe'renotblocking, *return-EINTR(failthesystemcall).Thisallowsprocesses *tobekilledorstopped. */ 65 LINUX内核模块编程[转] /* *EmmanuelPapirakis: * *Thisisalittleupdatetoworkwith2.2.*.Signalsnowarecontainedin *twowords(64bits)andarestoredinastructurethatcontainsanarrayof *avetomake2checksinourif. * *OriPomerantz: * *Nobodypromisedmethey'llneverusemorethan64bits,orthatthisbook *won'de *wouldworkinanycase. */ for(i=0;i<_NSIG_WORDS&&!is_sig;i++) is_sig= current->[i]&~current-> [i]; if(is_sig){ /* *It'simportanttoputmodule_put(THIS_MODULE)here, *becauseforprocesseswheretheopenisinterrupted * *don'tdecrementtheusagecounthere,wewillbe *leftwithapositiveusagecountwhichwe'llhaveno *waytobringdowntozero,givingusanimmortal *module,whichcanonlybekilledbyrebooting *themachine. */ module_put(THIS_MODULE); return-EINTR; } } /* *Ifwegothere,Already_Openmustbezero */ /* *Openthefile */ Already_Open=1; return0; } /*Allowtheaccess*/ 66 LINUX内核模块编程[转] /* *Calledwhenthe/procfileisclosed */ intmodule_close(structinode*inode,structfile*file) { /* *SetAlready_Opentozero,sooneoftheprocessesintheWaitQwill *beabletosetAlready_ *theotherprocesseswillbecalledwhenAlready_Openisbacktoone, *sothey'llgobacktosleep. */ Already_Open=0; /* *WakeupalltheprocessesinWaitQ,soifanybodyiswaitingforthe *file,theycanhaveit. */ wake_up(&WaitQ); module_put(THIS_MODULE); return0;/*success*/ } /* *Thisfunctiondecideswhethertoallowanoperation(returnzero)ornot *allowit(returnanon-zerowhichindicateswhyitisnotallowed). * *Theoperationcanbeoneofthefollowingvalues: *0-Execute(runthe"file"-meaninglessinourcase) *2-Write(inputtothekernelmodule) *4-Read(outputfromthekernelmodule) * *missions *returnedbyls-lareforreferenceonly,andcanbeoverriddenhere. */ staticintmodule_permission(structinode*inode,intop,structnameidata*nd) { /* *Wealloweverybodytoreadfromourmodule,butonlyroot(uid0) *maywritetoit */ if(op==4||(op==2&¤t->euid==0)) return0; 67 LINUX内核模块编程[转] /* *Ifit'sanythingelse,accessisdenied */ return-EACCES; } /* *Structurestoregisterasthe/procfile,withpointerstoalltherelevant *functions. */ /* *whereweplacepointerstoall * *meanswedon'twanttodealwithsomething. */ staticstructfile_operationsFile_Ops_4_Our_Proc_File={ .read=module_output, .write=module_input, .open=module_open, .release=module_close, }; /* *itsowe'llhavesomewhereto *specifythefileoperationsstructurewewanttouse,andthefunctionwe *'salsopossibletospecifyfunctionstobecalled *foranythingelsewhichcouldbedonetoaninode(althoughwedon'tbother, *wejustputNULL). */ staticstructinode_operationsInode_Ops_4_Our_Proc_File={ .permission=module_permission, }; /* *Moduleinitializationandcleanup */ /* *Initializethemodule-registertheprocfile */ intinit_module() { intrv=0; 68 /*"read"fromthefile*/ /*"write"tothefile*/ /*calledwhenthe/procfileisopened*/ /*calledwhenit'sclosed*/ /*checkforpermissions*/ LINUX内核模块编程[转] Our_Proc_File=create_proc_entry(PROC_ENTRY_FILENAME,0644,NULL); Our_Proc_File->owner=THIS_MODULE; Our_Proc_File->proc_iops=&Inode_Ops_4_Our_Proc_File; Our_Proc_File->proc_fops=&File_Ops_4_Our_Proc_File; Our_Proc_File->mode=S_IFREG|S_IRUGO|S_IWUSR; Our_Proc_File->uid=0; Our_Proc_File->gid=0; Our_Proc_File->size=80; if(Our_Proc_File==NULL){ rv=-ENOMEM; remove_proc_entry(PROC_ENTRY_FILENAME,&proc_root); printk(KERN_INFO"Error:Couldnotinitialize/proc/testn"); } returnrv; } /* *Cleanup-unregisterourfilefrom/uldgetdangerousif *therearestillprocesseswaitinginWaitQ,becausetheyareinsideour *openfunction,whichwillgetunloaded.I'llexplainhowtoavoidremoval *ofakernelmoduleinsuchacaseinchapter10. */ voidcleanup_module() { remove_proc_entry(PROC_ENTRY_FILENAME,&proc_root); } Notes [1]最方便的保持某个文件被打开的方法是使用命令tail-f打开该文件。 [2]这就意味着该进程仍然在内核态中,该进程已经调用了open的系统调用,但系统调用却没有返回。 在这段时间内该进程将不会得知别人正在使用CPU。 [3]这是因为我们使用的是module_interruptible_sleep_on。我们也可以使用module_sleep_on, 但这样会导致一些十分愤怒的用户,因为他们的Ctrl+c将不起任何作用。 69 LINUX内核模块编程[转] ingPrintks 替换printk 在theSectioncalled使用X带来的问题inChapter1中,我说过最好不要在X中进行内核模块编 程。在真正的内核模块开发中的确是这样。但在实际应用中,你想在任何加载模块的tty[1]终端中显示 信息。 实现的方法是使用current指针,一个指向当前运行进程的指针,来获取当前任务的tty终端的结构体。 然后,我们找到在该tty结构体中用来向tty写入字符信息的函数的指针。通过指针我们使用该函数来 向终端写入信息。 _string.c /* *print_string.c-Sendoutputtothettywe'rerunningon,regardlessifit's *throughX11,telnet,isbyprintingthestringtothetty *associatedwiththecurrenttask. */ #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("PeterJaySalzman"); staticvoidprint_string(char*str) { structtty_struct*my_tty; /* *ttystructwentintosignalstructin2.6.6 */ #if(LINUX_VERSION_CODE<=KERNEL_VERSION(2,6,5)) /* *Thettyforthecurrenttask */ my_tty=current->tty; #else 70 /*Forcurrent*/ /*Forthettydeclarations*/ /*ForLINUX_VERSION_CODE*/ LINUX内核模块编程[转] /* *Thettyforthecurrenttask,for2.6.6+kernels */ my_tty=current->signal->tty; #endif /* *Ifmy_ttyisNULL,thecurrenttaskhasnottyyoucanprintto *(ie,ifit'sadaemon).Ifso,there'snothingwecando. */ if(my_tty!=NULL){ /* *my_tty->driverisastructwhichholdsthetty'sfunctions, *oneofwhich(write)isusedtowritestringstothetty. *Itcanbeusedtotakeastringeitherfromtheuser'sor *kernel'smemorysegment. * *Thefunction's1stparameteristhettytowriteto, *becausethesamefunctionwouldnormallybeusedforall *tty'2ndparametercontrolswhether *thefunctionreceivesastringfromkernelmemory(false,0) *orfromusermemory(true,nonzero).The3rdparameteris *4thparameteristhelengthof *thestring. */ ((my_tty->driver)->write)(my_tty,/*Thettyitself*/ 0,/*Don'ttakethestring fromuserspace*/ str,/*String*/ strlen(str));/*Length*/ /* *ttyswereoriginallyhardwaredevices,which(usually) *I,tomoveto *anewlineyouneedtwocharacters,acarriagereturnanda *,theASCIIlinefeedisusedforboth *purposes-sowecan'tjustusen,becauseitwouldn'thave *acarriagereturnandthenextlinewillstartatthe *columnrightafterthelinefeed. * *ThisiswhytextfilesaredifferentbetweenUnixand */Mandderivatives,likeMS-DOSand *MSWindows,theASCIIstandardwasstrictlyadheredto, *andthereforeanewlinerequirsbothaLFandaCR. 71 LINUX内核模块编程[转] */ ((my_tty->driver)->write)(my_tty,0,"015012",2); } } staticint__initprint_string_init(void) { print_string("orld!"); return0; } staticvoid__exitprint_string_exit(void) { print_string("llworld!"); } module_init(print_string_init); module_exit(print_string_exit); Notes [1]Teletype,原先是一种用来和Unix系统交互的键盘和打印机结合起来的装置。现在,它只是一个用 来同Unix或类似的系统交流文字流的抽象的设备,而不管它具体是显示器,X中的xterm,还是一个 通过telnet的网络连接。 让你的键盘指示灯闪起来 你也许想让你的模块更直接的同外界交流,你的键盘指示灯就是一个不错的选择。它可以及时显示模块 的工作状态,吸引你的注意,并且它们不许要任何设置,使用起来也不像向终端或磁盘写入信息那么危 险。 下面的这个模块代码演示了一个相当小的模块:当被加载入内核时,键盘指示灯就不停的闪烁,直到它 被卸载。 .c /* *kbleds.c-Blinkkeyboardledsuntilthemoduleisunloaded. */ #include #include #include #include #include #include 72 /*Forfg_console,MAX_NR_CONSOLES*/ /*ForKDSETLED*/ /*Forvc_cons*/ LINUX内核模块编程[转] MODULE_DESCRIPTION("ExamplemoduleillustratingtheuseofKeyboardLEDs."); MODULE_AUTHOR("DanielePaoloScarpazza"); MODULE_LICENSE("GPL"); structtimer_listmy_timer; structtty_driver*my_driver; charkbledstatus=0; #defineBLINK_DELAY #defineALL_LEDS_ON HZ/5 0x07 #defineRESTORE_LEDS0xFF /* *Functionmy_timer_funcblinksthekeyboardLEDsperiodicallybyinvoking *commandKDSETLEDofioctl()nmoreonvirtual *terminalioctloperations,pleaseseefile: * * *TheargumenttoKDSETLEDisalternativelysetto7(thuscausingtheled *modetobesettoLED_SHOW_IOCTL,andalltheledsarelit)andto0xFF *(anyvalueabove7switchesbacktheledmodetoLED_SHOW_FLAGS,thus *theLEDsreflecttheactualkeyboardstatus).Tolearnmoreonthis, *pleaseseefile: * * */ staticvoidmy_timer_func(unsignedlongptr) { int*pstatus=(int*)ptr; if(*pstatus==ALL_LEDS_ON) *pstatus=RESTORE_LEDS; else *pstatus=ALL_LEDS_ON; (my_driver->ioctl)(vc_cons[fg_console].d->vc_tty,NULL,KDSETLED, *pstatus); my_s=jiffies+BLINK_DELAY; add_timer(&my_timer); } /usr/src/linux/drivers/char/keyboard.c,functionsetledstate(). /usr/src/linux/drivers/char/vt_ioctl.c,functionvt_ioctl(). 73 LINUX内核模块编程[转] staticint__initkbleds_init(void) { inti; printk(KERN_INFO"kbleds:loadingn"); printk(KERN_INFO"kbleds:fgconsoleis%xn",fg_console); for(i=0;i if(!vc_cons[i].d) break; printk(KERN_INFO"poet_atkm:console[%i/%i]#%i,tty%lxn",i, MAX_NR_CONSOLES,vc_cons[i].d->vc_num, (unsignedlong)vc_cons[i].d->vc_tty); } printk(KERN_INFO"kbleds:finishedscanningconsolesn"); my_driver=vc_cons[fg_console].d->vc_tty->driver; printk(KERN_INFO"kbleds:ttydrivermagic%xn",my_driver->magic); /* *SetuptheLEDblinktimerthefirsttime */ init_timer(&my_timer); my_on=my_timer_func; my_=(unsignedlong)&kbledstatus; my_s=jiffies+BLINK_DELAY; add_timer(&my_timer); return0; } staticvoid__exitkbleds_cleanup(void) { printk(KERN_INFO""); del_timer(&my_timer); (my_driver->ioctl)(vc_cons[fg_console].d->vc_tty,NULL,KDSETLED, RESTORE_LEDS); } module_init(kbleds_init); module_exit(kbleds_cleanup); 如果上面的方法都无法满足你调试的需要,你就可能需要其它的技巧了。还记得那个在make menuconfig时的CONFIG_LL_DEBUG参数吗?如果你激活该选项,你就可以获得对串口的底层操 纵。如果这仍然不够爽,你还可以对kernel/printk.c或其它的基本的系统底层调用打补丁来使用 printascii,从而可以通过串口跟踪内核的每步动作。如果你的架构不支持上面的例子却有一个标准的 74 LINUX内核模块编程[转] 串口,这可能应该是你首先应该考虑的了。通过网络上的终端调试同样值得尝试。 尽管有很多关于如何调试的技巧,但我要提醒的是任何调试都会代码带来影响。加入调试代码足以导致 原始代码产生bug的条件的消失,所以尽可能少的加入调试代码并且确保它们不出现在成熟的代码中。 75 LINUX内核模块编程[转] lingTasks 任务调度 经常我们要定期的抽空处理一些“家务活”。如果这样的任务通过一个用户进程完成的,那么我们可以将 它放到一个crontab文件中。如果是通过一个内核模块来完成,那么我们有两种选择。第一种选择是 使用crontab文件,启动一个进程,通过一个系统调用唤醒内核模块,例如打开一个文件。这很没效率。 我们通过crontab生成了一个新进程,读取了一段新的可执行代码进入内存,只是为了唤醒一个已经在 内存中的内核模块。 第二种选择是我们构造一个函数,然后该函数在每次时间中断发生时被调用。实现方法是我们构造一个 任务,使用结构体tq_struct,而该结构体又保存着指向该函数的指针。然后,我们用queue_task把 该任务放在叫做tq_timer任务队列中。该队列是将在下个时间中断发生时执行的任务。因为我们想要 使它不停的执行,所以当该函数执行完后我们还要将它放回tq_timer任务队列中等待下一次时间中断。 但我们似乎忘了一点。当一个模块用rmmod卸载时,它会检查使用计数。如果该计数为零,则调用 module_cleanup。然后,模块就同它的所有函数调用从内存中消失了。此时没人去检查任务队列中是 否正好还有一个等待执行的这些函数的指针。在可能是一段漫长的时间后(当然是相对计算机而言,对 于我们这点时间什么都不是,也就差不多百分之一秒吧),内核接收到一个时间中断,然后准备调用那 个在任务队列中的函数。不幸的是,该函数已经不存在了。大多数情况下,由于访问的内存页是空白的, 你只会收到一个不愉快的消息。但是如果其它的一些代码恰好就在那里,结果可能将会非常糟糕。同样 不幸的是,我们也没有一种轻易的向任务队列注销任务的机制。 既然cleanup_module不能返回一个错误代码(它是一个void函数),解决之道是让它不要返回。相 反,调用sleep_on或module_sleep_on[1]让rmmod的进程休眠。在此之前,它通知被时间中断 调度出任务队列的那个函数不要在返回队列。这样,在下一个时间中断发生时,rmmod就会被唤醒, 此时我们的函数已经不在队列中,可以很安全的卸载我们的模块了。 .c /* *sched.c-schedualeafunctiontobecalledoneverytimerinterrupt. * *Copyright(C)2001byPeterJaySalzman */ /* *Thenecessaryheaderfiles */ /* *Standardinkernelmodules */ 76 LINUX内核模块编程[转] #include #include #include #include #include andwakeuplater*/ #include #include structproc_dir_entry*Our_Proc_File; #definePROC_ENTRY_FILENAME"sched" #defineMY_WORK_QUEUE_NAME"WQsched.c" /* *Thenumberoftimesthetimerinterrupthasbeencalledsofar */ staticintTimerIntrpt=0; staticvoidintrpt_routine(void*); staticintdie=0;/*setthisto1forshutdown*/ /* *Theworkqueuestructureforthistask,fromworkqueue.h */ staticstructworkqueue_struct*my_workqueue; staticstructwork_structTask; staticDECLARE_WORK(Task,intrpt_routine,NULL); /* *thevoid* *pointer-taskfunctionscanbeusedformorethanonepurpose,eachtime *gettingadifferentparameter. */ staticvoidintrpt_routine(void*irrelevant) { /* *Incrementthecounter */ TimerIntrpt++; 77 LINUX内核模块编程[转] /* *Ifcleanupwantsustodie */ if(die==0) queue_delayed_work(my_workqueue,&Task,100); } /* *Putdataintotheprocfsfile. */ ssize_t procfile_read(char*buffer, char**buffer_location, off_toffset,intbuffer_length,int*eof,void*data) { intlen; /* *It'sstaticsoitwillstillbeinmemory *whenweleavethisfunction */ staticcharmy_buffer[80]; staticintcount=1; /* *Wegiveallofourinformationinonego,soiftheanybodyasksus *ifwehavemoreinformationtheanswershouldalwaysbeno. */ if(offset>0) return0; /* *Fillthebufferandgetitslength */ len=sprintf(my_buffer,"Timercalled%dtimessofarn",TimerIntrpt); count++; /* *Tellthefunctionwhichcalleduswherethebufferis */ *buffer_location=my_buffer; /*Thenumberofbytesactuallyused*/ 78 LINUX内核模块编程[转] /* *Returnthelength */ returnlen; } /* *Initializethemodule-registertheprocfile */ int__initinit_module() { intrv=0; /* *Putthetaskinthework_timertaskqueue,soitwillbeexecutedat *nexttimerinterrupt */ my_workqueue=create_workqueue(MY_WORK_QUEUE_NAME); queue_delayed_work(my_workqueue,&Task,100); Our_Proc_File=create_proc_entry(PROC_ENTRY_FILENAME,0644,NULL); Our_Proc_File->read_proc=procfile_read; Our_Proc_File->owner=THIS_MODULE; Our_Proc_File->mode=S_IFREG|S_IRUGO; Our_Proc_File->uid=0; Our_Proc_File->gid=0; Our_Proc_File->size=80; if(Our_Proc_File==NULL){ rv=-ENOMEM; remove_proc_entry(PROC_ENTRY_FILENAME,&proc_root); printk(KERN_INFO"Error:Couldnotinitialize/proc/%sn", PROC_ENTRY_FILENAME); } returnrv; } /* *Cleanup */ 79 LINUX内核模块编程[转] void__exitcleanup_module() { /* *Unregisterour/procfile */ remove_proc_entry(PROC_ENTRY_FILENAME,&proc_root); printk(KERN_INFO"/proc/%sremovedn",PROC_ENTRY_FILENAME); die=1;/*keepintrp_routinefromqueueingitself*/ /*no"newones"*/ /*waittillall"oldones"finished*/ cancel_delayed_work(&Task); flush_workqueue(my_workqueue); destroy_workqueue(my_workqueue); /* *Sleepuntilintrpt_ *necessary,becauseotherwisewe'lldeallocatethememoryholding *intrpt_routineandTaskwhilework_timerstillreferencesthem. *Noticethatherewedon'tallowsignalstointerruptus. * *SinceWaitQisnownotNULL,thisautomaticallytellstheinterrupt *routineit'stimetodie. */ } /* *somework_queuerelatedfunctions *arejustavailabletoGPLlicensedModules */ MODULE_LICENSE("GPL"); Notes [1]它们实际上是一回事。 80 LINUX内核模块编程[转] uptHandlers InterruptHandlers 除了刚结束的那章,我们目前在内核中所做的每件事都只不过是对某个请求的进程的响应,要么是对某 个特殊的文件的处理,要么是发送一个ioctl(),要么是调用一个系统调用。但是内核的工作不仅仅是响 应某个进程的请求。还有另外一项非常重要的工作就是负责对硬件的管理。 在CPU和硬件之间的活动大致可分为两种。第一种是CPU发送指令给硬件,第二种就是硬件要返回某 些信息给CPU。后面的那种又叫做中断,因为要知道何时同硬件对话才适宜而较难实现。硬件设备通常 只有很少的缓存,如果你不及时的读取里面的信息,这些信息就会丢失。 在Linux中,硬件中断被叫作IRQ(InterruptRequests,中断请求)[1]。有两种硬件中断,短中断 和长中断。短中断占用的时间非常短,在这段时间内,整个系统被阻塞,任何其它中断都不会处理。长 中断占用的时间相对较长,在此期间,可能会有别的中断发生请求处理(不是相同设备发出的中断)。 可能的话,尽量将中断声明为长中断。 当CPU接收到一个中断时,它停止正在处理的一切事务(除非它在处理另一个更重要的中断,在这种 情况下它只会处理完这个重要的中断才会回来处理新产生的中断),将运行中的那些参数压入栈中然后 调用中断处理程序。这同时意味着中断处理程序本身也有一些限制,因为此时系统的状态并不确定。解 决的办法是让中断处理程序尽快的完成它的事务,通常是从硬件读取信息和向硬件发送指令,然后安排 下一次接收信息的相关处理(这被称为"bottomhalf"[2]),然后返回。内核确保被安排的事务被尽快 的执行。当被执行时,在内核模块中允许的操作就是被允许的。 实现的方法是调用request_irq()函数,当接受到相应的IRQ时(共有15种中断,在Intel架构平台 上再加上1种用于串连中断控制器的中断)去调用你的中断处理程序。该函数接收IRQ号,要调用的 处理IRQ函数的名称,中断请求的类别标志位,文件/proc/interrupts中声明的设备的名字,和传递 给中断处理程序的参数。中断请求的类别标志位可以为SA_SHIRQ来告诉系统你希望与其它中断处理 程序共享该中断号(这通常是由于一些设备共用相同的IRQ号),也可以为SA_INTERRUPT来告诉 系统这是一个快速中断,这种情况下该函数只有在该IRQ空闲时才会成功返回,或者同时你又决定共享 该IQR。 然后,在中断处理程序内部,我们与硬件对话,接着使用带tq_immediate()和 mark_bh(BH_IMMEDIATE)的queue_task_irq()去对bottomhalf队列进行调度。我们不能使用 2.0版本种标准的queue_task的原因是中断可能就发生在别人的queue_task[3]中。我们需要 mark_bh是因为早期版本的Linux只有一个可以存储32个bottomhalf的数组,并且现在它们中的 一个(BH_IMMEDIATE)已经被用来连接没有分配到队列中的入口的硬件驱动的bottomhalf。 Intel架构中的键盘 剩余的这部分是只适用Intel架构的。如果你不使用Intel架构的平台,它们将不会工作,不要去尝试编 译以下的代码。 在写这章的事例代码时,我遇到了一些困难。一方面,我需要一个可以得到实际有意义结果的,能在各 种平台上工作的例子。另一方面,内核中已经包括了各种设备驱动,并且这些驱动将无法和我的例子共 81 LINUX内核模块编程[转] 存。我找到的解决办法是为键盘中断写点东西,当然首先禁用普通的键盘中断。因为该中断在内核中定 义为一个静态连接的符号(见drivers/char/keyboard.c)),我们没有办法恢复。所以在insmod前, 如果你爱惜你的机器,新打开一个终端运行sleep120;reboot。 该代码将自己绑定在IRQ1,也就是Intel架构中键盘的IRQ。然后,当接收到一个键盘中断请求时, 它读取键盘的状态(那就是inb(0x64)的目的)和扫描码,也就是键盘返回的键值。然后,一旦内核认 为这是符合条件的,它运行got_char去给出操作的键(扫描码的头7个位)和是按下键(扫描码的第 8位为0)还是弹起键(扫描码的第8位为1)。 .c /* *intrpt.c-Aninterrupthandler. * *Copyright(C)2001byPeterJaySalzman */ /* *Thenecessaryheaderfiles */ /* *Standardinkernelmodules */ #include #include #include #include #include #include #defineMY_WORK_QUEUE_NAME"WQsched.c" staticstructworkqueue_struct*my_workqueue; /* *Thiswillgetcalledbythekernelassoonasit'ssafe *todoeverythingnormallyallowedbykernelmodules. */ staticvoidgot_char(void*scancode) { printk("ScanCode%x%s.n", (int)*((char*)scancode)&0x7F, *((char*)scancode)&0x80?"Released":"Pressed"); } /*Wewantaninterrupt*/ /*We'redoingkernelwork*/ /*Specifically,amodule*/ 82 LINUX内核模块编程[转] /* *stherelevant *informationfromthekeyboardandthenputsthenontimecritical *llberunwhenthekernelconsidersitsafe. */ irqreturn_tirq_handler(intirq,void*dev_id,structpt_regs*regs) { /* *Thisvariablesarestaticbecausetheyneedtobe *accessible(throughpointers)tothebottomhalfroutine. */ staticintinitialised=0; staticunsignedcharscancode; staticstructwork_structtask; unsignedcharstatus; /* *Readkeyboardstatus */ status=inb(0x64); scancode=inb(0x60); if(initialised==0){ INIT_WORK(&task,got_char,&scancode); initialised=1; }else{ PREPARE_WORK(&task,got_char,&scancode); } queue_work(my_workqueue,&task); returnIRQ_HANDLED; } /* *Initializethemodule-registertheIRQhandler */ intinit_module() { my_workqueue=create_workqueue(MY_WORK_QUEUE_NAME); 83 LINUX内核模块编程[转] /* *Sincethekeyboardhandlerwon'tco-existwithanotherhandler, *suchasus,wehavetodisableit(freeitsIRQ)beforewedo *edon'tknowwhereitis,there'snowayto *reinstateitlater-sothecomputerwillhavetoberebooted *whenwe'redone. */ free_irq(1,NULL); /* *RequestIRQ1,thekeyboardIRQ,togotoourirq_handler. *SA_SHIRQmeanswe'rewillingtohaveothehandlersonthisIRQ. *SA_INTERRUPTcanbeusedtomakethehandlerintoafastinterrupt. */ returnrequest_irq(1, irq_handler, /*ThenumberofthekeyboardIRQonPCs*/ /*ourhandler*/ SA_SHIRQ,"test_keyboard_irq_handler", (void*)(irq_handler)); } /* *Cleanup */ voidcleanup_module() { /* *'stotallyirrelevant,since *wedon'thaveawaytorestorethenormalkeyboardinterruptsothe *computeriscompletelyuselessandhastoberebooted. */ free_irq(1,NULL); } /* *somework_queuerelatedfunctionsarejustavailabletoGPLlicensedModules */ MODULE_LICENSE("GPL"); Notes [1]这是Linux起源的Intel架构中的标准的起名方法。 [2]这里是译者给出的关于“bottomhalf”的一点解释,来源是google上搜索到的英文资料: “底部”,“bottomhalf”常在涉及中断的设备驱动中提到。 84 LINUX内核模块编程[转] 当内核接收到一个中断请求,对应的设备驱动被调用。因为在这段时间内无法处理别的任何事务,让中 断处理尽快的完成并重新让内核返回正常的工作状态是非常重要的。就是因为这个设计思想,驱动的“顶 部”和“底部”的概念被提出:“顶部”是被内核调用时最先被执行的部分,快速的完成一些尽量少的却是必 需的工作(像对硬件或其它资源的独享访问这种必须立刻执行的操作),然后做一些设置让“底部”去完 成那些要求时间相对比较宽裕的,剩下的工作。 “底部”什么时候如何运作是内核的设计问题。你也许会听到“底部”的设计已经在最近的内核中被废除了。 这种说法不是很确切,在新内核中其实你可以去选择怎样去执行:像软中断或任务,就像它们以前那样, 还是加入任务队列,更像启动一个用户进程。 [3]queue_task_irq被一个全局的锁(有锁定作用的变量) queue_task_irq而且queue_task也是被一个锁保护的。 保护着,在版本2.2中,并没有 85 LINUX内核模块编程[转] ricMultiProcessing 对称多线程处理 提高性能的最简单也是最便宜的方法是给你的主板加第二个CPU(如果你的主板支持的话)。这可以通 过让不同的CPU完成不同的工作(非对称多线程处理)或是相同的工作(对称多线程处理)。实现高效 率的非对称的多线程处理需要特殊硬件相关的知识,而对于Linux这样通用操作系统这是不可能的。相 对而言,对称多线程处理是较容易实现的。 我这里所说的相对容易,老实说,还是不容易。在一个对称多线程处理的环境中,多个CPU共享内存, 导致的结果是其中一个CPU运行的代码会对别的CPU也产生影响。你不能再确定你代码中第一行中设 置的变量在接下来的那行代码中还是那个设置值;其它的CPU可能会趁你不注意已经把它修改了。显 然,如果是这样的话,是无法进行任何编程的。 对于进程层面上的编程这通常不是个问题,因为一个进程通常同一时间只在一个CPU上运行[1]。但 是,对于内核,就可以被在不同的CPU上的同时运行的不同的进程使用。 在内核版本2.0.x中,这还不算作什么问题,因为整个内核是一个spinlock[2],这就意味着一旦某个 CPU进入内核态,别的CPU将不允许进入内核态。这使Linux的SMP实现很安全[3],但缺乏效率。 在内核版本2.2.x以后,多CPU已经允许同时进入内核态。内核模块的作者应该意识到这一点。 Notes [1]存在例外,就是线程化的进程,这样的进程可以在多个CPU上同时运行。 [2]抱歉,我没有找到合适的词语来表达这个单词。这是内核中的一种机制,可以对内核中的关键数据 结构进行锁定保护,防止其被破坏。 [3]意味着这样的SMP机制使用起来很安全。 86 LINUX内核模块编程[转] Pitfalls 注意 在我让你们进入内核模块的世界之前,我需要提醒你们下面的一些注意。如果我没警告到你们但是的确 发生了,那么你将问题报告我,我将全额退还你的书款。 使用标准库文件: 你无法这样做。在内核模块中,你只能使用内核提供的函数,也就是你在/proc/kallsyms能查到的那 些。 禁用中断: 你如果这样做了但只是一瞬间,没问题,当我没提这事。但是事后你没有恢复它们,你就只能摁电源键 来重启你僵死的系统了。 尝试一些非常危险的东西: 这也许不应该由我来说,但是以防万一,我还是提出来吧! s:2.0To2.2 从2.0到2.2的变化 从2.0到2.2的变化 我对内核的了解并不很完全所以我也无法写出所有的变化。在修改代码(更确切的说,是采用 EmmanuelPapirakis的修改)时,我遇到了以下的这些修改。我将它们都列出来以方便模块编写者 们,特别是学习该档案先前版本并熟悉我提到的这些技巧(但已经更换到新版本的)的那些人。 更多的这方面的参考资料在RichardGooch's的站点上。 asm/uaccess.h 如果你要使用put_user或get_user你就需要#include它。 get_user 在2.2版本中,get_user同时接收用户内存的指针和用来设置信息的内核内存中变量的内存指针。变 化的原因是因为当我们读取的变量是二或四个字节长的时候,get_user也可以读取二或四个字节长的 变量。 file_operations 改结构体现在有了一个可以在open和close之间进行的刷新操作函数。 closeinfile_operations 2.2版本中,close返回整形值,所以可以检测是否失败。 87 LINUX内核模块编程[转] read,writeinfile_operations 这些函数的头文件改变了。它们现在返回ssize_t而不是整形值,且它们的参数表也变了。inode不再 是一个参数,文件中的偏移量也一样。 proc_register_dynamic 该函数已经不复存在。你应该使用用0作为inode参数的proc_register函数来替代它。 Signals 在task结构体中的signals不再是一个32位整形变量,而是一个为_NSIG_WORDS整形的数组。 queue_task_irq 即使你想在中断处理内部调度一个任务,你也应该使用queue_task而不是queue_task_irq。 ModuleParameters 你不必在将模块参数声明为全局变量。在2.2中,使用MODULE_PARM去声明模块参数。这是一个进 步,这样就允许模块接受以数字开头的参数名而不会被弄糊涂。 SymmetricalMulti-Processing 内核本省已不再是一个spinlock,意味着你的模块也应该考虑SMP的问题。 88 LINUX内核模块编程[转] oGoFromHere 为什么这样写? 我其实可以给这本书再加入几章,例如如何为实现新的文件系统加上一章,或是添加一个新的协议栈(如 果有这样的必要的话,想找到Linux不支持的网络协议已经是非常的困难的了)。我还可以解释一下我 们尚未接触到的内核实现机制,像系统的引导自举,或磁盘存储。 但是,我决定否。我写本书的目的是提供基本的,入门的对神秘的内核模块编程的认识和这方面的常用 技巧。对于那些非常热衷与内核编程的人,我推荐Juan-MarianodeGoyeneche的内核资源列表。 同样,就同Linus本人说的那样,学习内核最好的方法是自己阅读内核源代码。 如果你对更多的短小的示例内核模块感兴趣,我向你推荐Phrackmagazine这本杂志。即使你不关 心安全问题,作为一个程序员你还是应该时时考虑这个问题的。这些内核模块代码都很短,不需要费多 大劲就能读懂。 我希望我满足了你希望成为一个更优秀的程序员的要求,至少在学习技术的过程中体会到了乐趣。如果 你真的写了一些非常有用的模块,我希望你使用GPL许可证发布你的模块,这样我也就可以使用它们了。 89
版权声明:本文标题:Linux内核开发 内容由热心网友自发贡献,该文观点仅代表作者本人,
转载请联系作者并注明出处:https://m.elefans.com/dianzi/1721784902a897143.html,
本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论