深入理解:阻塞IO、非阻塞IO、水平触发与边缘触发

深入理解:阻塞IO、非阻塞IO、水平触发与边缘触发

在网络编程和并发处理中,理解不同的 I/O 模型和事件通知机制至关重要。本文将深入探讨阻塞IO(Blocking IO)、非阻塞IO(Non-Blocking IO)、水平触发(Level Triggering)以及边缘触发(Edge Triggering)这四个核心概念,帮助开发者更好地选择和使用合适的 I/O 模型。

一、阻塞IO(Blocking IO)

定义: 阻塞IO是最简单也是最常见的IO模型。当应用程序发起一个IO操作(例如,读取数据)时,如果数据尚未准备好,操作系统会将该线程或进程 阻塞 起来,直到数据准备就绪并被拷贝到用户空间后,该线程或进程才会继续执行。

工作方式:

  1. 用户进程发起一个读操作。
  2. 操作系统内核检查数据是否准备好。
  3. 如果数据没有准备好,内核会将该进程/线程挂起(阻塞)。
  4. 一旦数据准备好,内核将数据从内核空间拷贝到用户空间。
  5. 内核唤醒被挂起的进程/线程。
  6. 用户进程继续执行,完成IO操作。

优点: 编程模型简单直观,易于理解和实现。

缺点: 在等待IO完成期间,进程或线程会被阻塞,无法执行其他任务。这在高并发场景下会导致大量的线程被阻塞,效率低下。

示例: 默认情况下,socketrecv() 函数就是一个阻塞调用。如果接收缓冲区中没有数据,recv() 会一直等待直到有数据到达。

二、非阻塞IO(Non-Blocking IO)

定义: 非阻塞IO与阻塞IO相反。当应用程序发起一个IO操作时,如果数据尚未准备好,操作系统会立即返回一个错误(通常是 EAGAINEWOULDBLOCK),而不会阻塞该线程或进程。应用程序需要不断地轮询(polling)内核,检查数据是否已经准备好。

工作方式:

  1. 用户进程将 socket 设置为非阻塞模式。
  2. 用户进程发起一个读操作。
  3. 操作系统内核检查数据是否准备好。
  4. 如果数据没有准备好,内核会立即返回一个错误。
  5. 用户进程不会被阻塞,可以继续执行其他任务,并在稍后再次尝试读取数据。
  6. 一旦数据准备好,内核将数据拷贝到用户空间,并且下次用户进程尝试读取时会成功返回。

优点: 进程或线程在等待IO操作完成时不会被阻塞,可以执行其他任务,提高了并发处理能力。

缺点: 需要应用程序不断地轮询内核,检查IO操作是否完成,这会消耗大量的CPU资源,尤其是在大多数轮询都返回"数据未准备好"的情况下。

示例: 可以通过设置 socketO_NONBLOCK 标志将其设置为非阻塞模式。此时,调用 recv() 如果没有数据会立即返回错误。

三、I/O 多路复用(The Need for)

非阻塞IO虽然避免了线程阻塞,但其轮询机制效率低下。为了更高效地处理多个连接的IO事件,出现了I/O多路复用技术,例如 selectpollepoll。这些技术允许一个进程或线程同时监视多个文件描述符(例如,socket),一旦某个或某些文件描述符上的IO事件就绪(例如,有数据可读),内核就会通知应用程序。

在使用 I/O 多路复用时,我们需要关注事件的触发方式,这就是水平触发和边缘触发的概念。

四、水平触发(Level Triggering,LT)

定义: 水平触发是一种事件通知机制。当内核检测到文件描述符上的某个条件满足时(例如,socket 接收缓冲区中有数据可读),就会通知应用程序。 只要该条件持续满足,内核就会重复通知应用程序

工作方式:

  • 当使用 selectpoll 时,如果一个文件描述符就绪(例如,可读),selectpoll 会返回该文件描述符。即使应用程序没有完全读取完所有的数据,下次再次调用 selectpoll 时,如果该文件描述符仍然处于就绪状态(缓冲区中还有数据),它仍然会被报告为就绪。
  • 在使用 epoll 时,如果以水平触发模式注册了一个文件描述符的读事件,只要该文件描述符的读缓冲区中还有数据,epoll_wait 就会一直返回该文件描述符,直到所有数据都被读取完毕。

