前言
epoll 和 IOCP(IO Completion Ports) 分别是 Linux 和 Windows 系统上的高效网络模型。相比其他网络模型,同样是 polling 方式,这两种模型有如下特点:
- 在系统资源允许下,监控的文件描述符没有上限。
- 多线程同时监视和修改文件描述符是可行的,少量的线程即可实现支持大量连接的服务端程序。
- 相比其他网络模型,这两种模型的事件通知会有一定开销。如果应用程序不需要太多的客户端连接,
select 和 poll 是更合适的模型。epoll 和 IOCP 的设计目的是应付成千上万(C10K、C10M)的连接并发。
那 epoll 和 IOCP 技术上有什么不同呢?
事件通知方式
第一个不同点是二者的事件通知方式,主要体现在内核帮用户做了多少工作。具体而言:
- 当应用程序收到
epoll 的事件通知时,表示现在用就绪的 IO 可以操作,具体怎么操作由用户来执行(读、写、关闭fd等); - 当收到
IOCP 的通知时,表示在此之前的某个或多个 IO 请求操作已经完成,操作可能成功也可能失败。
使用 epoll 的关键步骤:
- 确定在指定的文件描述符上需要执行哪些操作(读写、监听和接受新连接、断开连接等)。
- 调用
epoll_ctl 设置事件和文件描述符相关掩码。 - 调用
epoll_wait 阻塞当前进程,直到至少一个被监听的事件触发。多个事件同时到达时,该调用返回事件个数(就绪IO)。 - 通过遍历已触发的
epoll_event ,并根据 data union 来索取 IO 上下文。 - 根据
revent 的置位情况,来判断该事件的类型(IO类型:读、写)。 - 针对相应的 IO 事件类型,执行相关操作。注意,同一个文件描述符可能同时有两个 IO 事件触发,应用程序都需要处理。
使用 IOCP 的关键步骤:
- 调用
WIN32 API: CreateIoCompletionPort 初始化完成端口并将监听的文件句柄关联到该完成端口。 - 通过
OVERLAPPED 结构指定的异步 IO 操作完成后,会将一个 IO 完成数据包加到与该完成端口关联的队列中(FIFO)。 - 调用
GetQueuedCompletionStatus 阻塞进程并等待 IO 完成。事实上该调用等待的是 IO 完成数据包被加到上述队列中,而不是等待异步 IO 完成。GetQueuedCompletionStatus 从队列中每次只取一个 IO 完成包,而GetQueuedCompletionStatusEx 可以取出多个 IO 完成包(如果有)。 - 通过
CompletionKey 和 OVERLAPPED 结构获取 IO 上下文。 - 继续执行与完成端口关联的 IO 操作。
小结: 由于 Linux 网络操作不支持异步 IO,所以 epoll 和 IOCP 在通知方式上有一些区别,epoll 的通知发生在数据从内核拷贝到用户空间之前,而 IOCP 通知时 IO 操作已全部完成。epoll 搭配一个线程可以模拟 IOCP 的操作(wine 就是这么做的),然而 IOCP 模拟 epoll 却没有那么容易。 关于异步和同步 IO ,Linux IO 多路复用(select、poll、epoll)这篇文章里有介绍。
数据访问方式
如果需要读写数据,应用程序就需要在某处准备数据缓冲区用来存储读取和发送的数据。然而,在这一点上,两种模型技术上又有不同:
epoll 不关心数据缓冲区,也不用它们。- 但
IOCP 需要,因为 Windows 内核在进行 IO 完成通知前,(读操作)需要将数据从内核拷贝到这些缓冲区。
由于IOCP 作用与异步 IO,被执行的 WSARecv 和 WSASend 不会立即执行完成,所以相关 buffer 需要保留到 IO 操作执行完成,在此期间,buffer 的读写不能被干扰。IOCP 几点重要的约束:
- 为了保证 IO 操作完成时缓冲区有效,不能使用局部变量且存储于栈空间的缓冲区,来进行读写操作。
- 对于写操作,在一次 IO 执行期间,写缓冲区大小不能进行动态缩放,也不能重新申请内存,否则缓冲区可能变得无效。
- 对于起代理作用的应用,最佳实现是维护两份 buffer,因为两个 socket 的 IO 操作有可能重叠,此时两份 buffer 必须独立和完整。
- 当应用程序中有连接管理类时,它可能在任何时候有关闭连接的需要(例如,读写错误、远端连接意外关闭时),此时,相关资源不能被释放,直到所有正在执行的 IO 操作完成通知以后。
此外,每一个 IO 操作,OVERLAPPED 必须独立。
epoll 用不到 IO buffer, 没有上述问题。
修改监听事件
相同点:epoll 和 IOCP 都可以在任何时候添加监听事件(或等待事件)。 不同点:
epoll 可以在任何时候修改或移出已经存在的监听 IO 事件,通过 epoll_ctl 完成。该操作是线程安全的,即使另一个线程正在等待该 IO 事件。IOCP 稍微复杂一点。如果一个 IO 操作正在被系统调度,修改监听事件首先需要调用 CancelIO 停止IO操作,该调用只对当前线程生效,如果使 IO 相关的所有线程生效,调用CancelIOEx 。并且 IO 状态在调用 GetOverlappedResult 之前是不确定的。
其他不同
IOCP 使用微软技术规范 ConnextEx 实现无阻塞连接,而 epoll 的接口和 select/poll 一致(connect )。- 对于一次未读完的数据,
epoll 可以根据 EAGAIN 错误码进行重复读取。但 IOCP 必须重新发起一个 IO 请求,并等待其完成。
总结
epoll 和 IOCP 都是支持高并发的高性能模型。但两者在实现技术上有很大不同,开发人员需要针对不同平台使用不同的处理逻辑和编程风格。一般来讲,IOCP 程序向 epoll 的移植会相对容易。
- IOCP 编程更加复杂, epoll 相对容易。
- 使用 IOCP 时需要注意缓冲区的管理。
了解更多 : https://ke.qq.com/course/417774?flowToken=1041371
|