一、内核模块基础代码解析
一个内核模块代码错误仍然会导致的内核崩溃。
GPL协议:开源规定,使用内核一些函数需要
1、单内核的缺点
- 单内核扩展性差的缺点
- 减小内核镜像文件体积,一定程度上节省内存资源
- 提高开发效率
- 不能彻底解决稳定性低的缺点:内核模块代码出错可能会导致整个系统崩溃
内核模块的本质:一段隶属于内核的“动态”代码,与其它内核代码是同一个运行实体,共用同一套运行资源,只是存在形式上是独立的。
#include <linux/module.h> //包含内核编程最常用的函数声明,如printk
#include <linux/kernel.h> //包含模块编程相关的宏定义,如:MODULE_LICENSE
/*该函数在模块被插入进内核时调用,主要作用为新功能做好预备工作
被称为模块的入口函数
__init的作用 :
1. 一个宏,展开后为:__attribute__ ((__section__ (".init.text"))) 实际是gcc的一个特殊链接标记
2. 指示链接器将该函数放置在 .init.text区段
3. 在模块插入时方便内核从ko文件指定位置读取入口函数的指令到特定内存位置
*/
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;
}
/*该函数在模块从内核中被移除时调用,主要作用做些init函数的反操作
被称为模块的出口函数
__exit的作用:
1.一个宏,展开后为:__attribute__ ((__section__ (".exit.text"))) 实际也是gcc的一个特殊链接标记
2.指示链接器将该函数放置在 .exit.text区段
3.在模块插入时方便内核从ko文件指定位置读取出口函数的指令到另一个特定内存位置
*/
void __exit myhello_exit(void)
{
printk("myhello will exit\n");
}
/*
MODULE_LICENSE(字符串常量);
字符串常量内容为源码的许可证协议 可以是"GPL" "GPL v2" "GPL and additional rights" "Dual BSD/GPL" "Dual MIT/GPL" "Dual MPL/GPL"等, "GPL"最常用
其本质也是一个宏,宏体也是一个特殊链接标记,指示链接器在ko文件指定位置说明本模块源码遵循的许可证
在模块插入到内核时,内核会检查新模块的许可证是不是也遵循GPL协议,如果发现不遵循GPL,则在插入模块时打印抱怨信息:
myhello:module license 'unspecified' taints kernel
Disabling lock debugging due to kernel taint
也会导致新模块没法使用一些内核其它模块提供的高级功能
*/
MODULE_LICENSE("GPL");
/*
module_init 宏
1. 用法:module_init(模块入口函数名)
2. 动态加载模块,对应函数被调用
3. 静态加载模块,内核启动过程中对应函数被调用
4. 对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.initcall段),方便系统初始化统一调用。
5. 对于动态加载的模块,由于内核模块的默认入口函数名是init_module,用该宏可以给对应模块入口函数起别名
*/
module_init(myhello_init);
/*
module_exit宏
1.用法:module_exit(模块出口函数名)
2.动态加载的模块在卸载时,对应函数被调用
3.静态加载的模块可以认为在系统退出时,对应函数被调用,实际上对应函数被忽略
4.对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.exitcall段),方便系统必要时统一调用,实际上该宏在静态加载时没有意义,因为静态编译的驱动无法卸载。
5.对于动态加载的模块,由于内核模块的默认出口函数名是cleanup_module,用该宏可以给对应模块出口函数起别名
*/
module_exit(myhello_exit);
myhello.c:内核模块函数代码
(二)内核模块的三要素
模块三要素:入口函数 出口函数 MODULE__LICENSE
二、内核模块多源文件
如果一个内核模块太大,多个文件编译成一个.ko文件
ifeq ($(KERNELRELEASE),)
ifeq ($(ARCH),arm)
KERNELDIR ?= 目标板linux内核源码顶层目录的绝对路径
ROOTFS ?= 目标板根文件系统顶层目录的绝对路径
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) INSTALL_MOD_PATH=$(ROOTFS) modules_install
clean:
rm -rf *.o *.ko .*.cmd *.mod.* modules.order Module.symvers .tmp_versions
else
obj-m += hello.o
hello-objs = f1.o f2.o ... ... ... ... ...
endif
Makefile中:
obj-m用来指定模块名,注意模块名加.o而不是.ko
可以用 模块名-objs 变量来指定编译到ko中的所有.o文件名(每个同名的.c文件对应的.o目标文件)
一个目录下的Makefile可以编译多个模块:
添加:obj-m += 下一个模块名.o
makefile:
三、内核模块信息宏
MODULE_AUTHOR(字符串常量); //字符串常量内容为模块作者说明
MODULE_DESCRIPTION(字符串常量); //字符串常量内容为模块功能说明
MODULE_ALIAS(字符串常量); //字符串常量内容为模块别名
四、模块传参
perm权限:0664
6:用户可读可写
6:主用户可读可写
4:其他用户可读
可执行一般不用
module_param(name,type,perm);//将指定的全局变量设置成模块参数
/*
name:全局变量名
type:
使用符号 实际类型 传参方式
bool bool insmod xxx.ko 变量名=0 或 1
invbool bool insmod xxx.ko 变量名=0 或 1
charp char * insmod xxx.ko 变量名="字符串内容"
short short insmod xxx.ko 变量名=数值
int int insmod xxx.ko 变量名=数值
long long insmod xxx.ko 变量名=数值
ushort unsigned short insmod xxx.ko 变量名=数值
uint unsigned int insmod xxx.ko 变量名=数值
ulong unsigned long insmod xxx.ko 变量名=数值
perm:给对应文件 /sys/module/name/parameters/变量名 指定操作权限
#define S_IRWXU 00700
#define S_IRUSR 00400
#define S_IWUSR 00200
#define S_IXUSR 00100
#define S_IRWXG 00070
#define S_IRGRP 00040
#define S_IWGRP 00020
#define S_IXGRP 00010
#define S_IRWXO 00007
#define S_IROTH 00004
#define S_IWOTH 00002 //不要用 编译出错
#define S_IXOTH 00001
*/
module_param_array(name,type,&num,perm);
/*
name、type、perm同module_param,type指数组中元素的类型
&num:存放数组大小变量的地址,可以填NULL(确保传参个数不越界)
传参方式 insmod xxx.ko 数组名=元素值0,元素值1,...元素值num-1
*/
可用MODULE_PARAM_DESC宏对每个参数进行作用描述,用法:
MODULE_PARM_DESC(变量名,字符串常量);
字符串常量的内容用来描述对应参数的作用
modinfo可查看这些参数的描述信息
五、模块依赖
一个模块用到另外一个模块的参数
B模块依赖于A模块,B模块要extern变量,A模块内也需要用宏声明可以被外部调用的全局变量。
编译顺序一定要先A后B,插入顺序也要先A后B。卸载顺序先B后A。
modulea.c:
moduleb:
既然内核模块的代码与其它内核代码共用统一的运行环境,也就是说模块只是存在形式上独立,运行上其实和内核其它源码是一个整体,它们隶属于同一个程序,因此一个模块或内核其它部分源码应该可以使用另一个模块的一些全局特性。
一个模块中这些可以被其它地方使用的名称被称为导出符号,所有导出符号被填在同一个表中这个表被称为符号表。
最常用的可导出全局特性为全局变量和函数
查看符号表的命令:nm nm查看elf格式的可执行文件或目标文件中包含的符号表,用法:
nm 文件名
(可以通过man nm查看一些字母含义)
两个用于导出模块中符号名称的宏:
EXPORT_SYMBOL(函数名或全局变量名) EXPORT_SYMBOL_GPL(函数名或全局变量名) 需要GPL许可证协议验证
使用导出符号的地方,需要对这些符号进行extern声明后才能使用这些符号
B模块使用了A模块导出的符号,此时称B模块依赖于A模块,则:
- 编译次序:先编译模块A,再编译模块B,当两个模块源码在不同目录时,需要:i. 先编译导出符号的模块A ii. 拷贝A模块目录中的Module.symvers到B模块目录 iii. 编译使用符号的模块B。否则编译B模块时有符号未定义错误
- 加载次序:先插入A模块,再插入B模块,否则B模块插入失败
- 卸载次序:先卸载B模块,在卸载A模块,否则A模块卸载失败
补充说明:
乌班图内核符号表:
运行前: /proc/kallsyms运行时 /boot/System.map编译后
运行后:
开发板:
六、内核空间和用户空间
为了彻底解决一个应用程序出错不影响系统和其它app的运行,操作系统给每个app一个独立的假想的地址空间,这个假想的地址空间被称为虚拟地址空间(也叫逻辑地址),操作系统也占用其中固定的一部分,32位Linux的虚拟地址空间大小为4G,并将其划分两部分:
-
0~3G 用户空间 :每个应用程序只能使用自己的这份虚拟地址空间
-
3G~4G 内核空间:内核使用的虚拟地址空间,应用程序不能直接使用这份地址空间,但可以通过一些系统调用函数与其中的某些空间进行数据通信
七、执行流
内核态使用内核空间,用户态使用用户空间。
执行流:有开始有结束总体顺序执行的一段独立代码,又被称为代码上下文
计算机系统中的执行流的分类:
执行流:
- 任务流--任务上下文(都参与CPU时间片轮转,都有任务五状态:就绪态 运行态 睡眠态 僵死态 暂停态)
- 进程
- 线程
- 内核线程:内核创建的线程
- 应用线程:应用进程创建的线程
- 异常流--异常上下文
- 中断
- 其它异常
应用编程可能涉及到的执行流:
- 进程
- 线程
内核编程可能涉及到的执行流:
- 应用程序自身代码运行在用户空间,处于用户态 ----------------- 用户态app
- 应用程序正在调用系统调用函数,运行在内核空间,处于内核态,即代码是内核代码但处于应用执行流(即属于一个应用进程或应用线程) ---- 内核态app
- 一直运行于内核空间,处于内核态,属于内核内的任务上下文 --------- 内核线程
- 一直运行于内核空间,处于内核态,专门用来处理各种异常 --------- 异常上下文
八、模块编程和应用编程的比较
1、API:内核不能使用任何库函数,应用程序可以使用库函数。
2、并发考虑:内核考虑多种执行流并发,手段丰富,应用层需要考虑多任务,异常上下文。
3、程序出错
九、内核接口头文件查询
大部分API函数包含的头文件在include/linux目录下,因此:
- 首先在include/linux 查询指定函数:grep 名称 ./ -r -n
- 找不到则更大范围的include目录下查询,命令同上
定位函数/结构体对应的头文件: