分享

【Jabberd2源码剖析系列 mio】

 水木天涯阁 2019-03-12

mio是一个xmpp的I/O复用事件库, 对开发者提供透明API进行开发, 而在API之下允许灵活使用不同的I/O复用作为底层实现.

  mio采用了大量的宏替换, 实现了 开发者API -> 底层实现 的解耦, 该解耦逻辑发生在预编译阶段, 而不是我们习惯的运行阶段解耦技巧.

  mio暴露给用户的API是固定不变的, 这些API的声明存在于mio.h中, 如下:

  1, 描述符:

typedef struct mio_fd_st
{
    int fd; 
} *mio_fd_t;

  2, mio核心结构体: 内部看似采用了函数指针解耦, 但其实各个函数指针最终都指向了固定的函数, 并不是用来解耦底层I/O复用实现而存在的, 这处设计很容易造成误解.

复制代码
typedef enum { action_ACCEPT, action_READ, action_WRITE, action_CLOSE } mio_action_t;
typedef int (*mio_handler_t) (struct mio_st **m, mio_action_t a, struct mio_fd_st *fd, void* data, void *arg);

typedef struct mio_st
{
  void (*mio_free)(struct mio_st **m);

  struct mio_fd_st *(*mio_listen)(struct mio_st **m, int port, char *sourceip,
                  mio_handler_t app, void *arg);

  struct mio_fd_st *(*mio_connect)(struct mio_st **m, int port, char *hostip,
                   char *srcip, mio_handler_t app, void *arg);

  struct mio_fd_st *(*mio_register)(struct mio_st **m, int fd, 
                   mio_handler_t app, void *arg);

  void (*mio_app)(struct mio_st **m, struct mio_fd_st *fd,
          mio_handler_t app, void *arg);

  void (*mio_close)(struct mio_st **m, struct mio_fd_st *fd);

  void (*mio_write)(struct mio_st **m, struct mio_fd_st *fd);

  void (*mio_read)(struct mio_st **m, struct mio_fd_st *fd);

  void (*mio_run)(struct mio_st **m, int timeout);
} **mio_t;
复制代码

  3, API: 直接与用户接触, 其中m是上述mio结构体. 即直接与用户接触的两样东西是struct mio_st, 以及这些API.

复制代码
/** create/free the mio subsytem */
JABBERD2_API mio_t mio_new(int maxfd); /* returns NULL if failed */

#define mio_free(m) (*m)->mio_free(m)

/** for creating a new listen socket in this mio (returns new fd or <0) */
#define mio_listen(m, port, sourceip, app, arg) \
    (*m)->mio_listen(m, port, sourceip, app, arg)

/** for creating a new socket connected to this ip:port (returns new fd or <0, use mio_read/write first) */
#define mio_connect(m, port, hostip, srcip, app, arg) \
    (*m)->mio_connect(m, port, hostip, srcip, app, arg)

/** for adding an existing socket connected to this mio */
#define mio_register(m, fd, app, arg) \
    (*m)->mio_register(m, fd, app, arg)

/** re-set the app handler */
#define mio_app(m, fd, app, arg) (*m)->mio_app(m, fd, app, arg)

/** request that mio close this fd */
#define mio_close(m, fd) (*m)->mio_close(m, fd)

/** mio should try the write action on this fd now */
#define mio_write(m, fd) (*m)->mio_write(m, fd)

/** process read events for this fd */
#define mio_read(m, fd) (*m)->mio_read(m, fd)

/** give some cpu time to mio to check it's sockets, 0 is non-blocking */
#define mio_run(m, timeout) (*m)->mio_run(m, timeout)

/** all MIO related routines should use those for error reporting */
#ifndef _WIN32
# define MIO_ERROR       errno
# define MIO_SETERROR(e) (errno = e)
# define MIO_STRERROR(e) strerror(e)
# define MIO_WOULDBLOCK  (errno == EWOULDBLOCK || errno == EINTR || errno == EAGAIN)
复制代码

  上述并没有涉及到任何实际函数声明, 都是函数指针.

  这些API的实现主要集中在mio_impl.h中, 在该文件中, 实现了对应上面struct mio_st结构体中的各个函数指针对应的实际实现, 这些函数实现是固定不变的, 也就是之前提到的struct mio_st结构体在设计上有一点迷惑阅读者, 简单的说, 作者希望在C中实现面向对象的思想, 只好用函数指针配合C全局函数实现类似的风格.

  1, 在API层实现中, 用了一些私有定义, 这些定义不会暴露给用户: mio_type_t是一个fd所处的状态, 特别需要注意:

