前言
这篇文章读不懂的没关系,可以先收藏一下。笔者准备介绍完epoll和NIO等知识点,然后写一篇JAVA网络IO模型的介绍,这样可以使Java网络IO的知识体系更加地完整和严谨。初学者也可以等看完IO模型介绍的博客之后,再回头看这些博客,会更加有收获。
如果你顺利啃下这篇博客,恭喜你,Nginx、redis和NIO等核心思想已经被你掌握了,可以顺势去拓展自己的理解。否则,只是孤立的看epoll,时间一长会很快忘记的。
当然,这些核心思想,笔者也会在之后的博客慢慢做详细讲解,欢迎关注
epoll是一种I/O事件通知机制,是linux 内核实现IO多路复用的一个实现。
IO多路复用是指,在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。
IO多路复用,以后的博客会有详细讲解。
I/O
输入输出(input/output)的对象可以是文件(file), 网络(socket),进程之间的管道(pipe)。在linux系统中,都用文件描述符(fd)来表示。
事件
通知机制
通知机制,就是当事件发生的时候,则主动通知。通知机制的反面,就是轮询机制。
epoll的通俗解释
结合以上三条,epoll的通俗解释是一种当文件描述符的内核缓冲区非空的时候,发出可读信号进行通知,当写缓冲区不满的时候,发出可写信号通知的机制
epoll的核心是3个API,核心数据结构是:1个红黑树和1个链表
epoll
1. int epoll_create(int size)
功能:
size参数表示所要监视文件描述符的最大值,不过在后来的Linux版本中已经被弃用(同时,size不要传0,会报invalid argument错误)
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
功能:
typedef union epoll_data { void *ptr; /* 指向用户自定义数据 */ int fd; /* 注册的文件描述符 */ uint32_t u32; /* 32-bit integer */ uint64_t u64; /* 64-bit integer */ } epoll_data_t; struct epoll_event { uint32_t events; /* 描述epoll事件 */ epoll_data_t data; /* 见上面的结构体 */ };
对于需要监视的文件描述符集合,epoll_ctl对红黑树进行管理,红黑树中每个成员由描述符值和所要监控的文件描述符指向的文件表项的引用等组成。
op参数说明操作类型:
struct epoll_event结构描述一个文件描述符的epoll行为。在使用epoll_wait函数返回处于ready状态的描述符列表时,
常用的epoll事件描述如下:
3. int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:
处于ready状态的那些文件描述符会被复制进ready list中,epoll_wait用于向用户进程返回ready list。events和maxevents两个参数描述一个由用户分配的struct epoll event数组,调用返回时,内核将ready list复制到这个数组中,并将实际复制的个数作为返回值。注意,如果ready list比maxevents长,则只能复制前maxevents个成员;反之,则能够完全复制ready list。
另外,struct epoll event结构中的events域在这里的解释是:在被监测的文件描述符上实际发生的事件。
参数timeout描述在函数调用中阻塞时间上限,单位是ms:
epoll的两种触发方式
epoll监控多个文件描述符的I/O事件。epoll支持边缘触发(edge trigger,ET)或水平触发(level trigger,LT),通过epoll_wait等待I/O事件,如果当前没有可用的事件则阻塞调用线程。
select和poll只支持LT工作模式,epoll的默认的工作模式是LT模式。
1.水平触发的时机
当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你。如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率。
2.边缘触发的时机
当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。
在ET模式下, 缓冲区从不可读变成可读,会唤醒应用进程,缓冲区数据变少的情况,则不会再唤醒应用进程。
举例1:
举例2:(以脉冲的高低电平为例)
JDK并没有实现边缘触发,Netty重新实现了epoll机制,采用边缘触发方式;另外像Nginx也采用边缘触发。
JDK在Linux已经默认使用epoll方式,但是JDK的epoll采用的是水平触发,而Netty重新实现了epoll机制,采用边缘触发方式,netty epoll transport 暴露了更多的nio没有的配置参数,如 TCP_CORK, SO_REUSEADDR等等;另外像Nginx也采用边缘触发。
1. 用户态将文件描述符传入内核的方式
2. 内核态检测文件描述符读写状态的方式
3. 找到就绪的文件描述符并传递给用户态的方式
4. 重复监听的处理方式
epoll更高效的原因
虽然epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
更多内容,欢迎关注微信公众号:全菜工程师小辉~