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

Posted zcj仲从建

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了:Linux设备驱动中的异步通知与同步I/O相关的知识,希望对你有一定的参考价值。

在设备驱动中使用异步通知可以使得对设备的访问可进行时,由驱动主动通知应用程序进行访问。因此,使用无阻塞I/O的应用程序无需轮询设备是否可访问,而阻塞访问也可以被类似“中断”的异步通知所取代。异步通知类似于硬件上的“中断”概念,比较准确的称谓是“信号驱动的异步I/O”。

9.1 异步通知的概念和作用

  • 异步通知:一旦设备就绪,则主动通知应用程序,该应用程序无需查询设备状态
  • 几种通知方式比较:
    • 阻塞I/O :一直等待设备可访问后开始访问
    • 非阻塞I/O:使用poll()查询设备是否访问
    • 异步通知 :设备主动通知用户应用程序
    • -

9.2 linux异步通知编程

9.2.1 linux信号

  • 作用:linux系统中,异步通知使用信号来实现
SIGHUP     终止进程     终端线路挂断
SIGINT     终止进程     中断进程
SIGQUIT   建立CORE文件终止进程,并且生成core文件
SIGILL   建立CORE文件       非法指令
SIGTRAP   建立CORE文件       跟踪自陷
SIGBUS   建立CORE文件       总线错误
SIGSEGV   建立CORE文件       段非法错误
SIGFPE   建立CORE文件       浮点异常
SIGIOT   建立CORE文件       执行I/O自陷
SIGKILL   终止进程     杀死进程
SIGPIPE   终止进程     向一个没有读进程的管道写数据
SIGALARM   终止进程     计时器到时
SIGTERM   终止进程     软件终止信号
SIGSTOP   停止进程     非终端来的停止信号
SIGTSTP   停止进程     终端来的停止信号
SIGCONT   忽略信号     继续执行一个停止的进程
SIGURG   忽略信号     I/O紧急信号
SIGIO     忽略信号     描述符上可以进行I/O
SIGCHLD   忽略信号     当子进程停止或退出时通知父进程
SIGTTOU   停止进程     后台进程写终端
SIGTTIN   停止进程     后台进程读终端
SIGXGPU   终止进程     CPU时限超时
SIGXFSZ   终止进程     文件长度过长
SIGWINCH   忽略信号     窗口大小发生变化
SIGPROF   终止进程     统计分布图用计时器到时
SIGUSR1   终止进程     用户定义信号1
SIGUSR2   终止进程     用户定义信号2
SIGVTALRM 终止进程     虚拟计时器到时

9.2.2 信号的接收

  • 信号捕获函数signal()
    • 参数:
      • signum:信号值
      • handler:针对signum的处理函数
        • 若为SIG_IGN:忽略该信号
        • 若为SIG_DFL:系统默认方式处理
        • 若为用户自定义函数:信号被捕获,该函数被执行
    • 返回值
      • 成功:最后一次为信号signum绑定的处理函数的handler值
      • 失败:返回SIG_ERR
    • sigaction()
      • 作用:改变进程接收到特定信号后的行为
      • 参数
        • signum:信号值
          • 除SIG_KILL及SIG_STOP以外的一个特定有效的信号
        • act:指向结构体sigaction的一个实例的指针
          • 在结构体sigaction中,指定了处理信号的函数,若为空则进程会以缺省值的方式处理信号
        • oldact:保存原来对应的信号的处理函数,可设为NULL
int sigaction(int signo,const struct sigaction *restrict act, struct sigaction *restrict oact);
  • 实例:使用信号实现异步通知
    • 在用户空间处理设备释放信号的准备工作
      • 通过F_SETOWN IO控制命令设置设备文件的拥有者为本进程,以使信号被本进程捕获
      • 通过F_SETFL IO控制命令设置设备文件以支持FASYNC,及异步通知模式
      • 通过signal()函数连接信号和信号处理函数
//启动信号机制

void sigterm_handler(int sigo)
{

char data[MAX_LEN];
int len;
len = read(STDIN_FILENO,&data,MAX_LEN);
data[len] = 0;
printf("Input available:%s\n",data);
exit(0);

}

int main(void)
{

int oflags;
//启动信号驱动机制

signal(SIGIO,sigterm_handler);
fcntl(STDIN_FILENO,F_SETOWN,getpid());
oflags = fcntl(STDIN_FILENO,F_GETFL);
fctcl(STDIN_FILENO,F_SETFL,oflags | FASYNC);
//建立一个死循环,防止程序结束

whlie(1);

return 0;

}

