1. epoll
的优越性
上一节介绍的select
有几个缺点:
- 存在最多监听的描述符上限
FD_SETSIZE
- 每次被唤醒时必须遍历才能知道是哪个描述符上状态
ready
,CPU随描述符数量线性增长 - 描述符集需要从内核copy到用户态
这几个缺点反过来正是epoll
的优点,或者说epoll
就是为了解决这些问题诞生的:
- 没有最多监听的描述符上限
FD_SETSIZE
,只受最多文件描述符的限制,在系统中可以使用ulimit -n
设置,运维一般会将其设置成20万以上 - 每次被唤醒时返回的是所有
ready
的描述符,同时还带有ready
的类型 - 内核态与用户态共享内存,不需要copy
2. 简述epoll
的工作过程
2.1 创建
首先由epoll_create
创建epoll
的实例,返回一个用来标识此实例的文件描述符。
2.2 控制
通过epoll_ctl
注册感兴趣的文件描述符,这些文件描述符的集合也被称为epoll set
。
2.3 阻塞
最后调用epoll_wait
阻塞等待内核通知。
3. 水平触发(LB)和边缘触发(EB)
epoll
的内核通知机制有水平触发和边缘触发两种表现形式,我们在下面例子中看一下两者的区别。
-
有一个代表读的文件描述符(
rfd
)注册在epoll
上 -
在管道的写端,写者写入了2KB数据
-
调用
epoll_wait
会返回rfd
作为ready的文件描述符 -
管道读端从
rfd
读取了1KB数据 -
再次调用
epoll_wait
如果rfd
文件描述符以ET的方式加入epoll
的描述符集,那么上边最后一步就会继续阻塞,尽管rfd
上还有剩余的数据没有读完。相反LT模式下,文件描述符上数据没有读完就会一直通知下去。
epoll的两种模式LT和ET
二者的差异在于level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。
所以,在epoll的ET模式下,正确的读写方式为:
读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN
写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN
正确的读
n = 0; |
while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) { |
n += nread; |
} |
if (nread == -1 && errno != EAGAIN) { |
perror("read error"); |
} |
正确的写
int nwrite, data_size = strlen(buf); |
n = data_size; |
while (n > 0) { |
nwrite = write(fd, buf + data_size - n, n); |
if (nwrite < n) { |
if (nwrite == -1 && errno != EAGAIN) { |
perror("write error"); |
} |
break; |
} |
n -= nwrite; |
} |
man手册:
When used as an edge-triggered interface, for performance reasons, it is possible to add the file descriptor inside the epoll interface (EPOLL_CTL_ADD) once by specifying (EPOLLIN|EPOLLOUT). This allows you to avoid continuously switching between EPOLLIN and EPOLLOUT calling epoll_ctl(2) with EPOLL_CTL_MOD.
ET模式:高速模式,不需要不停的调用Epoll_ctl的EPOLL_CTL_MOD进行切换in\out.连接描述符需要设置为非阻塞.需要考虑数据未读完的情况.
LT模式:不需要考虑数据是否读完,但是需要不停的切换in\out,避免cpu空转.
一般情况下,LT就够了,Redis只支持LT模式.
4. epoll
的两个数据结构
4.1 epoll_event
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
参数events
:
此参数是一个位集合,可能有以下几种中的组合:
-
EPOLLIN
:适用read
操作,包括对端正常关闭 -
EPOLLOUT
:适用write
操作 -
EPOLLRDHUP
:TCP对端关闭了连接 -
EPOLLPRI
:对于read
操作有紧急的数据到来 -
EPOLLERR
:文件描述符上的错误,不需要设置在events
上,因为epoll
总是会等待错误 -
EPOLLHUP
:与上边EPOLLERR
相同 -
EPOLLET
:设置边缘触发方式 -
EPOLLONESHOT
:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
4.2 epoll_data
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
这个结构体有些tricky,是四种不同数据类型的union,实际上设计者的意思是内容是什么交给使用者决定,相当于一个上下文。一般使用int fd
来区分是哪个socket发生的事件。
5. API详解
5.1 epoll_create
int epoll_create(int size);
int epoll_create1(int flags);
epoll_create
创建了一个epoll
的实例,请求内核为size
大小的文件描述符分配一个事件通知对象。实际上size
只是一个提示,并没有什么实际的作用。此函数返回用来标识epoll
实例的文件描述符,此后所有对epoll
的请求都要通过这个文件描述符。当不需要使用epoll
时需要使用close
关闭这个文件描述符,告诉内核销毁实例。
5.2 epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
在epoll
实例epfd
上的控制操作,op
的取值有以下三种:
-
EPOLL_CTL_ADD
: 将fd
带着事件参数event
注册到epfd
上 -
EPOLL_CTL_MOD
: 改变事件 -
EPOLL_CTL_DEL
: 从epfd
上删除
返回的错误码请参阅man手册。
5.3 epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
等待注册在epfd
上的事件,事件再events参数中带出。对于timeout参数:
- -1:永远等待
- 0:不等待直接返回
- 其他:在超时时间内没有事件发生,返回0
6. 完整C++代码示例
/*
* Serverepoll2.cpp
*
* Created on: 2015-1-19
* Author: shuyan
*/
#include "Serverepoll2.h"
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <vector>
#include <algorithm>
void activate_nonblock(int fd);
typedef std::vector<struct epoll_event> EventList;
/* 相比于select与poll,epoll最大的好处是不会随着关心的fd数目的增多而降低效率 */
int main(void) {
int count = 0;
int listenfd;
if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
perror("socket");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(1234);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY );
int on = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
perror("setsockopt");
if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0)
perror("bind");
if (listen(listenfd, SOMAXCONN) < 0)
perror("listen");
std::vector<int> clients;
int epollfd;
epollfd = epoll_create1(EPOLL_CLOEXEC); //epoll实例句柄
struct epoll_event event;
event.data.fd = listenfd;
event.events = EPOLLIN | EPOLLET; //边沿触发
epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event);
EventList events(16);
struct sockaddr_in peeraddr;
socklen_t peerlen;
int conn;
int i;
int nready;
while (1) {
nready = epoll_wait(epollfd, &*events.begin(),
static_cast<int>(events.size()), -1);
if (nready == -1) {
if (errno == EINTR)
continue;
perror("epoll_wait");
}
if (nready == 0)
continue;
if ((size_t) nready == events.size())
events.resize(events.size() * 2);
for (i = 0; i < nready; i++) {
if (events[i].data.fd == listenfd) {
peerlen = sizeof(peeraddr);
conn = accept(listenfd, (struct sockaddr *) &peeraddr,
&peerlen);
if (conn == -1)
perror("accept");
printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
ntohs(peeraddr.sin_port));
printf("count = %d\n", ++count);
clients.push_back(conn);
activate_nonblock(conn);
event.data.fd = conn;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, conn, &event);
} else if (events[i].events & EPOLLIN) {
conn = events[i].data.fd;
if (conn < 0)
continue;
char recvbuf[1024] = { 0 };
int ret = recv(conn, recvbuf, 1024, 0);
if (ret == -1)
perror("readline");
if (ret == 0) {
printf("client close\n");
close(conn);
event = events[i];
epoll_ctl(epollfd, EPOLL_CTL_DEL, conn, &event);
clients.erase(
std::remove(clients.begin(), clients.end(), conn),
clients.end());
}
printf("id:%d,data:%s", events[i].data.fd, recvbuf);
send(conn, recvbuf, strlen(recvbuf), 0);
}
}
}
return 0;
}
/* activate_nonblock - 设置IO为非阻塞模式
* fd: 文件描述符
*/
void activate_nonblock(int fd) {
int ret;
int flags = fcntl(fd, F_GETFL);
if (flags == -1)
perror("fcntl error");
flags |= O_NONBLOCK;
ret = fcntl(fd, F_SETFL, flags);
if (ret == -1)
perror("fcntl error");
}
client端同上篇