Linux 6.6 UDP Socket connect 调用链源码分析

1. 概述

UDP 是无连接协议,但应用程序仍可调用 connect()socket 与远端地址绑定。
对于 UDP 而言,connect() 并不进行三次握手,而是完成路由查找及 socket 状态和路由缓存的更新。

1.1. 小结

UDP socketconnect() 调用在内核中经历了一条清晰的路径:

  1. 系统调用层

    • 系统调用通过 ops 找到 ip4_datagram_connect(),提取用户传入的地址和端口,处理组播特殊逻辑;

    • 然后调用 __ip4_datagram_connect()

  2. 路由查找层
    __ip4_datagram_connect() 构造 flowi4,依次调用 ip_route_connect()ip_route_output_flow()ip_route_output_key_hash(),最终由 fib_lookup() 在 FIB 表中查找匹配路由。

  3. 路由构造层

    • __mkroute_output() 尝试从 nexthop 缓存中获取已有的 rtable 路由;

    • 未命中则通过 rt_dst_alloc() 分配新路由,再由 rt_set_nexthop() 填充下一跳信息并写回缓存。

  4. 状态更新
    __ip4_datagram_connect()

    • 将路由结果写回 inet 结构;

    • 设置 sk_state = TCP_ESTABLISHED

    • 并通过 sk_dst_set() 将路由绑定到 socket

    整个过程中,“第一次确定 saddr”发生在 ip_route_connect() 内对 fl4->saddr 的赋值,而 XFRM 查找则在 ip_route_output_flow() 中根据 flowi4_proto 是否非零来决定是否执行。最终,UDP socket 拥有了完整的四元组信息和缓存路由,后续 sendmsg 可直接使用该路由发送数据。

整个调用链的核心是路由查找:通过 ip_route_connect() 触发 FIB 查找,在路由表中确定出接口、下一跳和源地址,最终将结果缓存到 socketdst 中。


2. 调用栈

  • 内核版本:Linux 6.6

  • 分析范围:IPv4 UDP socket 调用 connect() 后的内核执行路径

  • 核心入口:ip4_datagram_connect()

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 /*
359 * Now struct inet_timewait_sock also uses sock_common, so please just
360 * don't add nothing before this first member (__sk_common) --acme
361 */
362 struct sock_common __sk_common;
...
445 struct dst_entry __rcu *sk_dst_cache; // 每个 sock 缓存路由的查找结果

内核里会使用 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 /* Info on neighbour */
73 union {
74 __be32 rt_gw4;
75 struct in6_addr rt_gw6;
76 };
77
78 /* Miscellaneous cached information */
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 };

最基本的路由信息,包含:

  • dev:从哪个口出

  • ops:一堆函数,其中本场景下用 neigh_lookup 来查找下一跳 GW 对应的 MAC 地址

其他字段在本场景中关联度不大,先忽略。

这里只是最通用的部分,其他部分在 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 发送相关的,有几个关键的域:

  • tablefi 所在的路由表,如 mainlocal,以及策略路由场景下的其他 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 的路由树上,是树的一个叶子节点。

  • nh:新内核里,多个 fi 共享的下一跳信息

  • fib_nhs:下一跳个数

  • fib_nh:下一跳数组,老内核里使用,fi 独享的下一跳信息

使用上没有差别,可以不关注这个细节。

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 /* v4 specific, but allows fib6_nh with v4 routes */
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; /* Update source address */

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 }

函数说明

  • ip_route_connect_init() 初始化 flowi4,设置目的地址、源地址、TOS、出接口、协议、端口等。

  • udp connect场景下,

    • 此处源port和源ip是0,通过 __ip_route_output_key() 查找合适的源地址。

    • __ip_route_output_key是个重要入口函数,后续专门介绍。

  • 调用 ip_route_output_flow() 重新执行路由查找。

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);

函数说明

  • 调用 __ip_route_output_key() 进行路由查找。

  • xfrm跟本场景无关,不影响rt结果,可忽略。

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场景下,

  • 先直接被调用一次,确定源IP和port,

  • 然后再经过ip_route_output_flow, 再次被调用。

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);

函数说明

  • 初始化 fib_result 结构,

  • 调用ip_route_output_key_hash_rcu()查找路由,并把结果通过res返回。

  • 此时已经与skb无关, 只跟五元组等key信息相关。

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 /* hold dst before doing cmpxchg() to avoid race condition
1484 * on this dst
1485 */
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 内核源码分析,仅供学习交流。


Linux 6.6 UDP Socket connect 调用链源码分析
https://martinbj2008.github.io/2026/09/24/udp-connect/
Author
Martinbj2008
Posted on
September 24, 2026
Licensed under