设备驱动中的异步通知与异步I/O

在设备驱动中使用异步通知可以使得再进行对设备的访问时,由驱动主动通知用户程序进行访问。这样,使用非阻塞I/O的应用程序无需轮询机制查询设备是否可访问,而阻塞访问也可以被类似“中断”的异步通知所取代。除了异步通知以为,应用还可以在发起I/O请求后,立即返回。之后,在查询I/O完成情况,或者I/O完成后被返回。这个过程为异步I/O。

阻塞与非阻塞访问、poll函数提供了较好的解决设备访问机制,但是如果有了异步通知,整套机制则更加完整了。异步通知的意思是:一旦设备就绪,则主动通知用户程序,这样用户程序就不需要查询设备状态,这一点非常类似于硬件上的“中断”的概念,比较准确的称呼是“信号驱动的异步I/O”。信号是在软件层次上对终端机制的一种模拟,在原理上,一个进程手收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不需要任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。

阻塞I/O意味着一直等待设备可访问后访问,非阻塞I/O中使用了poll函数意味着查询设备是或否可访问,而异步通知则意味着设备通知用户程序自身可访问,之后用户在进行I/O处理。由此可见,这几种I/O方式可以相互补充。阻塞、非阻塞、异步通知本身没有优劣,应根据不同的应运场景合理选择。

异步通知使用信号来实现,信号是进程间通信(IPC)的一种机制,linux可用的信号有30多种,可以百度查询。除了SIGSYOP与SIGKILL两个信号外,进程能够忽略或获取其他全部信号。一个信号被捕获的意识是当一个信号到达是有相应的代码处理它。如果一个信号没有被这个进程捕获,内核将采取默认行为处理。

信号的接收函数

void (*singal(int signum,void(* handler))(int))(int);
//分解为
typedef void (* sighsndler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
//第一个参数是指定信号的值,第二个参数时指定针对前面信号值的处理函数,若为SIG_IGN,表示忽略给信号,若为SIG_DFL,表示使用
系统默认的方式处理信号,若为用户自定义的函数,则信号被捕获后,该函数将被执行。若signal()函数调用成功,他返回最后一次为
信号signum绑定的处理函数的handler值,失败返回SIG_ERR。

获取“Ctrl+C”并打印相应信号值

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
void sigtem_handler(int signo)
{
    printf("Have change sig N.O %d\n",signo);
    exit(0);
}

int main()
{
    signal(SIGINT,sigtem_handler);
 //   signal(SIGTERM,sigtem_handler);
    while(1);
    return 0;
}

除了signal函数外,sigaction()函数可以用于改变进程接收到信号后的行为,它的原型为:

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
//第一个参数为信号的值,可以是除却SIGKILL及SIGSTOP外的任何一个特定有效的信号。第二个参数是指向结构体sigaction的一个实例的指针,
在结构体sigaction的实例中,指定了对特定信号的处理函数,若为空,则进程会以缺省方式对信号处理;第三个参数oldcat 指向的对象用来
保存原来对相应信号的处理函数,可指定oldact为NULL。如果把第二、第三个参数都设置为NULL,那么刚函数可以用来检测信号的有效性。

为了使设备支持异步通知机制,驱动程序中涉及3项工作

  1. 支持F_SETOWN命令,能在这个控制命令处理中设置filp->f_owner为对应进程ID。不过西乡工程已有内核完成,设备驱动无需处理。、
  2. 支持F_SETFL命令的处理,每当FASYNC标志改变时,驱动程序中的fasync()函数将得以执行。因此,驱动中应该实现fasync()函数。
  3. 在设备资源可获得时,调用kill_fasync()函数激发相应的信号。

设备驱动中异步通知编程比较简单,主要用到一项数据结构和两个函数,数据结构是fasync_struct结构体,两个函数分别是:

//处理FASYNC标志变更的函数
int fasync_helper(int fd,struct file *filp,int mode,struct fasync_struct **fa);
//释放信号用的函数
void kill_fasync(struct fasync_struct **fa,int sig,int band);

 异步通知函数模板

1.在设备结构体中添加异步结构体
struct xxx_dev{
    struct cdev cdve;
    ...
    struct fasync_struct *async_queue; //异步通知结构体
};
2.file_operations结构体中添加fasync
static struct file_operations xxx_fops{
    ...
    .fasync = xxx_fasync,
};
3.设备驱动fasync()函数模板
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);
} //即只需要把 异步结构体 作为fasync_helper的第四个参数返回即可
4.在write函数中添加 异步信号的读取
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);//调用kill_fasync 释放信号
}
5.在文件关闭,即release函数中应用函数fasync将文件从异步通信列表中删除
static int xxx_release(struct inode *inode,struct file *filp)
{
    xxx_fasync(-1,filp,0);
    ...
    return 0;
}

 修改阻塞I/O博文中的驱动代码,编译加载,测试代码

