第9章Linux驱动程序设计_第1页
第9章Linux驱动程序设计_第2页
第9章Linux驱动程序设计_第3页
第9章Linux驱动程序设计_第4页
第9章Linux驱动程序设计_第5页
已阅读5页,还剩79页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、第第9章章 Linux驱动程序设计驱动程序设计9.1Linux 设备驱动程序设备驱动程序9.2Linux经典经典Hello world驱动程序驱动程序9.3Linux字符设备驱动程序实例字符设备驱动程序实例LinuxLinux操作系统的内核是单一体系结构(操作系统的内核是单一体系结构(monolithic kernelmonolithic kernel) 有了模块机制后,有了模块机制后,提高提高LinuxLinux操作系统的可扩充性操作系统的可扩充性,内核编程不再是一个,内核编程不再是一个恶梦恶梦什么是模块呢?什么是模块呢? 模块的全称是模块的全称是“动态可加载内核模块动态可加载内核模块”(L

2、oadable Kernel ModuleLoadable Kernel Module,LKMLKM) 模块在内核空间运行模块在内核空间运行 模块实际上是一种目标对象文件模块实际上是一种目标对象文件 没有链接,不能独立运行没有链接,不能独立运行,但是其代码可以在运行时链接到系统中,但是其代码可以在运行时链接到系统中作为内核的一部分运行或从内核中取下,从而可以作为内核的一部分运行或从内核中取下,从而可以动态扩充内核的动态扩充内核的功能功能 这种目标代码通常由一组函数和数据结构组成这种目标代码通常由一组函数和数据结构组成优点优点使得内核更加使得内核更加紧凑和灵活紧凑和灵活修改内核时,修改内核时,不

3、必全部重新编译不必全部重新编译整个内核。系统如果需要使整个内核。系统如果需要使用新模块,只要编译相应的模块,然后使用用新模块,只要编译相应的模块,然后使用insmodinsmod将模块将模块装载即可装载即可模块的目标代码一旦被链接到内核,它的作用域和静态链接模块的目标代码一旦被链接到内核,它的作用域和静态链接的内核目标代码完全等价的内核目标代码完全等价缺点缺点由于内核所占用的内存是不会被换出的,所以链接进内核的由于内核所占用的内存是不会被换出的,所以链接进内核的模块会给整个系统带来一定的模块会给整个系统带来一定的性能和内存利用方面的损失性能和内存利用方面的损失; ;装入内核的模块就成为内核的一

4、部分,可以修改内核中的其装入内核的模块就成为内核的一部分,可以修改内核中的其他部分,因此,模块的使用不当会导致系统崩溃他部分,因此,模块的使用不当会导致系统崩溃; ;为了让内核模块能访问所有内核资源,为了让内核模块能访问所有内核资源,内核必须维护符号表内核必须维护符号表,并在装入和卸载模块时修改符号表,并在装入和卸载模块时修改符号表; ;模块会要求利用其它模块的功能,所以,模块会要求利用其它模块的功能,所以,内核要维护模块之内核要维护模块之间的依赖性间的依赖性. . C C语言程序语言程序 LinuxLinux内核模块内核模块运行运行 用户空间用户空间 内核空间内核空间入口入口 main()

5、main() module_init()module_init()指定指定; ;出口出口 无无 module_exit()module_exit()指定指定; ; 编译编译 gcc gcc c Makefilec Makefile连接连接 ld insmodld insmod运行运行 直接运行直接运行 insmodinsmod调试调试 gdb kdbug, kdb, kgdbgdb kdbug, kdb, kgdb等等 Linux系统的设备分为字符设备(系统的设备分为字符设备(char device)、块设备(、块设备(block device)和网络设备()和网络设备(network dev

6、ice)三种。)三种。 字符设备字符设备是指存取时没有缓存的设备。典型的字是指存取时没有缓存的设备。典型的字符设备包括鼠标,键盘,串行口等。符设备包括鼠标,键盘,串行口等。 字符设备在字符设备在I/O传输过程中以字符为单位的,但是传输过程中以字符为单位的,但是不一定是以字节为单位,因为一个字符展不一定是以字节为单位,因为一个字符展16bit(2个字个字节)。它是通过文件系统节点来存储,在节)。它是通过文件系统节点来存储,在Linux系统中系统中,字符设备以特别的文件方式在文件目录树中占据位置,字符设备以特别的文件方式在文件目录树中占据位置并拥有自己的结点,并且指明了文件类型,但是,操作并拥有自

7、己的结点,并且指明了文件类型,但是,操作(包括打开、关闭、读、写操作)起来却和普通文件一(包括打开、关闭、读、写操作)起来却和普通文件一样。大部分字符设备仅仅是数据通道,只能顺序存取。样。大部分字符设备仅仅是数据通道,只能顺序存取。 当字符设备与主机连接后,可以使用当字符设备与主机连接后,可以使用mknod命命令来为设备创建字符特别文件。例如,为名为令来为设备创建字符特别文件。例如,为名为/dev/kui的主机创建字符特别文件的命令如下:的主机创建字符特别文件的命令如下: mknod /dev/kui c 4 9 其中:其中:4是设主设备号;是设主设备号;9是次设备为是次设备为9;c是字符型是

8、字符型类型标记。类型标记。 字符设备和主机的通信字符设备和主机的通信一般采用中断方式,当字一般采用中断方式,当字符设备上的数据传输完成时,就通过总线向系统发出符设备上的数据传输完成时,就通过总线向系统发出中断处理信号,数据传输的底层控制由中断处理程序中断处理信号,数据传输的底层控制由中断处理程序和设备驱动程序协同完成。和设备驱动程序协同完成。 块设备块设备主要包括硬盘软盘设备、主要包括硬盘软盘设备、CD-ROM等。块设等。块设备的读写都有缓存来支持,并且块设备必须能够随机备的读写都有缓存来支持,并且块设备必须能够随机存取(存取(random access),字符设备则没有这个要求。),字符设备

9、则没有这个要求。 块设备和字符设备块设备和字符设备也有相似之处,块设备通过位也有相似之处,块设备通过位于于 /dev 目录的文件系统结点来存取,其读写操作跟字目录的文件系统结点来存取,其读写操作跟字符设备一样,可以一次传送任意数目的字节。符设备一样,可以一次传送任意数目的字节。1)内核在内部管理数据的方式不同,)内核在内部管理数据的方式不同,块设备将信息存储在固定大块设备将信息存储在固定大小的块中,每个块都有自己的地址,数据块的大小通常在小的块中,每个块都有自己的地址,数据块的大小通常在51232768字节之间,并且每个块之间都能独立进行读写操作。字节之间,并且每个块之间都能独立进行读写操作。

10、2)内核)内核/驱动的软件接口完全不同。驱动的软件接口完全不同。在大多数的在大多数的UNIX操作系统中操作系统中,块设备只支持以块为单位的访问方式。,块设备只支持以块为单位的访问方式。Linux支持以字符方式支持以字符方式来访问块设备,在来访问块设备,在/dev目录中的块设备,均以字符设备的外观目录中的块设备,均以字符设备的外观出现。所以,字符设备和块设备的区别主要体现在内核中的管出现。所以,字符设备和块设备的区别主要体现在内核中的管理方式,操作方式和内核理方式,操作方式和内核/设备驱动接口上。设备驱动接口上。 网络设备网络设备是一个连接在多个主机之间,并能够相是一个连接在多个主机之间,并能够

11、相互交换数据的设备。它不一定是硬件设备,也可能是互交换数据的设备。它不一定是硬件设备,也可能是一个纯粹的软件设备。一个纯粹的软件设备。 网络设备网络设备在在Linux里做专门的处理。里做专门的处理。Linux的网络的网络系统主要是基于系统主要是基于BSD unix的的socket机制。在系统和驱机制。在系统和驱动程序之间定义有专门的数据结构(动程序之间定义有专门的数据结构(sk_buff)进行数)进行数据的传递。系统里支持对发送数据和接收数据的缓存据的传递。系统里支持对发送数据和接收数据的缓存,提供流量控制机制,提供对多协议的支持。,提供流量控制机制,提供对多协议的支持。 网络设备驱动程序网络

12、设备驱动程序异步地接收来自外部世界的异步地接收来自外部世界的网络数据包,通过网络数据包,通过push操作将进来的数据包压向内核操作将进来的数据包压向内核,而块设备驱动程序则将一片数据缓冲发送给内核,而块设备驱动程序则将一片数据缓冲发送给内核,驱动程序将相应设备的特征信息登记到内核中特定的驱动程序将相应设备的特征信息登记到内核中特定的数据结构中。数据结构中。 块设备驱动程序可以使用文件形式来描述,而网块设备驱动程序可以使用文件形式来描述,而网络驱动程序则不可以使用一般的文件读写操作调用,络驱动程序则不可以使用一般的文件读写操作调用,网络设备驱动程序有自己的内存名字空间,使用网络设备驱动程序有自己

13、的内存名字空间,使用push等操作来完成数据包的转换与递送。等操作来完成数据包的转换与递送。 内核中有专门为网络设备驱动程序设计的数据包操作接口,内核中有专门为网络设备驱动程序设计的数据包操作接口,如图如图9-1所示。路由就是通过网络接口所示。路由就是通过网络接口sn0到达网络到达网络snullnet0的意的意思,使用思,使用route add -net snullnet0 dev sn0指令来完成,指令来完成,Linux 2.2版本以上内核就不用这样做,内核将自动添加。版本以上内核就不用这样做,内核将自动添加。远程 Linux中大部分的驱动程序都中大部分的驱动程序都以模块的形式编写以模块的形

14、式编写,这些驱动程序源码可以修改到内核中,也可以把他们这些驱动程序源码可以修改到内核中,也可以把他们编译成模块形式,在需要的时候动态加载。例如,在编译成模块形式,在需要的时候动态加载。例如,在块驱动设备中,当系统试图读取这个设备(即调用块驱动设备中,当系统试图读取这个设备(即调用read()时),就会运行驱动程序中的时),就会运行驱动程序中的block_read() 这个这个函数。函数。(1)查看模块信息命令。)查看模块信息命令。可以用可以用lsmod、cat /proc/modules、modinfo三个命令查看模块信息三个命令查看模块信息(2)modprobe命令的功能是自动处理可载入模块

15、。命令的功能是自动处理可载入模块。语法语法:modprobe -acdlrtvV-help模块文件模块文件符号名称符号名称 = 符号值符号值 (3)insmod(install module)挂载模块)挂载模块 。功能说明:载入模块。功能说明:载入模块。语法:语法:insmod -fkmpsvxX-o 模块文件模块文件符符号名称号名称 = 符号值符号值(4)rmmod 移除已挂载模块。移除已挂载模块。(5)depmod 创建模块依赖关系的列表。创建模块依赖关系的列表。这个模块管理这个模块管理工具是创建模块依赖关系的列表,有几个参数需要注工具是创建模块依赖关系的列表,有几个参数需要注意一下,目前

16、的意一下,目前的Linux 2.6x版本,是自动解决依赖关系版本,是自动解决依赖关系 Linux设备驱动程序及用户程序、操作系统内容之间设备驱动程序及用户程序、操作系统内容之间的关系及结构的关系及结构如图如图9-2所示。其中所示。其中Linux的设备驱动程的设备驱动程序大致可以分为:驱动程序注册、设备打开、设备读序大致可以分为:驱动程序注册、设备打开、设备读写操作、设备控制操作、设备中断和轮询处理、设备写操作、设备控制操作、设备中断和轮询处理、设备释放、驱动程序注销等七个部分。释放、驱动程序注销等七个部分。 向系统增加一个驱动程序意味着要赋予它一个主向系统增加一个驱动程序意味着要赋予它一个主设

17、备号,这可以通过在驱动程序的初始化过程中调用设备号,这可以通过在驱动程序的初始化过程中调用register_chrdev()或者或者register_blkdev()来完成。来完成。 打开设备是通过调用打开设备是通过调用file_operations结构中的函结构中的函数数open()来完成的,它是驱动程序用来为今后的操作完来完成的,它是驱动程序用来为今后的操作完成初始化准备工作的。在大部分驱动程序中,成初始化准备工作的。在大部分驱动程序中,open()通通常需要完成下列工作:常需要完成下列工作:检查设备相关错误,如设备未准备就绪等。如果是第一次打开,则初始化硬件设备。识别次设备号,如果有必要

18、则更新读写操作的当前位置指针f_ops。分配和填写要放在file-private_data里的数据结构。使用计数器增加1。 但如果是块设备,则需要调用函数但如果是块设备,则需要调用函数block_read()和和block_write()来进行数据读写,这两个函数将向来进行数据读写,这两个函数将向设备请求表中增加读写请求,以便设备请求表中增加读写请求,以便Linux内核可以对请求顺序进内核可以对请求顺序进行优化处理,由于内核操作是对内存缓冲区而不是直接对设备行优化处理,由于内核操作是对内存缓冲区而不是直接对设备本身,因此能很大程度上加快读写速度。如果内存缓冲区中没本身,因此能很大程度上加快读写

19、速度。如果内存缓冲区中没有所要读入的数据,或者需要执行写操作将数据写入设备,那有所要读入的数据,或者需要执行写操作将数据写入设备,那么就要执行真正的数据传输,这是通过调用数据结构么就要执行真正的数据传输,这是通过调用数据结构blk_dev_struct中的函数中的函数request_fn()来完成的。来完成的。 除了读写操作外,应用程序有时还需要对设备进除了读写操作外,应用程序有时还需要对设备进行控制,这时可以通过设备驱动程序中的行控制,这时可以通过设备驱动程序中的函数函数ioctl()来来完成,完成,ioctl()的用法与具体设备密切关联,因此需要根的用法与具体设备密切关联,因此需要根据设备

20、的实际情况进行具体分析。据设备的实际情况进行具体分析。 对于不支持中断的硬件设备,对于不支持中断的硬件设备,读写时需要轮流查询读写时需要轮流查询设备状态设备状态,以便决定是否继续进行数据传输。如果设,以便决定是否继续进行数据传输。如果设备支持中断,则可以按中断方式进行操作。备支持中断,则可以按中断方式进行操作。 通过通过Linux提供的系统调用函数(例如提供的系统调用函数(例如init_module等等)进入内核,这些函数在进入内核,这些函数在2.6内核版本下总共有两百多内核版本下总共有两百多个,提供了几乎所有应用程序进入内核所需要执行的个,提供了几乎所有应用程序进入内核所需要执行的操作。操作

21、。 系统的内核函数都有系统的内核函数都有“sys_”前缀(例如函数前缀(例如函数sys_init_module()),),应用程序通过访问设备文件系统应用程序通过访问设备文件系统来调用这些函数。这一层主要是来调用这些函数。这一层主要是“devfs”(device filesystem())文件管理机制,它是从普通文件和设备)文件管理机制,它是从普通文件和设备文件抽象出来的一个文件系统层,完成进入具体的设文件抽象出来的一个文件系统层,完成进入具体的设备文件操作之前的准备工作。备文件操作之前的准备工作。 释放设备是通过调用释放设备是通过调用file_operations结构中的函结构中的函数数re

22、lease()来完成的,这个设备方法有时也被称为来完成的,这个设备方法有时也被称为close(),它的作用正好与,它的作用正好与open()相反,通常要完成下列相反,通常要完成下列工作:工作: 使用计数器减1。释放在file-private_data中分配的内存。如果计数器为0,则关闭设备。 向系统增加一个驱动程序意味着要赋予它一个主向系统增加一个驱动程序意味着要赋予它一个主设备号,设备号,这可以通过在驱动程序的初始化过程中调用这可以通过在驱动程序的初始化过程中调用register_chrdev()或者或者register_blkdev()来完成。而在关来完成。而在关闭字符设备或者块设备时,则

23、需要通过调用闭字符设备或者块设备时,则需要通过调用unregister_chrdev()或或unregister_blkdev()从内核中注从内核中注销设备,同时释放占用的主设备号。销设备,同时释放占用的主设备号。(1)file结构。结构。file结构是设备驱动程序一个重要的数据结构,指结构是设备驱动程序一个重要的数据结构,指示当前系统中已打开的文件。它在示当前系统中已打开的文件。它在C语言库中定义,在调用内语言库中定义,在调用内核核open函数时创建,并传递给在该设备上进行操作的所有函数函数时创建,并传递给在该设备上进行操作的所有函数,直到最后的,直到最后的close()函数执行完毕。函数执

24、行完毕。file结构中还包含了指向它结构中还包含了指向它所拥有的所拥有的file_operations结构的指针。结构的指针。 file代表一个打开的文件,在执行代表一个打开的文件,在执行file_operations中的中的open操操作时被创建,这里需要注意的是与用户空间作时被创建,这里需要注意的是与用户空间inode指针的区别,指针的区别,一个在内核;而一个在内核;而file指针在用户空间,由指针在用户空间,由C库来定义。库来定义。 inode结构由内核自动生成,代表已打开文件的描述符,与每结构由内核自动生成,代表已打开文件的描述符,与每个打开的文件一一对应。个打开的文件一一对应。ino

25、de被内核用来代表一个文件,注意被内核用来代表一个文件,注意和和struct file的区别在于:的区别在于:struct inode一个是代表文件,而一个是代表文件,而struct file一个是代表打开的文件。一个是代表打开的文件。 dev_t i_rdev 设备文件的设备号设备文件的设备号 struct cdev *i_cdev代表字符设备的数据结构代表字符设备的数据结构 dev_t扩展到扩展到32位,其中位,其中12位主设备号,位主设备号,20位从设备号;而位从设备号;而cdev用于存储一个指向字符设备文件的指针。用于存储一个指向字符设备文件的指针。 inode结构是用来在内核内部表示

26、文件的,同一个文件可以结构是用来在内核内部表示文件的,同一个文件可以被多次打开,所以可以对应很多被多次打开,所以可以对应很多file结构,但是只对应一个结构,但是只对应一个inode结构。结构。 在在Linux内核下使用内核下使用file_operations数据结构,来数据结构,来建立设备驱动程序中的函数与主设备号(建立设备驱动程序中的函数与主设备号(major number)之间的对应关系。该数据结构中包含了指向)之间的对应关系。该数据结构中包含了指向驱动程序内部大多数函数的指针,描述了虚拟文件系驱动程序内部大多数函数的指针,描述了虚拟文件系统如何操作一个打开的外围设备。统如何操作一个打开

27、的外围设备。file_operations是把是把系统调用和驱动程序关联起来的关键数据结构,这个系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都对应着一个系统调用。结构的每一个成员都对应着一个系统调用。 先读取先读取file_operation中相应的函数指针,接着把控中相应的函数指针,接着把控制权转交给函数,从而完成了制权转交给函数,从而完成了Linux设备驱动程序的工设备驱动程序的工作。作。file_operations是一个字符设备把驱动的操作和设是一个字符设备把驱动的操作和设备号联系在一起的纽带,是一系列指针的集合,每个备号联系在一起的纽带,是一系列指针的集合,每个被打

28、开的文件都对应于一系列的操作,这就是被打开的文件都对应于一系列的操作,这就是file_operations,用来执行一系列的系统调用。,用来执行一系列的系统调用。 在系统内部,在系统内部,I/O设备的存取操作通过特定的入口点设备的存取操作通过特定的入口点来进行,而这组特定的入口点是由设备驱动程序提供来进行,而这组特定的入口点是由设备驱动程序提供的。通常这组设备驱动程序接口是由结构的。通常这组设备驱动程序接口是由结构file_operations结构体向系统说明的,它的定义在结构体向系统说明的,它的定义在include/linux/fs.h中。中。 一个一个file_operation 结构或者

29、其一个指针称为结构或者其一个指针称为 fops( 或者它的一些变体)。结构中的每个成员必须指向驱或者它的一些变体)。结构中的每个成员必须指向驱动中的函数,这些函数实现一个特别的操作,或者对动中的函数,这些函数实现一个特别的操作,或者对于不支持的操作置为于不支持的操作置为 NULL。struct file_operations struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char _user *, size_t, loff_t *); ss

30、ize_t (*write) (struct file *, const char _user *, size_t, loff_t *); ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); int (*readdir) (struct file *, void *, filldir_t); unsigned int

31、 (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct v

32、m_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *,

33、 int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long,unsigned long, unsigned long); int (*check_flags)(int); int (*dir_notify)(struct

34、file *filp, unsigned long arg); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t,unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t,unsigned int); int (*setlease)(stru

35、ct file *, long, struct file_lock *); int (*fsetattr)(struct file *, struct iattr *); 根据上文提到过的,旧版本根据上文提到过的,旧版本Hello World驱动程序驱动程序代码如下:代码如下:#define MODULE #include #include int init_module(void) printk(KERN_INFO Hello, worldn); return 0; void cleanup_module(void) printk(KERN_INFO Goodbye cruel worldn

36、); 在在Linux2.6版本中的版本中的hello world驱动与上述驱动与上述2.4版本有所不同,具版本有所不同,具体内容如下:体内容如下: #include #include #include MODULE_LICENSE(GPL);/新新, 否则有否则有waring, 去掉了去掉了#define MODULE, 自动定义自动定义 static int hello_init(void) printk(KERN_ALERT Hello, worldn); return 0; static void hello_exit(void) printk(KERN_ALERT Goodbye, c

37、ruel worldn); module_init(hello_init);/这行是必需的这行是必需的module_exit(hello_exit);/这行是必需的这行是必需的 在在Linux2.6版本中,采用版本中,采用module_init与与module_exit两个宏来定义驱动程序的注册与注消,这两个宏来定义驱动程序的注册与注消,这两个宏在两个宏在init.h中定义,中定义,module_init与与module_exit宏的宏的代码如下:代码如下:/* 每一个模块使用前必须进行初始化每一个模块使用前必须进行初始化 */#define module_init(initfn) stati

38、c inline initcall_t _inittest(void) return initfn; int init_module(void) _attribute_(alias(#initfn); /* 这个只是在想要注销模块是调用这个只是在想要注销模块是调用*/ #define module_exit(exitfn) static inline exitcall_t _exittest(void) return exitfn; void cleanup_module(void) _attribute_(alias(#exitfn);从上述代码可以看出,从上述代码可以看出,module_i

39、nit实际上是把用户程序中的实际上是把用户程序中的module_init(initfn)一行代码分解成一行代码分解成_inittest(void)和和init_module(void)两个函数。两个函数。实现实现_inittest函数比较简单,就是返回函数比较简单,就是返回initfn,而,而initfn对应的是用对应的是用户户module_init函数中的参数,代表用户自定义的驱动程序入口函数中的参数,代表用户自定义的驱动程序入口真正地实现函数地址。真正地实现函数地址。实现实现init_module函数复杂一些,采用了函数复杂一些,采用了_attribute_机制,机制,_attribute

40、_可以设置函数属性(可以设置函数属性(Function Attribute)、变量)、变量属性(属性(Variable Attribute)和类型属性()和类型属性(Type Attribute)。)。 上述示例中大量应用了上述示例中大量应用了printk函数。函数。printk函数允许函数允许按照相关的记录级或优先级将消息严格分类和输出,按照相关的记录级或优先级将消息严格分类和输出,并且是内核中直接支持的一个消息输出函数。并且是内核中直接支持的一个消息输出函数。 printk函数通常需要一个宏来指定记录等级。例如函数通常需要一个宏来指定记录等级。例如KERN_INFO、KERN_ALERT等

41、。在前面的例子中看等。在前面的例子中看到过这个宏,它就是消息记录等级的一种。记录等级到过这个宏,它就是消息记录等级的一种。记录等级宏的作用是扩展为一个字符串,这个字符串会在编译宏的作用是扩展为一个字符串,这个字符串会在编译期间与相应的消息文本相连接,这样在优先级和格式期间与相应的消息文本相连接,这样在优先级和格式化字符串之间没有逗号。化字符串之间没有逗号。 printk()函数是直接使用了向终端写函数函数是直接使用了向终端写函数tty_write()。而。而printf()函数是调用函数是调用write()系统调用函数向标准输系统调用函数向标准输出设备写。所以在用户态(如进程出设备写。所以在用

42、户态(如进程0)不能够直接使用)不能够直接使用printk()函数,而在内核态由于他已是特权级,所以无函数,而在内核态由于他已是特权级,所以无需系统调用来改变特权级,因而能够直接使用需系统调用来改变特权级,因而能够直接使用printk()函数。函数。1驱动程序的驱动程序的make文件文件 模块的建立过程与用户应用程序的建立过程有显著模块的建立过程与用户应用程序的建立过程有显著不同,内核是一个大的、独立的程序,对于它的各个不同,内核是一个大的、独立的程序,对于它的各个部分如何组合在一起有详细而明确的要求。部分如何组合在一起有详细而明确的要求。(1)保证内核版本足够新,包括编译器、模块工具,以)保

43、证内核版本足够新,包括编译器、模块工具,以及其他必要工具等。及其他必要工具等。(2)参考在内核文档目录下的文件)参考在内核文档目录下的文件 Documentation/Changes,它列出了需要的工具版本;,它列出了需要的工具版本;因为建立一个内核(包括它的模块),如果用错误的因为建立一个内核(包括它的模块),如果用错误的工具版本,可能导致很多奇怪的问题。工具版本,可能导致很多奇怪的问题。(3)如果没有源码树在文件系统上,或者还没有配置和)如果没有源码树在文件系统上,或者还没有配置和建立内核,就无法为建立内核,就无法为 Linux2.6 内核建立可加载的模块内核建立可加载的模块。(4)如果已

44、建立起所有东西,给模块创建一个)如果已建立起所有东西,给模块创建一个makefile并不是非常复杂,例如对于本节的并不是非常复杂,例如对于本节的“hello world”程程序,只需要采用单行指令即要完成:序,只需要采用单行指令即要完成: obj-m := hello.o obj-m表示该文件将以模块的方式编译,因为本模块由多个文件组表示该文件将以模块的方式编译,因为本模块由多个文件组成,采用模块名加成,采用模块名加objs(minix-objs)后缀的形式来定义模块)后缀的形式来定义模块的组成文件。当然,在使用的组成文件。当然,在使用“obj-m := hello.o”这一行之前,这一行之前

45、,还需要做一些其他的准备工作,这些准备工作将要用到如下一还需要做一些其他的准备工作,这些准备工作将要用到如下一些定义:些定义:$(KERNELDIR)定义了代码树的位置,定义了代码树的位置,PWD定义了当前文件夹位定义了当前文件夹位置。置。make命令中命令中-C选项指定了代码树的位置(由选项指定了代码树的位置(由KERNELDIR给出)。给出)。“M=”表示这是个外部模块,表示这是个外部模块,M=$(PWD)指定了该模块文件所在指定了该模块文件所在的路径。的路径。makefile预定义了预定义了$(PWD)变量,此处可以不必重复定变量,此处可以不必重复定义。义。如果是如果是obj-y,那这部

46、分代码就会编译成内核的一部分,如果是,那这部分代码就会编译成内核的一部分,如果是obj-m,那么就会编译成一个单独的驱动模块。,那么就会编译成一个单独的驱动模块。(5)上面不是一个传统的)上面不是一个传统的makefile,实际上是内核建立,实际上是内核建立系统处理了余下的工作。系统处理了余下的工作。(6)上面的例子(它利用了由)上面的例子(它利用了由 GNU make 提供的扩展提供的扩展语法)表明有一个模块要从目标文件语法)表明有一个模块要从目标文件 hello.o 建立,在建立,在从目标文件建立后结果模块命名为从目标文件建立后结果模块命名为 hello.ko。反之,如果有一个模块名为反之

47、,如果有一个模块名为 module.ko,并且来自,并且来自2个源文件(假个源文件(假设名为设名为file1.c 和和 file2.c),正确的书写应当是:),正确的书写应当是: obj-m := module.o module-objs := file1.o file2.o 对于(对于(4)中的)中的makefile,它必须在更大的内核建立系统的上下文,它必须在更大的内核建立系统的上下文被调用,如果内核源码位于被调用,如果内核源码位于 /kernel-2.6 目录,用来建立模块的目录,用来建立模块的 make 命令(在包含模块源码和命令(在包含模块源码和makefile 的目录下键入)会是:

48、的目录下键入)会是:make -C /kernel-2.6 M=pwd modules命令先是改变它的目录到用命令先是改变它的目录到用 -C 选项提供的目录下(内核选项提供的目录下(内核源码目录),它会在那里发现内核的顶层源码目录),它会在那里发现内核的顶层 makefile。M=选项使选项使 makefile在试图建立模块目标前,回到模块在试图建立模块目标前,回到模块源码目录。目标依次指在源码目录。目标依次指在 obj-m 变量中发现的模块列变量中发现的模块列表,在示例里假设为表,在示例里假设为module.o。输入输入make命令总是感觉繁琐,所以内核开发者就开发了命令总是感觉繁琐,所以内

49、核开发者就开发了一种一种makefile方式,方便那些在内核树之外建立模块的方式,方便那些在内核树之外建立模块的开发者,开发者,makefile如下所示:如下所示:#如果如果KERNELRELEASE被定义被定义, 表明已经引用表明已经引用 kernel建立内核系建立内核系统并能使用它的语言统并能使用它的语言ifneq ($(KERNELRELEASE),) obj-m := hello.o # 否则称为直接命令行否则称为直接命令行, 引用引用kernel可建立内核系统可建立内核系统else KERNELDIR ?= /lib/modules/$(shell uname -r)/build P

50、WD := $(shell pwd) default: $(MAKE) -C $(KERNELDIR) M=$(PWD) modulesEndif 这个这个makefile在建立中要被读在建立中要被读 2 次,当从命令行中调用这个次,当从命令行中调用这个makefile 时,发现没有设置时,发现没有设置 KERNELRELEASE 变量,它利用变量,它利用已安装模块目录中的内核建立树来定位内核源码目录。如果实已安装模块目录中的内核建立树来定位内核源码目录。如果实际上没有运行建立的内核,可以在命令行提供一个际上没有运行建立的内核,可以在命令行提供一个KERNELDIR= 选项,设置选项,设置KE

51、RNELDIR环境变量或者修改环境变量或者修改 makefile 中设置中设置 KERNELDIR 的这一行,一旦发现内核源码树的这一行,一旦发现内核源码树,makefile调用调用default:目标来运行第:目标来运行第 2 个个 make 命令(在命令(在 makefile 里参数化成里参数化成 $(MAKE))像前面描述过的一样来调用内)像前面描述过的一样来调用内核建立系统。在第核建立系统。在第2次被读,次被读,makefile 设置设置 obj-m,并且内核的,并且内核的 makefile 文件完成实际的建立模块工作。文件完成实际的建立模块工作。 上面的不是一个完整的上面的不是一个完

52、整的makefile,一个完整的,一个完整的 makefile通常包括通常包括过期目标文件的清除、文件的安装等模块。过期目标文件的清除、文件的安装等模块。(1)编译,在终端中转到)编译,在终端中转到hello.c和和make文件的目录下,文件的目录下,只要输入只要输入make命令即可编译。编译成功后,生成一个命令即可编译。编译成功后,生成一个hello.o文件,同时还生成一个文件,同时还生成一个hello.ko文件。文件。(2)手工加载驱动模块,在终端中输入命令)手工加载驱动模块,在终端中输入命令insmod ./ hello.ko 加载驱动模块到内核。加载驱动模块到内核。(3)查看驱动模块信

53、息,输入)查看驱动模块信息,输入cat /proc/modules | grep hello 查看查看hello模块在内核中的地址,不加过滤器可以模块在内核中的地址,不加过滤器可以看到全部加载的模块。看到全部加载的模块。(4)显示模块,输入)显示模块,输入lsmod 显示模块,这时可以看到所显示模块,这时可以看到所有的模块名字,后面跟的是主设备号和次设备号。有的模块名字,后面跟的是主设备号和次设备号。(5)卸载模块,输入)卸载模块,输入rmmod key_test,把模块从内核里,把模块从内核里卸载。卸载。(6)在)在Ubuntu终端中,如果看不到加载和卸载终端中,如果看不到加载和卸载Hell

54、o模模块的块的printk函数输入的消息,这时可以通过函数输入的消息,这时可以通过dmesg命令命令查看查看printk函数输出的函数输出的“Hello,World!”消息。消息。1头文件定义头文件定义 在头文件中,除了包括驱动程序所必需的头文件,在头文件中,除了包括驱动程序所必需的头文件,还定义了还定义了file_operations实例实例char_ops,该实例将在模,该实例将在模块加载函数块加载函数char_init中使用。在中使用。在char_ops结构中,只结构中,只包括了最常见的包括了最常见的read、write、open和和release属性属性 驱动程序是内核的一部分,因此需

55、要给其添加模块驱动程序是内核的一部分,因此需要给其添加模块初始化函数,该函数用来完成对所控设备的初始化,初始化函数,该函数用来完成对所控设备的初始化,并调用并调用register_chrdev() 函数注册字符设备。模块初始函数注册字符设备。模块初始化函数名为化函数名为char_init,通过宏,通过宏module_init(char_init)指指定成别名定成别名init_module的实现函数。模块初始化函数代的实现函数。模块初始化函数代码如下:码如下:#include chardev.hstatic int _init char_init(void) dev_t dev; printk(

56、KERN_ALERT字符设备字符设备chardev设备初始化设备初始化.n); dev=MKDEV(DP_MAJOR,DP_MINOR); chardev = cdev_alloc( ); if(chardev=NULL) return -1; if(register_chrdev_region(dev,10,chardev)0) printk(KERN_ALERT字符设备字符设备chardev注册出错!注册出错!n); return -1; chropen=0; len=0; cdev_init(chardev,&char_ops); if(cdev_add(chardev, dev, 1)

57、private_data中的所有内容。中的所有内容。在最后一次关闭操作时关闭设备。在最后一次关闭操作时关闭设备。关闭设代码如下:关闭设代码如下:static int char_release(struct inode *inode,struct file *file) chropen-; printk(KERN_ALERT注消字符设备注消字符设备chardev!n); module_put(THIS_MODULE); return 0; 驱动驱动module的的file_operations结构体中可以定义结构体中可以定义很多设备操作服务函数,实现这些函数与系统调用,很多设备操作服务函数,实现

58、这些函数与系统调用,或者外部应用程序交互,而不管具体的设备操作。下或者外部应用程序交互,而不管具体的设备操作。下面以面以read函数为例介绍驱动函数的调用过程。函数为例介绍驱动函数的调用过程。(1)read实现过程。实现过程。read函数的原型如下:函数的原型如下: ssize_t read(struct file *filp, char _user *buf, size_t count, loff_t *f_pos)(2)write的实现过程。的实现过程。write的函数原型如下:的函数原型如下:static int char_write(struct file *filp, const c

59、har _user *buffer, size_t length, loff_t *offset)static int char_read(struct file *filp, char _user *buffer, size_t length, loff_t *offset) if(length12) if(copy_to_user(buffer, hello world!,12) printk(KERN_ALERT字符设备字符设备chardev读操作出错!读操作出错!n); return 0; else if(copy_to_user(buffer, hello world!, lengt

60、h) printk(KERN_ALERT字符设备字符设备chardev读操作出错!读操作出错!n); return 0; printk(KERN_ALERT添加字符设备添加字符设备chardev读操作成功!读操作成功!n); return 1;static int char_write(struct file *filp, const char _user *buffer, size_t length, loff_t *offset) printk(KERN_ALERT添加字符设备添加字符设备chardev写操作写操作!n); return 0; 卸载驱动与前面的初始化驱动程序相反。每个重要的

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论