5大IO模型
理解同步异步和阻塞非阻塞
这四个概念总是很容易让人混淆,有时还会将同步跟阻塞等同起来或是“异步=非阻塞。”
- 1.同步:调用一个功能时,死等结果,没有结果之前不会反回,针对的是被调用者。
- 2.异步:调用一个功能时,可以立即返回,待这个功能处理完后通过回调等其方式进行通知,也是针对被调用者而言的。
- 3.阻塞:调用一个功能时,在调用结果返回之前,当前线程会被挂起,只有在得到结果之后才会返回,针对调用者。
- 4、非阻塞:非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回,然后通过select或poll或epoll方式通知调用者,针对调用者。
看到这里,我们心中多少有点b数了,我们可以说调用者以同步或异步的方式调用一个函数,但不能说以阻塞和非阻塞的方式调用一个函数。我们也可以说用阻塞的方式在实现同步调用,但不能将“阻塞=同步”
再拓展一下,阻塞将当前线程挂起后,此时这个线程就不再是激活状态了,而对于同步而言,当前线程还是可以处于激活状态的,只是没有返回结果,一直白等着而已(占用资源)。
Linux中5大IO模型
首先,我们要明白IO的过程,无论是读写文件还是针对socket的数据传输,我们一般都不是“直接”操作,如:读写文件,就调用read、write函数对磁盘操作。用socket传输数据就调用send/accept来发送或接受数据。这个过程需要借助操作系统的内核中的缓冲区,如借助socket发送数据,是将用户进程的数据拷贝到缓冲区中,然后由内核决定什么时候发送。
-
1.[同步]阻塞I/O(blocking I/O)
对于阻塞IO来说,进程会一直阻塞,直到数据拷贝完成 -
2.[同步]非阻塞I/O (nonblocking I/O)
对于非阻塞IO来说,当用户线程发起一个read或write操作后,并不需要等待,而是立即返回结果。此时,可能处于两种状态,如果数据已经准备好了,那没事,正常操作。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送同样的操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上将数据拷贝到内核缓冲区或者从内核的缓冲区读取数据,然后返回。
在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。注意一点:
在数据拷贝的过程中,进程是阻塞的
。 -
3.[同步]I/O复用(select 和poll、pselect、epoll) (I/O multiplexing)
在Linux系统中,支持IO复用的有着几种方式:select 和poll、pselect、epoll。关于这方面的描述,copy了盛延敏大佬在网络编程一文中的描述:epoll 的性能是要比 poll 或者 select 好。
这要从两个角度来说明。第一个角度是事件集合(读写请求)。在每次使用 poll 或 select 之前,都需要准备一个"感兴趣"(
对应代码中,可读、可写和异常事件对应的文件描述符集合
)的事件集合,系统内核拿到事件集合,进行分析(时间复杂度O(n))并在内核空间构建相应的数据结构来完成对事件集合的注册。int select( int nfds, /**被监听的文件描述符的总数*/ fd_set *readfds, /*可读事件对应的文件描述符集合*/ fd_set *writefds, /**可写事件对应的文件描述符集合*/ fd_set *exceptfds, /*异常事件对应的文件描述符集合*/ struct timeval *timeout ); int poll( /*程序员感兴趣的文件描述符上发生的可读、可写、异常等事件的集合*/ struct pollfd *fds, nfds_t nfds, int timeout );
[备注]文件描述符,指的是一个非负整数,表明每个被进程打开的文件。
而 epoll 则不是这样,epoll 维护了一个全局的事件集合,通过 epoll 句柄,可以操纵这个事件集合,增加、删除或修改这个事件集合里的某个元素。要知道在绝大多数情况下,事件集合的变化没有那么的大,这样操纵系统内核就不需要每次重新扫描事件集合(O(1)的时间复杂度),构建内核空间数据结构。
第二个角度是就绪列表。每次在使用 poll 或者 select 之后,应用程序都需要扫描整个感兴趣的事件集合,从中找出真正活动的事件,这个列表如果增长到 10K 以上,每次扫描的时间损耗也是惊人的。事实上,很多情况下扫描完一圈,可能发现只有几个真正活动的事件。而 epoll 则不是这样,epoll 返回的直接就是活动的事件列表(活动的事件列表用一个双向链表保存着,找活动事件时,直接调用这个链表),应用程序减少了大量的扫描时间。
-
4.[同步]信号驱动I/O (signal driven I/O (SIGIO))
信号驱动式会通过系统调用先建立 SIGIO 的信号处理函数, 然后立即返回而不阻塞。 当内核准备好数据, 向用户进程递交 SIGIO 信号, 此时进程可以开始使用 recvfrom 函数系统调用, 将数据复制到用户空间(此过程是阻塞的,这里也是用户进程主动去读取的). 之后, 只有当 recvfrom 成功执行后, 进程才执行 sendto 向客户端响应数据.
注意,同步信号在TCP中比较少用,因为SIGO是没有额外的标志信息的,而TCP中譬如三次握手、四次分手都会产生多种信号,这对系统而言是无法区分的。但UDP可以,UDP只有一次请求,SIGO信号可以用来表明内核已经准备好数据了。
-
5.异步I/O (asynchronous I/O (the POSIX aio_functions))
前面4种方式,无论阻不阻塞,又或者信号驱动,最后都是需要用户进程去读进行IO操作(对于写而言,是将用户进程数据写到内核缓冲区,对于读操作是将内核缓冲区数据读取到用户进程的缓冲区中),而异步不是的,数据的IO操作都是由内核完成的。在完成之后,内核通知用户进程IO完成,而对于信号驱动IO,是通知用户进程数据准备好了。