基于 Linux 内核 6.0、64 位系统和 Intel 网卡驱动 igb。 由于篇幅过长切分多篇,下一篇《十年码农内功:网络发包详细过程(二)》,所有参考内容在最后一篇。
一、概述 Linux 中的网络包发送过程大致如下: 应用层:应用程序通过调用send、sendto和write函数发送数据到 Socket 发送缓冲区里; 套接字:执行 send / sendto 系统调用、构建 msghdr 和获取 socket; 传输层:执行 cgroup BPF 程序判断是否允许发送;获取路由信息和构建 sk_buff,然后构建并填充 UDP 头部;处理 GSO(如果开启)相关的情况; 网络层:构建 IP 包,然后经过 NetFilter 和 BPF 的过滤与修改、GSO 和分片处理后发给邻居子系统; 邻居子系统:检查是否存在邻居缓存,有直接发给邻居,否则查找(ARP)后再发送; 网络接口层:内核将封装好的数据包发送给网络接口驱动程序。驱动程序负责将数据包传递给硬件设备以便发送; 硬件发送:网络接口驱动程序将数据包传递给物理网络接口,通过物理链路发送到目标主机。
 图1 完整流程图
 图2 完整调用链二、应用层 应用程序通过调用send、sendto和write函数发送数据到 Socket 发送缓冲区里。send和sendto函数的区别与recv和recvfrom类似,就是是否不指定目的地址。write就是把socket当作文件描述符使用。 三、Socket 3.1 概述 应用层调用发送函数会执行 send/sendto 系统调用; 将用户空间的数据和地址导入(非拷贝)到内核空间( msghdr); 根据文件描述符找到对应的套接字 (socket); 根据协议类型选择 TCP / UDP 发送函数入口;
