- 浏览: 1481942 次
- 性别:
- 来自: 北京
文章分类
- 全部博客 (691)
- linux (207)
- shell (33)
- java (42)
- 其他 (22)
- javascript (33)
- cloud (16)
- python (33)
- c (48)
- sql (12)
- 工具 (6)
- 缓存 (16)
- ubuntu (7)
- perl (3)
- lua (2)
- 超级有用 (2)
- 服务器 (2)
- mac (22)
- nginx (34)
- php (2)
- 内核 (2)
- gdb (13)
- ICTCLAS (2)
- mac android (0)
- unix (1)
- android (1)
- vim (1)
- epoll (1)
- ios (21)
- mysql (3)
- systemtap (1)
- 算法 (2)
- 汇编 (2)
- arm (3)
- 我的数据结构 (8)
- websocket (12)
- hadoop (5)
- thrift (2)
- hbase (1)
- graphviz (1)
- redis (1)
- raspberry (2)
- qemu (31)
- opencv (4)
- socket (1)
- opengl (1)
- ibeacons (1)
- emacs (6)
- openstack (24)
- docker (1)
- webrtc (11)
- angularjs (2)
- neutron (23)
- jslinux (18)
- 网络 (13)
- tap (9)
- tensorflow (8)
- nlu (4)
- asm.js (5)
- sip (3)
- xl2tp (5)
- conda (1)
- emscripten (6)
- ffmpeg (10)
- srt (1)
- wasm (5)
- bert (3)
- kaldi (4)
- 知识图谱 (1)
最新评论
-
wahahachuang8:
我喜欢代码简洁易读,服务稳定的推送服务,前段时间研究了一下go ...
websocket的helloworld -
q114687576:
http://www.blue-zero.com/WebSoc ...
websocket的helloworld -
zhaoyanzimm:
感谢您的分享,给我提供了很大的帮助,在使用过程中发现了一个问题 ...
nginx的helloworld模块的helloworld -
haoningabc:
leebyte 写道太NB了,期待早日用上Killinux!么 ...
qemu+emacs+gdb调试内核 -
leebyte:
太NB了,期待早日用上Killinux!
qemu+emacs+gdb调试内核
转载http://hi.baidu.com/_kouu/blog/item/e225f67b337841f42f73b341.html
linux异步IO浅析
2011-06-18 16:46
知道异步IO已经很久了,但是直到最近,才真正用它来解决一下实际问题(在一个CPU密集型的应用中,有一些需要处理的数据可能放在磁盘上。预先知道这些数据的位置,所以预先发起异步IO读请求。等到真正需要用到这些数据的时候,再等待异步IO完成。使用了异步IO,在发起IO请求到实际使用数据这段时间内,程序还可以继续做其他事情)。
假此机会,也顺便研究了一下linux下的异步IO的实现。
linux下主要有两套异步IO,一套是由glibc实现的(以下称之为glibc版本)、一套是由linux内核实现,并由libaio来封装调用接口(以下称之为linux版本)。
glibc版本
接口
glibc版本主要包含如下接口:
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、异步处理线程处于闲置状态一段时间后(没有新的请求),则会自动退出。等到再有新的请求时,再去动态创建;
看起来,换作是我们,要在用户态实现一个异步IO,似乎大概也会设计成类似的样子……
linux版本
接口
下面再来看看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的数目及存取游标 */
这个数据结构看起来有些奇怪,直接弄一个io_event数组不就完事了么?为什么要维护mmap_base、mmap_size、ring_pages、nr_pages这么复杂的一组信息,而又把io_event结构隐藏起来呢?
这里的奇妙之处就在于,io_event结构的buffer是在用户态地址空间上分配的。注意,我们在内核里面看到了诸多数据结构都是在内核地址空间上分配的,因为这些结构都是内核专有的,没必要给用户程序看到,更不能让用户程序去修改。而这里的io_event却是有意让用户程序看到,而且用户就算修改了也不会对内核的正确性造成影响。于是这里使用了这样一个有些取巧的办法,由内核在用户态地址空间上分配buffer。(如果换一个保守点的做法,内核态可以维护io_event的buffer,然后io_getevents的时候,将对应的io_event复制一份到用户空间。)
按照这样的思路,io_setup时,内核会通过mmap在对应的用户空间分配一段内存,mmap_base、mmap_size就是这个内存映射对应的位置和大小。然后,光有映射还不行,还必须立马分配物理内存,ring_pages、nr_pages就是分配好的物理页面。(因为这些内存是要被内核直接访问的,内核会将异步IO的结果写入其中。如果物理页面延迟分配,那么内核访问这些内存的时候会发生缺页异常。而处理内核态的缺页异常又很麻烦,所以还不如直接分配物理内存的好。其二,内核在访问这个buffer里的信息时,也并不是通过mmap_base这个虚拟地址去直接访问的。既然是异步,那么结果写回的时候可能是在另一个上下文上面,虚拟地址空间都不同。为了避免进行虚拟地址空间的切换,内核干脆直接通过kmap将ring_pages映射到高端内存上去访问好了。)
然后,在mmap_base指向的用户空间的地址上,会存放着一个struct aio_ring结构,用来管理这个ring buffer。其主要包含了如下字段:
unsigned id; /* 等于aio_ring_info中的user_id */
unsigned nr; /* 等于aio_ring_info中的nr */
unsigned head,tail; /* io_events数组的游标 */
unsigned magic,compat_features,incompat_features;
unsigned header_length; /* aio_ring结构的大小 */
struct io_event io_events[0]; /* io_event的buffer */
终于,我们期待的io_event数组出现了。
看到这里,如果前面的内容你已经理解清楚了,你一定会有个疑问:既然整个aio_ring结构及其中的io_event缓冲都是放在用户空间的,内核还提供io_getevents系统调用干什么?用户程序不是直接就可以取用io_event,并且修改游标了么(内核作为生产者,修改aio_ring->tail;用户作为消费者,修改aio_ring->head)?我想,aio_ring之所以要放在用户空间,其原本用意应该就是这样的。
那么,用户空间如何知道aio_ring结构的地址(aio_ring_info->mmap_base)呢?其实kioctx结构中的user_id,也就是io_setup返回给用户的io_context_t,就等于aio_ring_info->mmap_base。
然后,aio_ring结构中还有诸如magic、compat_features、incompat_features这样的字段,用户空间可以读这些magic,以确定数据结构没有被异常篡改。如果一切可控,那么就自己动手、丰衣足食;否则就还是走io_getevents系统调用。而io_getevents系统调用通过aio_ring_info->ring_pages得到aio_ring结构,再将相应的io_event拷贝到用户空间。
下面贴一段libaio中的io_getevents的代码(前面提到过,linux版本的异步IO是由用户态的libaio来封装的):
int io_getevents_0_4(io_context_t ctx, long min_nr, long nr, struct io_event * events, struct timespec * timeout){
struct aio_ring *ring;
ring = (struct aio_ring*)ctx;
if (ring==NULL || ring->magic != AIO_RING_MAGIC)
goto do_syscall;
if (timeout!=NULL && timeout->tv_sec == 0 && timeout->tv_nsec == 0) {
if (ring->head == ring->tail)
return 0;
}
do_syscall:
return __io_getevents_0_4(ctx, min_nr, nr, events, timeout);
}
其中确实用到了用户空间上的aio_ring结构的信息,不过尺度还是不够大。
以上就是异步IO的context的结构。那么,为什么linux版本的异步IO需要“上下文”这么个概念,而glibc版本则不需要呢?
在glibc版本中,异步处理线程是glibc在调用者进程中动态创建的线程,它和调用者必定是在同一个虚拟地址空间中的。这里已经隐含了“同一上下文”这么个关系。
而对于内核来说,要面对的是任意的进程,任意的虚拟地址空间。当处理一个异步请求时,内核需要在调用者对应的地址空间中存取数据,必须知道这个虚拟地址空间是什么。不过当然,如果设计上要想把“上下文”这个概念隐藏了也是肯定可以的(比如让每个mm隐含一个异步IO上下文)。具体如何选择,只是设计上的问题。
struct iocb在内核中又对应到struct kiocb结构,主要包含以下字段:
struct kioctx* ki_ctx; /* 请求对应的kioctx(上下文结构) */
struct list_head ki_run_list; /* 需要aio线程处理的请求,通过该字段链入ki_ctx->run_list */
struct list_head ki_list; /* 链入ki_ctx->active_reqs */
struct file* ki_filp; /* 对应的文件指针 */
void __user* ki_obj.user; /* 指向用户态的iocb结构 */
__u64 ki_user_data; /* 等于iocb->aio_data */
loff_t ki_pos; /* 等于iocb->aio_offset */
unsigned short ki_opcode; /* 等于iocb->aio_lio_opcode */
size_t ki_nbytes; /* 等于iocb->aio_nbytes */
char __user * ki_buf; /* 等于iocb->aio_buf */
size_t ki_left; /* 该请求剩余字节数(初值等于iocb->aio_nbytes) */
struct eventfd_ctx* ki_eventfd; /* 由iocb->aio_resfd对应的eventfd对象 */
ssize_t (*ki_retry)(struct kiocb *); /*由ki_opcode选择的请求提交函数*/
调用io_submit后,对应于用户传递的每一个iocb结构,会在内核态生成一个与之对应的kiocb结构,并且在对应kioctx结构的ring_info中预留一个io_events的空间。之后,请求的处理结果就被写到这个io_event中。
然后,对应的异步读写(或其他)请求就被提交到了虚拟文件系统,实际上就是调用了file->f_op->aio_read或file->f_op->aio_write(或其他)。也就是,在经历磁盘高速缓存层、通用块层之后,请求被提交到IO调度层,等待被处理。这个跟普通的文件读写请求是类似的。
在《linux文件读写浅析》中可以看到,对于非direct-io的读请求来说,如果page cache不命中,那么IO请求会被提交到底层。之后,do_generic_file_read会通过lock_page操作,等待数据最终读完。这一点跟异步IO是背道而驰的,因为异步就意味着请求提交后不能等待,必须马上返回。而对于非direct-io的写请求,写操作一般仅仅是将数据更新作用到page cache上,并不需要真正的写磁盘。page cache写回磁盘本身是一个异步的过程。可见,对于非direct-io的文件读写,使用linux版本的异步IO接口完全没有意义(就跟使用同步接口效果一样)。
为什么会有这样的设计呢?因为非direct-io的文件读写是只跟page cache打交道的。而page cache是内存,跟内存打交道又不会存在阻塞,那么也就没有什么异步的概念了。至于读写磁盘时发生的阻塞,那是page cache跟磁盘打交道时发生的事情,跟应用程序又没有直接关系。
然而,对于direct-io来说,异步则是有意义的。因为direct-io是应用程序的buffer跟磁盘的直接交互(不使用page cache)。
这里,在使用direct-io的情况下,file->f_op->aio_{read,write}提交完IO请求就直接返回了,然后io_submit系统调用返回。(见后面的执行流程。)
通过linux内核异步触发的IO调度(如:被时钟中断触发、被其他的IO请求触发、等),已经提交的IO请求被调度,由对应的设备驱动程序提交给具体的设备。对于磁盘,一般来说,驱动程序会发起一次DMA。然后又经过若干时间,读写请求被磁盘处理完成,CPU将收到表示DMA完成的中断信号,设备驱动程序注册的处理函数将在中断上下文中被调用。这个处理函数会调用end_request函数来结束这次请求。这个流程跟《linux文件读写浅析》中所说的非direct-io读操作的情况是一样的。
不同的是,对于同步非direct-io,end_request将通过清除page结构的PG_locked标记来唤醒被阻塞的读操作流程,异步IO和同步IO效果一样。而对于direct-io,除了唤醒被阻塞的读操作流程(同步IO)或io_getevents流程(异步IO)之外,还需要将IO请求的处理结果填回对应的io_event中。
最后,等到调用者调用io_getevents的时候,就能获取到请求对应的结果(io_event)。而如果调用io_getevents的时候结果还没出来,流程也会被阻塞,并且会在direct-io的end_request过程中得到唤醒。
linux版本的异步IO也有aio线程(每CPU一个),但是跟glibc版本中的异步处理线程不同,这里的aio线程是用来处理请求重试的。某些情况下,file->f_op->aio_{read,write}可能会返回-EIOCBRETRY,表示需要重试(只有一些特殊的IO设备会这样)。而调用者既然使用的是异步IO接口,肯定不希望里面会有等待/重试的逻辑。所以,如果遇到-EIOCBRETRY,内核就在当前CPU对应的aio线程添加一个任务,让aio线程来完成请求的重新提交。而调用流程可以直接返回,不需要阻塞。
请求在aio线程中提交和在调用者进程中提交相比,有一个最大的不同,就是aio线程使用的地址空间可能跟调用者线程不一样。需要利用kioctx->mm切换到正确的地址空间,然后才能发请求。(参见《浅尝异步IO》中的讨论。)
内核处理流程
最后,整理一下direct-io异步读操作的处理流程:
io_submit。对于提交的iocbpp数组中的每一个iocb(异步请求),调用io_submit_one来提交它们;
io_submit_one。为请求分配一个kiocb结构,并且在对应的kioctx的ring_info中为它预留一个对应的io_event。然后调用aio_rw_vect_retry来提交这个读请求;
aio_rw_vect_retry。调用file->f_op->aio_read。这个函数通常是由generic_file_aio_read或者其封装来实现的;
generic_file_aio_read。对于非direct-io,会调用do_generic_file_read来处理请求(见《linux文件读写浅析》)。而对于direct-io,则是调用mapping->a_ops->direct_IO。这个函数通常就是blkdev_direct_IO;
blkdev_direct_IO。调用filemap_write_and_wait_range将相应位置可能存在的page cache废弃掉或刷回磁盘(避免产生不一致),然后调用direct_io_worker来处理请求;
direct_io_worker。一次读可能包含多个读操作(对应于类readv系统调用),对于其中的每一个,调用do_direct_IO;
do_direct_IO。调用submit_page_section;
submit_page_section。调用dio_new_bio分配对应的bio结构,然后调用dio_bio_submit来提交bio;
dio_bio_submit。调用submit_bio提交请求。后面的流程就跟非direct-io是一样的了,然后等到请求完成,驱动程序将调用 bio->bi_end_io来结束这次请求。对于direct-io下的异步IO,bio->bi_end_io等于dio_bio_end_aio;
dio_bio_end_aio。调用wake_up_process唤醒被阻塞的进程(异步IO下,主要是io_getevents的调用者)。然后调用aio_complete;
aio_complete。将处理结果写回到对应的io_event中;
比较
从上面的流程可以看出,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的工作就只能靠用户程序来实现了。
linux异步IO浅析
2011-06-18 16:46
知道异步IO已经很久了,但是直到最近,才真正用它来解决一下实际问题(在一个CPU密集型的应用中,有一些需要处理的数据可能放在磁盘上。预先知道这些数据的位置,所以预先发起异步IO读请求。等到真正需要用到这些数据的时候,再等待异步IO完成。使用了异步IO,在发起IO请求到实际使用数据这段时间内,程序还可以继续做其他事情)。
假此机会,也顺便研究了一下linux下的异步IO的实现。
linux下主要有两套异步IO,一套是由glibc实现的(以下称之为glibc版本)、一套是由linux内核实现,并由libaio来封装调用接口(以下称之为linux版本)。
glibc版本
接口
glibc版本主要包含如下接口:
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、异步处理线程处于闲置状态一段时间后(没有新的请求),则会自动退出。等到再有新的请求时,再去动态创建;
看起来,换作是我们,要在用户态实现一个异步IO,似乎大概也会设计成类似的样子……
linux版本
接口
下面再来看看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的数目及存取游标 */
这个数据结构看起来有些奇怪,直接弄一个io_event数组不就完事了么?为什么要维护mmap_base、mmap_size、ring_pages、nr_pages这么复杂的一组信息,而又把io_event结构隐藏起来呢?
这里的奇妙之处就在于,io_event结构的buffer是在用户态地址空间上分配的。注意,我们在内核里面看到了诸多数据结构都是在内核地址空间上分配的,因为这些结构都是内核专有的,没必要给用户程序看到,更不能让用户程序去修改。而这里的io_event却是有意让用户程序看到,而且用户就算修改了也不会对内核的正确性造成影响。于是这里使用了这样一个有些取巧的办法,由内核在用户态地址空间上分配buffer。(如果换一个保守点的做法,内核态可以维护io_event的buffer,然后io_getevents的时候,将对应的io_event复制一份到用户空间。)
按照这样的思路,io_setup时,内核会通过mmap在对应的用户空间分配一段内存,mmap_base、mmap_size就是这个内存映射对应的位置和大小。然后,光有映射还不行,还必须立马分配物理内存,ring_pages、nr_pages就是分配好的物理页面。(因为这些内存是要被内核直接访问的,内核会将异步IO的结果写入其中。如果物理页面延迟分配,那么内核访问这些内存的时候会发生缺页异常。而处理内核态的缺页异常又很麻烦,所以还不如直接分配物理内存的好。其二,内核在访问这个buffer里的信息时,也并不是通过mmap_base这个虚拟地址去直接访问的。既然是异步,那么结果写回的时候可能是在另一个上下文上面,虚拟地址空间都不同。为了避免进行虚拟地址空间的切换,内核干脆直接通过kmap将ring_pages映射到高端内存上去访问好了。)
然后,在mmap_base指向的用户空间的地址上,会存放着一个struct aio_ring结构,用来管理这个ring buffer。其主要包含了如下字段:
unsigned id; /* 等于aio_ring_info中的user_id */
unsigned nr; /* 等于aio_ring_info中的nr */
unsigned head,tail; /* io_events数组的游标 */
unsigned magic,compat_features,incompat_features;
unsigned header_length; /* aio_ring结构的大小 */
struct io_event io_events[0]; /* io_event的buffer */
终于,我们期待的io_event数组出现了。
看到这里,如果前面的内容你已经理解清楚了,你一定会有个疑问:既然整个aio_ring结构及其中的io_event缓冲都是放在用户空间的,内核还提供io_getevents系统调用干什么?用户程序不是直接就可以取用io_event,并且修改游标了么(内核作为生产者,修改aio_ring->tail;用户作为消费者,修改aio_ring->head)?我想,aio_ring之所以要放在用户空间,其原本用意应该就是这样的。
那么,用户空间如何知道aio_ring结构的地址(aio_ring_info->mmap_base)呢?其实kioctx结构中的user_id,也就是io_setup返回给用户的io_context_t,就等于aio_ring_info->mmap_base。
然后,aio_ring结构中还有诸如magic、compat_features、incompat_features这样的字段,用户空间可以读这些magic,以确定数据结构没有被异常篡改。如果一切可控,那么就自己动手、丰衣足食;否则就还是走io_getevents系统调用。而io_getevents系统调用通过aio_ring_info->ring_pages得到aio_ring结构,再将相应的io_event拷贝到用户空间。
下面贴一段libaio中的io_getevents的代码(前面提到过,linux版本的异步IO是由用户态的libaio来封装的):
int io_getevents_0_4(io_context_t ctx, long min_nr, long nr, struct io_event * events, struct timespec * timeout){
struct aio_ring *ring;
ring = (struct aio_ring*)ctx;
if (ring==NULL || ring->magic != AIO_RING_MAGIC)
goto do_syscall;
if (timeout!=NULL && timeout->tv_sec == 0 && timeout->tv_nsec == 0) {
if (ring->head == ring->tail)
return 0;
}
do_syscall:
return __io_getevents_0_4(ctx, min_nr, nr, events, timeout);
}
其中确实用到了用户空间上的aio_ring结构的信息,不过尺度还是不够大。
以上就是异步IO的context的结构。那么,为什么linux版本的异步IO需要“上下文”这么个概念,而glibc版本则不需要呢?
在glibc版本中,异步处理线程是glibc在调用者进程中动态创建的线程,它和调用者必定是在同一个虚拟地址空间中的。这里已经隐含了“同一上下文”这么个关系。
而对于内核来说,要面对的是任意的进程,任意的虚拟地址空间。当处理一个异步请求时,内核需要在调用者对应的地址空间中存取数据,必须知道这个虚拟地址空间是什么。不过当然,如果设计上要想把“上下文”这个概念隐藏了也是肯定可以的(比如让每个mm隐含一个异步IO上下文)。具体如何选择,只是设计上的问题。
struct iocb在内核中又对应到struct kiocb结构,主要包含以下字段:
struct kioctx* ki_ctx; /* 请求对应的kioctx(上下文结构) */
struct list_head ki_run_list; /* 需要aio线程处理的请求,通过该字段链入ki_ctx->run_list */
struct list_head ki_list; /* 链入ki_ctx->active_reqs */
struct file* ki_filp; /* 对应的文件指针 */
void __user* ki_obj.user; /* 指向用户态的iocb结构 */
__u64 ki_user_data; /* 等于iocb->aio_data */
loff_t ki_pos; /* 等于iocb->aio_offset */
unsigned short ki_opcode; /* 等于iocb->aio_lio_opcode */
size_t ki_nbytes; /* 等于iocb->aio_nbytes */
char __user * ki_buf; /* 等于iocb->aio_buf */
size_t ki_left; /* 该请求剩余字节数(初值等于iocb->aio_nbytes) */
struct eventfd_ctx* ki_eventfd; /* 由iocb->aio_resfd对应的eventfd对象 */
ssize_t (*ki_retry)(struct kiocb *); /*由ki_opcode选择的请求提交函数*/
调用io_submit后,对应于用户传递的每一个iocb结构,会在内核态生成一个与之对应的kiocb结构,并且在对应kioctx结构的ring_info中预留一个io_events的空间。之后,请求的处理结果就被写到这个io_event中。
然后,对应的异步读写(或其他)请求就被提交到了虚拟文件系统,实际上就是调用了file->f_op->aio_read或file->f_op->aio_write(或其他)。也就是,在经历磁盘高速缓存层、通用块层之后,请求被提交到IO调度层,等待被处理。这个跟普通的文件读写请求是类似的。
在《linux文件读写浅析》中可以看到,对于非direct-io的读请求来说,如果page cache不命中,那么IO请求会被提交到底层。之后,do_generic_file_read会通过lock_page操作,等待数据最终读完。这一点跟异步IO是背道而驰的,因为异步就意味着请求提交后不能等待,必须马上返回。而对于非direct-io的写请求,写操作一般仅仅是将数据更新作用到page cache上,并不需要真正的写磁盘。page cache写回磁盘本身是一个异步的过程。可见,对于非direct-io的文件读写,使用linux版本的异步IO接口完全没有意义(就跟使用同步接口效果一样)。
为什么会有这样的设计呢?因为非direct-io的文件读写是只跟page cache打交道的。而page cache是内存,跟内存打交道又不会存在阻塞,那么也就没有什么异步的概念了。至于读写磁盘时发生的阻塞,那是page cache跟磁盘打交道时发生的事情,跟应用程序又没有直接关系。
然而,对于direct-io来说,异步则是有意义的。因为direct-io是应用程序的buffer跟磁盘的直接交互(不使用page cache)。
这里,在使用direct-io的情况下,file->f_op->aio_{read,write}提交完IO请求就直接返回了,然后io_submit系统调用返回。(见后面的执行流程。)
通过linux内核异步触发的IO调度(如:被时钟中断触发、被其他的IO请求触发、等),已经提交的IO请求被调度,由对应的设备驱动程序提交给具体的设备。对于磁盘,一般来说,驱动程序会发起一次DMA。然后又经过若干时间,读写请求被磁盘处理完成,CPU将收到表示DMA完成的中断信号,设备驱动程序注册的处理函数将在中断上下文中被调用。这个处理函数会调用end_request函数来结束这次请求。这个流程跟《linux文件读写浅析》中所说的非direct-io读操作的情况是一样的。
不同的是,对于同步非direct-io,end_request将通过清除page结构的PG_locked标记来唤醒被阻塞的读操作流程,异步IO和同步IO效果一样。而对于direct-io,除了唤醒被阻塞的读操作流程(同步IO)或io_getevents流程(异步IO)之外,还需要将IO请求的处理结果填回对应的io_event中。
最后,等到调用者调用io_getevents的时候,就能获取到请求对应的结果(io_event)。而如果调用io_getevents的时候结果还没出来,流程也会被阻塞,并且会在direct-io的end_request过程中得到唤醒。
linux版本的异步IO也有aio线程(每CPU一个),但是跟glibc版本中的异步处理线程不同,这里的aio线程是用来处理请求重试的。某些情况下,file->f_op->aio_{read,write}可能会返回-EIOCBRETRY,表示需要重试(只有一些特殊的IO设备会这样)。而调用者既然使用的是异步IO接口,肯定不希望里面会有等待/重试的逻辑。所以,如果遇到-EIOCBRETRY,内核就在当前CPU对应的aio线程添加一个任务,让aio线程来完成请求的重新提交。而调用流程可以直接返回,不需要阻塞。
请求在aio线程中提交和在调用者进程中提交相比,有一个最大的不同,就是aio线程使用的地址空间可能跟调用者线程不一样。需要利用kioctx->mm切换到正确的地址空间,然后才能发请求。(参见《浅尝异步IO》中的讨论。)
内核处理流程
最后,整理一下direct-io异步读操作的处理流程:
io_submit。对于提交的iocbpp数组中的每一个iocb(异步请求),调用io_submit_one来提交它们;
io_submit_one。为请求分配一个kiocb结构,并且在对应的kioctx的ring_info中为它预留一个对应的io_event。然后调用aio_rw_vect_retry来提交这个读请求;
aio_rw_vect_retry。调用file->f_op->aio_read。这个函数通常是由generic_file_aio_read或者其封装来实现的;
generic_file_aio_read。对于非direct-io,会调用do_generic_file_read来处理请求(见《linux文件读写浅析》)。而对于direct-io,则是调用mapping->a_ops->direct_IO。这个函数通常就是blkdev_direct_IO;
blkdev_direct_IO。调用filemap_write_and_wait_range将相应位置可能存在的page cache废弃掉或刷回磁盘(避免产生不一致),然后调用direct_io_worker来处理请求;
direct_io_worker。一次读可能包含多个读操作(对应于类readv系统调用),对于其中的每一个,调用do_direct_IO;
do_direct_IO。调用submit_page_section;
submit_page_section。调用dio_new_bio分配对应的bio结构,然后调用dio_bio_submit来提交bio;
dio_bio_submit。调用submit_bio提交请求。后面的流程就跟非direct-io是一样的了,然后等到请求完成,驱动程序将调用 bio->bi_end_io来结束这次请求。对于direct-io下的异步IO,bio->bi_end_io等于dio_bio_end_aio;
dio_bio_end_aio。调用wake_up_process唤醒被阻塞的进程(异步IO下,主要是io_getevents的调用者)。然后调用aio_complete;
aio_complete。将处理结果写回到对应的io_event中;
比较
从上面的流程可以看出,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的工作就只能靠用户程序来实现了。
发表评论
-
xl2tp 备份
2019-09-24 16:25 7292019年9月24日更新: 注意,需要开启firewall ... -
sdl笔记
2019-01-31 17:19 740sdl教程教程 https://github.com/Twin ... -
tinyemu
2019-01-24 17:59 1439参考https://bellard.org/jslinux/t ... -
aws搭建xl2tp给iphone使用
2018-12-26 21:37 19012019年12月26日 可以参考原来的配置 https:// ... -
consul的基本使用
2017-06-27 11:13 1409### 安装 [centos7上consul的安装](ht ... -
lvs的helloworld
2017-06-13 20:36 600###################lvs######### ... -
系统调用的helloworld
2017-05-04 16:14 657《2.6内核标准教程》 p293 #include < ... -
bitcoin和cgminer的安装
2017-04-05 22:45 1962参考 http://blog.csdn.net/rion_ch ... -
ceph安装和常用命令
2017-03-21 21:55 961/etc/hosts ssh-keygen ssh-copy- ... -
mobile terminal 笔记
2016-12-02 15:35 646找出旧的iphone4 越狱之后可以变个小操作系统 mobi ... -
socket基础和select(python)
2016-06-14 17:21 1807上接 c语言的socket基础ht ... -
socket基础(c语言)
2016-06-14 16:45 1005不使用select 普通的基础socket连接,对多个客户端的 ... -
ffmpeg+nginx 的直播(2,直播摄像头和麦克风)
2016-05-28 20:21 4382假设我的服务器是centos7 192.168.139.117 ... -
ffmpeg+nginx 的直播(1,直播播放的视频文件)
2016-05-26 17:11 661564位操作系统centos7 ############ 1.一 ... -
socat和netcat(nc)
2016-04-29 22:36 1756转 原文链接: http://www.wenquan.name ... -
neutron基础九(qemu nat网络)
2016-02-06 17:21 1630接上基础八,kvm透传nested忽略 1.在主机ce ... -
neutron基础八(qemu 桥接网络)
2016-02-06 13:13 1549qemu的桥接和nat的qemu启动命令是一样的,但是后续的脚 ... -
neutron基础七(qemu tap)
2016-02-02 17:02 1033使用qemu 建立个虚拟机 然后用tap设备, 根据基础六,t ... -
neutron基础六(bridge fdb)
2016-01-28 18:30 2276转发表 在三台机器上建立三个namespace 192.16 ... -
南北流量
2016-01-23 23:26 1834一、三层网络架构: 接入层:负责服务器的接入和隔离 汇聚层:汇 ...
相关推荐
### Linux异步IO详解 #### 引言 在Linux环境下,输入/输出(I/O)操作是系统资源管理和数据交互的核心部分。传统上,Linux采用的最常见I/O模型是同步I/O,其中应用程序在发出请求后会阻塞,直至请求完成。然而,...
linux异步IO编程实例分析参照.pdf
C# 异步编程 简单例子
这里我们将深入探讨同步IO、异步IO、阻塞IO和非阻塞IO的概念,理解它们的工作原理以及在实际应用中的差异。 1. 同步IO与异步IO: - **同步IO**:在同步模式下,应用程序执行I/O操作时会等待操作完成。这意味着程序...
**VC++与VCsocket异步IO:** 在Windows平台上,Visual C++(简称VC++)提供了对异步I/O的良好支持,特别是通过使用完成端口(Completion Port, CP)。完成端口是一种I/O完成机制,它将I/O操作的结果与特定线程关联...
本项目是一个基于C语言在Linux环境下实现的简单异步IO库,旨在提供对socket、文件读取、定时器以及文件状态变更监控的支持。 首先,我们来看一下异步I/O的基本概念。传统的同步I/O模式在执行I/O操作时会阻塞,直到...
本资源"Python高级编程和异步IO并发编程"旨在深入探讨这些主题,帮助开发者提升技能,以实现更高效、更强大的程序设计。 首先,让我们从面向对象编程(OOP)开始。在Python中,OOP是一种强大的设计模式,它允许我们...
Linux下的异步I/O主要通过libaio库来实现,这正是“Linux中的异步IO包”标题所指的内容。 libaio,全称为“Linux Asynchronous I/O interface”,是一个开源的C语言库,提供了对Linux内核异步I/O接口的封装。libaio...
基于java的开发源码-异步IO框架 Cindy.zip 基于java的开发源码-异步IO框架 Cindy.zip 基于java的开发源码-异步IO框架 Cindy.zip 基于java的开发源码-异步IO框架 Cindy.zip 基于java的开发源码-异步IO框架 Cindy.zip ...
### Linux异步通信socket详解 #### 一、异步通信概览 异步通信是现代计算机网络中的一个重要概念,尤其在Linux环境下,异步通信机制提供了高效的数据传输方式,能够处理大量的并发连接,这对于构建高性能的网络...
"异步IO模型编程实例(纯C语言)" 异步IO模型是指在进行IO操作时,不会阻塞当前线程或进程,而是将IO操作交由操作系统或专门的IO处理线程处理,从而提高系统的并发性和响应速度。在Windows平台上,异步IO模型主要...
WinSock异步IO模型是Windows Socket编程中一种高效的数据传输方式。它允许应用程序在等待数据传输完成时执行其他任务,从而提高系统资源利用率和程序响应性。与传统的同步IO模型不同,异步IO不会阻塞调用线程,而是...
在这个场景中,"MFC IOCP模型异步IO"指的是使用MFC来实现基于IOCP的异步I/O操作。 IOCP的主要优点在于其高度的并发性和非阻塞特性。当一个I/O操作开始时,系统会将请求添加到完成端口,然后立即返回,让调用线程...
同步(synchronous) IO和异步(asynchronous) IO,阻塞(blocking) IO和非阻塞(non-blocking)IO分别是什么,到底有什么区别?这个问题其实不同的人给出的答案都可能不同,比如wiki,就认为asynchronous IO和non...
在标题所提及的“基于异步IO的socket通信程序”中,我们可以推测有以下三个主要部分: 1. **Socket通信的抽象**:这是对socket通信的基本封装,通常包含连接建立、数据传输和断开连接等基本操作。抽象类或接口可以...
本文将深入探讨Node.js异步IO的实现,并结合给定的资源进行分析。 首先,异步I/O是Node.js的核心特性之一,它允许程序在等待I/O操作完成时继续执行其他任务,从而提高了整体的执行效率和系统资源利用率。在传统的...
在IT领域,网络编程是构建分布式系统和互联网应用程序的基础,而WSAEventSelect是Windows Socket API(Winsock)提供的一种实现异步IO的重要机制。本文将深入探讨WSAEventSelect的功能、工作原理以及如何在PDA(个人...
异步 I/O(AIO)是 Linux 系统中一种高效的数据处理机制,它允许应用程序在发起 I/O 操作后不被阻塞,而是继续执行其他任务,直到 I/O 完成并得到通知。这对于 I/O 密集型的进程尤其有益,因为它能够最大化 CPU 的...
### 异步IO:Python中的并发编程革命 Python 自问世以来,就因其简洁优雅的语法、易学且功能强大等特点迅速赢得了程序员们的喜爱。随着计算机技术的发展与应用场景的不断拓展,传统的同步IO模型逐渐难以满足高并发...