linux 异步IO通信
一. 回顾
做java开发的,一定对BIO,NIO,AIO通信很了解了,现在再在下面罗列一下:
同步阻塞IO(JAVA BIO):
同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
同步非阻塞IO(Java NIO) :
同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。用户进程也需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问。
异步阻塞IO(Java NIO):
此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄(如果从UNP的角度看,select属于同步操作。因为select之后,进程还需要读写数据),从而提高系统的并发性!
(Java AIO(NIO.2))异步非阻塞IO:
在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。
BIO、NIO、AIO适用场景分析:
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
针对Java开发,基本上也就上面的三种通信模型了。我们都知道Linux下的五种I/O模型
1)阻塞I/O(blocking I/O)
2)非阻塞I/O (nonblocking I/O)
3) I/O复用(select 和poll) (I/O multiplexing)
4)信号驱动I/O (signal driven I/O (SIGIO))
5)异步I/O (asynchronous I/O (the POSIX aio_functions))
前四种都是同步,只有最后一种才是异步IO。是的,异步IO是今天介绍的重点。
二. 异步IO通信版本
异步IO通信在linux下有两个版本:glibc版本,linux内核版本。
2.1 glibc版本
2.1.1 接口和实现信息
int aio_read(struct aiocb *aiocbp); /* 提交一个异步读 */
int aio_write(struct aiocb *aiocbp); /* 提交一个异步写 */
int aio_cancel(int fildes, struct aiocb *aiocbp); /* 取消一个异步请求(或基于一个fd的所有异步请求,aiocbp==NULL) */
int aio_error(const struct aiocb *aiocbp); /* 查看一个异步请求的状态(进行中EINPROGRESS?还是已经结束或出错?) */
ssize_t aio_return(struct aiocb *aiocbp); /* 查看一个异步请求的返回值(跟同步读写定义的一样) */
int aio_suspend(const struct aiocb * const list[], int nent, const struct timespec *timeout); /* 阻塞等待请求完成 */
其中,struct aiocb主要包含以下字段:
int aio_fildes; /* 要被读写的fd */
void * aio_buf; /* 读写操作对应的内存buffer */
__off64_t aio_offset; /* 读写操作对应的文件偏移 */
size_t aio_nbytes; /* 需要读写的字节长度 */
int aio_reqprio; /* 请求的优先级 */
struct sigevent aio_sigevent; /* 异步事件,定义异步操作完成时的通知信号或回调函数 */
glibc的aio实现是比较通俗易懂的:
1、异步请求被提交到request_queue中;
2、request_queue实际上是一个表结构,"行"是fd、"列"是具体的请求。也就是说,同一个fd的请求会被组织在一起;
3、异步请求有优先级概念,属于同一个fd的请求会按优先级排序,并且最终被按优先级顺序处理;
4、随着异步请求的提交,一些异步处理线程被动态创建。这些线程要做的事情就是从request_queue中取出请求,然后处理之;
5、为避免异步处理线程之间的竞争,同一个fd所对应的请求只由一个线程来处理;
6、异步处理线程同步地处理每一个请求,处理完成后在对应的aiocb中填充结果,然后触发可能的信号通知或回调函数(回调函数是需要创建新线程来调用的);
7、异步处理线程在完成某个fd的所有请求后,进入闲置状态;
8、异步处理线程在闲置状态时,如果request_queue中有新的fd加入,则重新投入工作,去处理这个新fd的请求(新fd和它上一次处理的fd可以不是同一个);
9、异步处理线程处于闲置状态一段时间后(没有新的请求),则会自动退出。等到再有新的请求时,再去动态创建;
2.2 linux内核版本
2.2.1 接口和实现信息
下面再来看看linux版本的异步IO。它主要包含如下系统调用接口:
int io_setup(int maxevents, io_context_t *ctxp); /* 创建一个异步IO上下文(io_context_t是一个句柄) */
int io_destroy(io_context_t ctx); /* 销毁一个异步IO上下文(如果有正在进行的异步IO,取消并等待它们完成) */
long io_submit(aio_context_t ctx_id, long nr, struct iocb **iocbpp); /* 提交异步IO请求 */
long io_cancel(aio_context_t ctx_id, struct iocb *iocb, struct io_event *result); /* 取消一个异步IO请求 */
long io_getevents(aio_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout) /* 等待并获取异步IO请求的事件(也就是异步请求的处理结果) */
其中,struct iocb主要包含以下字段:
__u16 aio_lio_opcode; /* 请求类型(如:IOCB_CMD_PREAD=读、IOCB_CMD_PWRITE=写、等) */
__u32 aio_fildes; /* 要被操作的fd */
__u64 aio_buf; /* 读写操作对应的内存buffer */
__u64 aio_nbytes; /* 需要读写的字节长度 */
__s64 aio_offset; /* 读写操作对应的文件偏移 */
__u64 aio_data; /* 请求可携带的私有数据(在io_getevents时能够从io_event结果中取得) */
__u32 aio_flags; /* 可选IOCB_FLAG_RESFD标记,表示异步请求处理完成时使用eventfd进行通知(百度一下) */
__u32 aio_resfd; /* 有IOCB_FLAG_RESFD标记时,接收通知的eventfd */
其中,struct io_event主要包含以下字段:
__u64 data; /* 对应iocb的aio_data的值 */
__u64 obj; /* 指向对应iocb的指针 */
__s64 res; /* 对应IO请求的结果(>=0: 相当于对应的同步调用的返回值;<0: -errno) */
io_context_t句柄在内核中对应一个struct kioctx结构,用来给一组异步IO请求提供一个上下文。其主要包含以下字段:
struct mm_struct* mm; /* 调用者进程对应的内存管理结构(代表了调用者的虚拟地址空间) */
unsigned long user_id; /* 上下文ID,也就是io_context_t句柄的值(等于ring_info.mmap_base) */
struct hlist_node list; /* 属于同一地址空间的所有kioctx结构通过这个list串连起来,链表头是mm->ioctx_list */
wait_queue_head_t wait; /* 等待队列(io_getevents系统调用可能需要等待,调用者就在该等待队列上睡眠) */
int reqs_active; /* 进行中的请求数目 */
struct list_head active_reqs; /* 进行中的请求队列 */
unsigned max_reqs; /* 最大请求数(对应io_setup调用的int maxevents参数) */
struct list_head run_list; /* 需要aio线程处理的请求列表(某些情况下,IO请求可能交给aio线程来提交) */
struct delayed_work wq; /* 延迟任务队列(当需要aio线程处理请求时,将wq挂入aio线程对应的请求队列) */
struct aio_ring_info ring_info; /* 存放请求结果io_event结构的ring buffer */
其中,这个aio_ring_info结构比较值得一提,它是用于存放请求结果io_event结构的ring buffer。它主要包含了如下字段:
unsigned long mmap_base; /* ring buffer的地始地址 */
unsigned long mmap_size; /* ring buffer分配空间的大小 */
struct page** ring_pages; /* ring buffer对应的page数组 */
long nr_pages; /* 分配空间对应的页面数目(nr_pages * PAGE_SIZE = mmap_size) */
unsigned nr, tail; /* 包含io_event的数目及存取游标 */
2.3 例子
#include <stdio.h> #include <libaio.h> #include <sys/eventfd.h> #include <sys/epoll.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <errno.h> #include <string> using std::string; #define TEST_FILE "./aiotestfile.txt" #define ALIGN_SIZE 512 #define RD_SIZE 1024 #define RD_EVENT_NUM 5 bool create_test_file(const string &a_strFileName) { FILE *file = fopen(a_strFileName.c_str(), "w"); if(NULL == file) { perror("fopen"); return false; } fprintf(file, "my name is \n"); fprintf(file, "stars\n"); fprintf(file, "what's your name?\n"); fprintf(file, "what's your name?\n"); fprintf(file, "what's your name?\n"); fprintf(file, "what's your name?\n"); fprintf(file, "what's your name?\n"); fprintf(file, "what's your name?\n"); fprintf(file, "what's your name?\n"); fprintf(file, "what's your name?\n"); fprintf(file, "what's your name?\n"); fprintf(file, "stars\n"); fprintf(file, "stars\n"); fprintf(file, "stars\n"); fprintf(file, "stars\n"); fprintf(file, "stars\n"); fclose(file); return true; } void aio_call_back(io_context_t ctx, struct iocb *iocb, long res, long res2) { printf("can read %d bytes, and in fact has read %d bytes\n", iocb->u.c.nbytes, res); printf("the content is :\n%s", iocb->u.c.buf); } int main(int argc, char* argv[]) { if(!create_test_file(TEST_FILE)) { perror("create_test_file"); return -1; } int efd = eventfd(0, EFD_NONBLOCK|EFD_CLOEXEC); if(efd == -1) { perror("eventfd"); return -1; } int epollfd = epoll_create(1); if(epollfd == -1) { perror("epoll_create"); return -1; } struct epoll_event epevent; epevent.events = EPOLLIN|EPOLLET; epevent.data.ptr = NULL; if(epoll_ctl(epollfd, EPOLL_CTL_ADD, efd, &epevent) != 0) { perror("epoll_ctr"); return -1; } int fd = open(TEST_FILE, O_RDWR|O_DIRECT); if(fd < 0) { perror("open"); return -1; } void *buf; if(posix_memalign(&buf, ALIGN_SIZE, RD_SIZE)) { perror("posix_memalign"); return -1; } struct iocb *ocbs[RD_EVENT_NUM]; for(int i = 0; i < RD_EVENT_NUM; ++i) { ocbs[i] = (struct iocb*)malloc(sizeof(struct iocb)); io_prep_pread(ocbs[i], fd, buf, RD_SIZE, RD_SIZE * i); io_set_eventfd(ocbs[i], efd); io_set_callback(ocbs[i], aio_call_back); } io_context_t ctx = NULL; if(io_setup(8192, &ctx)) { perror("io_setup"); return -1; } if(io_submit(ctx, RD_EVENT_NUM, ocbs) != RD_EVENT_NUM) { perror("io_submit"); return -1; } struct epoll_event *events_list = (struct epoll_event *)malloc(sizeof(struct epoll_event) * 32); int retnum = 0; while(retnum < RD_EVENT_NUM) { int epnum = epoll_wait(epollfd, events_list, 32, -1); if(epnum <= 0){ if(errno != EINTR){ perror("epoll_wait"); exit(9); } } else { int ready; int n = read(efd, &ready, sizeof(ready)); if(n != 8){ perror("read error"); exit(10); } printf("finished io number: %d\n", ready); while(ready > 0) { struct timespec tms; tms.tv_sec = 0; tms.tv_nsec = 0; struct io_event *events = (struct io_event *)malloc(sizeof(struct io_event) * RD_EVENT_NUM); int ret = io_getevents(ctx, 1, RD_EVENT_NUM, events, &tms); if(ret > 0) { for(int v = 0; v < ret; ++v) { ((io_callback_t)(events[v].data))(ctx, events[v].obj, events[v].res, events[v].res2); } retnum += ret; ready -= ret; } } } } close(epollfd); free(buf); close(fd); io_destroy(ctx); close(efd); remove(TEST_FILE); for(int m = 0; m < RD_EVENT_NUM; m++) { free(ocbs[m]); } return 0; }
2.4 比较
从上面的流程可以看出,linux版本的异步IO实际上只是利用了CPU和IO设备可以异步工作的特性(IO请求提交的过程主要还是在调用者线程上同步完成的,请求提交后由于CPU与IO设备可以并行工作,所以调用流程可以返回,调用者可以继续做其他事情)。相比同步IO,并不会占用额外的CPU资源。
而glibc版本的异步IO则是利用了线程与线程之间可以异步工作的特性,使用了新的线程来完成IO请求,这种做法会额外占用CPU资源(对线程的创建、销毁、调度都存在CPU开销,并且调用者线程和异步处理线程之间还存在线程间通信的开销)。不过,IO请求提交的过程都由异步处理线程来完成了(而linux版本是调用者来完成的请求提交),调用者线程可以更快地响应其他事情。如果CPU资源很富足,这种实现倒也还不错。
还有一点,当调用者连续调用异步IO接口,提交多个异步IO请求时。在glibc版本的异步IO中,同一个fd的读写请求由同一个异步处理线程来完成。而异步处理线程又是同步地、一个一个地去处理这些请求。所以,对于底层的IO调度器来说,它一次只能看到一个请求。处理完这个请求,异步处理线程才会提交下一个。而内核实现的异步IO,则是直接将所有请求都提交给了IO调度器,IO调度器能看到所有的请求。请求多了,IO调度器使用的类电梯算法就能发挥更大的功效。请求少了,极端情况下(比如系统中的IO请求都集中在同一个fd上,并且不使用预读),IO调度器总是只能看到一个请求,那么电梯算法将退化成先来先服务算法,可能会极大的增加碰头移动的开销。
最后,glibc版本的异步IO支持非direct-io,可以利用内核提供的page cache来提高效率。而linux版本只支持direct-io,cache的工作就只能靠用户程序来实现了。
参考地址:
http://blog.csdn.net/u012398613/article/details/22897279 参考
http://lse.sourceforge.net/io/aio.html 使用帮助
http://www.ibm.com/developerworks/cn/linux/l-async/ 专业技术文档
http://www.jiangmiao.org/blog/2290.html API整理
http://blog.sina.com.cn/s/blog_87c80c500100yol3.html 有例子
http://blog.sina.com.cn/s/blog_3e3fcadd0100grgk.html 例子和API讲的很细