LINUX设备驱动程序的注意事项(两)建设和执行模块

         <一>:设置測试系统

首先准备好一个内核源代码树,构造一个新内核,然后安装到自己的系统中。

          <二>:HelloWorld模块

#include <linux/init.h> //定义了驱动的初始化和退出相关的函数
#include <linux/module.h> //定义了内核模块相关的函数、变量及宏 MODULE_LICENSE("Dual BSD/GPL"); //该宏告诉内核,该模块採用*许可证 static int hello_init(void) //初始化函数仅在模块载入时调用
{
printk(KERN_ALERT"Helloworld\n");
return 0;
} static void hello_exit(void) //卸载函数仅在模块卸载时调用
{
printk(KERN_ALERT"Goodbye,cruelworld\n");
} module_init(hello_init);
module_exit(hello_exit);

    <三>:核心模块与应用程序的对照

1.内核模块与应用程序之间的种种不同:

a.大多数小规模及中规模应用程序是从头到尾运行单个任务。而模块却仅仅是预先注冊自己以便服务于将来的某个请求。然后它的初始化函数就马上结束。

b.事件驱动的应用程序和内核代码之间的还有一个不同是:应用程序在退出时。能够无论资源的释放或者其它的清除工作,但模块的退出函数却必须细致撤销初始化函数所做的一切,否则。在系统又一次引导之前某些东西就会残留在系统中。

c.应用程序可以调用它并没有定义的函数,这是由于连接过程可以解析外部引用从而使用适当的函数库。而模块只被链接到内核,因此它能调用的函数不过内核导出的那些函数。而不存在任何可链接的函数库。

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

2.用户空间和内核空间

模块运行在所谓的内核空间里,而应用程序运行在所谓的用户空间里。

操作系统的作用是为应用程序提供一个对计算机硬件的一致视图。同一时候操作系统必须负责程序的独立操作并保护资源不受非法訪问。这个任务仅仅有在CPU可以保护系统软件不受应用程序破换时才干完毕。人们选择的方法是在CPU中实现不同操作模式(或者级别)。每当应用程序运行系统调用或者被硬件中断挂起时,Unix将运行模式从用户空间切换到内核空间。

模块化代码在内核空间中运行。用于扩展内核的功能。

通常一个驱动程序要运行两个任务:模块中的某些函数作为系统调用的一部分而运行。而其它函数则负责中断处理。

3.内核中的并发

有几方面的原因促使内核编程必须考虑并发问题:

a.Linux系统中通常正执行多个并发进程。而且可能有多个进程同一时候使用我们的驱动程序。

b.大多数设备可以中断处理器。而中断处理程序异步执行,并且可能在驱动程序正试图处理其它任务时被调用

c.有一些软件抽象也在异步执行着。

d.Linux还能够执行在对称多处理器系统上。因此可能同一时候有不止一个CPU执行我们的驱动程序。

e.在2.6中内核代码已经是抢占式的,这意味着即使在单处理器系统上也存在很多类似多处理器系统的并发问题。

4.当前进程

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

current在<asm/current.h>中定义。是一个指向struct task_struct的指针,而task_struct结构在<linux/sched.h>文件里定义。current指针指向当前正执行的进程。在open/read等系统调用的执行过程中。当前进程指的是调用这些系统调用的进程。

假设须要。内核代码能够通过current获得与当前进程相关的信息。

设备驱动程序仅仅要包括<linux/sched.h>头文件就可以引用当前进程。比如,以下的语句通过訪问struct task_struct

的某些成员来打印当前进程的ID和命令名:

