您的位置:首页 > 运维架构 > Linux

《Linux设备驱动程序》——构造和运行模块

2014-10-30 20:57 155 查看
一、设置测试系统

1、学习驱动程序的编写,选择标准内核。

2、配置和构造内核树。

二、Hello World模块

1、Hello World模块解析

1)、module_init把模块装载到内核,module_exit把模块从内核中移除。

2)、函数printk在Linux内核中定义,功能和标准C库中的函数printf类似。

3)、字符串KERN_ALERT定义了此信息的优先级。

4)、insmod用来加载模块,rmmod用来卸载模块。

2、编写一个模块并不困难,真正的困难在于理解设备并最大化其性能。

三、核心模块和应用程序的对比

1、执行的不同

1)、大多数小规模及中规模应用程序是从头到尾执行单个任务。

2)、模块却只是预先注册自己以便服务于未来的某个请求,然后它的初始化函数就立即结束。

2、资源释放或清除工作不同

1)、应用程序在退出时,可以不管资源的释放或其他的清除工作。

2)、但模块的退出函数却必须仔细撤销初始化函数所做的一切,否则,在系统重新引导之前某些东西就会残留在系统。

3、调用函数的不同

1)、应用程序可以调用它未定义的函数,因为链接过程能够解析外部引用从而使用适当的函数库。

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

I、因没有任何函数库会和模块链接,所以,源文件中不能包含通常的头文件。

II、和内核相关的任何内容都在我们安装并配置好的内核源代码数的头文件中声明,其中,大多数头文件都保存在include/linux和include/asm目录中。

4、处理错误方式的不同

1)、应用程序开发过程中的段错误是无害的,并且总可以使用调试器跟踪带源代码中的问题所在。

2)、而模块错误即使不影响整个系统,也至少会杀死当前进程。

5、用户空间和内核空间

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

2)、操作系统的作用

I、为应用程序提供一个对计算机硬件的一致视图。

II、负责程序的独立操作并保护资源不受非法访问,此任务只有在CPU能够保护系统软件不受应用程序破坏才能完成。

III、所有的现代处理器都具备此功能,方法是在CPU中实现不同的操作模式(或者级别)。不同级别具有不同功能,在较低的级别中将禁止某些操作。

3)、在unix中,内核运行在最高级别(也称作超级用户态),在这个级别中可以进行所有的操作。而应用程序运行在最低级别(即所谓的用户态)。通常将运行模式称为内

核空间和用户空间。

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

II、通常来讲,一个驱动程序要执行先前讲诉过的两类任务:模块中的某些函数作为系统条哦一年的一部分而执行,而其他函数则负责中断处理。

6、内核中的并发

1)、大部分应用程序,除了多线程应用程序之外,通常是顺序执行的,从头到尾,而不需要关心因为其他一些事情的发生会改变它们的运行环境。而内核代码在编写时铭

记:同一时刻,可能会有许多事情正在发生。

2)、内核编程考虑并发问题的原因

I、Linux系统中通常正在运行多个并发进程,并且可能有多个进程同时使用同一驱动程序。

II、大多数设备能够中断处理器,而中断处理程序异步运行,而且可能在驱动程序正试图处理其他任务时被调用。

III、某些软件业抽象运行。

IV、Linux可以运行在对称多处理器(SMP)。

V、2.6内核艾玛是可抢占的,意味在单处理器系统上也存在并发问题。

3)、常见错误:认为只要某段代码没有进入睡眠状态(或者堵塞),就不会产生并发问题。

7、当前进程

1)、内核执行的大多数操作和某个特定的进程相关。

2)、内核代码可以访问全局项current来获得当前进程。

I、current在<asm.current.h>中定义,是一个指向struct task_struct的指针,而task_struct结构在<linux/sched.h>文件中定义。

II、current指针指向当前正在运行的进程。

3)、不依赖特定架构的机制通常是:将指向task_struct结构的指针隐藏在内核栈中。此实现的细节同样也对其他内核子系统隐藏,设备驱动程序只需要包含<linux/sched.h>

头文件即可引用当前进程。

8、其他一些细节

1)、应用程序在虚拟内存中布局,并具有一块很大的栈空间。而内核具有非常小的栈,它可能和一个4096字节大小的页那样小。

2)、内核API具有两个下划线(_ _)的函数名称。具有这种名称的函数通常是接口的底层组件,应谨慎使用。

3)、内核代码不能实现浮点运算。

四、编译和装载

1、构建内核模块注意事项

