linux设备驱动归纳总结(二):模块的相关基础概念 系统平台:Ubuntu 10.04 开发平台:S3C2440开发板 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 一。初探linux内核模块 内核模块:内核本身是很庞大的一个结构,需要的组件很多。编译内核时,用户可以把所有的代码编译进内核,但是这样会引起两个问题:一是内核过大;二是当需要添加或者删除内核时,需要重新再编译内核。所以有了内核模块的概念。模块并不编译到内核中,编译后存放在指定的目录,当需要使用时动态加载。 1.1下面是一个非常经典的hello world代码: 目录:1st /*2nd_module/1st*/ 1 #include <linux/module.h> //包含了很多装载模块需要的符号和函数的定义 2 #include <linux/init.h> //用于指定初始化函数和清除函数 3 4 static int __init test_init(void) //内核初始化函数 5 { 6 printk("hello world!\n"); //打印函数,和prinft类似 7 return 0; 8 } 9 10 static void __exit test_exit(void)//内核清除函数 11 { 12 printk("good bye!\n"); 13 } 14 15 module_init(test_init); //指定初始化函数 16 module_exit(test_exit); //指定清除函数 17 18 MODULE_LICENSE("GPL"); //指定代码使用的许可证 19 MODULE_AUTHOR("xiao bai"); //指定作者 20 MODULE_VERSION("1.0"); //指定代码修订号 1.2再来一个Makefile: (注:如果不知道“make -C $(KDIR) M=`pwd` modules ”语句的意思,可以查看linux内核驱动归纳总结(一):内核的相关基础概念的第六小节) obj-m += test.o KDIR:=/root/Desktop/drives/nfsroot-29/linux-2.6.29 all: make -C $(KDIR) M=`pwd` modules clean: make -C $(KDIR) M=`pwd` modules clean rm -f modules.order 1.3编写完毕后在代码目录下执行“make”命令,就会产生test.ko文件,在开发板上通过命令“insmod test.ko”,插入模块,通过命令“lsmod”查看当前的所有装载上的模块,通过命令“rmmod test”卸载该模块。并且,加载时会输出 “hello world!”,卸载时会输出“good bye!”。 [root: 1st]# rmmod test good bye! [root: 1st]# insmod test.ko hello world! [root: 1st]# lsmod test 1060 0 - Live 0xbf00c000 [root: 1st]# rmmod test good bye! [root: 1st]# 1.4上面的程序包含了三个知识点: 1.4.1内核初始化函数: static int __init test_init(void) //内核初始化函数 { } module_init(test_init); //指定初始化函数 1)初始化函数是在模块加载时自动被调用,执行相关的初始化工作。 2)static和__init都是可以不加的,因为初始化函数除了加载时执行外没有别的用处,加上static只是声明一下,该函数只能在模块内部使用。而加上__init后,它暗示内核该函数仅在初始化时使用,所以在模块被装载后,模块装载器就会把该函数扔掉,释放占用的内存空间。 3)但是moudle_init()是必须要的,因为这样才能让模块加载器知道这是个初始化函数,没有这一步,函数就不会得到调用。 4)初始化函数成功返回0,失败返回对应的错误码。 1.4.2内核清除函数: static void __exit test_exit(void)//内核清除函数 { } module_exit(test_exit); //指定清除函数 1)内核清除函数是在模块卸载是自动被调用,执行相关的清除工作。 2)同上,static和__exit都是可以不加的,但如果加上__exit,模块直接编译进内核或者不允许卸载,被标志为__exit的函数会被自动丢弃掉。 3)module_exit是必须的,因为这样内核才能找到清除函数。 4)清除函数的没有返回值。 5)一个没有定义清除函数的模块,是不允许被加载的。 1.4.3模块的描述性定义: MODULE_LICENSE("GPL"); //指定代码使用的许可证 MODULE_AUTHOR("xiao bai"); //指定作者 MODULE_VERSION("1.0"); //指定代码修订号 1)以上的都是一些都该模块的描述,除了上面的还有MODULE_ALIAS(模块的别名) MODULE_DESCRIPTION(描述用途)等。 2)MODULE_LICENSE一般都是要写的,告诉内核该程序使用的许可证,不然在加载时它会提示该模块污染内核。 3)MODULE_声明可以声明在源代码任意位置,但习惯放在代码的最后。 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 二。内核中的printk printk与printf的用法是差不多的,最大的区别就是printk可以指定打印的优先级。另外一个区别就是,printf只用在用户态,printk用于内核态。 下面由程序讲解 目录:2nd /*2nd_module/2nd*/ 1 #include <linux/module.h> 2 #include <linux/init.h> 3 4 static int __init test_init(void) 5 { 6 printk("hello world!\n"); 7 printk("<0>" "hello world! 0\n"); 8 printk("<1>" "hello world! 1\n"); 9 printk("<2>" "hello world! 2\n"); 10 printk("<3>" "hello world! 3\n"); 11 printk("<4>" "hello world! 4\n"); 12 printk("<5>" "hello world! 5\n"); 13 printk("<6>" "hello world! 6\n"); 14 printk("<7>" "hello world! 7\n"); 15 return 0; 16 } 17 18 static void __exit test_exit(void) 19 { 20 printk("good bye!\n"); 21 } 22 23 module_init(test_init); 24 module_exit(test_exit); 25 26 MODULE_LICENSE("GPL"); 27 MODULE_AUTHOR("xiao bai"); 28 MODULE_VERSION("1.0"); 编译后加载模块,发现输出内容为: [root: 2nd]# insmod test.ko hello world! hello world! 0 hello world! 1 hello world! 2 hello world! 3 hello world! 4 hello world! 5 hello world! 6 输出唯独缺少了最后一个"hello world! 7",这是因为printk输出优先级的导致的。printk的优先级如下,在内核目录下inxlude/linux/kernel.h下有记录: 91 #define KERN_EMERG "<0>" /* system is unusable */ 92 #define KERN_ALERT "<1>" /* action must be taken immediately */ 93 #define KERN_CRIT "<2>" /* critical conditions */ 94 #define KERN_ERR "<3>" /* error conditions */ 95 #define KERN_WARNING "<4>" /* warning conditions */ 96 #define KERN_NOTICE "<5>" /* normal but significant condition */ 97 #define KERN_INFO "<6>" /* informational */ 98 #define KERN_DEBUG "<7>" /* debug-level messages */ 其中<0>的优先级最高,<7>优先级最低。上面的printk语句的优先级都可以用字符串代替,如下面两句是同等作用的: p { margin-bottom: 0.21cm; }printk("<3>" "hello world! 3\n"); printk(KERN_ERR "hello world! 3\n") 如果调用printk使用的优先级低于或等于控制台的默认优先级,就不能被输出到控制台终端上显示,所以在minicom界面中看不到最后一句的输出。 按照以上的推测,可以得到两个结论: 一、如果不指定prinfk的优先级,prinfk的默认优先级比控制台的优先级高,所以才能显示在控制台上。 二、控制台的优先级是6,因为低于6优先级的语句不能打印出来。 printk的默认优先级在内核目录kernel/printk.c定义: 47 /* printk's without a loglevel use this.. */ 48 #define DEFAULT_MESSAGE_LOGLEVEL 4 /* KERN_WARNING */ 49 50 /* We show everything that is MORE important than this.. */ 51 #define MINIMUM_CONSOLE_LOGLEVEL 1 /* Minimum loglevel we let people use */ 52 #define DEFAULT_CONSOLE_LOGLEVEL 7 /* anything MORE serious than KERN_DEBUG*/ 文件中定义了printk的默认输出优先级为4,并定义了一般使用的最大和最小优先级1和7。 而终端控制台的输出优先级配置在文件/proc/sys/kernel/printk中: [root: /]# cat /proc/sys/kernel/printk 7 4 1 7 7 4 1 7分别是: 7:console_loglevel //这个就是控制台的默认优先级 4:default_message_loglevel // 这个是printk的默认输出优先级 1:minimum_console_level 7:default_console_loglevel 可以通过修改该文件使所有优先级的消息都显示出来。 [root: /]# echo 8 > /proc/sys/kernel/printk 注意的是,即使没有显示在控制台的内核消息,也会追加到/var/log/messages,通过查看/var/log/messages就能看到。 小技巧:可以通过printk的优先级定义是否输出调试信息:目录:3rd 1 #include <linux/module.h> 2 #include <linux/init.h> 3 4 #define DEBUG_SWITCH 0 5 #if DEBUG_SWITCH 6 #define P_DEBUG(fmt, args...) printk("<1>" "<kernel>[%s]"fmt, __FUNCTION__, ##args) 7 #else 8 #define P_DEBUG(fmt, args...) printk("<7>" "<kernel>[%s]"fmt, __FUNCTION__, ##args) 9 #endif 10 11 12 static int __init test_init(void) 13 { 14 printk("hello world!\n"); 15 P_DEBUG("debug!\n"); 16 return 0; 17 } 18 19 static void __exit test_exit(void) 20 { 21 printk("good bye!\n"); 22 } 23 24 module_init(test_init); 25 module_exit(test_exit); 26 27 MODULE_LICENSE("GPL"); 28 MODULE_AUTHOR("xiao bai"); 29 MODULE_VERSION("1.0"); 当#define DEBUG_SWITCH 0时,P_DEBUG语句并不输出到控制台。 相反,当#define DEBUG_SWITCH 1时,P_DEBUG语句输出到控制台。 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 三。内核传参————module_param 在用户态的C语言中,函数的传参使用main(int argc, char* argv),内核的传参使用了另外一种方法: 步骤一、在内核函数中用module_param指定模块参数。 步骤二、加载内核时传递参数给模块。 module_param函数使用方法: module_param(name, type, perm) name:内核参数的名称,自己定义; type:内核参数的类型,常见的类型byte、short、int、long、ulong、bool 、charp(字符指针); perm:内核参数的权限S_IRUGO(对模块参数具有读权限)。其实权限和文件的权限差不多,具体可以查看"include/linux/stat.h"。 所以,要定义一个int模块参数,权限是0644,在函数中需要定义: int a=0; module_paaram(a, int, 0644); 内核加载模块时传递参数的方法: 使用命令:insmod xxx.ko a=1 如果加载模块时不指定参数,模块会使用默认值0,否则会使用1。 模块加载后,并且他的权限不为0,就可以在/sys/module/xxx/parameter目录下找到对应的模块参数。 模块参数使用例子:目录 4th /*2nd_module/4th*/ 1 #include <linux/module.h> 2 #include <linux/init.h> 3 4 int num = 123; 5 char *name = "xiao bai"; 6 7 static int __init test_init(void) 8 { 9 printk("hello world!\n"); 10 printk("num = %d, name:[%s]\n", num, name); 11 return 0; 12 } 13 14 static void __exit test_exit(void) 15 { 16 printk("good bye!\n"); 17 } 18 19 module_init(test_init); 20 module_exit(test_exit); 21 module_param(num, int, 0644); 22 module_param(name, charp, 0644); 23 24 MODULE_LICENSE("GPL"); 25 MODULE_AUTHOR("xiao bai"); 26 MODULE_VERSION("1.0"); 几种指定模块参数时的效果: [root: 4th]# insmod test.ko hello world! num = 123, name:[xiao bai] [root: 4th]# rmmod test good bye! [root: 4th]# insmod test.ko num=321 name='haha' hello world! num = 321, name:[haha] [root: 4th]# rmmod test good bye! [root: 4th]# insmod test.ko name='haha' hello world! num = 123, name:[haha] 查看/sys/module/test/parameter目录,出现了模块参数: [root: 4th]# ls /sys/module/test/parameters/ name num xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 四、内核函数间的调用 内核函数间的调用有两种方法: 1)、把需要调用的函数编译进模块,与C语言的静态库类似。 2)、把许压迫被调用的函数导出到符号表,方便模块使用,与C语言的动态库类似 先说第一个方法:多文件编译目录 5th 1)编写被调用的函数文件haha.c 1 #include <linux/module.h> 2 #include "haha.h" 3 4 void haha(void) 5 { 6 printk("haha!\n"); 7 } 2)编写头文件haha.h 1 #ifndef __HAHA_H__ 2 #define __HAHA_H__ 3 4 void haha(void); 5 6 #endif 3)编写模块文件test.c 1 #include <linux/module.h> 2 #include <linux/init.h> 3 #include "haha.h" 4 5 static int __init test_init(void) 6 { 7 printk("hello world!\n"); 8 haha(); 9 return 0; 10 } 11 12 static void __exit test_exit(void) 13 { 14 printk("good bye!\n"); 15 } 16 17 module_init(test_init); 18 module_exit(test_exit); 19 20 MODULE_LICENSE("GPL"); 21 MODULE_AUTHOR("xiao bai"); 22 MODULE_VERSION("1.0"); 4)编写Makefile,与之前的Makefile不一样,因为涉及到多文件编译 1 obj-m += test_haha.o //生成test_haha.ko 2 test_haha-objs += haha.o test.o //test_haha.ko有haha.o和test.o组成 3 4 KDIR:=/root/Desktop/drives/nfsroot-29/linux-2.6.29 5 all: 6 make -C $(KDIR) M=`pwd` modules 7 clean: 8 make -C $(KDIR) M=`pwd` modules clean 9 rm -f modules.order 编译完毕后生成test_haha,ko,看一下效果 [root: 5th]# insmod test_haha.ko hello world! haha! 值得指出的是,上面的方法一般不用。应该使用下面的方法。 第二种方法:导出符号表EXPORT_SYMBOL :目录 6th 导出符号表是指,在定义一个函数后,在模块中使用语句"EXPORT_SYMBOL(xxxxx)" 将函数导出。通过这样,内核就知道了该函数和在内存中对应的地址,这样模块就可以调用导出的函数了。在"/proc/kallsyms"文件中对应这符号表,它记录了函数的符号和函数在内存所在的地址。 看看方法: 1)在6th/core目录下编写被调用的函数的文件haha.c 1 #include <linux/module.h> 2 #include <linux/init.h> 3 #include "../include/haha.h" 4 5 int haha(void) 6 { 7 printk("haha!\n"); 8 return 0; 9 } 10 11 EXPORT_SYMBOL(haha); //导出函数 12 MODULE_LICENSE("GPL"); 2)在6th/core目录下编写Makefile,用于编译haha.c成haha.ko 1 obj-m += haha.o 2 3 KDIR:=/root/Desktop/drives/nfsroot-29/linux-2.6.29 4 all: 5 make -C $(KDIR) M=`pwd` modules 6 clean: 7 make -C $(KDIR) M=`pwd` modules clean 8 rm -f modules.order 3)在6th/include目录下编写头文件haha.h 1 #ifndef __HAHA_H__ 2 #define __HAHA_H__ 3 4 int haha(void); 5 6 #endif 4)在6th/driver目录下编写文件test.c 1 #include <linux/module.h> 2 #include <linux/init.h> 3 #include "../include/haha.h" 4 5 static int __init test_init(void) 6 { 7 printk("hello world!\n"); 8 haha(); 9 return 0; 10 } 11 12 static void __exit test_exit(void) 13 { 14 printk("good bye!\n"); 15 } 16 17 module_init(test_init); 18 module_exit(test_exit); 19 20 MODULE_LICENSE("GPL"); 21 MODULE_AUTHOR("xiao bai"); 22 MODULE_VERSION("1.0"); 5)在6th/driver目录下编写Makefile,用于编译test.c 1 obj-m += test.o 2 3 KDIR:=/root/Desktop/drives/nfsroot-29/linux-2.6.29 4 all: 5 make -C $(KDIR) M=`pwd` modules 6 clean: 7 make -C $(KDIR) M=`pwd` modules clean 8 rm -f modules.order 最终会生成两个文件,一个是core目录下的haha.ko,一个是driver目录下的 test.ko。这两个文件的加载也要讲究顺序。 如果先加载test.ko的话会出错: [root: driver]# insmod test.ko test: Unknown symbol haha insmod: cannot insert 'test.ko': unknown symbol in module or invalid parameter 这是因为内核不能找到函数haha: [root: core]# cat /proc/kallsyms | grep haha //没有显示任何东西 所以需要先加载模块haha.ko,这样就可以在/proc/kallsyms中找到 [root: /]# cd review_driver/2nd_module/6th/core/ [root: core]# insmod haha.ko [root: core]# cat /proc/kallsyms | grep haha 00000000 a haha.c [haha] bf000000 t $a [haha] bf00001c t $d [haha] bf000044 r __kstrtab_haha [haha] c4827080 ? __mod_license12 [haha] bf000054 r __ksymtab_haha [haha] bf000054 r $d [haha] 00000000 a haha.mod.c [haha] c482708c ? __module_depends [haha] c4827098 ? __mod_vermagic5 [haha] bf000280 d __this_module [haha] bf000000 T haha [haha] c0225c10 u printk [haha] [root: core]# 到出符号表之后就可以加载模块test.ko了 [root: core]# cd ../driver/ [root: driver]# insmod test.ko hello world! haha! 随便提一下两个小问题: 1)、函数是必须有"#include <linux/module.h>" 原因一:printk的调用需要 原因二:如果不加上面的命令就不会显示"bf000000 T haha [haha]", 而显示"bf000000 t haha [haha]",小写t表示该符号未定义。 2)、模块的引用技术: [root: 6th]# insmod core/haha.ko [root: 6th]# lsmod haha 904 0 - Live 0xbf000000 //0 无引用 [root: 6th]# insmod driver/test.ko hello world! haha! [root: 6th]# lsmod test 1084 0 - Live 0xbf003000 haha 904 1 test, Live 0xbf000000 //被引用一次,test模块引用 可以看到,加载了test.ko时,haha模块产生了变化,如果卸载的时候先卸载haha 是不可以的,因为它还被人引用,必须先卸载test。 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 源代码: 2nd_module.rar |
|
来自: mandrave > 《linux设备驱动归纳总结》