在外围芯片上市之前,芯片厂商往往进行了严格的验证,在他们的验证过程中,必然会编写代码去访问和控制这些芯片。很多时候,这些代码稍经整理就被芯片厂商随同 datasheet 一起在网站上作为参考代码发布。
范例程序往往停留在无操作系统的层次上,只是最底层的硬件操作代码,这一部分代码对驱动工程师的意义如下。
l 帮助工程师进一步理解芯片与 CPU 的接口原理、芯片的访问和控制方法。
l 直接加以改进后搬到 Linux 设备驱动中。Linux 设备驱动的硬件操作方法会与无操作系统时的硬件操作方法有如下差异。
l 无操作系统的硬件访问方法中往往没有物理地址到虚拟地址的映射过程,因此,在搬到 Linux 系统中的时候,要注意以静态映射或 ioremap()等方式映射到虚拟地址。
l 硬件访问中往往夹杂着延时,因此,在无操作系统的源码中,经常会出现xxx_delay()这样的 for 循环延迟,这些代码应该被内核中的 ndelay()或 udelay()替换。如果延迟时间达到数十 ms,应该使用msleep()或 msleep_interruptible()等函数。
l 芯片范例程序只是对芯片的操作方法进行示范,它并不会考虑真实应用场景中对 CPU 的资源占用以及代码的时间性能。例如,如果在写寄存器 REGA后,要判断寄存器 REGB 的第 0 位为 1 后才能进行下一次写,则无操作系统中的代码呈现为:
write_rega(int value)
{
rega = value;
while (!(regb &0x1));
}
第 2 句的 while (!(regb &0x1))是比较致命的,如果系统中用的 Linux 不支持抢占调度,而 REGB 的第 0 位变成 1 需要相当长的时间(如数十 ms),这种忙等待会导致其他的进程全部得不到机会执行。即使 Linux 支持抢占调度,进行这样的忙等待也毫无意义,Linux 中理想的做法是进行在这种情况下调度其他进程执行。
使用 while (!(regb &0x1))这样的判断还有一个更严重的陷阱,如果硬件出现了故障,REGB 的第 0 位总是变不成 1 的话,在系统不支持抢占调度的情况下,就“死机”了。
从 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 模板
Linux 2.6 中内核模块的 Makefile 模板
通过以上比较可以看到,从 Makefile 编写角度来看,在 Linux 2.6 内核下,内核模块编译不必定义复杂的 CFLAGS,而且模块中各文件依赖关系的表示更加简洁清晰。
在分析清楚 Linux 2.4 和 Linux 2.6 的内核模块 Makefile 的差异之后,可以给出同时支持 Linux 2.4 内核和 Linux 2.6 内核的内核模块 Makefile 文件, 如代码清单 23.6 所示。这个模板中实际上根据内核版本,去读取不同的 Makefile。
同时支持 Linux 2.4/2.6 的内核模块
#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
内核模块加载时的版本检查
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 等机制在 Linux2.6 内核的引入和完善,模块运行时对内核的依赖不再仅仅取决于内核版本,还取决于内核的配置,此时内核符号的校验码是否一致不能成为判断模块可否被加载的充分条件
在 Linux 2.6 内核的 linux/vermagic.h 头文件中定义了“版本魔术字符串”—VERMAGIC_ STRING(如代码清单 23.7 所示),VERMAGIC_STRING 不仅包含内核版本号,还包含内核编译所使用的 GCC 版本、SMP 与 PREEMPT 等配置信息。在编译模块时,我们可以看到屏幕上会显示“MODPOST”(模块后续处理),在内核源码目录下 scripts/mod/modpost.c 文件中可以看到模块后续处理部分的代码。就是在这个阶段,VERMAGIC_STRING 会被添加到模块的 modinfo 段中,模块编译生成后,通过“modinfo mymodule.ko”命令可以查看此模块的 vermagic 等信息。
Linux 2.6 内核下的模块装载器里保存有内核的版本信息,在装载模块时,装载器会比较所保存的内核 vermagic 与此模块的 modinfo 段里保存的 vermagic 信息是否一致,两者一致时,模块才能被装载。
VERMAGIC_STRING 的定义
#ifdef CONFIG_SMP
//配置了 SMP
#define MODULE_VERMAGIC_SMP "SMP "
#else
#define MODULE_VERMAGIC_SMP ""
#endif
#ifdef CONFIG_PREEMPT
//配置了 PREEMPT
#define MODULE_VERMAGIC_PREEMPT "preempt "
#else
#define MODULE_VERMAGIC_PREEMPT ""
#endif
#ifdef CONFIG_MODULE_UNLOAD //支持 module 卸载
#define MODULE_VERMAGIC_MODULE_UNLOAD "mod_unload "
#else
#define MODULE_VERMAGIC_MODULE_UNLOAD ""
#endif
#ifndef MODULE_ARCH_VERMAGIC //体系结构 VERMAGIC
#define MODULE_ARCH_VERMAGIC ""
#endif
/* 拼接内核版本、上述 VERMAGIC 以及 GCC 版本 */
#define VERMAGIC_STRING
UTS_RELEASE " "
MODULE_VERMAGIC_SMP MODULE_VERMAGIC_PREEMPT
MODULE_VERMAGIC_MODULE_UNLOAD MODULE_ARCH_VERMAGIC
"gcc-" _ _stringify(_ _GNUC_ _) "." _ _stringify(_ _GNUC_MINOR_ _)
在通过 make menuconfig 对内核进行新的配置后,再基于 Linux 2.6.15.5 内核编译生成的 hello.ko 模块
[root@localhost driver_study]# modinfo hello.ko
filename: hello.ko
license: Dual BSD/GPL
author: xxx
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 不一致,发生冲突,从而加载失败
内核模块使用计数
不管是在 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(),设备驱动框架结构中的驱动核心往往已经承担了此项工作。
内核模块导出符号
在 Linux 2.4 内核下, 默认情况下模块中的非静态全局变量及函数在模块加载后会输出到内核空间。而在 Linux 2.6 内核下,默认情况时模块中的非静态全局变量及函数在模块加载后不会输出到内核空间,需要显式调用宏 EXPORT_SYMBOL 才能输出。
所以在 Linux 2.6 内核的模块下,EXPORT_NO_SYMBOLS 宏的调用没有意义,是空操作。在同时支持 Linux 2.4 内核与 Linux 2.6 内核的设备驱动中,可以通过代码清单23.9 来导出模块的内核符号。
同时支持 Linux 2.4/2.6 内核的导出内核符号代码段
#include <linux/module.h>
#ifndef LINUX26
EXPORT_NO_SYMBOLS;
#endif
EXPORT_SYMBOL(var);
EXPORT_SYMBOL(func);
从良好的代码风格角度出发,模块中不需要输出到内核空间且不需为模块中其他文件所用的全局变量及函数最好显式申明为 static 类型,需要输出的内核符号最好以模块名为前缀。模块加载后,Linux 2.4 内核下可通过/proc/ksyms,Linux 2.6 内核下可通过/proc/kallsyms 查看模块输出的内核符号。
内核模块别名、加载接口
Linux 2.6 内核在 linux/module.h 中提供了 MODULE_ALIAS(alias)宏,模块可以通过调用此宏为自己定义一个或若干个别名。而在 Linux 2.4 内核下,用户只能在/etc/modules.conf 中为模块定义别名。
加载内核模块的接口 request_module()在 Linux 2.4 内核下为 request_module(constchar * 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));
结构体初始化
在 Linux 2.4 内核中习惯以“成员:值”的方式。但是,在 Linux 2.6 内核中,为了尽量向标准 C 靠拢,习惯使用“.成员=值”的方式。
字符设备驱动
在 Linux 2.6 内核中, 将 Linux 2.4 内核中都为 8 位的主次设备号分别扩展为 12 位和 20 位。鉴于此,Linux 2.4 内核中的 kdev_t 被废除,Linux 2.6 内核中新增的 dev_t拓展到了 32 位。 在 Linux 2.4 内核中, 通过 inode->i_rdev 即可得到设备号, 而在 Linux2.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()这样的函数。
l proc 操作。
以前的/proc 中只能给出字符串, 而新增的 seq_file 操作使得/proc 中的文件能导出如 long 等多种数据,为了支持这一新的特性,需要实现 seq_operations 结构体中的seq_printf()、seq_putc()、seq_puts()、seq_escape()、seq_path()、seq_open()等成员函数。
l 内存分配。
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()等函数替换。
l 内核时间变化。
在 Linux 2.6 中,一些平台的节拍(HZ)发生了变化,因此引入了新的 64 位计数器 jiffies_64,新的时间结构体 timespec 增加了 ns 成员变量,新增了 add_timer_on()定时器函数,新增了 ns 级延时函数 ndelay()。
l 并发/同步。
任务队列(task queue)接口函数都被取消,新增了 work queue 接口函数。
l 音频设备驱动。
Linux 2.4 内核中音频设备驱动的默认框架是 OSS, 而 Linux2.6 内核中音频设备驱动的默认框架则是 ALSA,这显示 ALSA 是一种未来的趋势。
如果驱动源代码要同时支持 Linux 2.4 和 Linux 2.6 内核,其实也非常简单,因为通过 linux/version.h 中的 LINUX_VERSION_CODE 可以获知内核版本, 之后便可以针对不同的宏定义实现不同的驱动源代码
同时支持 Linux 2.4 和 Linux 2.6 内核的驱动编写方法
#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
原作者:金乌秃鹫
|