I/O 模型
I/O读写基本原理
在用户程序进行IO的读写,依赖底层的IO读写,基本上会使用到底层的read/write两大系统调用。
这里涉及到一个基础知识,read系统调用,并不是直接从物理设备把数据读取到内存中;write系统调用,也不是直接把数据写入到物理设备。上层应用无论是调用操作系统的read,还是调用操作系统的write,都会涉及到缓存区,具体来说,调用操作系统的read,是把数据从内核缓冲区复制到进程缓冲区;而write系统调用,是把数据从进程缓冲区复制到内核缓冲区。
也就是说,上层程序的IO操作,实际不是物理设备级别的读写,而是缓存的复制。read/write两大系统调用,都不负责数据在内核缓存区和物理设备之间的交换,这项底层读写交换,是由操作系统内核来换成的。
在用户程序中,无论是Socket的IO,还是文件IO操作,都属于上层应用的开发,它们的输入输出处理,在编程的流程上都是一致的。
缓冲区(Buffer)的存在意义
缓冲区的目的就是为了减少频繁的与设备之间的物理交换。首先应该知道:外部设备的读写,涉及操作系统的中断,发生系统中断时,需要保存之前的进程数据和状态等信息,而结束中断后还需要恢复到之前的进程数据和状态等信息。为了减少这种底层系统的时间损耗、性能损耗,才出现了内存缓存区。
有了内存缓冲区,上层用户进程使用read系统调用时,仅仅把数据从内核缓冲区复制到用户进程缓冲区。上层用户进程使用write系统调用时,仅仅把数据从进程缓冲区复制到内核缓冲区中。底层操作会对内核缓冲区进行监控,等待缓冲区达到一定数量的时候,再进行IO设备的中断处理,集中执行物理设备的实际IO操作。至于什么时候读中断,写中断,由操作系统的内核来决定,用户程序并不关心。
从数量上来看,在linux系统中,操作系统内核只有一个内核缓冲区。而每个用户程序都有自己独立的缓冲区,叫做进程缓冲区。所以用户进程的IO读写程序,在大多数情况下,并没有进行实际的IO操作,而是在进程缓冲区和内核缓冲区之间进行数据的交换
详解典型系统调用流程
- 客户端发起请求:Linux通过网卡读取客户端的请求数据,将数据读取到内核缓冲区。
- 用户程序(服务端)获取请求数据: 服务端程序通过read系统调用,从Linux内核缓冲区读取数据,再复制到Java进程缓冲区。
- 服务端业务处理:服务端在自己的用户空间中处理客户端的请求。
- 服务端返回数据:完成处理后,通过write系统调用,将构建好的数据从用户缓冲区复制到内核缓冲区。
- 发送给客户端:Linux内核通过网络IO,将内核缓冲区的数据写入网卡,网卡通过底层的通信协议,将数据发送到目标客户端。
同步和异步
同步和异步描述的是用户线程,它与内核的交互方式:
- 同步:用户线程向内核发起I/O执行请求后会等待内核I/O操作完成后才能继续执行。
- 异步:用户线程向内核发起I/O执行请求后会继续执行后面的命令,当内核I/O操作完成后会通知用户线程,或者调用用户线程之前注册的回调函数。
阻塞和非阻塞
阻塞和非阻塞描述的是内核I/O,它对用户线程的响应的方式:
- 阻塞:I/O操作需要彻底完成后才返回用户空间,它使得用户线程陷入阻塞。
- 非阻塞: I/O操作被调用后立即返回给用户一个状态值,无须等到I/O操作完成,它使得用户线程非阻塞。
图解
四种主要的I/O模型
同步阻塞IO(Blocking IO)
同步意味着用户线程会等待内核回应才会继续往下执行,中间会阻塞等待。
阻塞意味着内核IO不会立即给用户线程回应。此时用户线程会陷入等待。
同步非阻塞IO (Non-blocking)
同步意味着用户线程会等待内核回应才会继续往下执行。
非阻塞意味着内核IO会立即对用户线程的请求给予回应。此时用户线程不会陷入阻塞。
IO多路复用模型
为避免同步非阻塞IO模型中轮询等待的问题。提出了IO多路复用模型。
其本质还是同步阻塞的,但是将整个IO过程分割成两个功能独立的部分。
- select/epoll 系统调用,轮询所有Socket连接,当某些socket网络连接有IO就绪的状态,就返回对应的执行读写操作。
- read调用,内核复制数据到用户缓存区。
IO多路复用模型与同步非阻塞模型是密切相关的,多路复用IO也需要轮询,但是这个轮询是通过一个操作系统内核提供的选择器查询线程来查找出IO操作已就绪的socket连接。
它最大的优势在于,一个选择器查询线程可同时处理成千上万个连接,系统不必创建大量的线程。减少系统开销。
异步IO模型
Asynchronous IO ,在异步IO模型中,用户线程始终不会阻塞。
就目前而言,在Linux下,其底层实现还不完善,在性能上没有明显的优势。大多仍使用IO多路复用模型。