• 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)才明白,这些宏仅仅是在帮助编译器产生更优代码,而对真值的判断没有影响
  • 相关阅读:
    path.join()和path.resolve()
    __dirname和__filename
    使用css-loader
    博客主题
    Python使用pandas库读取txt文件中的Json数据,并导出到csv文件
    为什么一个星期工作量的工作,我做了一个多月,还没结束 (基于socket的分布式数据处理程序Java版)
    Docker 命令
    Python使用pandas库读取csv文件,并分组统计的一个例子
    Linux 进程守护脚本
    Linux 安装 JDK
  • 原文地址:https://www.cnblogs.com/imlucky/p/3063302.html
Copyright © 2020-2023  润新知