type
status
date
slug
summary
tags
category
icon
password
 
前面几章我们完成了[基础篇]的学习,学习了 sockets 的基础 API。从这一章开始,我们就正式进入[性能篇]的学习。
网络程序很多时候都是在处理网络 I/O 事件。因此如何感知 I/O 事件,就成了网络编程的重中之重。在 Linux 系统中,我们有五种方式来感知 I/O 事件,也就是五种 I/O 模型。

1. The Big Picture——I/O 模型

这五种 I/O 模型分别是:
  • 阻塞 I/O
  • 非阻塞 I/O
  • I/O 多路复用(selectpollepoll
  • 信号驱动 I/O(SIGIO
  • 异步 I/O(POSIX aio_等函数)
在具体讲这五种 I/O 模型之前,我们先来看一下读数据操作必须经历的两个阶段:
  1. 等待数据就绪(可能很漫长)
  1. 讲数据从内核缓冲区拷贝到应用程序的缓冲区。

1.1 阻塞 I/O 模型

阻塞 I/O 是最常见的,默认情况下所有的套接字都是阻塞的。到目前为止,我们用得都是阻塞 I/O。以recvfrom()为例,其模型如下图所示:
notion image
在等待数据以及拷贝数据的过程中,应用程序都处于阻塞状态。

1.2 非阻塞 I/O 模型

非阻塞 I/O 和阻塞 I/O 的区别是,如果数据没有就绪,非阻塞 I/O 会立即返回(不会陷入阻塞),并返回错误EAGAINEWOULDBLOCK
notion image
应用程序不断询问内核数据是否就绪,这种方式我们称之为轮询(polling)。轮询的方式非常消耗 CPU 资源。

1.3 I/O 多路复用

当使用 I/O 多路复用时,我们可能会阻塞在selectpollepoll等系统调用,而不会阻塞在真正的 I/O 读写操作上。
notion image
I/O 多路复用强大的地方在于,它可以一次性监视很多个文件描述符。只要其中有一个文件描述符就绪,select等函数就会返回。接下来的 I/O 读写操作就不会阻塞了。
💬
阻塞 I/O + 多线程 阻塞 I/O + 多线程的方式,非常类似 I/O 多路复用。一个线程负责一个文件描述符的读写操作,就算这个线程阻塞了,其它线程依然可以正常工作。不过这种方式会消耗比较多的资源。

1.4 信号驱动 I/O(了解)

我们也可以使用SIGIO信号,告诉内核:当 I/O 事件就绪时就通知应用程序,然后由应用程序去处理 I/O 事件。这种模型我们称之为信号驱动 I/O 模型。
notion image
💬
信号驱动 I/O 一般用得很少,因为它引入了复杂的信号机制,增加了程序的不确定性。

1.5 异步 I/O(了解)

异步 I/O 和信号驱动 I/O 不同。异步 I/O 会让内核拷贝数据,当数据拷贝完成后再通知应用程序;而信号驱动 I/O,仅仅是让内核通知数据就绪,然后由应用程序自己拷贝数据。
notion image
📢
Linux 对异步 I/O 的支持 aio_系列函数是 POSIX 定义的异步 I/O 接口。遗憾的是,Linux 下的aio_操作并不是操作系统级别的,它是 GNU libc 在用户空间借由 pthread 库实现的;而且它仅仅支持磁盘 I/O,不支持套接字 I/O。 这也是为什么在 Linux 平台上,我们一般都是使用epoll+非阻塞 I/O 来构建高性能高并发网络程序的根本原因。

1.6 五种 I/O 模型对比

如下图所示,前四种 I/O 模型都需要应用程序自己拷贝数据(同步 I/O),它们的不同之处仅在于等待数据阶段。而异步 I/O 和前四种都不同,等待数据和拷贝数据都由内核完成,应用程序在这两个阶段都不会阻塞。
notion image

2. select()

接下来,我们重点学习一下 I/O 多路复用模型。首先,我们来看一下select()函数:
参数说明
nfds: 它的值应该设置为三个集合中,最大的文件描述符加 1。(目的是告诉内核,我们监听文件描述符的上限)。
readfds: 读集合。传入的时候,集合中是所有要监视读事件的文件描述符;传出的时候,集合中是所有读事件已经就绪的文件描述符。
writefds: 写集合。传入的时候,集合中是所有要监视写事件的文件描述符;传出的时候,集合中是所有写事件已经就绪的文件描述符。
exceptfds: 异常集合。传入的时候,集合中是所有要监视异常事件的文件描述符;传出的时候,集合中是所有有异常事件发生的文件描述符。
timeout: 超时时间,精度为微秒。传入的时候,表示超时时间;传出的时候,表示还剩多少时间。如果timeoutNULL,表示无限期阻塞;如果timeout的两个字段都为 0,则表示立马返回,不阻塞。
fd_set几乎都是用位图实现的,并且我们提供了四个宏函数用于操作这些集合:
好,到这儿select()的理论部分,我们就学习完了。接下来,我们来构建一个经典的聊天室案例,来看一下在实际应用中,是如何使用select()的。

3. 经典案例——聊天室

这是一个多人聊天室服务器:
  • 准许多人同时进入到聊天室,也允许任何人随时退出聊天室。
  • 发送的消息会同时转发给聊天室的其他成员。

归纳总结

这一章,我们讲了以下内容:
  • 五种 I/O 模型——重点理解阻塞 I/O,非阻塞 I/O,以及 I/O 多路复用。
  • select()——select是 I/O 多路复用的一种方式,也是本章的重点。几乎各个平台都支持select()函数。
  • 最后,我们构建了经典的聊天室案例。通过这个案列,希望大家掌握select()的用法,以及 I/O 多路复用的使用场景。

参考文章

  • UNIX Network Programming, Volume 1: The Sockets Networking API, 3rd Edition
 
💡
欢迎您在底部评论区留言,我们一起交流学习~
高性能网络编程(7)——I/O 多路复用之 poll高性能网络编程(5)——UDP 编程