Linux epoll模型实现初步探讨
select、poll、epoll一样都是I/O多路复用技术。网络编程还有其他常用模型,如每连接一进程(PPC, 在Apache服务器中采用)、每连接一线程(TPC)。还有Windows中的IOCP
select/pselect, poll/ppoll与epoll的比较:
1. 历史上,select最先出现,pselect是POSIX定义的pselect变体的版本,可以指定信号屏蔽字。select可以侦听的文件描述符数收到FD_SETSIZE,Linux的值为1024
#include <sys/select.h> int pselect(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, const struct timespec *restrict timeout, const sigset_t *restrict sigmask); int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);2. 随后,出现了poll/ppoll,去掉select的FD_SETSIZE的限制,并使用struct pollfd描述感兴趣的事件,粒度更细。
#include <poll.h> int poll(struct pollfd fds[], nfds_t nfds, int timeout); int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout_ts, const sigset_t *sigmask);
3. 在后,Linux2.5开始引入了epoll模型,该模型的使用需要一组系统调用,将注册感兴趣的事件(epoll_ctl)与获取时间通知操作(epoll_wait)解耦,解决了每次调用select和poll都需要在内核态和用户态之间来回拷贝文件描述符列表,且在返回后,调用者还需要手动遍历整个列表进行判断所带来的性能问题。
注:尽管epoll号称异步事件通知,但仍需调用者将事件poll out,而非像IOCP那样自动在独立的工作线程中完成用户回调的方式。
#include <sys/epoll.h> int epoll_create(int size); //只要size>=0即可。内核会忽略掉size的值 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // maxevents不可大于#define EP_MAX_EVENTS (INT_MAX / sizeof(struct epoll_event))
epoll是基于eventpollfs文件系统实现的。
1. epoll_create实际上会在内核中创建一个eventpollfs文件系统的文件和一个struct eventpoll的实例,后者拥有一个描述已注册侦听事件列表(每一项用struct epitem描述,基于红黑树实现)、一个ready列表(维护待通知事件)和一个等待队列(用于对epoll_wait的进程阻塞)。
2. epool_ctl用户注册侦听事件,简单来讲,就是创建一个epitem,加入已注册侦听事件列表(内部使用红黑树组织),同时会为item注册一个回调函数(ep_ptable_queue_proc)到被侦听描述符,描述符上有事件到达(如socket接收缓冲区中到来数据)时会回调本函数。回调函数的主要作用就是将item加入ready列表,并唤醒阻塞在epoll_wait调用上的进程,这样就完成了所谓的“异步事件”通知机制。
3. epoll_wait主要内容为:首先判断ready列表是否有待通知事件,若无,则会阻塞在eventpoll上对应的等待队列。等待通知到达后,会将到达通知对应数据从内核态拷贝到用户态的events数组中。
注:网上有人说从内核态到用户态传递通知是基于共享内存(mmap)的,那应该是较老的内核或2.5版本中的实现,3.10版本并没有使用mmap。