[ZLG-ARM] 内核模块编程之入门

[复制链接]
2293|5
 楼主| tmake 发表于 2009-7-4 13:53 | 显示全部楼层 |阅读模式
内核模块是Linux内核向外部提供的一个插口,其全称为动态可加载内核模块(Loadable&nbsp;Kernel&nbsp;Module,LKM),我们简称为模块。Linux内核之所以提供模块机制,是因为它本身是一个单内核(monolithic&nbsp;kernel)。单内核的最大优点是效率高,因为所有的内容都集成在一起,但其缺点是可扩展性和可维护性相对较差,模块机制就是为了弥补这一缺陷。<br /><br />一、什么是模块<br />模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程是不同的。模块通常由一组函数和数据结构组成,用来实现一种文件系统、一个驱动程序或其他内核上层的功能。<br /><br />二、编写一个简单的模块<br />模块和内核都在内核空间运行,模块编程在一定意义上说就是内核编程。因为内核版本的每次变化,其中的某些函数名也会相应地发生变化,因此模块编程与内核版本密切相关。以下例子针对2.6内核<br /><br />1.程序举例<br /><br /><br />代码:<br />hellomod.c001//&nbsp;hello&nbsp;world&nbsp;driver&nbsp;for&nbsp;Linux&nbsp;2.6&nbsp;004&nbsp;&nbsp;#include&nbsp;&ltlinux/module.h&gt005&nbsp;&nbsp;#include&nbsp;&ltlinux/kernel.h&gt006&nbsp;&nbsp;#include&nbsp;&ltlinux/init.h&gt&nbsp;&nbsp;/*&nbsp;必要的头文件*/&nbsp;009&nbsp;&nbsp;static&nbsp;int&nbsp;__init&nbsp;lkp_init(&nbsp;void&nbsp;){&nbsp;&nbsp;printk('&lt1&gtHello,World!&nbsp;from&nbsp;the&nbsp;kernel&nbsp;space...\n');&nbsp;&nbsp;return&nbsp;0;013&nbsp;&nbsp;}&nbsp;015&nbsp;&nbsp;static&nbsp;void&nbsp;__exit&nbsp;lkp_cleanup(&nbsp;void&nbsp;){&nbsp;&nbsp;printk('&lt1&gtGoodbye,&nbsp;World!&nbsp;leaving&nbsp;kernel&nbsp;space...\n');018&nbsp;&nbsp;}&nbsp;020&nbsp;&nbsp;module_init(lkp_init);021&nbsp;&nbsp;module_exit(lkp_cleanup);022&nbsp;&nbsp;MODULE_LICENSE('GPL');<br /><br />2.说明&nbsp;&nbsp;<br />第4行:所有模块都要使用头文件module.h,此文件必须包含进来。<br /><br />第5行:头文件kernel.h包含了常用的内核函数。<br /><br />第6行:头文件init.h包含了宏_init和_exit,它们允许释放内核占用的内存。建议浏览一下该文件中的代码和注释。<br /><br />第9-12行:这是模块的初始化函数,它必需包含诸如要编译的代码、初始化数据结构等内容。<br /><br />第11行使用了printk()函数,该函数是由内核定义的,功能与C库中的printf()类似,它把要打印的信息输出到终端或系统日志。字符串中的&lt1&gt是输出的级别,表示立即在终端输出。&nbsp;&nbsp;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />第15-18行:这是模块的退出和清理函数。此处可以做所有终止该驱动程序时相关的清理工作。<br /><br />第20行:这是驱动程序初始化的入口点。对于内置模块,内核在引导时调用该入口点;对于可加载模块则在该模块插入内核时才调用。<br /><br />第21行:对于可加载模块,内核在此处调用module_cleanup()函数,而对于内置的模块,它什么都不做。&nbsp;<br /><br />第22行:提示可能没有GNU公共许可证。有几个宏是在2.4版的内核中才开发的(详情参见modules.h)。&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br /><br />函数module_init()和cleanup_exit()是模块编程中最基本也是必须的两个函数。module_init()向内核注册模块所提供的新功能,而cleanup_exit()注销由模块提供的所有功能。<br />&nbsp;<br /> &nbsp;&nbsp;<br />模块编程属于内核编程,因此,除了对内核相关知识有所了解外,还需要了解与模块相关的知识。<br />1.应用程序与内核模块的比较<br />为了加深对内核模块的了解,表一给出应用程序与内核模块程序的比较。<br />表一&nbsp;应用程序与内核模块程序的比较&nbsp;<br />C语言应用程序<br />&nbsp;&nbsp;&nbsp;内核模块程序<br /><br />使用函数<br />&nbsp;&nbsp;&nbsp;Libc库<br />&nbsp;&nbsp;&nbsp;&nbsp;内核函数<br /><br />运行空间<br />&nbsp;&nbsp;&nbsp;用户空间<br />&nbsp;&nbsp;&nbsp;&nbsp;内核空间<br /><br />运行权限<br />&nbsp;&nbsp;&nbsp;普通用户<br />&nbsp;&nbsp;&nbsp;&nbsp;超级用户<br /><br />入口函数<br />&nbsp;&nbsp;&nbsp;&nbsp;main()<br />&nbsp;&nbsp;&nbsp;module_init()<br /><br />出口函数<br />&nbsp;&nbsp;&nbsp;&nbsp;exit()<br />&nbsp;&nbsp;&nbsp;module_exit()<br /><br />编译<br />&nbsp;&nbsp;&nbsp;Gcc&nbsp;–c<br />&nbsp;&nbsp;&nbsp;Makefile&nbsp;<br />连接<br />&nbsp;&nbsp;&nbsp;Gcc<br />&nbsp;&nbsp;&nbsp;insmod<br /><br />运行<br />&nbsp;&nbsp;&nbsp;直接运行<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;insmod<br /><br />调试<br />&nbsp;&nbsp;&nbsp;Gdb<br />kdbug,&nbsp;kdb,kgdb等<br /><br />从表一我们可以看出,内核模块程序不能调用libc库中的函数,它运行在内核空间,且只有超级用户可以对其运行。另外,模块程序必须通过module_init()和module-exit()函数来告诉内核“我来了”和“我走了”。<br /><br /><br />2.内核符号表(如果对以下第2~4点理解上有困难,可以越过)<br /><br />如前所述,Linux内核是一个整体结构,像一个圆球,而模块是插入到内核中的插件。尽管内核不是一个可安装模块,但为了方便起见,Linux把内核也看作一个“母”模块。那么模块与模块之间如何进行交互呢,一种常用的方法就是共享变量和函数。但并不是模块中的每个变量和函数都能被共享,内核只把各个模块中主要的变量和函数放在一个特定的区段,这些变量和函数就统称为符号。到低哪些符号可以被共享?&nbsp;Linux内核有自己的规定。对于内核这个特殊的母模块,在kernel/ksyms.c中定义了从中可以“移出”的符号,例如进程管理子系统可以“移出”的符号定义如下:<br /><br />/*&nbsp;进程管理&nbsp;*/<br /><br />EXPORT_SYMBOL(do_mmap_pgoff);<br /><br />EXPORT_SYMBOL(do_munmap);<br /><br />EXPORT_SYMBOL(do_brk);<br /><br />EXPORT_SYMBOL(exit_mm);<br /><br />…<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;EXPORT_SYMBOL(schedule);<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;EXPORT_SYMBOL(jiffies);<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;EXPORT_SYMBOL(xtime);<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;…<br /><br />你可能对这些变量和函数已经很熟悉。其中宏定义EXPORT_SYMBOL()本身的含义是“移出符号”。为什么说是“移出”呢?因为这些符号本来是内核内部的符号,通过这个宏放在一个公开的地方,使得装入到内核中的其他模块可以引用它们。<br /><br />实际上,仅仅知道这些符号的名字是不够的,还得知道它们在内核地址空间中的地址才有意义。因此,内核中定义了如下结构来描述模块的符号:<br /><br />struct&nbsp;module_symbol<br /><br />{<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unsigned&nbsp;long&nbsp;value;&nbsp;/*符号在内核地址空间中的地址*/<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;const&nbsp;char&nbsp;*name;&nbsp;&nbsp;&nbsp;/*符号名*/<br /><br />};<br /><br />我们可以从/proc/ksyms文件中读取所有内核模块“移出”的符号,这所有符号就形成内核符号表,其格式如下:<br /><br /><br />内存地址&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;符号名&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[所属模块]<br /><br /><br />在模块编程中,可以根据符号名从这个文件中检索出其对应的地址,然后直接访问该地址从而获得内核数据。第三列“所属模块”指符号所在的模块名,对于从内核这一母模块移出的符号,这一列为空。<br /><br />&nbsp;&nbsp;模块加载后,2.4内核下可通过&nbsp;/proc/ksyms、&nbsp;2.6&nbsp;内核下可通过/proc/kallsyms查看模块输出的内核符号<br /><br />3.模块依赖<br /><br />&nbsp;&nbsp;&nbsp;如前所述,内核符号表记录了所有模块可以访问的符号及相应的地址。当一个新的模块被装入内核后,它所申明的某些符号就会被登记到这个表中,而这些符号可能被其他模块所引用,这就引出了模块依赖这个问题。<br /><br />&nbsp;&nbsp;&nbsp;一个模块A引用另一个模块B所移出的符号,我们就说模块B被模块A引用,或者说模块A依赖模块B。如果要链接模块A,必须先链接模块B。这种模块间相互依赖的关系就叫模块依赖。<br /><br />4.模块引用计数器<br /><br />&nbsp;&nbsp;&nbsp;为了确保模块安全地卸载,每个模块都有一个引用计数器。当执行模块所涉及的操作时就递增计数器,在操作结束时就递减这个计数器;另外,当模块B被模块A引用时,模块B的引用计数就递增,引用结束,计数器递减。什么时候可以卸载这个模块?当然只有这个计数器值为0的时候,例如,当一个文件系统还被安装在系统上时就不能将其卸载,当这个文件系统不再被使用时,引用计数器就为0,于是可以卸载。<br /><br />四.模块编译<br /><br />&nbsp;&nbsp;&nbsp;Linux&nbsp;中最重要的软件开发工具是&nbsp;GCC。GCC&nbsp;是&nbsp;GNU&nbsp;的&nbsp;C&nbsp;和&nbsp;C++&nbsp;编译器。但是,在大型的开发项目中,通常有几十到上百个的源文件,如果每次均手工键入&nbsp;gcc&nbsp;命令进行编译的话,则会非常不方便。因此,人们通常利用&nbsp;make&nbsp;工具来自动完成编译工作。利用这种自动编译可大大简化开发工作,避免不必要的重新编译。这些工作包括:如果仅修改了某几个源文件,则只重新编译这几个源文件;如果某个头文件被修改了,则重新编译所有包含该头文件的源文件。<br /><br />1.编译工具make<br /><br />实际上,make&nbsp;工具通过一个称为&nbsp;Makefile&nbsp;的文件来完成并自动维护编译工作。Makefile&nbsp;需要按照某种语法进行编写,其中说明了如何编译各个源文件并连接生成可执行文件,并定义了源文件之间的依赖关系。下面给出2.6&nbsp;内核模块的Makefile模板(有点复杂,实在看不懂,可以仅作参考)<br />#&nbsp;Makefile2.6<br />ifneq&nbsp;($(KERNELRELEASE),)<br />#kbuild&nbsp;syntax.&nbsp;dependency&nbsp;relationshsip&nbsp;of&nbsp;files&nbsp;and&nbsp;target&nbsp;modules&nbsp;are&nbsp;listed&nbsp;here.<br />mymodule-objs&nbsp;:=&nbsp;file1.o&nbsp;file2.o<br />&nbsp;&nbsp;&nbsp;obj-m&nbsp;:=&nbsp;mymodule.o&nbsp;<br />else<br />&nbsp;&nbsp;PWD&nbsp;&nbsp;:=&nbsp;$(shell&nbsp;pwd)<br />&nbsp;&nbsp;KVER&nbsp;?=&nbsp;$(shell&nbsp;uname&nbsp;-r)<br />&nbsp;&nbsp;KDIR&nbsp;:=&nbsp;/lib/modules/$(KVER)/build<br />all:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />&nbsp;&nbsp;&nbsp;&nbsp;$(MAKE)&nbsp;-C&nbsp;$(KDIR)&nbsp;M=$(PWD)&nbsp;<br />clean:<br />&nbsp;&nbsp;&nbsp;rm&nbsp;-rf&nbsp;.*.cmd&nbsp;*.o&nbsp;*.mod.c&nbsp;*.ko&nbsp;.tmp_versions<br />endif<br /><br />KERNELRELEASE是在内核源码的顶层Makefile中定义的一个变量,在第一次读取执行此Makefile时,KERNELRELEASE没有被定义,&nbsp;所以make将读取执行else之后的内容。如果make的目标是clean,直接执行clean操作,然后结束。当make的目标为all时,-C&nbsp;$(KDIR)&nbsp;指明跳转到内核源码目录下读取那里的Makefile;M=$(PWD)&nbsp;表明然后返回到当前目录继续读入、执行当前的Makefile。当从内核源码目录返回时,KERNELRELEASE已被被定义,kbuild也被启动去解析kbuild语法的语句,make将继续读取else之前的内容。else之前的内容为kbuild语法的语句,&nbsp;指明模块源码中各文件的依赖关系,以及要生成的目标模块名。mymodule-objs&nbsp;:=&nbsp;file1.o&nbsp;file2.o表示mymoudule.o&nbsp;由file1.o与file2.o&nbsp;连接生成。obj-m&nbsp;:=&nbsp;mymodule.o表示编译连接后将生成mymodule.o模块。<br />补充一点,'$(MAKE)&nbsp;-C&nbsp;$(KDIR)&nbsp;M=$(PWD)'与'$(MAKE)&nbsp;-C&nbsp;$(KDIR)&nbsp;SUBDIRS&nbsp;=$(PWD)'的作用是等效的,后者是较老的使用方法。推荐使用M而不是SUBDIRS,前者更明确。<br />通过以上比较可以看到,从Makefile编写来看,在2.6内核下,内核模块编译不必定义复杂的CFLAGS,而且模块中各文件依赖关系的表示简洁清晰。<br /><br /><br />&nbsp;&nbsp;例如,要把第一部分中所提到的hellomod.c编译成一个模块,简单的使用GCC无法完成。最好写一个Makefile文件,内容如下(以后写其他模块的Makefile可以如法**制,改变一下模块名字即可)<br />&nbsp;&nbsp;<br />obj-m&nbsp;+=&nbsp;hellomod.o<br />all:<br />&nbsp;&nbsp;&nbsp;&nbsp;make&nbsp;-C&nbsp;/usr/src/linux&nbsp;&nbsp;M=$(PWD)&nbsp;modules<br />clean:<br />&nbsp;&nbsp;&nbsp;&nbsp;make&nbsp;-C&nbsp;/usr/src/linux&nbsp;&nbsp;M=$(PWD)&nbsp;clean&nbsp;<br /><br />&nbsp;&nbsp;&nbsp;在这里要特别说明的是,/usr/src/linux&nbsp;中的Linux目录,因发布版不同而不同,比如在我安装的Ubuntu下为linux-headers-2.6.20.16-generic,因此,只需要建立一个针对Linux的符号链接就可以。<br />有了Makefile,执行make命令,会自动形成相关的后缀为.o和.ko文件。<br />&nbsp;&nbsp;到此,模块编译好了,该把它插入到内核了:<br />&nbsp;&nbsp;如:insmod&nbsp;hellomod.ko<br /><br />&nbsp;&nbsp;是否插入成功可以通过dmesg命令查看,屏幕最后几行的输出就是你程序中输出的内容。<br />&nbsp;&nbsp;当不再模块需要时,可以通过rmmod命令移去。<br />&nbsp;<br /> &nbsp;&nbsp;<br />modutils是管理内核模块的一个软件包。可以在任何获得内核源代码的地方获取Modutils(modutils-x.y.z.tar.gz)源代码,然后选择最高级别的patch.x.y.z等于或小于当前的内核版本,安装后在/sbin目录下就会有insomod、rmmod、ksyms、lsmod、modprobe等实用程序。当然,通常我们在加载Linux内核时,modutils已经被载入。<br /><br />1.Insmod命令<br />&nbsp;&nbsp;&nbsp;调用insmod程序把需要插入的模块以目标代码的形式插入到内核中。在插入的时候,insmod自动调用init_module()函数运行。注意,只有超级用户才能使用这个命令,其命令格式为:<br />#&nbsp;insmod&nbsp;&nbsp;[path]&nbsp;modulename.c<br /><br />2.&nbsp;rmmod命令<br />&nbsp;&nbsp;&nbsp;调用rmmod程序将已经插入内核的模块从内核中移出,rmmod会自动运行cleanup_module()函数,其命令格式为:<br />&nbsp;&nbsp;#rmmod&nbsp;&nbsp;[path]&nbsp;modulename.c<br /><br />3.lsmod命令<br />&nbsp;&nbsp;&nbsp;调用lsmod程序将显示当前系统中正在使用的模块信息。实际上这个程序的功能就是读取/proc文件系统中的文件/proc/modules中的信息,其命令格式为:<br />&nbsp;&nbsp;&nbsp;&nbsp;#lsmod<br /><br />4.ksyms命令<br />&nbsp;&nbsp;&nbsp;ksyms这个程序用来显示内核符号和模块符号表的信息。与lsmod相似,它的功能是读取/proc文件系统中的另一个文件/proc/kallsyms。<br />在此,我们将编写一个模块,其中有一个中断函数,当内核接收到某个&nbsp;IRQ&nbsp;上的一个中断时会调用它。先给出全部代码,读者自己调试,把对该程序的理解跟到本贴后面。<br /><br />----------------------------------------<br /><br />#include&nbsp;<br />#include&nbsp;<br />#include&nbsp;<br /><br />static&nbsp;int&nbsp;irq;<br />static&nbsp;char&nbsp;*interface;<br /><br />//MODULE_PARM_DESC(interface,'A&nbsp;network&nbsp;interface');&nbsp;&nbsp;2.4内核中该宏的用法<br />molule_parm(interface,charp,0644)&nbsp;//2.6内核中的宏<br />//MODULE_PARM_DESC(irq,'The&nbsp;IRQ&nbsp;of&nbsp;the&nbsp;network&nbsp;interface');<br />module_param(irq,int,0644);<br /><br />static&nbsp;irqreturn_t&nbsp;myinterrupt(int&nbsp;irq,&nbsp;void&nbsp;*dev_id,&nbsp;struct&nbsp;pt_regs&nbsp;*regs)<br />{<br />&nbsp;&nbsp;&nbsp;&nbsp;static&nbsp;int&nbsp;mycount&nbsp;=&nbsp;0;<br />&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(mycount&nbsp;&lt&nbsp;10)&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;printk('Interrupt!\n');<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;mycount++;<br />&nbsp;&nbsp;&nbsp;&nbsp;}<br />&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;IRQ_NONE;<br />}<br />static&nbsp;int&nbsp;__init&nbsp;myirqtest_init(void)<br />{&nbsp;<br />&nbsp;&nbsp;&nbsp;&nbsp;printk&nbsp;('My&nbsp;module&nbsp;worked!11111\n');<br />&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(request_irq(irq,&nbsp;&myinterrupt,&nbsp;SA_SHIRQ,interface,&nbsp;&irq))&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;printk(KERN_ERR&nbsp;'myirqtest:&nbsp;cannot&nbsp;register&nbsp;IRQ&nbsp;%d\n',&nbsp;irq);<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;-EIO;<br />&nbsp;&nbsp;&nbsp;&nbsp;}<br />&nbsp;&nbsp;&nbsp;&nbsp;printk('%s&nbsp;Request&nbsp;on&nbsp;IRQ&nbsp;%d&nbsp;succeeded\n',interface,irq);<br />&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;0;<br />}<br />static&nbsp;void&nbsp;__exit&nbsp;myirqtest_exit(void)<br />{<br />&nbsp;&nbsp;&nbsp;&nbsp;printk&nbsp;('Unloading&nbsp;my&nbsp;module.\n');<br />&nbsp;&nbsp;&nbsp;&nbsp;free_irq(irq,&nbsp;&irq);<br />&nbsp;&nbsp;&nbsp;&nbsp;printk('Freeing&nbsp;IRQ&nbsp;%d\n',&nbsp;irq);<br />&nbsp;&nbsp;&nbsp;&nbsp;return;<br />}<br />module_init(myirqtest_init);<br />module_exit(myirqtest_exit);<br />MODULE_LICENSE('GPL');<br /><br /><br />----------------------------------------<br />这里要说明的是,在插入模块时,可以带两个参数,例如<br />insmod&nbsp;myirq.ko&nbsp;interface=eth0&nbsp;irq=9<br /><br />其中&nbsp;具体网卡&nbsp;irq的值可以查看&nbsp;cat&nbsp;/proc/interrupts<br />
lpc2410 发表于 2009-7-6 12:40 | 显示全部楼层

内核编程比较难了

  
 楼主| tmake 发表于 2009-7-9 13:37 | 显示全部楼层

学习了

  
armqt 发表于 2009-7-24 18:09 | 显示全部楼层

谢谢……

  
armqt 发表于 2009-7-24 18:09 | 显示全部楼层

需要用心去搞编程

  
孤独行者 发表于 2009-7-24 21:33 | 显示全部楼层

需要一点时间

  
您需要登录后才可以回帖 登录 | 注册

本版积分规则

40

主题

179

帖子

0

粉丝
快速回复 在线客服 返回列表 返回顶部