linux AIO (异步IO) 那点事儿

标签: 未分类 | 发表时间:2011-09-07 10:00 | 作者:爱多 zffl
出处:http://cnodejs.org/blog

        在高性能的服务器编程中,IO 模型理所当然的是重中之重,需要谨慎选型的,对于网络套接字,我们可以采用epoll 的方式来轮询,尽管epoll也有一些缺陷,但总体来说还是很高效的,尤其来大量套接字的场景下;但对于Regular File 来说,是不能够用采用 poll/epoll 的,即O_NOBLOCK 方式对于传统文件句柄是无效的,也就是说我们的 open ,read, mkdir 之类的Regular File操作必定会导致阻塞.在多线程、多进程模型中,可以选择以同步阻塞的方式来进行IO操作,任务调度由操作系统来保证公平性,但在单进程/线程模型中,以nodejs 为例 ,假如 我们需要在一个用户请求中处理10个文件:

function fun() {
 fs.readFileSync();
 fs.readFileSync();
 …
 }

这时候进程至少会阻塞10次,而这可能会导致其他的上千个用户请求得不到处理,这当然是不能接受的.

Linux AIO 早就被提上议程,目前比较知名的有 Glibc 的 AIO   与 Kernel Native AIO
Glibc AIO: http://www.ibm.com/developerworks/linux/library/l-async/
Kernel Native AIO: http://lse.sourceforge.net/io/aio.html

我们用Glibc 的AIO 做个小实验,写一个简单的程序:异步方式读取一个文件,并注册异步回调函数:

int  main()
{
struct aiocb my_aiocb;
fd = open("file.txt", O_RDONLY);
...
my_aiocb.aio_sigevent.sigev_notify_function = aio_completion_handler;
…
ret = aio_read(&my_aiocb);
…
write(1, "caller thread\n", 14);
sleep(5);
}

void aio_completion_handler(sigval_t sigval)
{
write(1, "callback\n", 9);
struct aiocb *req;
...
req = (struct aiocb *)sigval.sival_ptr;
printf("data: %s\n" ,req->aio_buf);
return;
}

我们用 strace 来跟踪调用,得到以下结果 (只保留主要语句):

23908 open(“file.txt”, O_RDONLY)        = 3
23908 clone(…) = 23909
23908 write(1, “caller thread\n”, 14)   = 14
23908 nanosleep({5, 0},  <unfinished …>

23909 pread(3, “hello, world\n”, 1024, 0) = 13
23909 clone(..)= 23910
23909 futex(0x3d3a4082a4, FUTEX_WAIT_PRIVATE, 1, {0, 999942000} <unfinished …>

23910 write(1, “callback\n”, 9)         = 9
23910 write(1, “data: hello, world\n”, 19) = 19
23910 write(1, “\n”, 1)                 = 1
23910 _exit(0)                          = ?
23909 <… futex resumed> )             = -1 ETIMEDOUT (Connection timed out)
23909 futex(0x3d3a408200, FUTEX_WAKE_PRIVATE, 1) = 0
23909 _exit(0)                          = ?
23908 <… nanosleep resumed> {5, 0})   = 0
23908 exit_group(0)                     = ?

在Glibc AIO 的实现中, 用多线程同步来模拟 异步IO ,以上述代码为例,它牵涉了3个线程,
主线程(23908)新建 一个线程(23909)来调用 阻塞的pread函数,当pread返回时,又创建了一个线程(23910)来执行我们预设的异步回调函数, 23909 等待23910结束返回,然后23909也结束执行..

实际上,为了避免线程的频繁创建、销毁,当有多个请求时,Glibc AIO 会使用线程池,但以上原理是不会变的,尤其要注意的是:我们的回调函数是在一个单独线程中执行的.
Glibc AIO 广受非议,存在一些难以忍受的缺陷和bug,饱受诟病,是极不推荐使用的.
详见:http://davmac.org/davpage/linux/async-io.html

在Linux 2.6.22+ 系统上,还有一种 Kernel AIO 的实现,与Glibc 的多线程模拟不同 ,它是真正的做到内核的异步通知,比如在较新版本的Nginx 服务器上,已经添加了AIO方式 的支持.

http://wiki.nginx.org/HttpCoreModule
aio
syntax: aio [on|off|sendfile]
default: off
context: http, server, location
This directive is usable as of Linux kernel 2.6.22. For Linux it is required to use directio, this automatically disables sendfile support.

location /video {
aio on;
directio 512;
output_buffers 1 128k;
}

听起来Kernel Native AIO 几乎提供了近乎完美的异步方式,但如果你对它抱有太高期望的话,你会再一次感到失望.

目前的Kernel AIO 仅支持 O_DIRECT 方式来对磁盘读写,这意味着,你无法利用系统的缓存,同时它要求读写的的大小和偏移要以区块的方式对齐,参考nginx 的作者 Igor Sysoev 的评论: http://forum.nginx.org/read.php?2,113524,113587#msg-113587

