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)

时间: 2024-12-29 01:17:06

Linux下编写驱动程序(VFS)的相关文章

在Linux下编写Daemon

在Linux(以Redhat Linux Enterprise Edition 5.3为例)下,有时需要编写Service.Service也是程序,一般随系统启动用户不干预就不退出的程序,可以称为Service.Linux下的Service一般称为Daemon. 以上是广义的Service的定义.Linux下的Service一般放在/etc/init.d文件夹下.浏览一下这个文件夹下的文件,可以发现在Linux下编写Service一般遵循的原则. 一 Linux下编写Service一般遵循的原则

使用VI编辑器在Linux下编写Java文件

1.cd 文件名,进入一个目录下 2.vi 文件名,新建一个文件(如此文件已存在则打开) 进入编辑器 3.按i(光标所在输入)/按a(光标后输入)进入编辑模式,写入JAVA代码 P.S.  Esc退出编辑状态,非编辑状态下X为删除,HJKL分别为左上下右 4.按Esc退出编辑模式 5.按:wq,回车 6.ls查看文件,已存在 7.javac编译(文件名需加后缀) 8.java运行(文件名不加后缀) 使用VI编辑器在Linux下编写Java文件

Linux下编写 makefile 详细教程

原文地址:https://www.cnblogs.com/mfryf/p/3305778.html 还有一篇也不错:http://wiki.ubuntu.org.cn/跟我一起写Makefile:MakeFile介绍 近期在学习Linux下的C编程,买了一本叫<Linux环境下的C编程指南>读到makefile就越看越迷糊,可能是我的理解能不行. 于是google到了以下这篇文章.通俗易懂.然后把它贴出来,方便学习. 后记,看完发现这篇文章和<Linux环境下的C编程指南>的mak

怎样在linux下编写C程序并编译执行

一.Hello, world! 在linux下输入:(以hello.c为例)首先选中文件要保存的路径(如:cd work)vi hello.c(要编辑的文件名) 输入程序:# include<stdio.h>int main(void) { printf("hello,world!\n");   return 0;   注:return和0之间要有空格} 退出并保存程序:在vi模式下,按ESC后,按 :wq加enter键 利用gcc编译程序:(先要找到程序存在哪个文件夹中)

Windows下与Linux下编写socket程序的区别 《转载》

原文网址:http://blog.chinaunix.net/uid-2270658-id-308160.html [[Windows]] [Windows: 头文件的区别] #include<winsock.h>#include<winsock2.h> [Windows: 初始化的区别] WSADATA wsaData;WSAStartup(0x202,&wsaData); [Windows: 声明Socket] SOCKET类型 [Windows: Socket关闭]c

Linux下编写互相通信的驱动模块并将其加入到内核中

以Mini2440为例,其Linux内核目录为/opt/FriendlyARM/mini2440/linux-2.6.32.2,在linux-2.6.32.2(Linux内核目录)下的drivers目录下新建目录名为add_sub_Kconfig. 在add_sub_Kconfig目录下新建文件add_sub.c,add_sub.h,test_communication.c,代码如下:(例子来自<Linux驱动开发入门与实践 第2版>) #ifndef ADD_SUB_H #define AD

linux下按键驱动程序

说明:由于调试的时候minicom出了问题,传送大一点的文件就会失败,所以下面的程序可能会有点问题,请注意 1.button.c #include<linux/module.h>#include<linux/kernel.h>#include<linux/moduleparam.h>#include<linux/init.h>#include<linux/kdev_t.h>#include<linux/fs.h>#include<

使用vim在Linux下编写C语言程序

1.进入字符界面 2.创建文件夹用于存放源文件 mkdir helloworld    //创建文件夹命令 cd helloworld        //进入新建的文件夹,这里应该说目录比较好,windows用习惯了 vim main.c        //打开vim并创建main.c文件 //按 i 键进入编辑状态,以下是程序 #include           int main() { printf("Hello world!/n"); return 0; } 按Esc进入命令模

Linux 下编写服务器程序时关于Address already in use 的小错误

新手,,学习linux服务器编程的时候,bind()函数出现了Address already in use 的错误,这是因为上一次bind过后,还未释放,,只要在socket和bind之间加一个函数就可以 1 int opt: 2 setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); 这个函数的作用是实现地址复用,,