type_CLOSED: fd应当被关闭, 但暂时还不能执行, 该状态存在的原因受限于作者的实现, 后面会详细说明.
type_CONNECT_READ: fd用于connect主动连接其他服务端, 并且希望完成connect后注册read事件.
type_CONNECT_WRITE: 同上, 完成connect后希望注册write事件. 这两个状态是为了满足常见的编程需求, 因为connect是由mio框架负责跟踪处理的, 所以允许用户在进行connect之前告知希望监听的事件, 在mio框架完成connect后框架会帮用户注册事件.

mio_priv_fd_st: 相对于暴露给用户的struct mio_fd_s结构体而言, mio API内部实现关注比用户更多的信息, 比如连接状态type, 回调函数app, 回调用户参数arg, 以及在API层的下层实现需要的数据, 即MIO_FD_VARS, 这是一个宏, 由下层实现提供支持.

mio_priv_st:相对于暴露给用户的struct mio_st结构体而言, mio API内部需要更多与底层实现相关的信息, 这主要是指:MIO_VARS, 这也是一个宏, 由下层实现提供支持.
可以看到, MIO_FD_VARS, MIO_VARS两个大写宏, 它们是下层实现提供支持的, 下面将会有所了解.

复制代码
/** our internal wrapper around a fd */
typedef enum {
    type_CLOSED = 0x00,
    type_NORMAL = 0x01,
    type_LISTEN = 0x02,
    type_CONNECT = 0x10, 
    type_CONNECT_READ = 0x11,
    type_CONNECT_WRITE = 0x12
} mio_type_t;

typedef struct mio_priv_fd_st
{
    struct mio_fd_st mio_fd;

    mio_type_t type;
    /* app event handler and data */
    mio_handler_t app;
    void *arg;

    MIO_FD_VARS
} *mio_priv_fd_t;

/** now define our master data type */
typedef struct mio_priv_st
{
    struct mio_st *mio;

    int maxfd;
    MIO_VARS
} *mio_priv_t;
复制代码

 

2, 作者玩弄指针的宏: 作者为了在心里上彻底的解耦, 在API层实现中用了一些列指针操作, 乍看是不容易懂.

下面的m是mio_t类型, 也就是typedef struct mio_st* *mio_t类型, 即暴露给用户那个mio类型. (注意, mio_t是指向指针的指针)

下面的f是mio_fd_t类型, 也就是struct mio_fd_st *mio_fd_t类型, 即暴露给用户的那个fd类型.

MIO(m)是把mio_t转换成mio_priv_t, 即完成了从用户mio转换成内部mio, 用户手持的mio_t mio(struct mio_st* *mio;)实际上是mio_priv_t中的那个struct mio_st *mio的地址.

FD(m, f)是类似的, 即完成了从用户fd转换成内部fd的过程.

ACT(m, f, a, d)借助上面两个宏, 完成了一次用户回调函数的调用, 因为我们回调的是用户的函数, 所以我们必须完成一次从内部结构体到API结构体的转换, 所以可以看到这样的代码:&FD(m,f)->mio_fd, 即FD宏完成用户到内部的转换, ->mio_fd取出了用户暴露的fd结构体. FD(m,f)->arg即FD宏完成用户到内部结构的转换, 最后取出内部结构中该用户预注册的arg, FD(m,f)->app是取出用户注册的回调函数.

为什么作者弄这么麻烦呢, 因为作者是真的希望让用户完全看不到内部结构体, 是完全... 用户想改内核的东西, 没门! (至少我不会把用户态和内核态(程序内核)分的那么清楚)

#define MIO(m) ((mio_priv_t) m)
#define FD(m,f) ((mio_priv_fd_t) f)
#define ACT(m,f,a,d) (*(FD(m,f)->app))(m,a,&FD(m,f)->mio_fd,d,FD(m,f)->arg)

3, API的实现:摘取几个有代表性的, 主要关注API是如何与底层解耦的. 既然是API, 我们要明确函数的参数都是用户态结构体, 需要借助上述的宏完成外部到内部的结构转换.

