1. 概述 UDP 是无连接协议,但应用程序仍可调用 connect() 将 socket 与远端地址绑定。 对于 UDP 而言,connect() 并不进行三次握手,而是完成路由查找及 socket 状态和路由缓存的更新。
1.1. 小结 UDP socket 的 connect() 调用在内核中经历了一条清晰的路径:
系统调用层
路由查找层 __ip4_datagram_connect() 构造 flowi4,依次调用 ip_route_connect() → ip_route_output_flow() → ip_route_output_key_hash(),最终由 fib_lookup() 在 FIB 表中查找匹配路由。
路由构造层
状态更新 __ip4_datagram_connect():
整个过程中,“第一次确定 saddr”发生在 ip_route_connect() 内对 fl4->saddr 的赋值,而 XFRM 查找则在 ip_route_output_flow() 中根据 flowi4_proto 是否非零来决定是否执行。最终,UDP socket 拥有了完整的四元组信息和缓存路由,后续 sendmsg 可直接使用该路由发送数据。
整个调用链的核心是路由查找 :通过 ip_route_connect() 触发 FIB 查找,在路由表中确定出接口、下一跳和源地址,最终将结果缓存到 socket 的 dst 中。
2. 调用栈
2.1. 系统调用入口函数 sys_connect 1 2 3 4 5 6 7 8 9 10 11 1 --> sys_connect 2 --> --> ip4_datagram_connect 3 --> --> --> __ip4_datagram_connect 4 --> --> --> --> ip_route_connect 5 --> --> --> --> --> if (!dst || !src) { // udp socket 源端口为 0 6 --> --> --> --> --> --> __ip_route_output_key // 暂不展开,后续还会进入 6 --> --> --> --> --> --> flowi4_update_output // 拿首次路由结果回填 fl 的出口和源目 IP 5 --> --> --> --> --> } 5 --> --> --> --> --> return ip_route_output_flow(net, fl4, sk); // 使用更新后的 fl 再次查询路由 4 --> --> --> --> sk->sk_state = TCP_ESTABLISHED; 4 --> --> --> --> sk_dst_set(sk, &rt->dst); // rt 是个 percpu 的 rtable
2.2. 路由查找的入口函数 ip_route_output_flow 1 2 3 4 5 6 6 --> --> --> --> --> --> rt = __ip_route_output_key(net, flp4); 7 --> --> --> --> --> --> --> return ip_route_output_key_hash(net, flp4, NULL); 8 --> --> --> --> --> --> --> --> rth = ip_route_output_key_hash_rcu(...) 8 --> --> --> --> --> --> --> --> return rth; 6 --> --> --> --> --> --> // XFRM 部分不影响结果,可忽略 6 --> --> --> --> --> --> return rt;
2.3. 路由查找及结果拼装函数 ip_route_output_key_hash_rcu 1 2 3 4 10 --> --> --> --> --> --> --> --> --> fib_lookup // FIB 路由查找,<核心入口> 10 --> --> --> --> --> --> --> --> --> fib_select_path // 从多个 nexthop 中选取一个 10 --> --> --> --> --> --> --> --> --> reth = __mkroute_output 10 --> --> --> --> --> --> --> --> --> return rth;
2.4. 路由结果拼装函数 __mkroute_output 1 2 3 4 5 6 7 8 11 --> --> --> --> --> --> --> --> --> --> if (fi) { 12 --> --> --> --> --> --> --> --> --> --> --> prth = raw_cpu_ptr(nhc->nhc_pcpu_rth_output); 12 --> --> --> --> --> --> --> --> --> --> --> if (rt_cache_valid(rth) && dst_hold_safe(&rth->dst)) 13 --> --> --> --> --> --> --> --> --> --> --> --> return rth; // 本 CPU 上后续路由查找在这里返回 11 --> --> --> --> --> --> --> --> --> --> } 11 --> --> --> --> --> --> --> --> --> --> rth = rt_dst_alloc(...) 11 --> --> --> --> --> --> --> --> --> --> rt_set_nexthop(rth, fl4->daddr, res, fnhe, fi, type, 0, do_cache); 11 --> --> --> --> --> --> --> --> --> --> return rth;
2.5. 路由结果缓存函数 rt_set_nexthop 1 2 3 4 12 --> --> --> --> --> --> --> --> --> --> nhc = FIB_RES_NHC(*res); 12 --> --> --> --> --> --> --> --> --> --> rt_cache_route(nhc, rt); // 把 rtable 设置到当前 CPU 的 nhc->nhc_pcpu_rth_output 上 13 --> --> --> --> --> --> --> --> --> --> --> p = (struct rtable **)raw_cpu_ptr(nhc->nhc_pcpu_rth_output); 13 --> --> --> --> --> --> --> --> --> --> --> cmpxchg(p, orig, rt);
3. 数据结构 3.1. socket 实例在内核的代表 struct sock 1 2 3 4 5 6 7 8 357 struct sock { 358 362 struct sock_common __sk_common ; ... 445 struct dst_entry __rcu *sk_dst_cache ;
内核里会使用 sock 关联用户态的多种 socket。 我们通过 socket() 系统调用创建 UDP socket 时,内核里会创建一个 sock 结构体与之对应。
后续内核某些系统调用(如 connect)会通过这个 socket 触发路由查找,查找成功后就会把路由结果缓存到结构体里的 *sk_dst_cache,例如 sk_dst_set(sk, &rt->dst);。 后续 socket 发包时,会直接使用这个路由,避免再次查找。
3.2. 路由查询的最终结果 struct rtable 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 60 struct rtable {61 struct dst_entry dst ;62 63 int rt_genid;64 unsigned int rt_flags;65 __u16 rt_type;66 __u8 rt_is_input;67 __u8 rt_uses_gateway;68 69 int rt_iif;70 71 u8 rt_gw_family;72 73 union {74 __be32 rt_gw4;75 struct in6_addr rt_gw6 ;76 };77 78 79 u32 rt_mtu_locked:1 ,80 rt_pmtu:31 ;81 };
路由查找最终返回给 socket 模块的结果就是个 rtable,里面包含:
dst:最通用(基本)的路由结果
rt_genid:版本号,路由条目变更后会增加这个值,确保路由结果检查时结果失效
rt_is_input:是 input(收包)阶段还是 output(发包)阶段触发的路由查找
rt_gw4/6:IPv4/IPv6 网关地址
rt_iif:input interface,保存的是入接口的 ifindex
其他字段在本场景中关联度不大,先忽略。
3.3. 路由查找结果的通用部分 struct dst_entry 1 2 3 4 5 6 7 8 26 struct dst_entry { 27 struct net_device *dev ; 28 struct dst_ops *ops ; ... 36 int (*input)(struct sk_buff *); 37 int (*output)(struct net *net, struct sock *sk, struct sk_buff *skb); ... 95 };
最基本的路由信息,包含:
其他字段在本场景中关联度不大,先忽略。
这里只是最通用的部分,其他部分在 rtable 里,比如 IPv4 网关就跟 IPv6 网关放在 rtable 中。
3.4. struct fib_result 1 2 3 4 5 6 7 8 9 10 11 12 169 struct fib_result {170 __be32 prefix;171 unsigned char prefixlen;172 unsigned char nh_sel;173 unsigned char type;174 unsigned char scope;175 u32 tclassid;176 struct fib_nh_common *nhc ;177 struct fib_info *fi ;178 struct fib_table *table ;179 struct hlist_head *fa_head ;180 };
路由查找过程中最重要的结构体,各个函数间传递路由查询结果,大部分时间是中间态的结果。 区别于 rtable:
rtable:最终结果
fib_result:中间过程结果
其中跟 UDP 发送相关的,有几个关键的域:
table:fi 所在的路由表,如 main、local,以及策略路由场景下的其他 table
fi:对应一个路由条目,比如 ip route get 192.168.1.9 返回的结果内容
nhc:下一跳的入口,如果路由有多个下一跳,这个是其中被选中的下一跳
3.5. struct fib_info 1 2 3 4 5 6 7 8 134 struct fib_info { ...154 int fib_nhs;158 struct nexthop *nh ;159 struct rcu_head rcu ;160 struct fib_nh fib_nh [];161 };162
每条路由条目,就有一个 fib_info 实例对应,挂在对应 table 的路由树上,是树的一个叶子节点。
使用上没有差别,可以不关注这个细节。
3.6. struct nexthop 路由条目的下一跳。如果存在多个下一跳,就会有多个 nexthop。
3.7. struct fib_nh_common 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 81 struct fib_nh_common { ... 91 union { 92 __be32 ipv4; 93 struct in6_addr ipv6 ; 94 } nhc_gw; 95 96 int nhc_weight; 97 atomic_t nhc_upper_bound; 98 99 100 struct rtable __rcu * __percpu *nhc_pcpu_rth_output ;101 struct rtable __rcu *nhc_rth_input ;102 struct fnhe_hash_bucket __rcu *nhc_exceptions ;103 };
4. 内核函数 4.1. ip4_datagram_connect 1 2 3 4 5 6 7 8 9 10 85 int ip4_datagram_connect (struct sock *sk, struct sockaddr *uaddr, int addr_len) 86 {87 int res;88 89 lock_sock(sk);90 res = __ip4_datagram_connect(sk, uaddr, addr_len);91 release_sock(sk);92 return res;93 }94 EXPORT_SYMBOL(ip4_datagram_connect);
函数说明 : UDP socket 对应ops里connect操作的函数。 TODO:系统调用如何借助OPS找到它,待专门介绍。
4.2. 2. __ip4_datagram_connect 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int __ip4_datagram_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)20 { 48 fl4 = &inet->cork.fl.u.ip4; 49 rt = ip_route_connect(fl4, usin->sin_addr.s_addr, saddr, oif, 50 sk->sk_protocol, inet->inet_sport, 51 usin->sin_port, sk); 64 if (!inet->inet_saddr) 65 inet->inet_saddr = fl4->saddr; 71 inet->inet_daddr = fl4->daddr; 72 inet->inet_dport = usin->sin_port; 74 sk->sk_state = TCP_ESTABLISHED; 78 sk_dst_set(sk, &rt->dst); 81 return err; 82 } 83 EXPORT_SYMBOL(__ip4_datagram_connect);
函数说明 :
调用 ip_route_connect() 执行路由查找。
对udp connect场景,路由查找前udp sock还没有源ip, 目的IP是用户指定的。
源port在本函数被调用前,就通过auto bind函数分配了。目的port是用户指定的。
路由查找结束后,会借助fl4返回最后一轮路由表匹配时的五元组等key信息。
返回值rt是个的路由查找的结果,后续会挂在udp socket下。
查找成功, 根据fl4更新sk里的缓存值,如目的ip,源ip等。
写入目的地址和目的端口,设置 sk_state = TCP_ESTABLISHED。
最后通过 sk_dst_set() 将路由结果记录到 socket。
4.3. ip_route_connect 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 309 static inline struct rtable *ip_route_connect (struct flowi4 *fl4, __be32 dst, 310 __be32 src, int oif, u8 protocol,311 __be16 sport, __be16 dport,312 const struct sock *sk) 313 {314 struct net *net = sock_net(sk);315 struct rtable *rt ;316 317 ip_route_connect_init(fl4, dst, src, oif, protocol, sport, dport, sk);318 319 if (!dst || !src) {320 rt = __ip_route_output_key(net, fl4);321 if (IS_ERR(rt))322 return rt;323 ip_rt_put(rt);324 flowi4_update_output(fl4, oif, fl4->daddr, fl4->saddr);325 }326 security_sk_classify_flow(sk, flowi4_to_flowi_common(fl4));327 return ip_route_output_flow(net, fl4, sk);328 }
函数说明 :
4.4. ip_route_output_flow 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 2869 struct rtable *ip_route_output_flow (struct net *net, struct flowi4 *flp4, 2870 const struct sock *sk) 2871 {2872 struct rtable *rt = __ip_route_output_key(net, flp4);2873 2874 if (IS_ERR(rt))2875 return rt;2876 2877 if (flp4->flowi4_proto) {2878 flp4->flowi4_oif = rt->dst.dev->ifindex;2879 rt = (struct rtable *)xfrm_lookup_route(net, &rt->dst,2880 flowi4_to_flowi(flp4),2881 sk, 0 );2882 }2883 2884 return rt;2885 }2886 EXPORT_SYMBOL_GPL(ip_route_output_flow);
函数说明 :
4.5. __ip_route_output_key 1 2 3 4 5 125 static inline struct rtable *__ip_route_output_key (struct net *net ,126 struct flowi4 *flp4 ) 127 { 128 return ip_route_output_key_hash(net, flp4, NULL );129 }
这个函数本身就是个套壳的,但却是个核心入口函数。 在路由查找时,经常被直接或者间接多次调用 UDP connect场景下,
4.6. ip_route_output_key_hash 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 2393 struct rtable *ip_route_output_key_hash (struct net *net, struct flowi4 *flp4, 2394 const struct sk_buff *skb) 2395 {2396 struct fib_result res = {2397 .type = RTN_UNSPEC,2398 .fi = NULL ,2399 .table = NULL ,2400 .tclassid = 0 ,2401 };2402 struct rtable *rth ;2403 2404 flp4->flowi4_iif = LOOPBACK_IFINDEX;2405 flp4->flowi4_flags |= FLOWI_FLAG_KNOWN_NH;2406 rth = ip_route_output_key_hash_rcu(net, flp4, &res, skb);2407 return rth;2408 }2409 EXPORT_SYMBOL_GPL(ip_route_output_key_hash);
函数说明 :
4.7. ip_route_output_key_hash_rcu 1 2 3 4 5 6 7 8 9 10 11 12 2564 struct rtable *ip_route_output_key_hash_rcu (struct net *net, 2565 struct flowi4 *fl4,2566 struct fib_result *res,2567 const struct sk_buff *skb) 2568 { ...2754 err = fib_lookup(net, fl4, res, 0 ); ...2810 fib_select_path(net, res, fl4, skb); ...2815 rth = __mkroute_output(res, fl4, orig_oif, dev_out, flags);2818 return rth;
函数说明 :
首先校验源地址合法性,组播/广播/零网地址直接返回错误。
调用 fib_lookup()根据fl4里的key查找路由表/树(talbes),结果存入fib_result res。
fib_select_path() 在有多个下一跳时,选择一个下一跳(nexthop)。
最终由 __mkroute_output() 构造 rtable 并返回。
4.8. fib_lookup 1 2 310 static inline int fib_lookup (struct net *net, const struct flowi4 *flp, 311 struct fib_result *res, unsigned int flags)
函数说明 : 通过路由表/树,查找路由的入口函数,也是核心函数。 详细分解见。。。, flp时查找时的key, res是查找的最终结果。 res的结果是跟路由树直接相关的,只能再路由系统内部使用,不能直接给其他模块比如socket。 所以需要在res基础上构造rtable再作为通用结果返回给socket。
4.9. fib_select_path 1 2 2253 void fib_select_path(struct net *net, struct fib_result *res,2254 struct flowi4 *fl4, const struct sk_buff *skb)
从多个下一跳里选择一个, 原理及详细介绍见。。。
4.10. __mkroute_output 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 2503 static struct rtable *__mkroute_output (const struct fib_result *res ,2504 const struct flowi4 *fl4 , int orig_oif , 2505 struct net_device *dev_out , 2506 unsigned int flags ) 2507 { 2508 struct fib_info *fi = res->fi; ...2568 if (fi) {2569 struct fib_nh_common *nhc = FIB_RES_NHC(*res);2570 struct rtable __rcu **prth ;2571 2572 fnhe = find_exception(nhc, fl4->daddr);2573 if (!do_cache)2574 goto add;2575 if (fnhe) {2576 prth = &fnhe->fnhe_rth_output;2577 } else { ...2585 prth = raw_cpu_ptr(nhc->nhc_pcpu_rth_output);2586 }2587 rth = rcu_dereference(*prth);2588 if (rt_cache_valid(rth) && dst_hold_safe(&rth->dst))2589 return rth;2590 }2591 2592 add:2593 rth = rt_dst_alloc(dev_out, flags, type,2594 IN_DEV_ORCONF(in_dev, NOXFRM)); ...2619 rt_set_nexthop(rth, fl4->daddr, res, fnhe, fi, type, 0 , do_cache);2620 lwtunnel_set_redirect(&rth->dst);2621 2622 return rth;2623 }
函数说明 :
udp connect场景下, fi是对应路由条目,因为非空,
fnhe对应的fib nhc 的exception场景, 跟本场景无关,可忽略。
从nhc下,获取当前cpu 对应的nhc->nhc_pcpu_rth_output.
备注:这里的per cpu的指针,指向的是个rtable的指针。 这个我也不懂。脱裤子XX?
判断上一步的路由是否有效,有效则直接返回。 //同cpu上的, 除首次查找,都会走这个分支,返回本cpu缓存的rtable。
如果没有找到,再alloc一个rtable,并通过rt_set_nexthop缓存下来。
4.11. rt_set_nexthop 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 1570 static void rt_set_nexthop (struct rtable *rt, __be32 daddr, 1571 const struct fib_result *res,1572 struct fib_nh_exception *fnhe,1573 struct fib_info *fi, u16 type, u32 itag,1574 const bool do_cache) 1575 {1576 bool cached = false ;1577 1578 if (fi) { ...1602 if (unlikely(fnhe))1603 cached = rt_bind_exception(rt, fnhe, daddr, do_cache);1604 else if (do_cache)1605 cached = rt_cache_route(nhc, rt); ...1618 } else 1619 rt_add_uncached_list(rt); ...1627 }
函数说明 :
当 do_cache 为真时,将路由写入 FIB nexthop 的每 CPU 缓存。
4.12. rt_cache_route 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 1471 static bool rt_cache_route (struct fib_nh_common *nhc, struct rtable *rt) 1472 {1473 struct rtable *orig , *prev , **p ;1474 bool ret = true ;1475 1476 if (rt_is_input_route(rt)) {1477 p = (struct rtable **)&nhc->nhc_rth_input;1478 } else {1479 p = (struct rtable **)raw_cpu_ptr(nhc->nhc_pcpu_rth_output);1480 }1481 orig = *p;1482 1483 1486 dst_hold(&rt->dst);1487 prev = cmpxchg(p, orig, rt);1488 if (prev == orig) {1489 if (orig) {1490 rt_add_uncached_list(orig);1491 dst_release(&orig->dst);1492 }1493 } else {1494 dst_release(&rt->dst);1495 ret = false ;1496 }1497 1498 return ret;1499 }
函数说明 :
通过cmpxchg,把rtable的dst指针,保存到当前cpu的(nhc->nhc_pcpu_rth_output)
本文基于 Linux 6.6 内核源码分析,仅供学习交流。