epoll是select、poll 的改进版。
使用select、poll的缺点:
(1)调用select 时,需要将用户空间的所有fd集合拷贝进内核空间。
(2)调用select 时,需要在内核空间遍历所有fd的状态。
(3)select 支持的fd 数目有限,不超过1024。
关于epoll的三个系统调用:epoll_create、epoll_ctl、epoll_wait:
#include
int epoll_create ( int size );
功能:创建一个文件描述符作为“监听的一大堆fd”的标识。
返回值:返回文件描述符epollfd。注意:使用完epoll后记得close(epollfd)。
参数介绍:这里的size是早期设计时产生的,那时所有需监测的文件描述符放入hash表中,而现在时放入红黑树中,所以该参数现在并没有实际用到,但是必须 >0。
#include
int epoll_ctl ( int epfd, int op, int fd, struct epoll_event *event );
功能:将需监听的fd添加到epfd对应的红黑树上,其中形参中指定:(1)对fd的操作类型:是删除还是增加。(2)监听fd的哪些事件:读事件还是写事件。(3)监听事件的方式:是水平触发还是边沿触发。
返回值:成功返回0,不成功返回-1。
参数介绍:
epfd:epoll_create 的返回值。
op:操作方式。有三种:(1)向事件表中注事件,EPOLL_CTL_ADD。(2)修改fd上事件,EPOLL_CTL_MOD。(3)删除fd上事件,EPOLL_CTL_DEL。
fd:要操作的文件描述符。
event:指定事件。介绍epoll_event:
struct epoll_event
{
__unit32_t events; // epoll事件
epoll_data_t data; // 用户数据
};
介绍epoll_event 的两个成员:
events:
EPOLLIN : 表示监听对应文件描述符,读事件(包括对端SOCKET正常关闭);
EPOLLOUT:表示监听对应文件描述符,写事件;
EPOLLPRI: 表示监听对应文件描述符,紧急数据可读事件(这里应该表示有带外数据到来);
EPOLLERR: 表示监听文件描述符,发生错误事件;
EPOLLHUP:表示监听文件描述符,被挂断事件;
EPOLLET: 将epoll设为边沿触发(Edge Triggered)模式,该参数缺省状态为水平触发LT。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socketfd的话,需重新把这个socketfd加入到EPOLL队列里。
data:在实际应用中多只会用到fd。
typedef union epoll_data
{
void* ptr; //fd相关用户数据
int fd; //事件从属的文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
epoll的水平触发和边沿触发:
(1)水平触发LT:接受缓冲区不为空,对应fd一直处于“读就绪”状态;发送缓冲区不满,对应fd一直处于“写就绪”状态(关于这里的缓冲区:读多少就少多少,写多少就多多少)。所以水平触发,只要数据没读完,那么在epoll_wait中,就一直认为该fd处于就绪状态。
(2)边沿触发ET:当fd对应的读缓冲区从”有“变为了”无“时,对应fd才处于”读就绪“。当fd对应的写缓冲区从”无“变为了”有“时,对应fd才处于”写就绪“。注意:一定是在”有“和”无“之间变化才能触发。
上述是自己的总结。想通过代码了解到两种方式不同的,推荐大牛博客:https://www.cnblogs.com/lojunren/p/3856290.html
epoll中的event什么时候移除:不处于就绪状态的fd,就会被移除,根据 LT/ET 而不同。
#include
int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );
功能:发现并获得”就绪“状态的fd。
返回值:成功,返回就绪文件描述符个数。失败返回-1,并设置errno(使用errno需#include
参数介绍:
(1) epfd:epoll_create创建的epollfd。
(2) events:一个数组,若检测到某fd处于就绪状态,将事件从内核事件表复制到该数组。
(3) maxevents:指定最多监听多少事件。
(4) 坑一:timeout:epoll超时时间,单位毫秒。为-1表示阻塞,为0表示非阻塞,其他值表示超时时间。这里一定要注意,程序涉猎多线程和多进程时,这个timeout的设置初学时也容易出错。我们来说明以下几种设置方式:
1. 设置为-1,程序阻塞在此,后续任务没法执行。
2. 设置为0,程序能继续跑,但即使没事件时,程序也在空转,十分占用cpu时间片,我测试时每个进程都是60+%的cpu占用时间。
3. 综上,我们给出比较好的设置方法:将其设置为1,但还没完,因为即使这样设置,处理其它任务时,
在每次循环都会在这浪费1ms的阻塞时间,多次循环后性能损失就比较明显了。为了避免该现象,我们通常
向epoll再添加一个fd,我们有其它任务要执行时直接向该fd随便写入一个字节,将epoll唤醒从而跳过
阻塞时间。没任务时epoll超过阻塞时间1ms也会自动挂起,不会占用cpu,两全其美。
坑二:在多进程中,epoll的创建和添加,最好放在同一进程中进行。本人就因此遇上了问题:
坑的创建:父进程创建epoll,子进程有4步操作:(1) socket;(2) 设置reuseport避免“惊群”;(3) 将socket_fd添加进epoll;(4) 子进程通过epoll_wait等待然后accept;这样做是错误的,运行程序会报错:Resource temporarily unavailable。
产生坑的原因:多个子进程socket产生的socket_fd是相同的数值,但表示不同的套接字(详见“网络编程”),所以实际上加入父进程epoll的,应该只是某一子进程的套接字,如果有客户端来connect,由于设置了reuseport,只有一个子进程来处理该事件,调用accept,至于能否accept成功取决于:添加入epoll的套接字是否是该子进程创建的套接字,那么只有 1/(进程数) 的概率accept 成功。
综上述:epoll的三个系统调用最好在同一进程内使用,会方便很多。
注:accept本身已经有效避免“惊群”了,引起“惊群”的是:epoll/select/poll等。