从 2.4 到 2.6:Linux 内核可装载模块机制的改变对设备驱动的影响

Linux: LINUX_VERSION_CODE & KERNEL_VERSION 宏 这两个宏是在linux系统编译时生成的filechk_version.h 文件包含的宏定义。 在开发kernel 模块时为了兼容老版本会经常用到这两个宏。 Makefile define filechk_version.h (echo \#define LINUX_VERSION_CODE $(shell \ expr $(VERSION) \* 65536 + 0$(PATCHLEVEL) \* 256 + 0$(SUBLEVEL)); \ echo 阅读详情

文章出处:
http://www-128.ibm.com/developerworks/cn/linux/l-module26/
周婷 ,软件工程师 2006 年 2 月 9 日
    Linux内核从 2.4版本 到 2.6版本在可装载模块机制、设备模型、一些核心 API 等方面发生较大改变。即便是当前的5.10版本的内核,其内核可装载模块的编写、编译和使用仍基本延续2.6版本内核可装载模块的方式。鉴于此,可以通过linux内核从2.4版本到2.6版本的可装载模块的编译、使用等的前后对比来进一步了解linux内核可装载模块的一些技术细节。

        在17年前,linux内核2.6发布的时候,设备驱动开发人员面临着将驱动从 2.4 移植到 2.6 内核,或是使驱动同时支持2.4 与 2.6 内核的任务。站在设备驱动开发人员的角度,驱动由一个或几个外部可加载内核模块组成,本文针对 2.6 内核里模块机制的改变对编写设备驱动程序的影响,从内核模块的编译、装载时的版本检查、初始化与退出、模块使用计数、输出内核符号、命令行输入参数、许可证声明等方面比较了 2.4 与 2.6 内核的区别;并总结了使设备驱动同时支持 2.4 与 2.6 内核的一系列模板。

1. 获取内核版本

        当设备驱动需要同时支持不同版本内核时,在编译阶段,内核模块需要知道当前使用的内核源码的版本,从而使用相应的内核 API。2.4 与 2.6 内核下,源码头文件 linux/version.h 定义有:
LINUX_VERSION_CODE ― 内核版本的二进制表示,主、从、修订版本号各对应一个字节;
KERNEL_VERSION(major, minor, release) - 由主、从、修订版本号构造二进制版本号。
在同时支持2.4与2.6 内核的设备驱动程序中,经常可以看到以下代码段:

清单1:判断内核版本的代码段

#include <linux/version.h>

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 0)

#define LINUX26

#endif

#ifdef LINUX26

/*code in 2.6 kernel*/

#else

/*code in 2.4 kernel */

2.内核模块机制的改变

2.1 模块编译

        从2.4到2.6,外部可装载内核模块的编译、连接过程以及Makefile的书写都发生了改变。

  • 2.4内核中,模块的编译只需内核源码头文件;需要在包含linux/modules.h之前定义MODULE;编译、连接后生成的内核模块后缀为.o。
  • 2.6内核中,模块的编译需要配置过的内核源码;编译、连接后生成的内核模块后缀为.ko;编译过程首先会到内核源码目录下,读取顶层的Makefile文件,然后再返回模块源码所在目录。

清单2:2.4 内核模块的Makefile模板

#Makefile2.4

KVER=$(shell uname -r)

KDIR=/lib/modules/$(KVER)/build

OBJS=mymodule.o

CFLAGS=-D__KERNEL__ -I$(KDIR)/include -DMODULE -D__KERNEL_SYSCALLS__ -DEXPORT_SYMTAB

  -O2 -fomit-frame-pointer  -Wall  -DMODVERSIONS -include $(KDIR)/include/linux/modversions.h

all: $(OBJS)
mymodule.o: file1.o file2.o
        ld -r -o $@ $^
clean:
        rm -f *.o

        在2.4 内核下,内核模块的Makefile与普通用户程序的Makefile在结构和语法上都相同,但是必须在CFLAGS中定义-D__KERNEL__- DMODULE,指定内核头文件目录-I$(KDIR)/include。有一点需注意,之所以在CFLAGS中定义变量,而不是在模块源码文件中定义,一方面这些预定义变量可以被模块中所有源码文件可见,另一方面等价于将这些预定义变量定义在源码文件的起始位置。在模块编译中,对于这些全局的预定义变量,一般在CFLAGS中定义。

清单3:2.6 内核模块的Makefile模板

# Makefile2.6
ifneq ($(KERNELRELEASE),)
#kbuild syntax. dependency relationshsip of files and target modules are listed here.
mymodule-objs := file1.o file2.o
obj-m := mymodule.o
else
PWD  := $(shell pwd)
KVER ?= $(shell uname -r)
KDIR := /lib/modules/$(KVER)/build
all:
        $(MAKE) -C $(KDIR) M=$(PWD)
