Linux模块编程框架

Linux是单内核系统,可通用计算平台的外围设备是频繁变化的,不可能将所有的(包括将来即将出现的)设备的驱动程序都一次性编译进内核,为了解决这个问题,Linux提出了可加载内核模块(Loadable Kernel Module,LKM)的概念,允许一个设备驱动通过模块加载的方式,在内核运行起来之后"融入"内核,加载进内核的模块和本身就编译进内核的模块一模一样。
一个程序在编译的地址的相对关系就已经确定了,运行的时候只是进行简单的偏移,为了使模块加载进内核后能够被放置在正确的地址,并正确的调用系统的运行的导出符号表,编译模块的时候必须要使用系统的编译地址,并调用系统编译出得静态的导出符号表。即模块必须使用系统的配置环境:Makefile+.config,一旦这两个文件任意一个发生了变化,都很可能导致模块的编译地址与系统的编译地址不匹配,造成运行时的错误甚至宕机。

导出符号表

从提供系统运行效率的角度,一个模块不是也不应该是完全独立的,即一个模块往往会调用其他模块提供的功能来实现自己的功能,这样做能更好实现系统的分工并提高效率。Linux为了实现模块间的相互调用,设计了导出符号表,每个模块都可以将自己的一个私有的标号导出到系统层级,以使该标号对其他模块可见,系统在编译一个模块的时候会自动导出这个模块的导出符号表到modules.syms文件(如果没有导出任何符号,可以为空),并在加载一个模块的时候会自动将该模块的导出符号表与系统自身的导出符号表合并。一个系统的源码的导出符号表一般在源码顶层目录的modules.syms文件中,查看正在运行的系统导出符号表使用cat /proc/kallsyms。注意,正如前面解释的,我们的模块之所以能够正常运行,一个重要原因就是编译我们模块使用的符号地址就是编译内核时使用的符号地址,所以运行起来虽然地址会有偏移,但是模块中相关的符号的地址也会和内核地址一起偏移,也就还能找得到。基于这种思想,我们也可以直接查看系统当前运行的地址,将地址赋值给一个函数指针并使用,也是没有问题的,当然,这只是阐述原理,并不建议这么写模块。
下面这个例子可以看出编译出的地址和运行时的地址是不一样的:

Linux模块编程框架

导出符号表可以大大的提高系统的运行效率,这也是只有开源系统才能提供的一个强大的功能,但是,导出符号表的引入会导致一个小小的麻烦--模块的依赖,当我们使用lsmod的时候,就可以查看系统当前的模块,其最后两列分别是该模块被引用的次数以及引用该模块的内核模块,当一个模块被其他模块引用时,我们是不能进行卸载的,同样,如果模块A依赖于模块B,那么如果模块B不加载的时候模块A也加载不了。在编写多模块的时候尤其要注意这个问题,可以写一个脚本管理多个依赖模块。Linux内核使用两个宏来导出一个模块的符号

EXPORT_SYMBOL(符号名) EXPORT_SYMBOL_GPL(符号名) 模块编译方法

借助内核的Makefile,编译出的xxx.ko(Kernel Object)就是可加载到该内核的外部模块,为了利用内核的Makefile,我们可以将编译外部模块的Makefile写成如下的格式:

ifneq ($(KERNELRELEASE),) export-objs = demo.o obj-m = extern.o else KERNELDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) endif all: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules clean: .tmp_versions Module.symvers modules.order .tmp_versions .*.cmd *.o *.ko *.mod.c

这个简单的Makfile是利用Ubuntu主机的源码Makefile来编译模块,学习模块编程的开始阶段在主机进行编译调试更方便一点,下面我解释一下这个Makefile,首先,我们的思路还是通过内核的Makefile来准备我们的模块,而内核的Makefile一旦执行,就会给KERNELRELEASE这个变量赋值,所以第一次进入我们这个Makfile的时候,这个变量还是空,所以执行else的部分——给相关的变量赋值,make默认编译第一个目标all,make -C $(KERNELRELEASE)就是进入到KERNELRELEASE指定的目录并执行里面的Makefile,显然,这就是我们内核源码的顶层Makefile,接下来的选项M=$(PWD) modules都是传入这个顶层Makefile的参数,表示我要编译一个模块,这个模块位于M指定的目录,所以内核会进行相关的配置并最终进入到"这个模块所在的目录",此时,我们的这个Makefile会再被进入一次,这一次是从内核Makefile中跳入这里的,,KERNELRELEASE已经被定义过,内核Makefile想要的就是obj-m后面指定的要编译的目标文件,所以内核Makfile就会找到我们写的模块源文件进行编译。如此我们就得到了能在ubuntu下执行的xxx.ko文件,如果需要在开发板上运行,只需要将内核路径改成开发板运行系统的源码路径即可,同时记得要导出相关的环境变量(ARCH,CROSS_COMPILE)

注册/注销模块

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/41594db577e0764ac55836de6efdf93f.html