首页 > 代码库 > Hasen的linux设备驱动开发学习之旅--异步通知

Hasen的linux设备驱动开发学习之旅--异步通知

/**
 * Author:hasen
 * 参考 :《linux设备驱动开发详解》
 * 简介:android小菜鸟的linux
 * 	         设备驱动开发学习之旅
 * 主题:异步通知
 * Date:2014-11-05
 */
一、异步通知的概念和作用
阻塞和非阻塞访问、poll()函数提供了较好地解决设备访问的机制,但是如果有了异步通知整套机制就更
加完整了。
异步通知的意思是:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这
一点非常类似于硬件上“中断”的概念,比较准确的称谓是“信号驱动的异步I/O”。信号时在软件层次上对中断机制
的一种模拟,进程收到信号和处理器收到中断可以说是一样的。信号是异步的,进程不知道信号什么时候到达。
阻塞I/O意味着一直等待设备可访问后再访问,非阻塞I/O使用poll()意味着查询设备是否可访问,而异步
通知则意味着设备通知自身可访问,实现了异步I/O。由此可见,这几种方式I/O可以互为补充。

二、Linux异步通知编程
1、Linux信号
            使用信号进行进程间通信(IPC)是UNIX中一种传统机制,LInux也支持这种机制。在Linux中,异步通知
使用信号来实现,Linux中可用的信号及其定义如下表:

Linux信号
信号含义
SIGHUP1挂起
SIGINT2终端中断
SIGQUIT3终端退出
SIGILL4无效命令
SIGTRAP5跟踪陷阱
SIGIOT6IOT陷阱
SIGBUS7BUS错误
SIGFPE8浮点异常
SIGKILL9强行终止(不能被捕捉或忽略)
SIGSR110用户定义的信号1
SIGSEGV11无效的内存段处理
SIGUSR212用户定义的信号2
SIGPIPE13半关闭管道的写操作已经发生
SIGALRM14计时器到期
SIGTERM15终止
SIGSTKFLT16堆栈错误
SIGCHLD17子进程已经停止或退出
SIGCONT18如果停止了,继续执行
SIGSTOP19停止执行(不能被捕获或忽略)
SIGTSTP20终端停止信号
SIGTTIN21后台进程需要从终端读取输入
SIGTTOU22后台进程需要向从终端写出
SIGURG23紧急的套接字事件
SIGXCPU24超额使用CPU分配的时间
SIGXFSZ25文件尺寸超额
SIGVTALRM26虚拟时钟信号
SIGPROF27时钟信号描述
SIGWINCH28窗口尺寸变化
SIGIO29I/O
SIGPWR30断电重启

2、信号的接收
           在用户程序中,为了捕获信号,可以使用signal()函数来设置对应信号的处理函数:
           void (*signal (int signum,void(*handler))(int))(int) ;
           该函数原型较难理解,它可以分解为:
           typedef void (*sighandler_t) (int) ;
           sighandler_t signal(int signum,sighandler_t sighandler) ;
第一个参数指定信号的值,第二个参数指定针对前面信号值的处理函数,若为SIG_IGN,表示忽略该信号;
若为SIG_DFL,表示采用系统默认方式处理信号;若为用户自定义函数,信号捕捉到后,将会执行该函数。
           如果signal()调用成功,它返回最后一为信号signum绑定的处理函数handler的值,失败返回SIG_ERR。
           在进程执行时,按下“Ctrl+c”将向其发出SIGINT信号,kill正在运行的进程将向其发送SIGTERM信号。
示例:进程捕捉SIGINT和SIGTERM信号并输出信号值

void sigterm_handler(int signo)
{
    printf("Have caught sig NO.%d\n",signo) ;
    exit(0) ;
}
int main(void)
{
	signal(SIGINT,sigterm_handler) ;
	signal(SIGTERM,sigterm_handler) ;
	while(1) ;
	
	return 0 ;
}
除了signal()函数之外,sigaction()函数也可用于改变进程接收到特点信号的行为,它的原型是:
int sigaction(int signum,const struct sigaction *act,struct sigaction oldact) ;
           该函数的第一个参数为信号的值,可以为除SIGKILL及SIGSTOP之外的任何一个特定有效的信号。第二个
参数是指向结构体sigaction的一个实例指针,在结构体sigaction的实例中,指定了对特定信号的处理函数,若为
空,则进程会以缺省方式对信号进行处理。第三个参数指向的对象用来保存原来对相应信号的处理函数,可指定
oldact为NULL,当后面两个参数都为NULL时,那么该函数可用以检查信号的有效性。
           下面是使用信号实现异步通知的实例,它通过signal(SIGIO,input_handler)对标准输入文件描述符
