IO多路复用:复用一个线程,处理多个socket中的事件。能够资源复用,防止创建多线程导致的上下文切换的开销。

五种I/O模式

IO模型工作方式特点优点缺点适用场景
阻塞式IO应用程序发起IO请求后,等待IO操作完成简单易用,但会阻塞进程编程模型简单效率低,无法处理高并发简单的应用,IO操作不频繁
非阻塞式IO应用程序发起IO请求后,立即返回,通过轮询检查IO操作是否完成非阻塞,但需要频繁检查可以处理更多的并发请求效率不高,CPU资源浪费需要处理一定并发,但并发量不是特别高
IO复用应用程序使用select/poll/epoll监听多个IO事件,事件发生时通知应用程序可以同时处理多个IO请求高效处理大量并发连接编程复杂,需要管理多个文件描述符高并发服务器,如Web服务器
信号驱动式IO应用程序发起IO请求后,通过信号通知应用程序IO操作完成非阻塞,响应式可以异步处理IO事件编程复杂,信号处理可能引入竞态条件需要异步处理IO事件的场景
异步IO应用程序发起IO请求后,系统完成IO操作后通知应用程序真正的非阻塞,无需轮询或信号编程模型简单,效率高实现复杂,资源消耗大高并发高性能要求的应用,如数据库

selectpollepoll

进程通过一个系统调用函数从内核中获取多个事件。

获取网络事件的方式:在获取事件时,先把所有连接(文件描述符)传给内核,再由内核返回产生了事件的连接,然后在用户态处理这些连接的请求。

特性/技术selectpollepoll
描述传统的多路复用机制,用于监听一组文件描述符select 的改进,用于监听多个文件描述符。Linux 提供的一种 I/O 事件通知机制。
文件描述符限制受限于 FD_SETSIZE 宏定义,默认通常是 1024没有文件描述符数量上的限制。没有文件描述符数量上的限制,适合大规模并发连接。
工作方式轮询文件描述符集合,检查是否有文件描述符就绪。遍历指定的文件描述符数组,检查是否有就绪的 I/O 事件。监视大量的文件描述符,并在文件描述符就绪时触发事件
触发模式仅支持水平触发。仅支持水平触发。支持水平触发(默认)和边缘触发(EPOLLET)。
性能在文件描述符较少时性能可接受,连接数较大时效率下降。性能优于 select,在大量文件描述符上仍可能有性能问题。较高的 I/O 效率,适用于大规模并发连接。
适用场景较小规模的并发连接或简单的客户端程序。中等规模的并发连接。大规模并发连接,如高性能服务器。
优化文件描述符限制 轮询struct_pollfd数组- 事件驱动模型维护事件表 - 支持边缘触发

select/poll

select需要遍历2次文件描述符集合,拷贝2次文件描述符集合。

select多路复用实现方式:将已连接的Socket都放在一个文件描述符集合,然后将文件描述符集合拷贝到内核里,由内核遍历文件描述符检查是否有网络事件,当检查到可读可写后,标记该socket为可读可写,然后再将这个文件描述符集合拷贝到用户态,用户态遍历找到可读可写Socket

select采用固定长度的BisMap表示文件描述符集合,受内核中FD_SETSIZE限制。poll使用动态数组替代,以链表形式来组织。

由于selectpoll都是采用线性结构存储集合,都需要时间遍历找到可读可写的Socket,同时也需要在用户态和内核态间拷贝

epoll

int s = socket(AF_INET, SOCK_STREAM, 0);
 
bind(s, ...);
 
listen(s, ...);
 
/**
 * @brief 创建一个epoll实例,用于I/O多路复用。
 * @param size 可选的初始大小,用于指定epoll实例可以管理的文件描述符数量。
 * @return 返回新创建的epoll文件描述符,或在出错时返回-1。
 */
int epfd = epoll_create(/* size */);
 
