GRPC浅析-epoll与IOCP
epoll与IOCP
epoll是Linux内核一种I/O事件通知机制,可将大量的文件描述符与事件注册到epoll实例上。内部使用红黑树管理文件描述符与事件。
- 一个file descriptor不可以多次注册到同一个epoll实例,但可以将一个已经注册了的file descriptor通过dup(2),dup2(2),fcntl(2) F_DUPFD来duplicate一个新file descriptor注册到同一个epoll实例
- 一个file descriptor可以注册到多个epoll实例,所有epoll实例都会收到同一的事件通知
- 一般来说close(2)file descriptor,会将file descriptor从epoll实例中移除。如果一个file descriptor存在其他duplicated file descriptor与之关联同一个file description,就算close(2)file descriptor后,也会从epoll实例收到事件通知。
IOCP比epoll更先进些,是一种I/O完成事件通知机制。I/O完成事件按照FIFO被取走,而所有等待通知的线程按照LIFO方式被唤醒。
- 创建一个IOCP实例,内核会维护5个数据结构。分别是设备列表,I/O完成请求队列,等待线程列表,释放线程列表和暂停线程列表
- WSARecv/WSASend用于投递一个I/O请求,完成时会收到一个完成请求事件。比epoll好的地方在于收到完成事件时,不用再read/write。
pollset
pollset是关于多线程并发等待I/O事件并处理I/O事件的过程的抽象。最主要的内容是如何管理多线程以及如何处理I/O事件,在不同平台上实现细节虽然是不一样的,但主体思想是一致的。
通过调用pollset的work接口消费epoll/IOCP产生I/O(完成)事件。调用work接口时,会创建worker对象标识线程已进入work函数体,wroker在work接口调用结束后被销毁。work接口会判断是否存在其他active worker等待I/O事件,如果存在则将线程通过条件变量阻塞起来,如果不存在将相关的worker标记为active worker并等待I/O事件发生。阻塞起来的线程,也许会在active worker结束后、通过pollset的kick接口、超时被唤醒。
目前(20200928),work接口在epoll模型下的实现跟IOCP模型下的实现有一个很大的区别,阻塞起来的线程在其他线程的worker结束后被唤醒后,epoll模型下的work版本,线程会等待I/O事件发生,而IOCP模型下的,线程不会去等待I/O事件。
事件注册
接下来讨论下streaming socket如何跟pollset关联起来。epoll模型下,当监听端口有读事件时,调用accept建立一个tcp连接。IOCP模型下则是提供一个socket给IOCP实例,当有连接建立时,内核将socket与新连接关联起来。
当连接建立时,内部会创建grpc_tcp对象,grpc_endpoint是其第一个成员变量。一个不同的地方,在epoll模型下,内部创建grpc_fd关联grpc_tcp对象。在IOCP模型下,内部创建grpc_winsocket关联grpc_tcp对象。
上层代码通过grpc_endpoint的read/write接口可以发起I/O请求(即向pollset注册一个关联FD的事件),如下。