STDIN_FILENO启动信号机制。用户输入后,应用程序将接收到SIGNO信号,处理函数input_handler()将被调用。
示例:使用信号实现异步通知的应用程序
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#define MAX_LEN 100 ;

void input_handler(int num)
{
    char data[MAX_LEN] ;
    int len ;
    /*读取并输出STDIN_FILENO上的输入*/
    len = read(STDIN_FILENO,&data,MAX_LEN) ;
    data[len] = 0 ;
    printf("input available:%s\n,data") ;
}
main()
{
	int oflags ;
	
	/*启动信号驱动机制*/
	signal(SIGIO,input_handler) ;//input_handler为信号处理函数
	fcntl(STDIN_FILENO,F_SETOWN,getpid()) ;//设备本进程为文件拥有者
	oflags = fcntl(STDIN_FILENO,F_GETFL) ;//得到文件标志
	fcntl(STDIN_FILENO,FSETFL,oflags|FASYNC) ;//为文件添加FASYNC标志(异步通知机制)
	/*最后进入一个死循环,仅为保持进程不终止,如果程序中没有这个死循环会立即执行完毕*/
	while(1) ;
}
           为了在用户空间中能处理一个设备释放的信号,需要完成下面三个步骤:
           (1)通过F_SETOWN IO控制命令设置文件的拥有者为本进程,这样从设备驱动发出的信号才能被本进
程接收到。
           (2)通过F_SETFL IO控制命令设置设备文件支持FASYNC,即异步通知模式。
           (3)通过signal()函数连接信号和信号处理函数。
3、信号的释放
           在设别驱动和应用程序的异步通知交互中,仅仅在应用程序端捕获信号是不够的,因为信号的源头
在设备驱动端,因此,在设备驱动中添加信号释放的代码。
           为了使设别支持异步通知机制,驱动程序设计3项工作:
           (1)支持F_SETOWN命令,能在这个控制命令处理中设置filp->f_owner为对应进程ID,不过,内核已
经处理了此项工作,设备驱动无需处理。
           (2)支持F_SETFL命令的处理,每当FASYNC标志改变时,驱动程序中的fasync()函数将得以执行。因
此,驱动中应该实现fasync()函数。
           (3)在设备资源可获得时,调用kill_fasync()函数激发相应的信号。


           驱动中的工作与用户空间中的是一一对应的,下图是异步通信过程中用户空间和设备驱动的交互。

           设备驱动中的异步通知编程,主要用到两个函数和一个结构体
结构体:fasync_struct 
方法:
 	(1)处理FASYNC标志变更的
 		int fasync_helper(int fd,struct file *flip,int mode,struct fasync_struct **fa);
 	(2)释放信号用的函数
		void kill_fasync(struct fasync_struct **fa ,int sig,int band);
           和其他的设备驱动一样,将fasync_struct结构体指针放在设备结构体中最合适。
示例:支持异步通知的设备结构体模板
struct xxx_dev{
    struct cdev cdev ;/*cdev结构体*/
    ...
    struct fasync_struct *fasync_queue ;/*异步结构体指针*/
}
           在设备驱动的fasync()函数中,只要简单地将该函数的3个参数以及fasync_struct结构体指针的指针
作为第4个参数传入fasync_helper()函数即可。
示例:支持异步通知的设备驱动fasync()函数模板
static int xxx_fasync()
{
    struct xxx_dev *dev = filp->private_data;
    return fasync_helper(fd,filp,mode,&dev->async_queue) ;
}
           在设备资源可以获得时,应该调用kill_fasync()释放SIGIO信号,可读时第三个参数设置为POLL_IN,
可写时第三个参数设置为POLL_OUT 。 
示例:支持异步通知的设备驱动信号释放。
static ssize_t xxx_write(struct file *filp,const char __user buf,
		size_t count,loff_t *f_pos)
{
    struct xxx_dev *dev = filp->private_data ;
    ...
    /*产生异步读信号*/
    if(dev->async_queue)
    	kill_fasync(&dev->async_queue,SIGIO,POLL_IN) ;
    ...
}
           最后在文件关闭时,即在设备的release()函数中,应调用设备驱动的fasync()函数将文件从异步通
知的列表中删除。
示例:支持异步通知的设备驱动release()函数模板    
static ssize_t xxx_release(struct inode *inode,struct file filp)
{
   struct xxx_dev *dev = filp->private_data ;
   ...
   /*产生异步读信号*/
   if(dev->async_queue)
	kill_fasync(&dev->async_queue,SIGIO,POLL_IN) ;
   ...
}
           下面是增加异步通知机制的设备驱动和验证代码