/**
 * @brief 将一个socket添加到epoll实例中进行监听。
 * @param epfd epoll文件描述符。
 * @param op 指定要执行的操作,如EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)或EPOLL_CTL_DEL(删除)。
 * @param fd 要监听的socket文件描述符。
 * @param event 指向struct epoll_event的指针,包含监听事件的类型和数据。
 * @return 成功时返回0,失败时返回-1。
 */
epoll_ctl(epfd, EPOLL_CTL_ADD, s, ...);
 
/**
 * @brief 无限循环,持续监听和处理I/O事件。
 */
while(1) {
    /**
     * @brief 等待I/O事件的发生。
     * @param epfd epoll文件描述符。
     * @param events 指向epoll_event数组的指针,用于存储就绪的事件。
     * @param maxevents 指定events数组的最大长度。
     * @param timeout 指定等待事件的最长时间(毫秒),或0表示立即返回,或-1表示无限等待。
     * @return 返回就绪的事件数量,或在出错时返回-1。
     */
    int n = epoll_wait(epfd, ...);
 
    /**
     * @brief 遍历所有就绪的socket,处理它们的I/O事件。
     */
    for(int i = 0; i < n; ++i) {
        // 处理接收到数据的socket
    }
}

epoll通过两个方面优化解决select/poll的问题。

  1. 红黑树保存待检测的文件描述符epoll使用红黑树来跟踪进程所有待检测的文件描述符,通过epoll_ctl()将需要监控的socket加入到内核的红黑树中。
  2. 就绪链表epoll采用事件驱动机制,当事件发生时,通过回调函数将其加入到就绪事件列表,用户通过调用epoll_wait()得到有事件发生的文件描述符个数,不再需要轮询整个集合。

拷贝开销

  • 每次调用select,都需要将文件描述符集合从用户态拷贝到内核态,这个开销在文件描述符很多时会很大。
  • epoll保证了每个文件描述符在整个过程中只会拷贝一次,因此不会有拷贝开销的增加。

遍历开销

  • 每次调用select,需要在内核遍历传递进来的所有文件描述符,即使只有很少的文件描述符就绪。
  • epoll只需要轮询一次文件描述符集合,然后查看就绪链表中有没有就绪的文件描述符即可,避免了遍历所有文件描述符的开销。

文件描述符数量限制

  • select有一个固定的限制,它支持的文件描述符数量较小,默认情况下是1024
  • epoll没有这个限制,它所支持的文件描述符上限取决于系统最大可以打开的文件数目,可使用ulimit -a查看。

epoll 水平与边缘触发

特性/触发模式水平触发(LT)边缘触发(ET)
定义只要文件描述符准备好,就会通知。只在文件描述符状态首次变化时通知一次。
触发条件有数据可读或可写时触发,即使已通知过。仅在状态从“未准备”变为就绪时触发。
通知频率持续通知,直到read所有数据。epoll_wait()只通知一次,需一次性将内核缓冲区数据读取完成。
配置方式默认模式,无需特殊标志。需要设置 EPOLLET 标志。
I/O 方式可以使用阻塞或非阻塞 I/O不知道能读多少数据,所以需要循环读写数据,防止进程阻塞必须使用非阻塞 I/O
应用场景数据量大且需要多次读写的场景。减少系统调用,需要快速响应状态变化且避免错过事件的场景。

零拷贝

传统 IO 的工作方式,从硬盘读取数据,然后再通过网卡向外发送,我们需要进行 4 次上下文切换,和 4 次数据拷贝,其中 2 次数据拷贝发生在内存里的缓冲区和对应的硬件设备之间,这个是由 DMA 完成,另外 2 次则发生在内核态和用户态之间,这个数据搬移工作是由 CPU 完成的。

零拷贝(Zero-copy) 技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。

零拷贝技术的文件传输方式通过一次系统调用sendfile方法合并了磁盘读取和网络发送两个操作,减少了 2 次上下文切换和数据拷贝次数。 只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU2 次都是由 DMA 来搬运。

零拷贝技术可以把文件传输的性能提高至少一倍以上