Linux 网络 IO 模型
同步和异步,阻塞和非阻塞
同步和异步
关注的是调用方是否主动获取结果
同步:同步的意思就是调用方需要主动等待结果的返回
异步:异步的意思就是不需要主动等待结果的返回,而是通过其他手段比如,状态通知,
回调函数等。
阻塞和非阻塞
主要关注的是等待结果返回调用方的状态
阻塞:是指结果返回之前,当前线程被挂起,不做任何事
非阻塞:是指结果在返回之前,线程可以做一些其他事,不会被挂起。
两者的组合
1
.同步阻塞:
同步阻塞基本也是编程中最常见的模型,打个比方你去商店买衣服,你去了
之后发现衣服卖完了,那你就在店里面一直等,期间不做任何事(包括看手机),等着商家进
货,直到有货为止,这个效率很低。
2.同步非阻塞:
同步非阻塞在编程中可以抽象为一个轮询模式,你去了商店之后,发现衣
服卖完了,这个时候不需要傻傻的等着,你可以去其他地方比如奶茶店,买杯水,但是你还
是需要时不时的去商店问老板新衣服到了吗。
3.异步阻塞:
异步阻塞这个编程里面用的较少,有点类似你写了个线程池,submit 然后马
上 future.get(),这样线程其实还是挂起的。有点像你去商店买衣服,这个时候发现衣服没有
了,这个时候你就给老板留给电话,说衣服到了就给我打电话,然后你就守着这个电话,一
直等着他响什么事也不做。这样感觉的确有点傻,所以这个模式用得比较少。
4.异步非阻塞:
异步非阻塞。好比你去商店买衣服,衣服没了,你只需要给老板说这是我
的电话,衣服到了就打。然后你就随心所欲的去玩,也不用操心衣服什么时候到,衣服一到,
电话一响就可以去买衣服了
Linux 下的五种 I/O 模型
![](https://img-blog.csdnimg.cn/direct/b913a472a3074880a395c0e7161e2690.png)
总的来说,
阻塞 IO 就是 JDK 里的 BIO 编程,IO 复用就是 JDK 里的 NIO 编程,Linux 下异
步 IO 的实现建立在 epoll 之上,是个伪异步实现,而且相比 IO 复用,没有体现出性能优势,
使用不广。
非阻塞 IO 使用轮询模式,会不断检测是否有数据到达,大量的占用 CPU 的时间,
是绝不被推荐的模型。
信号驱动 IO 需要在网络通信时额外安装信号处理函数,使用也不广
。
![](https://img-blog.csdnimg.cn/direct/be8cb3388b3f45b290808f63c55f2748.png)
阻塞 IO 模型
I/O 复用模型
比较上面两张图,IO
复用需要使用两个系统调用
(select
和
recvfrom)
,而
blocking IO
只
调用了一个系统调用
(recvfrom)
。但是,用
select
的优势在于它可以同时处理多个
connection
。
所以,如果处理的连接数不是很高的话,使用
select/epoll
的
web server
不一定比使用
multi-threading + blocking IO
的
web server
性能更好,可能延迟还更大。
select/epoll
的优势
并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
从
Linux
代码结构看网络通信
![](https://img-blog.csdnimg.cn/direct/d0fa56edc8994661b2b3497bfc569e40.png)
Linux 内核的源码包含的东西很多,在 Linux 的源代码中,网络设备驱动对应的逻辑位于
driver/net/ethernet, 其中 intel 系列网卡的驱动在 driver/net/ethernet/intel 目录下。
协议栈模
块代码位于
kernel
和
net
目录。
其中 net
目录中包含
Linux
内核的网络协议栈的代码。子目录
ipv4
和
ipv6
为
TCP/IP
协议
栈的
IPv4
和
IPv6
的实现,主要包含了
TCP
、
UDP
、
IP
协议的代码,还有
ARP
协议、
ICMP
协
议、
IGMP
协议代码实现,以及如
proc
、
ioctl
等控制相关的代码。
站在网络通信的角度,源代码组织的表现形式如下:
![](https://img-blog.csdnimg.cn/direct/63c96effd27f46dd822880828f060d1a.png)
网络协议栈是由若干个层组成的,网络数据的流程主要是指在协议栈的各个层之间的传递
。
一个 TCP 服务器的流程按照建立 socket()函数,绑定地址端口 bind()函数,侦听端口 listen()
函数,接收连接 accept()函数,发送数据 send()函数,接收数据 recv()函数,关闭 socket()函
数的顺序来进行。
与此对应内核的处理过程也是按照此顺序进行的,网络数据在内核中的处理过程
主要是在
网卡
和
协议栈
之间进行
:
从网卡接收数据,交给协议栈处理;协议栈将需要发送的数据通过网络发出去。
由下图中可以看出,数据的流向主要有两种。应用层输出数据时,数据按照自上而下的顺
序,依次通过应用
API
层、协议层和接口层
;
当有数据到达的时候,自下而上依次通过接口
层、协议层和应用
API
层的方式,在内核层传递。
![](https://img-blog.csdnimg.cn/direct/c15aa6a61d0d455eaa8f88b482e1903b.png)
应用层 Socket
的初始化、绑定
(bind)
和销毁是通过调用内核层的
socket()
函数进行资源的申
请和销毁的。
发送数据的时候,将数据由应用 API
层传递给协议层,协议层在
UDP
层添加
UDP
的首部、
TCP
层添加
TCP
的首部、
IP
层添加
IP
的首部,接口层的网卡则添加以太网相关的信息后,
通过网卡的发送程序发送到网络上。
接收数据的过程是一个相反的过程,当有数据到来的时候,网卡的中断处理程序将数据从
以太网网卡的
FIFO
对列中接收到内核
,
传递给协议层
,
协议层在
IP
层剥离
IP
的首部、
UDP
层
剥离
UDP
的首部、
TCP
层剥离
TCP
的首部后传递给应用
API
层,应用
API
层查询
socket
的
标识后,将数据送给用户层匹配的
socket
。
在 Linux
内核实现中,链路层协议靠网卡驱动来实现,内核协议栈来实现网络层和传输层。
内核对更上层的应用层提供
socket
接口来供用户进程访问。