深入浅出用户态协议栈
一、前言
在讲网络协议栈前,先理解一个数据包在网络传输是一个怎么样的流程,如下图所示。
正常的流程是网卡接收到数据后,把数据 copy 到协议栈(sk_buff),协议栈把 sk_buff 数据解析完后再把数据放到 recv_buff,此时应用程序调用 recv 把数据从协议栈 copy 到应用程序;发送数据包,则与之相反,应用程序调用 send 把数据包 copy 到 send_buff,协议栈从 send_buff 取数据放到 sk_buff,交给网卡发送出去。这个过程有多次拷贝,为避免多次拷贝,使用 dma 的方式(零拷贝),把网卡的数据直接映射到内存,再由应用程序访问内存。
二、数据包分析
从网卡接收到一帧完整的数据包,可以使用原生的 socket、netmap、dpdk 等,完整的一帧数据由以太网头、IP 头、tcp/udp 头、用户数据构成,这些层级涉及到 7 层网络模型 OSI,如 udp 协议分布到 7 层 OSI 如下图所示。
由上图可知,以太网头属于链路层、IP 头属于网络层、UDP 头属于传输层,而实际的用户数据在应用层。
(1)以太网头
以太网头分布如下图所示。
对应结构体如下,由此可知 MAC 地址存在以太网头。
(2)IP 头
IP 头结构如下图所示。
其数据结构如下所示,IP 地址在 IP 头中,属于网络层。
(3)协议头
该层涉及到具体的不同协议,就有不同的结构,本文主要分析 udp 和 tcp
(a)udp 协议结构如图所示。
其数据结构如下,由此可知端口在协议头里面,属于传输层。
(b) tcp 协议头如图所示。
其数据结构如下,属于传输层。
(c)arp 和 icmp
arp 和 icmp 头定义如下。
arp 是地址解析协议,在局域网中,每一台主机都会对局域网内每一台机器进行广播 arp 包,当收到对端主机 arp 请求包后,把本机的 IP 和 MAC 地址做为响应发送回请求方,发出请求的主机便可获得整个局域网内所有主机的 IP 和 MAC 地址,并保存到 arp 表中,记录着局域网所有机器的 IP 和 MAC 地址信息;当 arp 表中某台机器的 arp 信息超时后,就会从 arp 表中删除,导致收不到数据;所以在局域网内网络通信看似是通过 IP,其实是通过 MAC 地址。
ICMP 是 Internet 控制报文协议,在命令行上 ping + IP 地址,此时发送的就是向目标主机发送 ICMP 请求,目标主机收到 ICMP 求情后,就会响应 ICMP,表明两台主机的网络是畅通的。
相关视频推荐
C/C++Linux服务器开发高级架构师/C++后台开发架构师免费学习地址
另外还整理一些 C++后台开发架构师 相关学习资料,面试题,教学视频,以及学习路线图,免费分享有需要的可以自行添加:Q群720209036~点击加入 需要自取
由以上每个协议头的定义,得到 udp、tcp、arp、icmp 它们的协议 packet 可定义如下。
三、深入理解网络协议栈
从网络协议栈是如何实现 tcp 连接、传输数据、断开连接,经过这 3 个方面加深对网络协议栈了解。
(1)三次握手
tcp 三次握手流程图如下。
客户端发送 syn 包开始第一次握手,服务端收到 syn 后完成第一次握手;服务端发送 ack 包开始第二次握手,acknum 等于第一次握手 seqnum+1,客户端收到 ack 后完成第二次握手,客户端发送 ack 开始第三次握手,acknum 等于第二次握手 seqnum+1,服务端收到 ack 后完成第三次握手。
第一次握手完成时,服务端从 IP 头、TCP 头获取到源 IP、目的 IP、源端口、目的端口、协议等信息构成五元组,存到半连接队列节点中;当第三次握手完成,遍历半连接队列找到对应的节点,并把节点移动到全连接队列中,应用层调用 accept 消费全连接队列数据,并分配 fd,全连接队列每个节点可以叫 tcp 控制块,fd 与 tcp 控制块一一对应。
在三次握手过程中存在 3 个状态(状态机):
(a)listen:服务器处于 listen 状态;
(b)syn_recv:服务器接收到数据包之后进入 syn_recv 状态;
(c)established:在接收完数据后进入 established 状态。
以上 3 个状态存在 tcp 控制块中。
应用层调用 listen(fd, backlog),参数 backlog 有两种理解,在 linux 系统中指的是半连接队列的长度,在 unix 系统中指半连接队列和全连接队列大小之和。
如果第三次握手 ack 包丢失,那么第二次握手会不会重发 ack 包, 答案是不会重发,没有重发得意义,但是包的超时可以设置。这就引出了超时怎么计算,客户端发包到服务端,服务端发包到客户端,客户端发包开始记录一个时间,到客户端收到包时也记录一个时间,服务端也类似发包记录一个时间到收包也记录一个时间,这个往返的时间叫做 RTT,当前 RTT 往返时间 = 上一次 RTT*0.9 + 下一次 RTT*0.1。
(2)数据传输
tcp 传输并不是发一个包回一个 ack 再发下一个包,这样速度很慢,实际是多个包一起发,再等待 ack 确认。这样导致不能保证先发的数据就是先到,后发的数据后到,tcp 为了保证顺序,引入了超时重传的机制。收到一个包,启动一个 200ms 定时器,等待接收到下一个包,如果在 200ms 内收到,就会重置定时器等待接收下一个包,如果 200ms 没收到就会超时,超时后,就会遍历那个包没有收到,并回一个 ack 确认消息告知发送端那没有收到,让发送端从该数据包开始,包括之后的数据包都要重新发。
慢启动,拥塞控制如下图所示。
一开始数量是指数级增长(慢启动),到达初始化的阈值后线性增长,增长到对方接收数据时,回 ack 的包超过 RTT 时间,这时网络拥塞,数据包太多来不及处理,此时降一半。
tcp 重要的定时器有:
(a)超时重传
(b)坚持定时器,当 cwin=0 时,接收端告诉发送端不能再发数据了,如果客户端想再发送数据,就会启动一个坚持定时器,发一个探测包给接收端,告诉对端你能不能接收数据,接收端 recv_buff 不满时,就会回 ack 告知发送端可以发数据了。
(c)keepalive
(d)time_wait,4 次挥手中避免最后一次 ack 丢失
(3)四次挥手
四次挥手的流程如下图所示。
(a) 客户端调用 close(fd)发送 fin 包,服务端收到 fin 包后,回 ack 包确认;
(b) 服务端在处理完缓冲区的数据后,调用 close(fd)关闭对应的 fd,发送 fin 包;
(c) 客户端收到 fin 包后,回 ack 包确认,等待 2msl(2 个数据包发送周期)后释放连接;
(d) 服务端收到 ack 包后,释放连接。
思考:
(1)服务端大量出现 close_wait 如何解?
原因:
服务端 recv()返回 0 后,处理数据不及时,导致 close 调用不及时。
解决思路:
(a)检查代码有没有调用 close;
(b)把处理数据做成异步处理,即抛到线程异步处理;
(2)客户端出现大量的 fin_wait_2 如何解?
原因:
服务端 recv 返回 0 后,不调用 close,客户端就会出现 fin_wait_2
解决思路:
(a)服务端处理;
(b)客户端 kill;
(c)重新建个连接。
(3)客户端出现大量的 time_wait?
原因是客户端发送的最后一次 ack 包,服务端没有收到,超时后服务端重发 fin 包,导致客户端出现大量的 time_wait。
(4)TCP 既然有 keepalive 探活包,为什么应用层也需要做心跳检测?
因为探活包在传输层,无法判断进程阻塞或者死锁的情况。
评论