第23章Linux设备驱动的移植之从Linux2.4移植设备驱动到Linux2.6

23.3 从 Linux 2.4 移植设备驱动到 Linux 2.6 

        从 Linux 2.4 内核到 Linux 2.6 内核,Linux 在可装载模块机制、设备模型、一些核心 API 等方面发生了较大改变,随着公司产品的过渡,驱动工程师会面临着将驱动从 Linux 2.4 内核移植到Linux 2.6 内核,或是让驱动能同时支持 Linux 2.4 内核与 Linux 2.6 内核的任务。 

        分析 Linux 2.4 内核和 Linux 2.6 内核在设备驱动方面的几个主要的不同点。

 1.内核模块的 Makefile Linux 2.4 内核中,模块的编译只需内核源码头文件,并在包括 linux/modules.h 头文件之前定义 MODULE,且其编译、连接后生成的内核模块后缀为.o。而在 Linux 2.6 内核中,模块的编译需要依赖配置过的内核源码,编译过程首先会到内核源码目录下,读取顶层的 Makefile 文件,然后再返回模块源码所在目录,且编译、连接后生成的内核模块后缀为.ko。Linux 2.4 中内核模块Makefile 模板如代码清单 23.4 所示。 

代码清单 23.4 Linux 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

而 Linux 2.6 中内核模块的 Makefile 模板如代码清单 23.5 所示。 

代码清单 23.5 Linux 2.6 中内核模块的 Makefile 模板 

# Mcakefile2.6
 ifneq ($(KERNELRELEASE),)
 #dependency relationshsip of files and target modules(多文件模块编译)
 #mymodule-objs := file1.o file2.o
 #obj-m := mymodule.o

 obj-m := second.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

        Linux 2.6 内核模板 Makefile 中的 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 模块。

        “$(MAKE) -C $(KDIR) M=$(PWD)”与“$(MAKE) -C $(KDIR) SUBDIRS =$(PWD)”的作用

是等效的,后者是较老的使用方法。

        通过以上比较可以看到,从 Makefile 编写角度来看,在 Linux 2.6 内核下,内核模块编译不必定义复杂的 CFLAGS,而且模块中各文件依赖关系的表示更加简洁清晰。

        在分析清楚 Linux 2.4 和 Linux 2.6 的内核模块 Makefile 的差异之后,可以给出同时支持Linux 2.4 内核和 Linux 2.6 内核的内核模块 Makefile 文件,如代码清单 23.6 所示。这个模板中实际上根据内核版本,去读取不同的 Makefile。

代码清单 23.6 同时支持 Linux 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.内核模块加载时的版本检查

        Linux 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”的错误。

        Linux 内核所采用的在内核符号添加校验字符串来验证模块的版本与内核的版本是否匹配的方法很复杂且会浪费内核空间,而且随着 SMP、PREEMPT 等机制在 Linux 2.6 内核的引入和完善,模块运行时对内核的依赖不再仅仅取决于内核版本,还取决于内核的配置,此时内核符号的校验码是否一致不能成为判断模块可否被加载的充分条件。

        在 Linux 2.6 内核的 linux/vermagic.h 头文件中定义了“版本魔术字符串” — VERMAGIC_STRING(如代码清单 23.7 所示),VERMAGIC_STRING 不仅包含内核版本号,还包含内核编译所使用的 GCC 版本、SMP 与 PREEMPT 等配置信息。在编译模块时,可以看到屏幕上会显示“MODPOST”(模块后续处理),在内核源码目录下 scripts/mod/modpost.c 文件中可以看到模块后续处理部分的代码。

代码清单 23.7 VERMAGIC_STRING 的定义

