Linux操作系统的五种IO模型
Linux系统的IO
Java中常说的IO是指文件的输入和输出;而在操作系统中的一次IO可以简化成把数据从硬件(硬盘)中读取到用户空间中;详细分为两个阶段
- 将数据从磁盘文件先加载到内核内存空间(缓存区),等待数据准备完成,此过程时间较长。(准备数据的阶段)
- 然后将数据从内核缓冲区复制到用户空间的进程内存中,此过程时间较短(拷贝数据阶段)
在了解五种模型前先知道一些名词
同步/异步:关注点是消息的通信机制
- 同步:调用者等到被调用者返回消息,才能继续往下执行
- 异步:被调用者通过状态、通知或者回调机制主动通知调用者当前被调用者的状态
阻塞/非阻塞:关注点是调用者在等待结果返回之前所处的状态
- 阻塞:指在IO操作彻底完成后才返回到用户空间,在获得调用结果之前被挂起
- 非阻塞:指在发起IO操作后立即返回给用户一个状态值,无须等到IO操作彻底完成才返回;在调用结果返回之前,用户进程不会被挂起
文件描述符FD
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数
实际上它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符
在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统
同步阻塞IO模型
同步阻塞UI模型是最简单的IO模型,用户线程在内核进行IO操作的时候被阻塞;用户线程通过系统调用recvfrom
发起IO读操作,由用户空间转到内核空间。内核等到数据包到达后,再将接受的数据拷贝到用户空间,这才能完成一次读操作。
但是用户在进行读操作的时候,由于内核还不能立刻准备好数据包,应用进程就会阻塞住,直到内核准备好数据包,recvfrom
完成数据报复制工作,应用进程才能结束阻塞状态;这就导致用户在发起IO请求时,不能再进行其他任何操作,这对CPU的资源利用率是很低的
同步非阻塞IO模型
用户线程在发起IO请求后立即返回,但是此时并没有读取到任何数据,用户线程需要不断的通过 recvfrom
调用去和内核交互,直到内核准备好数据(即轮询机制)。如果没有准备好,内核会返回error
,应用进程在得到error
后,过一段时间再发送recvfrom
请求。在两次发送请求的时间段,进程可以先做别的事情
整个IO请求的过程,虽然用户线程立即返回了,但是为了得到数据还是需要通过轮询机制去请求是否准备好数据;重复的请求会消耗大量的CPU资源,一般很少会直接使用这个模型
信号驱动IO模型
用户进程可以通过sigaction系统调用注册一个信号处理程序,然后主程序返回不会阻塞;当有IO操作准备就绪时,由内核通知触发一个SIGIO信号处理程序执行,然后将用户进程再通过recvfrom
将数据从内核空间拷贝到用户空间(数据拷贝是同步的)
这种模型的优势在于等到数据包到达期间进程不会被阻塞,用户主程序可以继续执行,只要等待来自信号处理函数的通知即可(此模式实现复杂,不常用)
IO多路复用模型
多个进程的IO可以注册到同一个select
上,当用户进程调用该select
,select
会监听所有注册好的IO,如果所有被监听的IO需要的数据都没有准备好时,进程调用select
后会阻塞;当任意一个IO所需的数据准备好之后,select
调用就会返回,然后用户进程在通过recvfrom
来进行数据拷贝
虽然select
可以同时监控多个IO操作,但是每个IO请求过程还是会阻塞(阻塞在select函数)的;进程在发出select
后,要等到select
监听的所有IO操作中至少有一个需要的数据准备好,才会有返回,并且也需要再次发送请求去进行文件的拷贝
所以如果处理的连接数不是很高的话,使用IO多路复用不一定比使用多线程 + 阻塞 IO的性能更好,可能延迟还更大。IO多路复用的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接
select、poll、epoll
select
,poll
,epoll
都是多路复用IO的函数;其三者的区别在于:
- select
- select函数会无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长
- select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点
- 其缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上32位机默认是1024个,64位机默认是2048。也可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低
- poll
- poll本质上和select没有区别, 但是它没有最大连接数的限制,原因是它是基于链表来存储的(select是数组存储)
- epoll
- epoll是通过事件通知的方式(事件关联上fd),只要有可写的IO就会通过回调的方式告知用户进程
- epoll的最大连接数也是没有限制的,其基于哈希表来存储
异步IO模型
用户进程发起aio_read
操作之后,给内核传递描述符、缓冲区指针、缓冲区大小等,告诉内核当整个操作完成时,如何通知进程,然后就立刻去做其他事情了。当内核收到aio_read
后,会立刻返回,然后内核开始等待数据准备,数据准备好以后,直接把数据拷贝到用户空间,然后再通知进程本次IO已经完成
相比于IO多路复用,异步IO并不常用,更多的服务程序会使用IO多路复用模型 + 多线程任务处理的架构来满足业务需求
异步IO与信号驱动最主要的区别是
- 信号驱动IO是由内核通知用户进程何时进行IO操作,而异步IO是内核把数据拷贝到用户空间后才通知用户进程IO操作何时完成
- 信号驱动还需要用户进程阻塞在从内核空间缓冲区拷贝数据到用户空间,而异步IO是内核直接把所有数据准备好并且拷贝到用户空间后才通知用户进程可以进行后续的操作;即数据准备阶段两者都是非阻塞的,而数据拷贝阶段,信号驱动是阻塞的而异步IO是非阻塞的