epoll 是在 2.6 内核中提出的,是之前的select和 poll的增强版本。相对于 select和 poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。
一、epoll函数详解
#include <sys/epoll.h>
int epoll_create(int 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);
// 保存触发事件的某个文件描述符相关的数据(与具体使用方式有关)
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 队列里
1、epoll_create()函数
int epoll_create(int size);
- 功能:该函数生成一个 epoll 专用的文件描述符(创建一个 epoll 的句柄)。
- 参数:
- size:用来告诉内核这个监听的数目一共有多大,参数 size 并不是限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。自从 linux 2.6.8 之后,size 参数是被忽略的,也就是说可以填只有大于 0 的任意值。需要注意的是,当创建好 epoll 句柄后,它就是会占用一个 fd 值,在 linux 下如果查看 /proc/ 进程 id/fd/,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close() 关闭,否则可能导致 fd 被耗尽。
- 返回值:成功:epoll 专用的文件描述符;失败:-1。
2、epoll_ctl()函数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 功能:epoll的事件注册函数,它不同于 select() 是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
- 参数:
- epfd:epoll 专用的文件描述符,epoll_create()的返回值 。
- op:表示动作,用三个宏来表示:
- EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
- EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
- EPOLL_CTL_DEL:从 epfd 中删除一个 fd。
- fd:需要监听的文件描述符;
- event:告诉内核要监听什么事件;
- 返回值:成功:0,失败:-1。
3、epoll_wait()函数
int epoll_wait( int epfd, struct epoll_event * events, int maxevents, int timeout );
- 功能:等待事件的产生,收集在 epoll 监控的事件中已经发送的事件,类似于 select调用。
- 参数:
- epfd:epoll专用的文件描述符,epoll_create()的返回值;
- events:分配好的 epoll_event 结构体数组,epoll 将会把发生的事件赋值到events 数组中(events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存);
- maxevents:告知内核这个 events 有多大 ;
- timeout:超时时间,单位为毫秒,为 -1 时,函数为阻塞。
- 返回值:成功:返回满足监听条件的事件数目,如返回 0 表示已超时;失败:-1。
二、epoll高并发服务器的流程
#include <头文件>
int main(int argc, char const *argv[])
{
lfd = socket();
bind();
listen();
// 建立epoll模型,efd指向红黑树根节点, OPEN_MAX为监听节点数量(仅供内核参考)
efd = epoll_create(OPEN_MAX);
// tep:epoll_ctl参数(只是一个临时变量,用来设置单个fd属性),ep[]:epoll_wait参数
struct epoll_event tep, ep[OPEN_MAX];
// 指定lfd的监听事件为读事件,文件描述符为lfd
tep.events = EPOLLIN;
tep.data.fd = lfd;
// 添加lfd到红黑树,通过efd可以找到lfd
res = epoll_ctl(efd, EPOLL_CTL_ADD, lfd, &tep);
while(1)
{
// nready返回的是满足监听事件的总个数(可作为循环上限),如果为0则表示没有fd满足监听事件
nready = epoll_wait(efd, ep, OPEN_MAX, -1);
// 现在无需再轮询,在数组中的元素都是满足监听条件的
for (int i = 0; i < nready; ++i)
{
if (!(ep[i].events & EPOLLIN))
continue;
if (ep[i].data.fd == lfd)
{
cfd = accept();
tep.events = EPOLLIN;
tep.data.fd = cfd;
// 把已连接的客户端加入红黑树
res = epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &tep);
}
else
{
sockfd = ep[i].data.fd;
recv(sockfd, buf, sizeof(buf), 0);
int ret = strlen(buf);
if (ret <= 0)
{
// // 删除该节点
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
close(sockfd);
}
else
/*事务处理*/
}
}
}
close(lfd);
return 0;
}
三、epoll高并发服务器的demo
#pragma GCC diagnostic error "-std=c++11"
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <ctype.h>
#include <vector>
using namespace std;
const int OPEN_MAX = 5000;
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc, char **argv)
{
int i, lfd, cfd, efd, res, sockfd;
int n, num = 0;
socklen_t clt_addr_len;
ssize_t nready;
char buf[512];
struct sockaddr_in srv_addr, clt_addr;
// 将地址结构清零(按字节),容易出错(后面两个参数容易颠倒)
// memset(&srv_addr, 0, sizeof(srv_addr));
// bzero也可以用来清零操作
bzero(&srv_addr, sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(8080);
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int opt = 1;
// 设置套接字选项
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 创建套接字
lfd = socket(AF_INET, SOCK_STREAM, 0);
// 绑定套接字
bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
// 监听客户端的连接
listen(lfd, 128);
// 建立epoll模型,efd指向红黑树根节点, OPEN_MAX为监听节点数量(仅供内核参考)
efd = epoll_create(OPEN_MAX);
if (efd < 0)
sys_err("epoll_create");
// tep:epoll_ctl参数(只是一个临时变量,用来设置单个fd属性),ep[]:epoll_wait参数
struct epoll_event tep, ep[OPEN_MAX];
// 指定lfd的监听事件为读事件,文件描述符为lfd
tep.events = EPOLLIN;
tep.data.fd = lfd;
// 添加lfd到红黑树,通过efd可以找到lfd
res = epoll_ctl(efd, EPOLL_CTL_ADD, lfd, &tep);
if (res < 0)
sys_err("epoll_ctl");
while (1)
{
// epoll为server阻塞监听事件,ep为struct epoll_event 类型数组,OPEN_MAX为数组容量, -1表示阻塞
// nready返回的是满足监听事件的总个数(可作为循环上限),如果为0则表示没有fd满足监听事件
nready = epoll_wait(efd, ep, OPEN_MAX, -1);
if (nready < 0)
sys_err("epoll_wait");
// 现在无需再轮询,在数组中的元素都是满足监听条件的
for (int i = 0; i < nready; ++i)
{
if (!(ep[i].events & EPOLLIN))
continue;
if (ep[i].data.fd == lfd)
{
clt_addr_len = sizeof(clt_addr);
// 接收客户端的连接
cfd = accept(lfd, (struct sockaddr *)&clt_addr, &clt_addr_len);
memset(buf, 0, 512);
cout << "客户端连接:" << inet_ntop(AF_INET, &clt_addr.sin_addr.s_addr, buf, sizeof(buf))
<< "," << ntohs(clt_addr.sin_port) << endl;
// 把已连接的客户端加入红黑树
tep.events = EPOLLIN;
tep.data.fd = cfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &tep);
if (res < 0)
sys_err("epoll_ctl");
}
else // 数据读事件
{
sockfd = ep[i].data.fd;
memset(buf, 0, 512);
// 接收来自客户端的数据
recv(sockfd, buf, sizeof(buf), 0);
int ret = strlen(buf);
if (ret < 0)
{
// 收到RST标志
if (errno == ECONNRESET)
{
cout << "连接被重置" << endl;
// 删除该节点
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
if (res < 0)
sys_err("epoll_ctl");
close(sockfd);
}
else
sys_err("read");
}
// 客户端关闭连接了
else if (ret == 0)
{
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
if (res < 0)
sys_err("epoll_ctl");
close(sockfd);
cout << "客户端关闭:" << inet_ntop(AF_INET, &clt_addr.sin_addr.s_addr, buf, sizeof(buf))
<< "," << ntohs(clt_addr.sin_port) << endl;
}
else
{
for (int j = 0; j < ret; ++j)
buf[j] = toupper(buf[j]);
// 回射到客户端
send(sockfd, buf, ret, 0);
// 客户端写到标准输出
write(STDOUT_FILENO, buf, ret);
}
}
}
}
close(lfd);
return 0;
}
四、epoll高并发服务器总结
在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而 epoll事先通过 epoll_ctl() 来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似 callback 的回调机制(软件中断 ),迅速激活这个文件描述符,当进程调用 epoll_wait() 时便得到通知。
【优点】:
- 监视的描述符数量不受限制,它所支持的 FD 上限是最大可以打开文件的数目,这个数字一般远大于 1024,举个例子,在 1GB 内存的机器上大约是 10 万左右,具体数目可以 cat /proc/sys/fs/file-max 察看,一般来说这个数目和系统内存关系很大。select的最大缺点就是进程打开的 fd 是有数量限制的。这对于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案( Apache 就是这样实现的),不过虽然 Linux 上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案;
- I/O 的效率不会随着监视 fd 的数量的增长而下降。select,poll 实现需要自己不断轮询所有 fd 集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而 epoll 其实也需要调用 epoll_wait() 不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait() 中进入睡眠的进程。虽然都要睡眠和交替,但是 select和 poll在“醒着”的时候要遍历整个 fd 集合,而 epoll 在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的 CPU 时间。这就是回调机制带来的性能提升;
- select,poll每次调用都要把 fd 集合从用户态往内核态拷贝一次,而 epoll 只要一次拷贝,这也能节省不少的开销。
【缺点】:
- 不能跨平台。