目录
1.阻塞blocking
同步阻塞的IO模型,效率不高,但编程简单
内核空间就是网络接收到远端发送过来数据的tcp接收缓冲区。
应用进程调用read,系统调用,是系统的I/O接口。
内核数据未准备好,即数据未就绪,在应用进程看来,进程/线程是一直阻塞住,整个数据是否就绪、数据就绪后的从内核空间向用户空间的拷贝的整个过程,应用程序时间都要在read上花费完,一直阻塞住。应用程序调用read这个I/O接口,内核把这一系列事情做完以后,然后再唤醒当前这个应用线程,可以起来处理数据了。
2.非阻塞 non-blocking
同步非阻塞模型。
当应用进程调用read之前,用setsockopt()这个系统的方法把用socket创建的sockfd设置成EAGAIN(未阻塞),在数据未就绪状态下,它是不断的返回。通过返回值的判断,size=-1&&EAGAIN,表示正常的返回,内核的数据未就绪。如果数据准备好以后,数据从内核空间TCP接收缓冲区拷贝到用户空间的buf,花费的还是应用程序的时间。
3.IO复用(IO multiplexing)
过程是同步的
上图右边对应数据是否就绪,事件是否发生的阶段。默认的select/poll/epoll都是可以设置timeout超时时间的。如果不设置,默认工作在阻塞模式下,就是事件未发生,调用I/O复用接口的进程线程是阻塞住的。
设置超时时间,就是可以工作在非阻塞模式下,同样去检测返回值是不是EAGAIN,是错误返回还是正常非阻塞的返回。
如果数据准备好了以后,返回可读的fd,返回发生事件的event,接下来对发生事件的event调用accept或者read,上图中是发现是已建立连接的读写事件,发起系统调用,整个的数据从内核tcp接收缓冲区拷贝到用户空间的buf,还是要耗费应用程序的时间,还是同步的过程。
与阻塞和非阻塞的区别:
数据的未就绪到就绪调用了I/O复用接口,就绪以后根据具体的发生事件fd,进行相应的I/O接口的调用,好处是在一个线程里面,调用一个I/O复用接口,可以监听很多很多的套接字(高并发),而阻塞和非阻塞,一个线程只能处理一个,当多个套接字有数据可操作的话,I/O复用会给应用程序返回可读或者可写的socket列表,然后应用程序根据I/O复用返回的这些可读写事件的fd进行相应的读写操作。
4.信号驱动(signal-driven)
Linux独有
在数据未就绪到数据就绪的过程中,应用进程是在继续执行,可以做任何事清,它调用相应的方法注册SIGIO信号处理程序,相当于协商了通知的方式,相当于回调操作,给内核注册以后,内核就给应用进程返回“知道了”,知道应用进程对什么事件感兴趣了,知道事件发生时该怎么通知应用进程。等待数据的过程就是一个异步的过程。
但是应用进程在读或者写的过程中,不能放飞自我,调用read去读这个数据,数据从内核TCP缓冲区拷贝到用户的buf,应用进程是要花费时间的,数据搬完了,应用进程才能进行向下执行。这个过程是一个同步的过程。
内核在第一个阶段是异步,在第二个阶段是同步;与非阻塞IO的区别在于它提供了消息通知机制,不需要用户进程不断的轮询检查(就是相当于非阻塞模式,或者是IO复用设置了timeout,不断地超时返回),减少了系统API的调用次数(这就是说,如果你是非阻塞,或者是设置了timeout的I/O复用的话,当你检查返回值=-1&&EAGAIN,会再次调动read或者I/O复用接口来检查内核的数据,是否从未就绪到就绪了)提高了效率。
5.异步(asynchronous)
基于操作系统级别的异步非阻塞I/O,效率高,编程复杂,出了问题不易排查。
struct aiocb {
int aio_fildes
off_t aio_offset
volatile void *aio_buf
size_t aio_nbytes
int aio_reqprio
struct sigevent aio_sigevent
int aio_lio_opcode
}
整个过程从数据就绪到数据拷贝,不耗费应用程序的时间。应用进程/线程继续执行的,可以去做其他事情。通过aio_read告诉内核,我应用进程的一个缓冲区、内核通知应用进程的信号,应用进程感兴趣的事件sockfd;内核返回“好的”,如果处理好了应用程序所感兴趣的事件,内核就通知应用程序,在此期间,应用程序可以做其它的事情,更好的利用CPU时间片。
内核看数据就绪后,将数据拷贝到用户的buf缓冲区,内核处理好之后,通过之前约定好的方式,发送信号,通知应用程序“好了,该处理这个了”。
典型的异步非阻塞状态,Node.js采用的网络IO模型。