Chinaunix首页 | 论坛 | 博客
  • 博客访问: 819825
  • 博文数量: 117
  • 博客积分: 2583
  • 博客等级: 少校
  • 技术积分: 1953
  • 用 户 组: 普通用户
  • 注册时间: 2008-12-06 22:58
个人简介

Coder

文章分类
文章存档

2013年(1)

2012年(10)

2011年(12)

2010年(77)

2009年(13)

2008年(4)

分类: LINUX

2010-03-26 23:11:33

模块编程

设置测试系统

由发行版厂商提供的内核通常打了许多的补丁,从而和主线内核存在很大差异;某些情况下,厂商的补丁回叙该设备驱动程序使用的API。如果正在编写一个只适用于某特定发行版的驱动程序,则应该针对相关内核创建和测试自己的驱动程序。如果想要学习驱动程序的编写,则标准内核是最好的。

不管内核来自哪里,要想为2.6.x内核构造模块,还必须在自己的系统中配置并购造好内核树。因为2.6内核的模块要和内核源代码树中的目标文件链接,通过这种方式,可得到一个更加健壮的模块装载器,但也需要这些目标文件存在于内核目录树中。

 

编写模块,真正的困难在于理解设备并最大化性能。

 

核心模块与应用程序的对比

模块只是预先注册自己以便服务于将来的某个请求,然后它的初始化函数就立即结束。模块初始化函数的任务就是为以后调用模块函数做准备。模块的退出函数将在模块被卸载之前调用。模块是事件驱动的。模块的退出函数必须仔细撤销初始化函数所做的一切,否则,在系统重新引导之前,某些东西就会残留在系统中。

模块仅仅被链接到内核,因此它能调用的函数仅仅是由内核导出的那些函数,而不存在任何可连接的函数库。

因为没有任何函数库会和模块链接,因此源文件中不能包含通常的头文件,想以及一些非常特殊的情况是仅存的例外。内核模块只能使用作为内核一部分的函数。

和内核相关的任何内容都在我们安装并配置好的内核源代码树的头文件中声明,其中,大多数相关的头文件保存在include/linuxinclude/asm目录中,但include得其他子目录中保存有和特定内核子系统相关的头文件。

内核编程和应用程序编程的另外一点重要不同之处在于,各环境下处理错误的方式不同:应用程序开发过程中的段错误是无害的,并且总是可以使用调试器跟踪源代码中的问题所在,而一个内核错误即使不影响整个系统,也至少会杀死当前进程。

 

用户空间和内核空间

模块运行在所谓的内核空间里,而应用程序运行在所谓的用户空间中。内核空间和用户空间,这两个术语不仅说明两种模式具有不同的优先权等级,而且还说明每个模式都有自己的内存映射,也即自己的地址空间。

系统调用或硬件中断时,Unix将执行从用户空间到内核空间的切换。执行系统调用,内核代码运行在进程上下文中,它代表调用进程执行操作,因此能够访问地址空间的所有数据。而处理器中断的内核代码和进程是异步的,与任何一个特定进程无关。一个驱动程序,实际上通常主要由两部分组成,一部分作为系统调用的一部分执行(部分函数),其他函数则负责处理中断。

 

内核的并发

即使是最简单的内核模块,都需要在编写时铭记:同一时刻,可能会有许多事情正在发生。

Linux内核代码(包括驱动程序代码)必须是可重入的,它必须能够同时执行在多个上下文中。Linux内核并发问题的来源很多,Linux支持多用户多进程,中断的异步执行,一些软件的异步执行,如内核定时器,中断下半部等,SMP系统中同一时间在不同处理器上运行多个线程,Linux 2.6内核支持内核代码抢占。

对编写正确的内核代码来说,优良的并发管理是必需的。

2.6中,内核代码几乎始终不能假定在给定代码中能够独占处理器。

 

当前进程

内核的大多数操作还是和某个特定的进程相关。

内核代码可通过访问全局项current来获得当前进程。

current中定义,是一个指向struct task_struct的指针,而task_struct结构在文件中定义。current指针指向当前正在运行的进程。

如果需要,内核代码可以通过current获得与当前进程相关的信息。

2.6内核中,current不再是一个全局变量,为了支持SMP,将指向task_struct结构的指针隐藏在内核栈中。这种实现的细节同样也对其他内核子系统隐藏,设备驱动程序只要包含头文件即可引用当前进程。

 

