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

1 异步通知的概念与作用

        阻塞与非阻塞访问、 poll() 函数提供了较好的解决设备访问的机制, 但是如果有了异步通知, 整套机制则更加完整了。

        异步通知的意思是:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这一点非常类似于硬件上“中断”的概念, 比较准确的称谓是“信号驱动的异步I/O”。信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。

        阻塞I/O意味着一直等待设备可访问后再访问,非阻塞I/O中使用poll()意味着查询设备是否可访问,而异步通知则意味着设备通知用户自身可访问,之后用户再进行I/O处理。由此可见, 这几种I/O方式可以相互补充。

2 Linux异步通知编程

2.1 Linux信号

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

        除了SIGSTOP和SIGKILL两个信号外, 进程能够忽略或捕获其他的全部信号。 一个信号被捕获的意思是当一个信号到达时有相应的代码处理它。 如果一个信号没有被这个进程所捕获, 内核将采用默认行为处理。

 2.2 信号的捕获(注册)

2.2.1 signal()

        在用户程序中, 为了捕获信号, 可以使用signal() 函数来设置对应信号的处理函数:

void (*signal(int signum, void (*handler))(int)))(int);

//函数原型可以分解为
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler));

        第一个参数signum指定信号的值, 第二个参数handler指定针对前面信号值的处理函数, 若为SIG_IGN, 表示忽略该信号; 若为SIG_DFL, 表示采用系统默认方式处理信号; 若为用户自定义的函数, 则信号被捕获到后, 该函数将被执行。

        如果signal() 调用成功, 它返回最后一次为信号signum绑定的处理函数的handler值, 失败则返回SIG_ERR。

        在进程执行时, 按下“Ctrl+C”将向其发出SIGINT信号, 正在运行kill的进程将向其发出SIGTERM信号, 代码清单9.1的进程可捕获这两个信号并输出信号值。

//signal使用范例
void sigterm_handler(int signo)
{
    printf("Have caught sig N.O. %d\n", signo);
    exit(0);
}

int main(void)
{
    signal(SIGINT, sigterm_handler);
    signal(SIGTERM, sigterm_handler);
    while(1);

    return 0;
}

2.2.2 sigaction()

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

        该函数的第一个参数为信号的值, 可以是除SIGKILL及SIGSTOP外的任何一个有效的信号。

        第二个参数是指向结构体sigaction的一个实例的指针, 在结构体sigaction的实例中, 指定了对特定信号的新的处理方式, 若为空, 则进程会以缺省方式对信号处理;

        第三个参数oldact指向的对象用来保存原来对相应信号的处理函数, 可指定oldact为NULL。 如果把第二、 第三个参数都设为NULL, 那么该函数可用于检查信号的有效性。

2.2.3 信号实现异步通知

        通过signal(SIGIO, input_handler) 对标准输入文件描述符STDIN_FILENO启动信号机制。 用户输入后, 应用程序将接收到SIGIO信号, 其处理函数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);
    fcntl(STDIN_FILENO, F_SETOWN, getpid()); // 设置本进程为STDIN_FILENO标准输入fd的拥有者

    oflags = fcntl(STDIN_FILENO, F_GETFL);
    fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);  //这两行为了启用异步通知机制,对设备设置FASYNC标志

    /* 最后进入一个死循环, 仅为保持进程不终止, 如果程序中
    没有这个死循会立即执行完毕 */
    while (1);
}

程序执行结果

        从中可以看出, 当用户输入一串字符后, 标准输入设备释放SIGIO信号, 这个信号“中断”与驱使对应的应用程序中的input_handler() 得以执行, 并将用户输入显示出来。

        由此可见, 为了能在用户空间中处理一个设备释放的信号, 它必须完成3项工作。

        1) 通过F_SETOWN IO控制命令设置设备文件的拥有者为本进程, 这样从设备驱动发出的信号才能被本进程接收到。
        2) 通过F_SETFL IO控制命令设置设备文件以支持FASYNC, 即异步通知模式。
        3) 通过signal( ) 函数连接信号和信号处理函数。
        

 2.3 信号的释放(发送)

        在设备驱动和应用程序的异步通知交互中,仅仅在应用程序端捕获信号是不够的,因为信号的源头在设备驱动端。因此,应该在合适的时机让设备驱动释放信号,在设备驱动程序中增加信号释放的相关代码。

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

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

        驱动中的上述3项工作和应用程序中的3项工作是一一对应的, 图9.2所示为异步通知处理过程中用户空间和设备驱动的交互。

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

//支持异步通知的设备结构体模板
//和其他的设备驱动一样, 将fasync_struct结构体指针放在设备结构体中仍然是最佳选择。
struct xxx_dev {
    struct cdev cdev; /* cdev结构体*/
    ...
    struct fasync_struct *async_queue; /* 异步结构体指针 */
};

 2.3.1 处理FASYNC标志变更的函数

