分享

十年码农内功:网络发包详细过程(一)

 天选小丑 2023-08-05 发布于广西

基于 Linux 内核 6.0、64 位系统和 Intel 网卡驱动 igb。

由于篇幅过长切分多篇,下一篇《十年码农内功:网络发包详细过程(二)》,所有参考内容在最后一篇。

一、概述

Linux 中的网络包发送过程大致如下:

  1. 应用层:应用程序通过调用sendsendtowrite函数发送数据到 Socket 发送缓冲区里;

  2. 套接字:执行 send / sendto 系统调用、构建 msghdr 和获取 socket

  3. 传输层:执行 cgroup BPF 程序判断是否允许发送;获取路由信息和构建 sk_buff,然后构建并填充 UDP 头部;处理 GSO(如果开启)相关的情况;

  4. 网络层:构建 IP 包,然后经过 NetFilter 和 BPF 的过滤与修改、GSO 和分片处理后发给邻居子系统;

  5. 邻居子系统:检查是否存在邻居缓存,有直接发给邻居,否则查找(ARP)后再发送;

  6. 网络接口层:内核将封装好的数据包发送给网络接口驱动程序。驱动程序负责将数据包传递给硬件设备以便发送;

  7. 硬件发送:网络接口驱动程序将数据包传递给物理网络接口,通过物理链路发送到目标主机。

Image

图1 完整流程图

Image

图2 完整调用链

二、应用层

应用程序通过调用sendsendtowrite函数发送数据到 Socket 发送缓冲区里。sendsendto函数的区别与recvrecvfrom类似,就是是否不指定目的地址。write就是把socket当作文件描述符使用。

三、Socket

3.1 概述

  1. 应用层调用发送函数会执行 send/sendto 系统调用;

  2. 将用户空间的数据和地址导入(非拷贝)到内核空间( msghdr);

  3. 根据文件描述符找到对应的套接字 (socket);

  4. 根据协议类型选择 TCP / UDP 发送函数入口;

3.2 系统调用

3.2.1 用户调用 send/sendto 函数发送数据(用户态)

当应用程序调用clibsendsendto函数接收数据时,通过strace命令可以跟踪到分别执行了sendsendto系统调用。

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

sendsendto系统调用的主要逻辑都封装在 __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;
}

其主要逻辑有四:

  1. 定义 msghdr 结构体,将用户空间的数据区域导入(非拷贝)到内核空间(msghdr),并检查数据区域是否可读;

  2. 查找给定文件描述符对应的套接字 (socket),并返回该套接字的引用;

  3. 将用户空间的地址结构体移动到内核空间(msghdr);

  4. 调用 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);
}

其主要逻辑有二:

  1. 将套接字和消息传递给安全性模块 (LSM) 进行安全性检查;

  2. 如果没有错误,那么调用 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 消息的主要函数,其主要逻辑如下:

  1. 处理消息长度和标志;

  2. 获取和验证目标地址;

  3. 存储选项和路由信息;

  4. 处理消息的控制信息;

  5. 运行 cgroup BPF 程序来检查是否允许发送消息;

  6. 处理源地址和目的地址;

  7. 计算服务类型(TOS);

  8. 设置输出接口索引(oif)和源地址(saddr);

  9. 如果不存在路由缓存(路由表项),则根据目标地址和其他参数进行路由查找;

  10. 如果消息标志包含 MSG_CONFIRM ,则进行确认处理;

  11. 如果需要延迟发送,挂起数据并标记套接字为挂起状态;

  12. 如果不需要延迟发送,在非延迟发送的情况下,使用 ip_make_skb 函数创建一个 sk_buff 结构体(这是第一次复制,从 msghdr 到 sk_buff 的复制)并调用 udp_send_skb 函数发送数据;

  13. 检查是否有错误发生,增加相应的统计数据。

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

其主要逻辑如下:

  1. 获取套接字 (sk) 和与套接字关联的 inet_sock 结构体 (inet);

  2. 计算数据报的长度,UDP 头部的偏移量,有效负载长度和校验和值 (csum);

  3. 创建一个 UDP 头部 (uh),填充 UDP 头部的源端口 (source)、目标端口 (dest)、长度 (len) 和校验和 (check) 字段;

  4. 如果启用了 GSO (Generic Segmentation Offload),则进行 GSO 处理,将数据报拆分为多个片段并进行相关校验;

  5. 计算校验和相关处理;

  6. 对于 UDP-Lite 协议,计算 UDP-Lite 校验和并将其存储在csum变量中;

  7. 对于禁用校验和 (sk_no_check_tx) 的情况,将数据报的校验和字段设置为CHECKSUM_NONE,表示不需要校验和;

  8. 对于使用硬件卸载校验和的情况,调用udp4_hwcsum函数计算硬件校验和;

  9. 对于其他情况,计算UDP校验和并存储在csum变量中。

  10. 添加伪首部,并计算最终的 UDP 校验和。如果校验和值为0,则设置为CSUM_MANGLED_0,以避免零校验和;

  11. 调用 ip_send_skb 函数进行实际的发送数据报;

  12. 根据发送是否成功,增加统计信息。

五、传输层(TCP)

敬请期待!

六、网络层(IP)

6.1 概述

  1. 数据包来到网络层,初始化 IP 头信息;

  2. 数据包经过 Netfilter 过滤和修改;

  3. 根据协议类型(IPv4 或 IPv6)调用相应的网络输出函数;

  4. 并再次经过 Netfilter 过滤和修改;

  5. 执行 cgroup egress BPF 对数据包进行过滤:

  6. 如果 skb 是 GSO 数据包,则使用特定函数处理;

  7. 如果 skb 长度大于 MTU 或者有分片信息,则进行分片处理;

  8. 做进入下一层邻居子系统前的处理。

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

其主要逻辑有二:

  1. 首先调用 ip_local_out 函数,将数据包 skb 发送到IP层;

  2. 如果发送出现错误,将错误码转换为负数,并增加相应的 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;
}

其主要逻辑有二:

  1. 首先调用 __ip_local_out 函数,将数据包 skb 发送到本地 IP 层进行处理;

  2. 如果返回值为 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
);
}

其主要逻辑有五:

  1. 设置 IPv4 首部的总长度字段(tot_len),将 skb 的长度转换为网络字节序(大端序);

  2. 计算 IPv4 首部的校验和字段(checksum),并将其填充到 iph 结构体中。

  3. skb 传递给 l3mdev_ip_out 函数进行处理。如果 skbNULL,表示已经被处理,无需继续传递,直接返回 0;

  4. 设置 skb 的协议字段为 IPv4 协议(ETH_P_IP);

  5. 最后,调用网络过滤钩子(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));
}

其主要逻辑有三:

  1. 设置 skb 的输出网络设备和协议类型,以便将数据包发送到指定的物理设备;

  2. 调用网络过滤钩子(Netfilter hook)NF_HOOK_COND 处理数据包;

  3. 然后调用 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;
}
}

其主要逻辑有二:

  1. 调用 BPF_CGROUP_RUN_PROG_INET_EGRESS 函数对数据包进行 BPF 过滤;

  2. 根据 BPF 过滤的结果(ret),进行相应的处理:

    1. 如果过滤结果为 NET_XMIT_SUCCESS,表示允许数据包继续进行 IP 输出处理,调用 __ip_finish_output 函数继续处理;

    2. 如果过滤结果为 NET_XMIT_CN,表示需要继续进行IP输出处理,或者返回 NET_XMIT_CN,表示控制网络;

    3. 如果过滤结果为其他值,表示不允许数据包继续发送,释放数据包,并返回 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);
}

其主要逻辑有五:

  1. 首先检查是否需要进行策略查找和重新路由。如果 skb 绑定了 XFRM(安全传输模块),表示需要进行策略查找,重新路由,将 IPCB(skb)->flags 设置为 IPSKB_REROUTED,然后调用 dst_output 函数继续处理数据包;

  2. 接着,获取 IP 协议层的 MTU(最大传输单元),以便后续处理;

  3. 如果 skb 是 GSO(Generic Segmentation Offload)数据包,则调用 ip_finish_output_gso 函数进行特定处理;

  4. 如果 skb 长度大于 MTU 或者有分片信息(IPCB(skb)->frag_max_size),则调用 ip_fragment 函数进行分片处理;

  5. 否则,直接调用 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;
}

其主要逻辑有四:

  1. 首先,根据路由表 rt 中的类型(RTN_MULTICAST 或 RTN_BROADCAST)更新 IPv4 协议统计信息中的多播数据包或广播数据包的数量;

  2. 然后,检查是否需要扩展数据包头部空间,如果需要则进行扩展。这是为了确保数据包能够存放下目标网络设备的链路层(MAC)头部;

  3. 接着,检查是否需要进行隧道传输,如果需要则调用 lwtunnel_xmit 函数进行隧道传输处理;

  4. 最后,通过路由表查找数据包的下一跳邻居,并调用 neigh_output 函数向邻居发送数据包。如果找不到下一跳的邻居,则释放数据包,并返回错误。

该函数的主要目的是确保数据包能够正确发送到下一跳,以便进行最终的物理设备发送,或者通过隧道传输发送。

七、邻居子系统(Neighbor)

这一层属于网络层,但单独拿出一节介绍是因为其逻辑相对独立。

7.1 概述

  1. 检查邻居的状态和缓存是否有效;

  2. 如果有状态和缓存都满足,则直接将数据包发给邻居;

  3. 如果不满足,则需要先获取邻居(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);
}

其主要逻辑有二:

  1. 首先检查邻居的状态和缓存是否有效。如果邻居状态为 NUD_CONNECTED,且缓存有效(hh_len 非零),则直接调用 neigh_hh_output 函数向邻居发送数据包。neigh_hh_output 函数是专门用于向邻居发送数据包的函数,它使用缓存中的信息直接发送数据包,避免了再次查找邻居的过程,提高了发送效率。

  2. 如果缓存无效或者需要跳过缓存,则直接调用邻居的输出函数 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);
}

其主要逻辑有四:

  1. 首先,通过读取 hh_cache(硬件头部缓存)的数据长度,并获取锁来保证读取的一致性。根据硬件头部缓存的数据长度,确定数据的对齐方式和长度;

  2. 然后,检查是否有足够的 sk_buff headroom 来存放硬件头部缓存的数据。如果有足够的 headroom,则将硬件头部缓存的数据复制到 sk_buff 的头部;

  3. 接着,检查 headroom 是否足够来存放硬件头部缓存的数据。如果 headroom 不足,则释放 sk_buff,并返回 NET_XMIT_DROP 错误码;

  4. 最后,将 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;
}

其主要逻辑有四:

  1. 首先,检查邻居是否处于事件队列中。如果邻居不在事件队列中,则调用 neigh_event_send 函数发送邻居事件(ARP 请求),以便进行邻居缓存的更新或解析;

  2. 接着,检查网络设备是否支持硬件头部缓存,并检查邻居的硬件头部缓存长度。如果硬件头部缓存长度为0,则调用 neigh_hh_init 函数初始化硬件头部缓存,用于后续的硬件头部处理;

  3. 然后,移动 sk_buff 的网络层偏移,即准备数据部分,以便进行硬件头部处理。接着,获取邻居硬件地址锁,并在获取硬件地址前获取邻居硬件地址。在获取硬件地址时,使用 dev_hard_header 函数填充硬件头部,并进行硬件地址的解析;

  4. 最后,检查是否成功获取硬件地址。如果获取硬件地址成功,则调用 dev_queue_xmit 函数将 sk_buff 发送出去。如果获取硬件地址失败,则释放 sk_buff 并返回错误码。

该函数是网络层向邻居发送数据包的一个重要环节,它根据目标邻居的状态、地址等信息选择合适的网络设备和物理链路,解析邻居硬件地址并进行硬件头部处理,最后确保数据包能够正确发送到目标邻居。

neigh_hh_output 函数和 neigh_resolve_output 函数最后都调用了 dev_queue_xmit 函数继续处理,进入网络接口层。


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多