其他一些细节

应用程序在虚拟内存中布局,并具有一块很大的栈空间。栈是用来保存函数调用历史以及当前活动函数中的自动变量的。而内核具有非常小的栈。我们自己的函数必须和整个内核空间调用链一同共享这个栈。声明大的自动变量并不是一个好主意,如果需要大的结构,则应该在调用时动态分配。内核API中具有两个下划线前缀(__)的函数,通常是接口的底层组件,应谨慎使用。

内核代码不能实现浮点数运算。

 

编译和装载

编译模块

内核是一个大的独立的程序,为了将它的各个片段放在一起,要满足很多详细而明确的要求。(内核的构造详见Documentation/kbuild目录下的几个文件。)

构造内核模块的先决条件:正确版本的编译器,模块工具和其他一些必要的工具。Documentation/Changes文件列出了需要的工具版本。内核源代码对编译器作了大量的假定。

创建Makefile文件,如对“hello world”模块:

obj-m := hello.o

上面的赋值语句(它利用了GNU makede 扩展语法)说明了有一个模块要从目标文件hello.o中构造,而从该目标文件中构造的模块名称为hello.ko(由内核的构造系统决定)。

对于构造有多个源文件的模块,,如:

obj-m := module.o                                             

module-objs := file1.o file2.o

为了使上面这种类型的Makefile文件正常工作,必须在内核构造系统环境中调用它们。再包含模块源码和Makefile文件的目录中键入:

make –C  ~/kernel-2.6 M=`pwd` modules

上述命令首先改变目录到-C选项指定的位置(即内核源代码目录),其中保存有内核的顶层Makefile文件。M=选项让Makefile在构造modules目标之前返回到模块源代码目录。然后,modules目标指向obj-m变量中设置的模块,即module.o

另外一种编写Makefile的技巧:

# If KERNELRELEASE is defined, we've been invoked from the

# kernel build system and can use its language.

ifneq ($(KERNELRELEASE),)

 

 obj-m := hello.o

# Otherwise we were called directly from the command

# line; invoke the kernel build system.

else

 

 KERNELDIR ?= /lib/modules/$(shell uname -r)/build

 PWD := $(shell pwd)

default:

 $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

 

endif

在一个典型的构造过程中,该Makefile将被读取两次(扩展GNU make语法)。当Makefile从命令行调用时,他注意到KERNELRELEASE变量尚未设置。已安装的模块目录中存在一个符号链接,它指向内核的构造树,这样这个Makefile就可以定位内核的源码目录。

如果实际运行的内核并不是要构造的内核,则可以在命令行提供KERNELDIR=选项或者设置环境变量KERNELDIR,也可以修改用来设置KERNELDIR的行。

在找到内核源代码树之后,这个Makefile会调用default:目标,这个目标使用先前描述的方法第二次运行make命令($(MAKE)),以便运行内核构造系统。在第二次读取Makefile文件时,它设置了obj-m,而内核的Makefile负责真正构造模块。

KERNELRELEASE变量实际上是在内核构造树根目录中的Makefile中定义的,这个变量替换成其他在这个Makefile中定义的变量应该也是可以的。

这个Makefile还不够完整,一个真正的Makefile应该包含通常用来清除无用目标文件的目标、安装模块的目标等等。

 

装载和卸载模块

Insmod,将模块代码和数据装入内核,然后使用内核的符号表解析模块中任何未解析的符号。但内核不会修改模块的磁盘文件,而仅仅修改内存中的副本。Insmod可以接受一些命令行选项,并且可以在模块链接到内核之前给模块中的整型和字符串变量赋值。

一个良好设计的模块应该可以在装载时进行配置,这些编译时的配置为用户提供了更多的灵活性。Insmod的工作方式: 它依赖一个在 kernel/module.c 中定义的系统调用. 函数 sys_init_module 分配内核内存来存放模块 ( 这个内存用 vmalloc 分配); 它接着拷贝模块的代码段到这块内存区, 借助内核符号表解决模块中的内核引用, 并且调用模块的初始化函数来启动所有东西。(名子以 sys_ 为前缀. 这对所有系统调用都是成立的, 并且没有别的函数。)

