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就这么多, 其实是非常简单的, 只是作者玩了太多宏, 实在有点架构洁癖..