对于_mio_read来说, 如果当前fd处于type_CONNECT状态, 表明fd的连接未完成, 此时用户希望读, 我们只能先保留, 待连接完成后再实际的注册用户希望的事件. 而对于普通fd来说, 立即为它注册读事件.

对于_mio_write来说, 原理是类似的, 另外有一个额外操作即立即回调了用户的回调函数, 告知它发生了write事件, 由用户自己完成write操作, 如果用户返回非0, 表明用户没有写完所有数据, 那么我们注册write事件.  对于_mio_write, 通常是这样使用的, 即用户先将一些数据存到了buffer里, 然后调用_mio_write希望写出这些数据, _mio_write立即回调用户的注册回调函数, 给用户立即写出的机会, 但用户可能因为网卡阻塞等无法完全写出, 那么_mio_write会替用户注册write事件, 以便后续触发再次写出.

复制代码
/** start processing read events */
static void _mio_read(mio_t m, mio_fd_t fd)
{
    if(m == NULL || fd == NULL) return;

    /* if connecting, do this later */
    if(FD(m,fd)->type & type_CONNECT)
    {
        FD(m,fd)->type |= type_CONNECT_READ;
        return;
    }

    MIO_SET_READ(m, FD(m,fd));
}

/** try writing to the socket via the app */
static void _mio_write(mio_t m, mio_fd_t fd)
{
    if(m == NULL || fd == NULL) return;

    /* if connecting, do this later */
    if(FD(m,fd)->type & type_CONNECT)
    {
        FD(m,fd)->type |= type_CONNECT_WRITE;
        return;
    }

    if(FD(m,fd)->type != type_NORMAL)
        return;

    if(ACT(m, fd, action_WRITE, NULL) == 0) return;

    /* not all written, do more l8r */
    MIO_SET_WRITE(m, FD(m,fd));
}
复制代码

其他API实现不一一列举, 我们可以注意MIO_SET_WRITE(m, FD(m,fd)), MIO_SET_READ(m, FD(m,fd)), 这种大写MIO开头的宏, 这些即使我们所说的底层实现解耦, 作者使用宏在预编译阶段完成解耦. 作者的设计很清晰, 即暴露给用户的是用户态的数据结构与API方法, 上面都已经见到过了, 而在API的实现层, 借助了这些宏实现了底层I/O复用具体实现的解耦, 具体点说, 对于select或者epoll, 它们对于MIO_SET_WRITE宏都有一份各自的实现(注册一个fd的可写事件, 对于select, epoll肯定是各不相同的), 在预编译阶段分别将不同实现的宏包含进来即可实现灵活的I/O复用层替换.

 

在该文件中, 最后一个函数显得比较重要, 它把上面所说的在何处解耦, 作者何处设计有一些迷惑性全部解释清晰.

可以看到, 所有的API函数就像是mio_t的成员函数一样注册到mio_t结构体中, 并且调用了MIO_INIT_VARS(m), 由底层I/O完成自己需要数据的初始化, 比如epoll需要epoll_create创建一个实例.

复制代码
/** eve */
static mio_t _mio_new(int maxfd)
{
    static struct mio_st mio_impl = {
        _mio_free,
        _mio_listen, _mio_connect, _mio_setup_fd,
        _mio_app,
        _mio_close,
        _mio_write, _mio_read,
        _mio_run
    };
    mio_t m;

    /* init winsock if we are in Windows */
#ifdef _WIN32
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD( 1, 1 ), &wsaData))
        return NULL;
#endif

    /* allocate and zero out main memory */
    if((m = calloc(1, sizeof(struct mio_priv_st))) == NULL) {
        fprintf(stderr,"Cannot allocate MIO memory! Exiting.\n");
        exit(EXIT_FAILURE);
    }

    /* set up our internal vars */
    *m = &mio_impl;
    MIO(m)->maxfd = maxfd;

    MIO_INIT_VARS(m);

    return m;
}         
复制代码

 

对于mio, 我们需要清晰的知道什么可变的, 什么是不可变的, 什么是给用户暴露的, 什么是作者想藏起来的, 最后就可以开始事件循环了:

同样, 大写的宏是底层实现相关的, 其他代码是底层无关不变的, 可以看到, MIO_CHECK监听了事件, 如果底层实现是epoll, 那么可能就是epoll_wait.

