【Linux网络】五种IO模型与阻塞IO
IO
在Linux网络环境里,IO(Input/Output)指的是网络数据在系统与外部网络(像其他设备、服务器或者客户端)之间进行传输的过程。
它是网络编程和系统性能优化的核心内容。
- IO :INPUT和OUTPUT(站在进程角度)
- 关于IO,read和write都是阻塞式的IO
- 网络传输数据问题,本质上就是IO问题
- 如何更好的理解IO问题,就需要更好的理解什么叫做高效的IO?
- 首先明确,任何通信场景其IO通信场景,效率上一定是会有上限的 。
- 抽象来说:IO=等+拷贝
- 在拷贝的角度来说,我们需要充分利用硬件资源以及网络资源
- 在“等”的角度来说,我们需要减少IO中“等”的权重
网络IO的核心流程
网络IO包含两个关键阶段:
- 数据就绪:数据从网络传输到网卡,再由网卡传至内核缓冲区。
- 数据拷贝:数据从内核缓冲区复制到用户空间的应用程序。
网络IO模型
Linux支持多种网络IO模型,它们的主要差异在于如何处理这两个阶段(阻塞、非阻塞、同步、异步)。
常见的网络IO模型有:
- 阻塞IO(Blocking IO)
- 非阻塞IO(Non-blocking IO)
- IO多路复用(IO Multiplexing)
- 信号驱动IO(Signal-driven IO)
- 异步IO(Asynchronous IO)
注意:
前四种IO模型都属于同步IO,等的方式不同,但还是自己在深度参与IO过程,只要是自己参与了IO过程,就是同步IO
钓鱼例子
- 张三:眼睛盯着鱼漂,鱼漂动了,钓鱼
- 阻塞IO
- 李四 :查看鱼漂是否动了,如果没有,就看手机刷抖音,期间不断查看鱼漂是否动了
- 非阻塞IO(轮巡)
- 王五 : 带来一车鱼竿插在岸边,来回检测哪一个鱼竿上的鱼漂动了
- IO多路复用
- 赵六 :在鱼漂上系上一个铃铛,开始钓鱼后刷抖音,直到铃铛响起,开始钓上鱼。
- 信号驱动IO
- 田七:我雇佣一个人给我钓鱼,钓到的鱼给我。
- 异步IO
上述例子中:
- 鱼竿:文件描述符
- 钓鱼佬:进程
1. 阻塞IO(最基础的模型)
- 工作方式:应用程序调用系统调用后会被阻塞,一直等到数据就绪并被复制到用户空间,或者出现错误时才会返回。所有的套接字,默认
都是阻塞方式 - 特点:编程逻辑简单,但同一时间只能处理一个连接,容易造成性能瓶颈。
2. 非阻塞IO
- 工作方式:应用程序调用系统调用后会立即返回。如果数据未就绪,会返回
EWOULDBLOCK
错误。应用程序需要不断轮询来检查数据是否就绪。 - 特点:非阻塞IO能够避免线程长时间阻塞,但往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询.这对CPU来说是较大的浪费,一般只有特定场景下才使用.
3. IO多路复用(高性能服务器的核心)
- 工作方式:借助
select
、poll
、epoll
等系统调用,一个进程可以同时监视多个文件描述符(FD)。当某个FD的数据就绪时,就会通知应用程序进行处理。 - 优势:能够用单线程处理大量并发连接,显著减少了系统开销。
虽然从流程图上看起来和阻塞IO类似.实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态.
4. 信号驱动IO
- 工作方式:应用程序先通过
sigaction
注册信号处理函数,然后继续执行其他任务。当数据就绪时,内核会发送SIGIO
信号,应用程序在信号处理函数中进行数据读取操作。 - 特点:属于异步通知模式,但数据拷贝阶段仍然是同步的。
5. 异步IO(真正的异步模型)
- 工作方式:应用程序通过
aio_read
等接口发起异步IO请求,然后继续执行后续操作。内核会自动完成数据的读取和拷贝工作,完成后通过回调函数或者信号通知应用程序。 - 特点:整个IO过程都是异步的,极大地提高了系统的并发处理能力。
小结: - 任何IO过程中,都包含两个步骤.第一是等待,第二是拷贝.
- 而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间.让IO更高效,最核心的办法就是让等待的时间尽量少
高级IO重要概念
在这里,我们要强调几个概念
同步通信 vs 异步通信(synchronous communication/ asynchronous communication)
同步和异步是消息通信机制。
- 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了;换句话说,就是由调用者主动等待这个调用的结果;
- 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果;换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果;而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
另外,我们回忆在讲多进程多线程的时候,也提到同步和互斥。这里的同步通信和进程之间的同步是完全不相干的概念。
- 进程/线程同步也是进程/线程之间直接的制约关系。
- 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系。尤其是在访问临界资源的时候。
同学们以后在看到“同步”这个词,一定要先搞清楚大背景是什么。这个同步,是同步通信异步通信的同步,还是同步与互斥的同步。
阻塞 vs 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
- 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
其他高级IO
非阻塞IO,纪录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射IO(mmap),这些统称为高级IO。
我们此处重点讨论的是I/O多路转接
非阻塞IO
fcntl
一个文件描述符,默认都是阻塞IO。
函数原型如下:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fcntl的cmd的值不同,后面追加的参数也不相同。
fcntl函数有5种功能:
- 复制一个现有的描述符(cmd=F_DUPFD)。
- 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)。
- 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)。
- 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)。
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)。
我们此处只是用第三种功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞。
实现函数SetNoBlock
基于fcntl,我们实现一个SetNoBlock函数,将文件描述符设置为非阻塞。
void SetNoBlock(int fd) {int fl = fcntl(fd, F_GETFL);if (fl < 0) {perror("fcntl");return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
- 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图)。
- 然后再使用F_SETFL将文件描述符设置回去。设置回去的同时,加上一个O_NONBLOCK参数。
轮询方式读取标准输入
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>void SetNoBlock(int fd) {int fl = fcntl(fd, F_GETFL);if (fl < 0) {perror("fcntl");return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}int main() {SetNoBlock(0);while (1) {char buf[1024] = {0};ssize_t read_size = read(0, buf, sizeof(buf) - 1);if (read_size < 0) {perror("read");sleep(1);continue;}printf("input:%s\n", buf);}return 0;
}