12. Epoll内部是怎么工作的
Last updated
Was this helpful?
Last updated
Was this helpful?
epoll = event poll, 允许进程同时监控好几个fd(socket也是一种fd), 也就是我们常说的搞多个socket的multiplexing. 相比较于 poll, select 的本质是一个函数, 你调用这个函数, 然后操作系统通过某种方式完成多路复用的功能. 而epoll是一个数据结构, 一种能让你搞多路复用的数据结构.
想在epoll上搞多路复用, 那你得先创建它, 我们通过epoll_create函数创建出来一个躺在内核里的 instance, 并返还一个fd指向这个epoll instance.
好, 现在我们已经有epoll这个数据结构了, 在上面的例子中进程483就创建了这个数据结构, 我们拿到一个fd指向这个instance, 我们叫它 epfd , 接下来我们可以对它展开操作, 比如往上添加, 删除我们想要监控的socket.
通过epoll_ctl (control), 我们可以往上加socket, 在上面的例子中, 我们一次性加了5个socket, 分别叫它 fd1 ~ fd5, 这个socket会被存到一个列表里, 这个列表叫interest list, 代表我们目前对这个5个socket的状态感兴趣, 请epoll监控一下这5个socket.
接下来如果这5个socket谁就绪了, 就会被移到一个叫 ready list的列表中, 代表目前它们状态已经就绪了, 已经是可读了
[epoll_ctl函数: 注册fd]
epoll_ctl函数的参数包含 (epfd: 指定往哪个epoll里添加) , (op: 操作类型添加删除等), (fd: 你要注册的socket), (ev: event事件) 我们举个例子:
如果你传入fd是socket, 你可能想知道这个socket什么时候可以读了, 那么这个时候ev填写 EPOLLIN
或者某种情况下你只想监控这个事件一次, 这个时间出现一次就可以了, 第二次就不要出现了, 这种时候就使用 edge-triggered, ev就填写 EPOLLIN | EPOLLONESHOT
[epoll_wait函数: 取事件]
如果你现在想看看这个epoll里有哪些socket已经就绪了,就用epoll_wait, 这个函数的参数能帮助我们了解epoll_wait是怎么取出事件集合的, 参数有:
epfd: 指定epoll instance
evlist: event-list, 一个链表指针, 你需要预先分配好内存, 然后传进去, 函数就把ready list里的已经就绪的事件往里面写
timeout: 如果设置成0, 不会堵塞无论ready list是否为空都直接返回, 如果设置成-1, 这个函数会一直阻塞到ready list出现一个为可读socket为止.
我们之前曾经说过: epoll是一个内核里的数据结构, fd是一个指向内核文件表的指针, 而返回给你的epfd也是一个fd. 那么现在假设情况如下图所示fd0/fd1是进程A创建的, 分别指向两个不同文件的fd
此时我们创建了一个epoll instance, 返回一个epfd (fd9), 如果此时我们通过epoll_ctl 将fd0添加进了interest list,这种做法的本质, 是我们把fd0的内核表项导入进了interest list, 并由epfd所指代.
换句话说, epfd所指代的数据结构里, 包含fd0的表项, 这种做法的直接后果, 会导致如果此时进程A fork出子进程B, B就能继承到fd9, fd9也能做 epoll_wait , 也能监听fd0, 而且子进程先取到事件, 父进程反而取不到了.
只在注册的时候拷贝一次:
我们总是会把epoll与select/poll放在一起做对比. select的执行过程是一旦有内容已经返回了, 你就需要遍历所有一整个数组的socket对象, 来查找出具体是哪一个好了, 时间复杂度O(N), 而epoll的工作, 如果某个socket已经好了, 它会被自动放进epoll instance的ready list, 不要你指定, 内核自动放进ready list. 并且你通过epoll_wait取出来的时候, 拿出一个链表的都是已经就绪的, 因此时间复杂度是O(1).
时间复杂度低, 不需要遍历:
此外, 因为select是一个函数, 函数的参数是你想要观测的socket对象, 因此每次你调用这个函数的时候都需要吧一组fd的完整信息全都传进去. 而对于epoll, 一个socket只要被注册进interest list, 他就一直出现在epoll instance里, 就可以保持一致观测它, 不需要每次都传进去.
我们上面说的那些特性都是level-triggered, 在这种场景下, 我们只想处理一个socket的读操作. 那么现在设想一个场景, 这些socket产生了某种关联属性: socket1是必读的, socket2/3....选读. 换句话说, 今天你还是给我把已经就绪的挑出来, 但这里面必须包含socket1, 如果没有你就给我卡着, 直到socket1就绪了再一并还给我. 这种情况下, 在使用epoll_ctl注册socket1的时候你就需要指定它的属性为 EPOLLIN | EPOLLET
本文译自: 这里