0%

Linux网络内核源码分析|传输层之UDP处理过程

关于UDP

UDP数据包

source/dest:长16位,取值范围1~65535。

len:有效负载和UDP数据报头的总长度,单位为字节。

UDP初始化

UDP和其他核心协议都是在启动阶段通过方法inet_init()来初始化的。

定义对象udp_protocol,用inet_add_protocol()添加。

内核还提供了UDP传输层操作集:struct proto udp_prot{...},用proto_register()注册。

UDP模块处理流程

概述

回顾套接字发送数据的流程,见套接字之收发数据、监听、连接、绑定

send()|write()|sendto() → sys_send() → sys_sendto() → sock_sendmsg() → __sock_sendmsg() → sock->prot->sendmsg() → sk->sk_prot->sendmsg()

UDP采用SOCK_DGRAM类型的INET套接字,所以查询函数集后,最后两步具体调用的函数是:

→ inet_sendmsg() → udp_sendmsg()

自此,网络数据包从应用程序的发送函数开始,已经过套接字接口,应用程序把应用层数据内容置入内核,并通知内核的UDP协议模块继续发送数据。下面主要关注UDP协议模块中的处理流程,直到UDP协议模块将数据包交给IP层协议模块。

如果在应用层调用发送函数,若采用SOCK_DGRAM套接字,会触发内核udp_sendmsg调用,查找路由信息后,调用ip_append_data对数据做分片处理,然后调用udp_push_pending_frames进行UDP包封装,并把数据包交给IP层处理。

如果在应用层调用接收函数,IP层取出数据内容后,函数ip_local_deliver_finish会调用UDP模块的udp_rcv函数。数据包会被加入套接字队列,再由函数udp_recvmsg取出,并通知应用程序函数读取数据。

UDP包的发送

应用层采用SOCK_DGRAM套接字发送数据,会触发内核udp_sendmsg调用。

udp_sendmsg(iocb, sk, msg, len) udp.c

  1. 设置corkreq:用于指出是否应该使用缓冲区机制,如果没有则数据包立即被发送,如果有则数据包会交给udp_sendmsg()累积,直到取消该选项——最后一个数据包到达。
  2. 检查长度len是否越界。
  3. 检查msg中是否有MSG_OOB(指定flag为MSG_MORE时,corkreq=1,等于使用了缓冲区机制)。
  4. 通过检查msg->msg_name字段,判断目的地址是否合法:
    1. 目的地址不为空:检查是否目的地址是否合法,协议族是否正确,有错误则return -EINVAL;
    2. 目的地址为空:若套接字处于TCP_ESTABLISHED状态(套接字已连接)仍认为目的地址合法,允许继续传送数据;若套接字不处于TCP_ESTABLISHED状态,则return -EDESTADDRREQ;
  5. 检查msg->msg_controllen,即:如果是控制报文(不为0),则通过ip_cmsg_send()处理控制报文,该方法分析指定的msghdr对象,并创建一个icmp_cookie对象(包含可供处理数据包时使用的信息)。
  6. 确定是否需要路由信息,是否广播。
  7. 若套接字已连接,则不需要查询路由,从套接字管理信息中返回路由表信息,并记录到rtable中。
  8. 若套接字无路由信息(rt==NULL),则调用ip_route_output_flow()查询路由表。
  9. cork the socket: inet->cork.fl的赋值。
  10. ip_append_data():对UDP数据包进行分片处理,为IP层分片处理做好准备
  11. 若设置了corkreq,则需要调用lock_sock()获取套接字锁,之后再发送数据包,ip_append_data()会将数据加入缓冲区,不立即传输,之后第调用udp_push_pending_frames(sk, up)来完成传输。
  12. 出错则:udp_flush_pending_frames(sk)→ip_flush_pending_frames(),清空所有等待传输的SKB;
  13. 没有出错且corkreq=0(不使用缓冲机制)则:udp_push_pending_frames(sk, up);
  14. 一些结束处理。标签out和do_confirm。

ip_append_data ip_output.c

创建套接字缓冲区sk_buff,为IP层数据分片做好准备;该函数根据路由查询得到的接口MTU,把超过MTU长度的数据分片保存在多个套接字缓冲区中,并插入套接字的发送队列sk_write_queue中(对于较大的数据包,该函数可能循环多次)。