iter是迭代器, 配合MIO_ITERATE_RESULTS用于遍历所有发生事件的fd.

对于deffered是一个需要重点说明的, 可能影响阅读, 稍后解释. 

可以看到, 对于connect, listen的fd而言, 它们并不是监听普通的I/O读写, 而是连接相关的状态, 所以并不需要走type_NORMAL状态相关的分支(即读分支和写分支), 所以直接goto deffered结束处理. 必须郑重提示, 对于connect, listen的fd来说, deffered标签对于它们俩而言只是一个单纯的跳转, 为了避过接下来的两个if判断, deffered标签下的if对于connect,listen的fd来说永远不可能成立, 即deffered标签下的代码仅仅为type_NORMAL的普通fd作用, 稍后分析为什么.

复制代码
/** main select loop runner */
static void _mio_run(mio_t m, int timeout)
{
    int retval;
    MIO_INIT_ITERATOR(iter);

    mio_debug(ZONE, "mio running for %d sec", timeout);

    /* wait for a socket event */
    retval = MIO_CHECK(m, timeout);

    /* nothing to do */
    if(retval == 0) return;

    /* an error */
    if(retval < 0)
    {   
        mio_debug(ZONE, "MIO_CHECK returned an error (%d)", MIO_ERROR);

        return;
    }   

    mio_debug(ZONE,"mio processing %d file descriptors", retval);

    /* loop through the sockets, check for stuff to do */
    MIO_ITERATE_RESULTS(m, retval, iter)
    {   
        mio_fd_t fd = MIO_ITERATOR_FD(m,iter);
        if (fd == NULL) continue;

        /* skip already dead slots */ 
        if(FD(m,fd)->type == type_CLOSED) continue; 

        /* new conns on a listen socket */
        if(FD(m,fd)->type == type_LISTEN && MIO_CAN_READ(m,iter))
        {   
            _mio_accept(m, fd);
            goto deferred;
        }   

        /* check for connecting sockets */
        if(FD(m,fd)->type & type_CONNECT &&
           (MIO_CAN_READ(m,iter) || MIO_CAN_WRITE(m,iter)))        {
            _mio__connect(m, fd);
            goto deferred;
        }

        /* read from ready sockets */
        if(FD(m,fd)->type == type_NORMAL && MIO_CAN_READ(m,iter))
        {
            /* if they don't want to read any more right now */
            if(ACT(m, fd, action_READ, NULL) == 0)
                MIO_UNSET_READ(m, FD(m,fd));
        }

        /* write to ready sockets */
        if(FD(m,fd)->type == type_NORMAL && MIO_CAN_WRITE(m,iter))
        {
            /* don't wait for writeability if nothing to write anymore */
            if(ACT(m, fd, action_WRITE, NULL) == 0)
                MIO_UNSET_WRITE(m, FD(m,fd));
        }

    deferred:
        /* deferred closing fd
         * one of previous actions might change the state of fd */ 
        if(FD(m,fd)->type == type_CLOSED)
        {
            MIO_FREE_FD(m, fd);
        }
    }
}
复制代码

 

假设在type_NORMAL的读事件中, ACT用户回调中, 用户调用了mio_close希望关闭该fd, 那么对于不同的底层实现(select/epoll), 效果是不同的, 主要区别即是否该关闭操作被deffered(延迟关闭), 这得提到mio_close的API实现了:

可以看到, 首先MIO_REMOVE_FD从底层I/O复用中移出这个fd的事件监听, 然后ACT(m, fd, action_CLOSE, NULL)回调用户注册函数, 让用户自己决定做点什么, 比如释放arg.

最后, 我们还需要释放掉完成的fd结构体, 但我们发现它做了一个判断, MIO_CAN_FREE(m) -> MIO_FREE_FD(m, fd); 我们的API实现首先询问底层I/O复用实现, 是否可以释放该fd的结构体, 可以则立即释放, 否则不释放(实际上MIO_FREE_FD里已经设置该fd被deffered了).