9.2.3 信号的释放 (在设备驱动端释放信号)

  • 为了是设备支持异步通知机制,驱动程序中涉及以下3项工作
    • 支持F_SETOWN命令,能在这个控制命令处理中设置filp->f_owner为对应的进程ID。不过此项工作已由内核完成,设备驱动无须处理
    • 支持F_SETFL命令处理,每当FASYNC标志改变时,驱动函数中的fasync()函数得以执行。因此,驱动中应该实现fasync()函数
    • 在设备资源中可获得,调用kill_fasync()函数激发相应的信号
    • -
  • 设备驱动中异步通知编程:
    • 处理FASYNC标志变更函数:fasync_helper()
    • 释放信号的函数:kill_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);
  • 将fasync_struct结构体指针放到设备结构体中是最佳的选择
//异步通知的设备结构体模板
struct xxx_dev{
    struct cdev cdev;
    ...
    struct fasync_struct *async_queue;//异步结构体指针
};
  • 在设备驱动中的fasync()函数中,只需简单地将该函数的3个参数以及fasync_struct结构体指针的指针作为第四个参数传入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 *ppos)

{
    struct xxx_dev *dev = filp->private_data;
    ...
    //产生异步读信息
    if(dev->async_queue)
    kill_fasync(&dev->async_queue,GIGIO,POLL_IN);
    ...
}
  • 最后在文件关闭时,要将文件从异步通知列表中删除
int xxx_release(struct inode *inode,struct file *filp)

{
    //将文件从异步通知列表中删除
    xxx_fasync(-1,filp,0);
    ...
    return 0;
}

9.4 linux异步I/O

9.4.1 AIO概念与GNU C库 AIO

9.4.1.1 AIO概念

  • 同步I/O:linux系统中最常用的输入输出(I/O)模型是同步I/O,在这个模型中,当请求发出后,应用程序就会阻塞,知道请求满足

  • 异步I/O:I/O请求可能需要与其它进程产生交叠

  • Linux 系统中最常用的输入/输出(I/O)模型是同步 I/O

    • 在这个模型中,当请求发出之后,应用程序就会阻塞,直到请求满足为止
    • 调用应用程序在等待 I/O 请求完成时不需要使用任何中央处理单元(CPU)
    • 在某些情况下,I/O 请求可能需要与其他进程产生交叠,可移植操作系统接口(POSIX)异步 I/O(AIO)应用程序接口(API)就提供了这种功能

9.4.1.1 AIO系列API:

  • aio_read–异步读
    • 作用:请求对一个有效的文件描述符进行异步读写操作
      • 请求进行排队之后会立即返回
      • 这个文件描述符可以表示一个文件、套接字,甚至管道
    • 参数aiocb:结构体包含了传输的所有信息,以及为AIO操作准备的用户空间缓存区
    • 返回值
      • 成功:返回0
      • 失败:返回-1,并设置errno的值
int aio_read( struct aiocb *aiocbp );
  • aio_write–异步写
    • 作用:请求一个异步写操作
      • 请求进行排队之后会立即返回
      • 这个文件描述符可以表示一个文件、套接字,甚至管道
    • 参数aiocb:结构体包含了传输的所有信息,以及为AIO操作准备的用户空间缓存区
    • 返回值
      • 成功:返回0
      • 失败:返回-1,并设置errno的值
int aio_write( struct aiocb *aiocbp );
  • aio_error
    • 作用:确定请求的状态
    • 参数aiocb:结构体包含了传输的所有信息,以及为AIO操作准备的用户空间缓存区
    • 返回值
      • EINPROGRESS:说明请求尚未完成
      • ECANCELED:说明请求被应用程序取消
      • 失败:返回-1,并设置errno的值
int aio_error( struct aiocb *aiocbp );
  • aio_return–获得异步操作的返回值
    • 异步 I/O 和标准块 I/O 之间的另外一个区别是不能立即访问这个函数的返回状态,因为并没有阻塞在 read()调用上
    • 在标准的 read()调用中,返回状态是在该函数返回时提供的。但是在异步 I/O 中,我们要使用 aio_return()函数
    • 只有在 aio_error()调用确定请求已经完成(可能成功,也可能发生了错误)之后,才会调用这个函数
    • 参数aiocb:结构体包含了传输的所有信息,以及为AIO操作准备的用户空间缓存区
    • 返回值
      • 成功:返回所传输的字节数
      • 失败:返回-1
