目录
在前面的高级IO(多路转接)中,我们看了 select 和 poll 但是前面两个多路转接都不是很好:
1.select 需要大量的遍历
2.select 需要维护第三方数组
3.select 还会有大量的拷贝,从内核到用户,从用户到内核
4.select 还有上限
5.poll 解决了有上限的问题
6.poll也不需要维护第三方数组,但是poll 还是会有拷贝,从内核到用户,同时也需要遍历
以上就是前面学习的多路转接的缺点的一个小复习!正是因为 select 和 poll 有这么多缺点,所以我们需要学些 epoll 来解决以上的缺点。
我们现在已经明白什么是多路转接了,下面我们打算:
1.直接看 epoll 的系统接口
2.理解 epoll 模型
3.理解 epoll 接口与 epoll 模型之间的关系
4.说明ET模式与LT模式
epoll 多路转接一共有三个接口,不像 select 和 poll 都只有一个,虽然 epoll 有三个接口,但是这三个接口都并不复杂:
1.第一个接口就是创建一个 epoll 模型
2.第二个接口就是关于对 epoll 模型的控制(增删改)
3.第三个接口就是等待
NAME
? ? ? epoll_create, epoll_create1 - open an epoll file descriptor
?
SYNOPSIS
? ? ? #include <sys/epoll.h>
?
? ? ? int epoll_create(int size);
该函数作用就是创建一个 epoll 模型
这个函数有一个参数,但是这个参数现在没用了,所以这个参数就是随便设置一个值即可!
该函数的返回值是一个文件描述符,也就是关于 epoll 模型的文件描述符,后面的函数都需要用到这个返回值
NAME
? ? ? epoll_ctl - control interface for an epoll descriptor
?
SYNOPSIS
? ? ? #include <sys/epoll.h>
?
? ? ? int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
该函数就是对 epoll 的控制,其中控制有三种:
EPOLL_CTL_ADD:这个就是添加
EPOLL_CTL_MOD:这个就是修改
EPOLL_CTL_DEL :这个就是删除
第一个参数就是 epoll_create 的返回值,其中也是一个文件描述符
第二个参数就是如何控制,也就是上面的三种
第四个参数就是表示要控制哪一个文件描述符
最后一个参数是一个结构体,其中里面就包含了我们需要告诉参佐系统帮我们关心那个文件描述符的哪种事件
? ? ? ? ? typedef union epoll_data {
? ? ? ? ? ? ? void ? ? ? ?*ptr;
? ? ? ? ? ? ? int ? ? ? ? ?fd;
? ? ? ? ? ? ? uint32_t ? ? u32;
? ? ? ? ? ? ? uint64_t ? ? u64;
? ? ? ? ? } epoll_data_t;
?
? ? ? ? ? struct epoll_event {
? ? ? ? ? ? ? uint32_t ? ? events; ? ? ?/* Epoll events */
? ? ? ? ? ? ? epoll_data_t data; ? ? ? ?/* User data variable */
? ? ? ? ? };
struct epoll_event 这个结构体就是上面的参数
这个结构体里面有两个属性:
uint32_t 类型的 events 就是表示我们要告诉操作系统帮我们关心的事件
第二个又是一个结构体,这个表示我们关心的文件描述符,但是这个结构体里面有多种属性,我们只需要挑一个使用即可,一般我们都挑 fd
NAME
? ? ? epoll_wait, epoll_pwait - wait for an I/O event on an epoll file descriptor
?
SYNOPSIS
? ? ? #include <sys/epoll.h>
?
? ? ? int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
该函数就是等待,也就是将事件到达的文件描述符返回给用户
第一个参数就是 epoll_create 的返回值
第二个参数就是一个struct epoll_event 的结构体的数组,也就是用来存放事件到达后的文件描述符和对应的事件
第三个参数就是第二个数组的最大值
最后一个参数就是超时时间,超时时间和之前都是一样的
既然系统调用接口我们已经看完了,那么我们就可以看一下epoll模型,那么什么是 epoll 模型呢?
其实多路转接是系统帮我们提供的一个功能,所以就需要系统帮我们实现!
epoll 模型中,首先我们要想清楚,当我们读取一个文件描述符的时候,如果里面没有数据,那么此时会怎么样呢?如果以阻塞形式读取那么就会阻塞,所以多路转接就是系统帮我们等待文件描述符,当文件描述符对应的事件好了,系统就会通知我们。
那么系统是怎么知道文件描述符中有数据了呢? 假设现在是 0 号文件描述符,那么 0 号文件描述符有数据了,系统怎么知道的?因为我们平时也是进程打字,我们发现一旦输入,屏幕上就给我们回显出来了,操作系统怎么知道呢? 如果操作系统想要知道,那么操作系统有几种方法: 1.操作系统自己定时去检查一下,但是这种方法太麻烦了,还需要操作系统检查,如果文件描述符特别多的话,那么操作系统就需要一直检查 2.当对应的硬件有数据了,自己告诉操作系统,其实就是这种方法
也就是当对应的硬件有数据,那么会通过中断告诉操作系统,然后操作系统回来取数据,那么如果是网络的话,那么就是网卡里面有数据了,然后网卡通过中断来通知操作系统,然后操作系统会将数据取走。
现在操作系统已经知道那个文件描述符里面有数据了,那么操作系统还需要做到什么才可以完成多路转接呢?
其实在epoll模型中,我们告诉了操作系统,帮我们关心文件描述符对应的事件,那么epoll中会有一个红黑树来帮我们关心,也就是一颗红黑树在管理这些文件描述符!
操作系统还帮我们创建一个队列,这个队列里面放的就是事件到达的文件描述符,那么操作系统需要怎么帮我们将事件到达的文件描述符放到队列中呢?需要操作系统遍历吗?不需要,因为当对应的文件描述符有数据了,那么硬件就会自己通知操作系统,然后将数据放到队列中。
那么既然是队列中放的是事件到达的文件描述符,那么用户想要知道有没有文件描述符到达的时候,只需要O(1) 的时间复杂度就可以知道是否有文件描述符到达!
基本就是上图这样。
那么 epoll 的接口与 epoll 模型之间是怎么对应的呢?这三个函数分别会做什么工作呢?
epoll_create: 这个函数就是创建一个 epoll 模型,而上面这一整个就是 epoll 模型,所以 epoll_create 这个函数的作用就是创建上面的这个 epoll 模型!
epoll_ctl: 这个函数就是增删改对文件描述符事件的关心,而这个模型中,系统怎么知道该文件描述符是否是被关心的呢?我们前面说了,当我们有关心的文件描述符都会放到epoll 模型中的红黑树中,也就是这个函数就是对红黑树的增删改!
epoll_wait: 该函数就是将事件到达的文件描述符拷贝给用户,而事件到达的文件描述符在哪呢?在上面的队列中,也就是说这个函数就是检测这个队列,看是否有文件描述符被放到这个队列中,如果有的话,那么就可以返回将里面的事件拷贝给用户,用户也就拿到了好了的文件描述符。
epoll 说了这么多,那么 epoll 解决了上面的问题了吗?其实解决了! 首先是 select 有上限的问题, epoll 模型中是通过红黑树来管理的,那么有上限吗?没有上限,和主机的配置有关! select 的需要大量遍历的问题,在用户层只需要将文件描述符设置进去一次,那么除非我们修改或者删除,否则操作系统帮我们一直关心,其中操作系统关心的时候,操作系统会进行自己遍历来关心吗?不会,因为硬件有数据后会自动通知操作系统,所以操作系统不需要关心,那么用户需要大量遍历吗?也是不需要的,因为 epoll_wait 的返回值表示的是好了几个文件描述符,而好了的文件描述符放到了 epoll_wait 的第二个参数中了,而且是按序放的,所以只需要遍历返回值个就可以了。
大量拷贝的问题解决了吗?在 epoll 中其实也是解决了,因为用户只需要拷贝一次给系统,而系统也是当有数据后才会拷贝给用户,所以也是解决了大量用户到内核的数据拷贝!
不知道还记不记得,在 select 和 poll 中,当有一个事件到达的时候,系统就会一直通知我们有事件到达,而一直通知就是LT模式!
LT模式:水平触发 ET模式:边缘触发
那么水平触发我们知道是怎么回事,那么边缘触发是什么样子呢?边缘触发就是当事件到达时候,只会通知一次,这个就是边缘触发!
那么边缘触发和水平触发这两种模式有什么区别呢? 因为水平触发是一旦有数据,如果我们不取走,那么就会一直提示有数据到达,所以水平触发写起来比较简单,如果当数据没有取干净,那么下一次还是会通知,所以就不需要担心数据没有取走的问题。
边缘触发就是只会通知一次,所以如果有数据没有取走,那么下一次就不会通知了,所以数据就可能会丢失。
那么听起来时水平触发更好,那么为什么要学习边缘触发呢?因为在相同的时间里,操作系统的通知是有上限的,如果是水平触发,那么可能会大量的时间被浪费重复的通知同一个人,而边缘触发只会通知一次,所以就可以高效的通知人。
为了更好的理解ET与LT模式,下面我们将一个故事:
现在你买了一堆快递,张三现在在派发你的快递,那你呢此时正在打游戏,张三就给你打电话说你的快递到了,让你下去取快递,但是你正在打游戏抽不开身,所以你就说好的,但是你并没有下去取快递,张三等了两分钟,看见你没有下来,此时就又给你打电话,但是你还是没有下去,张三害怕你不知道你快递到了,所以就一直打电话催你取快递,此时张三就是水平触发(LT模式)。
第二天呢,你的快递又到了,但是今天送快递的是李四,李四看到你的快递就打电话给你说,你快递到了,下来取,然后你此时还是在打游戏,你没有下去取,但是李四看到你没有下去,李四也不管,李四心里想,我可通知你了,你自己不来的,可不怪我,但是呢,当李四刚好要走的时候,张三看到李四在送你的快递,就把自己手里昨天美送完的快递给李四,,让李四送了,此时李四手里你的快递增加了,此时李四又看到快递了,给你打电话,让你下来取,李四就是每次当你的快递,从 无到有,或者从有到多的时候,才会通知一下,否则通知了就不管了。
上面就是水平触发与边缘触发,那么我们可以总结一下两种模式:
水平触发:当是LT、模式的时候,当有数据到达了,我们可以暂时不取数据,因为下一次还是会通知我们取走快递,但是如果我们不取的话,那么就会一直通知。
边缘触发:是ET模式,如果是边缘触发的话,有数据到了,我们可以不取或者只取一部分吗?不可以,因为如果我们只取一部分,那么可能导致数据丢失,所以我们必须一次性全部取走。
那么水平触发我们可以一次性全部取走吗?可以的,如果水平触发一次性将数据全部取走,那么其实就是和边缘触发一样了。