Linux 驱动开发之内核模块开发
一、内核模块的概念
1、什么是模块? 内核模块是一些可以让操作系统内核在需要时载入和执行的代码,同时在不需要的时候可以卸载。这是一个好的功能,扩展了操作系统的内核功能,却不需要重新启动系统,是一种动态加载的技术。 特点:动态加载,随时载入,随时卸载,扩展功能
2、内核模块的加载作用 内核模块只是向linux内核预先注册自己,以便于将来的请求使用;由目标代码组成,没有形成完整的可执行程序。只是告诉内核,它有了新增的功能,而并不马上使用(执行),只是等待应用程序的调用;而应用程序在加载后就开始执行。
3、内核模块所用函数 内核模块代码编写没有外部的函数库可以用,只能使用内核导出的函数。而应用程序习惯于使用外部的库函数,在编译时将程序与库函数链接在一起。例如对比printf( ) and printk( )。 所以驱动所用头文件均来自内核源代码,应用程序所用头文件来自库函数。
4、内核模块代码运行空间 内核代码运行在内核空间,而应用程序在用户空间。应用程序的运行会形成新的进程,而内核模块一般不会。每当应用程序执行系统调用时,linux执行模式从用户空间切换到内核空间。
二、linux内核模块的框架
众所周知,Linux是一个开源的项目,为了使Linux在发展中能不成为一个封闭的项目,就要求任何使用Linux内核源码的个人或者组织在免费获得源码并做任意修改和再发布的同时,必须将修改后的源码发布。这就是所谓的GPL协议。 所以在每个驱动模块的代码里面必须声明:MODULE_LICENSE(“GPL”); 其实MODULE_LICENSE是一个宏,代表着相应的许可证协议,没有这行代码内核中的某些函数是不能使用的,类似的还有MODULE_AUTHOR 用于描述作者的信息、MODULE_DESCRIPTION用于模块的详细信息说明。
module_init(drv_init);//模块加载函数
module_exit(drv_exit);//模块卸载函数
这里利用 module_init 和 module_exit 这两个函数接口实现模块加载和卸载到内核。参数是我们自己定义的函数名,利用模块初始化函数和清除函数就可以用别名来定义了,但是指定任意的函数名又带来一个问题,就是可能会和内核中的已经有的函数重名,因为模块的代码最终也是属于内核代码的一部分。C语言没有类似C++的命名空间的概念,为了避免因为重名而带来重复定义的问题,函数可以加 static 关键字修饰。经过修饰的函数的链接属性为内部,从而解决了问题。这就是几乎所有的模块驱动程序都需要在函数前加static关键字修饰的原因。 在函数名前面加 __init 是把标记的函数放到ELF文件的特定代码段,在模块加载这些段时将会单独分配内存,这些函数调用成功后,模块的加载程序会释放这部分内存空间、 __exit用于修饰卸载函数,和__init的作用类似。这样便可以更加节约内存,调用完成后就直接释放掉。 代码示例:my_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int __init drv_init(void)
{
printk("%s\n", __FUNCTION__);
return 0;
}
static void __exit drv_exit(void)
{
printk("%s\n", __FUNCTION__);
}
module_init(hello_drv_init);
module_exit(hello_drv_exit);
MODULE_LICENSE("GPL");
应用程序和内核模块对比总结如下 :
| 应用程序 | 模块 | |
|---|---|---|
| 入口函数 | main | 加载时候调用drv_init |
| 函数的调用 | /lib | 所有函数可以直接调用 |
| 运行空间 | 用户空间 | 内核空间 |
| 资源的释放 | 系统自动释放kill -9 pid 手动释放 | 手动释放 |
三、Linux 内核模块的编译和加载
在编写完驱动程序后,接下来需要编写与之配对的Makefile文件。驱动编译分为静态编译和动态编译;静态编译即为将驱动直接编译进内核,动态编译即为将驱动编译成模块。 而动态编译又分为两种: a – 内部编译 在内核源码目录内编译(因为会修改内核的源码,可能造成不稳定的因素) b – 外部编译 在内核源码的目录外编译(一般采取的编译方法) Makefile示例:my_drv.c
ROOTFS_DIR = /home/linux/NFS/rootfs //测试平台上的NFS服务路径
ifeq ($(KERNELRELEASE), ) //在linux内核源代码中的顶层makefile中有定义
KERNEL_DIR =/home/linux/Exynos4412/linux-4.17 //测试平台上的内核源码路径(4.17版本内核)
CUR_DIR = $(shell pwd) //取得当前工作路径
all :
make -C $(KERNEL_DIR) M=$(CUR_DIR) modules
clean :
make -C $(KERNEL_DIR) M=$(CUR_DIR) clean
install:
cp -raf *.ko $(ROOTFS_DIR)/drv_module/
else
obj-m += my_drv.o
endif
make 的的执行步骤 第一次进来的时候,宏“KERNELRELEASE”未定义,因此进入 else; 记录内核路径,记录当前路径;由于make 后面没有目标,所以make会在Makefile中的第一个不是以.开头的目标作为默认的目标执行。默认执行all这个规则
make -C $(KERNEL_DIR) M=$(CUR_DIR) modules
-C 进入到内核的目录执行Makefile ,在执行的时候KERNELRELEASE就会被赋值,M=$(CUR_DIR)表示返回当前目录,再次执行makefile,modules 编译成模块的意思,再次执行该makefile,KERNELRELEASE就有值了,就会执行obj-m += my_drv.o, obj-m:表示把my_drv.o 和其他的目标文件链接成my_drv.ko模块文件,编译的时候还要先把my_drv.c编译成hello.o文件。