nginx supports file AIO only in 0.8.11+, but the file AIO is functional
on FreeBSD only. On Linux AIO is supported by nginx only on kerenl
2.6.22+ (although, CentOS 5.5 has backported the required AIO features).
Anyway, on Linux AIO works only if file offset and size are aligned
to a disk block size (usually 512 bytes) and this data can not be cached
in OS VM cache (Linux AIO requires DIRECTIO that bypass OS VM cache).
I believe a cause of so strange AIO implementaion is that AIO in Linux
was developed mainly for databases by Oracle and IBM.

同时注意上面的橙色字部分,启用AIO 就会关闭sendfile -这是显而易见的,当你用Nginx作为静态服务器,你要么选择以AIO 读取文件到用户缓冲区,然后发送到套接口,要么直接调用sendfile发送到套接口,sendfile 虽然会导致短暂的阻塞,但开启AIO 却无法充分的利用缓存,也丧失了零拷贝的特征 ;当你用Nginx作为动态服务器,比如 fastcgi + php 时,这时php脚本中文件的读写是由php 的 文件接口来操作的,这时候是多进程+同步阻塞模型,和文件异步模式扯不上关系的.

所以现在Linux 上,没有比较完美的异步文件IO 方案,这时候苦逼程序员的价值就充分体现出来了,libev 的作者 Marc Alexander Lehmann 老大就重新实现了一个AIO library :

http://software.schmorp.de/pkg/libeio.html

其实它还是采用线程池+同步模拟出来的,和Glibc 的 AIO 比较像,用作者的话说,这个库相比与Glibc 的实现,开销更小,bug更少(不然重新造个轮子还有毛意义呢?反正我是信了) ,不过这个轮子的代码可读性实在不敢恭维,Marc 老大自己也说了:Currently in BETA! Its code, documentation, integration and portability quality is currently below that of libev, but should soon be ready for use in production environments.

(其实libev代码和文档可读性也不咋地,貌似驱动内核搞多了都这样?)好吧,腹诽完了,我们还是阅读下它的源码 ,来稍微分析一下它的原理:

(这个文章的流程图还是蛮靠谱的:http://cnodejs.org/blog/?p=244  ,此处更详细的补充一下下)

int eio_init (void (*want_poll)(void), void (*done_poll)(void))

初始化时设定两个回调函数,它有两个全局的数据结构 : req 存放请求队列,res 存放已经完成的队列 当我,当你提交一个异步请求时(eio_submit),其实是放入req队列中,然后向线程池中处于信号等待的线程发送信号量(如果线程池中没有线程就创建一个),获得信号的线程会执行如下代码:

ETP_EXECUTE (self, req);
X_LOCK (reslock);
++npending;
if (!reqq_push (&res_queue, req) && want_poll_cb)
want_poll_cb ();
X_UNLOCK (reslock);

ETP_EXECUTE 就是实际的阻塞调用,比如read,open,,sendfile之类的,当函数返回时,表明操作完成,此时加锁方式向完成队列添加一项 ,然后调用 want_pool ,这个函数是我们eio_init时候设置的,然后释放锁。

注意:每次完成任务时,都要调用want_poll ,所以这个函数应该是线程安全且尽量短促,实际上我们为了避免陷入多线程的泥淖,我们往往配合eio使用事件轮询机制,比如:我们创建一对管道,我们把“读”端的管道加入 epoll 监控结构中,want_poll 函向“写”端管道写数入一个字节或字长 ,所以当下次epoll_wait 返回时,我们会执行 “读” 端管道的 回调函数,类似如下:

void r_pipe_cb(){
...
eio_poll();
}

在eio_poll 中 有类似以下代码:

for(;;){
X_LOCK (reslock);
req = reqq_shift (&res_queue);
if (req){
if (!res_queue.size && done_poll_cb)
done_poll_cb ();
}
X_UNLOCK (reslock);
res = ETP_FINISH (req);
...
if(empty) break;

}

eio_poll 函数就是从完成队列res 依次shift ,依次执行我们的回调函数(ETP_FINISH 就是执行用户回调),在取出完成队列的最后一项但还没有执行用户回调之前,调用我们设定的done_poll ,对res队列的操作当然也是加锁的,注意此时我们自定义的异步回调函数是在我们的主线程中执行的!这才是我们的最终目的!

在eio 线程池中,默认最多4个线程,在高性能的程序中,过多的进程/线程往往也是一个瓶颈,
寄存器的进出栈还是其次,进程虚存地址切换、各级cache 的miss ,这才是最昂贵的,所以,最理想的情形就是:有几个cpu ,就有同样数目的active  线程/进程,但因为io线程往往会陷入sleep模式,所以,还是需要额外的待切换的线程的,作为经验法则,线程池的数量最好是cpu 的数目 X 2(参见windows 核心编程 IOCP卷).

libeio 虽不完美,但目前还是将就着用用吧 …

关于作者
爱多, 游民
码奴,求收留,求包养,求打赏,求…
您可能也喜欢:

Node App Engine开始内测

libev 设计分析

NodeParty – CNodeJS北京聚会,邀请你参加

NodeParty-上海分享会第二期(9.17),诚邀您的参与(讲师继续征集中)

node协作绘图程序在线测试
无觅

相关 [linux aio 异步] 推荐:

linux AIO (异步IO) 那点事儿

- zffl - CNode社区
这时候进程至少会阻塞10次,而这可能会导致其他的上千个用户请求得不到处理,这当然是不能接受的.. Linux AIO 早就被提上议程,目前比较知名的有 Glibc 的 AIO   与 Kernel Native AIO. 我们用Glibc 的AIO 做个小实验,写一个简单的程序:异步方式读取一个文件,并注册异步回调函数:.

Java BIO、NIO、AIO

- - zzm
先来个例子理解一下概念,以银行取款为例:. 同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写). 异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API).