特点:

  • 可靠性高:只要条件满足,就会一直通知,不容易丢失事件。
  • 处理方式灵活:应用程序可以根据自己的节奏处理数据,不必一次性读取所有数据。
  • 效率相对较低:可能会因为条件持续满足而产生不必要的重复通知。

适用场景: 对数据完整性要求较高,但对实时性要求不是特别苛刻的场景。selectpoll 默认采用水平触发。epoll 默认也采用水平触发,但可以通过设置标志来使用边缘触发。

五、边缘触发(Edge Triggering,ET)

定义: 边缘触发是另一种事件通知机制。当内核检测到文件描述符上的状态 发生变化 时,才会通知应用程序。例如,当 socket 接收到新的数据时,会产生一个读事件的边缘触发。 只有在状态发生变化时才会通知一次

工作方式:

  • 在使用 epoll 并以边缘触发模式注册了一个文件描述符的读事件时,只有当新的数据到达该文件描述符的读缓冲区时,epoll_wait 才会返回该文件描述符。即使缓冲区中仍然有未读取的数据,如果后续没有新的数据到达,epoll_wait 不会再次返回该文件描述符。

特点:

  • 效率高:只有在状态发生变化时才通知,减少了不必要的重复通知。
  • 要求高:应用程序需要及时地处理所有触发的事件,否则可能会丢失后续的事件。对于读事件,需要一次性读取所有可读的数据,对于写事件,需要在可写状态发生变化后尽可能多地写入数据。

适用场景: 对性能要求非常高的场景,需要应用程序能够快速且完整地处理事件。Nginx 等高性能服务器通常会选择使用 epoll 的边缘触发模式。

注意事项: 在使用边缘触发时,需要特别小心,确保在每次事件触发后都能够完全处理所有的数据,避免数据丢失或遗漏。通常会配合使用非阻塞IO,循环读取或写入数据直到返回错误(例如 EAGAIN)。

六、总结与比较

特性 阻塞IO 非阻塞IO 水平触发(LT) 边缘触发(ET)
行为 等待IO完成才返回 立即返回,可能出错或返回部分数据 只要条件满足(例如,有数据),就持续通知 只有在状态发生变化时才通知一次
CPU 消耗 低(等待时不占用) 高(需要轮询) 适中 高(需要及时处理所有事件)
编程复杂度 较高(需要处理错误和轮询) 相对简单 较高(需要确保完整处理事件)
可靠性 取决于轮询策略 高(不易丢失事件) 较高,但需要正确处理,否则可能丢失事件
效率 低(并发处理能力差) 较低(轮询开销) 相对较低(可能重复通知) 较高(减少了重复通知)
常见应用 简单的客户端程序,单线程服务器 需要并发处理但对实时性要求不高的场景 selectpollepoll(默认) epoll(通过设置 EPOLLET 标志)

理解这四种概念对于进行高效的网络编程至关重要。在选择合适的IO模型和触发机制时,需要根据具体的应用场景、性能要求以及编程复杂度进行权衡。例如,对于需要处理大量并发连接且对性能要求极高的服务器,epoll 的边缘触发模式通常是一个不错的选择,但同时也需要开发者具备更高的编程技巧来确保程序的正确性。

相关推荐
想躺在地上晒成地瓜干4 分钟前
树莓派超全系列文档--(17)树莓派配置显示器
linux·树莓派·raspberrypi·raspi-config
Brandon汐26 分钟前
Linux中常用的文件管理命令
linux·运维·服务器
残花月伴34 分钟前
linux详细安装/配置(mysql/nginx/tomcat)
linux·mysql·nginx
Vacancy空白1 小时前
【Ubuntu常用命令】
linux·运维·ubuntu·ssh
老天文学家了2 小时前
课表周视图数据【示例】
linux·运维·服务器
榆榆欸2 小时前
7.从Server到Acceptor,优化Reactor模式的实现
linux·服务器·网络
小麦嵌入式2 小时前
Linux驱动开发实战(九):Linux内核pinctrl_map详解与优势分析
linux·c语言·汇编·驱动开发·stm32·嵌入式硬件·硬件工程
爪娃侠2 小时前
解决wsl2下CentOS 7 的 yum 仓库无法连接问题
linux·运维·centos
fengyehongWorld2 小时前
Linux 随机数据生成
linux·运维
周Echo周3 小时前
5、vim编辑和shell编程【超详细】
java·linux·c++·后端·编辑器·vim