我们都知道Netty是一个高性能异步事件驱动的网络框架。
它的设计异常优雅简洁,扩展性高,稳定性强。拥有非常详细完整的用户文档。
同时内置了很多非常有用的模块基本上做到了开箱即用,用户只需要编写短短几行代码,就可以快速构建出一个具有高吞吐,低延时,更少的资源消耗,高性能(非必要的内存拷贝最小化)等特征的高并发网络应用程序。
本文我们来探讨下支持Netty具有高吞吐,低延时特征的基石----netty的网络IO模型。
由Netty的网络IO模型开始,我们来正式揭开本系列Netty源码解析的序幕:
网络包接收流程
网络包收发过程
-
当
网络数据帧
通过网络传输到达网卡时,网卡会将网络数据帧通过DMA的方式
放到环形缓冲区RingBuffer
中。
RingBuffer
是网卡在启动的时候分配和初始化
的环形缓冲队列
。当RingBuffer满
的时候,新来的数据包就会被丢弃
。我们可以通过ifconfig
命令查看网卡收发数据包的情况。其中overruns
数据项表示当RingBuffer满
时,被丢弃的数据包
。如果发现出现丢包情况,可以通过ethtool命令
来增大RingBuffer长度。
-
当
DMA操作完成
时,网卡会向CPU发起一个硬中断
,告诉CPU
有网络数据到达。CPU调用网卡驱动注册的硬中断响应程序
。网卡硬中断响应程序会为网络数据帧创建内核数据结构sk_buffer
,并将网络数据帧拷贝
到sk_buffer
中。然后发起软中断请求
,通知内核
有新的网络数据帧到达。
sk_buff
缓冲区,是一个维护网络帧结构的双向链表
,链表中的每一个元素都是一个网络帧
。虽然 TCP/IP 协议栈分了好几层,但上下不同层之间的传递,实际上只需要操作这个数据结构中的指针,而无需进行数据复制
。
-
内核线程
ksoftirqd
发现有软中断请求到来,随后调用网卡驱动注册的poll函数
,poll函数
将sk_buffer
中的网络数据包
送到内核协议栈中注册的ip_rcv函数
中。
每个CPU
会绑定一个ksoftirqd
内核线程专门
用来处理软中断响应
。2个 CPU 时,就会有ksoftirqd/0
和ksoftirqd/1
这两个内核线程。
这里有个事情需要注意下: 网卡接收到数据后,当
DMA拷贝完成
时,向CPU发出硬中断
,这时哪个CPU
上响应了这个硬中断
,那么在网卡硬中断响应程序
中发出的软中断请求
也会在这个CPU绑定的ksoftirqd线程
中响应。所以如果发现Linux软中断,CPU消耗都集中在一个核上
的话,那么就需要调整硬中断的CPU亲和性
,来将硬中断打散
到不通的CPU核
上去。
-
在
ip_rcv函数
中也就是上图中的网络层
,取出
数据包的IP头
,判断该数据包下一跳的走向,如果数据包是发送给本机的,则取出传输层的协议类型(TCP
或者UDP
),并去掉
数据包的IP头
,将数据包交给上图中得传输层
处理。
传输层的处理函数:
TCP协议
对应内核协议栈中注册的tcp_rcv函数
,UDP协议
对应内核协议栈中注册的udp_rcv函数
。
-
当我们采用的是
TCP协议
时,数据包到达传输层时,会在内核协议栈中的tcp_rcv函数
处理,在tcp_rcv函数中去掉
TCP头,根据四元组(源IP,源端口,目的IP,目的端口)
查找对应的Socket
,如果找到对应的Socket则将网络数据包中的传输数据拷贝到Socket
中的接收缓冲区
中。如果没有找到,则发送一个目标不可达
的icmp
包。 -
内核在接收网络数据包时所做的工作我们就介绍完了,现在我们把视角放到应用层,当我们程序通过系统调用
read
读取Socket接收缓冲区
中的数据时,如果接收缓冲区中没有数据
,那么应用程序就会在系统调用上阻塞
,直到Socket接收缓冲区有数据
,然后CPU
将内核空间
(Socket接收缓冲区)的数据拷贝
到用户空间
,最后系统调用read返回
,应用程序读取
数据。
资料直通车:Linux内核源码技术学习路线+视频教程内核源码
性能开销
从内核处理网络数据包接收的整个过程来看,内核帮我们做了非常之多的工作,最终我们的应用程序才能读取到网络数据。
随着而来的也带来了很多的性能开销,结合前面介绍的网络数据包接收过程我们来看下网络数据包接收的过程中都有哪些性能开销:
-
应用程序通过
系统调用
从用户态
转为内核态
的开销以及系统调用返回
时从内核态
转为用户态
的开销。 -
网络数据从
内核空间
通过CPU拷贝
到用户空间
的开销。 -
内核线程
ksoftirqd
响应软中断
的开销。 -
CPU
响应硬中断
的开销。 -
DMA拷贝
网络数据包到内存
中的开销。
网络包发送流程
网络包发送过程
-
当我们在应用程序中调用
send
系统调用发送数据时,由于是系统调用所以线程会发生一次用户态到内核态的转换,在内核中首先根据fd
将真正的Socket找出,这个Socket对象中记录着各种协议栈的函数地址,然后构造struct msghdr
对象,将用户需要发送的数据全部封装在这个struct msghdr
结构体中。 -
调用内核协议栈函数
inet_sendmsg
,发送流程进入内核协议栈处理。在进入到内核协议栈之后,内核会找到Socket上的具体协议的发送函数。
比如:我们使用的是
TCP协议
,对应的TCP协议
发送函数是tcp_sendmsg
,如果是UDP协议
的话,对应的发送函数为udp_sendmsg
。
-
在
TCP协议
的发送函数tcp_sendmsg
中,创建内核数据结构sk_buffer
,将struct msghdr
结构体中的发送数据拷贝
到sk_buffer
中。调用tcp_write_queue_tail
函数获取Socket
发送队列中的队尾元素,将新创建的sk_buffer
添加到Socket
发送队列的尾部。
Socket
的发送队列是由sk_buffer
组成的一个双向链表
。
发送流程走到这里,用户要发送的数据总算是从
用户空间
拷贝到了内核
中,这时虽然发送数据已经拷贝
到了内核Socket
中的发送队列
中,但并不代表内核会开始发送,因为TCP协议
的流量控制
和拥塞控制
,用户要发送的数据包并不一定
会立马被发送出去,需要符合TCP协议
的发送条件。如果没有达到发送条件
,那么本次send
系统调用就会直接返回。
-
如果符合发送条件,则开始调用
tcp_write_xmit
内核函数。在这个函数中,会循环获取Socket
发送队列中待发送的sk_buffer
,然后进行拥塞控制
以及滑动窗口的管理
。 -
将从
Socket
发送队列中获取到的sk_buffer
重新拷贝一份
,设置sk_buffer副本
中的TC