五种IO模型
用户空间与内核空间
操作系统把内存空间划分成了两个部分:内核空间和用户空间。
为了保护内核空间的安全,操作系统一般都限制用户进程直接操作内核。
所以,当我们使用TCP发送数据的时候,需要先将数据从用户空间拷贝到内核空间,再由内核空间拷贝到网卡;
当我们使用TCP读取数据的时候,数据需要先从网卡拷贝到内核空间,再从内核空间拷贝到用户空间供用户进程使用。
内核空间与网卡的数据拷贝使用DMA技术,不需要CPU参与。
缓存IO
缓存I/O又被称作标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。 在Linux的缓存I/O机制中,
数据先从磁盘复制到内核空间的缓冲区(PageCache), 然后从内核空间缓冲区复制到应用程序的地址空间。
读操作 操作系统检查内核的缓冲区有没有需要的数据,如果已经缓存了,那么就直接从缓存中返回;
否则从磁盘中读取,然后缓存在操作系统的缓存中。
写操作 将数据从用户空间复制到内核空间的缓存区。这时对用户程序来说写操作就已经完成,
至于什么时候再写到磁盘中由操作系统决定,除非显示地调用了sync同步命令。
缓存I/O的优点 首先,在一定程度上分离了内核空间和用户空间,保护系统本身的运行安全;
其次,可以减少读盘的次数,从而提高性能。
缓存I/O的缺点 在缓存 I/O 机制中,DMA方式可以将数据直接从磁盘读到页缓存中,
或者将数据从页缓存直接写回到磁盘上,而不能直接在应用程序地址空间和磁盘之间进行数据传输,
这样,数据在传输过程中需要在应用程序地址空间(用户空间)和缓存(内核空间)之间进行多次数据拷贝操作,
这些数据拷贝操作所带来的CPU以及内存开销是非常大的。
IO模式
对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,
然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间(用户空间)。
所以说,当一个read操作发生时,它会经历两个阶段:
1.内核空间准备好数据
2.将数据从内核空间拷贝到用户空间
因为这两个阶段,Linux系统产生了下面五种IO模型
阻塞型IO
当用户进程发起read操作,一直阻塞直到数据拷贝到用户空间为止才返回。
阻塞型IO第一阶段和第二阶段都阻塞。
非阻塞型IO
非阻塞型IO,用户进程不断询问内核,数据准备好了吗?
一直重试,直到内核说数据准备好了,然后把数据从内核空间拷贝到用户空间,返回成功,开始处理数据。
非阻塞型IO第一阶段不阻塞,第二阶段阻塞。
IO多路复用
IO多路复用,会将多个Socket注册到一个选择器(Selector)。
相当于可以使用一个线程来管理多个Socket连接。在最初的阻塞型IO,每来一个新的连接都需要分配一个线程来处理。
IO多路复用有三种模式:select、poll、epoll。当用户调用select/poll/epoll,会阻塞当前进程,
内核会不断的轮询注册到选择器(Selector)上的Socket,当任何一个Socket的数据准备好了,就会结束阻塞。
这个时候用户进程再调用read操作,将数据从内核空间拷贝到用户空间。
IO多路复用,第一阶段会阻塞在Selector上,第二阶段拷贝数据也会阻塞。
信号驱动IO
信号驱动IO,用户进程发起read请求之前先注册一个信号给内核说明自己需要什么数据,这个注册请求立即返回。
等内核数据准备好了,主动通知用户进程,用户进程再去请求读取数据,此时,需要等待数据从内核空间拷贝到用户空间再返回。
信号驱动,第一阶段不阻塞,第二阶段阻塞。
异步IO
异步IO,用户进程发起read请求后立马返回,当数据完全拷贝到用户空间后通知用户直接使用数据。
异步IO,两个阶段都不阻塞。
阻塞与非阻塞
阻塞:是指调用结果返回之前,当前线程会被挂起,直到调用结果返回。
比如Socket编程中,使用java.io.InputStream#read(byte[])
方法读取数据,
如果没有可读数据,就会一直阻塞当前线程,直到有可读数据才会返回。
非阻塞:不能立即得到结果之前,当前线程不被挂起,而是可以继续做其它的事。
同步与异步
同步:调用者会被阻塞直到IO操作完成,调用的结果随着请求的结束而返回。。
异步:调用者不会被阻塞,调用的结果不随着请求的结束而返回,而是通过通知或回调函数的形式返回。
阻塞/非阻塞,更关心的是当前线程是不是被挂起。
同步/异步,更关心的是调用结果是不是随着请求结束而返回。
阻塞是指整个IO过程中是否有阻塞,所以,
阻塞型IO、非阻塞型、IO多路复用、信号驱动IO都是同步IO,只有最后一种才是异步IO。