Linux网络IO模型
Unix提供了五种IO模式,分别是:
阻塞IO
非阻塞IO
IO复用
信号驱动IO
异步IO
在之前的学习中我们也了解了从用户进程到底层硬件执行IO的过程,以read为例:
数据需要从硬件设备拷贝到内核空间的缓冲区,然后从内核缓冲区拷贝到用户进程空间。
我们把数据需要从硬件设备拷贝到内核空间的缓冲区这个过程类比为烧水,从内核缓冲区拷贝到用户进程空间这个过程类比为用烧好的水泡茶。
阻塞IO
阻塞IO是最常用的IO模型,我们在java中调用传统BIO(InputStream、OutpuytStream)的读写方法都是这种IO模型。
观察上图,在进程空间中调用recvfrom,其系统调用直到数据从硬件设备拷贝到内核缓冲区并且从内核拷贝到用户进程空间时才会返回,在此期间一直是阻塞的,进程在从调用recvfrom到他返回这段时间一直都是阻塞的,故称为阻塞IO。
阻塞IO对应了我们上面提到的同步阻塞。在这种IO模式下整个过程相当于使用不会响的普通水壶烧水,并且老张一直在旁边盯着,干不了其他事。水烧好后老张再去泡茶。整个过程是同步阻塞的。
在阻塞IO模式下,在同一个线程当中,我们对于多个连接,只能依次处理:
while true { for i in stream[] { //可能会阻塞很长时间 read until available } }
非阻塞IO
用户进程发起一个recvfrom调用的时候,如果内核缓冲区的数据还没有准备好(没有完全从硬件拷贝到内核),那么他不会阻塞用户进程,而是立刻返回一个error。用户发起一个recvfrom操作之后,不需要等待,而是马上会得到一个结果,用户可以判断这个结果,如果是一个error,表示数据还没有准备好,于是可以再次发起recvfrom操作,一旦内核数据准备好了,就可以把数据拷贝到用户进程空间,然后返回。
这种IO模型称之为非阻塞IO,整个过程可以类比为:在这种IO模式下调用recvfrom相当于使用不会响的普通水壶烧水,老张时不时跑到厨房看看水烧开了没(这个过程是同步非阻塞的),如果水烧开了,他就用烧开的水泡茶(相当于从内核copy数据到用户空间这一段,这个过程其实是同步阻塞的)
在非阻塞IO模式下,我们发现可以在一个线程中处理多个连接了:
// 忙轮询while true { for i in stream[]; { // 如果数据没有准备好,就立即返回,处理下一个流 read until unavailable } }
我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为如果所有的流都没有数据,那么只会白白浪费CPU。
为了避免CPU空转,可以引进了一个代理: select或poll(两者本质上相同)
IO复用
Linux 提供了select/poll,进程将一个或多个fd传递给select或poll系统调用,并且阻塞在select或poll方法上。同时,kernel会侦测所有select负责的fd是否处于就绪状态,如果有任何一个fd就绪,select或poll就会返回,这个时候用户进程再调用recvfrom,将数据从内核缓冲区拷贝到用户进程空间。
这个图和blocking IO的图有些相似,但是还有一些区别。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
while true { // 在select上阻塞 select(streams[]) // 无差别轮询 for i in streams[] { read until unavailable } }
于是,如果没有I/O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,每一次无差别轮询时间就越长。
Linux还提供了一个epoll系统调用,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的(复杂度降低到了O(1))。
// 事先调用epoll_ctl注册感兴趣的事件到epollfdwhile true { // 返回触发注册事件的流 active_stream[] = epoll_wait(epollfd) // 无须遍历所有的流 for i in active_stream[] { read or write till } }
信号驱动IO
首先开启套接口信号驱动IO功能,并通过系统调用sigaction执行一个信号处理函数,此系统调用立即返回。当数据准备就绪时,就为该进程生成一个sigio信号,通过信号回调通知进程。进程调用recvfrom读取数据,将数据从内核缓冲区拷贝到用户进程空间。
上面的过程可以类比为:老张使用会响的水壶烧水,然后就去客厅看电视了。水烧好后水壶响起来(这个过程是异步非阻塞的),老张再来厨房用烧好的水泡茶(这个过程是同步阻塞的)。
异步IO
用户进程发起recvfrom操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它收到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
这种IO模式与信号驱动IO的区别在于:信号驱动IO由内核通知我们什么时候可以开始一个IO操作,异步IO则由内核告诉我们IO操作何时完成。
登录 | 立即注册