在设备驱动中使用异步通知可以使得对设备的访问可进行时,由驱动主动通知应用程序进行访问。因此,使用无阻塞I/O的应用程序无需轮询设备是否可访问,而阻塞访问也可以被类似“中断”的异步通知所取代。异步通知类似于硬件上的“中断”概念,比较准确的称谓是“信号驱动的异步I/O”。
9.1 异步通知的概念和作用
- 异步通知:一旦设备就绪,则主动通知应用程序,该应用程序无需查询设备状态
- 几种通知方式比较:
- 阻塞I/O :一直等待设备可访问后开始访问
- 非阻塞I/O:使用poll()查询设备是否访问
- 异步通知 :设备主动通知用户应用程序 -
9.2 linux异步通知编程
9.2.1 linux信号
- 作用:linux系统中,异步通知使用信号来实现
- Linux信号及其定义如下:
信号 | 值 | 含义 |
---|---|---|
SIGHUP | 1 | 挂起 |
SIGINT | 2 | 终端中断 |
SIGQUIT | 3 | 终端退出 |
SIGILL | 4 | 无效命令 |
SIGTRAP | 5 | 跟踪陷阱 |
SIGIOT | 6 | IOT陷阱 |
SIGBUS | 7 | BUS错误 |
SIGFPE | 8 | 浮点异常 |
SIGKILL | 9 | 强行终止 |
SIGUSR1 | 10 | 用户定义信号1 |
SIGSEGV | 11 | 无效的内存段处理 |
SIGUSR2 | 12 | 用户定义信号2 |
SIGPIPE | 13 | 半关闭管道的写操作已经发生 |
SIGALRM | 14 | 计时器到时 |
SIGTERM | 15 | 终止进程 |
SIGSTKFLT | 16 | 堆栈错误 |
SIGCHLD | 17 | 子进程已经停止或退出 |
SIGCONT | 18 | 如果停止了,继续执行 |
SIGSTOP | 19 | 停止执行 |
SIGTSTP | 20 | 终端来的停止信号 |
SIGTTIN | 21 | 后台进程读终端 |
SIGTTOU | 22 | 后台进程写终端 |
SIGURG | 23 | I/O紧急信号 |
SIGXGPU | 24 | CPU时限超时 |
SIGXFSZ | 25 | 文件长度过长 |
SIGVTALRM | 26 | 虚拟计时器到时 |
SIGPROF | 27 | 统计分布图用计时器到时 |
SIGWINCH | 28 | 窗口大小发生变化 |
SIGIO | 29 | 描述符上可以进行I/O |
SIGPWR | 30 | 断电重启 |
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
- signum:信号值
- 参数:
int sigaction(int signo,const struct sigaction *restrict act, struct sigaction *restrict oact);
- 1
- 实例:使用信号实现异步通知
- 在用户空间处理设备释放信号的准备工作
- 通过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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
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);
- 1
- 2
- 3
- 将fasync_struct结构体指针放到设备结构体中是最佳的选择
//异步通知的设备结构体模板
struct xxx_dev{
struct cdev cdev;
...
struct fasync_struct *async_queue;//异步结构体指针
};
- 1
- 2
- 3
- 4
- 5
- 6
- 在设备驱动中的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);
}
- 1
- 2
- 3
- 4
- 5
- 在设备资源可获得时应该调用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);
...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 最后在文件关闭时,要将文件从异步通知列表中删除
int xxx_release(struct inode *inode,struct file *filp)
{
//将文件从异步通知列表中删除
xxx_fasync(-1,filp,0);
...
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
9.3 linux异步I/O
9.3.1 AIO概念与GNU C库 AIO
9.3.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.3.1.2 AIO系列API:
- aio_read–异步读
- 作用:请求对一个有效的文件描述符进行异步读写操作
- 请求进行排队之后会立即返回
- 这个文件描述符可以表示一个文件、套接字,甚至管道
- 参数aiocb:结构体包含了传输的所有信息,以及为AIO操作准备的用户空间缓存区
- 返回值
- 成功:返回0
- 失败:返回-1,并设置errno的值
- 作用:请求对一个有效的文件描述符进行异步读写操作
int aio_read( struct aiocb *aiocbp );
- 1
- aio_write–异步写
- 作用:请求一个异步写操作
- 请求进行排队之后会立即返回
- 这个文件描述符可以表示一个文件、套接字,甚至管道
- 参数aiocb:结构体包含了传输的所有信息,以及为AIO操作准备的用户空间缓存区
- 返回值
- 成功:返回0
- 失败:返回-1,并设置errno的值
- 作用:请求一个异步写操作
int aio_write( struct aiocb *aiocbp );
- 1
- aio_error
- 作用:确定请求的状态
- 参数aiocb:结构体包含了传输的所有信息,以及为AIO操作准备的用户空间缓存区
- 返回值
- EINPROGRESS:说明请求尚未完成
- ECANCELED:说明请求被应用程序取消
- 失败:返回-1,并设置errno的值
int aio_error( struct aiocb *aiocbp );
- 1
- aio_return–获得异步操作的返回值
- 异步 I/O 和标准块 I/O 之间的另外一个区别是不能立即访问这个函数的返回状态,因为并没有阻塞在 read()调用上
- 在标准的 read()调用中,返回状态是在该函数返回时提供的。但是在异步 I/O 中,我们要使用 aio_return()函数
- 只有在 aio_error()调用确定请求已经完成(可能成功,也可能发生了错误)之后,才会调用这个函数
- 参数aiocb:结构体包含了传输的所有信息,以及为AIO操作准备的用户空间缓存区
- 返回值
- 成功:返回所传输的字节数
- 失败:返回-1
ssize_t aio_return( struct aiocb *aiocbp );
- 1
- aio_suspend–挂起异步操作,直到异步请求完成为止
- 作用:挂起(或阻塞)调用进程,直到异步请求完成为止,调用者提供了一个 aiocb 引用列表,其中任何一个完成都会导致 aio_suspend()返回
int aio_suspend( const struct aiocb *const cblist[], int n, const struct timespec *timeout );
- 1
- 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:表明某个请求已经被取消了
- 如果要取消一个请求,用户需提供文件描述符和 aiocb 引用
int aio_cancel( int fd, struct aiocb *aiocbp );
- 1
- lio_listio–同时发起多个传输(一次系统调用可以启动大量的I/O操作)
- 作用:这个函数非常重要,它使得用户可以在一个系统调用(一次内核上下文切换)中启动大量的 I/O 操作
- 参数
- mode:可以是 LIO_WAIT 或 LIO_NOWAIT
- LIO_WAIT 会阻塞这个调用,直到所有的 I/O 都完成为止
- 在操作进行排队之后,LIO_NOWAIT 就会返回
- list :是一个 aiocb 引用的列表,最大元素的个数是由 nent 定义的
- 如果 list 的元素为 NULL,lio_listio()会将其忽略。
- mode:可以是 LIO_WAIT 或 LIO_NOWAIT
int lio_listio( int mode, struct aiocb *list[], int nent, struct sigevent *sig );
- 1
9.3.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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
9.3.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);
- 1
- 2
- 3
- 4
- 5
- 块设备和网络设备:本身是异步的