epoll 基本知识
epoll
是 linux2.6 内核的一个新的系统调用,是之前 select、poll 的增强版本。epoll 更加灵活没有描述符限制,使用一个 epoll 文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样只需要在用户空间和内核空间拷贝一次,因此 epoll
在高性能服务器领域应用广泛。
epoll 相关接口
需要的头文件:
#include <sys/epoll.h>
需要的函数:
int epoll_create(int size); //创建一个epoll文件描述符
int epoll_create1(int flags);
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);
其中 struct epoll_event
的定义如下:
struct epoll_event
{
uint32_t events; //epoll 事件
epoll_data_t data; /* User data variable */
};
上面 epoll_data_t
的定义如下:
typedef union epoll_data
{
void *ptr; //用户指针
int fd; //文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
- int epoll_create(int size)
用于创建一个 epoll 文件描述符,size
告诉内核这个监听的数目(从Linux 2.6.8开始 size
参数已经无意义,只要大于 0 就行),创建成功后会返回一个大于 0 的文件描述符,失败返回 -1。
- int epoll_create1(int flags)
功能与 epoll_create 相同,但可以在 flags 设置一些参数标志,比如 EPOLL_CLOEXEC
。因为 epoll_create 的 size 已经没有意义,所以 flag
如果设置为 0,则与 epoll_create 函数没有任何区别。
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
对用户的文件描述符和其关联的事件在 epoll 描述符中进行控制操作。
epfd
就是调用 epoll_create 创建的文件描述符
op
有以下三种
EPOLL_CTL_ADD:注册新的 fd 到 epfd 中。
EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件。
EPOLL_CTL_DEL:从 epfd 中删除一个 fd。
event
是一个结构体用于填充目标描述符和监听的事件,以及一些用户的数据。其中 events 可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将 EPOLL 设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的;
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里。
成功返回 0,错误返回 -1
- int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
等待事件产生,参数 events
用来从内核得到事件的集合,maxevents
告之内核这个 events 有多大,timeout
是超时毫秒时间(0 会立即返回,-1 则阻塞等待),返回发生了事件的文件描述符的数目,如果为 0 表示超时,-1 表示出错。
epoll 工作模式
- epoll 对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT 模式是默认模式,LT 模式与 ET 模式的区别如下:
LT模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。
ET模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。
ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
- ET 模式和 LT 模式在 socket 编程中对内核缓冲区的可读可写事件也有很多需要注意的地方,以免出现 busy-loop。
LT 模式下调用 accept 时关注 connfd 的 EPOLLIN 事件,不能直接关注 EPOLLOUT 事件,因为这样会出现 busy-loop。
当有可读事件到来时,直接 read。
当应用层写入内核缓冲区未写完,将未发送完毕的数据添加到应用层缓冲区,需要关注 connfd 的可写事件,当EPOLLOUT 事件到来,取出应用层缓冲区中的数据发送完毕,需要取消关注 EPOLLOUT 事件。
ET 模式下,调用 accept 时就需要关注 EPOLLIN 事件 EPOLLOUT 事件。
有可写事件到来,需要 read 直到返回 EAGAIN 错误,表示读完了,因为 ET 模式下,只有当读完了才能再次触发。
发送时如果未发送完,需要将数据添加到应用层缓冲区中,如果有 EPOLLOUT 事件,取出应用层缓冲区数据发送,直到应用层缓冲区发送完,或者发送返回 EAGAIN。
该模式下 EMFILE 无法成功接收客户端,下次也不会触发,下次新的客户端连接都不会触发。
事件就绪条件
可读条件
- 监听 socket:该套接字是一个监听套接字且已完成的连接数不为 0。而这样的套接字处于可读状态,是因为套接字收到了对方的 connect 请求,执行了三次握手的第一步:对方发送 SYN 请求过来,使该方监听套接字处于可读状态;通常情况下,对这样的套接字执行 accept 操作不会阻塞;
- 已连接 socket:该套接字的接收缓冲区中的数据字节大于等于该套接字的接收缓冲区低水位标记的当前大小。对这样的套接字执行读操作不会阻塞并返回一个大于 0 的值(也就是返回准备好读入的数据)。可以用 SO_RCVLOWAT 套接字选项设置该套接字的低水位标记。对于 TCP 和 UDP 套接字而言,其缺省值为 1,这意味着,默认情况下,只要缓冲区中有数据,那就是可读的。
- 已连接 socket:该连接的读半部关闭(也就是接收了 FIN 的 TCP 连接)。对这样的套接字的读操作将不阻塞并返回 0(也就是返回 EOF),此时必须且一直会返回 0;
- 已连接 socket:其上有一个套接字错误待处理。对这样的套接字的读操作将不会阻塞并返回 -1(即返回一个错误),同时把 errno 设置成确切的错误条件。这些待处理错误(pending error)也可通过指定 SO_ERROR 套接字选项调用 getsockopt 获取并清除。
可写条件
- 已连接 socket/UDP socket:该套接字发送缓冲区中的可用空间字节数大于等于该套接字的发送缓冲区低水位标记的当前大小(对于 TCP 的已连接 socket 或者 UDP socket 均可)。对这样的套接字的写操作将不阻塞并返回一个大于 0 的值(也就是返回准备好写入的数据)。可以用 SO_SNDLOWAT 套接字选项设置该套接字的低水位标记。对于 TCP 和 UDP 套接字而言,低水位默认值为 2048,发送缓冲区默认大小为 8K,这意味着,默认情况下,一个套接字连接成功后,总是可写的;
- 已连接 socket:该连接的写半部关闭(主动发送了 FIN 的 TCP 连接)。对这样的套接字的写操作将产生 SIGPIPE 信号,该信号的缺省行为是终止进程;
- 已连接 socket:其上有一个套接字错误待处理。对这样的套接字的写操作将不会阻塞并且返回 -1(即返回一个错误),同时把 errno 设置成确切的错误条件。这些待处理的错误也可以通过指定 SO_ERROR 套接字选项调用 getsockopt 函数来取得并清除;
- 使用非阻塞式 connect 的套接字已建立连接,或者 connect 已经以失败告终,即 connect 已经完成。
异常条件
- 该套接字存在带外数据或者仍处于带外标记
epoll 实例
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <errno.h>
#include <vector>
#include <algorithm>
#include <stdio.h>
#include <iostream>
using namespace std;
typedef std::vector<struct epoll_event> EVENTLIST; //内存连续,存放epoll_event
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
int main(int argc, char const *argv[])
{
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
int idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
//创建套接字描述符
int listenfd;
if ((listenfd = socket(PF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_TCP)) < 0)
{
ERR_EXIT("socket.");
}
//填充服务器地址信息
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8000);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//地址&端口复用
int on = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
ERR_EXIT("setsockopt reuseaddr.");
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on)) < 0)
ERR_EXIT("setsocket reuseport.");
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind.");
if (listen(listenfd, SOMAXCONN) < 0)
ERR_EXIT("listen.");
std::vector<int> clients;
//创建epoll描述符,epoll_create1可以指定一个选项EPOLL_CLOEXEC,而原来的size现在已经不需要了
int epollfd = epoll_create1(EPOLL_CLOEXEC);
//将监听套接字的可读事件加到epollfd中
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 connfd;
int nready;
while (1)
{
//events是一个传出参数
nready = epoll_wait(epollfd, &*events.begin(), static_cast<int>(events.size()), -1);
if (-1 == nready)
{
if (errno == EINTR)
continue;
ERR_EXIT("epoll_wait.");
}
if (nready == 0)
continue;
//空间不够需要扩充
if ((size_t)nready == events.size())
events.resize(events.size() * 2);
for (int i = 0; i < nready; i++)
{
//监听到新的客户端连接
if (events[i].data.fd == listenfd)
{
peerlen = sizeof(peeraddr);
connfd = accept4(listenfd, (struct sockaddr *)&peeraddr, &peerlen, SOCK_NONBLOCK | SOCK_CLOEXEC);
if (connfd == -1)
{
//套接字描述符已被用完
if (errno == EMFILE)
{
close(idlefd);
idlefd = accept(listenfd, NULL, NULL);
close(idlefd);
idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
continue;
}
else
{
ERR_EXIT("accept4");
}
}
cout << "ip=" << inet_ntoa(peeraddr.sin_addr) << "port=" << ntohs(peeraddr.sin_port) << endl;
clients.push_back(connfd);
//将新连接及事件加入加入到epoll描述符中
event.data.fd = connfd;
event.events = EPOLLIN /* | EPOLLET*/;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);
}
//如果时已连接套接字有事件发生
else if (events[i].events & EPOLLIN)
{
connfd = events[i].data.fd;
if (connfd < 0)
continue;
char buf[1024] = {0};
int ret = read(connfd, buf, 1024);
if (ret == -1)
ERR_EXIT("read.");
if (ret == 0) //对方关闭了套接字
{
cout << "client close." << endl;
close(connfd);
//从epoll描述符中删除此套接字及事件
event = events[i];
epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, &event);
clients.erase(remove(clients.begin(), clients.end(), connfd), clients.end());
continue;
}
//Linux下如果不输出换行不会刷新缓冲区,就不会输出
cout << "recv:" << buf << endl;
write(connfd, buf, strlen(buf));
}
}
}
return 0;
}