clean:
rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions
endif

        KERNELRELEASE是在内核源码的顶层 Makefile中定义的一个变量,在第一次读取执行此Makefile时,KERNELRELEASE没有被定义,所以make将读取执行else之后的内容。如果make的目标是clean,直接执行clean操作,然后结束。当make的目标为all时,-C $(KDIR) 指明跳转到内核源码目录下读取那里的Makefile;M=$(PWD) 表明然后返回到当前目录继续读入、执行当前的Makefile。当从内核源码目录返回时,KERNELRELEASE已被被定义,kbuild也被启动去解析kbuild语法的语句,make将继续读取else之前的内容。else之前的内容为kbuild语法的语句, 指明模块源码中各文件的依赖关系,以及要生成的目标模块名。mymodule-objs := file1.o file2.o表示mymoudule.o 由file1.o与file2.o 连接生成。obj-m := mymodule.o表示编译连接后将生成mymodule.o模块。
        补充一点,"$(MAKE) -C $(KDIR) M=$(PWD)"与"$(MAKE) -C $(KDIR) SUBDIRS =$(PWD)"的作用是等效的,后者是较老的使用方法。推荐使用M而不是SUBDIRS,前者更明确。
通过以上比较可以看到,从Makefile编写来看,在2.6内核下,内核模块编译不必定义复杂的CFLAGS,而且模块中各文件依赖关系的表示简洁清晰。

清单4: 可同时在2.4 与 2.6 内核下工作的Makefile

#Makefile for 2.4 & 2.6
VERS26=$(findstring 2.6,$(shell uname -r))
MAKEDIR?=$(shell pwd)
ifeq ($(VERS26),2.6)
include $(MAKEDIR)/Makefile2.6
else
include $(MAKEDIR)/Makefile2.4
endif

2.2 模块装载时的版本检查

        Linux内核一直在更新、完善,在a版本内核源码下编译的模块在b版本内核下通常不能运行,所以必须有一种机制,限制在a版本内核下编译生成的模块在b版本内核下被加载。
        2.4与2.6内核在可装载内核模块的版本检查机制方面发生了根本性的改变,不过这些改变对设备驱动开发人员而言基本是透明的。为了使模块装载时的版本检查机制生效,

  • 2.4 内核下,只需在CFLAGS中定义: 
-DMODVERSIONS -include $(KDIR)/include/linux/modversions.h;
  • 2.6内核下,开发人员无须采用任何操作。

        不过,在此仍有必要阐明2.4与2.6内核对可加载模块的版本检查机制。

        2.4 内核下, 执行`cat /proc/ksyms`可看到内核符号在名字后还跟随着一串校验字符串,此校验字符串与内核版本有关。在内核源码头文件linux/modules 目录下存在许多*.ver文件,这些文件起着为内核符号添加校验后缀的作用,如ksyms.ver 文件里有一行 #define printk _set_ver(printk)。linux/modversions.h 文件会包含全部的 ver文件。所以当模块包含linux/modversions.h文件后,编译时,模块里使用的内核符号实质是带有校验后缀的内核符号。在加载模块时,如果模块中所使用内核符号的校验字符串与当前运行内核所导出的相应的内核符号的校验字符串不一致,即当前内核空间并不存在模块所使用的内核符号,就会出现 "Invalid module format "的错误。

        为内核符号添加校验字符串来验证模块的版本与内核的版本是否匹配是繁杂和浪费内核空间的;而且随着SMP(对称多处理器)、PREEMPT(可抢占内核)等机制在2.6内核的引入和完善,模块运行时对内核的依赖不仅取决于内核版本,还取决于内核的配置,此时内核符号的校验码是否一致不能成为判断模块可否被加载的充分条件。2.6 内核下,在linux/vermagic.h中定义有VERMAGIC_STRING,VERMAGIC_STRING不仅包含内核版本号,还包含有内核使用的gcc版本,SMP与PREEMPT等配置信息。模块在编译时,我们可以看到屏幕上会显示"MODPOST"。在此阶段, VERMAGIC_STRING会添加到模块的modinfo段。在内核源码目录下scripts\mod\modpost.c文件中可以看到模块后续处理部分的代码。模块编译生成后,通过`modinfo mymodule.ko`命令可以查看此模块的vermagic等信息。2.6 内核下的模块装载器里保存有内核的版本信息,在装载模块时,装载器会比较所保存的内核vermagic与此模块的modinfo段里保存的 vermagic信息是否一致,两者一致时,模块才能被装载。譬如Fedora core 4 与core 2 使用的都是2.6 版本内核,在Fedore Core 2下去加载Fedora Core4下编译生成的hello.ko,会出现"invalid module format" 错误。

