epoll:
Linux下性能最高的多路转接模型
epoll 有3个相关的系统调用.
epoll_create
功能:创建epoll,在内核中创建eventpoll结构体,size决定了epoll最多监控多少个描述符,在Linux2.6.8之后被忽略,但是必须>0。返回一个文件描述符,作为epoll的操作句柄
struct eventpoll{
...rb_root rbr(红黑树)...struct list_head rdlist(双向链表)...
}
int epoll_create(int size)
创建一个epoll的句柄.
- 自从linux2.6.8之后,size参数是被忽略的.
- 用完之后, 必须调用close()关闭
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
功能:对内核中的eventpoll 结构体进行操作:epoll采用事件结构方式对描述符进行事件监控;用户定义struct epoll_event描述符事件结构信息;将事件信息可以拷贝到内核添加到eventpoll结构体中的红黑数中
参数说明
- 它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
- 第一个参数是epoll_create()的返回值(epoll的句柄).
- 第二个参数表示动作,用三个宏来表示. 、
- 第三个参数是需要监听的fd.
- 第四个参数是告诉内核需要监听什么事. 描述符对应的事件结构信息
第二个参数的取值:
EPOLL_CTL_ADD
:注册新的fd到epfd中;向红黑数中添加描述符的监控事件结构信息eventEPOLL_CTL_MOD
:修改已经注册的fd的监听事件;修改描述符在红黑数中的对应事件结构信息eventEPOLL_CTL_DEL
:从epfd中删除一个fd,从红黑数中移除描述符的监控事件结构信息event
struct epoll_event结构如下
struct epoll_event {
uint32_t events; /* 用户对描述符进行监控的事件 */
epoll_data_t data; /* User data variable */
};typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t;
events可以是以下几个宏的集合:
- EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
- EPOLLOUT : 表示对应的文件描述符可以写;
- EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
- EPOLLERR : 表示对应的文件描述符发生错误;
- EPOLLHUP : 表示对应的文件描述符被挂断;
- EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
- EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要 再次把这个socket加入到EPOLL队列里.
epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
- epfd: epoll 操作句柄
- events:epoll_event 事件结构信息数组的结点数量
- maxevents : epoll_event事件结构信息数组的结点数量
- timeout:epoll_wait 监控的等待超时时间
- 返回值:<0----监控出错 ==0----监控超时 >0----就绪的描述符事件个数
收集在epoll监控的事件中已经发送的事件
- 参数events是分配好的epoll_event结构体数组.
- epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存).
- maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
- 参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞).
- 如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函 数失败
epoll_wait 会将就绪的描述符对应事件结构信息拷贝到events结构数组中;相当于直接告诉用户哪个描述符就绪;用户直接就从epoll_event 结构体数组中取出信息,对描述符直接进行相应事件操作
epoll监控流程
- epoll对描述符的事件监控是一个异步操作;epoll_wait发起调用,让操作系统对描述符进行相应事件监控
- 操作系统对每个要监控的描述符都定义了就绪事件回调函数;当描述符相应事件就绪的时候,触发事件,调用回调函数(将描述符事件结构信息指针添加到eventpoll的双向链表中)
- 但是epoll_wait并没有直接返回(是一个阻塞操作),每隔一会就看一下eventpoll中双向链表是否为空;来判断是否有描述符就绪;若为空,则没有描述符就绪;则等待一会,重新查看
- 若双向链表不为空------表示有描述符事件就绪;将这个描述符对应的事件结构信息,拷贝到epoll_wait传入的事件结构数组中后调用返回。
epoll工作原理
struct eventpoll{
...rb_root rbr(红黑树)...struct list_head rdlist(双向链表)...
}
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成 员与epoll的使用方式密切相关.
- 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来 的事件.
- 这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插 入时间效率是lgn,其中n为树的高度).
- 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时 会调用这个回调方法.
- 这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中.
- 在epoll中,对于每一个事件,都会建立一个epitem结构体.
- 当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem 元素即可.
- 如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度 是O(1).
/* * 这个程序完成epoll接口的基本封装* bool Init()* bool Add(TcpSocket &sock)* bool Del(TcpSocket &sock)* bool Wait(std::vector<TcpSocket>&list,int timeout_msec)*/#include<iostream>
#include<vector>
#include<sys/epoll.h>
#include"tcpsocket.hpp"#define MAX_EVENTS 10
class Epoll
{public:bool Init(){//int epoll_create(int size)_epfd = epoll_create(1);if(_epfd < 0){ perror("epoll create error");return false;} return true;} bool Add(TcpSocket &sock){//int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) int fd = sock.GetFd();struct epoll_event ev; ev.data.fd = fd; ev.events = EPOLLIN;int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd , &ev);if(ret < 0){ perror("epoll add error");return false;} return true;} bool Del(TcpSocket &sock){int fd = sock.GetFd();int ret =epoll_ctl(_epfd, EPOLL_CTL_DEL, fd,NULL);if(ret < 0){perror("epoll del error");return false;}return true;}bool Wait(std::vector<TcpSocket>&list,int timeout_msec = 3000){//int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) struct epoll_event evs[MAX_EVENTS];int nfds = epoll_wait(_epfd, evs, MAX_EVENTS, timeout_msec);if(nfds < 0){perror("epoll wait error");}else if(nfds == 0){std::cerr<<"epoll wait timeout";return false;}for(int i =0 ; i < nfds; i++ ){TcpSocket sock;sock.SetFd(evs[i].data.fd);list.push_back(sock);}return true;}private:int _epfd;};int main()
{TcpSocket lst_sock;CHECK_RET(lst_sock.Socket());CHECK_RET(lst_sock.Bind("0.0.0.0",9000));CHECK_RET(lst_sock.Listen());Epoll epoll;CHECK_RET(epoll.Init());CHECK_RET(epoll.Add(lst_sock));while(1){std::vector<TcpSocket>list;bool ret = epoll.Wait(list);if(ret == false){ continue;}for(int i =0 ;i < list.size();i++){if(lst_sock.GetFd() == list[i].GetFd()){TcpSocket cli_sock;std::string cli_ip;uint16_t cli_port;ret = lst_sock.Accept(cli_sock,cli_ip,cli_port);if(ret == false){continue;}epoll.Add(cli_sock);}else{std::string buf;ret = list[i].Recv(buf);if(ret == false){//接收出错epoll.Del(list[i]);list[i].Close();continue;}std::cout<< "client-say:"<< buf <<std::endl;}}}lst_sock.Close();return 0;
}
epoll事件触发方式:
水平触发–EPOLLT/边缘触发EPOLLET
水平触发方式
可读事件就绪:接受缓冲区数据大小,大于低水位标记(默认1字节)
可写事件就绪:发送缓冲区中空闲大小,大于低水位标记(默认1字节)
只要接受/发送缓冲区中数据/剩余空间大小大于低水平标记就会一直触发事件
边缘触发方式
可读事件就绪:接受缓冲区中,只有新数据到来的时候才会触发一次
可写事件就绪:发送缓冲区中,只有从剩余空间大小从0变为大于0的时候才会触发
注意事项
边缘触发方式中,只有新数据到来的时候,可读事件才会触发一次
需要用户在这一次事件触发中将缓冲区中的数据全部读取完毕(循环读,直到不能读为止)
但是套接字默认recv没有数据的时候会阻塞;为了避免循环读取数据导致程序流程因为阻塞而无法继续推进,因此需要将描述符设置为非阻塞
fcntl
int fcntl(int fd,int cmd, .../*arg*/);fd : 要设置的描述符cmd : 对描述符要进行的操作F_SETFL 通过arg参数设置描述符属性状态F_GETFL 返回描述符的属性状态信息 ,arg被忽略arg:要设置的描述符属性状态信息O_NONBLOCK 将描述符设置为非阻塞
epoll优缺点分析
- epoll采用事件结构方式对描述符进行监控,简化了select集合操作的流程
- epoll描述符监控数量无上限
- 每个epoll监控的描述符事件信息,只需要向内核拷贝一次
- epoll_wait使用异步阻塞操作在内核中完成事件监控;事件监控是操作系统通过事件回调的方式就绪描述符事件信息添加到双向链表中;而epoll_wait只是每隔一段时间看一下双向链表是否为空判断是否有描述符就绪(并非轮询遍历)性能不会随着描述符增多而降低
- epoll直接通过epoll_wait传入的时间结构数组向用户返回就绪的事件信息;可以直接告诉用户哪些描述符就绪,不需要用户进行空遍历查找
缺点
- 不能跨平台
IO多路转接模型的适用场景
对大量描述符进行监控,但是同一时间只有少量描述符活跃的场景