#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>

static void signalio_handler(int signum)
{
    printf("receive a signal from mymodules,signalnum:%d\n",signum);
}

int main()
{
    int fd,oflags;
    fd = open("/dev/mymodules",O_RDWR,S_IRUSR | S_IWUSR);
    if(fd != -1){
        signal(SIGIO,signalio_handler);
        fcntl(fd,F_SETOWN,getpid());
        oflags = fcntl(fd,F_GETFL);
        fcntl(fd,F_SETFL,oflags | FASYNC);
        while(1){
            sleep(100);
        }
    }else
        printf("open device error\n");
    return 0;
}

编译后测试

下面理解一下Linux的异步I/O:AIO

linux中做常用的输入输出(I/O)模型是同步I/O。在这个模型中,当请求发出后,应用程序就会阻塞,直到请求满足为止。这是一种很好的解决方案,调用应用程序在等待I/O请求完成是不需要占用CPU资源。但是在许多应用场=场景中,I/O请求可能需要与CPU消耗产生交叠,已充分利用CPU和I/O提高吞吐率。

linux的AIO有多种实现方式,其中一种是在用户空间的glibc库中实现,它本质上是借用了多线程模型,用开启的线程以同步的方法来做I/O,新的AIO辅助线程与发起AIO的线程以pthread_cond_signal()的形式进行线程间的同步。glibc的AIO主要包含如下函数:

//请求对一个有效文件描述符进行异步读操作。
int aio_read(struct aiocb *aiocbp); //在请求进行排队之后立即返回(尽管读操作并未完成)。如果执行成功,返回值为0,失败返回-1,并设置errno的值,aiocb结构体包含了传输的所有信息。

//请求一个异步写操作
int aio_write(struct aiocb *aiocbp);//请求排队候立即返回,成功返回0,失败返回-1.并设置errno。

//确定请求的状态
int aio_error(struct aiocb *aiocbp); //返回 EINPROGRESS 说明尚未完成, ECANCELED 说明请求被应用程序取消了,-1 说明发生了错误,具体原因有errno记录。

//获取I/O操作的返回状态
ssize_t aio_return(struct aiocb *aiocbp); //只有在调用 aio_error 确定请求已经完成(可能完成,可能发生错误)之后,擦会调用,aio_return 的返回值就等价于同步情况的read()或write()系统调用的返回值(所传输的字节数如果发生错误,返回值为负数)。

//阻塞调用进程,知道异步请求完成为止
int aio_suspend(const struct aiocb *const cblistp[],int n,const struct timespec *timeout);

//允许用户取消对某个文件描述符执行的一个或所有I/O请求
int aio_cancel(int fd,struct aiocb *aiocbp);//如果请求被成功取消,返回 AIO_CANCLED 失败返回 AIO_NOTCANCELED。