int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);

        在设备驱动的fasync() 函数中, 只需要简单地将该函数的3个参数以及fasync_struct结构体指针的指针作为第4个参数传入fasync_helper() 函数即可。 支持异步通知的设备驱动程序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);
}

2.3.2 释放信号用的函数

void kill_fasync(struct fasync_struct **fa, int sig, int band);

        在设备资源可以获得时, 应该调用kill_fasync() 释放SIGIO信号。 在可读时, 第3个参数设置为POLL_IN, 在可写时, 第3个参数设置为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);
    ..
}

2.4 release()时删除异步通知

        最后, 在文件关闭时, 即在设备驱动的release() 函数中, 应调用设备驱动的fasync() 函数将文件从异步通知的列表中删除。 支持异步通知的设备驱动release() 函数的模板如下:

static int xxx_release(struct inode *inode, struct file *filp)
{
    /* 将文件从异步通知列表中删除 */
    xxx_fasync(-1, filp, 0);
    ...
    return 0;
}

3 Linux异步I/O

3.1 AIO与GUN C库AIO

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

        异步I/O:应用程序发起I/O动作后, 直接开始执行, 并不等待I/O结束, 它要么过一段时间来查询之前的I/O请求完成情况, 要么I/O请求完成了会自动被调用与I/O完成绑定的回调函数。

         glibc的AIO主要包括如下函数。

3.1.1 aio_read()

int aio_read( struct aiocb *aiocbp );

        aio_read() 函数请求对一个有效的文件描述符进行异步读操作。 这个文件描述符可以表示一个文件、 套接字, 甚至管道。

        aio_read() 函数在请求进行排队之后会立即返回(尽管读操作并未完成) 。 如果执行成功, 返回值就为0; 如果出现错误, 返回值就为-1, 并设置errno的值。

        参数aiocb(AIO I/O Control Block) 结构体包含了传输的所有信息, 以及为AIO操作准备的用户空间缓冲区。 在产生I/O完成通知时, aiocb结构就被用来唯一标识所完成的I/O操作。

3.1.2 aio_write()

int aio_write( struct aiocb *aiocbp );

        aio_write( ) 函数用来请求一个异步写操作。aio_write( ) 函数会立即返回, 并且它的请求已经被排队( 成功时返回值为0, 失败时返回值为-1,并相应地设置errno) 。

3.1.3 aio_error( )

int aio_error( struct aiocb *aiocbp );

         aio_error( ) 函数被用来确定请求的状态。

这个函数可以返回以下内容:

        EINPROGRESS: 说明请求尚未完成。
        ECANCELED: 说明请求被应用程序取消了。
        -1: 说明发生了错误, 具体错误原因由errno记录

 3.1.4 aio_return( )

ssize_t aio_return( struct aiocb *aiocbp );

        异步I/O和同步阻塞I/O方式之间的一个区别是不能立即访问这个函数的返回状态, 因为异步I/O并没有阻塞在read( ) 调用上。 在标准的同步阻塞read( ) 调用中, 返回状态是在该函数返回时提供的。 但是在异步I/O中, 我们要使用aio_return( ) 函数。

        只有在aio_error( )调用确定请求已经完成( 可能成功,也可能发生了错误)之后,才会调用这个函数。 aio_return( ) 的返回值就等价于同步情况中read( )或write( )系统调用的返回值( 所传输的字节数如果发生错误, 返回值为负数)。

用户空间异步读举例

        它首先打开文件, 然后准备aiocb结构体, 之后调用aio_read( &my_aiocb) 进行提出异步读请求, 当aio_error( &my_aiocb)==EINPROGRESS, 即操作还在进行中时, 一直等待, 结束后通过aio_return( &my_aiocb) 获得返回值。

include <aio.h>
...
    int fd, ret;
    struct aiocb my_aiocb;
    fd = open("file.txt", O_RDONLY);
    if (fd < 0)
        perror("open");

    /* 清零aiocb结构体 */
    bzero(&my_aiocb, sizeof(struct aiocb));

    /* 为aiocb请求分配数据缓冲区 */
    my_aiocb.aio_buf = malloc(BUFSIZE + 1);
    if (!my_aiocb.aio_buf)
        perror("malloc");
    
    /* 初始化aiocb的成员 */
    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_iocb)) > 0) {
    /* 获得异步读的返回值 */
    } else {
    /* 读失败, 分析errorno */
    }

3.1.5  aio_suspend()