在globalfifo驱动中增加异步通知

int GLOBALFIFO_SIZE = 100 ;

/*增加异步通知后的globalfifo设备结构体*/
struct globalfifo_dev {
	struct cdev cdev ;/*cdev结构体*/
	unsigned int current_len ;/*fifo有效数据长度*/
	unsigned char mem[GLOBALFIFO_SIZE];/*全局内存*/
	struct semaphore sem ;/*并发控制用的信号量*/
	wait_queue_head_t r_wait ;/*阻塞读用的等待队列头*/
	wait_queue_head_t w_wait ;/*阻塞写用的等待队列头*/
	struct fasync_struct *async_queue ;/*异步结构体指针*/	
} ;

/*支持异步通知的globalfifo设备驱动的fasync()函数*/
static int globalfifo_fasync(int fd,struct file *flip,int mode)
{
	struct globalfifo_dev *dev = flip->private_data ;
	return fasync_helper(fd,flip,mode,&dev->async_queue) ;
}

/*支持异步通知的globalfifo设备驱动写函数*/
static ssize_t globalfifo_write(struct file *filp,const char __user *buf,
	size_t count,loff_t *ppos)
{
	struct globalfifo_dev *dev = filp->private_data ;/*获得设备结构体指针*/
	int ret ;
	DECLARE_WAITQUEUE(wait,current) ;/*定义等待队列*/
	down(&dev->sem) ;/*获取信号量*/
	add_wait_queue(&dev->w_wait,&wait) ;/*进入写等待队列头*/
	/*等待fifo非满*/
	if(dev->current_len == GLOBALFIFO_SIZE) {
		if(filp->f_flag & O_NONBLOCK ){/*如果是非阻塞访问*/
			ret = -EAGAIN ;
			goto out ;
		}
		__set_current_state(TASK_INTERRPTIBLE) ;/*改变进程状态为睡眠*/
		up(&dev->sem) ;
		
		schedule() ;/*调度其他进程执行*/
		if(signal_pending(current)){ /*如果是因为信号唤醒*/
			ret = -ERESTARTSYS ;
			goto out2 ;
		}
		down(&dev->sem) ;/*获取信号量*/
	}
	
	/*从用户空间拷贝到内核空间*/
	if(count >GLOBALFIFO_SIZE - dev->current_len)
		count = GLOBALFIFO_SIZE - dev->current_len ;
	if(copy_from_user(dev->mem + dev->current_len , buf ,count)){
		ret = -EFAULT ;
		goto out ;
	}else{
		dev->current_len += count ;
		printk(KERN_INFO "writeen %d bytes(s),current_len:%d\n",count,dev->current_len) ;
		wake_up_interruptible(&dev->r_wait) ;/*唤醒读等待队列*/
		/*产生异步读信号*/
		if(dev->async_queue)
			/*释放信号,可写时为POLL_OUT,可读时为POLL_IN*/
			kill_fasync(&dev->async_queue,SIGIO,POLL_IN) ;
		ret = count ;
	}
	out: up(&dev->sem) ;/*释放信号量*/
	out2 :remove_wait_queue(&dev->w_wait,&wait) ;
	set_current_state(TASK_RUNNING) ;
	return ret ;
}

/*增加异步通知的globalfifo设备驱动release()函数*/
int globalfifo_release(struct inode *inode ,struct file *filp ) 
{
	/*将文件从异步通知列表中删除*/
	globalfifo_fasync(-1,filp,0) ;
	return 0 ;
}

在用户空间验证globalfifo的异步通知
#include <xxx.h>

void input_handler(int signum)
{
	printf("receive a signal from globalfifo,signum:%d\n",signum) ;
}

void main()
{
	int fd  , oflags ;
	fd = open("/dev/globalfifo",O_RDWR ,S_IRUSR|S_IWUSR) ;
	if(fd != -1){
		/*启动信号驱动机制*/
		signal(SIGIO,input_handler);/*让input_handler处理SIGIO信号*/
		fcntl(fd,F_SETOWN,getpid()) ;/*设置当前进程为文件所有者*/
		oflags = fcntl(fd,F_GETFL) ; /*得到文件的所有标志*/
		fcntl(fd.F_SETFL,oflags | FASYNC) ;/*给文件加上FASYNC标志,使支持异步通知模式*/
		while(1){
			sleep(100) ;
		}
	}else{
		printf("open driver failure\n") ;
	}
}


Hasen的linux设备驱动开发学习之旅--异步通知