目录
epoll模型
epoll是Linux下多路复用IO接口select/poll的增强版本。
它能显著减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它不会复用文件描述符集合来传递结果而迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合。
另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
epoll除了提供select/poll 那种IO事件的水平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。其中,以有可读数据事件为例,水平触发就是只要缓冲区中有数据,epoll_wait就会响应,就会返回;而边沿触发就是不管缓冲区中是否有数据,只有当有数据传来事件发生时,epoll_wait才会响应返回。
epoll函数
epoll中共有三个函数:
epoll_create
int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
epoll_ctl
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队列里
可以看到,这里调用epoll_ctl时,不仅在第三个参数中需要传入fd,在第四个参数的epoll_event中的data元素也需要传入相同的fd,这是为什么呢?
实际上,这里epoll创建的就是一颗红黑树,而每个插入的文件描述符fd就相当于一个结点,而对于每个结点,都会有一个epoll_event结构体挂在这个结点的旁边,当后面调用epoll_wait时,返回也是返回这个epoll_wait的,也就是说,第一个fd是用来插入结点,而epoll_wait中的fd则是用来返回的,因此应当将二者设置为相同。
epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
程序流程
1.创建套接字,绑定,监听....
2.创建epoll_event 结构体,为epoll函数调用做准备;
3.创建epoll,调用epoll_creat函数,将返回的句柄用epfd保存;
4.为监听套接字lfd设置epoll_event结构体,设置相应的事件及描述符,将lfd插入到epoll树中,调用函数epoll_ctl();
5.进入循环,调用epoll_wait(),epoll_wait监控epoll树上各节点相应事件;
6.epoll_wait返回,遍历传出参数epoll_event结构体数组,如果其中含有lfd对于的结构体,那么就说明有新的连接请求,则调用accept函数,并用connfd保存新连接套接字,然后将connfd相应的epoll_event结构体插入epoll树中;
7.继续遍历传出的epoll_event数组,如果数组中还有其他元素,就说明其相对应的套接字发生了感兴趣事件,由于前面设置的是EPOLLIN,因此这里就表示相应的套接字有数据传来,因此直接对其接收即可。
8.如果read函数返回0,说明客户端关闭,此时就需要将相应的套接字关闭掉,并且将其从epoll树上删除。
水平触发(LT)
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <unordered_map>
using namespace std;
#define SERV_IP "127.1.2.3"
#define SERV_PORT 8888
#define MAX_CONN 1024
int main()
{
sockaddr_in servaddr, clitaddr;
unordered_map<int,sockaddr_in> fdmap; //创建一个map,用来映射文件描述符与其地址信息
epoll_event evt, evts[MAX_CONN]; //创建epoll所需结构体
char buf[1024]; //读写缓冲区
int lfd; //用于监听
int connfd; //连接描述符
int readyfd; //保存epoll_wait返回值
int epfd; //用于保存epoll红黑树句柄
socklen_t addr_len = sizeof(clitaddr);
if ((lfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
cout << "creat socket fault : " << strerror(errno) << endl;
return 0;
}
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = inet_addr(SERV_IP);
if (bind(lfd, (sockaddr *) &servaddr, sizeof(servaddr)) == -1)
{
cout << "bind fault : " << strerror(errno) << endl;
return 0;
}
if (listen(lfd, 128) == -1)
{
cout << "listen fault : " << strerror(errno) << endl;
return 0;
}
epfd = epoll_create(MAX_CONN); //创建epoll树,返回树根的句柄
if (epfd == -1)
{
cout << "epoll creat fault : " << strerror(errno) << endl;
return 0;
}
//初始化evt结构体,对应监听描述符信息,用于向epoll树中加入lfd监听
evt.events = EPOLLIN;
evt.data.fd = lfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &evt) == -1) //将lfd加入epoll树中
{
cout << "epoll add error : " << strerror(errno) << endl;
return 0;
}
cout << "Init Success ! " << endl;
cout << "host ip : " << inet_ntoa(servaddr.sin_addr) << " port : " << ntohs(servaddr.sin_port) << endl;
cout << "Waiting for connections ... " << endl;
while (1)
{
readyfd = epoll_wait(epfd, evts, MAX_CONN, -1);
//执行到这里,说明epoll返回,readyfd表示有多少个文件描述符被监控成功,被监控成功的描述符信息放在evts中
if (readyfd == -1)
{
cout << "epoll fault : " << strerror(errno) << endl;
return 0;
}
for (int i = 0; i < readyfd; i++) //遍历输出参数evts数组
{
if (!(evts[i].events & EPOLLIN))continue;
if (evts[i].data.fd == lfd) //如果输出元素中有lfd,说明有新连接请求
{
connfd = accept(lfd, (sockaddr *) &clitaddr, &addr_len);
if (connfd == -1)
{
cout << "accept fault : " << strerror(errno) << endl;
continue;
}
cout << inet_ntoa(clitaddr.sin_addr) << ":" << ntohs(clitaddr.sin_port) << " connected ... " << endl;
fdmap[connfd] = clitaddr; //将新连接与其地址信息建立映射
//定义新连接connfd对应结构体
evt.events = EPOLLIN;
evt.data.fd = connfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &evt) == -1) //插入树中
{
cout << "epoll add error : " << strerror(errno) << endl;
return 0;
}
}
else //对于除监控描述符以外的文件描述符,如果被返回,说明有数据可读
{
int readstat = read(evts[i].data.fd, buf, sizeof(buf)); //读取数据
//客户端关闭
if (readstat == 0)
{
//删除epoll中的该节点
if (epoll_ctl(epfd, EPOLL_CTL_DEL, evts[i].data.fd, NULL) == -1)
{
cout << "epoll add error : " << strerror(errno) << endl;
return 0;
}
cout << inet_ntoa(fdmap[evts[i].data.fd] .sin_addr) << ":" << ntohs(fdmap[evts[i].data.fd] .sin_port) << " exit ... " << endl;
close(evts[i].data.fd);//关闭相应文件描述符
fdmap.erase(evts[i].data.fd);
}
else
{
cout << "(From " << inet_ntoa(fdmap[evts[i].data.fd].sin_addr) << ":" << ntohs(fdmap[evts[i].data.fd].sin_port) << ")";
for (int j = 0; j < readstat; j++)cout << buf[j];
cout << endl;
for (int j = 0; j < readstat; j++)buf[j] = toupper(buf[j]);
write(evts[i].data.fd, buf, readstat);
}
}
}
}
close(lfd);
return 0;
}
边沿触发(ET)
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <unordered_map>
#include <fcntl.h>
using namespace std;
#define SERV_IP "127.1.2.3"
#define SERV_PORT 8888
#define MAX_CONN 1024
int main()
{
sockaddr_in servaddr, clitaddr;
unordered_map<int,sockaddr_in> fdmap; //创建一个map,用来映射文件描述符与其地址信息
epoll_event evt, evts[MAX_CONN]; //创建epoll所需结构体
char buf[5]; //读写缓冲区 ,将大小设置为5,来测试边沿触发的效果
int lfd; //用于监听
int connfd; //连接描述符
int readyfd; //保存epoll_wait返回值
int epfd; //用于保存epoll红黑树句柄
socklen_t addr_len = sizeof(clitaddr);
if ((lfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
cout << "creat socket fault : " << strerror(errno) << endl;
return 0;
}
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = inet_addr(SERV_IP);
if (bind(lfd, (sockaddr *) &servaddr, sizeof(servaddr)) == -1)
{
cout << "bind fault : " << strerror(errno) << endl;
return 0;
}
if (listen(lfd, 128) == -1)
{
cout << "listen fault : " << strerror(errno) << endl;
return 0;
}
epfd = epoll_create(MAX_CONN); //创建epoll树,返回树根的句柄
if (epfd == -1)
{
cout << "epoll creat fault : " << strerror(errno) << endl;
return 0;
}
//初始化evt结构体,对应监听描述符信息,用于向epoll树中加入lfd监听
evt.events = EPOLLIN | EPOLLET; //设置为ET模式
evt.data.fd = lfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &evt) == -1) //将lfd加入epoll树中
{
cout << "epoll add error : " << strerror(errno) << endl;
return 0;
}
cout << "Init Success ! " << endl;
cout << "host ip : " << inet_ntoa(servaddr.sin_addr) << " port : " << ntohs(servaddr.sin_port) << endl;
cout << "Waiting for connections ... " << endl;
while (1)
{
cout<<"epoll_wait call ! "<<endl;
readyfd = epoll_wait(epfd, evts, MAX_CONN, -1);
cout<<"epoll_wait return ! "<<endl;
//执行到这里,说明epoll返回,readyfd表示有多少个文件描述符被监控成功,被监控成功的描述符信息放在evts中
if (readyfd == -1)
{
cout << "epoll fault : " << strerror(errno) << endl;
return 0;
}
for (int i = 0; i < readyfd; i++) //遍历输出参数evts数组
{
if (!(evts[i].events & EPOLLIN))continue;
if (evts[i].data.fd == lfd) //如果输出元素中有lfd,说明有新连接请求
{
connfd = accept(lfd, (sockaddr *) &clitaddr, &addr_len);
if (connfd == -1)
{
cout << "accept fault : " << strerror(errno) << endl;
continue;
}
cout << inet_ntoa(clitaddr.sin_addr) << ":" << ntohs(clitaddr.sin_port) << " connected ... " << endl;
fdmap[connfd] = clitaddr; //将新连接与其地址信息建立映射
fcntl(connfd,F_SETFL,O_NONBLOCK);
//定义新连接connfd对应结构体,前面已经定义了event,因此这里只需要定义fd
evt.data.fd = connfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &evt) == -1) //插入树中
{
cout << "epoll add error : " << strerror(errno) << endl;
return 0;
}
}
else //对于除监控描述符以外的文件描述符,如果被返回,说明有数据可读
{
int readstat;
while((readstat = read(evts[i].data.fd, buf, sizeof(buf))) >= 0) //循环read,如果返回值大于0,说明有数据可读, =0 说明客户端关闭,小于0说明数据已经读取结束
{
if (readstat == 0)
{
//删除epoll中的该节点
if (epoll_ctl(epfd, EPOLL_CTL_DEL, evts[i].data.fd, NULL) == -1)
{
cout << "epoll add error : " << strerror(errno) << endl;
return 0;
}
cout << inet_ntoa(fdmap[evts[i].data.fd] .sin_addr) << ":" << ntohs(fdmap[evts[i].data.fd] .sin_port) << " exit ... " << endl;
close(evts[i].data.fd);//关闭相应文件描述符
fdmap.erase(evts[i].data.fd);
}
else
{
cout << "(From " << inet_ntoa(fdmap[evts[i].data.fd].sin_addr) << ":" << ntohs(fdmap[evts[i].data.fd].sin_port) << ")";
for (int j = 0; j < readstat; j++)cout << buf[j];
cout << endl;
for (int j = 0; j < readstat; j++)buf[j] = toupper(buf[j]);
write(evts[i].data.fd, buf, readstat);
}
}
}
}
}
close(lfd);
return 0;
}
现在将buf的大小设置为5,实际发送的数据大于5,看看运行结果:
启动服务端与客户端,客户端成功连接:
客户端发送长度大于5的数据:
由结果可知,客户端发送的数据长度是大于read接收缓冲区的长度的,此时由于read是非阻塞的,因此在while的作用下会循环调用4次read直到将数据全部读完后退出循环,此时可以看到,虽然read调用了4次才将数据读完,但是epoll_wait从数据到来到数据读取结束仅仅被调用了一次!同样情况下如果是水平触发的阻塞模式(此时不建议用循环read),那么就会多次调用epoll_wait。
因此,在缓冲区内数据长度大于接收区大小时,利用非阻塞式I/O能够减少epoll_wait函数的调用次数,从而提高效率。
select、poll、epoll对比
| select | poll | epoll | |
| 支持最大连接数 | 1024(x86) or 2048(x64)由内核决定 | 无上限,由硬件决定 | 无上限,由硬件决定 |
| I/O效率 | 每次调用进行线性遍历,时间复杂度为O(N) | 每次调用进行线性遍历,时间复杂度为O(N) | 使用“事件”通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到rdllist里面,这样epoll_wait返回的时候我们就拿到了就绪的fd。时间发复杂度O(1) |
| fd拷贝 | 每次select都拷贝 | 每次poll都拷贝 | 调用epoll_ctl时拷贝进内核并由内核保存,之后每次epoll_wait不拷贝 |
为什么ET模式下,需要将套接字设置为非阻塞式?
LT模式是指只要缓冲区中有数据,epoll_wait就会返回,而在ET模式下,epoll_wait只有当新事件来临时才会返回。举个例子,对于一般的LT,在阻塞模式下,如果设置了read一次读取长度为1K,当对端发来2K的数据,read一次读取完1K后,由于是水平触发,那么epoll_wait会再次返回,然后read去读剩下的1K数据,这样就将2K数据全部读完;
而对于ET模式,那么在第一次读完1K数据后,epoll_wait是不会返回的,要返回也是需要等到下一次对端数据传来,假设下一次对端数据再传2K过来,此时epoll_wait返回,read读到的实际上是上一次剩下来的1K,这显然是不行的。因此,如果read是阻塞的,为了让read一次性读完数据,那么就需要用循环让read读取,循环两次来把2K读完,但是2K读完之后呢?由于read是阻塞的,当缓冲区中没有数据,read就会一直卡在那等待数据,此时read不返回,也就无法调用epoll_wait,如果这个时候一个客户端发起连接请求,不就没人来响应了吗?因此,在ET模式下,是不应该设置套接字为阻塞的。
另一方面,如果设置为非阻塞,此时让read循环读取,当没有数据可读的时候,read并不会阻塞在那里,此时read就会返回-1,表示数据读完了。
可以见得,阻塞与非阻塞,关键就在于read函数是否在无数据可读的时候会立刻返回,如果能返回,那自然就能继续处理其他请求了,而如果不能,那么read就会一直阻塞在原处,而其他连接请求也就无法及时处理了。
因此,对于ET模式,应当将套接字设置为非阻塞式。