golang面试中如何回答epoll的相关问题
epoll的网络模型的提问在面试过程中会被经常涉及,下面我来谈一下如果是我应该如何展开这个问题。
一般来说,我的习惯是先从多路复用的概念入手——>再聊Golang中的netpoller——>接着对比一下select/poll——>最后看面试官的兴趣决定是否提触发模式。
1. 多路复用的概念
所谓 I/O 多路复用指的就是 select/poll/epoll 这一系列的多路选择器:支持单一线程同时监听多个文件描述符(I/O 事件),阻塞等待,并在其中某个文件描述符可读写时收到通知。 I/O 复用其实复用的不是 I/O 连接,而是复用线程,让一个 thread of control 能够处理多个连接(I/O 事件)
2. Golang的底层网络模型是基于epoll实现的
Go 是一门跨平台的编程语言,而不同平台针对特定的功能有不同的实现,这当然也包括了 I/O 多路复用技术,比如 Linux 里的 I/O 多路复用有 select、poll 和 epoll,而 freeBSD 或者 MacOS 里则是 kqueue,而 Windows 里则是基于异步 I/O 实现的 iocp,等等;因此,Go 为了实现底层 I/O 多路复用的跨平台,分别基于上述的这些不同平台的系统调用实现了多版本的 netpollers。
Go netpoller 通过在底层对 epoll/kqueue/iocp 的封装,从而实现了使用同步编程模式达到异步执行的效果。总结来说,所有的网络操作都以网络描述符 netFD 为中心实现。netFD 与底层 PollDesc 结构绑定,当在一个 netFD 上读写遇到 EAGAIN 错误时,就将当前 goroutine 存储到这个 netFD 对应的 PollDesc 中,同时调用 gopark 把当前 goroutine 给 park 住,直到这个 netFD 上再次发生读写事件,才将此 goroutine 给 ready 激活重新运行。显然,在底层通知 goroutine 再次发生读写等事件的方式就是 epoll/kqueue/iocp 等事件驱动机制。
Go将多路复用器的操作进行了抽象和适配:
- 将新建多路复用器抽象为了netpollinit()
- 将插入监听事件抽象为了netpollopen()
- 将查询事件抽象为了netpoll()
- 最终返回等待事件的协程列表
同时Network Poller是Runtime中抽象多路复用器的工具,可以自动检测多个Socket的状态,由垃圾回收器周期地驱动。在查询到Socket状态可用时,快速返回成功;在Socket状态不可用时,休眠等待。(详细的信息存储在PollDesc中)
3. epoll本身的机制及与select/poll的对比
- epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn)。而 select/poll 内核里没有类似 epoll 红黑树这种保存所有待检测的 socket 的数据结构,所以 select/poll 每次操作时都传入整个 socket 集合给内核,而 epoll 因为在内核维护了红黑树,可以保存所有待检测的 socket ,所以只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。
- epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。
4. epoll的触发模式 epoll 支持两种事件触发模式,分别是边缘触发和水平触发。
- 边缘触发 使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
- 水平触发 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;