#insmod hello.ko
Invalid module format
hello: version magic '2.6.11-1.1369_FC4 686 REGPARM 4KSTACKS gcc-4.0'
should be '2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3'

2.3 模块的初始化与退出

        在2.6 内核中,内核模块必须调用宏module_init 与module_exit() 去注册初始化与退出函数。在2.4 内核中,如果初始化函数命名为init_module()、退出函数命名为cleanup_module(),可以不必使用module_init 与module_exit 宏。推荐使用module_init 与module_exit宏,使代码在2.4与2.6内核中都能工作。

清单5:适用于2.4与2.6内核的模块的初始化与退出模板

#include <linux/module.h>  /* Needed by all modules */
#include <linux/init.h>    /* Needed for init&exit macros */
static int mod_init_func(void)
{
/*code here*/
return 0;
}
static void mod_exit_func(void)
{
/*code here*/
}
module_init(mod_init_func);
module_exit(mod_exit_func);

        需要注意的是初始化与退出函数必须在宏module_init和module_exit使用前定义,否则会出现编译错误。

2.4 模块使用计数

        模块在被使用时,是不允许被卸载的。2.4内核中,模块自身通过MOD_INC_USE_COUNT、MOD_DEC_USE_COUNT宏来管理自己被使用的计数。2.6内核提供了更健壮、灵活的模块计数管理接口try_module_get(&module)及module_put(& module)取代2.4中的模块使用计数管理宏;模块的使用计数不必由自身管理,而且在管理模块使用计数时考虑到SMP与PREEMPT机制的影响。

  • int try_module_get(struct module *module):用于增加模块使用计数;若返回为0,表示调用失败,希望使用的模块没有被加载或正在被卸载中。
  • void module_put(struct module *module):减少模块使用计数。

        try_module_get 与module_put的引入与使用与2.6内核下的设备模型密切相关。模块是用来管理硬件设备的,2.6 内核为不同类型的设备定义了struct module *owner 域,用来指向管理此设备的模块。如字符设备的定义:

struct cdev {
        struct kobject kobj;
        struct module *owner;
        struct file_operations *ops;
        struct list_head list;
        dev_t dev;
        unsigned int count;
};

        从设备使用的角度出发,当需要打开、开始使用某个设备时,使用try_module_get(dev->owner)去增加管理此设备的owner模块的使用计数;当关闭、不再使用此设备时,使用 module_put(dev->owner)减少对管理此设备的owner模块的使用计数。这样,当设备在使用时,管理此设备的模块就不能被卸载;只有设备不再使用时模块才能被卸载。

        2.6内核下,对于为具体设备写驱动的开发人员而言,基本无需使用try_module_get与 module_put,因为此时开发人员所写的驱动通常为支持某具体设备的owner模块,对此设备owner模块的计数管理由内核里更底层的代码如总线驱动或是此类设备共用的核心模块来实现,从而简化了设备驱动开发。

2.5 模块输出的内核符号

  • 2.4 内核下,缺省情况时模块中的非静态全局变量及函数在模块加载后会输出到内核空间。
  • 2.6 内核下,缺省情况时模块中的非静态全局变量及函数在模块加载后不会输出到内核空间,需要显式调用宏EXPORT_SYMBOL才能输出。

        所以在2.6 内核的模块下,EXPORT_NO_SYMBOLS宏的调用没有意义,是空操作。在同时支持2.4与2.6内核的设备驱动中,可以通过以下代码段来输出模块的内核符号。

清单6: 同时支持2.4与2.6的输出内核符号代码段

#include <linux/module.h>

#ifndef LINUX26

EXPORT_NO_SYMBOLS;

#endif 

EXPORT_SYMBOL(var);

EXPORT_SYMBOL(func);

        需要注意的是如需在2.4内核下使用 EXPORT_SYMBOL,必须在 CFLAGS中定义 EXPORT_SYMTAB,否则编译将会失败。

        从良好的代码风格角度出发,模块中不需要输出到内核空间且不需为模块中其它文件所用的全局变量及函数最好显式申明为static类型,需要输出的内核符号以模块名为前缀。

        模块加载后,2.4内核下可通过 /proc/ksyms、 2.6 内核下可通过/proc/kallsyms查看模块输出的内核符号。

