转:http://hi.baidu.com/firstm25/item/8fe022155e1fa78988a9568f
摘要:设备驱动程序是操作系统内核与机器硬件之间的接口。设备驱动程序为应用程序屏蔽了硬件的细节。那么驱动程序如何书写实现这一接口功能是本文讨论的重点,并以一简单的驱动程序介绍书写细节。
在用户进程调用驱动程序时,系统进入核心态,这时不再是抢先式调度。(应用程序一般是在用户态下进行)也就是说系统必须在驱动程序的子函数返回后才能进行其它的工作,即驱动程序不能进入死循环。
字符型设备驱动程序的编写包含一下信息:
#define _NO_VERSION_
#include <linux/modules.h>
#include <linux/version.h>
char kernel_version[]=UTS_RELEASE
这段定义了一些版本信息,虽然用处不大,但也必不可少。<linux/config.h>最好要包含。由于用户进程是通过设备文件同硬件打交道,对设备文件的操作不外乎就是一些系统调用,如open,read,write,close……,(注意,不是fopen,fread,)但是如何把系统调用和驱动程序联系起来呢?这需要了解一个非常关键的数据结构:
struct file_opertions{
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)(struct inode*, struct file*, struct dirent*, int);/*读取相关目录*/
int(*select)(struct inlde*, struct file*, int, select_table*);/*非阻塞设备访问*/
int(*ioctl)(struct inlde*, struct file*, unsigned int, unsigned long);
int(*mmap)(struct inlde*, struct file*, struct vm_area_struct*);
int(*open)(struct inlde*, struct file*);
int(*release)(struct inlde*, struct file*);
int(*fsync)(struct inlde*, struct file*);/*强制同步*/
int(*fasync)(struct inlde*, struct file*);
int(*check_media_change)(struct inlde*, struct file*);
int(*revalidata)(dev_t dev);/*使设备重新有效*/
}
其中read,write,open,close(release),ioctl是最核心的,必须实现的。
这个结构体的每个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备注册程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是linux的设备驱动程序工作的基本原理。既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operatons的各个域。
以下是简单的字符型设备的驱动程序编写方式,例子程序并不牵扯到具体设备,只是个编写框架。
#include <linux/types.h> //Linux基本类型定义
#include <linux/fs.h> //文件系统相关头文件
#include <linux/mm.h> //memmory management内存管理
#include <linux/errno.h> //错误代码
#include <asm/segment.h> //汇编文件
unsigned int test_major = 0; /*定义一个主设备号(主设备号、从设备号在Linux设备管理中有相关介绍)*/
static int read_test(struct inode *inode, struct file *file, char *buf, int count)
{
/*本函数对应于file_opertions中read的实现,函数名自己定义。inode为设备节点,file为设备文件描述符(open()打开后自动或得),buf为数据缓冲区,count为数据传送个数。“static ”这里修饰函数名表示函数只在本文件中有效。这里函数只实现简单数据拷贝功能。*/
int left;
if(verify_area(VERIFY_WRITE, buf, count) == -EFAULT) //验证缓存中的数据是否有效
return -EFAULT; //错误码,在<linux/errno.h> 包含
for(left=count; left>0; left--)
{
__put_user(1, buf, 1);
/* “ __”表示内核调用函数,此函数表示把数据从内核空间放到用户空间,参数依次表示:填充数、用户空间、数据量。*/
buf++;
}
return count;
}
这个函数是为read调用准备的。当调用read时,read_test()被调用,它把用户的缓冲区全部写1。buf是read调用的一个参数。它是用户进程空间的一个地址。但是在read_test被调用时,系统进入核心态(内核空间),必须用__put_user(),这是kernel提供的一个函数,用于向用户传送数据。另外还有很多类似功能的函数,参考内核调用接口函数。在向用户空间拷贝数据之前,必须验证buf空间是否可用。这就用到verify_area()。
static int write_test(struct inode *inode *inode, struct file *file, const char *buf, int count)
{
return count;
}
写数据函数,具体没有实现,直接返回计数值。
static int open_test(struct inode *inode, struct file *file)
{
MOD_INC_USE_COUNT; //宏:注册模块数加1
return 0; //返回0表示成功,根据函数自己定义。
}
这个函数比较简单,它不牵扯到设备文件,仅将模块数加1。
static void release_test(struct inode *inode, struct file *file)
{
MOD_DEC_USE_COUNT; //模块数减1
}
以上实现四个函数,后三个函数都是空操作,实际调用发生时什么也不做,它们仅仅为file_operations结构体提供函数指针。
下面开始注册刚刚写好的函数
struct file_operations test_fops =
/*file_operations结构体名,test_fops结构体对象*/
{
NILL, /*seek*/
read_test,
write_test,
NULL, /*test_readdir*/
NULL, /*test_mmap*/
open_test,
release_test,
NULL, /*test_fsvnc*/
NULL, /*test_fasync*/
/* 其它位置均填为空NILL*/
};
设备驱动程序的主体可以说是写好了,现在把驱动程序嵌入内核。驱动程序可用按照两种方式编译。一种是编译进内核(kernel),另一种是编译成模块(modules)如*.o文件,如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以推荐使用模块方式。
1、登记注册设备:
方式一:编译进内核,利用函数init_module()
int init_module(void)
{
int result;
result = register_chrdev(0, "test", &test_fops);
/*内核函数:注册一个字符型设备到内核中去。参数:指定设备号(0:表示内核根据主设备号获得并返回给result)、设备名、设备结构体名*/
if(result < 0)
{
printk(KERN_INFO"test:: can‘t get major number\n");
/*在内核空间驱动程序打印数据,参数:打印优先级、打印信息*/
return result;
}
if(test_major == 0) test_major = result;
return 0;
}
方式二:编译加载模块方式,利用insmod命令,在用insmod命令将编译好的模块调用内存时,init_module()函数被调用。在这里,init_module()函数只做了一件事,就是向系统的字符设备表登记了一个字符设备。
register_chrdev()需要三个参数,参数一是希望获得的设备号,如果是0,系统将选择一个没有被占用的设备号返回。参数二是设备文件名,参数三用来登记驱动程序实际执行操作的函数指针。如果登记成功,返回设备的主设备号,不成功,返回一个负数。
2、卸载设备:
void cleanup_module(void)
{
unregister_chrdev(test_major, "test");
}
在用rmmod卸载模块时,cleanup_module()函数被调用,它释放字符设备test在系统字符设备表中占有的表项。
至此,一个及其简单的字符设备可以说写好了,为以下叙述方便,命名文件为test.c。
上文讲到驱动程序已经基本写好,并命名为test.c文件,下面进行编译:
$ gcc -O2 -DMODULE -D__KERNEL__ -c test.c
注释:-O2表示优化等级,-DMODULE表示编译成模块,-D__KERNEL__表示加载到内核的某个模块, -c表示编译生成test.o文件(2.4版本)
得到的文件test.o就是一个设备驱动程序。如果设备驱动程序有多个文件,把每个文件按上面的命令行编译,然后进行链接
$ ld -r file1.o file2.o -o <模块名>
驱动程序已经编译好了,现在把它安装到系统中去。
$ insmod -f test.o
注释:-f表示强制加载,test.o为模块名
如果安装成功,在/proc/devices文件中就可以看到设备test,并可以看到它的设备号。要卸载的话,运行
$ rmmod test
下一步要创建设备节点
$ mknod /dev/test c 主设备号 从设备号
注释:c表示字符型设备,主设备号就是在/proc/devices里看到的。用shell命令打印全部设备,就可以获得主设备号。
$ cat /proc/device
我们现在可以通过设备文件来访问我们的驱动程序。写一个测试程序。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
main()
{
int testdev;
int i;
char buf[10];
testdev = open("/dev/test", O_RDWR);
/*open()函数首先为打开文件分配文件句柄fd,然后再为打开的文件在文件表中分配一个空闲文件结构项,然后让刚分配的文件句柄fd的文件结构指针指向搜索到的文件结构,然后调用namei()取得对应文件i节点,然后让文件结构与这个i节点结构相关联。i节点中包含有该文件所代表的主设备号和子设备号,还有它属于什么类型文件(如普通文件、目录文件、字符设备文件、块设备文件、管道文件等)。*/
if(testdev == -1)
{
printf("Cann‘t open file \n"); //用户空间使用printf(),内核空间使用printk()
exit(0);//退出系统
}
read(testdev, buf, 10);
/*read()/write()根据open()返回的文件句柄fd,取得该文件的i节点。根据该i节点的属性字段(i_pipe和i_mode)来决定调用相应的读写操作函数。*/
for(i=0; i<10; i++)
printf("%d\n", buf[i]);
close(testdev);
}
编译运行,打印结果应该输出全1
以上只是一个简单的演示。真正使用的驱动程序要复杂的多,要处理中断,DMA,I/Oport等问题。这才是真正的难点。
Linux下编写驱动程序(VFS)