Linux内核传输层UDP源码分析
一、用户数据包协议(UDP)
1.UDP数据报头
UDP 提供面向消息的不可靠传输,但没有拥塞控制功能。很多协议都使用 UDP,如用于 IP 网络传输音频和视频的实时传输协议 (Real-time Transport Protocol,RTP),此类型容许一定的数据包丢弃。UDP 报头长 8 字节,具体内核源码如下:
2.UDP初始化操作
定义对象udp_protocol(net_protocol对象)并使用方法inet_add_protocol()来添加它,具体源码如下:
1.
udp_protocol
结构体分析(协议注册)static struct net_protocol udp_protocol = { .early_demux = udp_v4_early_demux, // 早期解复用函数,处理初始阶段的数据包 .early_demux_handler = udp_v4_early_demux, // 早期解复用处理器 .handler = udp_rcv, // UDP 数据包接收处理函数,处理完整的 UDP 数据接收 .err_handler = udp_err, // 错误处理函数,处理 UDP 传输中的错误 .no_policy = 1, // 标记是否忽略策略检查 .netns_ok = 1 // 标记是否支持网络命名空间 };
- 作用:这是内核中 UDP 协议的注册实现。通过定义
net_protocol
类型的udp_protocol
,向内核网络子系统注册 UDP 协议的处理函数。内核通过inet_add_protocol()
方法将其加入协议处理链,使内核能够识别和处理 UDP 数据包。2.
inet_init_net
初始化(端口范围等配置)
- 作用:初始化网络命名空间内的 IPv4 相关参数。例如设置本地端口分配范围(
ip_local_ports
),限制程序动态申请端口的范围;配置 ping 套接字的用户组权限(ping_group_range
),控制哪些用户组可创建 ping 套接字。
3.发送UDP数据包udp_sendmsg(...)
从UDP用户空间套接字中发送数据,可以使用系统调用send()、sendto()、sendmsg()和write()。这些系统调用最终都会由内核中的方法udp_sendmsg()来处理。以下是这个函数的流程图:
1. 函数参数
struct sock *sk
:指向套接字的指针,该套接字表示当前进行 UDP 数据发送操作所使用的套接字对象,其中包含了套接字的各种状态信息和配置。struct msghdr *msg
:指向msghdr
结构体的指针,这个结构体包含了要发送的数据以及目标地址等信息,例如数据缓冲区、目标地址结构体等。size_t len
:表示要发送的数据的长度。2. 函数功能
udp_sendmsg
函数是 Linux 内核中用于通过 UDP 套接字发送数据的核心函数。它会对传入的消息进行一系列的检查和处理,包括验证目标地址、处理控制消息、查找路由表等操作,最终将数据封装成 UDP 数据包并发送出去。如果在发送过程中遇到错误,会返回相应的错误码;如果发送成功,则返回实际发送的数据长度。3. 重要部分及流程讲解
3.1 基本参数检查和初始化
if (len > 0xFFFF) return -EMSGSIZE; if (msg->msg_flags & MSG_OOB) return -EOPNOTSUPP; getfrag = is_udplite ? udplite_getfrag : ip_generic_getfrag;
- 首先检查要发送的数据长度是否超过了 UDP 数据包的最大长度(
0xFFFF
),如果超过则返回EMSGSIZE
错误。- 接着检查消息标志中是否包含
MSG_OOB
(带外数据),由于 UDP 不支持带外数据,所以如果包含则返回EOPNOTSUPP
错误。- 根据套接字是否为 UDP-Lite 类型,选择不同的数据获取函数。
3.2 处理已 corked 的套接字
fl4 = &inet->cork.fl.u.ip4; if (up->pending) { lock_sock(sk); if (likely(up->pending)) { if (unlikely(up->pending != AF_INET)) { release_sock(sk); return -EINVAL; } goto do_append_data; } release_sock(sk); }
- 检查套接字是否已经处于 corked 状态(即有未发送的数据包)。如果是,则加锁并进一步检查状态是否正确,若不正确则返回
EINVAL
错误,若正确则跳转到do_append_data
标签处继续处理。3.3 获取并验证目标地址
if (usin) { if (msg->msg_namelen < sizeof(*usin)) return -EINVAL; if (usin->sin_family != AF_INET) { if (usin->sin_family != AF_UNSPEC) return -EAFNOSUPPORT; } daddr = usin->sin_addr.s_addr; dport = usin->sin_port; if (dport == 0) return -EINVAL; } else { if (sk->sk_state != TCP_ESTABLISHED) return -EDESTADDRREQ; daddr = inet->inet_daddr; dport = inet->inet_dport; connected = 1; }
- 如果
msg
中包含目标地址信息,则检查地址长度和地址族是否正确,获取目标 IP 地址和端口号,并验证端口号是否有效。- 如果
msg
中不包含目标地址信息,则检查套接字是否已经连接,如果未连接则返回EDESTADDRREQ
错误,否则使用套接字中保存的目标地址和端口号,并标记为已连接。3.4 处理控制消息
if (msg->msg_controllen) { err = udp_cmsg_send(sk, msg, &ipc.gso_size); if (err > 0) err = ip_cmsg_send(sk, msg, &ipc, sk->sk_family == AF_INET6); if (unlikely(err < 0)) { kfree(ipc.opt); return err; } if (ipc.opt) free = 1; connected = 0; }
3.5 查找路由表
if (connected) rt = (struct rtable *)sk_dst_check(sk, 0); if (!rt) { struct net *net = sock_net(sk); __u8 flow_flags = inet_sk_flowi_flags(sk); fl4 = &fl4_stack; flowi4_init_output(fl4, ipc.oif, ipc.sockc.mark, tos, RT_SCOPE_UNIVERSE, sk->sk_protocol, flow_flags, faddr, saddr, dport, inet->inet_sport, sk->sk_uid); security_sk_classify_flow(sk, flowi4_to_flowi(fl4)); rt = ip_route_output_flow(net, fl4, sk); if (IS_ERR(rt)) { err = PTR_ERR(rt); rt = NULL; if (err == -ENETUNREACH) IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES); goto out; } err = -EACCES; if ((rt->rt_flags & RTCF_BROADCAST) && !sock_flag(sk, SOCK_BROADCAST)) goto out; if (connected) sk_dst_set(sk, dst_clone(&rt->dst)); }
- 如果套接字已经连接,则检查缓存的路由信息是否可用。
- 如果没有可用的路由信息,则初始化
flowi4
结构体,进行安全分类,并调用ip_route_output_flow
函数查找路由表。如果查找失败,则根据错误码进行相应处理,如增加统计信息并跳转到out
标签处。- 如果路由表中包含广播标志,但套接字不允许广播,则返回
EACCES
错误。3.6 发送数据
if (!corkreq) { struct inet_cork cork; skb = ip_make_skb(sk, fl4, getfrag, msg, ulen, sizeof(struct udphdr), &ipc, &rt, &cork, msg->msg_flags); err = PTR_ERR(skb); if (!IS_ERR_OR_NULL(skb)) err = udp_send_skb(skb, fl4, &cork); goto out; } lock_sock(sk); if (unlikely(up->pending)) { release_sock(sk); net_dbg_ratelimited("socket already corked\n"); err = -EINVAL; goto out; } fl4 = &inet->cork.fl.u.ip4; fl4->daddr = daddr; fl4->saddr = saddr; fl4->fl4_dport = dport; fl4->fl4_sport = inet->inet_sport; up->pending = AF_INET; do_append_data: up->len += ulen; err = ip_append_data(sk, fl4, getfrag, msg, ulen, sizeof(struct udphdr), &ipc, &rt, corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags); if (err) udp_flush_pending_frames(sk); else if (!corkreq) err = udp_push_pending_frames(sk); else if (unlikely(skb_queue_empty(&sk->sk_write_queue))) up->pending = 0; release_sock(sk);
- 如果不需要 cork 操作(即
corkreq
为false
),则调用ip_make_skb
函数创建一个skb
(套接字缓冲区),并调用udp_send_skb
函数发送数据。- 如果需要 cork 操作,则加锁并检查套接字状态,更新相关信息,然后调用
ip_append_data
函数将数据追加到缓冲区中。如果追加过程中出现错误,则调用udp_flush_pending_frames
函数清空缓冲区;如果不需要 cork 操作,则调用udp_push_pending_frames
函数将缓冲区中的数据发送出去。3.7 清理资源并返回结果
out: ip_rt_put(rt); out_free: if (free) kfree(ipc.opt); if (!err) return len; if (err == -ENOBUFS || test_bit(SOCK_NOSPACE, &sk->sk_socket->flags)) { UDP_INC_STATS(sock_net(sk), UDP_MIB_SNDBUFERRORS, is_udplite); } return err;
如果
- 释放路由表资源和控制消息相关的内存。
- 如果没有错误发生,则返回实际发送的数据长度;如果出现错误,则根据错误码进行相应的统计更新并返回错误码。
msg
中包含控制消息,则调用udp_cmsg_send
和ip_cmsg_send
函数处理这些控制消息。如果处理过程中出现错误,则释放相关资源并返回错误码。
4.接收来自网络层的UDP数据包udp_rcv(...)
方法udp_rcv()是负责接收来自网络层的UDP数据包的主要处理程序,函数流程如下:
1. 函数参数和功能
函数参数
struct sk_buff *skb
:指向套接字缓冲区的指针,其中包含接收到的 UDP 数据包。struct udp_table *udptable
:指向 UDP 表的指针,该表用于存储 UDP 套接字的相关信息。int proto
:协议类型,通常为IPPROTO_UDP
。函数功能
udp_rcv
函数是一个简单的包装函数,它直接调用__udp4_lib_rcv
函数来处理接收到的 UDP 数据包。__udp4_lib_rcv
函数是处理 UDP 数据包接收的核心函数,它会对数据包进行一系列的验证和处理,包括检查数据包长度、校验和,查找对应的套接字,将数据包传递给相应的套接字进行处理,或者在找不到合适套接字时发送 ICMP 错误消息。2. 重要部分及流程讲解
2.1 基本参数检查和初始化
if (!pskb_may_pull(skb, sizeof(struct udphdr))) goto drop; /* No space for header. */ uh = udp_hdr(skb); ulen = ntohs(uh->len); saddr = ip_hdr(skb)->saddr; daddr = ip_hdr(skb)->daddr;
- 首先检查套接字缓冲区中是否有足够的空间来存储 UDP 头部,如果没有则跳转到
drop
标签处丢弃数据包。- 提取 UDP 头部信息,包括 UDP 数据包长度
ulen
,源 IP 地址saddr
和目的 IP 地址daddr
。2.2 数据包长度验证
if (ulen > skb->len) goto short_packet; if (proto == IPPROTO_UDP) { /* UDP validates ulen. */ if (ulen < sizeof(*uh) || pskb_trim_rcsum(skb, ulen)) goto short_packet; uh = udp_hdr(skb); }
- 检查 UDP 头部中记录的数据包长度
ulen
是否超过了实际接收到的数据包长度skb->len
,如果超过则跳转到short_packet
标签处处理。- 如果协议类型为
IPPROTO_UDP
,还需要检查ulen
是否小于 UDP 头部长度,或者调用pskb_trim_rcsum
函数对数据包进行裁剪和校验和更新,如果出现问题则跳转到short_packet
标签处。2.3 校验和初始化
if (udp4_csum_init(skb, uh, proto)) goto csum_error;
调用
udp4_csum_init
函数对 UDP 数据包的校验和进行初始化,如果初始化失败则跳转到csum_error
标签处处理。2.4 查找套接字并处理
sk = skb_steal_sock(skb); if (sk) { struct dst_entry *dst = skb_dst(skb); int ret; if (unlikely(sk->sk_rx_dst != dst)) udp_sk_rx_dst_set(sk, dst); ret = udp_unicast_rcv_skb(sk, skb, uh); sock_put(sk); return ret; }
- 尝试从套接字缓冲区中获取关联的套接字
sk
。- 如果获取到了套接字,检查接收目标地址是否与套接字中的记录一致,如果不一致则更新。
- 调用
udp_unicast_rcv_skb
函数将数据包传递给该套接字进行处理,并返回处理结果。2.5 处理广播或多播数据包
if (rt->rt_flags & (RTCF_BROADCAST|RTCF_MULTICAST)) return __udp4_lib_mcast_deliver(net, skb, uh, saddr, daddr, udptable, proto);
如果路由表项中包含广播或多播标志,则调用
__udp4_lib_mcast_deliver
函数处理广播或多播数据包。2.6 再次查找套接字
sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable); if (sk) return udp_unicast_rcv_skb(sk, skb, uh);
如果之前没有找到关联的套接字,再次调用
__udp4_lib_lookup_skb
函数根据源端口和目的端口在 UDP 表中查找合适的套接字。如果找到则将数据包传递给该套接字进行处理并返回结果,这个过程通常包含以下步骤:
- 把接收到的数据添加到套接字的接收缓冲区。
- 若应用程序正在阻塞等待数据,就会唤醒该应用程序,让其从接收缓冲区读取数据。
- 若应用程序采用的是非阻塞模式,就会在后续的轮询或事件通知中得知有新数据到达。
2.7 策略检查和校验和检查
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) goto drop; nf_reset_ct(skb); /* No socket. Drop packet silently, if checksum is wrong */ if (udp_lib_checksum_complete(skb)) goto csum_error;
- 重置连接跟踪信息。
- 调用
udp_lib_checksum_complete
函数检查 UDP 数据包的校验和,如果校验和错误则跳转到csum_error
标签处处理。2.8 发送 ICMP 错误消息
__UDP_INC_STATS(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE); icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0); /* * Hmm. We got an UDP packet to a port to which we * don't wanna listen. Ignore it. */ kfree_skb(skb); return 0;
如果没有找到合适的套接字,增加相应的统计信息,调用
icmp_send
函数发送 ICMP 目的不可达(端口不可达)消息,然后释放套接字缓冲区并返回 0。2.9 错误处理
short_packet: net_dbg_ratelimited("UDP%s: short packet: From %pI4:%u %d/%d to %pI4:%u\n", proto == IPPROTO_UDPLITE ? "Lite" : "", &saddr, ntohs(uh->source), ulen, skb->len, &daddr, ntohs(uh->dest)); goto drop; csum_error: /* * RFC1122: OK. Discards the bad packet silently (as far as * the network is concerned, anyway) as per 4.1.3.4 (MUST). */ net_dbg_ratelimited("UDP%s: bad checksum. From %pI4:%u to %pI4:%u ulen %d\n", proto == IPPROTO_UDPLITE ? "Lite" : "", &saddr, ntohs(uh->source), &daddr, ntohs(uh->dest), ulen); __UDP_INC_STATS(net, UDP_MIB_CSUMERRORS, proto == IPPROTO_UDPLITE); drop: __UDP_INC_STATS(net, UDP_MIB_INERRORS, proto == IPPROTO_UDPLITE); kfree_skb(skb); return 0;
调用
short_packet
标签处:记录数据包长度过短的调试信息,然后跳转到drop
标签处。csum_error
标签处:记录校验和错误的调试信息,增加校验和错误的统计信息,然后跳转到drop
标签处。drop
标签处:增加接收错误的统计信息,释放套接字缓冲区并返回 0。xfrm4_policy_check
函数进行安全策略检查,如果检查不通过则跳转到drop
标签处丢弃数据包。
5.UDP使用流程
1. 用户层调用(sendto)
- 用户通过 socket () 创建 UDP 套接字,调用 sendto () 发送数据报
- 参数包含目标 IP (47.95.193.211)、端口 (默认可能未指定,需填充)
- 内核进入 inet_sendmsg () 处理,分配 skb_buff(创建位置:传输层)
2. 传输层处理(UDP 层)
- 创建 sk_buff 数据结构(skb->dev = 网络设备指针)
- 填充 UDP 头(源端口 8888,目标端口待确认)
- 计算校验和(可选,由 net.core.udp_checksum 内核参数控制)
- skb->protocol = htons (ETH_P_IP),标识上层协议
3. 网络层处理(IP 层)
- 调用 ip_queue_xmit () 进行路由查找
- 路由表查询:
- 使用 FIB 表(fib_rules)查找最佳路由
- 确定输出设备(eth0/wlan0 等)
- 下一跳地址(可能是网关地址,若目标不在同一子网)
- 填充 IP 头:
- 源 IP (192.168.186.138),目标 IP (47.95.193.211)
- TTL、协议号 (17)、校验和
- 检查是否需要分片(根据 MTU)
4. 链路层处理
- 调用 dev_queue_xmit () 进入链路层
- ARP 表查询:
- 使用 neigh_table 结构查找 arp_cache
- 若下一跳是网关,查询网关 MAC
- 若目标在同一子网,直接查目标 MAC
- 若 ARP 缓存缺失,触发 ARP 请求
- 填充链路层头(以太网为例):
- 源 MAC(本地网卡 MAC)
- 目标 MAC(下一跳或目标 MAC)
- 类型字段 0x0800(IP 协议)
5. 物理层传输
- sk_buff 通过 net_device_ops->ndo_start_xmit () 发送
- 物理层将二进制数据转换为电信号 / 光信号传输
接收流程简要说明:
- 物理层接收信号并转为二进制数据
- 链路层剥离帧头,检查 CRC 校验
- IP 层校验和验证,路由表查找输入接口
- UDP 层校验和验证,端口号匹配
- 用户层通过 recvfrom () 接收数据
关键数据结构说明:
- sk_buff:贯穿各层,包含协议头、数据负载、设备指针等
- fib_rules:路由规则表,用于确定输出路径
- arp_cache:基于 neigh_table 的邻居缓存,存储 IP-MAC 映射
- net_device:网络设备结构体,包含发送 / 接收函数指针