Linux网络IO模型

学习Java-NIO在网络端的应用,就需要了解Linux的网络IO模型,才能够体会为什么需要NIO和NIO的好处在哪里。

什么是同步与异步、阻塞与非阻塞

引用知乎 怎样理解阻塞非阻塞与同步异步的区别? 上面的一个回答,很生动的说明了同步异步,阻塞非阻塞之间的区别联系:


老张爱喝茶,废话不说,煮开水。

出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。

1 老张把水壶放到火上,立等水开。(同步阻塞)

老张觉得自己有点傻

2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)

老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。

3 老张把响水壶放到火上,立等水开。(异步阻塞)

老张觉得这样傻等意义不大

4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)

老张觉得自己聪明了。

所谓同步异步,只是对于水壶而言。普通水壶,同步;响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。

所谓阻塞非阻塞,仅仅对于老张而言。立等的老张,阻塞;看电视的老张,非阻塞。情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。


看上面的例子,我们再来关注Linux网络IO模型,然后结合这个例子去理解。

Linux网络IO模型

Unix提供了五种IO模式,分别是:

  • 阻塞IO

  • 非阻塞IO

  • IO复用

  • 信号驱动IO

  • 异步IO

在之前的学习中我们也了解了从用户进程到底层硬件执行IO的过程,以read为例:

io

数据需要从硬件设备拷贝到内核空间的缓冲区,然后从内核缓冲区拷贝到用户进程空间。

我们把数据需要从硬件设备拷贝到内核空间的缓冲区这个过程类比为烧水,从内核缓冲区拷贝到用户进程空间这个过程类比为用烧好的水泡茶。

阻塞IO

blockIO

阻塞IO是最常用的IO模型,我们在java中调用传统BIO(InputStream、OutpuytStream)的读写方法都是这种IO模型。

观察上图,在进程空间中调用recvfrom,其系统调用直到数据从硬件设备拷贝到内核缓冲区并且从内核拷贝到用户进程空间时才会返回,在此期间一直是阻塞的,进程在从调用recvfrom到他返回这段时间一直都是阻塞的,故称为阻塞IO。

阻塞IO对应了我们上面提到的同步阻塞。在这种IO模式下整个过程相当于使用不会响的普通水壶烧水,并且老张一直在旁边盯着,干不了其他事。水烧好后老张再去泡茶。整个过程是同步阻塞的。

在阻塞IO模式下,在同一个线程当中,我们对于多个连接,只能依次处理:

1
2
3
4
5
6
while true {
for i in stream[] {
//可能会阻塞很长时间
read until available
}
}

非阻塞IO

nonblockingIO

用户进程发起一个recvfrom调用的时候,如果内核缓冲区的数据还没有准备好(没有完全从硬件拷贝到内核),那么他不会阻塞用户进程,而是立刻返回一个error。用户发起一个recvfrom操作之后,不需要等待,而是马上会得到一个结果,用户可以判断这个结果,如果是一个error,表示数据还没有准备好,于是可以再次发起recvfrom操作,一旦内核数据准备好了,就可以把数据拷贝到用户进程空间,然后返回。

这种IO模型称之为非阻塞IO,整个过程可以类比为:在这种IO模式下调用recvfrom相当于使用不会响的普通水壶烧水,老张时不时跑到厨房看看水烧开了没(这个过程是同步非阻塞的),如果水烧开了,他就用烧开的水泡茶(相当于从内核copy数据到用户空间这一段,这个过程其实是同步阻塞的)

在非阻塞IO模式下,我们发现可以在一个线程中处理多个连接了:

1
2
3
4
5
6
7
// 忙轮询
while true {
for i in stream[]; {
// 如果数据没有准备好,就立即返回,处理下一个流
read until unavailable
}
}

我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为如果所有的流都没有数据,那么只会白白浪费CPU。

为了避免CPU空转,可以引进了一个代理: select或poll(两者本质上相同)

IO复用

mutiplexingIO

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。

1
2
3
4
5
6
7
8
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))。

1
2
3
4
5
6
7
8
9
// 事先调用epoll_ctl注册感兴趣的事件到epollfd
while true {
// 返回触发注册事件的流
active_stream[] = epoll_wait(epollfd)
// 无须遍历所有的流
for i in active_stream[] {
read or write till
}
}

信号驱动IO

首先开启套接口信号驱动IO功能,并通过系统调用sigaction执行一个信号处理函数,此系统调用立即返回。当数据准备就绪时,就为该进程生成一个sigio信号,通过信号回调通知进程。进程调用recvfrom读取数据,将数据从内核缓冲区拷贝到用户进程空间。

上面的过程可以类比为:老张使用会响的水壶烧水,然后就去客厅看电视了。水烧好后水壶响起来(这个过程是异步非阻塞的),老张再来厨房用烧好的水泡茶(这个过程是同步阻塞的)。

异步IO

asyncIO

用户进程发起recvfrom操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它收到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

这种IO模式与信号驱动IO的区别在于:信号驱动IO由内核通知我们什么时候可以开始一个IO操作,异步IO则由内核告诉我们IO操作何时完成。

异步IO模式可以类比为:在这种IO模式下整个过程相当于使用会响的水壶烧水,并且,这个水壶更加智能,水烧好后可以自动泡茶,然后发出声响通知老张。老张把水放到火上就去客厅看电视了,水烧好并且茶叶泡好之后,水壶发出声响通知老张。

参考

Linux IO模式及 select、poll、epoll详解

怎样理解阻塞非阻塞与同步异步的区别?

epoll 或者 kqueue 的原理是什么?

《Netty权威指南》 电子工业出版社

推荐文章