本文章记录总结学习epoll的几大重点笔记
文章目录
前言
epoll是 Linux内核 为处理大批量 文件描述符 而作了改进的poll,是Linux下多路复用 IO 接口select/poll的增强版本,它能显著提高程序在大量 并发连接 中只有少量活跃的情况下的系统 CPU 利用率。 另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。 epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。。
一、为什么再用户态协议栈实现epoll?
sockfd文件系统关于vfs内核态实现,而对于程序人员来讲,fd属于用户空间,内核无法帮我们去管理,所以需要我们通过epoll来管理。
二、epoll的数据结构
epoll_create 创建的是什么样的结构?
1.所有io的总集合 --rbtree
集合::key——value 可以通过fd找到event(事件)
1> hash 结构
前:数组 后:拉链法或红黑树
fd event
缺点:对空间浪费的比较多,
优点:数量很多的时候查找效率很高
2> 数组 数组过于low 不适用于大量的网络fd管理
3> 红黑树
红黑树的优点:查找效率 空间利用率综合下来比其他优秀
2.就绪队列存储可读可写fd的集合。
1>队列
先进先出 一种队列形式适合对 就绪fd的处理
2>栈
先进后出 ,容易把最低层的栈节点忽略造成饥饿
综合考虑我们采用队列实现,内部用的是双向链表。
注意:红黑树和队列之间的关系
就绪队列是从红黑树 0拷贝取下来的 指针指向同一个地址
程序可能随时调用epoll_ctl添加监视socket,也可能随时删除。当删除时,若该socket已经存放在就绪列表中,它也应该被移除。(事实上,每个epoll_item既是红黑树节点,也是链表节点,删除红黑树节点,自然删除了链表节点)
三.epoll的工作环境
上面讲到,epoll是因为内核无法管理fd才实在在用户态下的,可以得出结论,epoll工作与应用程序和协议栈之间的组件。
而协议栈也就是包括了网络编程函数(connect,listen,accpect,recv,send,close)
四.select/poll和epoll的区别
1 前者要把总计拷贝到内核中,再轮询,返回用户态,
epoll 不用,红黑树的节点和就绪队列节点属于0拷贝。
2 前者实现原理 前者要循环遍历集合,当有fd发生改变时,需要轮询整个集合
epoll基于事件通知,只需要通过epoll_wait从就绪队列中取到相应的fd返回到events集合中即可。
五 .从协议栈如何与epoll 通信,什么时候通信,如何通信
1>epoll通信
sockfd : EPOLLIN EPOLLOUT
在协议栈上 何时有io处理,在accept时刻建议全连接队列,通知epoll
accept 通知epoll epollin
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 再把新的cfd 通过epoll_ctl 加入到epoll实例中 也就是加入到红黑树中
//--------------------EPOLLET 要把cfd设置成非阻塞才可以用
int flag = fcntl(cfd,F_GETFL);
flag |= O_NOBLOCK;
fcntl(cfd,F_SETFL,flag);
epev.events = EPOLLIN | EPOLLET;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
包括还有
recv 通知epoll epollin
send 通知epoll epollout
fin 通知epoll epollin
rst 通知epollerr
--------------------内部底层的数据传递和高效的原因小结------------------
当epoll_wait调用时,仅仅观察这个双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已。
六.如何加锁
epoll_ctr 对红黑树加锁
只允许一个线程 在rbtree.insert()/delete();的时候
epoll_wait对就绪队列加锁
用自旋锁
对于队列添加 避免SMP体系下 多核竞争,采用自旋锁 ,能够快速的操作list
七.epoll的API
1 int epoll_create(int size)
功能:
内核会产生一个epoll
实例数据结构并返回一个文件描述符epfd,这个特殊的描述符就是epoll实例的句柄,后面的两个接口都以它为中心。同时也会创建红黑树和就绪列表,红黑树来管理注册fd,就绪列表来收集所有就绪fd。size参数表示所要监视文件描述符的最大值,不过在后来的Linux版本中已经被弃用(同时,size不要传0,会报invalid
argument错误)
2 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
功能:
将被监听的socket文件描述符添加到红黑树或从红黑树中删除或者对监听事件进行修改;同时向内核中断处理程序注册一个回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数,该回调函数将文件描述符放在就绪链表中
添加
void addfd(int epollfd,int fd,bool one_shot){
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLRDHUP; //对方连接断开 有事件通知 不用对recv的返回值判断 利用底层 而不是用上层处理
//event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
if(one_shot){
event.events |= EPOLLONESHOT;
// 问题?
//eppll 即使使用ET模式,一个socket上的某个事件还是可能被触发多次,
//采用线程城池的方式来处理事件,可能一个socket同时被多个线程处理
// 解决!
//EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,
//如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
}
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);
// lt 模式 非阻塞
setnoblocking(fd);
}
删除
void removefd(int epollfd,int fd){
epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,0);
close(fd);
}
修改
// 修改文件描述符,重置socket上的EPOLLONESHOT事件,以确保下一次可读时,EPOLLIN事件能被触发
void modfd(int epollfd, int fd, int ev) {
epoll_event event;
event.data.fd = fd;
event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
epoll_ctl( epollfd, EPOLL_CTL_MOD, fd, &event );
}
3 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:
阻塞等待注册的事件发生,返回事件的数目,并将触发的事件写入events数组中。
events: 用来记录被触发的events,其大小应该和maxevents一致
maxevents: 返回的events的最大个数处于ready状态的那些文件描述符会被复制进ready
list中,epoll_wait用于向用户进程返回ready list(就绪列表)。events和maxevents两个参数描述一个由用户分配的struct epoll
event数组,调用返回时,内核将就绪列表(双向链表)复制到这个数组中,并将实际复制的个数作为返回值。注意,如果就绪列表比maxevents长,则只能复制前maxevents个成员;反之,则能够完全复制就绪列表。
另外,struct epoll event结构中的events域在这里的解释是:在被监测的文件描述符上实际发生的事件
整体代码思路
epoll_event events[MAX_EPOLL_EVENTS];
int epollfd = epoll_create(1);
// 把 “监听”的fd 添加到epoll中
addfd(epollfd,listenfd,false);
while (true)
{
/* code */
int ready = epoll_wait(epollfd,events,MAX_EPOLL_EVENTS,-1);
for (size_t i = 0; i < ready; i++)
{
//printf("ready = %d\n",ready);
int sockfd = events[i].data.fd;
if(sockfd == listenfd){
//处理连接的操作
}else if (events[i].events & (EPOLLRDHUP |EPOLLHUP | EPOLLERR)){
//异常断开 错误事件处理
}else if (events[i].events & EPOLLIN){
// read operate 判断读成功与否,可以利用线程池添加到就绪队列中
// pool->append(users+sockfd);
}else if (events[i].events & EPOLLOUT){
// write operate 写操作
}
}
}
}
八.ET和LT如何实现的
ET模式
因为ET模式只有从unavailable到available才会触发,ET io操作一次 调用一次回调, 所以
读事件:需要使用while循环读取完,一般是读到EAGAIN,也可以读到返回值小于缓冲区大小;
如果应用层读缓冲区满:那就需要应用层自行标记,解决OS不再通知可读的问题
写事件:需要使用while循环写到EAGAIN,也可以写到返回值小于缓冲区大小
如果应用层写缓冲区空(无内容可写):那就需要应用层自行标记,解决OS不再通知可写的问题。
LT模式
因为LT模式只要available就会触发,io操作一次 调用 直到就绪队列完毕,所以:
读事件:因为一般应用层的逻辑是“来了就能读”,所以一般没有问题,无需while循环读取到EAGAIN;
如果应用层读缓冲区满:就会经常触发,解决方式如下; 写事件:如果没有内容要写,就会经常触发,解决方式如下。
LT经常触发读写事件的解决办法:修改fd的注册事件,或者把fd移出epollfd。
LT模式的优点在于:事件循环处理比较简单,无需关注应用层是否有缓冲或缓冲区是否满,只管上报事件 缺点是:可能经常上报,可能影响性能。
而ET模式较复杂,在编程起来困难,但是所能实现的功能可以根据不同需求来做。
总结 epoll更高效的原因
epoll_ctl注册fd到红黑树拷贝一次,
然后从就绪队列拷贝少量的fd到数组中又是一次拷贝,
整体而言对比select和poll拷贝比较少(因为活跃的比较少,遇到大量活跃的可能性能就比较差了)。
再加上少量轮循就可以处理就绪fd,所以效率非常高。
技术参考
本文部分技术点出处,Linux C/C++服务器直
播视频:推荐免费订阅