libeio-异步I/O库初窥
在 Windows 平台上不可用。
Libeio是全功能的用于C语言的异步I/O库,建模风格和秉承的精神与libev类似。特性包括:异步的read、write、open、close、stat、unlink、fdatasync、mknod、readdir等(基本上是完整的POSIX API)。
Libeio完全基于事件库,可以容易地集成到事件库(或独立,甚至是以轮询方式)使用。Libeio非常轻便,且只依赖于POSIX线程。
Libeio当前的源码,文档,集成和轻便性都在libev之下,但应该很快可以用于生产环境了。
Libeio是用多线程实现的异步I/O库.主要步骤如下:
- 主线程接受请求,将请求放入请求队列,唤醒子线程处理。这里主线程不会阻塞,会继续接受请求
- 子线程处理请求,将请求回执放入回执队列,并调用用户自定义方法,通知主线程有请求已处理完毕
- 主线程处理回执。
源码中提供了一个demo.c用于演示,精简代码如下:
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <poll.h>
- #include <string.h>
- #include <assert.h>
- #include <fcntl.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include "eio.h"
- int respipe [2];
- /*
- * 功能:子线程通知主线程已有回执放入回执队列.
- */
- void
- want_poll (void)
- {
- char dummy;
- printf ("want_poll ()\n");
- write (respipe [1], &dummy, 1);
- }
- /*
- * 功能:主线程回执处理完毕,调用此函数
- */
- void
- done_poll (void)
- {
- char dummy;
- printf ("done_poll ()\n");
- read (respipe [0], &dummy, 1);
- }
- /*
- * 功能:等到管道可读,处理回执信息
- */
- void
- event_loop (void)
- {
- // an event loop. yeah.
- struct pollfd pfd;
- pfd.fd = respipe [0];
- pfd.events = POLLIN;
- printf ("\nentering event loop\n");
- while (eio_nreqs ())
- {
- poll (&pfd, 1, -1);
- printf ("eio_poll () = %d\n", eio_poll ());
- }
- printf ("leaving event loop\n");
- }
- /*
- * 功能:自定义函数,用户处理请求执行后的回执信息
- */
- int
- res_cb (eio_req *req)
- {
- printf ("res_cb(%d|%s) = %d\n", req->type, req->data ? req->data : "?", EIO_RESULT (req));
- if (req->result < 0)
- abort ();
- return 0;
- }
- int
- main (void)
- {
- printf ("pipe ()\n");
- if (pipe (respipe))
- abort ();
- printf ("eio_init ()\n");
- if (eio_init (want_poll, done_poll)) //初始化libeio库
- abort ();
- eio_mkdir ("eio-test-dir", 0777, 0, res_cb, "mkdir");
- event_loop ();
- return 0;
- }
可以将demo.c与libeio一起编译,也可以先将libeio编译为动态链接库,然后demo.c与动态链接库一起编译。
执行流程图如下所示:
流程图详细步骤说明如下:
1、通过pipe函数创建管道。
管道主要作用是子线程告知父线程已有请求回执放入回执队列,父线程可以进行相应的处理。
2. libeio执行初始化操作。
调用eio_init执行初始化。eio_init函数声明:int eio_init (void (*want_poll)(void), void (*done_poll)(void))。eio_init参数是两个函数指针,want_poll和done_poll是成对出现。want_poll主要是子线程通知父线程已有请求处理完毕,done_poll则是在所有请求处理完毕后调用。
eio_init代码如下:
- /*
- * 功能:libeio初始化
- */
- static int ecb_cold
- etp_init (void (*want_poll)(void), void (*done_poll)(void))
- {
- X_MUTEX_CREATE (wrklock);//子线程队列互斥量
- X_MUTEX_CREATE (reslock);//请求队列互斥量
- X_MUTEX_CREATE (reqlock);//回执队列互斥量
- X_COND_CREATE (reqwait);//创建条件变量
- reqq_init (&req_queue);//初始化请求队列
- reqq_init (&res_queue);//初始化回执队列
- wrk_first.next =
- wrk_first.prev = &wrk_first;//子线程队列
- started = 0;//运行线程数
- idle = 0;//空闲线程数
- nreqs = 0;//请求任务个数
- nready = 0;//待处理任务个数
- npending = 0;//未处理的回执个数
- want_poll_cb = want_poll;
- done_poll_cb = done_poll;
- return 0;
- }
3、父线程接受I/O请求
实例IO请求为创建一个文件夹。一般I/O请求都是阻塞请求,即父线程需要等到该I/O请求执行完毕,才能进行下一步动作。在libeio里面,主线程无需等待I/O操作执行完毕,它可以做其他事情,如继续接受I/O请求。
这里创建文件夹,调用的libeio中的方法eio_mkdir。libeio对常用的I/O操作,都有自己的封装函数。
- eio_req *eio_wd_open (const char *path, int pri, eio_cb cb, void *data); /* result=wd */
- eio_req *eio_wd_close (eio_wd wd, int pri, eio_cb cb, void *data);
- eio_req *eio_nop (int pri, eio_cb cb, void *data); /* does nothing except go through the whole process */
- eio_req *eio_busy (eio_tstamp delay, int pri, eio_cb cb, void *data); /* ties a thread for this long, simulating busyness */
- eio_req *eio_sync (int pri, eio_cb cb, void *data);
- eio_req *eio_fsync (int fd, int pri, eio_cb cb, void *data);
- eio_req *eio_fdatasync (int fd, int pri, eio_cb cb, void *data);
- eio_req *eio_syncfs (int fd, int pri, eio_cb cb, void *data);
- eio_req *eio_msync (void *addr, size_t length, int flags, int pri, eio_cb cb, void *data);
- eio_req *eio_mtouch (void *addr, size_t length, int flags, int pri, eio_cb cb, void *data);
- eio_req *eio_mlock (void *addr, size_t length, int pri, eio_cb cb, void *data);
- eio_req *eio_mlockall (int flags, int pri, eio_cb cb, void *data);
- eio_req *eio_sync_file_range (int fd, off_t offset, size_t nbytes, unsigned int flags, int pri, eio_cb cb, void *data);
- eio_req *eio_fallocate (int fd, int mode, off_t offset, size_t len, int pri, eio_cb cb, void *data);
- eio_req *eio_close (int fd, int pri, eio_cb cb, void *data);
- eio_req *eio_readahead (int fd, off_t offset, size_t length, int pri, eio_cb cb, void *data);
- eio_req *eio_seek (int fd, off_t offset, int whence, int pri, eio_cb cb, void *data);
- eio_req *eio_read (int fd, void *buf, size_t length, off_t offset, int pri, eio_cb cb, void *data);
- eio_req *eio_write (int fd, void *buf, size_t length, off_t offset, int pri, eio_cb cb, void *data);
从列举的函数中可以看出一些共同点,
- 返回值相同,都是结构体eio_req指针。
- 函数最后三个参数都一致。pri表示优先级;cb是用户自定义的函数指针,主线程在I/O完成后调用;data存放数据
这里需要指出的是,在这些操作里面,没有执行真正的I/O操作。下面通过eio_mkdir源码来说明这些函数到底做了什么?
- /*
- * 功能:将创建文件夹请求放入请求队列
- */
- eio_req *eio_mkdir (const char *path, mode_t mode, int pri, eio_cb cb, void *data)
- {
- REQ (EIO_MKDIR);
- PATH;
- req->int2 = (long)mode;
- SEND;
- }
不得不吐槽一下,libeio里面太多宏定义了,代码风格有点不好。这里REQ,PATH,SEND都是宏定义。为了便于阅读,把宏给去掉
- /*
- * 功能:将创建文件夹请求放入请求队列
- */
- eio_req *eio_mkdir (const char *path, mode_t mode, int pri, eio_cb cb, void *data)
- {
- eio_req *req;
- req = (eio_req *)calloc (1, sizeof *req);
- if (!req)
- return 0;
- req->type = EIO_MKDIR;// 请求类型
- req->pri = pri;//请求优先级
- req->finish = cb;//请求处理完成后调用的函数
- req->data = data;//用户数据
- req->destroy = eio_api_destroy;//释放req资源
- req->flags |= EIO_FLAG_PTR1_FREE;//标记需要释放ptr1
- req->ptr1 = strdup (path);
- if (!req->ptr1)
- {
- eio_api_destroy (req);
- return 0;
- }
- req->int2 = (long)mode;
- eio_submit (req); //将请求放入请求队列,并唤醒子线程
- return req;
- }
4、请求放入请求队列
请求队列由结构体指针数组qs,qe构成,数组大小为9,数组的序号标志了优先级,即qs[1]存放的是优先级为1的所有请求中的第一个,qe[1]存放的是优先级为1的所有请求的最后一个。这样做的好处是,在时间复杂度为O(1)的情况下插入新的请求。
- /*
- * 功能:将请求放入请求队列,或者将回执放入回执队列。 qe存放链表终点.qs存放链表起点.
- */
- static int ecb_noinline
- reqq_push (etp_reqq *q, ETP_REQ *req)
- {
- int pri = req->pri;
- req->next = 0;
- if (q->qe[pri])//如果该优先级以后请求,则插入到最后
- {
- q->qe[pri]->next = req;
- q->qe[pri] = req;
- }
- else
- q->qe[pri] = q->qs[pri] = req;
- return q->size++;
- }
5、唤醒子线程
- 创建的线程总数大于4(这个数字要想改变,只有重新编译libeio了)
- 线程数大于未处理的请求。
- /*
- * 功能:创建线程,并把线程放入线程队列
- */
- static void ecb_cold
- etp_start_thread (void)
- {
- etp_worker *wrk = calloc (1, sizeof (etp_worker));
- /*TODO*/
- assert (("unable to allocate worker thread data", wrk));
- X_LOCK (wrklock);
- //创建线程,并将线程插入到线程队列.
- if (thread_create (&wrk->tid, etp_proc, (void *)wrk))
- {
- wrk->prev = &wrk_first;
- wrk->next = wrk_first.next;
- wrk_first.next->prev = wrk;
- wrk_first.next = wrk;
- ++started;
- }
- else
- free (wrk);
- X_UNLOCK (wrklock);
- }
6、子线程从请求队列中取下请求
7、子线程处理请求
- /*
- * 功能:根据类型,执行不同的io操作
- */
- static void
- eio_execute (etp_worker *self, eio_req *req)
- {
- #if HAVE_AT
- int dirfd;
- #else
- const char *path;
- #endif
- if (ecb_expect_false (EIO_CANCELLED (req)))//判断该请求是否取消
- {
- req->result = -1;
- req->errorno = ECANCELED;
- return;
- }
- switch (req->type)
- {
- case EIO_MKDIR: req->result = mkdirat (dirfd, req->ptr1, (mode_t)req->int2); break;
- }
- }
8、写回执
9、通知父线程有回执
- /*
- * 功能:子线程通知主线程已有回执放入回执队列.
- */
- void
- want_poll (void)
- {
- char dummy;
- printf ("want_poll ()\n");
- write (respipe [1], &dummy, 1);
- }
10、父线程处理回执
调用eio_poll函数处理回执。或许看到这里你在想,eio_poll是个系统函数,我们没办法修改,但是我们如何知道每一个I/O请求执行结果。 其实还是用的函数指针,在我们构建一个I/O请求结构体时,有一个finsh函数指针。当父进程处理I/O回执时,会调用该方法。这里自定义的 finish函数名为res_cb,当创建文件夹成功后,调用该函数,输出一句话- /*
- * 功能:处理回执
- */
- static int
- etp_poll (void)
- {
- unsigned int maxreqs;
- unsigned int maxtime;
- struct timeval tv_start, tv_now;
- X_LOCK (reslock);
- maxreqs = max_poll_reqs;
- maxtime = max_poll_time;
- X_UNLOCK (reslock);
- if (maxtime)
- gettimeofday (&tv_start, 0);
- for (;;)
- {
- ETP_REQ *req;
- etp_maybe_start_thread ();
- X_LOCK (reslock);
- req = reqq_shift (&res_queue);//从回执队列取出优先级最高的回执信息
- if (req)
- {
- --npending;
- if (!res_queue.size && done_poll_cb)//直到回执全部处理完,执行done_poll();
- {
- //printf("执行done_poll()\n");
- done_poll_cb ();
- }
- }
- X_UNLOCK (reslock);
- if (!req)
- return 0;
- X_LOCK (reqlock);
- --nreqs;//发出请求,到收到回执,该请求才算处理完毕.
- X_UNLOCK (reqlock);
- if (ecb_expect_false (req->type == EIO_GROUP && req->size))//ecb_expect_false仅仅用于帮助编译器产生更优代码,而对真值无任何影响
- {
- req->int1 = 1; /* mark request as delayed */
- continue;
- }
- else
- {
- int res = ETP_FINISH (req);//调用自定义函数,做进一步处理
- if (ecb_expect_false (res))
- return res;
- }
- if (ecb_expect_false (maxreqs && !--maxreqs))
- break;
- if (maxtime)
- {
- gettimeofday (&tv_now, 0);
- if (tvdiff (&tv_start, &tv_now) >= maxtime)
- break;
- }
- }
- errno = EAGAIN;
- return -1;
- }
11、当所有请求执行完毕,调用done_poll做收尾工作。
- /*
- * 功能:主线程回执处理完毕,调用此函数
- */
- void
- done_poll (void)
- {
- char dummy;
- printf ("done_poll ()\n");
- read (respipe [0], &dummy, 1);
- }
至此,libeio就简单的跑了一遍,从示例代码可以看出,libeio使用简单。虽说现在是beat版,不过Node.js已经在使用了。
- #define ecb_expect(expr,value) __builtin_expect ((expr),(value))
- #define ecb_expect_false(expr) ecb_expect (!!(expr), 0)
- #define ecb_expect_true(expr) ecb_expect (!!(expr), 1)
- /* for compatibility to the rest of the world */
- #define ecb_likely(expr) ecb_expect_true (expr)
- #define ecb_unlikely(expr) ecb_expect_false (expr)