2.6 模块的命令行输入参数

        在装载内核模块时,用户可以向模块传递一些参数,如`modprobe modname var=value`,否则,var将使用模块内定义的缺省值。

        2.4 内核下,linux/module.h中定义有宏MODULE_PARM(var,type) 用于向模块传递命令行参数。var为接受参数值的变量名,type为采取如下格式的字符串[min[-max]]{b,h,i,l,s}。min及max 用于表示当参数为数组类型时,允许输入的数组元素的个数范围;b:byte;h:short;i:int;l:long;s:string。

        2.6内核下,宏MODULE_PARM(var,type)不再被支持。在头文件linux/moduleparam.h里定义了如下宏:

  • module_param(name, type, perm)        
  • module_param_array(name, type, nump, perm)

        type 类型可以是byte、short,、ushort、 int、 uint、long、ulong、charp, bool or invbool, 不再采用2.4内核中的字符串形式,而且在模块编译时会将此处申明的type与变量定义的类型进行比较,判断是否一致。

        perm表示此参数在sysfs文件系统中所对应的文件节点的属性。2.6内核使用sysfs文件系统,这是一个建立在内存中比proc更强大的文件系统。sysfs文件系统可以动态、实时,有组织层次地反应当前系统中的硬件、驱动等状态。当perm为0时,表示此参数不存在sysfs文件系统下对应的文件节点。模块被加载后,在/sys/module/ 目录下将出现以此模块名命名的目录。如果此模块存在perm不为0的命令行参数,在此模块的目录下将出现parameters目录,包含一系列以参数名命名的文件节点,这些文件的权限值等于perm,文件的内容为参数的值。

        nump 为保存输入的数组元素个数的变量的指针。当不需保存实际输入的数组元素个数时,可以设为NULL。从2.6.0至2.6.10 版本,须将变量名赋给nump;从2.6.10 版本开始,须将变量的引用赋给nump,这更易为开发人员理解。加载模块时,使用逗号分隔输入的数组元素。
清单7: 适用于2.4与2.6内核的模块输入参数模板

#include <linux/module.h>
#ifdef LINUX26
#include
#endif
int debug = 0;
char *mode = "800x600";
int tuner[4] = {1, 1, 1, 1};
#ifdef LINUX26
int tuner_c = 1;  
#endif
#ifdef LINUX26
MODULE_PARM(debug, "i");
MODULE_PARM(mode, "s");
MODULE_PARM(tuner,"1-4i");
#else
module_param(debug, int, 0644);
module_param(mode, charp, 0644);
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 10)
module_param_array(tuner, int, &tuner_c, 0644);
#else
module_param_array(tuner, int, tuner_c, 0644);
#endif
#endif

        模块编译生成后,加载模块时可以输入:`modprobe my_module mode=1024x768 debug=1 tuner=22,33`。

        在linux/moduleparam.h还定义有:

  • module_param_array_named(name, array, type, nump, perm)               
  • module_param_call(name, set, get, arg, perm)        
  • module_param_named(name, value, type, perm)

        读者可以参阅linux/moduleparam.h查看这些宏的详细描述,有一点需注意,在2.6内核里,module_param这一系列宏使用的都是小写名字。

2.7 模块的许可证声明

        从2.4.10 版本内核开始,模块必须通过MODULE_LICENSE宏声明此模块的许可证,否则在加载此模块时,会收到内核被污染"kernel tainted" 的警告。从linux/module.h文件中可以看到,被内核接受的有意义的许可证有 "GPL","GPL v2","GPL and additional rights","Dual BSD/GPL","Dual MPL/GPL","Proprietary"。

        在同时支持2.4与2.6内核的设备驱动中,模块可按如下方式声明自己的许可证。
清单8: 适用于2.4与2.6内核的模块许可证声明模板

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,10)
MODULE_LICENSE("GPL");
#endif

2.8 小结

        此外,2.6内核里还有一些模块机制的改变,不常为驱动开发人员用到。如加载内核模块的接口request_module在2.4 下为request_module(const char * module_name);在2.6内核下为request_module(const char *fmt, ...)。在2.6 内核下,驱动开发人员可以通过调用

request_module("msp3400");
request_module("char-major-%d-%d", MAJOR(dev), MINOR(dev));

 这种更灵活的方式加载其它内核模块。
        2.6内核在linux/module.h中还提供了MODULE_ALIAS(alias)宏,模块可以通过调用此宏为自己定义一或若干个别称。而在2.4内核下,用户只能在/etc/modules.conf中为模块定义别称。
通过以上比较可以看到,从2.4到2.6内核,可装载模块管理机制的改变使设备驱动的开发变得更加简洁、灵活、健壮。
      作者:周婷,软件工程师,S3 Graphics 上海研发中心,工作方向: 视频解码。email: moting9@hotmail.com。
  


  

如何免费访问和使用Gemini API? Gemini Ultra具有最先进的性能,在几个指标上超过了GPT-4的性能。它是第一个在大规模多任务语言理解基准测试中超越人类专家的模型,该基准测试57个不同学科的世界知识和解决问题的能力。Gemini是谷歌迄今为止开发的最先进、最庞大的AI模型。它为多模态而建,理解并处理不同类型的数据,比如文本、代码、音频、图像和视频。下面我们将加载Masood Aslami的图像,并用它来测试Gemini Pro Vision的多模态性。有了Gemini可以为查询提供图像、音频和文本,获得几乎完美的答案。 阅读详情

相关推荐

西电微机系统课程设计步进电机开环控制系统

** 微机系统与应用课程设计 课题二 步进电机开环控制系统设计** 一、程设计目的 掌握微机系统总线与各芯片管脚连接方法,提高接口扩展硬件电路的连接能力。 加深对 A/D 和并行接口芯片的工作方式和编程方法的理解。 搞懂步进电机的工作原理及控制方式,掌握开环控制系统的设计思路和实现方法。 二、课程设计的内容 手动调节电位器旋钮(0V~5V),通过 ADC0809 模拟输入水库水位 0 米~50...

qq_37015775的博客 4033

从2.4 到2.6:Linux 内核可装载模块机制的改变对设备驱动的影响

      从 2.4 到 2.6,Linux 内核在可装载模块机制、设备模型、一些核心 API 等方面发生较大改变,设备驱动开发人员面临着将驱动从 2.4 移植到 2.6 内核,或是使驱动同时支持2.4 与 2.6 内核的任务。站在设备驱动开发人员的角度,驱动由一个或几个外部可加载内核模块组成,本文针对 2.6 内核里模块机制的改变对编写设备驱动程序的影响,从内核模块的编译、装载时的版本检查、初

freenaut的专栏 566

Matlab实战:用卡尔曼滤波搞定无人机GPS轨迹优化(附完整代码)

本文详细介绍了如何使用Matlab实现卡尔曼滤波(KF)算法优化无人机GPS轨迹,提供完整代码和工程实践指南。通过线性卡尔曼滤波技术,有效降低定位噪声,提升轨迹平滑度,适用于无人机导航和控制系统开发。

weixin_29061425的博客 253

低成本红外方案把老空调接入Home Assistant,全流程解析

智能家居改造中,红外遥控是最通用且低成本的设备接入方式。本文从红外通信的基本原理切入,讲解如何利用ESP8266开发板与红外发射电路,抓取空调遥控器的原始脉冲,并逐步拆解出美的空调的编码协议。通过控制变量法反推帧结构后,即可借助MQTT或ESPHome将自定义控制指令无缝集成到Home Assistant中,实现远程开关、调温、模式切换等场景。整个方案不拆机、不改动空调硬件,成本仅数十元,具备很高的可复制性,适合普通家庭的老空调智能化升级,也为理解红外协议解析与智能家居联动提供了完整参考。

weixin_34198762的博客 360

别再手动拆解字节了!用博图这个功能指令,5分钟搞定ARRAY转BOOL数组

本文详细介绍了西门子博图中`ARRAY_TO_BOOL`功能指令的高效应用,帮助工程师快速实现ARRAY到BOOL数组的转换。通过实战案例和参数配置指南,展示了如何5分钟内完成传统方法需半小时的任务,大幅提升PLC编程效率,特别适用于通信报文解析和设备状态监控。

weixin_42653691的博客 260

【论文阅读1】机理引导的残差学习用于电池全生命周期状态监测 | Nature Communications

本文提出了一种机理引导的残差学习框架,用于提升电池全生命周期内荷电状态(SOC)和健康状态(SOH)的监测精度。该方法融合等效电路模型的先验知识与机器学习,设计了修正模型和补偿模型两种通用框架。实验验证表明,该方法在三种不同类型电池数据集上均表现出色,SOC和SOH估计误差较传统方法降低50%以上,且在未见工况下仍保持良好外推能力。模型具有强鲁棒性,在减少训练数据量和降低采样频率时性能稳定,并兼具物理可解释性。该框架无需离线测试和运行中校准,为电池安全监测提供了有效解决方案。

qq_21843433的博客 613
上一篇: 编译linux内核模块时的make -C M= modules的参数说明
下一篇: 编译出错,出现 vfs_read[******.ko] undefined !
土豆西瓜大芝麻
博客等级 码龄18年 1189粉丝 238原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值