复制代码
/** internal close function */
static void _mio_close(mio_t m, mio_fd_t fd) 
{
    if(FD(m,fd)->type == type_CLOSED)
        return;

    mio_debug(ZONE,"actually closing fd #%d", fd->fd);

    /* take out of poll sets */
    MIO_REMOVE_FD(m, FD(m,fd));

    /* let the app know, it must process any waiting write data it has and free it's arg */
    if (FD(m,fd)->app != NULL)
        ACT(m, fd, action_CLOSE, NULL);

    /* close the socket, and reset all memory */
    close(fd->fd);
    FD(m,fd)->type = type_CLOSED;
    FD(m,fd)->app = NULL;
    FD(m,fd)->arg = NULL;

    if (MIO_CAN_FREE(m))
    {   
        MIO_FREE_FD(m, fd);
    }   
}
复制代码

回头看一下_mio_run, 它在deffered标签下保证了对deffered的fd进行释放.

在一开始我提到, deffered是受限于作者的实现的, 假设我们在_mio_run的type_NOMARL类型fd发生了READ事件, 并在回调中mio_close(fd)希望释放掉该fd结构体, 但因为作者实现中接下来会执行WRITE事件的判断, 还会用到fd结构体, 所以mio_close(fd)需要谨慎判断是否真的可以在调用时刻立即释放内存, 所以引入了deffered逻辑.

 

具体的看, 对于select来说:

可以看到mio_select.h中如下实现, 根据我们对select的认识, 它只是遍历1024个描述符(FD_SETSIZE)依次判断是否在集合里集合, 因此对于select来说, 它的iterator迭代器仅仅是从0-1024的数字即可, 并且对于MIO_ALLOC_FD只是返回了MIO_VARS里预分配的FD_SETSIZE个mio_priv_fd_st之一而已, 因为这是select监听的极限, 这些mio_priv_fd_st不会随着客户端离开而释放, 是随着程序存在的. 自然, 可以考虑上面的情况, READ时候mio_close并立即完成了MIO_FREE_FD, 我们可以发现对于接下来的WRITE事件处理不会造成任何影响, 因为mio_close已经把这个fd设置为type_CLOSED了, if条件根本不会满足, 并且当走到deffered分支时也不会发生任何事, 因为MIO_FREE_FD实际上什么也没做.(mio_close里的MIO_REMOVE_FD已经完成了事件的取消注册, 我们现在关注的deffered关乎到是否释放fd的相关内存)

 

复制代码
#define MIO_FUNCS static void _mio_fds_init(mio_priv_t m) { int fd; for(fd = 0; fd < m->maxfd; fd++) { m->fds[fd].mio_fd.fd = fd; } m->highfd = 0; m->lowfd = m->maxfd; } static mio_fd_t _mio_alloc_fd(mio_priv_t m, int fd) { if(fd > m->highfd) m->highfd = fd; if(fd < m->lowfd) m->lowfd = fd; return &m->fds[fd].mio_fd; }

#define MIO_VARS struct mio_priv_fd_st *fds; int lowfd; int highfd; fd_set rfds_in, wfds_in, rfds_out, wfds_out;

#define MIO_INIT_VARS(m) do { if (maxfd > FD_SETSIZE) { mio_debug(ZONE,"wanted MIO larger than %d file descriptors", FD_SETSIZE); free(m); return NULL; } if((MIO(m)->fds = calloc(1, sizeof(struct mio_priv_fd_st) * maxfd)) == NULL) { mio_debug(ZONE,"internal error creating new mio"); free(m); return NULL; } _mio_fds_init(MIO(m)); FD_ZERO(&MIO(m)->rfds_in); FD_ZERO(&MIO(m)->wfds_in); } while(0)

#define MIO_FREE_VARS(m) free(MIO(m)->fds)

#define MIO_ALLOC_FD(m, rfd) _mio_alloc_fd(MIO(m), rfd)
#define MIO_FREE_FD(m, mfd) 
#define MIO_CAN_FREE(m) 1
复制代码

 

最后简单看一下mio_epoll.h为什么需要deffered吧, 因为它对于每一个客户端分配了一个mio_priv_fd_st结构体, 释放它将影响下面if type_WRITE分支, 所以必须延迟释放.

 

复制代码
#define MIO_FUNCS     static int _mio_poll(mio_t m, int t)                                    {                                                                           return epoll_wait(MIO(m)->epoll_fd,                                                       MIO(m)->res_event, 32, t*1000);                   }                                                                                                                                               static mio_fd_t _mio_alloc_fd(mio_t m, int fd)                          {                                                                           struct epoll_event event;                                               mio_priv_fd_t priv_fd = malloc(sizeof (struct mio_priv_fd_st));         memset(priv_fd, 0, sizeof (struct mio_priv_fd_st));                                                                                             priv_fd->mio_fd.fd = fd;                                                priv_fd->events = 0;                                                                                                                            event.events = priv_fd->events;                                         event.data.u64 = 0;                                                     event.data.ptr = priv_fd;                                      epoll_ctl(MIO(m)->epoll_fd, EPOLL_CTL_ADD, fd, &event);                                                                                         return (mio_fd_t)priv_fd;                                            }