linux异步IO浅析

- Sepher - kouu&#39;s home
知道异步IO已经很久了,但是直到最近,才真正用它来解决一下实际问题(在一个CPU密集型的应用中,有一些需要处理的数据可能放在磁盘上. 预先知道这些数据的位置,所以预先发起异步IO读请求. 等到真正需要用到这些数据的时候,再等待异步IO完成. 使用了异步IO,在发起IO请求到实际使用数据这段时间内,程序还可以继续做其他事情).

ASUS 發佈全新 E 系列 AIO 一體化電腦

- SotongDJ - Engadget 中文版
這一段時間對 ASUS 來說比較忙,而對於 AIO 一體機電腦市場,華碩也不忘記來搶一份蛋糕吃. 這次一下子推出 3 款個人系列一體機,組成三劍客征戰市場. 其中,23.6 吋的 ET2410 馬上就可以買到,價格從 US$899 起跳,約 HK$7,000 / NT$27,300. 你可以選擇第二代 Intel Core 處理器,最高達 1TB 的硬盤容量.

Javascript 里跑Linux

- rockmaple - Shellex&#39;s Blog
牛逼到暴的大拿 Fabrice Bellard,用Javascript实现了一个x86 PC 模拟器,然后成功在这个模拟器里面跑Linux(请用Firefox 4 / Google Chrome 11打开,Chome 12有BUG). 关于这个东西… 伊说 “I did it for fun“,大大啊大大啊….

Linux Ksplice,MySQL and Oracle

- Syn - DBA Notes
Oracle 在 7 月份收购了 Ksplice. 使用了 Ksplice 的 Linux 系统,为 Kernel 打补丁无需重启动,做系统维护的朋友应该明白这是一个杀手级特性. 现在该产品已经合并到 Oracle Linux 中. 目前已经有超过 700 家客户,超过 10 万套系统使用了 Ksplice (不知道国内是否已经有用户了.

linux makefile编写

- hl - C++博客-首页原创精华区
在讲述这个Makefile之前,还是让我们先来粗略地看一看Makefile的规则. target也就是一个目标文件,可以是Object File,也可以是执行文件. prerequisites就是,要生成那个target所需要的文件或是目标. command也就是make需要执行的命令. 这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在 command中.

Linux下的VDSO

- 圣斌 - Adam&#39;s
VDSO(Virtual Dynamically-linked Shared Object)是个很有意思的东西, 它将内核态的调用映射到用户态的地址空间中, 使得调用开销更小, 路径更好.. 开销更小比较容易理解, 那么路径更好指的是什么呢. 拿x86下的系统调用举例, 传统的int 0×80有点慢, Intel和AMD分别实现了sysenter, sysexit和syscall, sysret, 即所谓的快速系统调用指令, 使用它们更快, 但是也带来了兼容性的问题.

Linux wget命令

- - CSDN博客推荐文章
wget是linux最常用的下载命令, 一般的使用方法是: wget + 空格 + 要下载文件的url路径. 例如: # wget  http://www.linuxsense.org/xxxx/xxx.tar.gz. 简单说一下-c参数, 这个也非常常见, 可以断点续传, 如果不小心终止了, 可以继续使用命令接着下载.

linux 小技巧

- - DBA Blog
2:如何限制用户的最小密码长度. 修改/etc/login.defs里面的PASS_MIN_LEN的值. 比如限制用户最小密码长度是8:. 3:如何使新用户首次登陆后强制修改密码. 4:更改Linux启动时用图形界面还是字符界面. 将id:5:initdefault: 其中5表示默认图形界面. 改id:3: initdefault: 3表示字符界面.