1)、应确保具备了正确版本的编译器,模块工具和其它必要的工具。内核文档目录中的Documentational/Changes文件列出了需要的工具版本;在构造内核模块之前,应该

检查该文件并已安装了正确的工具。

2)、与使用老工具一样,使用太新的工具也偶尔会导致问题;内核源代码对编译器做了大量假定,因此新的编译器版本可能导致问题额出现。

3)、应该先准备内核数并配置和构造内核。

4)、最好运行和模块对应的内核。

2、为模块创建makefile文件

1)、obj -m :=hello.o

I、以上赋值语句说明有一个模块需要从目标文件hello.o中构造,而从该目标中构造的模块名为hello.ko。

2)、如果要构造的模块名为module.ko,并且两个源文件生成(比如file1.c和file2.c),则正确的makefile可如下编写:

obj -m :=module.o

module -objs := file1.o file2.o

I、为了让以上两种类型的makefile文件正常工作,必须在大的内核构造系统环境中调用它们。如内核代码数保存在~/kernel-2.6目录中,则用来构造模块的make命令应是:

make -C ~/kernel-2.6 M= `pwd` modiles

首先改变目录到-C选项指定的位置(即内核源代码目录),其中保存有内核的顶层makefile文件。

M=选项让该makefile在构造modules目标之前返回到模块源代码目录。

modules目标指向obj -m变量中设定的模块。

3)、扩展GNU make语法。

I、真正的makefile应该包括通常用来清除无用文件的目标、安装模块的目标等等。

3、编译和卸载模块

1)、insmod可将模块装入内核。还可以接受一些命令行选项,且可以在模块链接到内核之前给模块中的整型和字符串变量赋值。

2)、modprode工具也用来将模块装载到内核中。与insmod区别:

I、modprode会考虑要装载的模块是否引用了一些当前内核不存在的符号。如果有这类引用modprode会在当前模块搜索路径中查找定义了这些符号的其他模块。如果找到

这些模块,它会将这些模块装载到内核。

II、如果此时使用insmod,则该命令会失败,并在日志文件中记录"unresolved symbols(未解析的符号)"信息。

3)、可使用rmmod工具从内核中移除模块。

I、如果内核认为模块仍然在使用状态,或者内核被配置为禁止移除模块,则无法移除模块。

II、配置内核并使得内核在模块忙的时候仍能“强制”移除模块也是可能的。

4)、lsmod程序列出当前装载到内核中的所有模块还提供了其他一些信息。

4、版本依赖

1)、内核不会假定一个给定的模块是针对正确的内核版本构成的。如果要为某个特定的内核版本编译模块,则需要该特定版本的构造系统和源代码树。

2)、如果打算编写一个能够和多个内核版本一起工作的模块,则必须使用宏以及#ifdef来构造并编译自己的代码。可使用linux/version.h中的相关定义,这个头文件包含于

linux/module.h,并定义了如下宏:

I、UTS_RELEASE:扩展为一个描述内核版本的字符串。如“2.6.10”.

II、LINUX_VERSION_CODE:扩展为内核版本的二进制表示,版本发行号中的每一部分对应一个字节。

III、KERNEL_VERSION(maior, minor, release):以组成版本号的三部分为参数,创建整数的版本。

3)、通过检查KERNEL_VERSION和LINUX_VERSION_CODE而使用预处理条件,能够解决大部分基于内核版本的依赖性问题。

4)、一般而言,依赖于特定版本(或平台)的代码应该隐藏在底层宏或函数之后。然后,高层代码可直接调用这些函数,而无关注底层细节。

5、平台依赖

1)、内核开发人员可以根据不同的需求而将某些寄存器指定特定用途。且内核代码可以针对某个CPU家族的某种特定处理器进行优化,从而充分利用目标平台的特性。

2)、如果模块和特定的内核工作,它必须和内核一样了解目标处理器。

3)、如打算编写一个驱动程序用于一般性发布,则最好考虑好如何支持可能的不同处理器变种。

I、用GPL兼容许可证来发布自己的驱动程序,并将其贡献给内核主分支。

II、以源代码形式以及一组用于编译的脚本发布自己的驱动程序则是最好的办法。

五、内核符号表

1、公用内核符号表中包含了所有的全局内核项(即函数和变量)的地址,这是实现模块化驱动程序所必须的。

1)、当模块被装入内核后,它所导向的任何符号都会变成内核符号表的一部分。

2)、在通常情况下,模块只需实现自己的功能,而无须导出任何符号。但是,如果其他模块需要从某个模块中获得喊出,可以导出符号。

2、新模块可以使用由我们自己的模块导出的符号,这样,就可以在其他模块上层叠新的模块。模块层叠技术也是用在许多主流的内核代码中。

1)、此在复杂的项目中非常有用。如果以设备驱动程序额新式实现一个新的软件抽象,则可认为硬件相关的实现提供了一个“插头”。

3、modprode是处理层叠模块的一个实用工具。它的功能在很大程度上和insmod类似,但modprode除了装入指定的模块外还同时装入指定模块所依赖的其他模块。

1)、一个modprode命令有时候相当于调用几次insmod命令。

2)、在从当前目录中装入自己的模块仍然需要使用insmod,因为modprode只能从标准的已安装模块中搜索需要装入的模块。

4、一个模块向其他模块导出符号,则应该使用下面的宏:

EXPORT_SYMBOL(name);

EXPORT_SYMBOL_GPL(name);

I、这两个宏均用于将指定的符号导出到模块外部。

II、_GPL_版本使得要导出的模块只能被GPL许可证下的模块使用。

III、符号必须在模块文件额全局部分导出,不能再函数中导出。

六、预备知识

1、必须出现在每个可装载的模块中的头文件:

#include<linux/module.h>

#include<linux/init.h>

I、module.h包含了可装载模块需要的大量符号和函数的定义。

II、init.h指定初始化和清除函数。

III、大部分模块还包括moduleparam.h头文件,这样就可以在装载模块时向模块传递参数。

2、模块应该指定代码许可证:

MODULE_LICENSE(“GPL”);

I、内核能够识别许可证有GPL、GPL v2、GPL and additional rights、Dual BSD/GPL、Dual MPL/GPL以及Proprietary(专有)。

II、如果一个模块没有显式地标记为上述内核可识别的许可证,则会被认为是专有的,而内核装载这种模块就会被污染。

3、可在模块中包括的其他描述性定义包括:

I、MODULE_AUTHOR(描述模块作者)

II、MODULE_DESCRIPTION(用来说明模块用途的简短描述)

III、MODULE_NERSION(代码修订好,有关版本字符串的创建惯例)

IV、MODULE_AILAS(模块的别名)

V、MODULE_DEVICE_TABLE(用来告诉用户空间模块所支持的设备)

七、初始化和关闭

1、模块初始化函数

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

static int _ _init initiallization_function(void)

{

/*初始化代码*/

}

module_init(initialzation_function);

I、初始化函数应该被声明为static,因为这种函数在特定文件之外没有其他的意义。

II、_ _init表明该函数仅在初始化期间使用。在该模块被装载之后,模块装载器就会将初始化函数扔掉,这样就可以将该函数占用的内存释放出来。

注意:不要再初始化之后仍要使用的函数(或数据结构)上使用_ _init和_ _initdata这两个标识。

III、在内核源代码中可能还会遇到_ _devinit和_ _devinitdata,只有在内核未被设置为支持热插拔设备下,这两个标记才会被翻译为_ _init和_ _initdata。

IV、module_init的使用是强制性的此宏会在模块的目标代码中增加一个特殊的段,用于说明内核初始化所在位置。无此定义,初始化函数永远不会被调用。

2、模块注册的设施

1)、模块可以注册许多不同类型的设施,包括不同类型的设备、文件系统、密码变换等。

2)、对每种设施,对应有具体的内核函数用来完成注册。

3)、传递到内核注册函数中的参数通常是指向用来描述新设施及设备名称的数据结构指针,而数据结构指针通常包含指向模块函数的指针,因此,模块中的函数就会在恰当

的时间被内核调用。

4)、能够注册的设备包括串口、杂项设备、sysfs入口、/proc文件、可执行域以及线程规程。很多可注册的设备所支持的功能属于“软件抽象”范畴,而不与任何硬件直接相

关。

5)、其他一些设备可以注册为特定驱动程序的附加功能,但它们用途有限,如层叠模块技术。

6)、大部分注册函数名字带有register_前缀。

3、清除函数

1)、每个重要的模块都需要一个清除函数,该函数在模块被移除前注销接口并向系统中返回所有资源。

static void _ _exit cleanup_function(void)

{

/*清除代码*/

}

module_exit(cleanup_function);

I、清除函数没有返回值,所以声明为void。

II、_ _exit修饰词标记该代码用于模块卸载(编译器将把该函数放在特殊的ELF段中)。被标记为_ _exit的函数只能在模块被卸载或者系统关闭时被调用,其他任何用法都

是错误的。

III、module_exit声明对于帮助内核找到模块的清除函数是必须的。

