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库.主要步骤如下:

  1.  主线程接受请求,将请求放入请求队列,唤醒子线程处理。这里主线程不会阻塞,会继续接受请求
  2. 子线程处理请求,将请求回执放入回执队列,并调用用户自定义方法,通知主线程有请求已处理完毕
  3. 主线程处理回执。

     源码中提供了一个demo.c用于演示,精简代码如下:

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <unistd.h>  
  4. #include <poll.h>  
  5. #include <string.h>  
  6. #include <assert.h>  
  7. #include <fcntl.h>  
  8. #include <sys/types.h>  
  9. #include <sys/stat.h>  
  10.   
  11. #include "eio.h"  
  12.   
  13. int respipe [2];  
  14.   
  15. /* 
  16.  * 功能:子线程通知主线程已有回执放入回执队列. 
  17.  */  
  18. void  
  19. want_poll (void)  
  20. {  
  21.   char dummy;  
  22.   printf ("want_poll ()\n");  
  23.   write (respipe [1], &dummy, 1);  
  24. }  
  25.   
  26. /* 
  27.  * 功能:主线程回执处理完毕,调用此函数 
  28.  */  
  29. void  
  30. done_poll (void)  
  31. {  
  32.   char dummy;  
  33.   printf ("done_poll ()\n");  
  34.   read (respipe [0], &dummy, 1);  
  35. }  
  36. /* 
  37.  * 功能:等到管道可读,处理回执信息 
  38.  */  
  39. void  
  40. event_loop (void)  
  41. {  
  42.   // an event loop. yeah.  
  43.   struct pollfd pfd;  
  44.   pfd.fd     = respipe [0];  
  45.   pfd.events = POLLIN;  
  46.   
  47.   printf ("\nentering event loop\n");  
  48.   while (eio_nreqs ())  
  49.     {  
  50.       poll (&pfd, 1, -1);  
  51.       printf ("eio_poll () = %d\n", eio_poll ());  
  52.     }  
  53.   printf ("leaving event loop\n");  
  54. }  
  55.   
  56. /* 
  57.  * 功能:自定义函数,用户处理请求执行后的回执信息 
  58.  */  
  59. int  
  60. res_cb (eio_req *req)  
  61. {  
  62.   printf ("res_cb(%d|%s) = %d\n", req->type, req->data ? req->data : "?", EIO_RESULT (req));  
  63.   
  64.   if (req->result < 0)  
  65.     abort ();  
  66.   
  67.   return 0;  
  68. }  
  69.   
  70. int  
  71. main (void)  
  72. {  
  73.   printf ("pipe ()\n");  
  74.   if (pipe (respipe))  
  75.       abort ();  
  76.   printf ("eio_init ()\n");  
  77.   if (eio_init (want_poll, done_poll)) //初始化libeio库  
  78.       abort ();  
  79.   eio_mkdir ("eio-test-dir", 0777, 0, res_cb, "mkdir");      
  80.   event_loop ();  
  81.   return 0;  
  82. }  

   可以将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代码如下: 

  1. /* 
  2.  * 功能:libeio初始化 
  3.  */  
  4. static int ecb_cold  
  5. etp_init (void (*want_poll)(void), void (*done_poll)(void))  
  6. {  
  7.   X_MUTEX_CREATE (wrklock);//子线程队列互斥量  
  8.   X_MUTEX_CREATE (reslock);//请求队列互斥量  
  9.   X_MUTEX_CREATE (reqlock);//回执队列互斥量  
  10.   X_COND_CREATE  (reqwait);//创建条件变量  
  11.   
  12.   reqq_init (&req_queue);//初始化请求队列  
  13.   reqq_init (&res_queue);//初始化回执队列  
  14.   
  15.   wrk_first.next =  
  16.   wrk_first.prev = &wrk_first;//子线程队列  
  17.   
  18.   started  = 0;//运行线程数  
  19.   idle     = 0;//空闲线程数  
  20.   nreqs    = 0;//请求任务个数  
  21.   nready   = 0;//待处理任务个数  
  22.   npending = 0;//未处理的回执个数  
  23.   
  24.   want_poll_cb = want_poll;  
  25.   done_poll_cb = done_poll;  
  26.   
  27.   return 0;  
  28. }  

