异步 I/O 访问
信号机制提供了一种以异步方式通知某种事件已发生的方法,但是,这种异步 I/O 是有限制的,它们并不能用在所有的文件类型上,而且只能使用一个信号。如果要对一个以上的文件描述符进行异步 I/O,那么在进程收到该信号时并不知道这一信号对应哪一个文件描述符。
SylixOS 支持 POSIX1003.1b 实时扩展协议规定的标准异步 I/O 接口,即 aio_read 函数、aio_write 函数、aio_fsync 函数、aio_cancel 函数、aio_error 函数、aio_return 函数、aio_suspend 函数和 lio_listio 函数。这组 API 用来操作异步 I/O。异步 I/O 是针对同步 I/O 提出的概念,它不需要线程等待 I/O 结果,只需要请求进行传输,系统会自动完成 I/O 传输,结束或者出现错误时会产生相应的 I/O 信号,用户程序只需要设置好对应的信号陷入函数,即可处理一个异步 I/O 事件。
POSIX 异步 I/O 接口为不同类型的文件进行异步 I/O 提供了一套一致的方法。这些接口来自实时草案标准,这些异步 I/O 接口使用 AIO 控制块来描述 I/O 操作。aiocb 结构体定义了 AIO 控制块,SylixOS 实现该结构体如所示:
struct aiocb {
int aio_fildes; /* file descriptor */
off_t aio_offset; /* file offset */
volatile void *aio_buf; /* Location of buffer. */
size_t aio_nbytes; /* Length of transfer. */
int aio_reqprio; /* Request priority offset. */
struct sigevent aio_sigevent; /* Signal number and value. */
int aio_lio_opcode; /* Operation to be performed. */
……
};
下面对该结构的成员做详细说明:
- aio_fildes 是要操作的文件描述符,即 open 函数返回的文件描述符。
- aio_offset 是读、写开始的文件偏移量。
- aio_buf 是数据缓冲区指针,对于读,从该缓冲区读出数据。对于写,向该缓冲区中写入数据。
- aio_nbytes 是读、写的字节数。
- aio_reqprio 为异步 I/O 请求的优先级,该优先级决定了读写的顺序,也就意味着优先级越高越先被读或者写。
- aio_sigevent 是要发送的信号,当一次读或者写完成后,会发送应用指定的信号。
- aio_lio_opcode 是异步 I/O 操作的类型,如下表所示。
操作类型 | 说明 |
---|---|
LIO_NOP | 没有传输请求 |
LIO_READ | 请求一个读操作 |
LIO_WRITE | 请求一个写操作 |
LIO_SYNC | 请求异步 I/O 同步 |
在异步 I/O 操作之前我们需要先初始化 AIO 控制块,然后通过调用 aio_read 函数请求异步读操作和调用 aio_write 函数请求异步写操作。
#include <aio.h>
int aio_read(struct aiocb *paiocb);
int aio_write(struct aiocb *paiocb);
函数 aio_read 原型分析:
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 paiocb 是 AIO 控制块。
函数 aio_write 原型分析:
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 paiocb 是 AIO 控制块。
调用 aio_read 函数和 aio_write 函数成功之后,异步 I/O 请求便已经被操作系统放入等待处理的队列中了。这些返回值与实际 I/O 操作的结果没有任何关系,如果需要查看函数的返回状态可调用 aio_error 函数。
如果想强制所有等待中的异步操作不等待而写入存储中,可以建立一个 AIO 控制块并调用 aio_fsync 函数。
#include <aio.h>
int aio_fsync(int op, struct aiocb *paiocb);
函数 aio_fsync 原型分析:
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 op 是操作选项。
- 参数 paiocb 是 AIO 控制块。
结构体 struct aiocb 中 aio_fildes 成员是要被同步的文件,如果 op 参数设定为 O_SYNC, aio_fsync 调用类似于 fsync,如果 op 参数设定为 O_DSYNC,aio_fsync 调用类似于 fdatasync(目前 SylixOS 没有对这两种情况做具体的区分)。
同 aio_read 函数和 aio_write 函数一样,查看 aio_fsync 函数处理的状态可调用 aio_error。
#include <aio.h>
int aio_error(const struct aiocb *paiocb);
函数 aio_error 原型分析:
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 paiocb 是 AIO 控制块。
返回值应为下面的 4 种情况之一:
- 0:异步操作成功完成,调用 aio_return 函数可获得返回码。
- -1:对 aio_error 调用失败。
- EINPROGRESS:异步操作等待中。
- 其他情况:其他任何返回值是相关异步操作失败返回的错误码。
如果异步操作成功,可以调用 aio_return 函数来获得异步操作的返回值。
#include <aio.h>
ssize_t aio_return(struct aiocb *paiocb);
函数 aio_return 原型分析:
- 此函数成功返回异步操作完成的返回值,失败返回-1 并设置错误号。
- 参数 paiocb 是 AIO 控制块。
执行 I/O 操作时,如果还有其他事务要处理而不想被 I/O 操作阻塞,就可以使用异步 I/O。然而,如果在完成了所有事务时,还有异步操作未完成时,可以调用 aio_suspend 函数来阻塞进程,直到操作完成。
#include <aio.h>
int aio_suspend(const struct aiocb * const list[], int nent,
const struct timespec *timeout);
函数 aio_suspend 原型分析:
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 list 是 AIO 控制块数组。
- 参数 nent 是数组元素个数。
- 参数 timeout 是设定的超时时间。
aio_suspend 可能会返回以下 3 种情况中的一种:
- 如果阻塞超时了,那么 aio_suspend 函数将返回-1。
- 如果有任何 I/O 操作完成,aio_suspend 函数将返回 0。
- 如果在调用 aio_suspend 函数时,所有的异步 I/O 操作都已完成,那么 aio_suspend 函数将在不阻塞的情况下直接返回。
参数 list 会自动忽略空指针,非空元素是经过初始化的 AIO 控制块。
当还存在不需要再完成的等待中的异步 I/O 操作时,可以调用 aio_cancel 函数来取消它们。
#include <aio.h>
int aio_cancel(int fildes, struct aiocb *paiocb);
函数 aio_cancel 原型分析:
- 此函数返回值,如下表所示。
返回值类型 | 说明 |
---|---|
AIO_CANCELED | 所有请求的操作已被取消 |
AIO_NOTCANCELED | 至少有一个请求操作没被取消 |
AIO_ALLDONE | 在请求取消之前已经完成 |
-1 | aio_cancel 函数调用失败 |
- 参数 fildes 是要操作的文件描述符。
- 参数 paiocb 是 AIO 控制块。
如果 paiocb 为 NULL,系统将会尝试取消所有该文件上未完成的异步 I/O 操作。其他情况下,系统将尝试取消由 paiocb 指定的 AIO 控制块描述的单个异步 I/O 操作。
如果异步 I/O 操作被成功取消,那么相应的 AIO 控制块调用 aio_error 函数将会返回错误 ECANCELED。如果操作不能被取消,那么相应的 AIO 控制块不会因为对 aio_cancel 函数的调用而被修改。
下面程序展示了以上函数的使用方法,程序构造一个读操作的 AIO 控制块,然后调用 aio_read 请求读操作,操作完成后会通过信号(见信号系统章节)通知给 signal_handler 函数,以做进一步的处理。
#include <aio.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#define BUFSIZE (64)
void signal_handler (union sigval val)
{
struct aiocb *paio = (struct aiocb *)val.sival_ptr;
ssize_t ret;
ret = aio_return(paio);
if (ret < 0) {
fprintf(stderr, "aio_return error.\n");
return;
}
fprintf(stdout, "len: %ld\n content: %s\n", ret, (char *)(paio->aio_buf));
}
int main (int argc, char *argv[])
{
int fd;
int ret;
static struct aiocb aio;
const struct aiocb *const list[] = {&aio};
fd = open("file", O_RDONLY);
if (fd < 0) {
fprintf(stderr, "open file failed.\n");
return (-1);
}
memset(&aio, 0, sizeof(struct aiocb));
/*
* 设置AIO控制块
*/
aio.aio_fildes = fd;
aio.aio_buf = malloc(BUFSIZE + 1);
aio.aio_nbytes = BUFSIZE;
aio.aio_offset = 0;
aio.aio_reqprio = 1;
aio.aio_lio_opcode = LIO_READ;
aio.aio_sigevent.sigev_notify = SIGEV_THREAD | SIGEV_SIGNAL;
aio.aio_sigevent.sigev_value.sival_ptr = (void *)&aio;
aio.aio_sigevent.sigev_signo = SIGUSR1;
aio.aio_sigevent.sigev_notify_function = signal_handler;
ret = aio_read(&aio);
if (ret < 0) {
perror("aio_read");
close(fd);
return (-1);
}
aio_suspend(list, 1, NULL);
close(fd);
return (0);
}
在 SylixOS Shell 下运行程序:
# ./Function_AsynchronousIO
len: 64
content: This is a sylixos test file
调用 lio_listio 函数可以提交一系列由一个 AIO 控制块列表描述的 I/O 请求。
#include <aio.h>
int lio_listio(int mode, struct aiocb * const list[], int nent,
struct sigevent *sig);
函数 lio_listio 原型分析:
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 mode 是传输模式(LIO_WAIT、LIO_NOWAIT)。
- 参数 list 是请求 AIO 控制块数组。
- 参数 nent 是 AIO 控制块数组。
- 参数 sig 是所有 I/O 操作完成后产生的信号方法。
- mode 参数决定了 I/O 是否真的是异步的。如果该参数被设定为 LIO_WAIT,lio_listio 函数将在所有由列表指定的 I/O 操作完成后返回,这种情况下,参数 sig 将被忽略。如果 mode 参数被设定为 LIO_NOWAIT,lio_listio 函数将在 I/O 请求入队后立即返回。进程将在所有 I/O 操作完成后,按照参数 sig 指定的被异步通知。如果不想被通知,可以把参数 sig 设置为 NULL,需要注意的是,每个异步 I/O 操作可以设定自己的通知方式,而参数 sig 指定的通知方式,是额外的通知方式,并且只有所有操作都完成后才会通知。
在每一个 AIO 控制块中,aio_lio_opcode 字段指定了该操作是一个读操作(LIO_READ)、写操作(LIO_WRITE),还是将被忽略的空操作(LIO_NOP)。
下面程序展示了 lio_listio 函数的用法,从代码可以看出一次 lio_listio 函数调用发起了多个传输,从这一点可以得出 lio_listio 函数的性能方面得到了提高。
#include <aio.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#define BUFSIZE (64)
void signal_handler (union sigval val)
{
fprintf(stdout, "async operator complete.\n");
}
int main (int argc, char *argv[])
{
int fd;
int ret;
struct aiocb aio[2];
struct aiocb *const list[] = {&aio[0], &aio[1]};
static struct sigevent notify;
fd = open("file", O_RDONLY);
if (fd < 0) {
fprintf(stderr, "open file failed.\n");
return (-1);
}
memset(&aio[0], 0, sizeof(struct aiocb));
memset(&aio[1], 0, sizeof(struct aiocb));
/*
* 设置第一个AIO控制块
*/
aio[0].aio_fildes = fd;
aio[0].aio_buf = malloc(BUFSIZE + 1);
aio[0].aio_nbytes = BUFSIZE;
aio[0].aio_offset = 0;
aio[0].aio_reqprio = 1;
aio[0].aio_lio_opcode = LIO_READ;
/*
* 设置第二个AIO控制块
*/
aio[1].aio_fildes = fd;
aio[1].aio_buf = malloc(BUFSIZE + 1);
aio[1].aio_nbytes = BUFSIZE;
aio[1].aio_offset = BUFSIZE;
aio[1].aio_reqprio = 2;
aio[1].aio_lio_opcode = LIO_READ;
notify.sigev_signo = SIGUSR1;
notify.sigev_notify_function = signal_handler;
notify.sigev_notify = SIGEV_THREAD | SIGEV_SIGNAL;
ret = lio_listio(LIO_NOWAIT, list, 2, ¬ify);
if (ret < 0) {
perror("lio_listio");
close(fd);
return (-1);
}
sleep(60);
close(fd);
return (0);
}
在 SylixOS Shell 下运行程序:
# ./lio_listio_test
signal handler.
SylixOS 异步 I/O 的实现中,会通过一个额外的线程(代理线程)对 I/O 进行操作,为了能够设置或者获取代理线程的栈信息,SylixOS 增加了下面的一组函数,这组函数并非标准中定义。需要注意的是,设置的栈信息只对将来启动的线程有效(这通常发生在将来的 I/O 请求)。
#include <aio.h>
int aio_setstacksize(size_t newsize);
size_t aio_getstacksize(void);
函数 aio_setstacksize 原型分析:
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 newsize 是新的栈大小。
函数 aio_getstacksize 原型分析:
- 此函数总是返回当前线程栈的大小。
调用 aio_setstacksize 函数可以设置将来启动线程(代理线程)的栈大小,调用 aio_getstacksize 函数可以获得当前线程(代理线程)栈的大小。