具体过程:

  1. 一些变量的声明。其中:
    1. 从sk结构中获取inet结构:struct inet_opt *inet = inet_sk(sk);inet结构保存套接字选项,inet->cork成员存储了与分片有关的控制信息。
  2. 判断套接字发送队列sk->sk_write_queue是否为空。
    1. 队列为空,则对inet->cork初始化,为分片做准备;
      1. ip选项不为空,则获取一些分片用的信息设置inet->cork;
      2. 初始化分片位置信息:设置sk的指针指向分片首地址、下一分片的存放位置;
    2. 队列不为空/不是第一个分片,则套接字缓冲区的data内容中没有头部格式信息。
  3. 从路由表项中得到网络设备的硬件头部信息。
  4. 分片首部长度:${fragheaderlen}={ip头部}+{ip选项长度(如果有)}$;获取分片最大长度(maxfraglen)。
  5. 累计分片数据的总长度,由inet->cork.length记录。
  6. 如果是空队列/是第一个分片,需要分配一个新的套接字缓冲区。
  7. 把尚未插入队列的新数据插入到套接字发送队列中。若$length>0$则说明数据还有剩余,需要继续分片并插入队列。
    1. 如果当前套接字缓冲区中没有空间装剩下的数据,则要分配新套接字缓冲区给剩下的数据;
      1. 分片长度:$fraglen={fragheaderlen}+{数据长度}$;
      2. 为最后一个碎片分配更多空间;
      3. 分配套接字缓冲区;
      4. 设置IP数据包的校验和模式(csummode),并初始化校验和(csum);
      5. 在套接字缓冲区中预留容纳硬件头头部的空间skb_reserve
      6. 为套接字缓冲区设置数据存放的起始位置:设置skb;
      7. 计算实际需要复制的数据长度(copy),把数据复制到套接字缓冲区中;
      8. 计算分片偏移位置,计算尚未分配套接字缓冲区的数据长度;
      9. 把已分配得到的套接字缓冲区插入套接字发送队列中。
    2. 判断网络设备是否设置了scatter/gather,如果有,则按照scatter/gather设置分片处理。

udp_push_pending_frames(sk,*up) udp.c

传入参数*up为struct udp_opt型。

1
2
3
4
5
6
7
8
struct udp_opt {
int pending;// Any pending frames ?
unsigned int corkflag;// Cork is required
__u16 encap_type;// Is this an Encapsulation socket?
// Following member retains the infomation to create a UDP header
// when the socket is uncorked.
__u16 len;// total length of pending frames
};

该函数具体执行:

  1. 一些变量的声明。其中:
    1. 从sk结构中获取inet结构:struct inet_opt *inet = inet_sk(sk);inet结构保存套接字选项,inet->cork成员存储了与分片有关的控制信息。
    2. 声明一个struct sk_buff *skb用于存套接字队列中的一个套接字缓冲区。
    3. 声明一个struct udphdr *uh用于保存udp头部信息。
  2. 从套接字发送队列(&sk->sk_write_queue)中得到一个套接字缓冲区,skb_peek
  3. 为数据包设置udp头部信息:源端口、目的端口、长度等,存于uh。
  4. 如果不需要校验和计算,则直接去发送该数据包goto send
  5. 校验和处理。
  6. send:准备IP协议处理,ip_push_pending_frames(sk)

UDP包的接收

发现接收了一个UDP包,IP层协议调用udp_rcv把数据包递交到UDP协议模块。

通过调用udp_queue_rcv_skb和sock_queue_rcv_skb,UDP协议模块把数据包插入套接字的接收队列中,等待udp_recvmsg从队列中取出数据包,递交到应用程序。

udp_rcv(skb) udp.c

被注册为接收UDP数据包的方法。

UDP初始化时,内核通过struct net_protocol udp_protocol注册接收UDP数据包的方法。

1
2
static struct net_protocol udp_protocol = {
.handler = udp_rcv,

udp_rcv函数具体内容:

  1. 判断套接字缓冲区中是否存在一个UDP头部长度的存储区。
    1. 如果不存在则goto no_header处理。
  2. 获取UDP包的起始位置uh,获取UDP包的长度ulen。
  3. 得到UDP数据包的长度,pskb_trim()
  4. 是否组播?是:udp_v4_mcast_deliver();否:继续。
  5. 根据套接字信息(源IP地址、源端口、目的IP地址、目的端口),查找端口上是否有一个打开的套接字?sk=udp_v4_lookup()
    1. 如果还有,则把套接字缓冲区插入套接字sk的接收队列中,udp_queue_rcv_skb()返回
    2. sk==NULL:继续。
  6. 检查校验和,校验和正确则继续。如果校验和错误,则丢掉该数据包,goto csum_error
  7. 返回一个ICMP包,通知对方目的地/端口不可达,icmp_send()
  8. 丢弃数据包。

参考:《Linux网络内核分析与开发》,肖宇峰、李昕、时岩编著。

udp_queue_rcv_skb(sk,skb) udp.c

udp_rcv所调用,通过sock_queue_rcv_skb把收到的套接字缓冲区skb插入套接字sk的接收队列中。

IPsec与XFRM框架

涉及到IPsec——Internet协议安全子系统:是一组协议,是大多数IPVPN技术的标准配置。这组协议对通信会话中的每个数据包进行身份验证和加密,以确保IP流量的安全。

IPsec是由XFRM框架实现的,XFRM读作’transfrom’。XFRM框架是独立于协议簇的,旨在提供适用于生产环境的IPv6和IPsec协议栈。

XFRM策略(xfrm_policy)和XFRM状态(xfrm_state)是XFRM框架中的基本数据结构。

XFRM策略:告诉IPsec是否要对特定流进行处理的规则,策略包含一个选择器(xfrm_selector),用于指定要将策略应用于哪些流。

XFRM状态:表示IPsec安全关联,包含加密密钥、标志、请求ID、统计信息、重放参数等信息。内核将IPsec安全关联存储在安全关联数据库中。

函数具体内容

  1. XFRM安全策略检查,xfrm4_policy_check(sk,XFRM_POLICY_IN,skb)
  2. 对IPsec封装包进行分析处理。
  3. 检查sk、skb,如果需要校验,则计算校验和。
  4. 调用函数sock_queue_rcv_skb(sk,skb)将套接字缓冲区skb插入套接字sk的接收队列中。

sock_queue_rcv_skb(sk,skb) sock.h

对套接字缓冲区skb做简单处理后,函数sock_queue_rcv_skb把套接字缓冲区插入套接字接收队列sk->sk_receive_queue的尾部。

  1. 套接字的包过滤处理,sk_filter()。出错则结束函数,返回错误信息。
  2. 设置套接字缓冲区skb的一些数据
    1. skb->dev置空;
    2. skb->sk指针指向当前套接字结构sk,skb_set_owner_r(skb,sk)
  3. 插入套接字接收队列的尾部, skb_queue_tail(&sk->sk_receive_queue, skb);
  4. 通知被阻塞的操作:套接字接收队列中已有数据就绪,sock_flag()

udp_recvmsg() udp.c

应用程序通过套接字调用接收函数时,会调用内核函数udp_recvmsg()。该函数从套接字队列中取出数据,并通过struct msghdr结构把其中的数据和地址信息复制给用户程序。

  1. 检查地址长度;检查队列中是否有错误信息。
  2. 从套接字sk的接收队列中取出套接字缓冲区skb,skb_recv_datagram()
  3. 准备复制数据:需要复制的数据(copied)不包括UDP头部;如果缓冲区长度不够(copied>len),则设置缓冲区长度,并作截断标志。
  4. 校验判断:
    1. 如果不需要校验(skb->ip_summed==CHECKSUM_UNNECESSRY),则把套接字缓冲区skb数据复制到msg->msg_iov结构中,以便应用程序从接收缓冲区中读取数据,skb_copy_datagram_iovec()
    2. 如果需要校验,则先校验,再把套接字缓冲区skb数据复制到msg->msg_iov结构,skb_copy_datagram_iovec()
    3. 其他情况:复制套接字缓冲区的内容并进行校验,skb_copy_and_csum_datagram_iovec()
  5. 记录接收时间,sock_recv_timestamp(msg,sk,skb)
  6. 复制地址信息:sin <- skb。
  7. 处理IP选项,ip_cmsg_recv(msg,skb)
  8. 如果用户程序接收数据采用MSG_PEEK标志,则读出数据,但不删除套接字队列中的缓冲。