int aio_suspend( const struct aiocb *const cblist[],
       int n, const struct timespec *timeout );

        用户可以使用aio_suspend() 函数来阻塞调用进程, 直到异步请求完成为止。 调用者提供了一个aiocb引用列表, 其中任何一个完成都会导致aio_suspend() 返回。

//用户空间异步I/O aio_suspend() 函数使用例程
    struct aioct *cblist[MAX_LIST]
    /* 清零aioct结构体链表 */
    bzero( (char *)cblist, sizeof(cblist) );
    /* 将一个或更多的aiocb放入aioct的结构体链表中 */
    cblist[0] = &my_aiocb;
    ret = aio_read(&my_aiocb);
    ret = aio_suspend(cblist, MAX_LIST, NULL );

        当然, 在glibc实现的AIO中, 除了上述同步的等待方式以外, 也可以使用信号或者回调机制来异步地标明AIO的完成。

3.1.6 aio_cancel()

int aio_cancel(int fd, struct aiocb *aiocbp);

        aio_cancel() 函数允许用户取消对某个文件描述符执行的一个或所有I/O请求。

        要取消一个请求, 用户需提供文件描述符和aiocb指针。 如果这个请求被成功取消了, 那么这个函数就会返回AIO_CANCELED。 如果请求完成了, 这个函数就会返AIO_NOTCANCELED。

        要取消对某个给定文件描述符的所有请求, 用户需要提供这个文件的描述符, 并将aiocbp参数设置为NULL。 如果所有的请求都取消了, 这个函数就会返回AIO_CANCELED; 如果至少有一个请求没有被取消, 那么这个函数就会返回AIO_NOT_CANCELED; 如果没有一个请求可以被取消, 那么这个函数就会返回AIO_ALLDONE。 然后, 可以使用aio_error() 来验证每个AIO请求, 如果某请求已经被取消了, 那么aio_error() 就会返回-1, 并且errno会被设置为ECANCELED。

3.1.7 lio_listio()

int lio_listio( int mode, struct aiocb *list[], int nent, struct sigevent *sig );

        lio_listio() 函数可用于同时发起多个传输。 这个函数非常重要, 它使得用户可以在一个系统调用中启动大量的I/O操作。

        mode参数可以是LIO_WAIT或LIO_NOWAIT。 LIO_WAIT会阻塞这个调用, 直到所有的I/O都完成为止。 但是若是LIO_NOWAIT模型, 在I/O操作进行排队之后, 该函数就会返回。 list是一个aiocb引用的列表, 最大元素的个数是由nent定义的。 如果list的元素为NULL, lio_listio() 会将其忽略。

//用户空间异步I/O lio_listio() 函数使用例程
struct aiocb aiocb1, aiocb2;
struct aiocb *list[MAX_LIST];
...
/* 准备第一个aiocb */
aiocb1.aio_fildes = fd;
aiocb1.aio_buf = malloc( BUFSIZE+1 );
aiocb1.aio_nbytes = BUFSIZE;
aiocb1.aio_offset = next_offset;

/* 异步读操作*/
aiocb1.aio_lio_opcode = LIO_READ;  //因为是进行异步读操作, 所以操作码为LIO_READ, 对于写操作来说, 应该使用LIO_WRITE作为操作码, 而LIO_NOP意味着空操作。

... /* 准备多个aiocb */
bzero( (char *)list, sizeof(list) );

/* 将aiocb填入链表*/
list[0] = &aiocb1;
list[1] = &aiocb2;
...
ret = lio_listio( LIO_WAIT, list, MAX_LIST, NULL ); /* 发起大量I/O操作*/

3.2 Linux内核AIO与libaio

        Linux AIO也可以由内核空间实现, 异步I/O是Linux 2.6以后版本内核的一个标准特性。 对于块设备而言, 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完成的回调函数。

3.3 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);

        io_submit() 系统调用间接引起了file_operations中的aio_read() 和aio_write() 的调用。
        AIO一般由内核空间的通用代码处理, 对于块设备和网络设备而言, 一般在Linux核心层的代码已经解决。 字符设备驱动一般不需要实现AIO支持。 Linux内核中对字符设备驱动实现AIO的特例包括drivers/char/mem.c里实现的null、 zero等, 由于zero这样的虚拟设备其实也不存在在要去读的时候读不到东西的情况, 所以aio_read_zero() 本质上也不包含异步操作,以下代码可以一窥iovec的全貌。

//zero设备的aio_read实现
static ssize_t aio_read_zero(struct kiocb *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos)
{
    size_t written = 0;
    unsigned long i;
    ssize_t ret;

    for (i = 0; i < nr_segs; i++) {
        ret = read_zero(iocb->ki_filp, iov[i].iov_base, iov[i].iov_len, &pos);
        if (ret < 0)
            break;
        written += ret;
    }

    return written written : -EFAULT;
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值