0. 为什么要用epoll
既然用到epoll,一定对select和poll有一定的了解。
Select需要与fd_set结构体配合使用,并在用户空间维护一个客户端描述符,且管理句柄时有数目的限制。
Poll解决了句柄数目的限制(链表实现),同时维护一个pollfd结构体的客户端事件的集合。
这来俩性能局限点为:
Select和POLL都会遍历整个集合来确定活跃描述符
与内核交互时会把所有句柄拷贝到内核
注意的是:
服务器性能四大杀手:
1.数据拷贝-> 缓存方案
2.环境切换(线程切换)->单核单线程,多核多线程
3.内存分配->内存池
4.锁竞争->减少锁的使用
Poll每次需要从用户态将所有的句柄复制到内核态,如果以万计的句柄会导致每次都要copy几十几百KB的内存到内核态,非常低效。使用epoll时你只需要调用epoll_ctl事先添加到对应红黑树,真正用epoll_wait时不用传递socket句柄给内核,节省了拷贝开销。
以上此段出自阿里云《epoll全面讲解:从实现到应用》https://www.aliyun.com/jiaocheng/122174.html
Epoll在内核的实现使用了mmap共享内存,红黑树和锁,所以在一定条件下提升机器的性能:
大量链接的/不是所有的句柄都很活跃 条件下使用epoll
1. 为什么要使用非阻塞模式
ET模式需要非阻塞。
为此我们需要知道什么是阻塞模式,非阻塞模式,IO复用模型。
此外,在服务器程序中发生阻塞一般是读写数据和accept等待链接的时候。
以下图和思想,来源于《Unix网络编程 第二版》第一卷 第二部分 第六章 第二节
阻塞模式:
正如原文所说,一开始写的网络编程代码都是阻塞模式,直观一点的意思就是没有用到select/poll,直接使用socket-> sockaddr_in ->bind->listen->while(1)->accept模型的简单回射服务器就是阻塞IO模型应用。此模型的局限是一个线程或者进程只能同时处理一个描述符。
非阻塞模式:
也就是应用层一直检查内核是否准备好数据,直到完成。可以做个简单的实验,就上面说过的回射服务器,直接设置成非租塞,accept会一直返回-1。原因是一直在等待链接,当链接到来读写完数据,再次疯狂返回-1。
IO复用模式:
如图,IO复用其实就是select/poll/epoll这类的函数,它们们帮我们完成了内核的监控,并可以监控多个,当内核某个IO准备好后通知我们,我们在调用。与上面的非阻塞模式配合使用就不会反会-1的错误(当数据准备好后再accept,举例select也就是if (pollfds[0].revents & POLLIN){… accept …})。
我在使用第一次使用epoll时候(就是写这完文档的前一天)使用的是<非阻塞+IO复用+LT模式>,其实LT模式下非阻塞性能不高,但是好写。
之后会改ET。
先放个图。
图片来源图片来源CSDN《epoll EPOLLL、EPOLLET模式与阻塞、非阻塞》https://blog.csdn.net/zxm342698145/article/details/80524331
2. epoll使用(c++,面向过程)
先说一下用epoll和不用IO复用网络服务器编程的区别
首先是阻塞的编程流程(个人总结不是很严谨):
然后就是epoll 的IO复用编程模型:
代码中的体现如下:
int main()
{
/*Socket(AF_INET, SOCK_STREAM, 0),我这里设置了非阻塞模式,下面的accept4也是。*/
int listenfd;
listenfd = Socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); // fei zu se IO fu yong
/*设置服务器的sockaddr_in结构体,IPv4,当前地址,8000端口*/
struct sockaddr_in serveraddr;
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(8000);
/*重连处理*/
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
/*Bind绑定描述符和服务器结构体*/
Bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
/*Listen监听描述符*/
Listen(listenfd, 20);
/*准备客户端sockaddr_in结构体,以及accept的返回值*/
struct sockaddr_in clientaddr;
socklen_t clientlen;
int connfd;
/*准备epoll的epoll_event结构体集,用的是c++的向量,为了方便*/
typedef std::vector<struct epoll_event> EpollList;
/*epoll_create1(EPOLL_CLOEXEC)生成用于处理accept的epoll专用的文件描述符,创建一个epoll的句柄*/
int epollfd;
epollfd = epoll_create1(EPOLL_CLOEXEC);
//Creates a handle to epoll, the size of which tells the kernel how many listeners there are.
/*设置epoll_event结构体监听事件,epoll_event结构体的变量,epfd用于注册事件*/
struct epoll_event epfd;
epfd.data.fd = listenfd;
epfd.events = EPOLLIN/*| EPOLLET */;
/*epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &epfd);
epollfd为epoll_create1返回,epfd为epoll_event结构体监听事件的结构体
epoll的事件注册函数,它不同与select()是在监听事件时(epoll使用epoll_wait监听)告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型*/
epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &epfd);
/*设置epoll_event结构体集的大小*/
EpollList events(16);//You can listen for 16 at first
int nready;//活跃描述符个数
while(1)
{
/*nready = epoll_wait(epollfd, &*events.begin(), static_cast<int>(events.size()), -1);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,
这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1是永久阻塞)。
该函数返回需要处理的事件数目,如返回0表示已超时。*/
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(int i=0; i < nready; ++i)*/
for(int i=0; i < nready; ++i)
{
/*if (events[i].data.fd == listenfd)监听活跃*/
if (events[i].data.fd == listenfd)
{
/*Accept客户端结构体和监听描述符,返回一个客户描述符,accept4比accept定义一个参数*/
clientlen = sizeof(clientaddr);
connfd = Accept4(listenfd, (struct sockaddr*)&clientaddr, &clientlen,
SOCK_NONBLOCK | SOCK_CLOEXEC);// fei zu se IO fu yong
std::cout << connfd << "is come!" << std::endl;
/*有客户访问到来,修改结构体事件,把监听描述符改为客户链接描述符,写入内核*/
epfd.data.fd = connfd;
epfd.events = EPOLLIN/* | EPOLLET*/;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &epfd);
}
/*if (events[i].events & EPOLLIN)客户描述符活跃,客户链接描述符有可读事件*/
else if (events[i].events & EPOLLIN)
{
connfd = events[i].data.fd;//取出链接描述符使用
if (connfd < 0)
{
continue;
}
/*用于读写的准备*/
char buf[100];
bzero(buf, sizeof(buf));
int n;
if ((n = read(connfd, buf, 100)) > 0)
{
std::cout << "::" << connfd <<" Date: ["<< buf <<"]" << std::endl;
write(connfd, buf, n);
}
/*关闭描述符,就是客户断开连接后处理*/
else if (n == 0)
{
std::cout << connfd << "is go" << std::endl;
close(connfd);
epfd = events[i];
epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, &epfd);
}
}
}
}
return 0;
}
3. epoll接口和结构体
Epoll的头文件:
#include <sys/epoll.h>
Epoll的函数接口:
Int epoll_create(int size);
参数size为设置可以连接的多少,老的create函数,实例epoll,现在参数size被忽略,大小取决于内核的处理能力。
Int epoll_create1(int flags);
推荐使用的新版本, flags参数的值为EPOLL_CLOEXEC ;
Int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
事件注册函数
-
epfd:epoll_create返回的实例;
-
op:表示动作
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd; -
fd:监听的文件描述符;
-
event:通知内核的结构体下页说明
Int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
相当于select函数等待事件产生maxevents:通知内核event的大小;timeout:超时时间,-1为永远等待;
EPOLL结构体:
Typedef union epoll_date{
void *ptr;
int fd;
unit32_t u32;
unit64_t u64;
}epoll_data_t
联合体,用户数据变量,一般使用fd文件描述符
Struct epoll_event{
unit32_t events;
epoll_data_t date;
}
events可以是以下几个宏的集合:
**EPOLLIN **:表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个 socket加入到EPOLL队列里
参考博客
博主:lvyilong316
http://blog.chinaunix.net/uid/28541347.html
epoll专栏,共10篇