//用于同步发起多个传输
int lio_listo(int mode,struct aiocb *list[],int nent,struct sigeven *sig);//参数mode可以是 LIO_WAIT 或 LIO_NOWAIT,前者阻塞这个调用,知道所有的I/O完成为止。后者I/O进行排队之后,函数就会返回。list是一个aiocb引用的列表,最大元素个数由nent定义的。如果list的元素为NULL,lio_listio()会将其忽略。

 AIO glibc的测试例子

#include <stdio.h>
#include <aio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

#define BUFSIZE 20

int main()
{
    int fd,ret;
    struct aiocb my_aiocb;

    fd = open("./module_test_pthread.c",O_RDONLY);
    if(fd < 0)
        perror("open");
    bzero(&my_aiocb,sizeof(struct aiocb));

    my_aiocb.aio_buf = malloc(BUFSIZE + 1);
    if(!my_aiocb.aio_buf)
        perror("malloc");

    my_aiocb.aio_fildes = fd;
    my_aiocb.aio_nbytes = BUFSIZE;
    my_aiocb.aio_offset = 0;

    ret = aio_read(&my_aiocb);
    if(ret < 0)
        perror("aio_read");
    while(aio_error(&my_aiocb) == EINPROGRESS)
        continue;

    if((ret = aio_return(&my_aiocb)) > 0)
        printf("ok\n");
    else
        printf("error\n");
    
    printf("Hello world\n");
    return 0;
}

 使用man_read 查看用法

 使用 gcc test.c -lrt 编译。

 Linux AIO 也可以由内核空间实现。对于块设备而言,AIO可以一次性发出大量的read/write 调用并通过块层的I/O 调度来获得更好的性能,用户程序也可以减少过多的同步负载,还可以在业务逻辑控制中更贱灵活的进行并发控制和负载均衡。相较于glibc 的用户空间多线程同步等实现减少了线程的负载和上下文切换等。对于网络设备而言,在socket层面上,也可以使用AIO,让CPU和网卡的收发动作充分交叠以改善吞吐性能。

在用户空间,一般结合libaio来进行内核AIO的系统调用。内核AIO提供的系统调用主要包括:

int io_setup(int maxevents, io_context_t *ctxp); 
int io_destroy(io_context_t ctx); 
int io_submit(io_context_t ctx, long nr, struct iocb *ios[]); 
int io_cancel(io_context_t ctx, struct iocb *iocb, struct io_event *evt); 
int io_getevents(io_context_t ctx_id, long min_nr, long nr, struct io_event *events,    struct timespec *timeout); 
void io_set_callback(struct iocb *iocb, io_callback_t cb); 
void io_prep_pwrite(struct iocb *iocb, int fd, void *buf, size_t count, long long offset); 
void io_prep_pread(struct iocb *iocb, int fd, void *buf, size_t count, long long offset); 
void io_prep_pwritev(struct iocb *iocb, int fd, const struct iovec *iov, int iovcnt,    long long offset); 
void io_prep_preadv(struct iocb *iocb, int fd, const struct iovec *iov, int iovcnt,    long long offset);

AIO的读写请求都用io_submit() 下发。下发前通过io_prep_pwrite() 和 io_prep_pread() 生成 iocb 的结构体,作为io_submit() 的参数。这个结构体指定了读写类型、起始地址、长度和标志符的信息。读写请求下发之后,使用io_getevents() 函数等待I/O完成事件。io_set_callback() 则可设置一个AIO完成回调函数。

用户空间调用io_submit() 后,对应于用户传递的每一个iocb 结构,内核会产生一个与之对应的kiocb结构。file_operations包含3个与AIO相关的成员函数:

ssize_t (*aio_read) (struct kiocb *iocb, const struct iovec *iov, unsigned long    nr_segs, loff_t pos); 
ssize_t (*aio_write) (struct kiocb *iocb, const struct iovec *iov, unsigned    long nr_segs, loff_t pos); 
int (*aio_fsync) (struct kiocb *iocb, int datasync);

AIO一般由内核空间的通用代码处理,对于块设备和网络设备而言,一般linux核心层的代码已经解决。字符设备驱动一般不需要实现AIO支持。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章