ssize_t aio_return( struct aiocb *aiocbp );
  • aio_suspend–挂起异步操作,直到异步请求完成为止
    • 作用:挂起(或阻塞)调用进程,直到异步请求完成为止,调用者提供了一个 aiocb 引用列表,其中任何一个完成都会导致 aio_suspend()返回
int aio_suspend( const struct aiocb *const cblist[], int n, const struct timespec *timeout );
  • aio_cancel–取消异步请求
    • 作用:允许用户取消对某个文件描述符执行的一个或所有 I/O 请求
    • 要求:
      • 如果要取消一个请求,用户需提供文件描述符和 aiocb 引用
        • 函数返回AIO_CANCELED:请求被成功取消
        • 函数返回AIO_NOTCANCELED:请求完成
      • 如果要取消对某个给定文件描述符的所有请求,用户需要提供这个文件的描述符以及一个对 aiocbp 的 NULL 引用
        • 函数返回AIO_CANCELED:表明所有的请求都取消了
        • 函数返回AIO_NOTCANCELED:表明至少有一个请求没有被取消
        • 函数返回AIO_ALLDONE:表明没有一个请求可以被取消
      • 使用 aio_error()来验证每个 AIO 请求
        • aio_error()返回-1并且设置了errno被设置为ECANCELED:表明某个请求已经被取消了
int aio_cancel( int fd, struct aiocb *aiocbp );
  • lio_listio–同时发起多个传输(一次系统调用可以启动大量的I/O操作)
    • 作用:这个函数非常重要,它使得用户可以在一个系统调用(一次内核上下文切换)中启动大量的 I/O 操作
    • 参数
      • mode:可以是 LIO_WAIT 或 LIO_NOWAIT
        • LIO_WAIT 会阻塞这个调用,直到所有的 I/O 都完成为止
        • 在操作进行排队之后,LIO_NOWAIT 就会返回
      • list :是一个 aiocb 引用的列表,最大元素的个数是由 nent 定义的
        • 如果 list 的元素为 NULL,lio_listio()会将其忽略。
int lio_listio( int mode, struct aiocb *list[], int nent, struct sigevent *sig );

9.4.2 Linux内核AIO与libaio

  • linux AIO也可以由内核空间实现,异步I/O是linux2.6以后版本内核的标准特性
  • 对于块设备,AIO可以一次性发出大量的read/write调用并且通过通用块层的I/O调度来获得更好的性能,用户也可以减少过多的同步负载
  • 对网络设备而言,在socket层面上,也可以使用AIO,让CPU和网卡的收发充分交叠以改善吞吐性能
  • 用户空间中一般要结合libaio来进行内核AIO的系统调用
io_setup( )

//Initializes an asynchronous context for the current process

io_submit( )

//Submits one or more asynchronous I/O operations

io_getevents( )

//Gets the completion status of some outstanding asynchronous I/O operations

io_cancel( )

//Cancels an outstanding I/O operation

io_destroy( )

//Removes an asynchronous context for the current process

9.4.3 AIO与设备驱动

  • 用户空间调用io_submit()之后,对应于用户传递的每个iocb结构,内核会生成一个与之对应的kiocb结构
  • 通过is_sync_kiocb判断某kiocb是否为同步I/O请求

    • 如果是返回真,表示为异步I/O请求
  • 字符设备:必须明确应支持AIO(极少数是异步I/O操作)

  • 字符设备驱动程序中file_operations 包含 3 个与 AIO 相关的成员函数,
ssize_t (*aio_read) (struct kiocb *iocb, char *buffer, size_t count, loff_t offset);

ssize_t (*aio_write) (struct kiocb *iocb, const char *buffer, size_t count, loff_t offset);

int (*aio_fsync) (struct kiocb *iocb, int datasync);
  • 块设备和网络设备:本身是异步的

以上是关于:Linux设备驱动中的异步通知与同步I/O的主要内容,如果未能解决你的问题,请参考以下文章

I/O复用 select和poll

linux设备驱动中的异步通知机制

linux设备驱动中的异步通知机制

同步异步阻塞非阻塞与服务器

透彻Linux(Unix)五种IO模型

通信模型socket