Q:什么是异步/同步io,什么是阻塞/非阻塞io?
A:按照获取数据的方式可将io区分为同步和异步的:同步是指需要调用者主动访问I/O口来进行数据的读写;异步是指调用者只需要提出io请求,然后处理其他事情,由内核完成读写操作之后通知调用者。重点就是读写操作是不是调用者主动完成的。按照执行的时候进程是否被挂起可将io区分为阻塞和非阻塞:阻塞当然就是进程需要等待io完成而被挂起;非阻塞就是执行io时,无论结果如何都会即刻返回。
Q:为什么会出现posix aio这一套异步I/O接口?
A:开发这套接口是为了提高CPU和I/O设备效率,提高程序设计的并发性。早先的Linux只有signal driven I/O,而这不能算是真正意义上的异步I/O,因为内核只是通知我们I/O口准备就绪了,但是还是需要调用者主动去处理读写操作;相比之下,使用Posxi aio接口时,调用者向内核提交I/O申请之后就不用管了,等到系统通知自己的时候,I/O操作已经完成。这一套posix aio最早出现在real-time draft standard (APUE,不知道怎么翻译)中,后来被加入到SUS的基础标准(base)中,也即是说所有unix-like platform都应该实现它。不过据说,posix aio实际效果不佳,或是因为Linux kernel aio的实现还不够完善,对posix aio的支持还不够(存疑)。Posix aio对网络的支持也不够好,但是对文件I/O支持很好,网络I/O方面使用epoll更加,二者正好形成互补。
Q:Posix aio使用到的核心数据结构和意义?
A :
struct aiocb {
int aio_fildes; /* 用于aio的文件描述符 */
off_t aio_offset; /* 读写操作的偏移 */
volatile void *aio_buf; /* aio缓冲区 */
size_t aio_nbytes; /* 缓冲区大小 */
int aio_reqprio; /* aio优先级的建议值 */
struct sigevent aio_sigevent; /* aio完成后出发的notifying类型 */
int aio_lio_opcode; /* 用于list IO的操作码 */
};
aio_offset:此值与操作系统所维护的文件偏移量相互独立,所以不要对一个文件同时使 用常规I/O和异步I/O。同时,如果文件状态标志指定了O_APPEND,则此offset不起作用。
aio_reqprio:此数值必须在0到系统限制值之间(),否则后续接口对aiocb的参数检测不能通过,报EINVAL。
aio_lio_opcode:用于指明此aiocb对应的list I/O操作是读(LIO_READ)、写(LIO_WRITE)还是忽略(LIO_NOP)
struct sigevent {
int sigev_notify; /* 触发通知的类型 */
int sigev_signo; /* I/O完成后内核向调用进程发出的信号值 */
union sigval sigev_value; /* 通知中附带的数据 */
void (*sigev_notify_function)(union sigval); /* I/O完成后触发的detach线程函数 */
pthread_attr_t *sigev_notify_attributes; /* 控制上面线程的属性 */
};
sigev_notify:I/O操作完成后对调用进程的通知类型
tpye | action |
---|---|
SIGEV_NONE | 不进行通知 |
SIGEV_SIGNAL | 触发sigev_signo信号,进程自己在signal handler中处理相 关事务。如果在信号注册中使用了SA_SIGINFO标志,则信号会被排队,并且struct siginfo中的sig_value域被设置成sigev_value(参见sigaction函数) |
SIGEV_THREAD | 默认生成一个独立的detached状态的线程(可使用sigev_notify_attributes改变线程属性),线程中运行sigev_notify_function函数,参数为sigev_value |
sigev_value:为一个联合类型,定义如下
union sigev_value {
int sival_int;
void *sival_ptr;
};
Q:aio接口函数的原型和用法?
A:所有aio接口函数都在头文件aio.h中声明,而实现是打包在实时库librt.a/librt.so中,所以在编译的时候需要加入-lrt。(ps:标准库函数一般都放在libc.a/libc.so中,而gcc会默认连接此库,所以在使用的时候不需要加-lc,其他的库却需要显式说明)
int aio_read(struct aiocb *aiocb);
int aio_write(struct aiocb *aiocb);
功能:向内核提出异步I/O申请,调用这两个函数并不会正真执行I/O操作,而只是将aiocb加入到aio的等待队列中,返回结果指明该申请是否成功提交。
注意:在异步I/O没有执行完或者撤销之前必须保证aiocb和I/O缓冲的内存不被占用。
int aio_fsync(int op, struct aiocb *aiocb);
向内核提出与磁盘同步的请求,与前两个函数类似,返回的结果只是表示请求是否被受理。只有在内核实际执行同步操作后,数据才会被写入磁盘中。(这里涉及到linux“延迟写”的概念,用户缓冲区数据通过系统调用(如:read/write)被复制到内核缓冲区中,默认条件下只有等到内核缓冲满或需要被复用,才会被放入I/O队列,执行实际的I/O操作,而fsync族的函数可以让内核缓冲区直接被放入I/O队列中,但这离实际写入磁盘还是有延迟。)
op | meaning |
---|---|
O_DSYNC | 效果相当于调用fdatasync函数,只对文件数据部分进行同步 |
O_SYNC | 效果相当于调用fsync,对文件的数据和属性都进行同步操作 |
int aio_error(const struct aiocb *aiocb);
对aiocb对应的异步I/O进行错误检测,返回值有以下几种情况:
ret val | meaning |
---|---|
0 | I/O操作成功 |
-1 | 对aio_error自身调用失败,检查errno |
EINPROGRESS | I/O操作正在进行中 |
anything else | I/O操作出现错误,返回值为错误码 |
ssize_t aio_return(const struct aiocb *aiocb);
功能:当I/O操作完成后,调用此函数获得aiocb对应操作的返回值(read、wirte、fsync等操作的返回值)。
注意:此函数在一次异步I/O操作中只能调用一次,调用之后存储返回值的内存可能被复用;在I/O完成之前调用,返回值是undefined的。
int aio_suspend(const struct aiocb *const list[], int nent,
const struct timespec *timeout);
阻塞进程以等待list参数中的I/O操作完成,跳出阻塞的情况如下:
1. 被信号中断而返回,返回值为-1,且errno = EINTR;
2. 超过阻塞时间限制,返回值为-1,且errno = EAGAIN;
3. 阻塞时限内有至少一个I/O操作完成,返回值为0;
int aio_cancel(int fd, struct aiocb *aiocb);
为fd对应文件上的一个没有完成的I/O提出撤销申请,发出请求不一定能被成功撤销。如果aiocb = NULL,则会试图撤销fd上所有的异步I/O请求。返回值说明撤销情况:
ret val | meaning |
---|---|
-1 | aio_cancel自身调用出现错误,检查errno |
AIO_ALLDONE | 撤销前所有I/O操作都已经完成 |
AIO_CANCELED | 所有I/O操作都被撤销 |
AIO_NOTCANCELED | 至少有一个I/O操作没有被撤销 |
int lio_listio(int mode, struct aiocb *restrict const list[restrict],
int nent, struct sigevent *restrict sigev);
List I/O的调用接口。mode = LIO_WAIT同步调用,阻塞直到所有list中的I/O操作都完成才返回,此时sigev参数无效;mode = LIO_NOWAIT则是费阻塞调用,提交请求之后就返回,等到所有I/O操作完成之后,通过sigev中描述的方式通知调用进程。
实践
一个主进程fork三个子进程,父子进程之间通过FIFO通信,兄弟进程之间互不干扰。父进程使用非阻塞异步读FIFO端口,并使用detached线程处理读取到的结果;子进程使用阻塞异步写FIFO端口。兄弟进程之间会产生竞争关系,但是异步机制能够自己协调,当本进程确实获得了写FIFO的权利时,才会从aio_suspend返回,跳出阻塞来进行写操作。另外,主进程中FIFO要在fork子进程之后才能打开,否则打开操作会阻塞,因为FIFO写端没有打开;而主进程阻塞之后无法fork子進程,就没法作为FIFO写端,形成死锁。
host.c:
/*****************************************************
* the host use aio to monitor what the client wanna
* say through FIFO
* *************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <aio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <signal.h>
#define BSZ 64
sig_atomic_t is_reading = 0;
sig_atomic_t is_quitting = 0;
int pid[3];
void err_exit(const char *str)
{
perror(str);
exit(1);
}
void read_thread(union sigval val)
{
printf("%s", (char *)val.sival_ptr);
is_reading = 0;
}
void main_work(int fd)
{
static int cnt = 0;
struct aiocb aio_ctl;
char buf[BSZ];
int fl, i;
fl = fcntl(STDIN_FILENO, F_GETFL, 0);
fcntl(STDIN_FILENO, F_SETFL, fl | O_NONBLOCK);
//aio control block init
aio_ctl.aio_fildes = fd;
aio_ctl.aio_offset = 0;
aio_ctl.aio_buf = buf;
aio_ctl.aio_nbytes = BSZ;
//signal setting
aio_ctl.aio_sigevent.sigev_notify = SIGEV_THREAD;
aio_ctl.aio_sigevent.sigev_value.sival_ptr = (void *)buf;
aio_ctl.aio_sigevent.sigev_notify_function = read_thread;
aio_ctl.aio_reqprio = 0;
memset(buf, 0, BSZ);
while (!is_quitting)
{
if (read(STDIN_FILENO, buf, BSZ) > 0)
{
if (strncmp(buf, "quit", 4) == 0)
{
is_reading = 1;
is_quitting = 1;
}
else
printf("wrong code\n");
}
if (!is_reading)
{
aio_read(&aio_ctl);
is_reading = 1;
}
sleep(1);
printf("host work: %d\n", ++cnt);
}
for (i = 0; i < 3; i++)
{
kill(pid[i], SIGKILL);
waitpid(pid[i], NULL, 0);
}
fcntl(STDIN_FILENO, F_SETFL, fl);
return;
}
int main(void)
{
int i;
int fd;
//fork
for (i = 0; i < 3; i++)
{
if ((pid[i] = fork()) < 0)
err_exit("fork error");
//child
else if (pid[i] == 0)
{
printf("child %d has been born\n", i);
execl("client1", "client1", NULL);
err_exit("execl error");
}
else if (pid[i] > 0 && i == 2)
{
//open fifo, cannot open before child been born
//otherwise dead lock occur
if ((fd = open("fifo", O_RDONLY)) < 0)
err_exit("open error");
main_work(fd);
}
}
close(fd);
return 0;
}
client.c:
/*****************************************************
* send a string to standard output at a random time pause
****************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <errno.h>
#include <aio.h>
#define BSZ 64
void err_exit(const char *str)
{
perror(str);
exit(1);
}
int main(void)
{
char buf[BSZ] = {0};
int fd = -1;
int sleep_time;
int err;
static int call_id = 0;
struct aiocb aio_blk;
const struct aiocb *aio_list[1];
//random seed
srand((int)time(NULL));
//open fifo
fd = open("fifo", O_WRONLY);
//aio block init
aio_blk.aio_fildes = fd;
aio_blk.aio_buf = buf;
aio_blk.aio_nbytes = BSZ;
aio_blk.aio_offset = 0;
aio_blk.aio_reqprio = 0;
aio_blk.aio_sigevent.sigev_notify = SIGEV_NONE;
aio_list[0] = &aio_blk;
while (1)
{
sleep_time = rand() % 10 + 1;
sleep(sleep_time);
sprintf(buf, "This is the %d call from child %d\n", ++call_id, getpid());
if ((err = aio_write(&aio_blk)) < 0)
err_exit("aio_write error");
if((err = aio_suspend(aio_list, 1, NULL)) < 0)
err_exit("aio_suspend error");
if ((err = aio_error(&aio_blk)) == -1)
err_exit("aio_error error");
else if (err != 0)
err_exit("aio_write error");
}
close(fd);
return 0;
}