Linux内核模块基础代码架构解析
在进行Linux驱动开发时我们常常需要将一个设备驱动编译成内核模块的形式在内核中加载运行。内核模块就相当于是内核本身的插件。Linux提供了一种可以向正在运行的内核中插入新的代码段以及在代码段不需要继续运行时也可以从内核中移除的机制,这个可以被加载、移除的代码段就是内核模块。 使用内核模块的优缺点如下: 1、可以避免单内核扩展性差的缺点。 2、可以减小内核镜像文件体积,一定程度上节省内存资源。 3、可以提高开发效率。 4、不能彻底解决单内核稳定性低的缺点:内核模块代码出错可能会导致整个系统崩溃 内核模块的本质:一段隶属于内核的“动态”代码,与其它内核代码是同一个运行实体,共用同一套运行资源,只是存在形式上是独立的。 以上简要介绍了内核模块及其优缺点,接着介绍下Linux内核模块的基础代码架构。
内核模块的基础代码架构如下:
#include <linux/module.h>
#include <linux/kernel.h>
int __init myhello_init(void)
{
/*内核是裸机程序,不可以调用C库中printf函数来打印程序信息,
Linux内核源码自身实现了一个用法与printf差不多的函数,命名为printk (k-kernel)
printk不支持浮点数打印*/
printk("#####################################################\n");
printk("#####################################################\n");
printk("#####################################################\n");
printk("#####################################################\n");
printk("myhello is running\n");
printk("#####################################################\n");
printk("#####################################################\n");
printk("#####################################################\n");
printk("#####################################################\n");
return 0;
}
void __exit myhello_exit(void)
{
printk("myhello will exit\n");
}
module_init(myhello_init);
module_exit(myhello_exit);
MODULE_LICENSE("GPL");
说明: #include <linux/module.h> #include <linux/kernel.h> 头文件包含、包含模块编程相关的宏定义,如:MODULE_LICENSE、内核编程最常用的函数声明,如printk等。
int __init myhello_init(void) 该函数在模块被插入进内核时调用,主要作用为新功能做好预备工作,被称为模块的入口函数。 __init的作用: 1. 一个宏,展开后为:__attribute__ ((__section__ (".init.text"))) 实际是gcc的一个特殊链接标记。 2. 指示链接器将该函数放置在 .init.text区段。 3. 在模块插入时方便内核从ko文件指定位置读取入口函数的指令到特定内存位置。
void __exit myhello_exit(void) 该函数在模块从内核中被移除时调用,主要作用做些init函数的反操作,被称为模块的出口函数。 __exit的作用: 1.一个宏,展开后为:__attribute__ ((__section__ (".exit.text"))) 实际也是gcc的一个特殊链接标记。 2.指示链接器将该函数放置在 .exit.text区段。 3.在模块插入时方便内核从ko文件指定位置读取出口函数的指令到另一个特定内存位置。
module_init(myhello_init); module_init是一个宏定义。 1. 用法:module_init(模块入口函数名) 2. 动态加载模块,对应函数被调用。 3. 静态加载模块,内核启动过程中对应函数被调用。 4. 对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.initcall段),方便系统初始化统一调用。 5. 对于动态加载的模块,由于内核模块的默认入口函数名是init_module,用该宏可以给对应模块入口函数起别名。
module_exit(myhello_exit); module_exit宏 1.用法:module_exit(模块出口函数名) 2.动态加载的模块在卸载时,对应函数被调用。 3.静态加载的模块可以认为在系统退出时,对应函数被调用,实际上对应函数被忽略。 4.对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.exitcall段),方便系统必要时统一调用,实际上该宏在静态加载时没有意义,因为静态编译的驱动无法卸载。 5.对于动态加载的模块,由于内核模块的默认出口函数名是cleanup_module,用该宏可以给对应模块出口函数起别名。
MODULE_LICENSE("GPL"); MODULE_LICENSE(字符串常量); 字符串常量内容为源码的许可证协议,可以是"GPL"、"GPL v2"、"GPL and additional rights"、"Dual BSD/GPL"、"Dual MIT/GPL"、"Dual MPL/GPL"等, 其中"GPL"最为常用。 MODULE_LICENSE("GPL")其本质也是一个宏,宏体也是一个特殊链接标记,指示链接器在ko文件指定位置说明本模块源码遵循的许可证。 在模块插入到内核时,内核会检查新模块的许可证是不是也遵循GPL协议,如果发现不遵循GPL,则在插入模块时打印抱怨信息: myhello:module license 'unspecified' taints kernel Disabling lock debugging due to kernel taint 也会导致新模块没法使用一些内核其它模块提供的高级功能。
除了MODULE_LICENSE("GPL")之外,常用的内核模块信息宏还有:
MODULE_AUTHOR(字符串常量); //字符串常量内容为模块作者说明
MODULE_DESCRIPTION(字符串常量); //字符串常量内容为模块功能说明
MODULE_ALIAS(字符串常量); //字符串常量内容为模块别名
这些宏用来描述一些当前模块的一些信息。 以上这些宏的本质是定义static字符数组用于存放指定字符串内容,这些字符串内容链接时存放在.modinfo字段,可以用modinfo命令来查看这些模块信息,用法: modinfo 模块文件名