1. epoll简介
1. int epoll_create(int size);
创建一个epoll的句柄。自从linux2.6.8之后,size参数是被忽略的。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数是epoll_create()的返回值。
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd。
第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
- //保存触发事件的某个文件描述符相关的数据(与具体使用方式有关)
- typedef union epoll_data {
- void *ptr;
- int fd;
- __uint32_t u32;
- __uint64_t u64;
- } epoll_data_t;
- //感兴趣的事件和被触发的事件
- struct epoll_event {
- __uint32_t events; /* Epoll events */
- epoll_data_t data; /* User data variable */
- };
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在epoll监控的事件中已经发送的事件。参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时。
2. epoll触发方式

3. epoll编程框架
那么究竟如何来使用epoll呢?其实非常简单。
通过在包含一个头文件#include <sys/epoll.h> 以及几个简单的API将可以大大的提高你的网络服务器的支持人数。
首先通过create_epoll(int maxfds)来创建一个epoll的句柄。这个函数会返回一个新的epoll句柄,之后的所有操作将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。
之后在你的网络主循环里面,每一帧的调用epoll_wait(int epfd, epoll_event events, int max events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout是 epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件返回,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则返回。一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。
epoll_wait返回之后应该是一个循环,遍历所有的事件。
几乎所有的epoll程序都使用下面的框架:
- epollfd = epoll_create(...) //创建epoll描述符
- listenfd = socket(...) //创建用于端口监听的socket
- bind(listenfd ...) //绑定
- listen(listenfd ...) //开始在端口监听
- epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd...) //将listenfd加入epoll描述符监听
- for(;;) //无限循环
- {
- n = epoll_wait(...) //等待epoll描述符的事件发生
- for(0 ~ n) //可能有多个读写事件发生,遍历所有事件
- {
- if(events[i].data.fd == listenfd) //通过发生事件的socket描述符确认有新链接,
- { //而非已经打开的socket的读写事件
- connfd = accpet(...) //accept新连接为connfd
- epoll_ctl(...) //connfd加入epoll监听,像上面listenfd一样
- }
- else if(events[i].events & EPOLLIN) //发生事件的socket不是listenfd而是connfd
- { //且事件集里有EPOLLIN表明有数据要读取
- n = read(...) //读取数据
- if(n == 0) //读到0字节,需要关闭socket
- close(connfd) //close会自动将connfd从epoll监听中删除
- } //无需调用epoll_ctl(..EPOLL_CTL_DEL..)
- else if(...) //其他各种事件处理依次处理
- ... }
- }
4. epoll为什么这么快?
以一个生活中的例子来解释.假设你在大学中读书,要等待一个朋友来访,而这个朋友只知道你在A号楼,但是不知道你具体住在哪里,于是你们约好了在A号楼门口见面.
如果你使用的阻塞IO模型来处理这个问题,那么你就只能一直守候在A号楼门口等待朋友的到来,在这段时间里你不能做别的事情,不难知道,这种方式的效率是低下的.
现在时代变化了,开始使用多路复用IO模型来处理这个问题.你告诉你的朋友来了A号楼找楼管大妈,让她告诉你该怎么走.这里的楼管大妈扮演的就是多路复用IO的角色.
进一步解释select和epoll模型的差异.
select版大妈做的是如下的事情:比如同学甲的朋友来了,select版大妈比较笨,她带着朋友挨个房间进行查询谁是同学甲,你等的朋友来了,于是在实际的代码中,select版大妈做的是以下的事情:
int n = select(&readset,NULL,NULL,100);
for (int i = 0; n > 0; ++i)
{
if (FD_ISSET(fdarray[i], &readset))
{
do_something(fdarray[i]);
--n;
}
}
epoll版大妈就比较先进了,她记下了同学甲的信息,比如说他的房间号,那么等同学甲的朋友到来时,只需要告诉该朋友同学甲在哪个房间即可,不用自己亲自带着人满大楼的找人了.于是epoll版大妈做的事情可以用如下的代码表示:
for(i=0;i<n;++i)
{
do_something(events[n]);
}
在epoll中,关键的数据结构epoll_event定义如下:
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
别小看了这些效率的提高,在一个大规模并发的服务器中,轮询IO是最耗时间的操作之一.再回到那个例子中,如果每到来一个朋友楼管大妈都要全楼的查询同学,那么处理的效率必然就低下了,过不久楼底就有不少的人了.
对比最早给出的阻塞IO的处理模型, 可以看到采用了多路复用IO之后, 程序可以自由的进行自己除了IO操作之外的工作, 只有到IO状态发生变化的时候由多路复用IO进行通知, 然后再采取相应的操作, 而不用一直阻塞等待IO状态发生变化了.
从上面的分析也可以看出,epoll比select的提高实际上是一个用空间换时间思想的具体应用.
5. epoll测试例子程序
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <errno.h>
- #include <sys/socket.h>
- #include <netdb.h>
- #include <fcntl.h>
- #include <sys/epoll.h>
- #include <string.h>
- #define MAXEVENTS 64
- //函数:
- //功能:创建和绑定一个TCP socket
- //参数:端口
- //返回值:创建的socket
- static int
- create_and_bind (char *port)
- {
- struct addrinfo hints;
- struct addrinfo *result, *rp;
- int s, sfd;
- memset (&hints, 0, sizeof (struct addrinfo));
- hints.ai_family = AF_UNSPEC; /* Return IPv4 and IPv6 choices */
- hints.ai_socktype = SOCK_STREAM; /* We want a TCP socket */
- hints.ai_flags = AI_PASSIVE; /* All interfaces */
- s = getaddrinfo (NULL, port, &hints, &result);
- if (s != 0)
- {
- fprintf (stderr, "getaddrinfo: %s\n", gai_strerror (s));
- return -1;
- }
- for (rp = result; rp != NULL; rp = rp->ai_next)
- {
- sfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol);
- if (sfd == -1)
- continue;
- s = bind (sfd, rp->ai_addr, rp->ai_addrlen);
- if (s == 0)
- {
- /* We managed to bind successfully! */
- break;
- }
- close (sfd);
- }
- if (rp == NULL)
- {
- fprintf (stderr, "Could not bind\n");
- return -1;
- }
- freeaddrinfo (result);
- return sfd;
- }
- //函数
- //功能:设置socket为非阻塞的
- static int
- make_socket_non_blocking (int sfd)
- {
- int flags, s;
- //得到文件状态标志
- flags = fcntl (sfd, F_GETFL, 0);
- if (flags == -1)
- {
- perror ("fcntl");
- return -1;
- }
- //设置文件状态标志
- flags |= O_NONBLOCK;
- s = fcntl (sfd, F_SETFL, flags);
- if (s == -1)
- {
- perror ("fcntl");
- return -1;
- }
- return 0;
- }
- //端口由参数argv[1]指定
- int
- main (int argc, char *argv[])
- {
- int sfd, s;
- int efd;
- struct epoll_event event;
- struct epoll_event *events;
- if (argc != 2)
- {
- fprintf (stderr, "Usage: %s [port]\n", argv[0]);
- exit (EXIT_FAILURE);
- }
- sfd = create_and_bind (argv[1]);
- if (sfd == -1)
- abort ();
- s = make_socket_non_blocking (sfd);
- if (s == -1)
- abort ();
- s = listen (sfd, SOMAXCONN);
- if (s == -1)
- {
- perror ("listen");
- abort ();
- }
- //除了参数size被忽略外,此函数和epoll_create完全相同
- efd = epoll_create1 (0);
- if (efd == -1)
- {
- perror ("epoll_create");
- abort ();
- }
- event.data.fd = sfd;
- event.events = EPOLLIN | EPOLLET;//读入,边缘触发方式
- s = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);
- if (s == -1)
- {
- perror ("epoll_ctl");
- abort ();
- }
- /* Buffer where events are returned */
- events = calloc (MAXEVENTS, sizeof event);
- /* The event loop */
- while (1)
- {
- int n, i;
- n = epoll_wait (efd, events, MAXEVENTS, -1);
- for (i = 0; i < n; i++)
- {
- if ((events[i].events & EPOLLERR) ||
- (events[i].events & EPOLLHUP) ||
- (!(events[i].events & EPOLLIN)))
- {
- /* An error has occured on this fd, or the socket is not
- ready for reading (why were we notified then?) */
- fprintf (stderr, "epoll error\n");
- close (events[i].data.fd);
- continue;
- }
- else if (sfd == events[i].data.fd)
- {
- /* We have a notification on the listening socket, which
- means one or more incoming connections. */
- while (1)
- {
- struct sockaddr in_addr;
- socklen_t in_len;
- int infd;
- char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
- in_len = sizeof in_addr;
- infd = accept (sfd, &in_addr, &in_len);
- if (infd == -1)
- {
- if ((errno == EAGAIN) ||
- (errno == EWOULDBLOCK))
- {
- /* We have processed all incoming
- connections. */
- break;
- }
- else
- {
- perror ("accept");
- break;
- }
- }
- //将地址转化为主机名或者服务名
- s = getnameinfo (&in_addr, in_len,
- hbuf, sizeof hbuf,
- sbuf, sizeof sbuf,
- NI_NUMERICHOST | NI_NUMERICSERV);//flag参数:以数字名返回
- //主机地址和服务地址
- if (s == 0)
- {
- printf("Accepted connection on descriptor %d "
- "(host=%s, port=%s)\n", infd, hbuf, sbuf);
- }
- /* Make the incoming socket non-blocking and add it to the
- list of fds to monitor. */
- s = make_socket_non_blocking (infd);
- if (s == -1)
- abort ();
- event.data.fd = infd;
- event.events = EPOLLIN | EPOLLET;
- s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);
- if (s == -1)
- {
- perror ("epoll_ctl");
- abort ();
- }
- }
- continue;
- }
- else
- {
- /* We have data on the fd waiting to be read. Read and
- display it. We must read whatever data is available
- completely, as we are running in edge-triggered mode
- and won't get a notification again for the same
- data. */
- int done = 0;
- while (1)
- {
- ssize_t count;
- char buf[512];
- count = read (events[i].data.fd, buf, sizeof(buf));
- if (count == -1)
- {
- /* If errno == EAGAIN, that means we have read all
- data. So go back to the main loop. */
- if (errno != EAGAIN)
- {
- perror ("read");
- done = 1;
- }
- break;
- }
- else if (count == 0)
- {
- /* End of file. The remote has closed the
- connection. */
- done = 1;
- break;
- }
- /* Write the buffer to standard output */
- s = write (1, buf, count);
- if (s == -1)
- {
- perror ("write");
- abort ();
- }
- }
- if (done)
- {
- printf ("Closed connection on descriptor %d\n",
- events[i].data.fd);
- /* Closing the descriptor will make epoll remove it
- from the set of descriptors which are monitored. */
- close (events[i].data.fd);
- }
- }
- }
- }
- free (events);
- close (sfd);
- return EXIT_SUCCESS;
- }
运行方式:
在一个终端运行此程序:epoll.out PORT
另一个终端:telnet 127.0.0.1 PORT
运行结果: