HelloWorld内核
开始断断续续学习内核,大概半年了,多少开始对内核有点感悟了,但是对于这个庞然大物我显得很渺小,在枯燥的内核源码之中似乎没有一点点成功的喜悦,因此我选择学习内核模块编程,通过编写一些内核模块来体验那一点点小小的成就感吧!
什么是内核模块
内核模块是具有独立功能的程序。它可以被单独编译,但是不能单独运行,它的运行必须被链接到内核作为内核的一部分在内核空间中运行。
最简单的内核模块
#include //所有模块都必须包含的头文件#include //一些宏定义,例如这里的KERN_INFOint init_module(void) { printk(KERN_INFO "Hello world 1.\n"); /* * 返回非0表示模块初始化失败,无法载入 */ return 0; }void cleanup_module(void) { printk(KERN_INFO "Goodbye world 1.\n"); } //一个模块至少需要两个函数,一个初始化函数这里是init_module在载入内核的时候调用, //一个结束函数,这里是cleannup_module在从内核中注销的时候调用
一个Makefile来编译这个内核模块
obj-m += hello-1.oall: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
注意:本文所有环节都是基于Centos6.5下测试OK,你可能在有的书上看见Makefile是这样写的
make -C /usr/src/linux-headers-$(shell uname -r) M=$(PWD) modules 其实/lib/modules/$(shell uname -r)/build 这个路径就是上面路径的一个软链接 [root@localhost 2.6.32-431.el6.x86_64]# ls -al build lrwxrwxrwx. 1 root root 44 Mar 16 05:26 build -> /usr/src/kernels/2.6.32-504.12.2.el6.x86_64/
编写好makefile文件后,使用make进行编译,编译完就出现一个.ko的文件,这个就是内核模块,需要载入运行
载入内核模块进行运行
载入内核模块的方法有很多比如: modprobe 和 insmod前者会分析模块的依赖关系,并且会去指定路径查找内核模块载入,而后者需要指定内核模块的绝对路径进行载入并且不解决模块的依赖关系。这里我们使用insmod来载入内核模块,使用rmmod卸载内核模块 [root@localhost kernel_module]# insmod hello-1.ko使用dmes查看内核模块的输出Hello world 1.卸载内核模块 [root@localhost kernel_module]# rmmod hello-1 dmesg查看输出Goodbye world 1.
内核模块编程和应用程序编程的异同
内核模块编程是不能去使用标准库(比如malloc free等)和一些第三方的库
内核模块编程是没有内存保护的,如果内存访问错误,就会出现oops错误
内核模块编程是没有main函数的,只有一个初始化函数,和一个提出函数
内核模块编程需要使用内核提供的头文件和API
内核模块编程的标准输出是输出到文件,而不是输出到屏幕
内核模块编程的debug是不能使用gdb来进行调试的。
内核模块进阶
内核模块的编程不仅仅是上面的一个HelloWorld,内核模块编程还有一些更高级的写法,下面会一一介绍:
去掉init_module/cleanup_module
在上面的HelloWorld模块中,你会发现初始化函数和退出函数好像是固定的名称,那么有没有办法自己自定义名称呢其实是可以的,你可以自己自定义名称,然后进行注册即可(注册其实就是做了一个函数指针的赋值而已) 下面是自定义名称的写法:
//不需要固定内核模块的初始化函数的名字和结束的名字#include #include #include static int hello_2_init(void) { printk(KERN_INFO "Hello,world 2\n"); return 0; }static void hello_2_exit(void) { printk(KERN_INFO "Goodbye,world 2\n"); } //这两个函数来注册模块初始化和模块结束module_init(hello_2_init); module_exit(hello_2_exit);
__init/__initdata/__exit
在有的内核模块编程的书籍或者介绍内核模块编程的博客中,你或许会发现有这样的一些特殊关键字__init ,_initdata ,__exit等等,其实这些都是gcc的扩展属性:__init 宏最常用的地方是驱动模块初始化函数的定义处,其目的是将驱动模块的初始化函数放入名叫.init.text的输入段。当内核启动完毕后,这个段中的内存会被释放掉供其他使用。__initdata宏用于数据定义,目的是将数据放入名叫.init.data的输入段。其它几个宏也类似。
模块描述信息
可以使用modinfo去查看一个模块的模块信息,下面是自己编写的模块和系统自带的模块的两个模块信息的对比
[root@localhost kernel_module]# modinfo hello-1.kofilename: hello-1.kosrcversion: 0D3956C127A907CC9E7114Fdepends: vermagic: 2.6.32-504.12.2.el6.x86_64 SMP mod_unload modversions [root@localhost kernel_module]# modinfo/lib/modules/2.6.32-431.el6.x86_64/kernel/fs/ext4/ext4.ko filename: /lib/modules/2.6.32-431.el6.x86_64/kernel/fs/ext4/ext4.kolicense: GPLdescription: Fourth Extended Filesystemauthor: Remy Card, Stephen Tweedie, Andrew Morton, Andreas Dilger, Theodore Ts'o and others srcversion: 345EBDA2AEFF60FFED78864 depends: mbcache,jbd2 vermagic: 2.6.32-431.el6.x86_64 SMP mod_unload modversions
从上面的对比可知,自己编写的模块的模块信息很少,没有作者信息,没有许可证信息等等,其实这些都可以设置
#include #include #include #define DRIVER_AUTHOR "zyf"#define DRIVER_DESC "A sample driver"static int __init init_hello_4(void) { printk(KERN_INFO "Hello, world 4\n"); return 0; } static void __exit cleanup_hello_4(void) { printk(KERN_INFO "Goodbye, world 4\n"); } module_init(init_hello_4); module_exit(cleanup_hello_4); //模块的许可证 MODULE_LICENSE("GPL"); //模块的作者MODULE_AUTHOR(DRIVER_AUTHOR); //模块的描述MODULE_DESCRIPTION(DRIVER_DESC);
模块参数
在用户态编写程序的时候我们都应该清楚,是可以给程序传递参数的,那么同样内核模块同样也有这样的需求,下面的例子展示了如何去给内核模块传递参数:
#include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("ZYF");static short int myshort = 1;static int myint = 420;static long int mylong = 9999;static char *mystring = "blah";static int myintArray[2] = {-1,-1};static int arr_argc = 0;//需要使用module_param来对参数进行说明,指明这个参数的类型,权限等charp是字符指针//定义数组参数需要使用module_param_arraymodule_param(myshort,short,S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP); MODULE_PARM_DESC(myshort,"A short integer"); module_param(myint,int,S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); MODULE_PARM_DESC(myint,"A integer"); module_param(mylong,long,S_IRUSR); MODULE_PARM_DESC(mylong,"A long integer"); module_param(mystring,charp,0000); MODULE_PARM_DESC(mystring,"A character string"); module_param_array(myintArray,int,&arr_argc,0000); MODULE_PARM_DESC(myintArray,"An array of integer");static int __init hello_5_init(void) { int i; printk(KERN_INFO "Hello, world 5\n=============\n"); printk(KERN_INFO "myshort is a short integer: %hd\n", myshort); printk(KERN_INFO "myint is an integer: %d\n", myint); printk(KERN_INFO "mylong is a long integer: %ld\n", mylong); printk(KERN_INFO "mystring is a string: %s\n", mystring); for (i = 0; i < (sizeof myintArray / sizeof (int)); i++) { printk(KERN_INFO "myintArray[%d] = %d\n", i, myintArray[i]); } printk(KERN_INFO "got %d arguments for myintArray.\n", arr_argc); return 0; }static void __exit hello_5_exit(void) { printk(KERN_INFO "Goodbye,world 5\n"); } module_init(hello_5_init); module_exit(hello_5_exit);/* 载入模块的时候,如果不指定参数就是上面的默认值,如果要指定参数的话 可以像下面这样来指定参数。 insmod hello-5.ko mystring="superc" myint=444 */
模块文件分割
在用户态写程序的时候,你会将一个大的程序分割成好几个文件,这样程序脉络就显的很清晰。在这里我们将初始化函数和退出函数分开在两个文件中编写。
start.c中#include /* We're doing kernel work */#include /* Specifically, a module */int init_module(void) { printk(KERN_INFO "Hello, world - this is the kernel speaking\n"); return 0; } stop.c中#include /* We're doing kernel work */#include /* Specifically, a module */void cleanup_module() { printk(KERN_INFO "Short is the life of a kernel module\n"); } 那么Makefile编译的时候需要设置成这样: obj-m += startstop.o startstop-objs := start.o stop.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean