1、Linux 字符设备驱动程序编写基本流程Linux device driver 的概念 系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。设备驱动程序是内核的一部分,它完成以下的功能: 1、对设备初始化和释放; 2、把数据从内核传送到硬件和从硬件读取数据; 3、读取应用程序传送给设备文件的数据和回送应用程序请求的数据; 4、检测和处理设备出现的错误。 在 Linux 操作系统下有三类主要的设备文件类型,一是字符设备,二
2、是块设备,三是网络设备。字符设备和块设备的主要区别是:在对字符设备发出读/ 写请求时,实际的硬件I/O 一般就紧接着发生了,块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的 I/O 操作。块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的 CPU 时间来等待。已经提到,用户进程是通过设备文件来与实际的硬件打交道。每个设备文件都都有其文件属性(c/b),表示是字符设备还是块设备?另外每个文件都有两个设备号,第一个是主设备号,标识驱动程序,第二个是从设备号,标识使用同一个设备驱动程序的不同的硬件设备,比如有两个
3、软盘,就可以用从设备号来区分他们。设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法访问到驱动程序。 1.Linux 驱动程序编写基本流程:1.首先是一些版本信息,没什么用,但是不能少#define _NO_VERSION_#include #include char kernel_version = UTS_RELEASE;2.为了把系统调用和驱动程序关联起来,需要一个非常关键的数据结构:struct file_operations。 file_operations 结构的每一个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如 rea
4、d/write 操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是 linux 的设备驱动程序工作的基本原理。编写设备驱动程序的主要工作就是编写子函数,并填充 file_operations 的各个域3.简单驱动程序的编写(test.c):a.包含一些基本的头文件。b.编写一些功能函数,比如 read(),write()等。这些函数被调用时系统进入和心态。c.定义 struct file_operations 结构的对象,填充结构体。结构体中功能的顺序不能改变,若一些功能没有实现就用 NULL 填充,已经实现的功能如
5、read()、write() 分别添加到对应的位置。这步实现的是函数的注册。到这里驱动程序的主体可以说是写好了。现在需要把驱动程序嵌入内核。d.注册设备驱动程序,使用 register_chrdev 注册字符型设备。函数原型为:int register_chrdev(0, “test_name”, 到这里 test.c 基本就编写完成了。一个简单的字符设备可以说写好了。4.编译$ gcc -O2 -DMODULE -D_KERNEL_ -c test.o test.c得到文件 test.o 就是一个设备驱动程序。如果设备驱动程序有多个文件,把每个文件按上面的命令行编译,然后ld -r file
6、1.o file2.o -o modulename驱动程序已经编译好了,现在把它安装到系统中去。$ insmod -f test.o安装成功在/proc/devices 文件中就可以看到设备 test,并可以看到主设备号。要卸载运行:$ rmmod test5.创建设备节点mkmod /dev/test c major minorc 是指字符设备,major 是主设备号,minor 是从设备号,一般可以设置为 0以上就是 Linux 驱动编写的基本过程了,可能有遗漏的地方,这个我只是按我理解的整理的。具体问题还要在实践中再进行研究。1.Linux 驱动程序编写基本流程:1.首先是一些版本信息,
7、没什么用,但是不能少#define _NO_VERSION_#include #include char kernel_version = UTS_RELEASE; 2.为了把系统调用和驱动程序关联起来,需要一个非常关键的数据结构:struct file_operations。file_operations 结构的每一个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如 read/write 操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是 linux 的设备驱动程序工作的基本原理。编写设备驱动
8、程序的主要工作就是编写子函数,并填充 file_operations 的各个域3.简单驱动程序的编写(test.c):a.包含一些基本的头文件。b.编写一些功能函数,比如 read(),write()等。这些函数被调用时系统进入和心态。c.定义 struct file_operations 结构的对象,填充结构体。结构体中功能的顺序不能改变,若一些功能没有实现就用 NULL 填充,已经实现的功能如 read()、write() 分别添加到对应的位置。这步实现的是函数的注册。到这里驱动程序的主体可以说是写好了。现在需要把驱动程序嵌入内核。d.注册设备驱动程序,使用 register_chrdev
9、 注册字符型设备。函数原型为:int register_chrdev(0, “test_name”, 到这里 test.c 基本就编写完成了。一个简单的字符设备可以说写好了。 4.编译$ gcc -O2 -DMODULE -D_KERNEL_ -c test.o test.c得到文件 test.o 就是一个设备驱动程序。如果设备驱动程序有多个文件,把每个文件按上面的命令行编译,然后ld -r file1.o file2.o -o modulename驱动程序已经编译好了,现在把它安装到系统中去。$ insmod -f test.o安装成功在/proc/devices 文件中就可以看到设备 te
10、st,并可以看到主设备号。要卸载运行:$ rmmod test5.创建设备节点mkmod /dev/test c major minorc 是指字符设备,major 是主设备号,minor 是从设备号,一般可以设置为 0以上就是 Linux 驱动编写的基本过程了,可能有遗漏的地方,这个我只是按我理解的整理的。具体问题还要在实践中再进行研究。二、实例剖析 我们来写一个最简单的字符设备驱动程序。虽然它什么也不做,但是通过它可以了解Linux 的设备驱动程序的工作原理。把下面的 C 代码输入机器,你就会获得一个真正的设备驱动程序。 由于用户进程是通过设备文件同硬件打交道,对设备文件的操作方式不外乎就
11、是一些系统调用,如 open,read,write,close , 注意,不是 fopen, fread,但是如何把系统调用和驱动程序关联起来呢?这需要了解一个非常关键的数据结构: struct file_operations int (*seek) (struct inode * ,struct file *, off_t ,int); int (*read) (struct inode * ,struct file *, char ,int); int (*write) (struct inode * ,struct file *, off_t ,int); int (*readdir)
12、(struct inode * , struct file *, struct dirent * ,int); int (*select) (struct inode * ,struct file *, int ,select_table *); int (*ioctl) (struct inode * ,struct file *, unsined int ,unsigned long); int (*mmap) (struct inode * ,struct file *, struct vm_area_struct *); int (*open) (struct inode * ,str
13、uct file *); int (*release) (struct inode * ,struct file *); int (*fsync) (struct inode * ,struct file *); int (*fasync) (struct inode * ,struct file *,int); int (*check_media_change) (struct inode * ,struct file *); int (*revalidate) (dev_t dev); 这个结构的每一个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如 read/wr
14、ite 操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是 linux的设备驱动程序工作的基本原理。既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充 file_operations 的各个域。 下面就开始写子程序。 #include 基本的类型定义 #include 文件系统使用相关的头文件 #include #include #include unsigned int test_major = 0; static int read_test(struct inode *inode,struct file
15、 *file ,char *buf,int count) int left; 用户空间和内核空间 if (verify_area(VERIFY_WRITE,buf,count) = -EFAULT ) return -EFAULT; for(left = count ; left 0 ; left-) _put_user(1,buf,1); buf+; return count; 这个函数是为 read 调用准备的。当调用 read 时,read_test()被调用,它把用户的缓冲区全部写 1。buf 是 read 调用的一个参数。它是用户进程空间的一个地址。但是在read_test 被调用时
16、,系统进入核心态。所以不能使用 buf 这个地址,必须用_put_user() ,这是 kernel 提供的一个函数,用于向用户传送数据。另外还有很多类似功能的函数。请参考,在向用户空间拷贝数据之前,必须验证 buf 是否可用。这就用到函数 verify_area。为了验证 BUF 是否可以用。 static int write_test(struct inode *inode,struct file *file,const char *buf,int count) return count; static int open_test(struct inode *inode,struct fi
17、le *file ) MOD_INC_USE_COUNT; 模块计数加以,表示当前内核有个设备加载内核当中去 return 0; static void release_test(struct inode *inode,struct file *file ) MOD_DEC_USE_COUNT; 这几个函数都是空操作。实际调用发生时什么也不做,他们仅仅为下面的结构提供函数指针。 struct file_operations test_fops = ? read_test, write_test, open_test, release_test, ; 设备驱动程序的主体可以说是写好了。现在要把驱
18、动程序嵌入内核。驱动程序可以按照两种方式编译。一种是编译进 kernel,另一种是编译成模块(modules),如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以推荐使用模块方式。 int init_module(void) int result; result = register_chrdev(0, “test“, 对设备操作的整个接口 if (result #include #include #include main() int testdev; int i; char buf10; testdev = open(“/dev/test“,O_RDWR); if ( testdev = -1 ) printf(“Cannt open file n“); exit(0); read(testdev,buf,10); for (i = 0; i 10;i+) printf(“%dn“,bufi); close(testdev); 编译运行,看看是不是打印出全 1 ? 以上只是一个简单的演示。真正实用的驱动程序要复杂的多,要处理如中断,DMA,I/O port 等问本文来自 CSDN 博客,转载请标明出处:http:/