libeio-异步I/O库初窥

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lanyan822/article/details/7644745

      这段时间一直在学习Linux下编程,前两天看一帖子中提到Libeio这个异步I/O库,于是搜索来看看。libeio代码量不大用wc命令统计了一下,四千多行。于是乎决定学习一下,通过读代码来增加对linux编程的认识。

     Libeio是用多线程实现的异步I/O库.主要步骤如下:

  1.  主线程接受请求,将请求放入请求队列,唤醒子线程处理。这里主线程不会阻塞,会继续接受请求
  2. 子线程处理请求,将请求回执放入回执队列,并调用用户自定义方法,通知主线程有请求已处理完毕
  3. 主线程处理回执。
     源码中提供了一个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、子线程处理请求

     子线程调用eio_excute处理请求。这里才真正的执行I/O操作。之前我们传过来的是创建文件夹操作,子线程判断请求类型,根据类型,调用系统函数执行操作,并把执行结果,写回到请求的result字段,如果执行有误,设置errno
     因为eio_excute函数比较长,这里只贴出创建文件夹代码。
     
/*
 * 功能:根据类型,执行不同的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;
   }
}
   从代码中可以看出,用户是可以取消之前的I/O操作,如果I/O操作未执行,可以取消。如果I/O操作已经在运行了,则取消无效。

8、写回执

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

9、通知父线程有回执

    用户自己定义want_poll函数,用于子线程通知父线程有请求回执放入回执队列。示例代码是用的写管道。这里需要指出的时,当将请求回执放入空的回执队列才会通知父线程,如果在放入时,回执队列已不为空,则不会通知父线程。为什么了?因为父线程处理回执的时候,会处理现有的所有回执。
   
/*
 * 功能:子线程通知主线程已有回执放入回执队列.
 */
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已经在使用了。

最后简单说一下代码中的宏ecb_expect_false和ecb_expect_true,在if判断中,经常会出现这两个宏,一步一步的查看宏定义,宏定义如下:
#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)
刚开始我也不太懂啥意思,后来查阅资料(http://www.adamjiang.com/archives/251)才明白,这些宏仅仅是在帮助编译器产生更优代码,而对真值的判断没有影响




     

没有更多推荐了,返回首页