共计 2472 个字符,预计需要花费 7 分钟才能阅读完成。
1.1 I/O介绍
I/O(input/output)即输入输出。根据冯诺依曼计算机结构体系,计算机结构分为5大部分:运算器、控制器、存储器、输入设备和输出设备。
输入设备(比如键盘等)向计算机输入数据,输出设备(比如显示器)接收计算机输出的数据。当然还有既可以当输入设备,也可以当输出设备的有网卡、硬盘。
从整个计算机结构体系上看,I/O描述了计算机与外部设备的通信过程。
在Linux操作系统中,为了保证操作系统的稳定性和安全性,一个进程的地址空间被划分为用户空间和内核空间。一般的应用程序都是跑在用户空间上的,只有内核空间才能进行系统态级别的资源有关的操作,例如:文件管理、进程通信、内存管理等。也就是说,如果我们的应用程序需要进行IO操作,一定是依赖于内核空间。由于用户程序无法直接访问内核空间,因此用户程序执行IO操作只能向系统发起调用请求,让操作系统帮忙完成。
从应用程序的角度上看:应用程序向操作系统内核发起IO调用请求,操作系统负责的内核执行具体的IO操作。这一流程会经历两个步骤:
- 内核等待I/O设备装备好数据;
- 内核将数据从内核空间拷贝到用户空间;
这对于一个服务端套接字上的输入操作,第一步通常是等待客户端连接。当客户端发起连接时,连接数据会被复制到内核中的某个缓冲区。第二部就是把数据从内核缓冲区复制到用户缓存区供服务端使用。
1.2 I/O模型的种类
在UNIX系统中,IO模型一共有5种:阻塞I/O(BIO)、非阻塞I/O(NIO)、I/O 多路复用、信号驱动I/O和异步I/O(AIO)。
1.2.1 阻塞I/O
阻塞I/O,即BIO,是最流行的I/O模型,默认情况下,所有的网络请求模型都是BIO。
在该模型中,应用程序发起I/O操作时,进程调用recvfrom直到接收到数据报且被复制到应用进程的缓冲区中或者发生错误时才继续执行,否则阻塞进程。也就是说,进程从调用recvfrom开始到它返回的整段时间内是被阻塞的,当recvfrom成功返回后,应用进程才开始处理数据报。
在网络模型中,BIO可以很容易地应付客户端连接数量不高的情况,但是当面对十万甚至百万级连接时,传统的BIO模型就无能为力了。
1.2.2 非阻塞I/O
非阻塞I/O,即NIO。应用程序发起I/O操作时,进程调用recvfrom,recvfrom会直接返回一个处理状态码,并在之后的操作中根据状态码检查是否返回了数据。
在上面的示例图中,前三次调用recvfrom时都没有可返回的数据,因此直接返回一个EWOULDBLOCK错误,在第四次调用recvfrom时,存在可返回的数据,因此该数据被复制到应用进程缓冲区,于是recvfrom成功返回,继续处理接下来的数据。
在NIO中,应用进程会持续轮询内核以查看某个操作是否就绪。这样做往往会出现CPU空转的情况,耗费大量的CPU时间。
1.2.3 I/O多路复用
I/O多路复用解决的就是NIO中CPU空转的情况,它允许一个进程监视多个文件句柄,并在其中一个或多个文件句柄就绪时进行处理。IO多路复用模型的优点是能够有效处理大量的IO请求。
I/O多路复用的核心思想是:轮询。应用程序首先向操作系统内核注册要监视的文件句柄,然后内核会轮询这些文件句柄,检查它们是否就绪。如果某个文件句柄就绪,内核会通知应用程序,应用程序可以对该文件句柄进行读写操作。
在IO多路复用模型中,线程首先发起select调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起read调用。read调用的过程(数据从内核空间拷贝到用户空间)仍然是阻塞的。
在上图中,我们阻塞于select调用,等待数据报套接字变为可读,当select返回套接字可读这一条件时,我们才调用recvfrom把所读数据复制到应用进程缓冲区里。
目前支持IO多路复用的系统调用有select
,epoll
等,其中select系统调用在目前的所有操作系统上几乎都支持。
- select调用: 内核提供的系统调用,它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。
- epoll调用:linux 2.6内核,属于select调用的增强版本,优化了IO的执行效率。
1.2.4 信号驱动I/O
IO复用模型解决了一个线程监控多个内核数据是否准备就绪的问题,但是IO复用中select时采用轮询的方式判断内核数据是否准备就绪,在大部分情况下,很多轮询都是无效的。因此,是否有这样一个IO模型我不去监控你是否有数据准备就绪,而是你如果有数据准备就绪时,主动通知我我再发起IO调用呢?这就是信号驱动I/O模型。
信号驱动IO模型不是通过循环请求询问的方式去监控数据是否就绪,而是在调用sigaction时候建立一个SIGIO的信号联系,当内核数据准备好之后,再通过SIGIO信号通知线程数据已准备好,当线程收到数据准备好的信号后,再向内核发起recvfrom读取数据请求,因为信号驱动IO模型下应用线程再发出信号监控后即可返回,不会阻塞,所以在这样的方式下,一个应用线程也可以同时监控多个数据就绪状态。
1.2.5 异步I/O
异步I/O(AIO)的工作机制是:应用程序通知内核执行某个操作,并让内核在整个操作(例如:将数据从内核复制到用户缓冲区)完成后通知我们。异步I/O与前面的信号驱动I/O的区别在于:信号驱动I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O则是由内核通知我们I/O操作何时完成。
当我们调用aio_read函数(异步I/O函数以aio_
或lio_
开头),给内核传递描述符、缓冲区指针、缓冲区大小和文件偏移,并告诉内核当整个操作完成时如何通知我们。该模型被调用时会立即返回,而且在等待内核完成I/O操作期间,进程不会被阻塞。
目前AIO的应用不是非常广泛,著名网络框架Netty曾采用AIO实现,但与之前通过NIO实现的性能并没有明显的提升,因此被废弃了(netty 5.x采用AIO,目前已废弃,主要使用的4.x)。