在epoll模型的讲解中,这篇文章写的非常简单易懂,因此翻译过来分享给大家,做为epoll的入门。
原文地址:http://kovyrin.net/2006/04/13/epoll-asynchronous-network-programming/
一般情况下,应用“每个连接应用一个线程”的方式来实现TCP Server。但是在高负载的情况时,这种处理并不是很高效,因此需要应用其他的连接处理模型。在这篇文章中,将讨论怎么应用epoll来构建TCP Server的异步连接。
epoll是在Linux 2.6提出的新的系统调用,来代替被弃用的select(也包括poll)。select和poll的时间复杂度为O(n),而epoll的时间复杂度为O(1) - 这说明当被监听的描述符不断增加时,epoll仍然表现很好。select使用线性搜索方式,这导致它的时间复杂读为O(n),因此为了提高效率epoll则在内核文件架构中应用callback机制。
epoll中另一个根本的不同是epoll可以使用边沿触发,区别与水平触发。这说明当内核察觉文件描述符为I/O的就绪状态时你就会接受到“提示”(文件描述符可进行I/O操作的一瞬间),而不是被告知“描述符可以进行I/O操作”(只要文件描述符可以可进行I/O操作)。这样的方式由很多小优势:内核不必保持文件描述符的状态,而是给用户空间抛出异常,同时令用户空间的程序变得更灵活(如,可以忽略总是可读的状态)。
应用epoll方法你需要在程序中完成以下几个步骤:
创建epoll调用的描述符:
epfd = epoll_create(EPOLL_QUEUE_LEN);
EPOLL_QUEUE_LEN
是你想要同时管理的连接描述符的最大值。返回值epfd
将在接下来的epoll调用中使用。这个描述符可以被关闭,当在后续程序中不需要使用时。完成了第一步后,你可以通过如下调用将需要监听的描述符添加到epoll中。
static struct epoll_event ev;
int client_sock;
...
ev.events = EPOLLIN | EPOLLPRI | EPOLLERR | EPOLLHUP;
ev.data.fd = client_sock;
int res = epoll_ctl(epfd, EPOLL_CTL_ADD, client_sock, &ev);
ev
是epoll事件配置结构体,EPOLL_CTL_ADD
- 预定义的常量以用来添加socket到epoll中。epoll_ctrl
更详细的信息参考epoll_ctrl(2)的man页。当client_sock
被关闭时,它将自动在epoll中被删除。
- 当需要监听的描述符添加到epoll中后,你的程序将空闲或者等待以对epoll中socket进行操作。
while (1) {
// wait for something to do...
int nfds = epoll_wait(epfd, events,
MAX_EPOLL_EVENTS_PER_RUN,
EPOLL_RUN_TIMEOUT);
if (nfds < 0) die("Error in epoll_wait!");
// for each ready socket
for(int i = 0; i < nfds; i++) {
int fd = events[i].data.fd;
handle_io_on_socket(fd);
}
}
下面介绍了网络应用使用中的典型架构,这个架构在单进程和多进程中扩展性都非常好:
- Listener - 在一个线程中执行
bind()
和listen()
调用,同时等待连接接入。当有连接接入后,这个线程可以在监听的socket上调用accept()
,同时将已连入的连接发送给一个I/O-workers. - I/O-worker(s) - 一个或多个线程从linsener中接收连接并将他们添加到epoll中。
- Data Processing worker(s) - 一个或多个线程从I/O-workers中写入或读取数据,并进行处理。
如你所见,epoll()
API相当简单,但是却很强大。线性扩展将允许你管理大量的平行连接,这相比与一个连接一个进程来比,应用了更少的进程。