IV、如果一个模块未定义清除函数,则内核不允许卸载该函数。

4、初始化过程中的错误处理

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

能提供其功能。

2)、如果发生了某个特定类型的错误之后无法继续装载模块,则要将错误之前的任何注册工作撤销掉。如果未能撤销已注册的设施,则内核会处于一种不稳定的状态。在此

情况下,唯一有效的决解办法是重新引导系统。

3)、错误情况下的goto的仔细使用可避免大量复杂的、高度缩进的“结构化”逻辑。另一种不支持goto的使用,二十记录任何成功注册的设施,然后再出错的时候调用模块的

函数。清除函数仅仅回滚已成功完成的步骤。

4)、在Linux内核中,错误编码是定义在<linux/errno.h>中的负整数。如果我们不想使用其他函数返回额度错误编码,而使用自己的错误编码,则应该包含<linux/errno.h>。

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

I、当初始化和清除工作涉及很多设施时,goto方法可能变得难以管理。因此,需要考虑重新构思代码的结构。

II、每次发生错误时从初始化函数中调用清除函数,此方法减少代码的重复并使代码更清晰、更有条理。当然,清除函数必须在撤销每项设施的注册之前检查他的状态。

5、模块装载竞争

1)、首先需要注意的是:在注册完之后,内核的某些部分可能会立即使用刚刚注册的任何设施。

I、在首次注册完之后,代码就应该准备好被内核的其他部分调用。

II、在用来支持某个设施的所有内部初始化完成之前,不需要注册任何设备。

2)、当初始化失败而内核的某些部分已经使用了模块所注册的某个设施时如何处理:

I、如果发生在模块上,则根本不应该出现初始化失败的情况,必定模块已经成功导出了可用的功能及符号。

II、如初始化一定要失败,则应该仔细处理内核其他部分正在进行的操作,并且要等待这些操作的完成。

八、模块参数

1、由于系统不同,驱动程序需要的参数也许会发生变化,包括涉笔编号以及其他一些用来控制驱动程序操作方式的参数。内核允许对驱动程序指定参数,而这些参数可在装载

程序模块时改变。

1)、参数的值可在运行insmod或modprobe命令装载模块时赋值,而modprobe还可以从其配置文件(/etc/modprobe.conf)中读取参数。

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

2、参数必须用module_param宏来声明,此宏在moduleparam.h中定义。

1)、module_param需要三个参数:变量的名称、类型以及用于sysfs入口项的访问许可掩码。

2)、此宏必须放在任何函数之外,通常是在源文件的头部。

3、内核支持的模块参数

1)、bool、invbool:布尔值(取true或false),关联的变量应该是int类型。invbool类型反转其值,也就是说true变成false,false变成true。

2)、charp:字符指针值。内核回味用户提供的字符串分配内存,并相应设置指针。

3)、int、long、short、uint、ulong、ushort:具有不同长度的基本类型。以u开头的用于无符号值。

4、模块装载器支持数组参数,在提供数组值时用逗号划分各数组成员。要声明数组参数,则要使用宏:

module_param_arry(name, type, num.perm);

I、name是树组的名称(也就是参数的名称)。

II、type是数组元素的类型。

III、num是一个整数变量,而perm是常见的访问许可值。

5、如果以上没有我们想要的参数,则可以用模块代码中的钩子定义这些类型。具体的实现参阅moduleparam.h文件。

1)、所有的模块都应该指定一个默认值;insmod只会在用户明确设置了参数的值的情况下才会改变参数的值。

2)、模块可以根据默认值来判断是否是一个显式指定的参数。

6、module_param最后一个成员是访问许可值。应该使用<linux/stat.h>中存在的定义。此值能控制谁能够访问sysfs中队模块参数的表述。

1)、如果perm设置为0你就不会有对应的sysfs入口项;否则,模块参数会在/sys/module中出现,并设置为给定的访问许可。

2)、如果对参数使用S_IRUGO,则任何人均可读取该参数,但不能修改;S_IRUGOS_IWUSR允许root用户修改该参数。

3)、如果一个参数通过sysfs而被修改,则如同模块修改了此数值一样,但内核不会以任何方式通知模块。

九、在用户空间编写驱动程序

1、用户空间驱动程序的优点。

2、通常,用户空间驱动程序被实现为一个服务进程,起任务是替代内核作为硬件控制的唯一代理。客户应用程序可连接到该服务器并和设备执行实际的通信。

3、用户空间驱动程序的缺点。

4、准备处理一种新的,不常见的硬件时,可以用用户空间处理。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: