Linux内核模块编程是一个很重要的知识点。尤其是编撰底层驱动程序时,一定会涉及到它。内核模块编程也是Tiger哥学习Linux时第一节课所接触的知识。由此可以看出它的important,也可以看出虽然它很easy。
一引言:
1.哪些是内核模块
1>内核模块是具有独立功能的程序。它可以被单独编译,而且不能单独运行,它的运行必须被链接到内核作为内核的一部份在内核空间中运行。
2>模块编程和内核版本密切相连,由于不同的内核版本中个别函数的函数名会有变化。因而模块编程也可以说是内核编程。
3>特征:
模块本身不被编译进内核映像,进而控制了内核的大小;
模块一旦被加载,就和内核中的其他部份完全一样。
2.用户层编程和内核模块编程的区别
二.说了那么多,这么如何编撰一个内核模块的程序呢?
1.我们先来看两个最简单的函数实例,也是几乎所有程序员在学习一门新语言时就会编撰的程序:输出helloworld!
如今我们分别用模块编程输出helloworld!,和在用户层编程输出hellowrold!。通过这两个程序我们来剖析下怎样来编撰一个内核模块程序。
用户层编程:hello.c
#include
intmain(void)
printf("helloworld/n");
内核编程:module.c
#include
#include
#include
MODULE_LICENSE("DualBSD/GPL");
staticinthello_init(void)
printk(KERN_ALERT"hello,Iamedsionte/n");
return0;
staticvoidhello_exit(void)
printk(KERN_ALERT"goodbye,kernel/n");
module_init(hello_init);
module_exit(hello_exit);
//可选
MODULE_AUTHOR("Tiger-John");
MODULE_DESCRIPTION("Thisisasimpleexample!/n");
MODULE_ALIAS("Asimplestexample");
Tiger-John说明:
1.>相信只要是学过C语言的朋友对第一个程序都是没有问题的。并且或许你们看了第二个程序就有些不明白了。
可能有人会说:Tiger哥你没疯吧linux内核分析及编程,如何会把printf()那么简单的函数错写成了printk()呢。
也有的人忽然想起当初在学院学C编程时,老师告诉我们“一个C程序必需要有main()函数,而且系统会首先步入main()函数执行",这么你的程序如何没有main()函数呢?没有main()函数程序是怎样执行的呢?
可能也会有更仔细的人会发觉:如何两个程序头文件不一样呢?不是要用到输入和输出函数时,一定要用到这个头文件,你如何没有呢?
--------------------------------------------------------------------------------------------
Tiger哥很淡定的告诉你们虽然第二个程序是正确的,如今我们就来看见到底怎样来编撰一个内核模块程序。
2.内核模块编程的具体实现
第一步:首先我们来看一下程序的头文件
#include
#include
#include
这三个头文件是编撰内核模块程序所必须的3个头文件。
Tiger-John说明:
1>因为内核编程和用户层编程所用的库函数不一样,所以它的头文件也和我们在用户层编撰程序时所用的头文件也不一样。
2>我们在来瞧瞧在Linux中又是在那块储存它们的头文件
a.内核头文件的位置:/usr/src/linux-2.6.x/include/
b.用户层头文件的位置:/usr/include/
如今我们就明白了。虽然我们在编撰内核模块程序时所用的头文件和系统函数都和用层编程时所用的头文件和系统函数是不同的。
第二步:编撰内核模块时必需要有的两个函数:
1>加载函数:
staticintinit_fun(void)
//初始化代码
函数实例:
staticinthello_init(void)//不加void在调试时会出现报案
printk("helloworld!/n");
return0;
2>卸载函数无返回值
staticvoidcleaup_fun(void)
//释放代码
函数实例:
staticvoidhello_exit(void)//不加void会出现报案,若改为staticint也会报错,由于出口函数是不能返会值的
printk("bye,bye/n");
在模块编程中必需要有前面这两个函数;
Tiger-John补充:
注册函数和卸载函数还有另中学写法:
1>模块加载函数
staticint__initinit_fun(void)
//初始化代码
函数实例:
staticint__inithello_init(void)
printk("hellotiger/n");
return0;
2>卸载函数无返回值
staticvoid__exitcleaup_fun(void)
//释放代码
函数实例:
staticvoid__exitexit(void)
printk("byebye!/n");
Tiger-John补充:
通过比较我们可以发觉第一中函数的写法与第二中函数的写法主要不同就是加了__init和__exit前缀。(init和exit上面都是两个顿号)
这么第二种方式比第一种有哪些益处呢:
_init和__exit是Linux内核的一个宏定义,使系统在初始化完成后释放该函数linux命令chm,并释放其所占显存。因而它的优点是显而易见的。所以建议你们啊在编写入口函数和出口函数时采用第一中方式。
(1)在linux内核中,所有标识为__init的函数在联接的时侯都置于.init.text这个区段内,再者,所有的__init函数在区段.initcall.init中还保存了一份函数表针,在初始化时内核会通过这种函数表针调用那些__init函数,并在初始化完成后释放init区段(包括.init.text,.initcall.init等)。
(2)和__init一样,__exit也可以使对应函数在运行完成后手动回收显存。
3>如今我们来看一下printk()函数
a.里面早已说了,我们在内核编程时所用的库函数和在用户态下的是不一样的。printk是内核态信息复印函数,功能和比准C库的printf类似。printk还有信息复印级别。
b.如今我们来看一下printk()函数的原型:
intprintk(constchar*fmt,...)
消息复印级别:
fmt----消息级别:
#defineKERN_EMERG""/*紧急风波消息linux内核分析及编程,系统崩溃之前提示,表示系统不可用*/
#defineKERN_ALERT""/*报告消息,表示必须立刻采取举措*/
#defineKERN_CRIT""/*临界条件,一般涉及严重的硬件或软件操作失败*/
#defineKERN_ERR""/*错误条件,驱动程序常用KERN_ERR来报告硬件的错误*/
#defineKERN_WARNING""/*警告条件,对可能出现问题的情况进行警告*/
#defineKERN_NOTICE""/*正常但又重要的条件,用于提醒。常用于与安全相关的消息*/
#defineKERN_INFO""/*提示信息,如驱动程序启动时,复印硬件信息*/
#defineKERN_DEBUG""/*调试级别的消息*/
Tiger-John说明:
不同级别使用不同字符串表示,数字越小硬盘安装linux,级别越高。
c.为何内核态使用printk()函数,而在用户态使用printf()函数。
printk()函数是直接使用了向终端写函数tty_write()。而printf()函数是调用write()系统调用函数向标准输出设备写。所以在用户态(如进程0)不能否直接使用printk()函数,而在内核态因为它已是特权级,所以无需系统调用来改变特权级,因此还能直接使用printk()函数。printk是内核输出,在终端是看不见的。我们可以看一下系统日志。
并且我们可以使用命令:cat/var/log/messages,或则使用dmesg命令看一下输出的信息。
第三步:加载模块和卸载模块
1>module_init(hello_init)
a.告诉内核你编撰模块程序从哪里开始执行。
b.module_init()函数中的参数就是注册函数的函数名。
2>module_exit(hello_exit)
a.告诉内核你编撰模块程序从哪里离开。
b.module_exit()中的参数名就是卸载函数的函数名。
Tiger-John说明:
我们通常在注册函数里进行一些初始化诸如申请显存空间注册设备号等。这么我们就要在卸载函数进行释放我们所占有的资源。
(1)若模块加载函数注册了XXX,则模块卸载函数应当注销XXX
(2)若模块加载函数动态申请了显存,则模块卸载函数应当注销XXX
(3)若模块加载函数申请了硬件资源(中断,DMA通道)的占用,则模块卸载函数应当释放那些硬件资源。
(4)若模块加载函数开启了硬件,则卸载函数中通常要关掉硬件。