modprobe,相比于insmod,会考虑要装载的模块是否引用了当前内核中不存在的符号。可以使用rmmod工具从内核中移除模块。lsmod列出当前装载到内核中的所有模块,还提供了其他一些信息,lsmod通过读取/proc/modules虚拟文件来获得这些信息。有关当前已装载模块的信息也可以在sysfs虚拟文件系统的sys/module下找到。

 

版本依赖

在缺少modversion的情况下,模块代码必须针对要链接的每个版本的内核进行重新编译。模块和特定内核版本定义的数据结构和函数原型紧密关联。内核不会假定一个给定的模块是针对正确的内核版本构造的。版本检查(linux/version.h,自动包含于linux/module.h中):

UTS_RELEASELINUX_VERSION_CODEKERNEL_VERSION(major,minor,release)。一般而言,依赖于特定版本(或平台)的代码应该隐藏在底层宏或者函数之后。之后高层代码可直接调用。

 

内核符号表

当模块被转入内核后,它所导出的任何符号都会变成内核符号表的一部分。模块层叠,通过层叠技术,可以将模块划分为多个层,通过简化每个层,可缩短开发时间。导出符号:

EXPORT_SYMBOL(name);

EXPORT_SYMBOL_GPL(name);

符号必须在模块文件的全局部分导出,不能再函数中导出,这是因为上面这两个宏将被扩展为一个特殊变量的声明,而该变量必须是全局的。该变量将在模块可执行文件的特殊部分中保存,在装载时,内核通过这个段来寻找模块导出的变量。

 

预备知识

内核是一个特定的环境,对需要和它接口的代码有其自己的一些要求,所有的模块代码中都包含下面两行:

#include

#include

module.h包含有可装载模块需要的大量符号和函数的定义。包含init.h的目的是指定初始化和清除函数。大部分模块还包括moduleparam.h头文件,这样我们就可以在装载模块时向模块传递参数。

尽管不是严格要求,但模块应该指定所使用的许可证。为此,只需要包含MODULE_LICENSE行,如:

MODULE_LICENSE(“GPL”);

内核能够识别的许可证有“GPL”、“GPL v2、“GPL and additional rights”、“Dual BSD/GPL”、"Dual MPL/GPL" "Proprietary"。 除非你的模块明确标识是在内核认识的一个自由许可下, 否则就假定它是私有的, 内核在模块加载时被"弄污浊".

可以在模块中包含的其他描述性定义有 MODULE_AUTHOR ( 声明谁编写了模块 ), MODULE_DESCRIPION( 一个人可读的关于模块做什么的声明 ), MODULE_VERSION ( 一个代码修订版本号; 的注释以便知道创建版本字串使用的惯例), MODULE_ALIAS ( 模块为人所知的另一个名子 ), 以及MODULE_DEVICE_TABLE ( 来告知用户空间, 模块支持那些设备 )

上述MODULE_声明可出现在源文件中源代码函数中以外的任何地方。但新近的内核编码习惯是将这些声明放在文件的最后。

 

初始化和关闭

模块的初始化函数负责注册模块所提供的任何设施。这里的设施指的是一个可以被应用程序访问的新功能,它可能是一个完整的驱动程序或者仅仅是一个新的软件抽象。初始化函数通常定义为:

static int __init initialization_function(void)

{

……

}

module_init(initialization_function);

module_init的使用是强制性的。这个宏会在模块的目标代码中增加一个特殊的段,用于说明内核初始化函数所在的位置。

模块可以注册许多不同类型的设施,对每种设施,对应有具体的内核函数用来完成注册。传到内核注册函数中的参数通常是指向用来描述新设施及设施名称的数据结构指针,而数据结构通常包含指向模块函数的指针,这样,模块体中的函数就会在恰当的时间被内核调用。

清除函数

每个重要的模块都需要一个清除函数,该函数在模块被移除前注销接口并向系统中返回所有资源。通常定义如下:

static void __exit cleanup_function(void)

{

……

}

module_exit(cleanup_function);

__exit修饰词标记该代码仅用于模块卸载(编译器将把该函数放在特殊的ELF段中)。如果模块被直接内嵌到内核中,或者内核的配置不允许卸载模块,则被标记为__exit的函数将被简单的丢弃。module_exit声明对于帮助内核找到模块的清除函数是必须的。如果一个模块未定义清除函数,则内核不允许卸载该模块。

 

初始化过程中的错误处理

