一、I/O 概念
I/O 指的是相对内存而言的 input 和 output
从文件、数据库、网络向内存中写入数据叫做 input
从内存向文件、数据库、网络中输出数据叫做 output
I/O 操作相比 CPU 操作而言是极慢的,往往 CPU 运行一秒钟,I/O 要忙几个月,所以要提高 I/O 密集型程序的运行效率,异步 I/O 是毫无疑问的
举例说明 io 模块的使用:
import io
strio = io.StringIO()
strio.writelines('hello\n')
strio.writelines('world')
print('1、StringIO:')
print(strio.getvalue())
byteio = io.BytesIO()
byteio.write('你好'.encode())
print('2、BytesIO:')
print(byteio.getvalue().decode())
二、异步 I/O
2.1 系统调用
现代的操作系统通常都具有多任务处理的功能,通常靠进程来实现。操作系统的运算器(或处理器)快速在每个进程间切换执行,所以看起来就像多个进程同时运行。这样就带来了很多安全问题,例如一个进程可以轻易修改进程的内存空间中的数据来使另一个进程异常或达到一些其它目的,因此操作系统必须保证每一个进程都能安全地执行。这一问题的解决方法是在处理器中加入基址寄存器和界限寄存器。这两个寄存器中的内容限制了存取指令所访问的储存器的地址范围,这样就可以在系统切换进程时写入这两个寄存器的内容到该进程被分配的地址范围,从而避免恶意软件
为了防止用户程序修改基址寄存器和界限寄存器中的内容来达到访问其它内存空间的目的,这两个寄存器必须通过一些特殊的指令来访问。通常,处理器设有两种模式:“用户模式” 与 “内核模式” ,通过一个标签位来鉴别当前正处于什么模式。一些诸如修改基址寄存器内容的指令只有在内核模式中可以执行,而处于用户模式的时候硬件会直接跳过这个指令并继续执行下一个
同样,为了安全问题,一些 I/O 操作的指令都被设置为只有在内核模式下可以执行,因此操作系统有必要为应用程序提供诸如读取磁盘某位置的数据的接口,这些接口就被称为系统调用
当操作系统接收到系统调用请求后,会让处理器进入内核模式,从而执行诸如 I/O 操作、修改基址寄存器内容等指令,当处理完系统调用内容后,操作系统会让处理器返回用户模式,来执行用户代码
2.2 用户态和内核态
在 CPU 执行的所有指令中,有一些指令是非常危险的,如果错用将导致整个系统崩溃,例如清内存、设置时钟等。如果所有程序都能使用这些指令,那么你的系统将极不稳定,死机可能经常发生。所以 CPU 将指令分为特权指令和非特权指令。对于那些危险的指令,只允许操作系统及其相关模块使用,普通的应用程序只能使用那些不会造成灾难的指令(例如 Intel 的 CPU 将特权级别由高到低分为四个级别:RING0,RING1,RING2 和 RING3)
Linux 内核是一个有机的整体,每一个用户进程运行时都有一份内核的拷贝,每当用户进程使用系统调用时,都自动地将运行模式从用户级转为内核级(即上文提到的用户模式和内核模式),此时进程在内核的地址空间中运行
当一个任务(进程)执行系统调用而进入内核空间中执行时,我们就称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0 级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3 级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似
处理器总处于以下状态中的一种:
1、内核态,运行于进程上下文,内核代表进程运行于内核空间
2、内核态,运行于中断上下文,内核代表硬件运行于内核空间
3、用户态,运行于用户空间
三、Linux 系统中的 I/O 模型
计算机有运算器、控制器、存储器、输入设备、输出设备五部分组成。运算器(或处理器)的速度是最快的,内存读写数据、磁盘寻址、网络传输相对而言是极慢的,运算器和控制器主要集成在 CPU 中,其它的都是 I/O ,CPU 大部分时间都是在等待 I/O 完成操作,会浪费大量的时间,I/O 成了最大的性能瓶颈
网络通信是两个主机之间的通信,需要有外设接收外部的数据,再将数据存放到内存,整个过程是很消耗时间的。计算机之间互连离不开网络,而网络 I/O 相对来说速度最慢。为了提高 I/O 操作效率,操作系统提供了几种高性能的 I/O 模型,其中异步 I/O 是一个比较好的解决方案,性能得到了大幅提升
Linux 系统中有五种 I/O 模型,本文重点介绍的是 I/O 复用模型,广泛使用的 I/O 多路复用机制的系统调用有 select, poll, epoll 这三种
3.1 阻塞 I/O
当用户态进程调用系统函数获取数据时,如果内核中还没有准备好数据,用户态进程将挂起一直等待,不进行其它操作,等内核将数据准备好之后,将数据从内核空间拷贝到用户空间,这时候系统调用函数返回,解除阻塞状态,用户态进程处理接收到的数据
3.2 非阻塞 I/O
用户态进程调用系统函数获取数据时,如果内核中还没有准备好数据,内核会返回错误信息给进程,用户态进程接收到错误信息,不会阻塞在那里,但进程会不断调用系统函数询问内核,直到内核准备好数据,将数据从内核复制到用户空间,系统函数调用结束,开始处理接收到的数据
3.3 I/O 复用
在计算机网络里面,有很多关于 “复用” 的用法,比如多路复用。本来一条链路上一次只能传输一个数据流,如果要实现多个源之间多条数据流传输,那就得需要多条链路了,复用技术可以通过将一条链路划分频率,或者划分传输的时间,使一条链路上可以传输多条数据流。I/O 复用就是在一个进程里会处理多个消息事件
3.4 信号驱动式 I/O 模型
当用户态进程需要数据时,会向内核发送一个信号,告诉内核要什么数据,然后自己去做其它的事情,当内核态的数据准备好之后,内核马上给用户态进程发送信号,用户态进程收到信号立马调用系统函数,将数据从内核空间拷贝到用户空间,完成之后用户态进程开始处理接收到的数据
3.5 异步 I/O 模型
用户态进程需要数据时会告诉内核态需要什么,然后就不用管了,可以做其它事情,内核会将用户态需要的数据准备好,然后将数据复制到用户空间,这时才通知用户态进程直接处理用户空间的数据
我们看到前四个 I/O 模型处理数据时,都是用户态进程将数据从内核空间拷贝到用户空间,这一段时间对于进程来说是阻塞的,只有异步 I/O 是内核将数据从内核空间拷贝到用户空间,用户态进程完全是异步操作,所以前四个模型可以称为同步 I/O ,最后一个是异步 I/O
五个 I/O 模型的比较