printk(KERN_INFO"The process is\"%s\"(pid%i)\n,current->comm,current->pid);

5.其它一些细节

应用程序在虚拟内存中布局,并具有一块非常大的栈空间。当然,栈是用来保存函数调用历史以及当前活动函数中的自己主动变量的。而相反的是,内核具有非常小的栈,它可能仅仅和一个4096字节大小的页那样小。

常常会在内核API中看到具有两个下划线前缀(__)的函数名称。

具有这样的名称的函数一般是接口的底层组件。应该慎重使用。

       <四>:编译和装载

1.编译模块

首先来看看模块时怎样构造的,详细细节參考内核源码中Documentation/kbuild文件夹下的文件。

在构造内核模块前。应确保具有正确版本号的编译器、模块工具盒其它必要的工具。

内核文档文件夹中Documentation/Changes文件列出了须要的工具版本号。makefile里的一些规则:

假设要构造的模块名称为module.ko。并由两个源文件生成(比方file1.c和file2.c),则正确的makefile可例如以下编写:

obj-m := module.o
module-objs := file1.o file2.o

2.装载和卸载模块

装载模块一般使用insmod程序,它和ld有些类似,它将模块的代码和数据装入内核,然后使用内核的符号表解析模块中不论什么为解析的符号。

然而,与链接器不同,内核不会改动模块的磁盘文件,而只改动内存中的副本。insmod能够接受一些命令行选项,而且能够在模块链接到内核之前给模块中的整型和字符串型变量赋值。

与insmod类似的是modprobe工具,它不仅装载该模块。还装载该模块所一栏的模块。

卸载模块用rmmod工具。可从内核中移除模块。假设内核觉得模块仍然在使用状态。或者内核被配置为禁止移除模块,则无法移除该模块。

lsmod程序列出当前装载到内核中的全部模块,还提供了其它一些信息,比方其它模块是不是在使用某个特定模块等。

3.版本号依赖

4.平台依赖

      <五>:内核符号表

insmod使用公共内核符号表来解析模块中没有定义的符号。公共内核符号表中包括了全部全局内核项的地址,这是实现模块驱动程序所必须的。当模块被装入内核后。它所导出的不论什么符号都会变成内核符号表的一部分。

新模块能够使用由我们自己的模块导出的符号,这样。我们能够在其它模块上层叠新的模块。

modprobe是处理层叠模块的一个使用工具。它的功能在非常大程度上和insmod类似,可是它除了装入指定模块外还同一时候装入指定模块所依赖的其它模块。通过层叠技术。我们能够将模块划分为多个层,通过简化每一个层可缩短开发时间。

Linux内核头文件提供了一个方便的方法来管理符号对模块外部的可见性。从而降低了可能造成的名字空间污染。而且适当隐藏信息。假设一个模块须要向其它模块导出符号,则应该使用以下的宏。

EXPORT_SYMBOL(name);

EXPORT_SYMBOL_GPL(name);

GPL版本号使得要导出的模块仅仅能仅仅能被GPL许可证下的模块使用。

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

       <六>:预备知识

大部分内核代码中都要包括相当数量的头文件,以便获得函数、数据类型和变量的定义。有几个头文件时专门用于模块的,因此必须在出如今每一个可装载的模块中。

因此。全部的模块代码中都包括以下两行代码:

#include <linux/module.h>  /*module.h包括有可装载模块须要的大量符号和函数的定义*/

#include <linux/init.h>    /*包括init.h的目的是指定初始化和清除函数*/

大部分模块还包含moduleparam.h头文件,这样就能够在装载模块时向模块传递參数。

MODULE_LICENSE("GPL");内核可以识别的该许可证。

假设一个模块没有显示地标记为上述内核可识别的许可证,则会假定是专有的。而内核装载这样的模块就会被“污染”。

可在模块中包括的其它描写叙述性定义为包括MODULE_AUTHOR(描写叙述模块作者)、MODULE_DESCRIPTION(用来说明模块用途的简短描写叙述)、MODULE_VERSION(代码修订号)等。

         <七>:初始化和关闭

1.模块的初始化函数负责注冊模块所提供的不论什么设施。

这里的设施指的是一个能够被应用程序訪问的新功能,它可能是一个完整的驱动程序或者不过一个新的软件抽象。初始化函数的实际定义通常例如以下所看到的:

static int __init initialization_function(void)

{

/*这里时初始化代码*/

}

module_init(initialization_function);

初始化函数应该被声明为static,由于这样的函数在特定文件之外没有其它意义。__init标记对内核来讲是一种暗示,表明该函数仅在初始化期间使用。

在模块被装载之后,模块装载器就会将初始化函数扔掉,这样可将该函数占用的内存释放出来。以作他用注意,不要在结束初始化之后仍要使用的函数上使用这两个标记。

module_initd的使用是强制的。这个宏在模块的目标代码中添加一个特殊的段。用于说明内核初始化函数所在的位置。没有这个定义,初始化函数永远不会被调用。模块能够注冊很多不同类型的设施。包含不同类型的设备、文件系统、password交换等。对于每种设施,相应有详细的内核函数用来完毕注冊。大部分注冊函数名字带有register_前缀。

2.清除函数

每一个重要的模块都须要一个清除函数,该函数在模块被移除前注销接口并向系统中返回全部资源。该函数定义例如以下:

static void __exit cleanup_function(void)

{

/*这里是清除代码*/

}

module_exit(cleanup_function);

__exit修饰词标记该代码仅用于模块卸载(编译器将把该函数放在特殊的ELF段中)。假设模块被直接内嵌到内核中,或者内核的配置不同意卸载模块,则被标记为__exit的函数将被简单地丢弃。

出于以上原因,被标记为__exit的函数仅仅能在模块被卸载或者系统关闭时被调用。其它的不论什么使用方法都是错误的。module_exit声明为对于帮助内核找到模块的清除函数式必须的。

假设一个模块没有定义清除函数。则内核不同意卸载该模块。

3.初始化过程中的错误处理

4.模块装载竞争

a.在注冊完毕之后,内核的某些部分可能会马上使用我们刚刚注冊的不论什么设施。即。在初始化函数还在执行的时候。内核就全然可能会调用我们的模块。因此,在首次注冊完毕之后。代码就应该准备好被内核的其它部分调用;在用来支持某个设施的全部内部初始化完毕之前。不要注冊不论什么设施。

b.当初始化失败而内核的某些部分已经使用了模块所注冊的某个设施时应该怎样处理。

假设这样的情况可能发生在我们的模块上,则根本不应该出现初始化失败的情况,毕竟模块已经成功导出了可用的功能及符号。假设初始化一定要失败。则应该细致处理内核其它部分正在进行的操作,而且要等待这些操作的完毕。

       <八>:模块參数

因为系统的不同,驱动程序须要的參数或许会发生变化。

这包含设备编号以及其它一些用来控制驱动程序操作方式的參数。内核同意对驱动程序指定參数,而这些參数可在装载驱动程序模块时改变。

这些參数的值可在执行insmod或modprobe命令装载模块时赋值,而modprobe还能够从它的配置文件(/etc/modprob.conf)中读取參数值。

在insmod改变模块參数之前,模块必须让这些參数对insmod命令可见。

參数必须使用module_param宏来声明,这个宏在moduleparam.h中定义。module_param须要三个參数:变量的名称、类型以及用于sysfs入口项的訪问许可掩码。这个宏必须放在不论什么函数之外,一般是在源文件的头部。比如:

static char *whom = "world";

static int howmany = 1;

module_param(howmany, int, S_IRUGO);

module_param(whom, charp, S_IRUGO);

内核支持的模块參数类型有: bool invbool charp int long short uint ulongushort

模块装载器也支持数组參数,在提供数组值时用逗号划分个数组成员。要声明数组參数,须要使用以下的宏:

module_param_arry(name, type, num, perm);

当中name是数组名称。type是数组元素类型,num是一个整型变量。而perm它是一种常见的访问许可证值。

版权声明:本文博主原创文章,博客,未经同意不得转载。

上一篇:eclipse插件maven的使用,web打包成WAR,tomcat下直接运行


下一篇:C#操作Access的一些小结