3、父线程接受I/O请求

    实例IO请求为创建一个文件夹。一般I/O请求都是阻塞请求,即父线程需要等到该I/O请求执行完毕,才能进行下一步动作。在libeio里面,主线程无需等待I/O操作执行完毕,它可以做其他事情,如继续接受I/O请求。

    这里创建文件夹,调用的libeio中的方法eio_mkdir。libeio对常用的I/O操作,都有自己的封装函数。

    

  1. eio_req *eio_wd_open   (const char *path, int pri, eio_cb cb, void *data); /* result=wd */  
  2. eio_req *eio_wd_close  (eio_wd wd, int pri, eio_cb cb, void *data);  
  3. eio_req *eio_nop       (int pri, eio_cb cb, void *data); /* does nothing except go through the whole process */  
  4. eio_req *eio_busy      (eio_tstamp delay, int pri, eio_cb cb, void *data); /* ties a thread for this long, simulating busyness */  
  5. eio_req *eio_sync      (int pri, eio_cb cb, void *data);  
  6. eio_req *eio_fsync     (int fd, int pri, eio_cb cb, void *data);  
  7. eio_req *eio_fdatasync (int fd, int pri, eio_cb cb, void *data);  
  8. eio_req *eio_syncfs    (int fd, int pri, eio_cb cb, void *data);  
  9. eio_req *eio_msync     (void *addr, size_t length, int flags, int pri, eio_cb cb, void *data);  
  10. eio_req *eio_mtouch    (void *addr, size_t length, int flags, int pri, eio_cb cb, void *data);  
  11. eio_req *eio_mlock     (void *addr, size_t length, int pri, eio_cb cb, void *data);  
  12. eio_req *eio_mlockall  (int flags, int pri, eio_cb cb, void *data);  
  13. eio_req *eio_sync_file_range (int fd, off_t offset, size_t nbytes, unsigned int flags, int pri, eio_cb cb, void *data);  
  14. eio_req *eio_fallocate (int fd, int mode, off_t offset, size_t len, int pri, eio_cb cb, void *data);  
  15. eio_req *eio_close     (int fd, int pri, eio_cb cb, void *data);  
  16. eio_req *eio_readahead (int fd, off_t offset, size_t length, int pri, eio_cb cb, void *data);  
  17. eio_req *eio_seek      (int fd, off_t offset, int whence, int pri, eio_cb cb, void *data);  
  18. eio_req *eio_read      (int fd, void *buf, size_t length, off_t offset, int pri, eio_cb cb, void *data);  
  19. 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源码来说明这些函数到底做了什么?

  

  1. /* 
  2.  * 功能:将创建文件夹请求放入请求队列 
  3.  */  
  4. eio_req *eio_mkdir (const char *path, mode_t mode, int pri, eio_cb cb, void *data)  
  5. {  
  6.   REQ (EIO_MKDIR);   
  7.   PATH;  
  8.   req->int2 = (long)mode;   
  9.   SEND;  
  10. }  

不得不吐槽一下,libeio里面太多宏定义了,代码风格有点不好。这里REQ,PATH,SEND都是宏定义。为了便于阅读,把宏给去掉

  1. /* 
  2.  * 功能:将创建文件夹请求放入请求队列 
  3.  */  
  4. eio_req *eio_mkdir (const char *path, mode_t mode, int pri, eio_cb cb, void *data)  
  5. {  
  6.   eio_req *req;                                                                                                                
  7.   req = (eio_req *)calloc (1, sizeof *req);                       
  8.   if (!req)                                                       
  9.     return 0;                                                                                                                   
  10.   req->type    = EIO_MKDIR;// 请求类型                       
  11.   req->pri     = pri;//请求优先级       
  12.   req->finish  = cb;//请求处理完成后调用的函数         
  13.   req->data    = data;//用户数据       
  14.   req->destroy = eio_api_destroy;//释放req资源  
  15.   req->flags |= EIO_FLAG_PTR1_FREE;//标记需要释放ptr1            
  16.   req->ptr1 = strdup (path);                   
  17.   if (!req->ptr1)                          
  18.   {                               
  19.       eio_api_destroy (req);                      
  20.       return 0;                           
  21.   }  
  22.   req->int2 = (long)mode;   
  23.   eio_submit (req); //将请求放入请求队列,并唤醒子线程  
  24.   return req;  
  25. }  

