实验目的:
通过一个简单的设备驱动的实现过程。学会Linux中设备驱动程序的编写
实验内容:
设计和实现一个虚拟命名管道(FIFO)的字符设备。写一个模块化的字符设备驱动程序
实验提示:
一、设备的功能
设计和实现一个虚拟命名管道(FIFO)的字符设备。我们知道,管道是进程间通信的一种
方式:一个进程向管道中写数据,另一个进程从管道中读取数据,先写入的数据先读出。我
们的驱动程序要实现N(N=4)个管道,每个管道对应两个设备,次设备号是偶数的设备是只
写设备,次设备号是奇数的是只读设备。写入设备i(i是偶数)的字符可以从设备i+1读出。
这样,我们一共就需要2N 个次设备号。
我们的目标是写一个模块化的字符设备驱动程序。设备所使用的主设备号可以从尚未分
配的主设备号中任选一个,/Documentation/devices.txt 记录了当前版本内核的主设备号分配
情况。如果设备文件系统(devfs)尚未激活,我们在加载模块之后,还必须用mknod 命令创
建相应的设备文件节点。
如果FIFO 的写入端尚未打开,FIFO 中就不会有数据可读,所以此时试图从FIFO 中读
取数据的进程应该返回一个错误码。如果写入端已经打开,为了保证对临界区的互斥访问,
调用读操作的进程必须被阻塞。如果存在被阻塞的读者,在写操作完成后(或者关闭一个写
设备时)必须唤醒它。
如果写入的数据太多,超出了缓冲区中空闲块的大小,调用写操作的进程必须睡眠,以
等待缓冲区中有新的空闲块。
二、设备的实现
1. 数据结构:
首先,我们要包含一些必要的头文件、宏和全局变量。
vfifo.c
#ifndef __KERNEL__
#define __KERNEL__
#endif
#ifndef MODULE
#define MODULE
#endif
#define __NO_VERSION__
#include<linux/config.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/malloc.h>
#include<linux/fs.h>
#include<linux/proc_fs.h>
#include<linux/errno.h>
#include<linux/types.h>
#include<linux/fcntl.h>
#include<linux/init.h>
#include<asm/system.h>
#include<asm/uaccess.h>
#ifndef VFIFO_MAJOR
#define VFIFO_MAJOR 241
#endif
#ifndef VFIFO_NR_DEVS
#define VFIFO_NR_DEVS 4
#endif
#ifndef VFIFO_BUFFER
#define VFIFO_BUFFER 4000
#endif
#include<linux/devfs_fs_kernel.h>
devfs_handle_t vfifo_devfs_dir;
struct file_operations vfifo_fops;
int vfifo_major=VFIFO_MAJOR;
int vfifo_nr_devs=VFIFO_NR_DEVS;
int vfifo_buffer=VFIFO_BUFFER;
MODULE_PARM(vfifo_major,"i");
MODULE_PARM(vfifo_nr_devs,"i");
MODULE_PARM(vfifo_buffer,"i");
MODULE_AUTHOR("EBUDDY");
每个实际的FIFO 设备都对应于一个Vfifo_Dev{ }结构体。其中,rdq 是阻塞读的等待
队列,wrq 是阻塞写的等待队列,base 是所分配缓冲区的起始地址,buffersize 是缓冲区的
大小,len表示管道中已有数据块的长度,start 表示当前应该读取的缓冲区位置相对于base
的偏移量,即缓冲区起始数据的偏移量,readers和writers分别表示VFIFO 设备当前的读者
个数和写者个数,sem是用于互斥访问的信号量,r_handle和w_handle用于保存设备文件系
统的注册句柄,r_handle对应的是只读设备,w_handle对应的是同一管道的只写设备。具体
的定义如下所示:
vfifo.c
typedef struct Vfifo_Dev{
wait_queue_head rdq,wrq;
char* base;
unsigned int buffersize;
unsigned int len;
unsigned int start;
unsigned int readers,writers;
struct semaphore sem;
devfs_handle_t r_handle,w_handle;
}Vfifo_Dev;
注销的工作相对简单。需要注意的是在卸载驱动程序之后要删除设备节点。如果设备节
点是在加载时创建的,可以写一个简单的脚本在卸载时删除它们。如果动态节点没有从/dev
中删除,就可能造成不可预期的错误:系统可能会给另一个设备分配相同的主设备号,这样
在打开设备时就会出错。
我们可以看到在函数名前标有属性“__exit”,它的作用类似于“__init”,即使内建的
驱动程序忽略它所标记的函数。同样的,它对模块也没有影响。
vfifo.c
static void __exit vfifo_cleanup_module(void)
{
int i;
devfs_unregister_chrdev(vfifo_major,"vfifo");
#ifdef VFIFO_DEBUG
remove_proc_entry("vfifo",NULL);
#endif
if(vfifo_devices){
for(i=0;i<vfifo_nr_devs;i++){
if(vfifo_devices[i].base)
kfree(vfifo_devices[i].base);
devfs_unregister(vfifo_devices[i].r_handle);
devfs_unregister(vfifo_devices[i].w_handle);
}
kfree(vfifo_devices);
devfs_unregister(vfifo_devfs_dir);
}
}
}
(2). 打开与释放
打开设备主要是完成一些初始化工作,以及增加引用计数,防止模块在设备关闭前被注
销。我们知道内核用主设备号区分不同类型的设备,而驱动程序用次设备号识别具体的设备。
利用这一特性,我们可以用不同的方式打开同一个设备。
vfifo.c
static int vfifo_open(struct inode *inode,struct file *filp)
{
Vfifo_Dev *dev;
int num=MINOR(inode->i_rdev);
/*检查读写权限是否合法 */
if((flip->f_mode&FMODE_READ)&&!(num%2)||(filp->f_mode&FMODE_WRITE)&&(num%2))
return -EPERM;
if(!filp->private_data){
if(num>=vfifo_nr_devs*2)
return -ENODEV;
dev=&vfifo_nr_devices[num/2];
filp->private_data=dev;
}
else{
dev=filp->private_data;
}
/*获得互斥访问的信号量 */
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
/*如果尚未分配缓冲区,则分配并初始化 */
if(!dev->base){
dev->base=kmalloc(vfifo_buffer,GFP_KERNEL);
if(!dev->base){
up(&dev->sem);
return -ENOMEN;
}
dev->buffersize=vfifo_buffer;
dev->len=dev->start=0;
}
if(filp->mode&FMODE_READ)
dev->readers++;
if(filp->mode&FMODE_WRITE)
dev->writers++;
filp->private_data=dev;
MOD_INC_USE_COUNT;
return 0;
}
释放(或关闭)设备就是打开设备的逆过程。
vfifo.c
static int vfifo_release(struct inode *inode,struct file *filp)
{
Vfifo_Dev *dev=filp->private_data;
/*获得互斥访问的信号量 */
down(&dev->sem);
if(filp->f_mode&FMODE_READ)
dev->readers--;
if(filp->f_mode&FMODE_WRITE){
dev->writes--;
wake_up_interruptible(&dev->sem);
}
if((dev->readers+dev->writers==0)&&(dev->len==0)){
kfree(dev->base);
dev->base=NULL;
}
up(&dev->sem);
MOD_DEC_USE_COUNT;
return 0;
}
读写的操作:3
读写设备也就意味着要在内核地址空间和用户地址空间之间传输数据。由于指针只能在
当前地址空间操作,而驱动程序运行在内核空间,数据缓冲区则在用户空间,跨空间复制就
不能通过通常的方法,如利用指针或通过memcpy来完成。在Linux中,跨空间复制是通过
定义在里的特殊函数实现的。你既可以用通用的复制函数,也可以用针对不
同数据大小(char,short,int,long)进行了优化的复制函数。为了能传输任意字节的数据,
你可以用copy_to_user( )和copy_from_user( )两个函数。
尽管上面的两个函数看起来很像正常的memcpy函数,但是当你在内核代码中访问用户
空间时必须额外注意一些问题:正在被访问的用户页面现在可能不在内存中,而且缺页处理
函数有可能在传输页面的时候让进程进入睡眠状态。例如,当必须从交换区读取页面时就会
发生这种情况。驱动程序编写者在设计时必须注意,任何访问用户空间的函数都必须是可重
入的,而且能够与驱动程序内的其它函数并发执行。这就是我们用信号量来控制并发访问的
原因。
上述这两个函数的作用并不局限于传输数据,它们也可以检查用户空间的指针是否有
效。如果指针无效,复制不会进行;如果在复制过程中遇到了无效地址,则只复制部分数据。
在这两种情况下,函数的返回值都是尚未复制数据的字节数。如果你不需要检查用户空间指
针的有效性,你可以直接调用__copy_to_user( )和__copy_from_user( )。例如,你已经知道参
数是有效的,这样做就可以提高效率。
就实际的设备操作而言,读的任务是把数据从设备复制到用户空间(用copy_to_user( )),
而写操作则必须把数据从用户空间复制到设备(用copy_from_user( ))。每一个read或write
系统调用都会要求传输一定字节数的数据,但驱动程序可以随意传输其中一部分数据。
如果有错误发生,read和write都会返回一个负值。一个大于等于零的返回值会告诉调
用程序成功传输了多少字节的数据。如果某个数据成功地传输了,随后发生了错误,返回值
必须是成功传输的字节数,只有到下次函数被调用时才会报告错误。
虽然内核函数返回一个负值标识错误,该数的数值表示已发生的错误种类,但是运行在
用户空间的程序只能看到错误返回值-1。只有访问变量errno,程序才能知道发生了什么错
误。这两方面的不同行为,一方面是靠系统调用的POSIX 调用标准强加的,另一方面是内
核不处理errno的优点导致的。
具体的read代码如下:
vfifo.c
static ssize_t vfifo_read(struct file *filp,char *buf,size_t count,loff_t *f_pos)
{
Vfifo_Dev *dev=filp->private_data;
ssize_t read=0;
/*不允许进行定位操作 */
if(f_pos!=&filp->f_pos)
return -ESPIPE;
/*获得互斥访问的信号量 */
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
do_more_read:
/*没有数据可读,则进入循环等待 */
while(dev->len==0){
if(!dev->writers){
up(&dev->sem);
return -EAGAIN;
}
up(&dev->sem);
if(filp->f_flags&O_NONBLOCK)
return -EAGAIN;
printk("%s reading:going to sleep/n",current->comm);
if(wait_event_interruptible(dev->rdq,(dev->len>0)))
return -ERESTARTSYS;
printk("%s has been waken up/n",current->comm);
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
}
/*读数据 */
while(count>0&&dev->len){
char *pipebuf=dev->base+dev->start;
/*(buffersize – start)是可以一次性读取的最大数据量 */
ssize_t chars=dev->buffersize-dev->start;
if(chars>count) chars=count;
if(chars>dev->len) chars=dev->len;
if(copy_to_user(buf,pipebuf,chars)){
up(&dev->sem);
return -EFAULT;
}
read+=chars;
dev->start+=chars;
dev->start%=dev->buffersize;
dev->len-=chars;
count-=chars;
buf+=chars;
}
/*Cache behavior optimizition*/
if(!dev->len) dev->start=0;
if(count&&dev->writers&&!(filp->flags&O_NONBLOCK)){
up(&dev->sem);
wake_up_interruptible(&dev->wrq);
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
goto do_more_read;
}
up(&dev->sem);
wake_up_interruptible(&dev->wrq);
printk("%s did read %li bytes/n",current->comm, (long)read);
return read;
}
具体的write代码如下:
vfifo.c
static ssize_t vfifo_write(struct file *filp,const char *buf,size_t count,loff_t *f_pos)
{
Vfifo_Dev *dev=filp->private_data;
ssize_t written=0;
/*不允许进行定位操作 */
if(f_pos!=&filp->f-pos||count==0)
return -ESPIPE;
/*获得互斥访问的信号量 */
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
do_more_write:
/*缓冲区已满,则循环等待 */
while(dev->len==dev->buffersize){
up(&dev->sem);
if(filp->f_flags&O_NONBLOCK)
return -EAGAIN;
printk("%s writting:going to sleep/n",current->comm);
if(wait_event_interruptible(dev->wrq,(dev->lenbuffersize)))
return -ERESTARTSYS;
printk("%s has been waken up/n",current->comm);
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
}
/*写数据 */
while(count>0){
char *pipebuf=dev->base+(dev->len+dev->start)%dev->buffersize;
/*下面两行计算可以一次性写入的最大数据量 */
ssize_t chars=dev->buffersize-(dev->len+dev->start);
if(chars<0) chars+=dev->start;
if(chars!=0){
if(chars>count) chars=count;
if(copy_from_user(buf,pipebuf,chars)){
up(&dev->sem);
return -EFAULT;
}
written+=chars;
dev->len+=chars;
count-=chars;
buf+=chars;
}
}
if(count&&!(filp->f_flags&O_NONBLOCK)){
up(&dev->sem);
wake_up_interruptible(&dev->rdq);
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
goto do_more_write;
}
up(&dev->sem);
wake_up_interruptible(&dev->rdq);
printk("%s did write %li bytes/n",current->comm, (long)written);
return written;
}
poll方法.4
使用非阻塞型I/O 的应用程序经常要用到poll 和select 系统调用。poll 和select 本质上
具有相同的功能:它们都允许一个进程决定它是否能无阻塞地从一个或多个打开的文件中读
数据,或者向这些文件中写数据。这两个系统调用还可用来实现在无阻塞情况下的不同源输
入的多路复用。同样的功能为什么要由两个不同的函数提供呢?这是因为它们几乎是在同一
时间由两个不同的团体引入Unix 系统中的:BSD Unix引入了select,System V引入了poll。
在Linux 2.0 版本的内核中只支持select,从2.1.23 版本的内核开始,系统提供了对两种调用
的支持。我们的驱动程序是基于poll系统调用,因为poll提供了比select更详细的支持。
poll的实现可以执行poll和select两种系统调用,它的原型如下:
unsigned int (*poll) (struct file *,poll_table * )
驱动程序中的poll主要完成两个任务:
l。 在一个可能在将来唤醒它的等待队列中将当前进程排队。通常,这意味着同时在输
入和输出队列中对进程排队。函数poll_wait( )就用于这个目的,其工作方式说
select_wait( )非常类似。
2。 构造一个位掩码描述设备的状态,并将其返回给调用者。这个位掩码描述了能立即
被无阻塞执行的操作。
这两个操作通常是很简单的,在每个驱动程序中的实现都非常相似。然而,它们依赖于
一些只有驱动程序才能提供的信息,因此必须在每个驱动程序中分别实现。
poll_table 结构是在 中声明的,要使用poll 调用,你必须在源程序中包含
这个头文件。需要提醒的是,你无需了解它的内部结构,你只要调用操作该结构的函数就行
了。当然,如果你想了解的话,你可以自己去看源代码。
poll部分标志位的列表如下:
POLLIN 如果设备可以被无阻塞地读,那么该位必须被设置。
POLLRDNORM 如果“普通”数据可以被读,该位必须被设置。一个可读设备返回
(POLLIN | POLLRDNORM)。
POLLOUT 如果设备可以被无阻塞地写,则该位在返回值中被设置。
POLLWRNORM 该位与POLLOUT,有时甚至的确为同一个数。一个可写的设备返回
(POLLOUT | POLLWRNORM)。
具体的poll实现代码如下:
vfifo.c
unsigned int vfifo_poll(struct file *filp, poll_table *wait)
{
Vfifo_Dev *dev = filp->private_data;
unsigned int mask = 0;
poll_wait(filp, &dev->rdq, wait);
poll_wait(filp, &dev->wrq, wait);
if (dev->len > 0) mask |= POLLIN | POLLRDNORM; /* readable */
if (dev->len != dev->buffersize) mask |= POLLOUT | POLLWRNORM; /* writable */
return mask;
}
三、设备的安装
采用下面的命令可以对vfifo.c进行编译:
#gcc –c vfifo.c –D__KERNEL__ -DMODULE –O2 –g -Wall
如果没有出错的话,将会在本目录下生成一个vfifo.o 文件。
下面的操作必须是以root身份进行的:
先执行module的插入操作,
#insmod vfifo.o
如果设备文件系统已经应用起来的话,此时在设备文件系统挂接的目录(通常是/dev)
下,就可以找到vfifo 文件节点了。如果没有应用设备文件系统,则需要手工为设备添加文
件节点。首先进入dev目录,再执行如下命令:
[rootLinux /dev]#mknod vfifo c 241 0
[rootLinux /dev]#mknod vfifo c 241 1
……
[rootLinux /dev]#mknod vfifo c 241 7
此时就可以对设备进行读、写、ioctl等操作了。
当不再需要对设备进行操作时,可以采用下面的命令卸载module:
[rootLinux /dev]# rmmod vfifo
四、设备的使用
设备安装好之后就可以使用了。你可以用cp、dd等命令以及输入/输出重定向机制来测
试这个驱动程序。为了更清晰地了解程序是如何运行的,你可以在适当的位置加入printk( ),
通过它来跟踪程序。另外,你还可以用专门的调试工具如strace来监视程序使用的系统调用。
例如,你可以这样来写vfifo设备:
#strace ls /dev/vfifo* > /dev/vfifo0
#strace cat /dev/vfifo1
到此为止,我们已经完成了对Linux设备驱动的分析,并且自己设计了一个与具
体设备无关的特殊设备的驱动程序。但还有一些我们并没有涉及到的内容,如ioctl、I/O 端
口等,如有兴趣可以自己去深入钻研。
(驱动程序开发)