3.2 系统调用 3.2.1 用户调用 send/sendto 函数发送数据(用户态) 当应用程序调用clib的send或sendto函数接收数据时,通过strace命令可以跟踪到分别执行了send和sendto系统调用。 std::string data = '123'; // send int ret = send(fd, data.c_str(), data.size(), MSG_NOSIGNAL);
// sendto struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = inet_addr('172.17.0.2'); serverAddr.sin_port = htons(20000);
int ret = sendto(fd, data.c_str(), data.size(), MSG_DONTWAIT, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
3.2.2 send/sendto 系统调用(内核态) SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len, unsigned int, flags) { return __sys_sendto(fd, buff, len, flags, NULL, 0); }
SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len, unsigned int, flags, struct sockaddr __user *, addr, int, addr_len) { return __sys_sendto(fd, buff, len, flags, addr, addr_len); }
send和sendto系统调用的主要逻辑都封装在 __sys_sendto 函数中。 3.3 构建 msghdr 和获取 socket 3.3.1 __sys_sendto 函数 int __sys_sendto(int fd, void __user *buff, size_t len, unsigned int flags, struct sockaddr __user *addr, int addr_len) { struct socket *sock; struct sockaddr_storage address; int err; struct msghdr msg; struct iovec iov; int fput_needed; // 将用户空间的数据区域导入到内核空间,并检查数据区域是否可读。 err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter); if (unlikely(err)) return err; // 查找给定文件描述符对应的套接字 (socket),并返回该套接字的引用。 sock = sockfd_lookup_light(fd, &err, &fput_needed); if (!sock) goto out; // 初始化 msghdr 结构体 msg.msg_name = NULL; msg.msg_control = NULL; msg.msg_controllen = 0; msg.msg_namelen = 0; msg.msg_ubuf = NULL; if (addr) { // 将用户空间的地址结构体移动到内核空间 err = move_addr_to_kernel(addr, addr_len, &address); if (err < 0) goto out_put; msg.msg_name = (struct sockaddr *)&address; msg.msg_namelen = addr_len; } // 如果套接字的文件标志包含 O_NONBLOCK 标志,将 flags 的 MSG_DONTWAIT 标志位置为1。 if (sock->file->f_flags & O_NONBLOCK) flags |= MSG_DONTWAIT; msg.msg_flags = flags; // 将套接字 (sock) 和消息 (msg) 作为参数发送数据。 err = sock_sendmsg(sock, &msg);
out_put: // 释放套接字的引用。 fput_light(sock->file, fput_needed); out: return err; }
其主要逻辑有四: 定义 msghdr 结构体,将用户空间的数据区域导入(非拷贝)到内核空间(msghdr),并检查数据区域是否可读; 查找给定文件描述符对应的套接字 (socket),并返回该套接字的引用; 将用户空间的地址结构体移动到内核空间(msghdr); 调用 sock_sendmsg 函数将套接字 (sock) 和消息 (msg) 作为参数发送数据。
3.4 安全检查和选择传输层发送函数 3.4.1 sock_sendmsg 函数 int sock_sendmsg(struct socket *sock, struct msghdr *msg) { // 将套接字和消息传递给安全性模块 (LSM) 进行安全性检查。 int err = security_socket_sendmsg(sock, msg, msg_data_left(msg)); /* ?: 表示三元运算符,如果 err 为0(表示没有错误), * 则继续执行 sock_sendmsg_nosec 函数;否则,直接返回 err。 */ return err ?: sock_sendmsg_nosec(sock, msg); }
其主要逻辑有二: 将套接字和消息传递给安全性模块 (LSM) 进行安全性检查; 如果没有错误,那么调用 sock_sendmsg_nosec 函数继续处理。
3.4.2 sock_sendmsg_nosec 函数 static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg) { int ret = INDIRECT_CALL_INET(sock->ops->sendmsg, inet6_sendmsg, inet_sendmsg, sock, msg, msg_data_left(msg)); BUG_ON(ret == -EIOCBQUEUED); return ret; }
在收包中讲过 Socket 在创建初始化时指定了 ops,如下: const struct proto_ops inet_stream_ops = { .sendmsg = inet_sendmsg,// 发送数据 .recvmsg = inet_recvmsg,// 接收数据 };
对于 IPv4,sock_sendmsg_nosec 调用的是 inet_sendmsg 函数继续处理。 3.4.3 inet_sendmsg 函数 int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size) { struct sock *sk = sock->sk; if (unlikely(inet_send_prepare(sk))) return -EAGAIN; return INDIRECT_CALL_2(sk->sk_prot->sendmsg, tcp_sendmsg, udp_sendmsg, sk, msg, size); }
同样,在收包中讲过 Socket 在创建初始化时根据传输层协议类型指定了 proto,如下: struct proto tcp_prot = { .name = 'TCP', .recvmsg = tcp_recvmsg,// 接收数据 .sendmsg = tcp_sendmsg,// 发送数据 };
struct proto udp_prot = { .name = 'UDP', .sendmsg = udp_sendmsg,// 发送数据 .recvmsg = udp_recvmsg,// 接收数据 };
对于 UDP,inet_sendmsg 调用的是 udp_sendmsg 函数继续处理。 四、传输层(UDP) 4.1 概述 各种检查,获取和验证目标地址; 执行 cgroup BPF 程序; 获取路由信息和构建 sk_buff; 创建并填充 UDP 头部; 如果启用了 GSO,则处理 GSO 相关的情况; 调用 ip_send_skb 函数进入网络层;
4.2 详细过程 4.2.1 udp_sendmsg 函数 int udp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len) { struct inet_sock *inet = inet_sk(sk); struct udp_sock *up = udp_sk(sk); DECLARE_SOCKADDR(struct sockaddr_in *, usin, msg->msg_name); struct flowi4 fl4_stack; struct flowi4 *fl4; int ulen = len; struct ipcm_cookie ipc; struct rtable *rt = NULL; int free = 0; int connected = 0; __be32 daddr, faddr, saddr; __be16 dport; u8 tos; int err, is_udplite = IS_UDPLITE(sk); int corkreq = READ_ONCE(up->corkflag) || msg->msg_flags&MSG_MORE; int (*getfrag)(void *, char *, int, int, int, struct sk_buff *); struct sk_buff *skb; struct ip_options_data opt_copy; // 检查数据长度是否超过最大限制(65535 字节)。 if (len > 0xFFFF) // 如果超过,则返回错误码 -EMSGSIZE 表示消息大小超过限制。 return -EMSGSIZE; /* 检查消息的标志位中是否包含 MSG_OOB。如果包含,表示请求发送带外数据, * 但由于 UDP 不支持带外数据传输,因此返回错误码 -EOPNOTSUPP 表示不支持操作。 */ if (msg->msg_flags & MSG_OOB) /* Mirror BSD error message compatibility */ return -EOPNOTSUPP; /* 根据 is_udplite 变量的值,选择相应的函数指针赋值给 getfrag。 * 如果 is_udplite 为真,表示使用 UDPLite 协议,赋值为 udplite_getfrag 函数指针, * 否则赋值为 ip_generic_getfrag 函数指针。 */ getfrag = is_udplite ? udplite_getfrag : ip_generic_getfrag; // 如果存在挂起的数据包,表示套接字已经被挂起,锁定套接字并检查挂起数据包的类型。 fl4 = &inet->cork.fl.u.ip4; if (up->pending) { lock_sock(sk); if (likely(up->pending)) { if (unlikely(up->pending != AF_INET)) { // 如果类型不是AF_INET,则返回错误码 `-EINVAL`。 release_sock(sk); return -EINVAL; } goto do_append_data; } release_sock(sk); } // 将 ulen 增加一个 UDP 头部的大小 ulen += sizeof(struct udphdr); // 检查是否提供了目标地址结构体指针 usin。如果存在,表示用户指定了目标地址。 if (usin) { // 检查提供的目标地址结构体的长度是否大于等于 struct sockaddr_in 的大小 if (msg->msg_namelen < sizeof(*usin)) // 如果小于,则返回错误码 -EINVAL 表示参数无效。 return -EINVAL; // 检查提供的目标地址结构体的协议簇字段 (sin_family) 是否为 AF_INET。 if (usin->sin_family != AF_INET) { // 如果不是 AF_INET,则检查是否为 AF_UNSPEC。 if (usin->sin_family != AF_UNSPEC) // 如果也不是,则返回错误码 -EAFNOSUPPORT 表示地址簇不受支持。 return -EAFNOSUPPORT; } // 将目标地址和目标端口分别赋值为目标地址结构体中的值。 daddr = usin->sin_addr.s_addr; dport = usin->sin_port; // 如果目标端口为0,表示目标端口无效,返回错误码 -EINVAL 表示参数无效。 if (dport == 0) return -EINVAL; } else { // 如果没有提供目标地址结构体指针 usin,则检查套接字状态 (sk->sk_state) 是否为 TCP_ESTABLISHED。 if (sk->sk_state != TCP_ESTABLISHED) // 如果不是已建立连接状态,则返回错误码 -EDESTADDRREQ 表示目标地址未指定。 return -EDESTADDRREQ; // 将目标地址和目标端口分别赋值为套接字 inet 中的目标地址和目标端口。 daddr = inet->inet_daddr; dport = inet->inet_dport; // 在这种情况下,表示为已连接套接字,将 connected 置为1。 connected = 1; } // 初始化 ipc 变量,设置 ipc 的字段,包括 opt 和 gso_size。 ipcm_init_sk(&ipc, inet); ipc.gso_size = READ_ONCE(up->gso_size); // 判断消息的控制信息长度 (msg_controllen) 是否大于 0。 if (msg->msg_controllen) { // 如果是,则调用 udp_cmsg_send 函数和 ip_cmsg_send 函数来处理控制信息。 err = udp_cmsg_send(sk, msg, &ipc.gso_size); if (err > 0) err = ip_cmsg_send(sk, msg, &ipc, sk->sk_family == AF_INET6); // 如果处理控制信息时出现错误 (err < 0),释放 ipc.opt 内存,并返回错误码。 if (unlikely(err < 0)) { kfree(ipc.opt); return err; } // 如果 ipc.opt 不为空,将 free 置为 1,表示需要释放内存。 if (ipc.opt) free = 1; // 将 connected 置为 0,表示不是已连接套接字。 connected = 0; } // 如果 ipc.opt 为空,则尝试从 inet->inet_opt 获取选项信息。 if (!ipc.opt) { struct ip_options_rcu *inet_opt; rcu_read_lock(); inet_opt = rcu_dereference(inet->inet_opt); /* 如果 inet_opt 不为空,则将 inet_opt 的内容复制到 opt_copy 中, * 并将 ipc.opt 设置为指向 opt_copy.opt 的指针。 */ if (inet_opt) { memcpy(&opt_copy, inet_opt, sizeof(*inet_opt) + inet_opt->opt.optlen); ipc.opt = &opt_copy.opt; } rcu_read_unlock(); } // 如果启用了 cgroup BPF,并且不是已连接套接字,则运行 cgroup BPF 程序来检查是否允许发送消息。 if (cgroup_bpf_enabled(CGROUP_UDP4_SENDMSG) && !connected) { // 函数 BPF_CGROUP_RUN_PROG_UDP4_SENDMSG_LOCK 用于执行 cgroup BPF 程序。 err = BPF_CGROUP_RUN_PROG_UDP4_SENDMSG_LOCK(sk, (struct sockaddr *)usin, &ipc.addr); // 如果执行 cgroup BPF 程序时返回错误 (err != 0),则释放内存并返回错误码。 if (err) goto out_free; // 如果存在目标地址结构体指针 usin,则进一步检查目标端口是否为0。 if (usin) { // 如果为0,表示 BPF 程序设置了无效的目标端口,返回错误码 -EINVAL 表示参数无效。 if (usin->sin_port == 0) { err = -EINVAL; goto out_free; } // 将目标地址和目标端口分别赋值为目标地址结构体中的值 daddr = usin->sin_addr.s_addr; dport = usin->sin_port; } } // 将本地地址 saddr 设置为 ipc.addr,同时将 ipc.addr 设置为目标地址 daddr。 saddr = ipc.addr; ipc.addr = faddr = daddr; // 如果 ipc.opt 存在且 ipc.opt->opt.srr 字段为真,则进一步检查目标地址是否为空。 if (ipc.opt && ipc.opt->opt.srr) { if (!daddr) { // 如果为空,表示无效的目标地址,返回错误码 -EINVAL 表示参数无效。 err = -EINVAL; goto out_free; } // 将 faddr 设置为 ipc.opt->opt.faddr,表示使用源路由选项中的第一个中间地址。 faddr = ipc.opt->opt.faddr; connected = 0; } // 根据 ipc 和 inet 计算并返回服务类型(TOS)。 tos = get_rttos(&ipc, inet); // 套接字标志 (sk_flag) ,消息标志 (msg_flags) if (sock_flag(sk, SOCK_LOCALROUTE) || (msg->msg_flags & MSG_DONTROUTE) || (ipc.opt && ipc.opt->opt.is_strictroute)) { tos |= RTO_ONLINK; connected = 0; } // 如果目标地址是多播地址,则进一步检查 ipc.oif 是否为空或者是否为 L3 主设备的索引。 if (ipv4_is_multicast(daddr)) { if (!ipc.oif || netif_index_is_l3_master(sock_net(sk), ipc.oif)) // 如果满足条件,则将 ipc.oif 设置为 inet 中的多播索引 (inet->mc_index)。 ipc.oif = inet->mc_index; if (!saddr) // 如果本地地址 saddr 为空,则将其设置为 inet 中的多播地址 (inet->mc_addr)。 saddr = inet->mc_addr; connected = 0; } else if (!ipc.oif) { // 如果 ipc.oif 为空,则将其设置为 inet 中的非多播索引 (inet->uc_index)。 ipc.oif = inet->uc_index; } else if (ipv4_is_lbcast(daddr) && inet->uc_index) { /* 如果目标地址是本地广播地址,且 inet 中的非多播索引 (inet->uc_index) 不为0, * 则进一步检查 ipc.oif 是否等于 inet->uc_index,且 ipc.oif 是 L3 主设备索引的一部分。 */ if (ipc.oif != inet->uc_index && ipc.oif == l3mdev_master_ifindex_by_index(sock_net(sk), inet->uc_index)) { // 如果满足条件,则将 ipc.oif 设置为 inet->uc_index。 ipc.oif = inet->uc_index; } } /* 如果是已连接套接字,则通过 sk_dst_check 函数检查是否存在路由缓存(路由表项),并将结果赋值给 rt。 * 如果存在,则表示该缓存可用于发送数据。 */ if (connected) rt = (struct rtable *)sk_dst_check(sk, 0); // 如果没有路由缓存,则根据参数计算出 flowi4 并进行路由查找。 if (!rt) { struct net *net = sock_net(sk); __u8 flow_flags = inet_sk_flowi_flags(sk);
fl4 = &fl4_stack; /* 使用 flowi4_init_output 函数初始化 fl4 变量,设置了 flowi4 的各个字段, * 如出接口 (ipc.oif)、流标记 (ipc.sockc.mark)、服务类型 (tos)、 * 作用域 (RT_SCOPE_UNIVERSE)、套接字协议 (sk->sk_protocol)、 * 流标志 (flow_flags)、源地址 (saddr)、目标地址 (faddr)、目标端口 (dport)、 * 源端口 (inet->inet_sport) 和用户 ID (sk->sk_uid)。 */ 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 函数对套接字进行安全性分类。 security_sk_classify_flow(sk, flowi4_to_flowi_common(fl4)); // 调用 ip_route_output_flow 函数根据 fl4 查找路由,并将结果赋值给 rt。 rt = ip_route_output_flow(net, fl4, sk); if (IS_ERR(rt)) { // 如果查找失败,返回的错误码存储在 err 中,并在出现 -ENETUNREACH 错误时增加相应的统计数据。 err = PTR_ERR(rt); rt = NULL; if (err == -ENETUNREACH) IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES); goto out; } err = -EACCES; // 检查路由表项的 rt_flags 是否包含 RTCF_BROADCAST 标志,并且套接字的 SOCK_BROADCAST 标志未设置。 if ((rt->rt_flags & RTCF_BROADCAST) && !sock_flag(sk, SOCK_BROADCAST)) // 如果满足条件,表示不允许广播发送,返回错误码 -EACCES。 goto out; // 如果是已连接套接字,将套接字的目标地址设置为路由缓存的克隆(dst_clone(&rt->dst))。 if (connected) sk_dst_set(sk, dst_clone(&rt->dst)); } // 如果消息的标志位中包含 MSG_CONFIRM,则跳转到标签 do_confirm 处处理确认。 if (msg->msg_flags&MSG_CONFIRM) goto do_confirm; back_from_confirm: // 将本地地址 saddr 设置为 fl4->saddr 的值。 saddr = fl4->saddr; // 如果 ipc.addr 为空,则将目标地址 daddr 和 ipc.addr 设置为 fl4->daddr 的值。 if (!ipc.addr) daddr = ipc.addr = fl4->daddr; // 在非延迟发送的情况下,使用 ip_make_skb 函数创建一个 sk_buff 结构体,并调用 udp_send_skb 函数发送数据。 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)) { // 如果套接字已经被延迟发送,则释放套接字锁,返回错误码 -EINVAL,并打印警告消息。 release_sock(sk); net_dbg_ratelimited('socket already corked\n'); err = -EINVAL; goto out; } // 设置 fl4(路由信息)中的字段为目标地址和源地址等信息。 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; // 将数据追加到 sk_buff 中,并根据参数设置进行处理。 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) // 如果不需要延迟发送,则调用 udp_push_pending_frames 函数将已排队的帧发送出去。 err = udp_push_pending_frames(sk); else if (unlikely(skb_queue_empty(&sk->sk_write_queue))) // 如果排队的帧为空,则将 up->pending 置为0。 up->pending = 0; // 释放套接字锁。 release_sock(sk);
out: // 释放路由表项 (rt)。 ip_rt_put(rt); out_free: // 如果需要释放内存,则释放 ipc.opt 的内存。 if (free) kfree(ipc.opt); // 如果没有错误发生,则返回数据的长度。 if (!err) return len; // 当err是-ENOBUFS(no kernel mem)或包含SOCK_NOSPACE(no sndbuf space)标志,增加统计数据。 if (err == -ENOBUFS || test_bit(SOCK_NOSPACE, &sk->sk_socket->flags)) { UDP_INC_STATS(sock_net(sk), UDP_MIB_SNDBUFERRORS, is_udplite); } return err;
do_confirm: if (msg->msg_flags & MSG_PROBE) dst_confirm_neigh(&rt->dst, &fl4->daddr); if (!(msg->msg_flags&MSG_PROBE) || len) goto back_from_confirm; err = 0; goto out; }
这个是用于发送 UDP 消息的主要函数,其主要逻辑如下: 处理消息长度和标志; 获取和验证目标地址; 存储选项和路由信息; 处理消息的控制信息; 运行 cgroup BPF 程序来检查是否允许发送消息; 处理源地址和目的地址; 计算服务类型(TOS); 设置输出接口索引(oif)和源地址(saddr); 如果不存在路由缓存(路由表项),则根据目标地址和其他参数进行路由查找; 如果消息标志包含 MSG_CONFIRM ,则进行确认处理; 如果需要延迟发送,挂起数据并标记套接字为挂起状态; 如果不需要延迟发送,在非延迟发送的情况下,使用 ip_make_skb 函数创建一个 sk_buff 结构体(这是第一次复制,从 msghdr 到 sk_buff 的复制)并调用 udp_send_skb 函数发送数据; 检查是否有错误发生,增加相应的统计数据。
MSG_CONFIRM 标志用于 UDP(用户数据报协议)套接字。当设置了此标志时,它告诉内核需要确认远程对等方是否成功接收了发送的数据报。它通常与 sendto() 或 sendmsg() 系统调用一起使用。工作原理如下:1. 当在 sendto() 或 sendmsg() 的 msg_flags 参数中设置了 MSG_CONFIRM 标志时,表示应用程序想要发送数据报,但同时希望知道远程对等方是否成功接收了数据报。2. 在发送数据报后,sendto() 或 sendmsg() 系统调用不会立即返回,而是会阻塞并等待来自远程对等方的确认(ACK)或错误消息(ICMP 错误)。3. 当收到远程对等方的确认或错误消息后,sendto() 或 sendmsg() 系统调用将解除阻塞并返回相应的结果,应用程序可以据此了解数据报是否成功到达目标。
4.2.2 udp_send_skb 函数 static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4, struct inet_cork *cork) { /* fl4 是IPv4的路由信息。cork 是 inet_cork 结构体, * 用于处理GSO(Generic Segmentation Offload)和校验和等相关选项。 */ struct sock *sk = skb->sk; struct inet_sock *inet = inet_sk(sk); struct udphdr *uh; int err; int is_udplite = IS_UDPLITE(sk); int offset = skb_transport_offset(skb); int len = skb->len - offset; int datalen = len - sizeof(*uh); __wsum csum = 0;
// 创建UDP头部,uh 指向 skb 中的UDP头部位置。 uh = udp_hdr(skb); // 将UDP头部的源端口 (source) 和目标端口 (dest) 设置为套接字的源端口和 fl4 结构体中的目标端口。 uh->source = inet->inet_sport; uh->dest = fl4->fl4_dport; // 将UDP头部的长度 (len) 设置为数据报的总长度,并将字节序转换为网络字节序(大端序)。 uh->len = htons(len); // 将UDP头部的校验和 (check) 初始化为0。 uh->check = 0; // 如果启用了GSO(Generic Segmentation Offload),则处理GSO相关的情况。 if (cork->gso_size) { // hlen 是数据包的网络层头部长度和UDP头部的长度之和。 const int hlen = skb_network_header_len(skb) + sizeof(struct udphdr); // 检查UDP头部和GSO大小是否超过了片段大小 (cork->fragsize),如果超过则释放 skb 并返回错误码 -EINVAL。 if (hlen + cork->gso_size > cork->fragsize) { kfree_skb(skb); return -EINVAL; } // 检查有效负载的长度是否超过了允许的GSO大小乘以最大UDP段数 (UDP_MAX_SEGMENTS)。 if (datalen > cork->gso_size * UDP_MAX_SEGMENTS) { // 如果超过则释放 skb 并返回错误码 -EINVAL。 kfree_skb(skb); return -EINVAL; } // 检查套接字是否禁用了校验和 (sk->sk_no_check_tx)。 if (sk->sk_no_check_tx) { // 如果禁用则释放 skb 并返回错误码 -EINVAL。 kfree_skb(skb); return -EINVAL; } /* 检查数据报的校验和类型,如果不是部分校验和 (CHECKSUM_PARTIAL), * 或者是UDP-Lite协议,或者经过了转发处理(dst_xfrm(skb_dst(skb))), * 则释放 skb 并返回错误码 -EIO。 */ if (skb->ip_summed != CHECKSUM_PARTIAL || is_udplite || dst_xfrm(skb_dst(skb))) { kfree_skb(skb); return -EIO; } // 如果需要拆分GSO,设置skb_shinfo(skb)结构体中的GSO信息,并跳转到csum_partial标签处。 if (datalen > cork->gso_size) { skb_shinfo(skb)->gso_size = cork->gso_size; skb_shinfo(skb)->gso_type = SKB_GSO_UDP_L4; skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(datalen, cork->gso_size); } goto csum_partial; } // 根据是否是UDP-Lite协议,选择计算UDP校验和的方法: if (is_udplite)/* UDP-Lite */ // 如果是UDP-Lite协议,则调用udplite_csum函数计算UDP-Lite校验和并将结果存储在csum中。 csum = udplite_csum(skb); else if (sk->sk_no_check_tx) {/* UDP csum off */ // 如果套接字禁用了校验和,则将数据报的校验和类型设置为CHECKSUM_NONE,表示不需要进行校验和。 skb->ip_summed = CHECKSUM_NONE; goto send; } else if (skb->ip_summed == CHECKSUM_PARTIAL) { /* 如果数据报的校验和类型为部分校验和 (CHECKSUM_PARTIAL), * 则调用udp4_hwcsum函数计算硬件卸载的校验和,并跳转到send标签处。 */ csum_partial: udp4_hwcsum(skb, fl4->saddr, fl4->daddr); goto send;
} else // 否则,调用udp_csum函数计算UDP校验和,并将结果存储在csum中。 csum = udp_csum(skb); // 在计算完UDP校验和后,使用csum_tcpudp_magic函数添加协议相关的伪首部(pseudo-header),并计算最终的UDP校验和。 uh->check = csum_tcpudp_magic(fl4->saddr, fl4->daddr, len, sk->sk_protocol, csum); // 如果最终的UDP校验和值为0,则将其设置为CSUM_MANGLED_0,以避免零校验和。 if (uh->check == 0) uh->check = CSUM_MANGLED_0;
send: // 调用ip_send_skb函数将数据报发送出去,并将发送结果保存在err中。 err = ip_send_skb(sock_net(sk), skb); if (err) { /* 如果发送出现错误,并且错误码是-ENOBUFS,并且接收错误信息的标志 (inet->recverr) 未启用, * 则将发送缓冲区错误统计数增加,并将err设置为0表示忽略错误。 */ if (err == -ENOBUFS && !inet->recverr) { UDP_INC_STATS(sock_net(sk), UDP_MIB_SNDBUFERRORS, is_udplite); err = 0; } } else // 如果发送成功,则增加发送数据报统计数。 UDP_INC_STATS(sock_net(sk), UDP_MIB_OUTDATAGRAMS, is_udplite); return err; }
其主要逻辑如下: 获取套接字 (sk) 和与套接字关联的 inet_sock 结构体 (inet); 计算数据报的长度,UDP 头部的偏移量,有效负载长度和校验和值 (csum); 创建一个 UDP 头部 (uh),填充 UDP 头部的源端口 (source)、目标端口 (dest)、长度 (len) 和校验和 (check) 字段; 如果启用了 GSO (Generic Segmentation Offload),则进行 GSO 处理,将数据报拆分为多个片段并进行相关校验; 计算校验和相关处理; 对于 UDP-Lite 协议,计算 UDP-Lite 校验和并将其存储在csum变量中; 对于禁用校验和 (sk_no_check_tx) 的情况,将数据报的校验和字段设置为CHECKSUM_NONE,表示不需要校验和; 对于使用硬件卸载校验和的情况,调用udp4_hwcsum函数计算硬件校验和; 对于其他情况,计算UDP校验和并存储在csum变量中。 添加伪首部,并计算最终的 UDP 校验和。如果校验和值为0,则设置为CSUM_MANGLED_0,以避免零校验和; 调用 ip_send_skb 函数进行实际的发送数据报; 根据发送是否成功,增加统计信息。
五、传输层(TCP) 敬请期待! 六、网络层(IP) 6.1 概述 数据包来到网络层,初始化 IP 头信息; 数据包经过 Netfilter 过滤和修改; 根据协议类型(IPv4 或 IPv6)调用相应的网络输出函数; 并再次经过 Netfilter 过滤和修改; 执行 cgroup egress BPF 对数据包进行过滤: 如果 skb 是 GSO 数据包,则使用特定函数处理; 如果 skb 长度大于 MTU 或者有分片信息,则进行分片处理; 做进入下一层邻居子系统前的处理。
6.2 网络层目标出口 6.2.1 ip_send_skb 函数 int ip_send_skb(struct net *net, struct sk_buff *skb) { int err; // 调用ip_local_out函数将skb发送到IP层 err = ip_local_out(net, skb->sk, skb); if (err) { // 如果err为正数,转换为负数表示出错 if (err > 0) err = net_xmit_errno(err); // 如果发送出错,则增加IP层的输出丢弃统计数 if (err) IP_INC_STATS(net, IPSTATS_MIB_OUTDISCARDS); }
return err; }
其主要逻辑有二: 首先调用 ip_local_out 函数,将数据包 skb 发送到IP层; 如果发送出现错误,将错误码转换为负数,并增加相应的 IP 层的输出丢弃统计计数(IPSTATS_MIB_OUTDISCARDS)。
6.2.2 ip_local_out 函数 int ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb) { int err; // 调用 __ip_local_out 函数将 skb 发送到本地IP层 err = __ip_local_out(net, sk, skb); // 如果 __ip_local_out 返回值为1,表示数据包还未发送到目标,继续处理 if (likely(err == 1)) err = dst_output(net, sk, skb); // 返回发送的结果,可能是错误码或者是1(数据包还未发送到目标) return err; }
其主要逻辑有二: 首先调用 __ip_local_out 函数,将数据包 skb 发送到本地 IP 层进行处理; 如果返回值为 1,表示数据包还未发送到目标,然后调用 dst_output 函数将数据包继续传递到目标出口。
6.2.3 __ip_local_out 函数 int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb) { struct iphdr *iph = ip_hdr(skb); // 设置IPv4首部的总长度字段(total length),将skb的长度转换为网络字节序(大端序) iph->tot_len = htons(skb->len); // 计算IPv4首部的校验和字段(checksum) ip_send_check(iph); // 将skb传递给L3 master设备(例如路由器或虚拟路由器)的处理程序进行处理,例如 VLAN 网络和虚拟路由。 skb = l3mdev_ip_out(sk, skb); // 如果skb为NULL,表示已经被处理,无需继续传递,直接返回0 if (unlikely(!skb)) return 0; // 设置skb的协议字段为IPv4协议(ETH_P_IP) skb->protocol = htons(ETH_P_IP); // 调用网络过滤钩子(Netfilter hook)处理数据包 return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT, net, sk, skb, NULL, skb_dst(skb)->dev, dst_output); }
其主要逻辑有五: 设置 IPv4 首部的总长度字段(tot_len),将 skb 的长度转换为网络字节序(大端序); 计算 IPv4 首部的校验和字段(checksum),并将其填充到 iph 结构体中。 将 skb 传递给 l3mdev_ip_out 函数进行处理。如果 skb 为 NULL,表示已经被处理,无需继续传递,直接返回 0; 设置 skb 的协议字段为 IPv4 协议(ETH_P_IP); 最后,调用网络过滤钩子(Netfilter hook)nf_hook 处理数据包。nf_hook 函数负责在数据包传递过程中调用注册的网络过滤钩子函数,以便进行数据包处理和转发。它将IPv4的本地输出数据包(NFPROTO_IPV4)传递给注册的 NF_INET_LOCAL_OUT 钩子函数,然后继续传递给目标设备的输出函数 dst_output,进行数据包的处理和发送。
6.2.4 dst_output 函数 static inline int dst_output(struct net *net, struct sock *sk, struct sk_buff *skb) { // 根据协议类型调用对应的网络输出函数 return INDIRECT_CALL_INET(skb_dst(skb)->output, ip6_output, ip_output, net, sk, skb); }
通过 skb_dst(skb)->output 获取数据包对应的网络输出函数指针,并根据协议类型(IPv4 或 IPv6)调用相应的网络输出函数,实现数据包的发送。这样,数据包就会继续在网络层传递,并最终发送到目标地址。 IPv4 对应的处理函数是 ip_output。 6.3 数据包过滤和修改 6.3.1 ip_output 函数 int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb) { struct net_device *dev = skb_dst(skb)->dev, *indev = skb->dev; // 更新IPv4协议统计信息中的发送数据包数量 IP_UPD_PO_STATS(net, IPSTATS_MIB_OUT, skb->len); // 设置skb的输出网络设备和协议类型 skb->dev = dev; skb->protocol = htons(ETH_P_IP); // 调用网络过滤钩子(Netfilter hook)处理数据包 return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING, net, sk, skb, indev, dev, ip_finish_output, !(IPCB(skb)->flags & IPSKB_REROUTED)); }
其主要逻辑有三: 设置 skb 的输出网络设备和协议类型,以便将数据包发送到指定的物理设备; 调用网络过滤钩子(Netfilter hook)NF_HOOK_COND 处理数据包; 然后调用 ip_finish_output 函数继续发送数据包到指定的物理设备。
6.3.2 ip_finish_output 函数 static int ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb) { int ret; // 调用BPF_CGROUP_RUN_PROG_INET_EGRESS函数进行BPF过滤 ret = BPF_CGROUP_RUN_PROG_INET_EGRESS(sk, skb); // 根据BPF过滤的结果进行处理 switch (ret) { case NET_XMIT_SUCCESS: // 如果BPF过滤结果为 NET_XMIT_SUCCESS,则继续进行IP输出处理 return __ip_finish_output(net, sk, skb); case NET_XMIT_CN: // 如果BPF过滤结果为 NET_XMIT_CN,则继续进行IP输出处理,或者返回 NET_XMIT_CN return __ip_finish_output(net, sk, skb) ? : ret; default: // 如果BPF过滤结果为其他值,则释放数据包,并返回过滤结果 kfree_skb_reason(skb, SKB_DROP_REASON_BPF_CGROUP_EGRESS); return ret; } }
其主要逻辑有二: 调用 BPF_CGROUP_RUN_PROG_INET_EGRESS 函数对数据包进行 BPF 过滤; 根据 BPF 过滤的结果(ret),进行相应的处理: 如果过滤结果为 NET_XMIT_SUCCESS,表示允许数据包继续进行 IP 输出处理,调用 __ip_finish_output 函数继续处理; 如果过滤结果为 NET_XMIT_CN,表示需要继续进行IP输出处理,或者返回 NET_XMIT_CN,表示控制网络; 如果过滤结果为其他值,表示不允许数据包继续发送,释放数据包,并返回 BPF 过滤的结果。
该函数的主要目的是在IP层最终输出数据包之前,通过BPF过滤对数据包进行额外的处理或决策。 6.4 GSO和分片 6.4.1 __ip_finish_output 函数 static int __ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb) { unsigned int mtu; // 如果skb绑定了XFRM(安全传输模块),表示需要进行策略查找,重新路由 #if defined(CONFIG_NETFILTER) && defined(CONFIG_XFRM) if (skb_dst(skb)->xfrm) { IPCB(skb)->flags |= IPSKB_REROUTED; return dst_output(net, sk, skb); } #endif // 获取IP协议层的MTU(最大传输单元) mtu = ip_skb_dst_mtu(sk, skb); // 如果skb是GSO(Generic Segmentation Offload)数据包,则使用特定函数处理 if (skb_is_gso(skb)) return ip_finish_output_gso(net, sk, skb, mtu); // 如果skb长度大于MTU或者有分片信息(IPCB(skb)->frag_max_size),则进行分片处理 if (skb->len > mtu || IPCB(skb)->frag_max_size) return ip_fragment(net, sk, skb, mtu, ip_finish_output2);
// 否则直接进行IP输出处理 return ip_finish_output2(net, sk, skb); }
其主要逻辑有五: 首先检查是否需要进行策略查找和重新路由。如果 skb 绑定了 XFRM(安全传输模块),表示需要进行策略查找,重新路由,将 IPCB(skb)->flags 设置为 IPSKB_REROUTED,然后调用 dst_output 函数继续处理数据包; 接着,获取 IP 协议层的 MTU(最大传输单元),以便后续处理; 如果 skb 是 GSO(Generic Segmentation Offload)数据包,则调用 ip_finish_output_gso 函数进行特定处理; 如果 skb 长度大于 MTU 或者有分片信息(IPCB(skb)->frag_max_size),则调用 ip_fragment 函数进行分片处理; 否则,直接调用 ip_finish_output2 函数进行IP输出处理。
ip_finish_output_gso 函数和 ip_fragment 函数最后也都是调用了 ip_finish_output2 函数继续处理。 6.5 数据包发给邻居 6.5.1 ip_finish_output2 函数 static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb) { struct dst_entry *dst = skb_dst(skb); struct rtable *rt = (struct rtable *)dst; struct net_device *dev = dst->dev; unsigned int hh_len = LL_RESERVED_SPACE(dev); struct neighbour *neigh; bool is_v6gw = false; // 更新IPv4协议统计信息中的多播数据包或广播数据包的数量 if (rt->rt_type == RTN_MULTICAST) { IP_UPD_PO_STATS(net, IPSTATS_MIB_OUTMCAST, skb->len); } else if (rt->rt_type == RTN_BROADCAST) IP_UPD_PO_STATS(net, IPSTATS_MIB_OUTBCAST, skb->len); // 检查是否需要扩展数据包头部空间,并进行扩展 if (unlikely(skb_headroom(skb) < hh_len && dev->header_ops)) { skb = skb_expand_head(skb, hh_len); if (!skb) return -ENOMEM; } // 检查是否需要进行隧道传输,并进行隧道传输处理 if (lwtunnel_xmit_redirect(dst->lwtstate)) { int res = lwtunnel_xmit(skb); if (res < 0 || res == LWTUNNEL_XMIT_DONE) return res; } // 通过路由表查找下一跳的邻居,并向邻居发送数据包 rcu_read_lock_bh(); neigh = ip_neigh_for_gw(rt, skb, &is_v6gw); if (!IS_ERR(neigh)) { int res; sock_confirm_neigh(skb, neigh); // 调用 neigh_output 函数向邻居发送数据包 res = neigh_output(neigh, skb, is_v6gw); rcu_read_unlock_bh(); return res; } rcu_read_unlock_bh(); // 如果找不到下一跳的邻居,则释放数据包,并返回错误 net_dbg_ratelimited('%s: No header cache and no neighbour!\n', __func__); kfree_skb_reason(skb, SKB_DROP_REASON_NEIGH_CREATEFAIL); return -EINVAL; }
其主要逻辑有四: 首先,根据路由表 rt 中的类型(RTN_MULTICAST 或 RTN_BROADCAST)更新 IPv4 协议统计信息中的多播数据包或广播数据包的数量; 然后,检查是否需要扩展数据包头部空间,如果需要则进行扩展。这是为了确保数据包能够存放下目标网络设备的链路层(MAC)头部; 接着,检查是否需要进行隧道传输,如果需要则调用 lwtunnel_xmit 函数进行隧道传输处理; 最后,通过路由表查找数据包的下一跳邻居,并调用 neigh_output 函数向邻居发送数据包。如果找不到下一跳的邻居,则释放数据包,并返回错误。
该函数的主要目的是确保数据包能够正确发送到下一跳,以便进行最终的物理设备发送,或者通过隧道传输发送。 七、邻居子系统(Neighbor) 这一层属于网络层,但单独拿出一节介绍是因为其逻辑相对独立。 7.1 概述 检查邻居的状态和缓存是否有效; 如果有状态和缓存都满足,则直接将数据包发给邻居; 如果不满足,则需要先获取邻居(ARP),然后再把数据包发给邻居。
7.2 详细过程 7.2.1 neigh_output 函数 static inline int neigh_output(struct neighbour *n, struct sk_buff *skb, bool skip_cache) { const struct hh_cache *hh = &n->hh; // 检查邻居的状态(NUD_CONNECTED)以及缓存是否有效(hh_len),如果缓存有效则直接发送数据包。 if (!skip_cache && (READ_ONCE(n->nud_state) & NUD_CONNECTED) && READ_ONCE(hh->hh_len)) return neigh_hh_output(hh, skb); // 否则调用邻居的输出函数(output)发送数据包 return n->output(n, skb); }
其主要逻辑有二: 首先检查邻居的状态和缓存是否有效。如果邻居状态为 NUD_CONNECTED,且缓存有效(hh_len 非零),则直接调用 neigh_hh_output 函数向邻居发送数据包。neigh_hh_output 函数是专门用于向邻居发送数据包的函数,它使用缓存中的信息直接发送数据包,避免了再次查找邻居的过程,提高了发送效率。 如果缓存无效或者需要跳过缓存,则直接调用邻居的输出函数 n->output(实际指向的是 neigh_resolve_output 函数,内部可能有 arp 请求)发送数据包。后面会介绍 neigh_resolve_output 函数。
该函数是网络层向邻居发送数据包的一个重要环节,通过有效利用缓存可以提高发送效率,避免重复查找邻居的过程。 7.2.2 neigh_hh_output 函数 这是一个用于向邻居(Neighbor)发送数据包并利用硬件头部缓存(hh_cache)的函数,主要逻辑如下: static inline int neigh_hh_output(const struct hh_cache *hh, struct sk_buff *skb) { unsigned int hh_alen = 0; unsigned int seq; unsigned int hh_len; // 在读取 hh_cache 前获取锁并检查数据长度 do { seq = read_seqbegin(&hh->hh_lock); hh_len = READ_ONCE(hh->hh_len); if (likely(hh_len <= HH_DATA_MOD)) { hh_alen = HH_DATA_MOD; // 检查是否有足够的 headroom 来存放硬件头部缓存的数据 if (likely(skb_headroom(skb) >= HH_DATA_MOD)) { // 从硬件头部缓存复制数据到 sk_buff 的头部 memcpy(skb->data - HH_DATA_MOD, hh->hh_data, HH_DATA_MOD); } } else { hh_alen = HH_DATA_ALIGN(hh_len); // 检查是否有足够的 headroom 来存放硬件头部缓存的数据 if (likely(skb_headroom(skb) >= hh_alen)) { // 从硬件头部缓存复制数据到 sk_buff 的头部 memcpy(skb->data - hh_alen, hh->hh_data, hh_alen); } } } while (read_seqretry(&hh->hh_lock, seq)); // 检查 headroom 是否足够来存放硬件头部缓存的数据 if (WARN_ON_ONCE(skb_headroom(skb) < hh_alen)) { // 如果 headroom 不足,释放 sk_buff,并返回 NET_XMIT_DROP 错误码 kfree_skb(skb); return NET_XMIT_DROP; } // 将 sk_buff 的数据指针前移 hh_len 字节,即设置正确的数据头部 __skb_push(skb, hh_len);
// 将 sk_buff 发送出去 return dev_queue_xmit(skb); }
其主要逻辑有四: 首先,通过读取 hh_cache(硬件头部缓存)的数据长度,并获取锁来保证读取的一致性。根据硬件头部缓存的数据长度,确定数据的对齐方式和长度; 然后,检查是否有足够的 sk_buff headroom 来存放硬件头部缓存的数据。如果有足够的 headroom,则将硬件头部缓存的数据复制到 sk_buff 的头部; 接着,检查 headroom 是否足够来存放硬件头部缓存的数据。如果 headroom 不足,则释放 sk_buff,并返回 NET_XMIT_DROP 错误码; 最后,将 sk_buff 的数据指针前移 hh_len 字节,即设置正确的数据头部。然后通过调用 dev_queue_xmit 函数将 sk_buff 发送出去。
该函数的主要目的是尽可能地利用硬件头部缓存(hh_cache)来向邻居(Neighbor)发送数据包,避免了重复查找邻居的过程,从而提高了发送效率。 7.2.3 neigh_resolve_output 函数 int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb) { int rc = 0; // 如果邻居没有处于事件队列中,则发送邻居事件 if (!neigh_event_send(neigh, skb)) { int err; struct net_device *dev = neigh->dev; unsigned int seq; // 如果网络设备有头部缓存且硬件头部缓存长度为0,则初始化硬件头部缓存 if (dev->header_ops->cache && !READ_ONCE(neigh->hh.hh_len)) neigh_hh_init(neigh); // 移动 sk_buff 的网络层偏移,即准备数据部分 __skb_pull(skb, skb_network_offset(skb)); // 获取邻居硬件地址锁,并在获取硬件地址前获取邻居硬件地址 seq = read_seqbegin(&neigh->ha_lock); err = dev_hard_header(skb, dev, ntohs(skb->protocol), neigh->ha, NULL, skb->len); // 检查获取硬件地址时是否发生了竞态条件 while (read_seqretry(&neigh->ha_lock, seq)); // 如果获取硬件地址成功,则将 sk_buff 发送出去 if (err >= 0) rc = dev_queue_xmit(skb); else // 如果获取硬件地址失败,则释放 sk_buff 并返回错误码 goto out_kfree_skb; }
out: return rc;
out_kfree_skb: rc = -EINVAL; kfree_skb(skb); goto out; }
其主要逻辑有四: 首先,检查邻居是否处于事件队列中。如果邻居不在事件队列中,则调用 neigh_event_send 函数发送邻居事件(ARP 请求),以便进行邻居缓存的更新或解析; 接着,检查网络设备是否支持硬件头部缓存,并检查邻居的硬件头部缓存长度。如果硬件头部缓存长度为0,则调用 neigh_hh_init 函数初始化硬件头部缓存,用于后续的硬件头部处理; 然后,移动 sk_buff 的网络层偏移,即准备数据部分,以便进行硬件头部处理。接着,获取邻居硬件地址锁,并在获取硬件地址前获取邻居硬件地址。在获取硬件地址时,使用 dev_hard_header 函数填充硬件头部,并进行硬件地址的解析; 最后,检查是否成功获取硬件地址。如果获取硬件地址成功,则调用 dev_queue_xmit 函数将 sk_buff 发送出去。如果获取硬件地址失败,则释放 sk_buff 并返回错误码。
该函数是网络层向邻居发送数据包的一个重要环节,它根据目标邻居的状态、地址等信息选择合适的网络设备和物理链路,解析邻居硬件地址并进行硬件头部处理,最后确保数据包能够正确发送到目标邻居。 neigh_hh_output 函数和 neigh_resolve_output 函数最后都调用了 dev_queue_xmit 函数继续处理,进入网络接口层。
|