Linux 网络编程的5种IO模型:异步IO模型
资料已经整理好,但是还有未竟之业:复习多路复用epoll 阅读例程, 异步IO 函数实现
背景
上一讲《 Linux 网络编程的5种IO模型:信号驱动IO模型 》我们已经介绍了信号驱动模型,以及带有BUG的例程。
前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。
这一讲我们来介绍最后一种IO模型。
导言
两种高性能IO设计模式
在传统的网络服务设计模式中,有两种比较经典的模式:多线程,与 线程池。
多线程
对于多线程模式,也就说来了client,服务器就会新建一个线程来处理该client的读写事件,如下图所示:
这种模式虽然处理起来简单方便,但是由于服务器为每个client的连接都采用一个线程去处理,使得资源占用非常大。因此,当连接数量达到上限时,再有用户请求连接,直接会导致资源瓶颈,严重的可能会直接导致服务器崩溃。
线程池
因此,为了解决这种一个线程对应一个客户端模式带来的问题,提出了采用线程池的方式,也就说创建一个固定大小的线程池,来一个客户端,就从线程池取一个空闲线程来处理,当客户端处理完读写操作之后,就交出对线程的占用。因此这样就避免为每一个客户端都要创建线程带来的资源浪费,使得线程可以重用。
但是线程池也有它的弊端,如果连接大多是长连接,因此可能会导致在一段时间内,线程池中的线程都被占用,那么当再有用户请求连接时,由于没有可用的空闲线程来处理,就会导致客户端连接失败,从而影响用户体验。因此,线程池比较适合大量的短连接应用。
高性能IO模型
因此便出现了下面的两种高性能IO设计模式:Reactor和Proactor。
Reactor
在Reactor模式中,会先对每个client注册感兴趣的事件,然后有一个线程专门去轮询每个client是否有事件发生,当有事件发生时,便顺序处理每个事件,当所有事件处理完之后,便再转去继续轮询,如下图所示:
从这里可以看出,多路复用IO就是采用Reactor模式。
注意,上面的图中展示的 是顺序处理每个事件,当然为了提高事件处理速度,可以通过多线程或者线程池的方式来处理事件。
Proactor
在Proactor模式中:当检测到有事件发生时,会新起一个异步操作,然后交由内核线程去处理,当内核线程完成IO操作之后,发送一个通知告知操作已完成;可以得知,异步IO模型采用的就是Proactor模式。
Linux异步IO模型与有关函数
异步IO模型是比较理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要关心实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。
也就说在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。
这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作;而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用iO函数进行实际的读写操作。
sequenceDiagram
title : 异步IO模型
participant application
participant kernel
Note right of application: 应用程序调用系统调用
application ->> kernel: aio_read
kernel ->> application: 返回
kernel ->> kernel: 准备好数据,拷贝到用户空间
kernel ->> application: 递交到aio_read指定的信号中
application ->> application : 信号处理
#include <aio.h>
int aio_read(struct aiocb *__aiocbp);
int aio_write(struct aiocb *__aiocbp);
Link with -lrt.
/* 有关结构体 ,能够使用的成员已经标出 */
struct aiocb
{
▲ int aio_fildes; /* 对哪个文件进行读写. */
▲ int aio_lio_opcode; /* 要执行的操作 */
int aio_reqprio; /* Request priority offset. */
▲ volatile void *aio_buf; /* 读写用的buffer */
▲ size_t aio_nbytes; /* Length of transfer. */
▲ struct sigevent aio_sigevent; /* 告诉 AIO 在 I/O 操作完成时应该执行什么操作。 */
/* Internal members. */
struct aiocb *__next_prio;
int __abs_prio;
int __policy;
int __error_code;
__ssize_t __return_value;
#ifndef __USE_FILE_OFFSET64 // 针对大文件的支持
▲ __off_t aio_offset; /* 在传统的 read 调用中,偏移量是在文件描述符上下文中进行维护的, */
char __pad[sizeof (__off64_t) - sizeof (__off_t)];
#else
▲ __off64_t aio_offset; /* 对于异步 I/O 操作来说这是不可能的,因为我们可以同时执行很多读请求,因此必须为每个特定的读请求都指定偏移量。 */
#endif
char __glibc_reserved[32];
};
struct sigevent {
int sigev_notify; /* 通知方式:为SIGEV_NONE、SIGEV_SIGNAL、SIGEV_THREAD、SIGEV_THREAD_ID(只针对linux)当中的一个; */
int sigev_signo; /* 为signal的值,当sigev_notify为SIGEV_SIGNAL时,会将这个signal发送给进程; */
union sigval sigev_value; /* 信号传递的数据 */
void (*sigev_notify_function) (union sigval);/* 当sigev_notify为SIGEV_THREAD时,处理线程将调用这个处理函数 (SIGEV_THREAD) */
void *sigev_notify_attributes;/* sigev_notify_function的参数 (SIGEV_THREAD) */
pid_t sigev_notify_thread_id; /* 当sigev_notify为SIGEV_THREAD_ID时的处理线程ID (SIGEV_THREAD_ID) */
};
union sigval { /*传递的参数*/
int sival_int; /* 信号机制传递的参数 */
void *sival_ptr; /* 若是线程机制传递的参数 */
};
// 什么时候使用 AIO ?了解 AIO 机制之后,不难发现, AIO 其实是用于解决大量 IO 并发操作而出现的,牺牲一些信号处理耗时,用多线程的方式加速 IO ,提高并行效率。
函数 | 作用 |
---|---|
aio_read | 请求异步读操作 |
aio_error | 检查异步请求的状态 |
aio_return | 获得完成的异步请求的返回状态 |
aio_write | 请求异步写操作 |
aio_suspend | 挂起调用进程,直到一个或多个异步请求已经完成(或失败) |
aio_cancel | 取消异步 I/O 请求 |
aio_fsync | 强制同步 |
lio_listio | 发起一系列 I/O 操作 |
aio_read
#include <aio.h>
int aio_read( struct aiocb *aiocbp );
描述: 请求一个异步写操作。
返回值:成功返回值 0;出错返回值 -1,并设置 errno的值。
aio_read 例子
#include <unistd.h>
#include <stdio.h>
#include <aio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/timeb.h>
#define BUFFER_SIZE 1024*1024
void ptime(const char* tip){
struct timeb tb;
ftime(&tb);
fprintf(stdout, "%s %u : %u\n", tip, tb.time, tb.millitm);
}
int main(){
/* 句柄,返回码 */
int fd = -1, ret = -1;
fd = open("./file.txt", O_RDONLY);
if(fd <= 0){
fprintf(stderr, "open file errro: %s\n", strerror(errno));
return -1;
}
/* aio控制结构 */
aiocb my_aiocb;
memset(&my_aiocb, 0, sizeof(my_aiocb));
/* 初始化 */
my_aiocb.aio_fildes = fd;
my_aiocb.aio_reqprio = 0;
my_aiocb.aio_nbytes = BUFFER_SIZE;
char buf[BUFFER_SIZE + 1] = {0};
my_aiocb.aio_buf = buf;
ptime("start read");
/* aio 读 */
ret = aio_read(&my_aiocb);
if(ret < 0){
fprintf(stderr, "aio read error: %s\n", strerror(errno));
return -2;
}
ptime("reading");
/* 检查状态 */
while(aio_error(&my_aiocb) == EINPROGRESS);
/* (这种做法不是最有效的,只是为了演示aio_error如何使用)可以调用 aio_error 来确定 aio_read 的状态。只要状态是 EINPROGRESS,就一直忙碌等待,直到状态发生变化为止。请求可能成功,也可能失败。*/
ptime("after read");
if ((ret = aio_return( &my_iocb )) > 0) {
/* got ret bytes on the read */
fprintf(stdout, "read: %10.10s\n", my_aiocb.aio_buf);
} else {
/* read failed, consult errno */
fprintf(stderr, "return: %d\n", ret);
}
close(fd);
return 0;
}
aio_error
int aio_error( struct aiocb *aiocbp );
描述:用来确定请求的状态。
返回值:
- EINPROGRESS,说明请求尚未完成
- ECANCELLED,说明请求被应用程序取消了
- -1,说明发生了错误,具体错误原因可以查阅 errno
aio_return
ssize_t aio_return( struct aiocb *aiocbp );
描述:获得完成的异步请求的返回状态。
异步 I/O 和标准 I/O 之间的另外一个区别是我们不能立即访问这个函数的返回状态,因为我们并没有阻塞在
read
调用上。在标准的read
调用中,返回状态是在该函数返回时提供的。但是在异步 I/O 中, 我们要使用aio_return
函数。只有在
aio_error
调用确定请求已经完成(可能成功,也可能发生了错误)之后,才会调用这个函数。
返回值:所传输的字节数,如果出错,返回 -1
(等价于 read
或 write
系统调用的返回值)。
aio_write
int aio_write( struct aiocb *aiocbp );
描述: 请求一个异步写操作。
aio_write
函数会立即返回,说明请求已经进行排队(成功时返回值为 0
,失败时返回值为 -1
, 并相应地设置 errno
)。
这与 aio_read
类似,但是在偏移量上有一点不一样:对于write
来说,这个偏移量只有在没有设置 O_APPEND
选项的文件上下文中才会非常重要。
如果设置了
O_APPEND
,那么这个偏移量就会被忽略,数据都会被附加到文件的末尾。否则,aio_offset
域就确定了数据在要写入的文件中的偏移量。
aio_suspend
int aio_suspend( const struct aiocb *const aiocb_list[],
int nitems, const struct timespec *timeout );
描述:挂起(或阻塞)调用进程,直到以下情况发生:
- 一个或多个处于
aiocb_list
中的异步请求完成 - 有信号递达
- 调用时指定的时间已到,发生超时
调用者提供了一个 aiocb
引用列表,其中任何一个完成都会导致 aio_suspend
返回。
参数解析:
cblist:一组异步IO请求 (aiocb_list
中任何 NULL
元素都会被忽略)
nitems:该组的成员数量
timeout:超时时间,NULL代表永远阻塞
返回值:成功返回0;失败返回-1,设置errno:
EAGAIN :超时,希望程序重试。
EINTR : 被信号中断(也有可能是等待的某个操作的完成信号)
ENOSYS :这个功能未被当前系统支持(未实现)
aio_suspend 例程
使用非常简单。我们要提供一个
aiocb
引用列表。
...
struct aioct *cblist[MAX_LIST];
/* Clear the list. */
bzero( (char *)cblist, sizeof(cblist) );
/* Load one or more references into the list */
cblist[0] = &my_aiocb;
...
for(i = 0; i < ..; i++)
{
}
ret = aio_read( &my_aiocb );
...
ret = aio_suspend(cblist, MAX_LIST, NULL );
...
aio_cancel
int aio_cancel( int fd, struct aiocb *aiocbp);
描述:允许我们取消对某个文件描述符执行的一个或所有 I/O 请求。
参数解析:
fd : 与读写请求有关的文件描述符
aiocbp:读写请求(为NULL时,取消所有请求)
返回值:成功取消返回AIO_CANCELED
,请求被完成时返回AIO_NOTCANCELED
。
要取消对某个给定文件描述符的所有请求,我们需要提供这个文件的描述符,以及一个对 aiocbp
的 NULL
引用。
如果所有的请求都取消了,这个函数就会返回
AIO_CANCELED
;如果至少有一个请求没有被取消,那么这个函数就会返回
AIO_NOT_CANCELED
;如果没有一个请求可以被取消,那么这个函数就会返回
AIO_ALLDONE
。
可以使用
aio_error
来验证每个 AIO 请求。如果这个请求已经被取消了,那么aio_error
就会返回-1
,并且errno
会被设置为ECANCELED
。
aio_fsync
int aio_fsync(int op, struct aiocb *aiocbp);
描述: 在AIO是交给其他线程来完成的,如果需要手动执行同步,则需要调用这个函数 。 函数执行时,将强制完成该AIO上的所有操作。 与一般文件IO的fsync用法基本一致。
如果想要所有等待的异步操作不等待而写入持久化的存储中,可以设立一个AIO控制板并调用该函数。
aio_read、aio_write函数会进行数据的缓冲。使用了aio_fsync就不必再去使用aio_read和aio_write了
参数解析:
op: operation为操作码
- O_SYNC : 同步异步IO数据,当前所有IO操作均将完成
- O_DSYNC:同步一个IO请求,并不等待所有的IO完成 (相当于调用fdatasync函数 )
aiocbp:异步请求
lio_listio
int lio_listio( int mode, struct aiocb *aiocb_list[], int nitems,
struct sigevent *sevp);
#include <signal.h>
union sigval { /*传递的参数*/
int sival_int; /* 信号机制传递的参数 */
void *sival_ptr; /* 若是线程机制传递的参数 */
};
struct sigevent {
int sigev_notify; /* 设置通知机制方法,线程为SIGEV_THREAD,信号为SIGEV_SIGNAL*/
int sigev_signo; /* 若是信号机制,该参数设置为触发的信号 */
union sigval sigev_value;/* 传递的参数*/
void (*sigev_notify_function)(union sigval);
/* 若是线程机制,该参数为线程函数*/
void *sigev_notify_attributes;
/* 线程函数的属性 */
};
描述:同时发起多个传输。
意味着我们可以在一个系统调用(一次内核上下文切换)中启动大量的 I/O 操作。从性能的角度来看,大大提高了效率。
参数解析:
mode:
- LIO_WAIT:阻塞这个调用,直到所有的 I/O 都完成为止。
- LIO_NOWAIT:操作进行排队之后,立即返回。
list:一组异步IO请求 (aiocb_list
中任何 NULL
元素都会被忽略)
nitems:请求的个数
sigevent:在所有 I/O 操作都完成时产生信号的方法。
注意:
对于 lio_listio
的请求与传统的 read
或 write
请求在必须指定的操作方面稍有不同。
对于读操作来说,
aio_lio_opcode
域的值为LIO_READ
对于写操作来说,我们要使用
LIO_WRITE
;允许
LIO_NOP
(不执行)
lio_listio 例程
struct aiocb aiocb1, aiocb2;
struct aiocb *list[MAX_LIST];
...
/* Prepare the first 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;
...
bzero( (char *)list, sizeof(list) );
list[0] = &aiocb1;
list[1] = &aiocb2;
ret = lio_listio( LIO_WAIT, list, MAX_LIST, NULL );
AIO例程
现在我们已经了解了有关的 AIO 函数。
接下来,我们将通过信号(signal)和函数回调(callback)来探索异步函数的通知机制。
使用信号进行异步通知
使用信号进行进程间通信(IPC)是 UNIX 中的一种传统机制,AIO 也可以支持这种机制。在这种范例中, 应用程序需要定义信号处理程序,在产生指定的信号时就会调用这个处理程序。应用程序然后配置一个异步请求将在请求完成时产生一个信号。作为信号上下文的一部分,特定的aiocb
请求被提供用来记录多个可能会出现的请求。
/*
我们在 aio_completion_handler 函数中设置信号处理程序来捕获 SIGIO 信号。
然后
- 初始化 aio_sigevent 结构产生 SIGIO 信号来进行通知,
- 指定aio_sigevent.sigev_notify使用信号方式,
- 指定aio_sigevent.sigev_signo使用的信号。
当读操作完成时,信号处理程序就从该信号的 si_value 结构中提取出 aiocb,并检查错误状态和返回状态来确定 I/O 操作是否完成。
对于性能来说,这个处理程序也是通过请求下一次异步传输而继续进行 I/O 操作的理想地方。采用这种方式,在一次数据传输完成时,我们就可以立即开始下一次数据传输操作。
*/
void setup_io( ... )
{
int fd;
struct sigaction sig_act;
struct aiocb my_aiocb;
...
/* Set up the signal handler */
sigemptyset(&sig_act.sa_mask);
sig_act.sa_flags = SA_SIGINFO;
sig_act.sa_sigaction = aio_completion_handler;
/* Set up the AIO request */
bzero( (char *)&my_aiocb, sizeof(struct aiocb) );
my_aiocb.aio_fildes = fd;
my_aiocb.aio_buf = malloc(BUF_SIZE+1);
my_aiocb.aio_nbytes = BUF_SIZE;
my_aiocb.aio_offset = next_offset;
/* Link the AIO request with the Signal Handler */
my_aiocb.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
my_aiocb.aio_sigevent.sigev_signo = SIGIO;
my_aiocb.aio_sigevent.sigev_value.sival_ptr = &my_aiocb;
/* Map the Signal to the Signal Handler */
ret = sigaction( SIGIO, &sig_act, NULL );
...
ret = aio_read( &my_aiocb );
}
void aio_completion_handler( int signo, siginfo_t *info, void *context )
{
struct aiocb *req;
/* Ensure it's our signal */
if (info->si_signo == SIGIO) {
req = (struct aiocb *)info->si_value.sival_ptr;
/* Did the request complete? */
if (aio_error( req ) == 0) {
/* Request completed successfully, get the return status */
ret = aio_return( req );
}
}
return;
}
使用回调函数进行异步通知
另外一种通知方式是系统回调函数。这种机制不会为通知而产生一个信号,而是会调用用户空间的一个函数
来实现通知功能。我们在 sigevent
结构中设置了对 aiocb
的引用,从而可以惟一标识正在完成的特定请求。
/*
在创建自己的 aiocb 请求之后,我们使用 SIGEV_THREAD 请求了一个线程回调函数来作为通知方法(aio_sigevent.sigev_notify指定)。
然后我们将指定特定的通知处理程序,并将要传输的上下文加载到处理程序中(aio_sigevent.notify_function指定,在这种情况中,是个对 aiocb 请求自己的引用)。
在这个处理程序中,我们简单地引用到达的 sigval 指针并使用 AIO 函数来验证请求已经完成。
*/
void setup_io( ... )
{
int fd;
struct aiocb my_aiocb;
...
/* Set up the AIO request */
bzero( (char *)&my_aiocb, sizeof(struct aiocb) );
my_aiocb.aio_fildes = fd;
my_aiocb.aio_buf = malloc(BUF_SIZE+1);
my_aiocb.aio_nbytes = BUF_SIZE;
my_aiocb.aio_offset = next_offset;
/* Link the AIO request with a thread callback */
my_aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD;
my_aiocb.aio_sigevent.notify_function = aio_completion_handler;
my_aiocb.aio_sigevent.notify_attributes = NULL;
my_aiocb.aio_sigevent.sigev_value.sival_ptr = &my_aiocb;
...
ret = aio_read( &my_aiocb );
}
void aio_completion_handler( sigval_t sigval )
{
struct aiocb *req;
req = (struct aiocb *)sigval.sival_ptr;
/* Did the request complete? */
if (aio_error( req ) == 0) {
/* Request completed successfully, get the return status */
ret = aio_return( req );
}
return;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <aio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
static char *memBuffer;
static int sFileDesc;
static struct sigaction sOldSigAction;
static void MySigQuitHandler(int sig)
{
printf("Signal Quit! The number is: %d\n", sig);
}
static void MyFileReadCompleteProcedure(int sig, siginfo_t *si, void *ucontext)
{
printf("The file length is: %zu, and the content is: %s\n", strlen(memBuffer), memBuffer);
int status = close(sFileDesc);
if(status == 0)
puts("File closed successfully!");
else
printf("The error code is: %d\n", status);
free(memBuffer);
// 还原原来的SIGUSR1信号行为
if(sigaction(SIGUSR1, &sOldSigAction, NULL) == -1)
puts("SIGUSR1 signal restore failed!");
}
int main(void)
{
struct sigaction sigAction = { .sa_flags = SA_RESTART, .sa_handler = &MySigQuitHandler };
sigemptyset(&sigAction.sa_mask);
if (sigaction(SIGQUIT, &sigAction, NULL) == -1)
{
puts("Signal failed!");
return -1;
}
sigAction.sa_sigaction = &MyFileReadCompleteProcedure;
if(sigaction(SIGUSR1, &sigAction, &sOldSigAction) == -1)
{
puts("Signal failed!");
return -1;
}
const char *filePath = "myfile.txt";
const mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
sFileDesc = open(filePath, O_RDONLY, mode);
if(sFileDesc == -1)
{
printf("The file: %s cannot be opened!\n", filePath);
return -1;
}
const long fileLength = lseek(sFileDesc, 0, SEEK_END);
lseek(sFileDesc, 0, SEEK_SET);
memBuffer = malloc(fileLength + 1);
memBuffer[fileLength] = '\0';
struct aiocb aioBuffer;
aioBuffer.aio_fildes = sFileDesc;
aioBuffer.aio_offset = 0;
aioBuffer.aio_buf = memBuffer;
aioBuffer.aio_nbytes = fileLength;
aioBuffer.aio_reqprio = 0;
aioBuffer.aio_sigevent = (struct sigevent){.sigev_notify = SIGEV_SIGNAL, .sigev_signo = SIGUSR1, .sigev_value.sival_ptr = memBuffer };
aio_read(&aioBuffer);
getchar();
return 0;
}
附录 :对 AIO 进行系统优化
proc 文件系统包含了两个虚拟文件,它们可以用来对异步 I/O 的性能进行优化( 这对于大部分应用程序来说都已经足够了):
- /proc/sys/fs/aio-nr 文件提供了系统范围异步 I/O 请求现在的数目。
- /proc/sys/fs/aio-max-nr 文件是所允许的并发请求的最大个数。最大个数通常是 64KB
参考资料
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。
- POSIX.1b 实现 从 GNU Library 的角度介绍了 AIO 的详细内幕。
- Realtime Support in Linux 解释了更多有关 AIO 和很多实时扩展的信息,内容从调度、POSIX I/O 到 POSIX 线程和高分辨率的定时器(HRT)。
- 在为 2.5 版本内核集成而编写的 Design Notes 中,我们可以学习有关 Linux 中 AIO 的设计和实现的知识。
- 在 developerWorks Linux 专区 中可以找到为 Linux 开发人员准备的更多资源。
- 随时关注 developerWorks 技术事件和网络广播。