310 likes | 553 Views
Linux 模块编程. 主要内容. 1. 2. Linux 内核模块机制介绍. 内核编程. Linux 内核模块机制介绍. 1. 微内核与单内核 Linux 的模块机制. 先来区分进程的两种运行模式: 用户模式 (user mode) 运行在用户空间,实现用户的功能 内核模式 (kernel mode 运行在内核空间,一般是实现系统相关的功能。 进程在自己空间中实现用户功能时,如果想使用系统功能,必须将运行级别从用户态提升到核心态才能访问内核空间,实现相关操作。这个过程是通过系统调用实现的, 而用户态切换到核心态过程不可避免的存在一定开销 。.
E N D
主要内容 1 2 Linux内核模块机制介绍 内核编程
Linux内核模块机制介绍 1 微内核与单内核 Linux的模块机制
先来区分进程的两种运行模式: • 用户模式(user mode) 运行在用户空间,实现用户的功能 • 内核模式(kernel mode 运行在内核空间,一般是实现系统相关的功能。 进程在自己空间中实现用户功能时,如果想使用系统功能,必须将运行级别从用户态提升到核心态才能访问内核空间,实现相关操作。这个过程是通过系统调用实现的,而用户态切换到核心态过程不可避免的存在一定开销。
1.两种体系结构下的内核 • 微内核(Micro kernel) 最常用的功能模块被设计成内核模式运行的一个或一组进程,通常只包含进程调度、内存管理和进程间通信几个基本功能。其他功能都作为单独的进程在用户模式下运行,通过信号量、邮箱等信息传递方式进行通信。 • 灵活、 易于移植。 • 进程间通信开销大,速度相对较慢。 • 单内核(Monolithic kernel,有时也叫宏内核Macro kernel) 内核一般做为一个大进程存在,内部各功能模块可直接调用彼此的函数。 • 因为都在内核空间,没有切换开销,效率上有一定优势。 • 可扩展性和可维护性比较差。
Linux操作系统内核是单内核,速度和性能都很高,问题在于如何提高可扩展性和可维护性? Linux操作系统内核是单内核,速度和性能都很高,问题在于如何提高可扩展性和可维护性? 模块(Module)机制 用户可以根据需要,在不需要对内核重新编译的情况下,可以将模块动态地载入或移出内核。 2.Linux的模块机制
模块编程 2 模块的编写 模块的编译 模块的安装与使用
内核模式下编程的一些限制 : • 不能使用用户模式下的C标准库,因为内核模式下不存在libc库,也就没有这些用户函数供调用。 • 不能使用浮点运算,因为linux内核切换模式时不保存处理器的浮点状态。 • 不要让内核程序进行长时间等待,因为linux内核是非抢占的。 • 尽可能保持代码的清洁易懂,因为内核调试不方便,简洁的代码能减少并方便后期调试。
一个模块一般包含如下几部分 许可证声明 模块初始化和退出函数声明 初始化和退出函数 普通函数 模块导出符号表 其他操作 1.模块的编写
在C语言中,关键字static有三个明显的作用: • 在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。 • 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。 • 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。
关于模块导出符号表 只有模块中导出的内核函数才可以被其它模块调用。导出的内核符号表被看作是导出的内核接口,也可以看作内核API。 在内核中,导出内核函数使用的指令有:
关于模块的许可证 从Linux内核2.4.10开始,动态加载的模块必须通过MODULE_LICENSE宏声明此模块的许可证,否则动态加载时,会收到内核被污染“module license ‘unspecified’ taints kernel.”的警告。 被内核接受的许可证有很多,最常用的的是“GPL”和"Dual BSD/GPL"。 书写格式如下: • MODULE_LICENSE("GPL"); • MODULE_LICENSE("Dual BSD/GPL");
Linux操作系统内核模块也可带参数 定义一个模块参数 module_param(name, type, perm); name 参数名 type 参数类型 perm 指定模块在sysfs文件系统下的对应文件权限,可用八进制,或S_Ifoo形式,如S_IRUGO|S_IWUSR表示任何人可读,所有者可写。 如:static char *user_name=“username”; module_param(user_name,charp,S_IRUGO); 安装模块时使用参数 insmod module.ko user_name=book_user1 type参数: byte字节 short短整型 ushort无符号短整型 int整型 uint无符号整型 long长整型 ulong无符号长整型 charp字符指针 bool布尔类型 关于模块的参数
关于模块使用计数 内核需要记录加载到系统里的每一个模块的使用情况。 • 在Linux操作系统2.4内核中使用两个宏来完成对模块引用计数的操作: • 在Linux操作系统2.6内核中,使用下面的两个函数来完成对模块引用计数的操作:
内核编写实例 编写一个内核模块module,向外导出两个函数,分别是“求累积和”和“求阶乘”功能。 编写另两个内核模块module1、module2,分别使用上面module模块中的函数,实现计算。 • 注意路径清晰,分别在三个目录下编写
内核调试常用函数——printk printk是内核使用的函数,接口和printf()基本相似,可以在控制台显示多达1024个字符。 内核根据“记录级别”判断是否在终端打印消息。没有指定日志级别的printk语句默认采用的级别是 DEFAULT_ MESSAGE_LOGLEVEL(级别在4),看不到输出,因为正常输出的前提是──日志输出级别小于console_loglevel(在内核中数字越小优先级越高)。这些级别都是简单宏定义,可在include / linux / kernel.h中查看。
#include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("Dual BSD/GPL"); static int __init mod_init_modtest(void); static void __exit mod_exit_modtest(void); module_init(mod_init_modtest); module_exit(mod_exit_modtest); int sum_op(int numdata); int factorial_op(int N); //初始化与退出函数 int mod_init_modtest(void) { printk(KERN_INFO"--------Module_export_symbol init !---------\n"); return 0; } void mod_exit_modtest(void) { printk(KERN_INFO"-----Module_export_symbol was deleted!-----\n"); } //普通函数=== EXPORT_SYMBOL(sum_op); EXPORT_SYMBOL(factorial_op); MODULE_AUTHOR("book author"); MODULE_DESCRIPTION("module1:Module_export_symbol --sum_op--factorial_op--"); MODULE_VERSION("Ver 1.0"); int sum_op(int numdata) //用于计算比某一数字小的所有正整数的和, { char i = 0; char ret = 0; printk(KERN_INFO"sum operation\n"); while(i <= numdata) ret += i++; return ret; } int factorial_op(int N) //factorial_op( ) 用于计算某一数字的阶乘 { char i=1; int Nx=1; printk(KERN_INFO"factorial operation\n"); if( N == 0) return Nx; for(;i<=N;i++) Nx=Nx*i; return Nx; } module.c
#include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("Dual BSD/GPL"); static int __init mod_init_modtest1(void); static void __exit mod_exit_modtest1(void); module_init(mod_init_modtest1); module_exit(mod_exit_modtest1); static char *user_name = "book_user"; static int num_operator = 0; extern int sum_op(int); int mod_init_modtest1(void) { int result = 0; printk(KERN_INFO"Hello,I am module 1 !\n"); printk(KERN_INFO"%s,Welcome to use this sum_op!\n",user_name); result = sum_op(num_operator); printk(KERN_INFO"1 +..+ %d = %d\n",num_operator,result); return 0; } void mod_exit_modtest1(void) { printk(KERN_INFO"Module 1 : Goodbye %s\n",user_name); } module_param(user_name,charp,S_IRUGO); module_param(num_operator,int,S_IRUGO); MODULE_AUTHOR("book author"); MODULE_DESCRIPTION("Simple Module 1 ,used to sum_op"); MODULE_VERSION("Ver 1.0"); 声明为内核模块的参数 module1.c 安装模块时不提供参数时,缺省的参数值 改成KERN_ALERT级别试试。 module1用到了module中的导出符号,其makefile中会有一句: KBUILD_EXTRA_SYMBOLS=../module/Module.symvers
#include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("Dual BSD/GPL"); static int __init mod_init_modtest2(void); static void __exit mod_exit_modtest2(void); module_init(mod_init_modtest2); module_exit(mod_exit_modtest2); static char *user_name = "book_user"; static int num_operator = 0; extern int factorial_op(int); int mod_init_modtest2(void) { int result = 0; printk(KERN_INFO"Hello,I am module 2 !\n"); printk(KERN_INFO"%s,Welcome to use this factorial_op!\n",user_name); result = factorial_op(num_operator); printk(KERN_INFO" %d! = %d\n",num_operator,result); return 0; } void mod_exit_modtest2(void) { printk(KERN_INFO"Module 2 : Goodbye %s\n",user_name); } module_param(user_name,charp,S_IRUGO); module_param(num_operator,int,S_IRUGO); MODULE_AUTHOR("book author"); MODULE_DESCRIPTION("Simple Module 2 ,used to factorial_op"); MODULE_VERSION("Ver 1.0"); 与上一模块不同点只是调用的函数不一样 module2.c
分别执行make命令,编译module.c、module1.c、module2.c分别执行make命令,编译module.c、module1.c、module2.c
查看模块lsmod 查看安装是否成功,下图中可看到module1和module2的使用者数量为0,而module被module1和module2使用,因此其使用者数量为2。
查看模块输出信息 利用dmesg 命令获得一个内核日志的副本,查看系统信息:dmesg | tail -10
不同版本模块编译问题 问题:Linux kernel2.6.26版本及以后内核版本编译生成module1.ko时有警告信息,模块安装时会提示符号sum_op(定义在module中)对于模块module1来说不可见。 解决办法:把生成module.ko模块时生成的Module.symvers放到module1文件夹中。这样编译module1时,符号信息就会自动链接进去了。
2.模块的编译 经过编译、链接后生成的内核模块文件的后缀为.ko。 内核的makefile系统共有5种类型的文件,从linux2.6内核开始,模块的编译需要配置过的内核源码。
Linux内核Makefile分类 • Kernel Makefile Kernel Makefile位于Linux内核源代码的顶层目录,也叫 Top Makefile。它主要用于指定编译Linux Kernel目标文件(vmlinux)和模块(module)。编译内核或模块时,这个文件会被首先读取,并根据读到的内容配置编译环境变量。对于内核或驱动开发人员来说,这个文件几乎不用任何修改。 • ARCH Makefile ARCH Makefile位于ARCH/$(ARCH)/Makefile,是系统对应平台的Makefile。Kernel Top Makefile会包含这个文件来指定平台相关信息。只有平台开发人员会关心这个文件。 • Kbuild Makefile Kbuild系统使用Kbuild Makefile来编译内核或模块。当Kernel Makefile被解析完成后,Kbuild会读取相关的Kbuild Makefile进行内核或模块的编译。Kbuild Makefile有特定的语法指定哪些编译进内核中、哪些编译为模块、及对应的源文件是什么等。内核及驱动开发人员需要编写这个Kbuild Makefile文件。
Kbuild Makefile模板语法说明 内核源码顶层makefile中定义的变量 ifneq ($(KERNELRELEASE),) module-objs=module*.o obj-m += module1.o else PWD := $(shell pwd) KVER := $(shell uname -r) KDIR := /lib/modules/$(KVER)/build all: $(MAKE) -C $(KDIR) M=$(PWD) clean: rm -rf *.o *.mod.c *.ko *.symvers *.order *.markers endif • 模块名-objs=依赖关系 • obj-m += 模块编译链接生成的目标文件名。obj-m指明是动态编译 当前目录 顶层makefile所在目录 内核版本 • -C $(a) 指明跳转到a目录下读那里的makefile • M=$(b) 指明返回b目录下处理那里的makefile • 这样写主要是为了在make当前目录下的makefile前先读顶层makefile中的一些变量(如本例中使用的KERNELRELEASE)。
先读顶层makefile的举例: obj-$(CONFIG_EXT2_FS) += ext2.o ext2-y := balloc.o bitmap.o ext2-$(CONFIG_EXT2_FS_XATTR) += xattr.o 设模块名为ext2。将上述行写在ifneq下,意思是: • balloc.o和bitmap.o两个目标文件最终链接生成ext2.o; • ext2.ko是否包括xattr.o取决于内核配置文件中配置的CONFIG_EXT2_FS。 • 需要注意该kbuild Makefile所在的目录中不应该再包含和模块名相同的源文件如ext2.c/ext2.s。
3.模块的安装与使用 注意模块操作都在root权限下进行 1)模块安装(insmod) insmod 模块名.ko 注意有参数的模块别忘记安装时提供参数。 2)查看已安装模块(lsmod) 3)查看模块运行提示信息(dmesg) 4)模块退出(rmmod) rmmod 模块名 模块使用者计数为0时才能退出。
思考与练习 1.内核编程会受到哪些限制? 2.Linux内核编程的模块机制带来哪些好处? 3.分析模块编译的makefile文件,说明模块的编译过程。 4.编写三个模块文件mainmod.c、lenmod.c、summod.c,实现对某一数组的求和:在mainmod模块调用summod模块对数组进行求和运算,summod模块调用lenmod模块求数组中元素的个数。 5.说明查看系统信息的方法有哪几种?