4、请求放入请求队列

   请求队列由结构体指针数组qs,qe构成,数组大小为9,数组的序号标志了优先级,即qs[1]存放的是优先级为1的所有请求中的第一个,qe[1]存放的是优先级为1的所有请求的最后一个。这样做的好处是,在时间复杂度为O(1)的情况下插入新的请求。

 

  1. /* 
  2.  * 功能:将请求放入请求队列,或者将回执放入回执队列。 qe存放链表终点.qs存放链表起点. 
  3.  */  
  4. static int ecb_noinline  
  5. reqq_push (etp_reqq *q, ETP_REQ *req)  
  6. {  
  7.   int pri = req->pri;  
  8.   req->next = 0;  
  9.   
  10.   if (q->qe[pri])//如果该优先级以后请求,则插入到最后  
  11.     {  
  12.       q->qe[pri]->next = req;  
  13.       q->qe[pri] = req;  
  14.     }  
  15.   else  
  16.     q->qe[pri] = q->qs[pri] = req;  
  17.   
  18.   return q->size++;  
  19. }  

 5、唤醒子线程

     这里并不是来一个请求,就为该请求创建一个线程。在下面两种情况下,不创建线程。
  •   创建的线程总数大于4(这个数字要想改变,只有重新编译libeio了)
  •  线程数大于未处理的请求。
    线程创建之后,放入线程队列。
  1. /* 
  2.  * 功能:创建线程,并把线程放入线程队列 
  3.  */  
  4. static void ecb_cold  
  5. etp_start_thread (void)  
  6. {  
  7.   etp_worker *wrk = calloc (1, sizeof (etp_worker));  
  8.   
  9.   /*TODO*/  
  10.   assert (("unable to allocate worker thread data", wrk));  
  11.   
  12.   X_LOCK (wrklock);  
  13.   
  14.   //创建线程,并将线程插入到线程队列.  
  15.   if (thread_create (&wrk->tid, etp_proc, (void *)wrk))  
  16.     {  
  17.       wrk->prev = &wrk_first;  
  18.       wrk->next = wrk_first.next;  
  19.       wrk_first.next->prev = wrk;  
  20.       wrk_first.next = wrk;  
  21.       ++started;  
  22.     }  
  23.   else  
  24.     free (wrk);  
  25.   
  26.   X_UNLOCK (wrklock);  
  27. }  

 6、子线程从请求队列中取下请求

      取请求时按照优先级来取的。

7、子线程处理请求

     子线程调用eio_excute处理请求。这里才真正的执行I/O操作。之前我们传过来的是创建文件夹操作,子线程判断请求类型,根据类型,调用系统函数执行操作,并把执行结果,写回到请求的result字段,如果执行有误,设置errno
     因为eio_excute函数比较长,这里只贴出创建文件夹代码。
     
  1. /* 
  2.  * 功能:根据类型,执行不同的io操作 
  3.  */  
  4. static void  
  5. eio_execute (etp_worker *self, eio_req *req)  
  6. {  
  7. #if HAVE_AT  
  8.   int dirfd;  
  9. #else  
  10.   const char *path;  
  11. #endif  
  12.   
  13.   if (ecb_expect_false (EIO_CANCELLED (req)))//判断该请求是否取消  
  14.     {  
  15.       req->result  = -1;  
  16.       req->errorno = ECANCELED;  
  17.       return;  
  18.     }  
  19.    switch (req->type)  
  20.    {  
  21.        case EIO_MKDIR:     req->result = mkdirat   (dirfd, req->ptr1, (mode_t)req->int2); break;  
  22.    }  
  23. }  
   从代码中可以看出,用户是可以取消之前的I/O操作,如果I/O操作未执行,可以取消。如果I/O操作已经在运行了,则取消无效。

8、写回执

    回执其实就是之前传给子线程的自定义结构体。当子线程取下该请求,并根据类型执行后,执行结构写入请求的result字段,并将该请求插入到回执队列res_queue中。

9、通知父线程有回执

    用户自己定义want_poll函数,用于子线程通知父线程有请求回执放入回执队列。示例代码是用的写管道。这里需要指出的时,当将请求回执放入空的回执 队列才会通知父线程,如果在放入时,回执队列已不为空,则不会通知父线程。为什么了?因为父线程处理回执的时候,会处理现有的所有回执。
   
  1. /* 
  2.  * 功能:子线程通知主线程已有回执放入回执队列. 
  3.  */  
  4. void  
  5. want_poll (void)  
  6. {  
  7.   char dummy;  
  8.   printf ("want_poll ()\n");  
  9.   write (respipe [1], &dummy, 1);  
  10. }  

10、父线程处理回执

     调用eio_poll函数处理回执。或许看到这里你在想,eio_poll是个系统函数,我们没办法修改,但是我们如何知道每一个I/O请求执行结果。 其实还是用的函数指针,在我们构建一个I/O请求结构体时,有一个finsh函数指针。当父进程处理I/O回执时,会调用该方法。这里自定义的 finish函数名为res_cb,当创建文件夹成功后,调用该函数,输出一句话
  1. /* 
  2.  * 功能:处理回执 
  3.  */  
  4. static int  
  5. etp_poll (void)  
  6. {  
  7.   unsigned int maxreqs;  
  8.   unsigned int maxtime;  
  9.   struct timeval tv_start, tv_now;  
  10.   
  11.   X_LOCK (reslock);  
  12.   maxreqs = max_poll_reqs;  
  13.   maxtime = max_poll_time;  
  14.   X_UNLOCK (reslock);  
  15.   
  16.   if (maxtime)  
  17.     gettimeofday (&tv_start, 0);  
  18.   
  19.   for (;;)  
  20.     {  
  21.       ETP_REQ *req;  
  22.   
  23.       etp_maybe_start_thread ();  
  24.   
  25.       X_LOCK (reslock);  
  26.       req = reqq_shift (&res_queue);//从回执队列取出优先级最高的回执信息  
  27.   
  28.       if (req)  
  29.         {  
  30.           --npending;  
  31.   
  32.           if (!res_queue.size && done_poll_cb)//直到回执全部处理完,执行done_poll();  
  33.           {  
  34.               //printf("执行done_poll()\n");  
  35.               done_poll_cb ();  
  36.           }  
  37.         }  
  38.   
  39.       X_UNLOCK (reslock);  
  40.   
  41.       if (!req)  
  42.         return 0;  
  43.   
  44.       X_LOCK (reqlock);  
  45.       --nreqs;//发出请求,到收到回执,该请求才算处理完毕.  
  46.       X_UNLOCK (reqlock);  
  47.   
  48.       if (ecb_expect_false (req->type == EIO_GROUP && req->size))//ecb_expect_false仅仅用于帮助编译器产生更优代码,而对真值无任何影响  
  49.         {  
  50.           req->int1 = 1; /* mark request as delayed */  
  51.           continue;  
  52.         }  
  53.       else  
  54.         {  
  55.           int res = ETP_FINISH (req);//调用自定义函数,做进一步处理  
  56.           if (ecb_expect_false (res))  
  57.             return res;  
  58.         }  
  59.   
  60.       if (ecb_expect_false (maxreqs && !--maxreqs))  
  61.         break;  
  62.   
  63.       if (maxtime)  
  64.         {  
  65.           gettimeofday (&tv_now, 0);  
  66.   
  67.           if (tvdiff (&tv_start, &tv_now) >= maxtime)  
  68.             break;  
  69.         }  
  70.     }  
  71.   
  72.   errno = EAGAIN;  
  73.   return -1;  
  74. }  

11、当所有请求执行完毕,调用done_poll做收尾工作。

    在示例代码中是读出管道中的数据。用户可以自己定义一些别的工作
  1. /* 
  2.  * 功能:主线程回执处理完毕,调用此函数 
  3.  */  
  4. void  
  5. done_poll (void)  
  6. {  
  7.   char dummy;  
  8.   printf ("done_poll ()\n");  
  9.   read (respipe [0], &dummy, 1);  
  10. }  

至此,libeio就简单的跑了一遍,从示例代码可以看出,libeio使用简单。虽说现在是beat版,不过Node.js已经在使用了。
 
最后简单说一下代码中的宏ecb_expect_false和ecb_expect_true,在if判断中,经常会出现这两个宏,一步一步的查看宏定义,宏定义如下:
  1. #define ecb_expect(expr,value)         __builtin_expect ((expr),(value))  
  2. #define ecb_expect_false(expr) ecb_expect (!!(expr), 0)  
  3. #define ecb_expect_true(expr)  ecb_expect (!!(expr), 1)  
  4. /* for compatibility to the rest of the world */  
  5. #define ecb_likely(expr)   ecb_expect_true  (expr)  
  6. #define ecb_unlikely(expr) ecb_expect_false (expr)  
刚开始我也不太懂啥意思,后来查阅资料(http://www.adamjiang.com/archives/251)才明白,这些宏仅仅是在帮助编译器产生更优代码,而对真值的判断没有影响
原文地址:https://www.cnblogs.com/imlucky/p/3063302.html