Linux下编写驱动程序(VFS)

转: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),古老的榕树,5-wow.com

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。