Linux的异步IO有两种方式,一种是由glibc提供的aio_xxx函数组成的伪异步IO,另一种则是由Linux内核支持的io_xxx系统调用组成的异步IO。在命名的方式上,glibc相应的函数和数据结构均以aio开头,而Linux系统调用及相应的数据结构则以io或io_开头(aio_context_t是一个例外)。以下分别介绍这两种异步IO方式。
一、glibc异步IO
glibc采用多线程同步的方式来实现异步IO,确切地说这并不是真正意义上的异步IO。我们把调用aio_xxx的线程称为主线程,而把实际请求read/write/fsync的线程称为异步IO线程。
(1) int aio_read(struct aiocb *aiocbp);
主线程先将aiocbp->__error_code设为EINPROGRESS,然后把aiocbp挂到aiocbp->aio_fildes的请求队列。这个请求队列是一个优先队列,aio_fildes相同的aiocb按照优先级进行排序。如果不存在为aio_fildes处理的异步IO线程,那么主线程将会创建一个异步IO线程来处理aio_fildes队列中的请求。对每个aiocb,异步IO线程会先尝试使用pread来读取数据,如果pread返回ESPIPE的错误(例如对管道和套接字进行pread操作),它会再次用read系统调用来读取,在这种情况下,aio_offset将会被忽略。当操作成功完成后,异步IO线程将aiocbp->__error_code设成0,并用操作返回的值来设置aiocbp->__return_value。如果操作失败,则把它设成执行pread/read, pwrite/write,或者fsync/fdatasync出错时的errno。如果操作被撤销,则将__error_code设成ECANCELED。
(2)int aio_write(struct aiocb *aiocbp);
这个函数与aio_read类似,不同的是,异步IO线程会先尝试使用pwrite来写入数据,如果pwrite返回ESPIPE的错误(例如对管道和套接字进行pwrite操作),它会再次用write系统调用来写入,在这种情况下,aio_offset将会被忽略。
(3)int aio_fsync(int op, struct aiocb *aiocbp);
异步IO线程使用fsync或者fdatasync系统调用来完成主线程的请求:当op是O_SYNC时调用fsync,当op是O_DSYNC时调用fdatasync。
(4)int aio_cancel(int fd, struct aiocb *aiocbp);
主线程撤销异步IO请求。如果aiocbp为NULL,则撤销fd上所有的IO请求,否则只撤销aiocbp对应的请求(要求fd=aiocbp->aio_fildes)。
(5)int aio_error(const struct aiocb *aiocbp);
这个函数返回aiocbp->__error_code的值。
(6)int aio_return(const struct aiocb* aiocbp);
这个函数返回aiocbp->__return_value的值。
(7)int aio_suspend(const struct aiocb * const aiocb_list[], int nitems, const struct timespec *timeout);
这个函数有一点儿类似select,但也有所不同,select的fd_set是没用const修饰的,它们除了用作输入参数外也用作输出参数。而aio_suspend函数的第一个参数是用const修饰的,它只用作输入参数。
(8)获取异步IO的处理状态与结果
在aiocb结构体中有一个sigevent类型的成员aio_sigevent。sigevent的定义如下(sigevent(7)):
union sigval { /* Data passed with notification */
int sival_int; /* Integer value */
void *sival_ptr; /* Pointer value */
};
struct sigevent {
int sigev_notify; /* Notification method:SIGEV_NONE, SIGEV_SIGNAL, SIGEV_THREAD */
int sigev_signo; /* Notification signal */
union sigval sigev_value; /* Data passed with
notification */
void (*sigev_notify_function) (union sigval);
/* Function used for thread
notification (SIGEV_THREAD) */
void *sigev_notify_attributes;
/* Attributes for notification thread
(SIGEV_THREAD) */
pid_t sigev_notify_thread_id;
/* ID of thread to signal (SIGEV_THREAD_ID) */
};
第一种方式SIGEV_NONE:异步IO线程不通知。在这种情况下,主线程可以采用aio_error循环测试,这种方式效率低下且浪费CPU的时间,另外可以采用aio_suspend,这样效率会更高一些。
第二种方式SIGEV_SIGNAL:采用信号的方式通知,这种方式效率比第一种方式要高,但是编写的信号处理函数必须保证是异步信号安全的。
第三种方式SIGEV_THREAD:创建一个线程来完成通知事件的处理,这里涉及到线程同步的问题。
二、Linux内核异步IO处理
(1)int io_setup(unsigned nr_events, aio_context_t *ctx_idp);
该函数会在内核中创建一个kioctx对象。确切地说,在一个进程描述符中的mm_struct结构中,有一个ioctx_list指针,它指向了一个kioctx链表。每次创建一个kioctx对象时,都将它插入到这个链表的头部。kioctx::user_id用来唯一的标识了这个kioctx。io_setup函数返回的ctx_idp指向的值就是这个user_id。
(2)int io_submit(aio_context_t ctx_id, long nr, struct iocb **iocbpp);
将iocb数组提交到ctx_id指定的kioctx对象。
(3)int io_cancel(aio_context_t ctx_id, struct iocb *iocb, struct io_event *result);
将iocb从ctx_id指定的kioctx对象中撤销掉。
(4)int io_getevents(aio_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout);
从ctx_id指定的kioctx对象的完成队列中读取事件。
(5)int io_destroy(aio_context_t ctx_id);
销毁ctx_id指定的kioctx对象。
io_setup返回的描述符(aio_context_t)与epoll_create返回的描述符类似。io_submit和io_cancel函数则对应epoll_ctl函数(EPOLL_CTL_ADD和EPOLL_CTL_DEL)。io_getevents则对应于epoll_wait。io_destroy对应于close。
有关Linux内核异步IO的实现后续会在这里加上~~