当我们在内核注册设施时,要时刻铭记注册可能会失败。因此模块代码必须始终检查返回值,并确保所请求的操作已真正成功。

如果在注册设施时遇到任何错误,首先要判断模块是否可以继续初始化。通常在某个注册失败后可以通过降低功能来继续运转。因此,只要可能,模块应该继续向前并尽可能提供其功能。

如果发生了某个特定类型的错误之后无法继续转载模块,则要将出错之前的任何工作撤销掉。Linux中没有记录每个模块都注册了哪些设施,因此,当模块的初始化出现错误之后,模块必须自行撤销已注册的设施。错误处理的时候使用goto语句还是比较有效的。

另一种观点不支持goto的使用,而是记录任何成功注册的设施,然后在出错的时候调用模块的清除函数。清除函数将仅仅回流已完成的步骤。出错时应返回错误码,在Linux内核中,错误编码是定义在中的负整数。每次(并不仅限于初始化函数)返回合适的错误编码是一个好习惯,因为用户程序可以通过perror函数或类似的途径将它们转换为有意义的字符串。

模块的清除函数需要撤销初始化函数所处侧的所有设施,并且习惯上以相反于注册的顺序撤销设施。

如果初始化和清除工作涉及很多设施,则goto方法可能变得难以管理,因为所有用于清除设施的代码在初始化函数中重复,同时一些标号交织在一起。因此,有时候需要重新构思代码结构。

每当发生错误是,从初始化函数中调用清除函数,这种方法将减少代码的重复,并且使代码更清晰更有条理。当然清除函数必须在撤销每项设施的注册之前检查它的状态。根据调用的注册/分配函数的语义,可以使用或不适用外部标识来标记每个初始化步骤的成功。

因为清除函数被非退出代码调用,因此不能将清除函数标记为__exit

 

模块装载竞争

在注册完成之后,内核的某些部分可能会立即使用我们刚刚注册的任何设施。因此,在首次完成之后,代码就应该准备好被内核的其他部分调用;在用来支持某个设施的所有内部初始化完成之前,不要注册任何设施。我们还必须考虑,当初始化失败而内核的某些部分已经使用了模块所注册的某个设施时应该如何处理。

 

模块参数

参数必须使用 moudle_param 宏定义来声明, 这个宏定义在 moduleparam.h. module_param 需要3个参数: 变量名,类型,以及一个用于sysfs入口项的访问许可掩码。这个宏必须放在任何函数之外,通常是在源文件的头部。这些参数的值可在运行insmodmodprobe命令装载模块时赋值,而modprobe还可以从它的配置文件中(/etc/modprobe.conf)读取参数值。

如:insmod hellop howmany=10 whom="Mom"

模块参数支持许多类型:

Boolinvbool —— 布尔型( true 或者 false)值,相关的变量应当是 int 类型, invbool反转其值, 即真值变成false, 反之亦然。

charp ——字符指针值,内核会为用户提供的字串分配内存, 并相应设置指针。

Intlongshortuintulongushor —— 基本的变长整型值。以u开头的是无符号值。

模块装载器也支持数组参数,在提供数值是用逗号划分个数组成员。声明一个数组参数,使用:

module_param_array(name,type,num,perm);

这里 name 是数组的名字(也是参数名), type是数组元素的类型, num是一个整型变量, perm是通常的权限值. 如果数组参数在加载时设置, num被设置成提供的元素的个数. 模块加载器会拒绝接受超过数组大小的值。

如果确实需要一个没有出现在上面列表中的类型,模块代码中的钩子会允许我们来定义这些类型(详见 moduleparam.h)。

访问许可值,应当使用 中定义的值。这个值用来控制谁可以存取这些模块参数在 sysfs 中的表示。如果一个参数通过sysfs而被修改, 则如同模块修改了这个参数一样,但是模块没有得到任何内核给的通知。大多数情况下,我们不应该让模块参数是可写的。

 

用户空间编写驱动程序

通常,用户空间的驱动程序被实现为一个服务器进程,其任务是替代内核作为硬件控制的唯一代理。有一种情况适合在用户空间处理,这就是当我们准备处理一种新的不常见的硬件时。

 

总结:

1、内核模块编写的基本框架,两个头文件;初始化函数和清理函数。

2、内核模块编程还可以使用的一些东西,模块参数,描述性定义MODULE_

3、内核编程的一些特点和基本的规则。


阅读(1973) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~