Linux设备驱动中的异步通知和异步IO

在设备驱动中,使用异步通知可以使得在进行对设备的访问时,由驱动主动通知应用程序进行访问。
这样非阻塞IO的应用程序无须使用轮询机制,而阻塞访问也被类似“中断”的异步通知所代替

异步通知的概念和作用

异步通知:一旦设备就绪,则主动通知应用程序,无须应用程序去查询设备状态
回顾前面的知识:
阻塞:一直等待设备可用;非阻塞:用各种轮询机制去查看设备是否可用
这三剑客其实本身并没有优劣,需要在不同应用场景下去合理选择

Linux的异步通知编程

Linux信号

信号是进程间通信的传统机制,异步通知就是用信号实现的
Linux可用的信号一共30个,其中除了SIGSTOP和SIGKILL信号之外,别的信号都可以被忽略或者捕获。
当信号被捕获的时候,会有相应的代码进行处理;没有被捕获,则会按照默认的处理方式

信号的接受

用户程序中,为了捕获信号,可以使用signal函数来设置对应信号的处理函数
void (*signal(int signum, void (*handler))(int))(int);
这个函数比较难理解分解如下:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signal函数的第一个参数指定信号的值,第二个参数指定针对前面信号值的处理函数,
若为SIG_IGN,表示忽略该信号;若为SIG_DFL,表示采用系统默认方式处理信号
若为别的信号(除SIGSTOP,SIGKILL),则捕捉信号后调用对应的信号处理函数
这个函数看起挺复杂,但是用起来其实挺清晰的,e.g:

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;
}

这里面有个while(1);用处如下:
1.一般在调试代码时,为了检测一部分代码是否OK,防止后面的代码干扰执行结果,会在观测点加上while(1); 
2.有些代码检测到运行错误时,会抛出错误(打印、设置错误码),然后进入while(1); 
3.机器需要复位时,停止喂看门狗,进入while(1); 迫使看门狗超时,产生硬件复位
4.防止代码立即结束,方便观测(这里的用处是这个)

int sigaction(int signum, const struct sigcation *act, struct sigaction oldact)
该函数和signal函数一样,都可以改变收到某个信号之后执行的函数
该函数第二个参数是指向结构体sigaction的一个实例的指针,在结构体sigaction的实例中,指定了对
特定信号的处理函数,若为空则会以默认的方式来执行。
第三个参数保存的是对相应信号原来的处理函数的sigaction 实例指针,可指定为NULL
当第2个和第3个参数都置为NULL,那么可以用来检测信号的有效性
下面是使用信号实现异步通知的实例:
#include <...>
#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(....);
}

void main() 
{
    int oflags;
    //启动信号驱动机制
    signal(SIGIO, input_handler);
    fcntl(STDIN_FILENO, F_SETOWN, getpid());//1
    oflags = fcntl(STDIN_FILENO, F_GETFL);//2
    fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);//3
    while(1);
}
上面注释1:设置当前进程为STDIN_FILENO文件的拥有者,没有这一步,内核不知道把信号发给哪个进程
因为是这个文件产生了SIGIO的信号,所以会发给该文件的拥有进程
注释2,3:为了启用异步通知机制,需要对设备设置FASYNC标志
为了能让用户空间可以出处理一个设备释放的信号,它必须完成3项工作
1.signal函数连接信号和信号处理函数
2.通过F_SETOWN来设置设备文件的拥有者为本进程,这样从设备发出的信号才能被本设备接收到
3.通过F_SETFL设置设备文件FASYNC标记以支持异步通知机制

信号的释放

在设备驱动和应用程序的异步通知中,在应用程序端捕获信号是不够的,信号源头来自于设备驱动端,
所以需要在合适的时机让设备驱动释放信号,在设备驱动程序中添加相应的代码
为了支持异步通知机制,驱动程序需要涉及3项工作
1.支持F_SETOWN命令,能在这个控制命令的处理中设置filp->f_owner为对应的进程ID
2.支持F_SETFL命令的处理,等当FASYNC标志改变时,驱动程序中的fasync()函数将得以执行,
所以驱动中得实现fasync函数
3.在设备资源获得的时候,再调用kill_fasync函数激发相应的信号
可以发现这3项工作分别对应着应用程序的三项工作
设备驱动中异步通知编程比较简单,主要用到一项数据结构和两个函数。
数据结构是fasync_struct结构体
两个函数分别是:
1.处理FASYNC标志变更的函数
int fasync_helper(int fd, struct file *filp, 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 *async_queue; //异步结构体指针
};

在设备驱动的fasync函数中,只需要简单地将函数的3个参数以及fasync_struct结构体指针
作为第4个参数传入fasync_helper函数中就可
static int xxx_fasync(int fd, struct file *filp, int mode)
{
    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_ops)
{
    struct xxx_dev *dev = filp->private_data;
    ...
    //因为该设备被写入之后,它对于用户而言是可读的,所以产生异步读信号
    if (dev->async_queue)
    {
        kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
    }
}
最后在文件关闭时,即在设备驱动的release()函数中,应调用设备驱动的fasync()
函数将文件从异步通知的列表中删除。
static int xxx_realease(struct inode *inode, struct file *filp)
{
    //将文件从异步通知列表中删除
    xxx_fasync(-1, filp, 0);
}

Linux的异步IO

AIO概念和GNU C库的AIO

Linux最常用的的IO模型是同步IO模型,也就是说当IO请求发出的时候应用程序就会阻塞,直到
请求满足为止。
对于异步IO,应用程序在发起IO动作之后,不阻塞接着执行,它要么过一段时间来查询之前的IO
请求完成情况,要么IO请求完成了会自动调用绑定的回调函数
Linux的AIO有多种实现方式,其中一种实现是用户空间的glibc库中实现的。
还有一种方式是内核空间实现的Linux AIO,异步IO是Linux2.6以后版本内核的一个标准特性。
所以我这边只打算研究下内核空间如何实现Linux AIO的

Linux内核AIO与libaio

对于块设备而言,AIO可以一次性发出大量的read/write 调用并且通过通用块层的IO调度来获
得更好的性能;对于网络设备而言,在socket层面上,可以使用AIO,让CPU和网卡的收发动作充
分交叠以改善吞吐量性能,AIO一般用在这两个地方

用户空间调用AIO

在用户空间我们一般要结合内核AIO的系统调用来进行AIO的代码编写
AIO的读写请求都用io_submit下发。下发前通过io_prep_pwrite()和io_prep_pread()
生成iocb的结构体,作为io_submit的参数。这个结构体指定了读写类型、起始地址、长度和
设备标识符等信息。读写请求下发之后,使用io_getevents()函数等待IO完成时间。
set_callback()则可设置一个AIO完成的回调函数

AIO与设备驱动

用户空间调用io_submit()后,对应于用户传递的每一个iocb结构,内核会对应的生产一个
kiocb结构。file_operations包含3个与AIO相关的成员函数
aio_read,aio_write,aio_fsync
io_submit系统调用间接引起了file_operations中aio_read和aio_write的调用。

总结

异步IO可以让应用程序在等待IO操作的同时进行别的操作。Linux2.6之后的内核包含对AIO
的支持,为用户空间提供了统一的异步IO接口。
使用信号量可以实现设备驱动与用户程序之间的异步通信。

你可能感兴趣的:(驱动开发)