金钟国宋智孝粉红:linux驱动入门(转)

来源:百度文库 编辑:偶看新闻 时间:2024/04/28 20:28:30

linux驱动入门(转)

用来防止用户程序直接访问内核中关键性数据结构和硬件设备是驱动程序的主要目的之一,所以,一个设计精良的驱动通常应该隐藏了硬件的复杂性和多变性。例如,一个程序写数据到磁盘时,只需要打开一个文件并执行写命令即可,而不必了解磁盘中的每个扇区的大小是512字节或者是1024字节,细节性的问题则交由驱动程序处理。此外,驱动程序还可以隐藏硬件的多变性(可能来自于不同厂家,甚至同一厂家不同型号)并给用户提供一个统一的访问接口。这也是Unix/Linux信条之一“一切皆文件”的赖以存在的基础。1、可加载模块(loadable module)Linux采取了“宏内核”的结构,并附带一个设计精良的接口以实现在系统运行时动态的加载或移除驱动模块。这种富有弹性的设计,为最终用户,甚至也为程序的开发过程带来了极大的便利。基于此功能,在开发驱动的过程中,开发人员不用在每一次驱动程序的修改后重启系统就能对其进行测试。当然,驱动程序也可以以静态方式编译进内核,而且,许多关键性的驱动也需要这样编译。比如,对于无盘工作站而言,由于系统启动最初就需要通过网卡从其它共享文件系统中加载所需的资源,此时必须将网卡驱动静态编译进内核,因为可加载模块都是系统启动后才进行动态加载。通常使用启动脚本装载动态驱动模块,当然,也可以使用相关命令在需要时再进行加载。此外,内核也可以在某个服务需要某个特殊模块时自动请求加载所需的模块。虽然前文中一直称驱动程序为可加载模块,但内核模块并没有确定的术语,硬件驱动(device drivers)、可加载内核模块(LKM,loadable kernel modules)、内核模块(kernel modules)、可加载模块(loadable modules)、驱动模块(driver modules)和模块(modules)等都常用来表示可动态加载进内核的硬件驱动,后文中则不加区别的使用它们。2、硬件驱动结构尽管Linux/Unix驱动程序模块的开发一直处于不断地演进中,但其基本结构并没有太大改变。硬件设备大体上可分为两大类:字符设备和块设备。
  • 字符设备是以串行流式数据序列进行数据存取的设备,字符设备驱动负责实现这种行为;通常字符设备驱动至少需要实现 open、 close、read和 write等系统调用。常见的字符设备如控制台( /dev/console )和串口( /dev/ttyS0 )。
  • 块设备通常是可编址的,其数据存取也通常以固定大小的数据块进行,但数据块的存入位置则可能是随机的。在大部分Unix系统中, 一个块设备传送一个或多个长度经常是512字节(或者其2次幂倍)的整块数据,但Linux允许一次传递任意字节的数据,其跟字符设备的区别仅在于内核内部对数据的管理方式上和驱动程序的接口实现上有所不同。磁盘是常见的块设备。
3、一个驱动程序的例子因为Linux支持可加载式硬件驱动,所以很容易构建出一个关于简易驱动框架来说明驱动程序的结构。下面就是这样一个关于字符型设备驱动程序的例子:

/* Example Minimal Character Device Driver */
#include
static int __init hello_init(void)
{
printk("Hello Device Driver World!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk("Goodbye, Cruel World!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_AUTHOR("Marion");
MODULE_DESCRIPTION("An example of device driver!");
MODULE_LICENSE("GPL");
/* End the hello*/

上面这个例子虽然短小,但却充分具备了让内核加载或卸载以及初始化或退出的程序结构。不同于标准的二进制可执行程序,设备驱动是一种特殊的二进制模块,它不能简单的通过shell执行。2.6系列内核的模块通常是内核对象 (kernel object)格式,这些模块在完成编译后通常以.ko为后缀。创建内核对象步骤和编译选项非常复杂,本文仅描述其大体过程,以帮助读者简单了解内核对象的构建步骤。4、模块构建基础驱动程序的编译必须针对于特定的内核进行。尽管在另一个不同的内核上编译的驱动模块也可以运行,但如果不确定此模块是否依赖于编译时的内核的某些特性时,将会给后来运行过程来带来很大风险。因此,最稳妥的办法还是基于某内核自身的代码树(Source tree)构建内核对象,这也可以保证在开发人员改变了内核的配置后,驱动也会在新配置的内核的基础上进行重建。如果需要在不同的内核上构建内核对象,则必须确保驱动程序构建时的配置所使用的编译选项、引用内核头文件的位置和内核配置选项在驱动实际运行的内核上做了同样的配置。为了基于前文中的例子构建驱动程序,大致要经过以下几个步骤:(1)在内核源码目录中的.../driver/char目录中创建一个名为examples的子目录;
(2)在内核配置文件中添加一个菜单项,以允许在编译内核时可以选择编译examples(编译进内核或编译成模块);
(3)在.../driver/char/Makefile文件中添加一个子目录项examples以对应于前述第二步骤的菜单项;
(4)为.../driver/char/examples目录创建一个makefile文件,在其中为前述第二个步骤中创建的菜单项添加hello.o模块对象以进行编译;(5)添加驱动程序源码;下面详细描述前面的几个步骤:首先,在内核源码目录的.../driver/char目录中创建examples子目录,而后再创建两个文件:一个是前面例子中的驱动源码,一个是为其创建的makefile文件。makefile文件非常简单,其内容只有如下一行即可:
obj-$(CONFIG_EXAMPLES) += hello.o添加菜单项至内核配置工具的过程可能稍有些绕。首先需要在.../driver/char/Kconfig文件中添加一个"config"项以启用前文中的examples配置项。添加位置是menu "Character devices"一行的后面,添加内容如下:config EXAMPLES
tristate "Enable Examples"
default m
---help---
   Enable compilation option for driver examples而后回到内核目录中,运行内核编译命令make gconfig(需要xwindow的支持)后,可以Device Drivers->Character drivers找到我们添加的“Enable Examples”项,默认为“-”(通过default项指定),即编译为内核模块。如果指定为“对号”则表示以静态方式编译进内核;如果为空,则表示不编译此项。如下图所示:接下来还需要在.../drivers/char/Makefile文件中添加一个选项,以指示内核编译程序在我们选择了 CONFIG_EXAMPLES时会到examples子目录中编译hello1模块。这需要在 “obj-$(CONFIG_IPMI_HANDLER)      += ipmi/”一行附近添加如下行:obj-$(CONFIG_EXAMPLES)          += examples/至此,此示例驱动构建基础结构已经完成,而且会在内核编译过程中自动选择此项进行编译了。在执行完前面的"make gconfig"命令后,此时再执行如下命令即可完成驱动模块的编译。# make modules

CHK     include/linux/version.h
CHK     include/linux/utsrelease.h
CC [M] drivers/char/examples/hello.o
Building modules, stage 2.
MODPOST
CC      drivers/char/examples/hello.mod.o
LD [M] drivers/char/examples/hello.ko如果您的编译过程显示有如上信息,则表示内核模块编译完成。接下来就可以使用如下命令安装刚刚编译完成的内核模块:
# make modules_install使用此种方式安装时,安装过程会重新安装所有已编译的内核模块,包括此前编译的其它模块,而这并非是必须的。在一个通过标准方式安装的Linux系统上,内核模块通常位于/lib/modules//…之中,其中的即当前系统运行中的内核版本号,并且此目录的结构组织方式跟内核源代码树的结构是类似的。通常使用“make modules_install”命令安装的模块就位于此目录中。因此,在单独安装某个或某些内核模块时,可以通过在此目录中创建跟编译时内核源码树中一样的内核模块驱动相关的目录,并把编译完成的*.ko文件复制到新建的对应目录中来实现。5、加载/卸载内核模块安装完成后,便可以手动加载或卸载这些模块了,这可以使用modprobe实现。我们首先去加载hello模块。# modprobe hello
# tail -1 /var/log/messages
Sep 14 22:06:23 localhost kernel: Hello Device Driver World!此模块在加载时会调用模块初始化函数,程序中使用module_init()宏(macro)来指定的模块初始化函数,如 module_init(hello_init)。在此模块中,初始化函数仅用来打印一行信息至系统日志,信息内容是在hello_init()中定义好的。在实际驱动程序编写中,初始化函数常用来执行资源分配及硬件设备初始化。接下来可以使用lsmod命令以格式化列表的形式显示系统中加载的所有模块。如果其中有hello模块出现则表示前面的加载是成功的。例如:# lsmod
Module                  Size Used by
hello                   5632 0
ipv6                  274208 18
autofs4                25092 2
i2c_core               25344 1 i2c_piix4
…………其中Used by一列表示当前模块正在被使用的信息,以及依赖于当前模块的其它模块。如最后一行表示i2c_piix4模块依赖于i2c_core模块。内核模块的卸载可以通过使用modprobe的-r选项来实现。# modprobe -r hello
# tail -1 /var/log/messages
Sep 14 22:14:33 localhost kernel: Goodbye, Cruel World!hello模块退出时会调用exit例行函数,这使用module_exit()宏来实现。其工作方式类似前面的加载过程。