#ifdef CONFIG_SMP
#define MODULE_VERMAGIC_SMP "SMP "
#else
#define MODULE_VERMAGIC_SMP ""
#endif
#ifdef CONFIG_PREEMPT
#define MODULE_VERMAGIC_PREEMPT "preempt "
#else
#define MODULE_VERMAGIC_PREEMPT ""
#endif
#ifdef CONFIG_MODULE_UNLOAD
#define MODULE_VERMAGIC_MODULE_UNLOAD "mod_unload "
#else
#define MODULE_VERMAGIC_MODULE_UNLOAD ""
#endif
#ifndef MODULE_ARCH_VERMAGIC
#define MODULE_ARCH_VERMAGIC ""
#endif


#define VERMAGIC_STRING                                                 \
        UTS_RELEASE " "                                                 \
        MODULE_VERMAGIC_SMP MODULE_VERMAGIC_PREEMPT                     \
        MODULE_VERMAGIC_MODULE_UNLOAD MODULE_ARCH_VERMAGIC

     就是在这个阶段,VERMAGIC_STRING 会被添加到模块的 modinfo 段中,模块编译生成后,通过“modinfo mymodule.ko”命令可以查看此模块的 vermagic 等信息。

        Linux 2.6 内核下的模块装载器里保存有内核的版本信息,在装载模块时,装载器会比较所保存的内核 vermagic 与此模块的 modinfo 段里保存的 vermagic 信息是否一致,两者一致时,模块才能被装载。

        在通过 make menuconfig 对内核进行新的配置后,再基于 Linux 2.6.15.5 内核编译生成的hello.ko 模块(见第 4 章),这个模块的 modinfo 结果如下:

[root@localhost driver _ study]# modinfo hello.ko
filename: hello.ko
license: Dual BSD/GPL
author: Song Baohua
description: A simple Hello World Module
alias: a simplest module
vermagic: 2.6.15.5 SMP preempt PENTIUM4 gcc-3.2
depends:

        从中可以看出,其 vermagic 为“2.6.15.5 SMP preempt PENTIUM4 gcc-3.2”,运行“insmod
hello.ko”命令,得到如下错误:

        insmod: error inserting 'hello.ko': -1 Invalid module format
        hello: version magic '2.6.15.5 SMP preempt PENTIUM4 gcc-3.2' should be '2.6.15.5 686 gcc-3.2'

        原因在于加载该 hello.ko 时候所使用的内核虽然还是 Linux 2.6.15.5,但是和编译 hello.ko 时的内核的关键部分配置不一样,导致 vermagic 不一致,发生冲突,从而加载失败。

3.内核模块的加载与卸载函数

        在 Linux 2.6 内核中,内核模块必须调用宏 module_init 与 module_exit()去注册初始化与退出函数。在 Linux 2.4 内核中,如果加载函数命名为 init_module()卸载函数命名为 cleanup_module(),可以不必使用 module_init 与 module_exit 宏。因此,若使用 module_init 与 module_exit 宏,代码在 Linux 2.4 内核与 Linux 2.6 内核中都能工作,如代码清单 23.8 所示。

代码清单 23.8 同时支持 Linux 2.4/2.6 的内核模块加载/卸载函数

static __init int mod _ init _ func(void)
 {
             ...
             return 0;
 }

 static __eixt void mod _ exit _ func(void)

{

         ...

 }

module _ init( mod _ init _ func);
module _ exit( mod _ exit _ func);

4.内核模块使用计数

        不管是在 Linux 2.4 内核还是在 Linux 2.6 内核中,当内核模块正在被使用时,是不允许被卸载的,内核模板使用计数用来反映模块的使用情况。Linux 2.4 内核中,模块自身会通过MOD_INC_USE_COUNT、MOD_DEC_USE_COUNT 宏来管理自己被使用的计数。Linux 2.6 内核提供了更健壮、灵活的模块计数管理接口 try_module_get(&module)及 module_put (&module)取代 Linux 2.4 中的模块使用计数管理宏。而且,Linux 2.6 内核下,对于为具体设备写驱动的开发人员,基本无须使用 try_module_get()与 module_put(),设备驱动框架结构中的驱动核心往往已经承担了此项工作。

5.内核模块导出符号

        在 Linux 2.4 内核下,默认情况下模块中的非静态全局变量及函数在模块加载后会输出到内核空间。而在 Linux 2.6 内核下,默认情况时模块中的非静态全局变量及函数在模块加载后不会输出到内核空间,需要显式调用宏 EXPORT_SYMBOL 才能输出。所以在 Linux 2.6 内核的模块下,EXPORT_NO_SYMBOLS 宏的调用没有意义,是空操作。在同时支持 Linux 2.4 内核与 Linux 2.6内核的设备驱动中,可以通过代码清单 23.9 来导出模块的内核符号。

代码清单 23.9 同时支持 Linux 2.4/2.6 内核的导出内核符号代码段

 #include <linux/module.h>
 #ifndef LINUX26
 EXPORT_NO_SYMBOLS;

 #endif

// 非静态的全局变量以及函数

 EXPORT_SYMBOL(var);
 EXPORT_SYMBOL(func);

       另外,如果需要在 Linux 2.4 内核下使用 EXPORT_SYMBOL,必须在 CFLAGS 中定义EXPORT_SYMTAB,否则编译将会失败。

    从良好的代码风格角度出发,模块中不需要输出到内核空间且不需为模块中其他文件所用的全局变量及函数最好显式申明为 static 类型,需要输出的内核符号最好以模块名为前缀。模块加载后,Linux 2.4 内核下可通过/proc/ksymsLinux 2.6 内核下可通过/proc/kallsyms 查看模块输出的内核符号。

6.内核模块输入参数

    在 Linux 2.4 内核下,通过 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。

    在 Linux 2.6 内核下,宏 MODULE_PARM(var,type)不再被支持,而是使用 module_param(name,type, perm)和 module_param_array(name, type, nump, perm)宏。

     为了使驱动能根据内核的版本分别调用不同的宏导出内核符号,可以使用类似代码清单 23.10 所示的方法。

代码清单 23.10 同时支持 Linux 2.4/2.6 的模块输入参数范例

 #include <linux/module.h>

 #ifdef LINUX26

#include <linux/moduleparam.h>
 #endif
 int int_param = 0;
 char *strin_param = "I love Linux";
 int array_param[4] = {1, 1, 1, 1};
 #ifdef LINUX26
 int len = 1;
 #endif
 #ifdef LINUX26
 MODULE_PARM(int_param, "i");
 MODULE_PARM(string_param, "s");
 MODULE_PARM(array_param, "1-4i");
 #else
 module_param(int_param, int, 0644);
 module_param(string_param, charp, 0644);
 #if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 10)
 module_param_array(array_param, int,&len, 0644);
 #else
 module_param_array(array_param, int, len, 0644);
 #endif
 #endif

7.内核模块别名、加载接口

        Linux 2.6 内核在 linux/module.h 中提供了 MODULE_ALIAS(alias)宏,模块可以通过调用此宏为自己定义一个或若干个别名。在 Linux 2.4 内核下,用户只能在/etc/modules.conf 中为模块定义别名。

        加载内核模块的接口 request_module()在 Linux 2.4 内核下为 request_module(const char *
module_name),在 Linux 2.6 内核下则为 request_module(const char *fmt, ...)。在 Linux 2.6 内核下,
驱动开发人员通过调用以下的方法来加载内核模块。

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

8.结构体初始化

        在 Linux 2.4 内核中,习惯以代码清单 23.11 所示的方法来初始化结构体,即“成员:值”的方式。

代码清单 23.11 Linux 2.4 内核中结构体初始化习惯

static struct file_operations lp_fops =
 {
         owner: THIS_MODULE,
         write: lp_write,
         ioctl: lp_ioctl,
         open: lp_open,
         release: lp_release,
 };

        在 Linux 2.6 内核中,为了尽量向标准 C 靠拢,习惯使用如代码清单 23.12 所示的方法来初始化结构体,即“.成员=值”的方式。

代码清单 23.12 Linux 2.6 内核中结构体初始化习惯

static struct file_operations lp_fops =
 {
         .owner = THIS_MODULE,
         .write = lp_write,
         .ioctl = lp_ioctl,
         .open = lp_open,
         .release = lp_release,
 };

9.字符设备驱动

        在 Linux 2.6 内核中,将 Linux 2.4 内核中都为 8 位的主次设备号分别扩展为 12 位和 20 位。鉴于此,Linux 2.4 内核中的 kdev_t 被废除,Linux 2.6 内核中新增的 dev_t 拓展到了 32 位。在 Linux2.4 内核中,通过 inode->i_rdev 即可得到设备号,而在 Linux 2.6 内核中,为了增强代码的可移植性,内核中新增了 iminor()和 imajor()这两个函数来从 inode 获得设备号。

        在 Linux 2.6 内核中,对于字符设备驱动,提供专门用于静态申请/动态分配设备号的 

register_chrdev_region()函数和 alloc_chrdev_region()函数,而在 Linux 2.4 内核中,对设备号的静态申请和动态注册字符设备的行为都是在 register_chrdev()函数中进行的,没有单独的 cdev 结构体,因此也不存在
cdev_init()、cdev_add()、cdev_del()这些函数。要注意的是,register_chrdev()在 Linux 2.6 内核中仍然被支持,但是不能访问超过 256 的设备号。

        其次,devfs 设备文件系统在 Linux 2.6 内核中被取消了,因此,最新的驱动中也不宜再调用

devfs_register()、devfs_unregister()这样的函数。

1、proc 操作

        以前的/proc 中只能给出字符串,而新增的 seq_file 操作使得/proc 中的文件能导出如 long 等多种数据,为了支持这一新的特性,需要实现 seq_operations 结构体中的 seq_printf()、seq_putc()、seq_puts()、seq_escape()、seq_path()、seq_open()等成员函数。

2、内存分配

        Linux 2.4 和 Linux 2.6 在内存分配方面发生了一些细微的变化,这些变化主要包括:

<linux/malloc.h>头文件被改为<linux/slab.h>;

分配标志 GFP_BUFFER 被 GFP_NOIO 和 GFP_NOFS 取代;

新增了__GFP_REPEAT、__GFP_NOFAIL 和__GFP_NORETRY 分配标志;

页面分配函数 alloc_pages()、get_free_page()被包含在<linux/gfp.h>中;

对 NUMA 系统新增了 alloc_pages_node()、free_hot_page()、free_cold_page()函数;

新增了内存池;

针对 r-cpu 变量的 DEFINE_PER_CPU()、EXPORT_PER_CPU_SYMBOL()、EXPORT_PER_CPU_SYMBOL_GPL()、DECLARE_PER_CPU()、DEFINE_PER_CPU()等宏因为抢占调度的出现而变得不安全,被 get_cpu_var()、put_cpu_var()、alloc_percpu()、free_percpu()、per_cpu_ptr()、get_cpu_ptr()、put_cpu_ptr()等函数替换。

3、内核时间变化

       在 Linux 2.6 中, 一些平台的节拍(Hz)发生了变化,因此引入了新的 64 位计数器 jiffies_64,新的时间结构体 timespec 增加了 ns 成员变量,新增了 add_timer_on()定时器函数,新增了 ns 级延时函数 ndelay()。

4、并发/同步

任务队列(task queue)接口函数都被取消,新增了 work queue 接口函数。

5、音频设备驱动

    Linux 2.4 内核中音频设备驱动的默认框架是 OSS,而 Linux2.6 内核中音频设备驱动的默认框

架则是 ALSA。

总结:

        如果驱动源代码要同时支持 Linux 2.4 和 Linux 2.6 内核,其实也非常简单,因为通过linux/version.h 中的 LINUX_VERSION_CODE 可以获知内核版本,之后便可以针对不同的宏定义实现不同的驱动源代码,如代码清单 23.13 所示。

  #include <linux/version.h>
 #if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 0)
 #define LINUX26
 #endif
 #ifdef LINUX26
 /*Linux 2.6 内核中的代码*/
 #else
 /*Linux 2.4 内核中的代码 */
 #endif


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值