#define MIO_FD_VARS \
    uint32_t events;

#define MIO_VARS     int defer_free;                                                         int epoll_fd;                                                           struct epoll_event res_event[32];

#define MIO_INIT_VARS(m)     do {                                                                        MIO(m)->defer_free = 0;                                                 if ((MIO(m)->epoll_fd = epoll_create(maxfd)) < 0)                       {                                                                           mio_debug(ZONE,"unable to initialize epoll mio");                       free(m);                                                                return NULL;                                                        }                                                                   } while(0)
#define MIO_FREE_VARS(m)     do {                                                                        close(MIO(m)->epoll_fd);                                            } while(0)


#define MIO_ALLOC_FD(m, rfd)    _mio_alloc_fd(m, rfd)
#define MIO_FREE_FD(m, mfd)     if(mfd)free(mfd)

#define MIO_REMOVE_FD(m, mfd)     do {                                                                        struct epoll_event event;                                               event.events = 0;                                                       event.data.u64 = 0;                                                     event.data.ptr = mfd;                                                   epoll_ctl(MIO(m)->epoll_fd, EPOLL_CTL_DEL,                                        mfd->mio_fd.fd, &event);                                  } while (0)

#define MIO_CHECK(m, t)         _mio_poll(m, t)

#define MIO_SET_READ(m, mfd)     do {                                                                        struct epoll_event event;                                               mfd->events |= EPOLLIN;                                                 event.events = mfd->events;                                             event.data.u64 = 0;                                                     event.data.ptr = mfd;                                                   epoll_ctl(MIO(m)->epoll_fd, EPOLL_CTL_MOD,                                        mfd->mio_fd.fd, &event);                                  } while (0)

#define MIO_SET_WRITE(m, mfd)     do {                                                                        struct epoll_event event;                                               mfd->events |= EPOLLOUT;                                                event.events = mfd->events;                                             event.data.u64 = 0;                                                     event.data.ptr = mfd;                                                   epoll_ctl(MIO(m)->epoll_fd, EPOLL_CTL_MOD,                                        mfd->mio_fd.fd, &event);                                  } while (0)

#define MIO_UNSET_READ(m, mfd)     do {                                                                        struct epoll_event event;                                               mfd->events &= ~EPOLLIN;                                                event.events = mfd->events;                                             event.data.u64 = 0;                                                     event.data.ptr = mfd;                                                   epoll_ctl(MIO(m)->epoll_fd, EPOLL_CTL_MOD,                                        mfd->mio_fd.fd, &event);                                  } while (0)
#define MIO_UNSET_WRITE(m, mfd)     do {                                                                        struct epoll_event event;                                               mfd->events &= ~(EPOLLOUT);                                             event.events = mfd->events;                                             event.data.u64 = 0;                                                     event.data.ptr = mfd;                                                   epoll_ctl(MIO(m)->epoll_fd, EPOLL_CTL_MOD,                                        mfd->mio_fd.fd, &event);                                  } while (0)


#define MIO_CAN_READ(m,iter) \
    (MIO(m)->res_event[iter].events & (EPOLLIN|EPOLLERR|EPOLLHUP))

#define MIO_CAN_WRITE(m,iter) \
    (MIO(m)->res_event[iter].events & EPOLLOUT)

#define MIO_CAN_FREE(m)         (!MIO(m)->defer_free)

#define MIO_INIT_ITERATOR(iter)     int iter

#define MIO_ITERATE_RESULTS(m, retval, iter)     for(MIO(m)->defer_free = 1, iter = 0; (iter < retval) || ((MIO(m)->defer_free = 0)); iter++)

#define MIO_ITERATOR_FD(m, iter) \
    (MIO(m)->res_event[iter].data.ptr)
复制代码

 

关于mio就这么多, 其实是非常简单的, 只是作者玩了太多宏, 实在有点架构洁癖..

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多