当前位置: 首页 > news >正文

内核是如何发送数据包

1、网络发包总览

网络发包总流程图如下:
在这里插入图片描述
从上图中可以看到用户数据被拷贝到内核态,然后经过协议栈处理后进入RingBuffer。随后网卡驱动真正的将数据发送了出去。当发送完成的时候,是通过硬中断来通知CPU,然后清理RingBuffer。

下面从源码的角度给出一个流程图。
在这里插入图片描述

2、网卡启动准备

现在服务器上的网卡一般都是支持多队列的。每一个队列是由一个RingBuffer表示的,开启了多队列以后的网卡就会对应有多个RingBuffer。网卡启动时的最重要任务之一就是分配和初始化RingBuffer.
在网卡启动时,会调用__igb_open函数,RingBuffer就是在这里分配的。

static int __igb_open(struct net_device *netdev, bool resuming)
{struct igb_adapter *adapter = netdev_priv(netdev);struct e1000_hw *hw = &adapter->hw;struct pci_dev *pdev = adapter->pdev;//分配传输描述符数组err = igb_setup_all_tx_resources(adapter);//分配接收描述符数组err = igb_setup_all_rx_resources(adapter);//中断注册,igb_msix_ring就是在这里进行注册的err = igb_request_irq(adapter);//开启全部队列netif_tx_start_all_queues(netdev);..
}
static int igb_setup_all_tx_resources(struct igb_adapter *adapter)
{struct pci_dev *pdev = adapter->pdev;int i, err = 0;for (i = 0; i < adapter->num_tx_queues; i++) {//有几个队列就构造几个RingBuffererr = igb_setup_tx_resources(adapter->tx_ring[i]);if (err) {dev_err(&pdev->dev,"Allocation for Tx Queue %u failed\n", i);for (i--; i >= 0; i--)igb_free_tx_resources(adapter->tx_ring[i]);break;}}return err;
}
int igb_setup_tx_resources(struct igb_ring *tx_ring)
{//申请igb_tx_buffer数组内存size = sizeof(struct igb_tx_buffer) * tx_ring->count;tx_ring->tx_buffer_info = vmalloc(size);if (!tx_ring->tx_buffer_info)goto err;//申请e1000_adv_tx_desc DMA数组内存tx_ring->size = tx_ring->count * sizeof(union e1000_adv_tx_desc);tx_ring->size = ALIGN(tx_ring->size, 4096);tx_ring->desc = dma_alloc_coherent(dev, tx_ring->size,&tx_ring->dma, GFP_KERNEL);//初始化队列成员tx_ring->next_to_use = 0;tx_ring->next_to_clean = 0;...
}

上面__igb_open调用igb_setup_all_tx_resources分配所有传输的RingBuffer,调用igb_setup_all_rx_resources创建所有的接收RingBuffer。真正的RingBuffer构建是在igb_setup_tx_resources完成的。从上述源码可以看到一个传输RingBuffer的内部包含两个环形数组:igb_tx_buffer数组是内核使用;e1000_adv_tx_desc数组是硬件网卡使用。这两个数组在发送的时候,相同位置的指针都将指向同一个skb。这样内核和硬件就能共同访问同样的数据了,内核往skb写数据,网卡硬件负责发送。
硬中断的处理函数igb_msix_ring也是在__igb_open中注册的。

3、数据从用户进程到网卡的详细过程

3.1 send系统调用实现

send系统调用源码位于net/socket.c中,其内部实际使用的是sendto系统调用。该函数主要干了两件事:在内核中将真正的socket找出来,在这个对象里记录了各种协议栈的函数地址;构造一个struct msghdr对象,把用户传入的数据copy进去。之后就调用inet_sendmsg了。大致流程如下图:
在这里插入图片描述
源码如下:

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)
{struct socket *sock;struct msghdr msg;err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter);if (unlikely(err))return err;//1.根据fd找到socketsock = sockfd_lookup_light(fd, &err, &fput_needed);//2.构造msghdrmsg.msg_name = NULL;msg.msg_control = NULL;msg.msg_controllen = 0;msg.msg_namelen = 0;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;}if (sock->file->f_flags & O_NONBLOCK)flags |= MSG_DONTWAIT;msg.msg_flags = flags;//3.发送数据err = sock_sendmsg(sock, &msg);
}

从源码可以看到,send只是sendto封装出来的。在sendto系统调用里,首先根据用户传进来的句柄号来查找真正的socket对象,接着将用户请求的buf、len、flag等参数打包成一个msghdr对象。接着调用了sock_sendmsg==>sock_sendmsg_nosec,在sock_sendmsg_nosec中进入协议栈。

static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{//实际调用的是inet_sendmsgint ret = sock->ops->sendmsg(sock, msg, msg_data_left(msg));BUG_ON(ret == -EIOCBQUEUED);return ret;
}

3.2 传输层处理

传输层发送流程大致如下:
在这里插入图片描述

3.2.1 传输层拷贝

int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{...return sk->sk_prot->sendmsg(sk, msg, size);
}

对于TCP的socket来说,sk->sk_prot->sendmsg实际上是指向tcp_sendmsg(对于UDP的socket来说实际上是udp_sendmsg)。
由于这个函数比较长,下面分开进行理解。

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{// 开始发送数据copied = 0;
restart:mss_now = tcp_send_mss(sk, &size_goal, flags); // 获取当前 MSS 和目标大小while (msg_data_left(msg)) {int copy = 0;int max = size_goal;skb = tcp_write_queue_tail(sk); // 获取发送队列尾部的 sk_buffif (tcp_send_head(sk)) {if (skb->ip_summed == CHECKSUM_NONE)max = mss_now;copy = max - skb->len;}if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {bool first_skb;new_segment:// 分配新的 sk_buffif (!sk_stream_memory_free(sk))goto wait_for_sndbuf;if (process_backlog && sk_flush_backlog(sk)) {process_backlog = false;goto restart;}first_skb = skb_queue_empty(&sk->sk_write_queue);//申请skbskb = sk_stream_alloc_skb(sk,select_size(sk, sg, first_skb),sk->sk_allocation,first_skb);if (!skb)goto wait_for_memory;process_backlog = true;if (sk_check_csum_caps(sk))skb->ip_summed = CHECKSUM_PARTIAL;//把skb挂到socket的发送队列末尾skb_entail(sk, skb);copy = size_goal;max = size_goal;if (tp->repair)TCP_SKB_CB(skb)->sacked |= TCPCB_REPAIRED;}if (copy > msg_data_left(msg))copy = msg_data_left(msg);//如果skb有空余空间,则将msg存储的数据copy到skb中if (skb_availroom(skb) > 0) {copy = min_t(int, copy, skb_availroom(skb));//将用户空间的数据拷贝到内核空间,同时计算教育和err = skb_add_data_nocache(sk, skb, &msg->msg_iter, copy);if (err)goto do_fault;} ...//更新seqtp->write_seq += copy;TCP_SKB_CB(skb)->end_seq += copy;tcp_skb_pcount_set(skb, 0);wait_for_sndbuf:set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:if (copied)tcp_push(sk, flags & ~MSG_MORE, mss_now,TCP_NAGLE_PUSH, size_goal);//socket发送缓存不足时,如果是阻塞套接字会陷入等待err = sk_stream_wait_memory(sk, &timeo);if (err != 0)goto do_error;mss_now = tcp_send_mss(sk, &size_goal, flags);}out:if (copied) {tcp_tx_timestamp(sk, sockc.tsflags, tcp_write_queue_tail(sk));tcp_push(sk, flags, mss_now, tp->nonagle, size_goal);}
out_nopush:release_sock(sk);//返回已复制的长度return copied + copied_syn;
}

上面的源码主要是将用户层的数据拷贝到socket的发送缓存队列末尾,如果socket缓存空间不够,而socket又是阻塞模式的就会陷入等待直到超时或者条件满足。在这个copy步骤中,如果用户层发送的数据长度超过mss,则会进行多次分割copy。

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{while (msg_data_left(msg)) {//发送判断if (forced_push(tp)) {tcp_mark_push(tp, skb);__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);} else if (skb == tcp_send_head(sk))tcp_push_one(sk, mss_now);continue;}...
}

在发送只有满足forced_push(tp)或skb == tcp_send_head(sk)成立时,内核才会启动发送数据包。其中forced_push(tp)判断的时未发送的数据是否超过最大窗口的一半,skb == tcp_send_head(sk)判断的是队列最末尾的skb是不是待发送的skb。
条件不满足的话只是将用户数据拷贝到socket的发送队列。

3.2.2 传输层发送

tcp_write_xmit 是 Linux 内核 TCP 协议栈中用于处理数据包发送的核心函数。它负责从 TCP 套接字的发送队列中取出数据包,并在满足特定条件时通过 IP 层发送它们。这个函数处理了多种情况,包括 MTU 探测、拥塞窗口测试、发送窗口测试、Nagle 算法、TSO(TCP Segmentation Offload)处理等。

static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,int push_one, gfp_t gfp)
{struct tcp_sock *tp = tcp_sk(sk); // 获取 TCP 特定的 sock 结构struct sk_buff *skb; // 指向待发送的 sk_buff 结构unsigned int tso_segs, sent_pkts; // TSO 分段数,已发送数据包数int cwnd_quota; // 拥塞窗口配额int result; // 用于存储函数返回值bool is_cwnd_limited = false, is_rwnd_limited = false; // 拥塞窗口和发送窗口限制标志u32 max_segs; // 最大分段数sent_pkts = 0; // 初始化已发送数据包数if (!push_one) {// 执行 MTU 探测result = tcp_mtu_probe(sk);if (!result) {return false; // 如果探测失败,返回 false} else if (result > 0) {sent_pkts = 1; // 如果探测成功,增加已发送数据包数}}max_segs = tcp_tso_segs(sk, mss_now); // 计算 TSO 分段数while ((skb = tcp_send_head(sk))) { // 循环处理发送队列头部的数据包unsigned int limit; // 发送限制tso_segs = tcp_init_tso_segs(skb, mss_now); // 初始化 TSO 分段数BUG_ON(!tso_segs); // 确保 TSO 分段数有效if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) {// 如果需要修复,跳过网络传输skb_mstamp_get(&skb->skb_mstamp);goto repair;}// 测试拥塞窗口是否足够cwnd_quota = tcp_cwnd_test(tp, skb);if (!cwnd_quota) {if (push_one == 2) {// 强制发送一个丢包探测数据包cwnd_quota = 1;} else {break; // 如果拥塞窗口不足,退出循环}}// 测试发送窗口是否足够if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) {is_rwnd_limited = true; // 标记发送窗口限制break;}// 处理 Nagle 算法if (tso_segs == 1) {if (unlikely(!tcp_nagle_test(tp, skb, mss_now,(tcp_skb_is_last(sk, skb) ?nonagle : TCP_NAGLE_PUSH)))) {break; // 如果 Nagle 算法不允许发送,退出循环}} else {// 如果 TSO 分段数大于 1,检查是否应该推迟发送if (!push_one &&tcp_tso_should_defer(sk, skb, &is_cwnd_limited,max_segs)) {break; // 如果应该推迟,退出循环}}// 计算最大发送大小limit = mss_now;if (tso_segs > 1 && !tcp_urg_mode(tp)) {limit = tcp_mss_split_point(sk, skb, mss_now,min_t(unsigned int,cwnd_quota,max_segs),nonagle);}// 如果数据包大小超过限制,尝试分片if (skb->len > limit &&unlikely(tso_fragment(sk, skb, limit, mss_now, gfp))) {break; // 如果分片失败,退出循环}// 如果设置了 TCP_TSQ_DEFERRED 标志,清除它if (test_bit(TCP_TSQ_DEFERRED, &sk->sk_tsq_flags))clear_bit(TCP_TSQ_DEFERRED, &sk->sk_tsq_flags);// 检查发送队列大小是否过小if (tcp_small_queue_check(sk, skb, 0)) {break; // 如果队列太小,退出循环}// 通过 IP 层发送数据包if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp))) {break; // 如果发送失败,退出循环}repair:// 更新发送头部,标记数据包已发送tcp_event_new_data_sent(sk, skb);// 更新最小序列号tcp_minshall_update(tp, mss_now, skb);// 增加已发送数据包数sent_pkts += tcp_skb_pcount(skb);if (push_one) {break; // 如果设置了 push_one,退出循环}}// 如果发送窗口限制,启动计时器if (is_rwnd_limited)tcp_chrono_start(sk, TCP_CHRONO_RWND_LIMITED);elsetcp_chrono_stop(sk, TCP_CHRONO_RWND_LIMITED);// 如果已发送数据包,更新拥塞窗口和发送窗口if (likely(sent_pkts)) {if (tcp_in_cwnd_reduction(sk))tp->prr_out += sent_pkts;// 安排发送丢包探测if (push_one != 2)tcp_schedule_loss_probe(sk);is_cwnd_limited |= (tcp_packets_in_flight(tp) >= tp->snd_cwnd);tcp_cwnd_validate(sk, is_cwnd_limited);return false; // 返回 false,表示没有更多数据包需要发送}return !tp->packets_out && tcp_send_head(sk); // 如果没有数据包发送,返回 true
}
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,gfp_t gfp_mask)
{const struct inet_connection_sock *icsk = inet_csk(sk);struct inet_sock *inet;struct tcp_sock *tp;struct tcp_skb_cb *tcb;struct tcp_out_options opts;unsigned int tcp_options_size, tcp_header_size;struct tcp_md5sig_key *md5;struct tcphdr *th;int err;/* 确保传入的 sk_buff 有效,并且至少有一个 TCP 段被发送 */BUG_ON(!skb || !tcp_skb_pcount(skb));tp = tcp_sk(sk); /* 获取 TCP 特定的 sock 结构 *//* 如果需要克隆 sk_buff,进行时间戳获取和速率计算 */if (clone_it) {skb_mstamp_get(&skb->skb_mstamp);TCP_SKB_CB(skb)->tx.in_flight = TCP_SKB_CB(skb)->end_seq- tp->snd_una;tcp_rate_skb_sent(sk, skb);/* 克隆 sk_buff,为发送做准备 */if (unlikely(skb_cloned(skb)))skb = pskb_copy(skb, gfp_mask);elseskb = skb_clone(skb, gfp_mask);if (unlikely(!skb))return -ENOBUFS;}/* 获取 inet_sock 和 tcp_skb_cb 结构 */inet = inet_sk(sk);tcb = TCP_SKB_CB(skb);memset(&opts, 0, sizeof(opts)); /* 初始化 TCP 选项 *//* 根据是否为 SYN 包计算 TCP 选项的大小 */if (unlikely(tcb->tcp_flags & TCPHDR_SYN))tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5);elsetcp_options_size = tcp_established_options(sk, skb, &opts,&md5);tcp_header_size = tcp_options_size + sizeof(struct tcphdr); /* TCP 头部总大小 *//* 如果没有数据包在队列中,允许 XPS 选择另一个队列 */skb->ooo_okay = sk_wmem_alloc_get(sk) < SKB_TRUESIZE(1);/* 如果使用了内存预留来分配这个 sk_buff,这可能会导致回环时的丢包 */skb->pfmemalloc = 0;/* 为 TCP 头部腾出空间 */skb_push(skb, tcp_header_size);skb_reset_transport_header(skb);/* 将 sk_buff 标记为孤儿,以便它不会影响原始 socket */skb_orphan(skb);skb->sk = sk; /* 设置 sk_buff 所属的 socket */skb->destructor = skb_is_tcp_pure_ack(skb) ? __sock_wfree : tcp_wfree; /* 设置销毁函数 */skb_set_hash_from_sk(skb, sk); /* 设置哈希值 */atomic_add(skb->truesize, &sk->sk_wmem_alloc); /* 更新内存分配统计 *//* 设置目的确认为挂起状态 */skb_set_dst_pending_confirm(skb, sk->sk_dst_pending_confirm);//封装TCP头th = (struct tcphdr *)skb->data;th->source		= inet->inet_sport; /* 设置源端口 */th->dest		= inet->inet_dport; /* 设置目的端口 */th->seq			= htonl(tcb->seq); /* 设置序列号 */th->ack_seq		= htonl(tp->rcv_nxt); /* 设置确认序列号 */*(((__be16 *)th) + 6)	= htons(((tcp_header_size >> 2) << 12) |tcb->tcp_flags); /* 设置数据偏移和控制标志 */th->check		= 0; /* 清除校验和 */th->urg_ptr		= 0; /* 清除紧急指针 *//* 如果处于紧急模式,设置紧急指针 */if (unlikely(tcp_urg_mode(tp) && before(tcb->seq, tp->snd_up))) {if (before(tp->snd_up, tcb->seq + 0x10000)) {th->urg_ptr = htons(tp->snd_up - tcb->seq);th->urg = 1;} else if (after(tcb->seq + 0xFFFF, tp->snd_nxt)) {th->urg_ptr = htons(0xFFFF);th->urg = 1;}}/* 写入 TCP 选项 */tcp_options_write((__be32 *)(th + 1), tp, &opts);skb_shinfo(skb)->gso_type = sk->sk_gso_type; /* 设置 GSO 类型 *//* 设置窗口大小并处理 ECN */if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) {th->window      = htons(tcp_select_window(sk));tcp_ecn_send(sk, skb, th, tcp_header_size);} else {/* RFC1323: SYN & SYN/ACK 段的窗口不进行缩放 */th->window	= htons(min(tp->rcv_wnd, 65535U));}
#ifdef CONFIG_TCP_MD5SIG/* 如果启用了 MD5 签名,计算 MD5 哈希 */if (md5) {sk_nocaps_add(sk, NETIF_F_GSO_MASK);tp->af_specific->calc_md5_hash(opts.hash_location,md5, sk, skb);}
#endif/* 调用地址族特定的发送检查函数 */icsk->icsk_af_ops->send_check(sk, skb);/* 如果发送了 ACK,记录事件 */if (likely(tcb->tcp_flags & TCPHDR_ACK))tcp_event_ack_sent(sk, tcp_skb_pcount(skb));/* 如果发送了数据,记录事件 */if (skb->len != tcp_header_size) {tcp_event_data_sent(tp, sk);tp->data_segs_out += tcp_skb_pcount(skb);}/* 更新统计信息 */if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq)TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS,tcp_skb_pcount(skb));tp->segs_out += tcp_skb_pcount(skb); /* 更新段输出计数 *//* 设置 GSO 分段信息 */skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb);skb_shinfo(skb)->gso_size = tcp_skb_mss(skb);/* 清除时间戳 */skb->tstamp = 0;/* 清理 IP 栈的控制块 */memset(skb->cb, 0, max(sizeof(struct inet_skb_parm),sizeof(struct inet6_skb_parm)));//调用网络层发送接口err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);/* 处理发送结果 */if (likely(err <= 0))return err;/* 进入拥塞控制恢复模式 */tcp_enter_cwr(sk);/* 评估网络传输结果 */return net_xmit_eval(err);
}

tcp_transmit_skb的第一件事是克隆一个新的skb,这是因为最后到达网卡发送完成的时候,这个skb会被释放掉,而TCP支持丢失重传,所以传给网卡的是skb的一个拷贝,等收到ACK再真正删除。第二件事就是修改skb中的TCP头。这里的skb内部其实包含了网络协议所有的头,在网络协议栈不同层次传输时只需要移动指针即可,避免了频繁的内存申请和拷贝,提高了效率。第三步就是将数据交给网络层了。icsk->icsk_af_ops->queue_xmit实际是调用了ip_queue_xmit函数。

3.3 网络层发送处理

在网络层主要处理路由项查找、IP设置、netfilter过滤、skb切分(大于MTU的话)等工作。处理完这些工作之后交给更下一层的邻居子系统来处理。

int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{struct inet_sock *inet = inet_sk(sk);struct net *net = sock_net(sk);struct ip_options_rcu *inet_opt;struct flowi4 *fl4;struct rtable *rt;struct iphdr *iph;int res;/* 如果数据包已经被路由,跳过路由选择 */rcu_read_lock();inet_opt = rcu_dereference(inet->inet_opt);fl4 = &fl->u.ip4;rt = skb_rtable(skb);if (rt)goto packet_routed;//检查socket是否有缓存的路由表rt = (struct rtable *)__sk_dst_check(sk, 0);if (!rt) {__be32 daddr;/* 如果我们有选项,使用正确的目的地地址 */daddr = inet->inet_daddr;if (inet_opt && inet_opt->opt.srr)daddr = inet_opt->opt.faddr;/* 如果路由选择失败,传输层的重传机制会不断尝试查找路由项直到路由出现或连接超时 */rt = ip_route_output_ports(net, fl4, sk,daddr, inet->inet_saddr,inet->inet_dport,inet->inet_sport,sk->sk_protocol,RT_CONN_FLAGS(sk),sk->sk_bound_dev_if);if (IS_ERR(rt))goto no_route;sk_setup_caps(sk, &rt->dst);}//为skb设置路由表skb_dst_set_noref(skb, &rt->dst);packet_routed:if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_uses_gateway)goto no_route;/* 确定我们要发送的位置,分配并构建 IP 头部 */skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));skb_reset_network_header(skb);iph = ip_hdr(skb);*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df)iph->frag_off = htons(IP_DF);elseiph->frag_off = 0;iph->ttl      = ip_select_ttl(inet, &rt->dst);iph->protocol = sk->sk_protocol;ip_copy_addrs(iph, fl4);...//发送res = ip_local_out(net, sk, skb);...
}

ip_queue_xmit函数进行了路由项查找,在路由表查到某个网络应该通过哪个网卡、哪个网关发送出去之后,就缓存到socket上,避免重复查询。接着把路由表地址也缓存到skb中。然后按照规范填充IP头,再通过ip_finish_output进入下一步处理。

int ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{int err;//执行netfilter过滤err = __ip_local_out(net, sk, skb);if (likely(err == 1))//开始发送数据err = dst_output(net, sk, skb);return err;
}

在调用ip_local_out=>__ip_local_out=>nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT的过程中会执行netfilter过滤。

static inline int dst_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{return skb_dst(skb)->output(net, sk, skb);
}

此函数找到skb的路由表(dst条目),然后调用路由表的output方法。指向的是ip_output方法。

int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{struct net_device *dev = skb_dst(skb)->dev;//统计IP_UPD_PO_STATS(net, IPSTATS_MIB_OUT, skb->len);skb->dev = dev;skb->protocol = htons(ETH_P_IP);//再次交给netfilter,完毕回调ip_finish_outputreturn NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,net, sk, skb, NULL, dev,ip_finish_output,!(IPCB(skb)->flags & IPSKB_REROUTED));
}
static int ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{unsigned int mtu;int ret;.../* 获取数据包的目的 MTU(最大传输单元) */mtu = ip_skb_dst_mtu(sk, skb);if (skb_is_gso(skb))return ip_finish_output_gso(net, sk, skb, mtu);/* 检查数据包长度是否大于 MTU 或者需要分片 */if (skb->len > mtu || (IPCB(skb)->flags & IPSKB_FRAG_PMTU))return ip_fragment(net, sk, skb, mtu, ip_finish_output2);/* 如果不需要分片,直接调用 ip_finish_output2 发送数据包 */return ip_finish_output2(net, sk, skb);
}
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; // 邻居发现条目u32 nexthop; // 下一跳地址// 统计多播或广播数据包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)) {struct sk_buff *skb2;skb2 = skb_realloc_headroom(skb, LL_RESERVED_SPACE(dev));if (!skb2) {kfree_skb(skb); // 如果分配失败,释放数据包return -ENOMEM;}if (skb->sk)skb_set_owner_w(skb2, skb->sk); // 设置数据包所有者consume_skb(skb); // 释放旧的数据包skb = skb2; // 更新数据包指针}// 处理隧道重定向if (lwtunnel_xmit_redirect(dst->lwtstate)) {int res = lwtunnel_xmit(skb);if (res < 0 || res == LWTUNNEL_XMIT_DONE)return res;}// 获取下一跳邻居rcu_read_lock_bh(); // 锁定 RCU 读锁//根据下一跳的IP地址查找邻居项,找不到就创建一个nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);neigh = __ipv4_neigh_lookup_noref(dev, nexthop);if (unlikely(!neigh))neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);if (!IS_ERR(neigh)) {int res;sock_confirm_neigh(skb, neigh); // 确认邻居res = neigh_output(neigh, skb); // 发送数据包rcu_read_unlock_bh(); // 解锁 RCU 读锁return res;}rcu_read_unlock_bh(); // 解锁 RCU 读锁net_dbg_ratelimited("%s: No header cache and no neighbour!\n", __func__);kfree_skb(skb); // 释放数据包return -EINVAL; // 返回错误码
}

3.4 邻居子系统

static inline int neigh_output(struct neighbour *n, struct sk_buff *skb)
{const struct hh_cache *hh = &n->hh;if ((n->nud_state & NUD_CONNECTED) && hh->hh_len)return neigh_hh_output(hh, skb);else//return n->output(n, skb);
}

如果是新创建的邻居表项,此时因为目的MAC还未获取,不具备发送IP报文的能力,调用n->output,实际是调用neigh_resolve_output,在这个函数内部有可能发出Arp网络请求。

int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb) {int rc = 0; // 初始化返回值,0 表示成功// 尝试发送邻居事件,如果失败则继续处理if (!neigh_event_send(neigh, skb)) {int err; // 用于存储错误代码的变量struct net_device *dev = neigh->dev; // 获取邻居结构体中的网络设备指针unsigned int seq; // 用于序列化访问的变量// 如果网络设备支持缓存并且邻居的硬件头部长度为0,则初始化硬件头部if (dev->header_ops->cache && !neigh->hh.hh_len)neigh_hh_init(neigh);// 循环直到硬件头部正确填充或序列化访问失败do {// 调整数据包指针,使其指向网络层头部__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)); // 如果序列化访问失败则重试// 如果硬头部填充成功,则将数据包放入网络设备的发送队列if (err >= 0)rc = dev_queue_xmit(skb);}...
}
static inline int neigh_hh_output(const struct hh_cache *hh, struct sk_buff *skb)
{unsigned int seq;unsigned int hh_len;do {seq = read_seqbegin(&hh->hh_lock); // 开始读取序列,用于乐观并发读取hh_len = hh->hh_len; // 获取硬件头部长度if (likely(hh_len <= HH_DATA_MOD)) {// 如果硬件头部长度小于等于 HH_DATA_MOD,直接复制memcpy(skb->data - HH_DATA_MOD, hh->hh_data, HH_DATA_MOD);} else {unsigned int hh_alen = HH_DATA_ALIGN(hh_len); // 对齐硬件头部长度// 复制对齐后的硬件头部memcpy(skb->data - hh_alen, hh->hh_data, hh_alen);}} while (read_seqretry(&hh->hh_lock, seq)); // 检查序列是否重试// 将硬件头部推入数据包skb_push(skb, hh_len);return dev_queue_xmit(skb); // 将数据包加入到设备的发送队列
}

当获取到硬件MAC地址之后,就可以封装skb的MAC头。最后调用dev_queue_xmit将skb传递给Linux网络设备子系统。

3.5 网络设备子系统

int dev_queue_xmit(struct sk_buff *skb)
{return __dev_queue_xmit(skb, NULL);
}static int __dev_queue_xmit(struct sk_buff *skb, void *accel_priv) {struct net_device *dev = skb->dev; // 获取数据包关联的网络设备struct netdev_queue *txq; // 指向网络设备队列的结构体struct Qdisc *q; // 指向队列规则(qdisc)的结构体int rc = -ENOMEM; // 初始化返回值为-ENOMEM,表示内存分配失败skb_reset_mac_header(skb); // 重置数据包的MAC层头部// 如果数据包需要时间戳,则设置时间戳if (unlikely(skb_shinfo(skb)->tx_flags & SKBTX_SCHED_TSTAMP))__skb_tstamp_tx(skb, NULL, skb->sk, SCM_TSTAMP_SCHED);// 禁用软中断,获取RCU读锁rcu_read_lock_bh();skb_update_prio(skb); // 更新数据包的优先级qdisc_pkt_len_init(skb); // 初始化队列规则的长度// 如果配置了网络分类和动作支持
#ifdef CONFIG_NET_CLS_ACTskb->tc_at_ingress = 0;// 如果需要处理出口数据
# ifdef CONFIG_NET_EGRESSif (static_key_false(&egress_needed)) {skb = sch_handle_egress(skb, &rc, dev);if (!skb)goto out; // 如果处理失败,跳转到out标签}
# endif
#endif// 如果设备或队列规则不需要数据包的目的地址,则立即释放if (dev->priv_flags & IFF_XMIT_DST_RELEASE)skb_dst_drop(skb);elseskb_dst_force(skb); // 否则强制设置目的地址// 选择一个发送队列txq = netdev_pick_tx(dev, skb, accel_priv);// 获取此队列关联的排队规则q = rcu_dereference_bh(txq->qdisc); trace_net_dev_queue(skb); // 跟踪网络设备队列// 如果队列规则有enqueue函数,则调用__dev_xmit_skb继续处理if (q->enqueue) {rc = __dev_xmit_skb(skb, q, dev, txq);goto out; // 跳转到out标签}// 如果设备没有队列规则,通常是软件设备的情况if (dev->flags & IFF_UP) {int cpu = smp_processor_id(); // 获取当前CPU编号// 如果当前CPU不是队列的锁持有者if (txq->xmit_lock_owner != cpu) {...HARD_TX_LOCK(dev, txq, cpu); // 获取硬件发送锁// 如果队列没有停止,则发送数据包if (!netif_xmit_stopped(txq)) {__this_cpu_inc(xmit_recursion); // 增加递归计数skb = dev_hard_start_xmit(skb, dev, txq, &rc); // 调用硬件发送函数__this_cpu_dec(xmit_recursion); // 减少递归计数if (dev_xmit_complete(rc)) {HARD_TX_UNLOCK(dev, txq); // 释放硬件发送锁goto out; // 跳转到out标签}}HARD_TX_UNLOCK(dev, txq); // 释放硬件发送锁net_crit_ratelimited("Virtual device %s asks to queue packet!\n", dev->name);} ...
}
static inline int __dev_xmit_skb(struct sk_buff *skb, struct Qdisc *q,struct net_device *dev,struct netdev_queue *txq) {spinlock_t *root_lock = qdisc_lock(q); // 获取队列规则的锁struct sk_buff *to_free = NULL; // 用于存储需要释放的数据包bool contended; // 标记是否在竞争状态int rc; // 用于存储返回值qdisc_calculate_pkt_len(skb, q); // 计算数据包的长度// 如果队列规则正在运行,尝试获取忙锁以序列化竞争contended = qdisc_is_running(q);if (unlikely(contended))spin_lock(&q->busylock); // 获取忙锁spin_lock(root_lock); // 获取队列规则的主锁// 如果队列规则被停用,则丢弃数据包if (unlikely(test_bit(__QDISC_STATE_DEACTIVATED, &q->state))) {__qdisc_drop(skb, &to_free); // 丢弃数据包rc = NET_XMIT_DROP; // 设置返回值为丢弃} else if ((q->flags & TCQ_F_CAN_BYPASS) && !qdisc_qlen(q) &&qdisc_run_begin(q)) { // 如果队列规则允许绕过并且队列为空// 直接发送数据包,不经过队列qdisc_bstats_update(q, skb); // 更新队列规则的统计信息if (sch_direct_xmit(skb, q, dev, txq, root_lock, true)) {// 如果直接发送成功if (unlikely(contended)) {spin_unlock(&q->busylock); // 释放忙锁contended = false; // 标记不再竞争}__qdisc_run(q); // 运行队列规则} else {qdisc_run_end(q); // 结束队列规则的运行}rc = NET_XMIT_SUCCESS; // 设置返回值为成功} else {// 将数据包加入队列rc = q->enqueue(skb, q, &to_free) & NET_XMIT_MASK;if (qdisc_run_begin(q)) {if (unlikely(contended)) {spin_unlock(&q->busylock); // 释放忙锁contended = false; // 标记不再竞争}__qdisc_run(q); // 运行队列规则}}spin_unlock(root_lock); // 释放队列规则的主锁if (unlikely(to_free)) // 如果有数据包需要释放kfree_skb_list(to_free); // 释放数据包if (unlikely(contended)) // 如果在竞争状态spin_unlock(&q->busylock); // 释放忙锁return rc; // 返回结果
}

__dev_xmit_skb函数的主要任务是将数据包发送到网络设备。它首先检查队列规则的状态,如果队列规则允许并且队列为空,它将尝试直接发送数据包。否则,它将数据包加入队列。在处理过程中,它还处理了竞争状态,以确保在多核处理器上正确地序列化访问。

void __qdisc_run(struct Qdisc *q)
{int quota = dev_tx_weight; // 从设备传输权重获取配额,这个配额限制了处理数据包的数量int packets; // 用于存储本轮处理的数据包数量while (qdisc_restart(q, &packets)) { // 循环,直到没有更多数据包可以处理// 根据可能的发生顺序排序:如果1.我们超出了数据包配额 2.另一个进程需要CPU,则推迟处理quota -= packets; // 从配额中减去本轮处理的数据包数量if (quota <= 0 || need_resched()) { // 如果配额用完或者系统需要调度其他进程运行__netif_schedule(q); // 调度网络设备,以便其他进程可以运行break; // 跳出循环}}qdisc_run_end(q); // 结束队列规则的运行,执行任何清理工作
}

从上述代码可以看到while循环不断的从队列取skb并进行发送。
注意:此时占用的是用户进程的系统态实际(sy)。只有当quota用尽或者需要调度其他进程运行时才触发软中断进行发送。
所以这就是在服务器上查看/proc/softirqs时,一般NET_RX比NET_TX大得多的原因。

static inline int qdisc_restart(struct Qdisc *q, int *packets)
{...//取出一个要发送的skbskb = dequeue_skb(q, &validate, packets);if (unlikely(!skb))return 0;root_lock = qdisc_lock(q);dev = qdisc_dev(q);txq = skb_get_tx_queue(dev, skb);return sch_direct_xmit(skb, q, dev, txq, root_lock, validate);
}

qdisc_restart从队列中取出一个skb,并调用sch_direct_xmit继续发送。

int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,struct net_device *dev, struct netdev_queue *txq,spinlock_t *root_lock, bool validate) {int ret = NETDEV_TX_BUSY; // 初始化返回值为 NETDEV_TX_BUSY,表示设备忙// 释放 qdisc 锁spin_unlock(root_lock);// 在没有锁的情况下验证 skb(GSO, 校验和, ...)if (validate)skb = validate_xmit_skb_list(skb, dev); // 验证要发送的 skbif (likely(skb)) { // 如果 skb 有效HARD_TX_LOCK(dev, txq, smp_processor_id()); // 获取硬件发送锁if (!netif_xmit_frozen_or_stopped(txq))skb = dev_hard_start_xmit(skb, dev, txq, &ret); // 调用驱动程序尝试直接发送 skbHARD_TX_UNLOCK(dev, txq); // 释放硬件发送锁} else {spin_lock(root_lock); // 如果 skb 无效,重新获取 qdisc 锁return qdisc_qlen(q); // 返回队列长度}spin_lock(root_lock); // 重新获取 qdisc 锁if (dev_xmit_complete(ret)) { // 如果发送完成// 驱动程序成功发送了 skb 或者 skb 被消耗ret = qdisc_qlen(q); // 返回队列长度} else {// 驱动程序返回 NETDEV_TX_BUSY - 重新排队 skbif (unlikely(ret != NETDEV_TX_BUSY))net_warn_ratelimited("BUG %s code %d qlen %d\n",dev->name, ret, q->q.qlen); // 如果返回值不是 NETDEV_TX_BUSY,发出警告ret = dev_requeue_skb(skb, q); // 将 skb 重新排队}if (ret && netif_xmit_frozen_or_stopped(txq))ret = 0; // 如果设备被冻结或停止,返回 0return ret; // 返回最终的返回值
}

这个函数主要工作是调用dev_hard_start_xmit驱动函数发送skb,如果发送失败将skb重新加入队列。

3.6 软中断调度

在上面我们看到当用户进程配额用尽时会调用__netif_schedule触发一个软中断。该函数会进入__netif_reschedule发出一个NET_TX_SOFTIRQ类型中断。
软中断由内核进程运行,该进程会进入net_tx_action函数,在该函数能获取发送队列,并也最终调用到驱动程序里的入口函数dev_hard_start_xmit,如下图所示:
在这里插入图片描述

static void __netif_reschedule(struct Qdisc *q)
{struct softnet_data *sd; // 定义一个指向软网数据结构的指针unsigned long flags; // 用于保存当前中断状态的标志local_irq_save(flags); // 保存当前中断状态并禁用本地中断sd = this_cpu_ptr(&softnet_data); // 获取当前 CPU 的软网数据结构q->next_sched = NULL; // 将队列规则的下一个调度指针设置为 NULL*sd->output_queue_tailp = q; // 将队列规则添加到输出队列的尾部sd->output_queue_tailp = &q->next_sched; // 更新输出队列的尾指针raise_softirq_irqoff(NET_TX_SOFTIRQ); // 触发网络传输软中断local_irq_restore(flags); // 恢复之前的中断状态
}

在这里设置了将保存了发送队列的q添加到CPU的soft_data的发送队列尾部,然后触发软中断。

static __latent_entropy void net_tx_action(struct softirq_action *h)
{struct softnet_data *sd = this_cpu_ptr(&softnet_data); // 获取当前 CPU 的软网数据结构if (sd->completion_queue) { // 如果有完成队列需要处理struct sk_buff *clist;local_irq_disable(); // 禁用本地中断clist = sd->completion_queue; // 获取完成队列sd->completion_queue = NULL; // 清空完成队列local_irq_enable(); // 启用本地中断while (clist) { // 遍历完成队列struct sk_buff *skb = clist; // 获取当前数据包clist = clist->next; // 移动到下一个数据包WARN_ON(atomic_read(&skb->users)); // 如果数据包的使用者计数器不为0,则发出警告if (likely(get_kfree_skb_cb(skb)->reason == SKB_REASON_CONSUMED))trace_consume_skb(skb); // 如果数据包已被消耗,则跟踪elsetrace_kfree_skb(skb, net_tx_action); // 否则,跟踪数据包的释放if (skb->fclone != SKB_FCLONE_UNAVAILABLE)__kfree_skb(skb); // 如果数据包不是克隆的,则立即释放else__kfree_skb_defer(skb); // 否则,延迟释放__kfree_skb_flush(); // 刷新所有延迟释放的数据包}}if (sd->output_queue) { // 如果有输出队列需要处理struct Qdisc *head;local_irq_disable(); // 禁用本地中断head = sd->output_queue; // 获取输出队列sd->output_queue = NULL; // 清空输出队列sd->output_queue_tailp = &sd->output_queue; // 重置尾指针local_irq_enable(); // 启用本地中断while (head) { // 遍历输出队列struct Qdisc *q = head; // 获取当前队列规则spinlock_t *root_lock;head = head->next_sched; // 移动到下一个队列规则root_lock = qdisc_lock(q); // 获取队列规则的锁spin_lock(root_lock); // 上锁/* We need to make sure head->next_sched is read* before clearing __QDISC_STATE_SCHED*/smp_mb__before_atomic(); // 确保内存操作的顺序clear_bit(__QDISC_STATE_SCHED, &q->state); // 清除调度状态位qdisc_run(q); // 发送数据spin_unlock(root_lock); // 释放锁}}
}

软中断这里会获取soft_data,软中断主要做了两件事:一清理发送完成队列的skb,二将发送队列的数据发送出去。这里发送数据消耗的CPU都显示在si这里,不会消耗用户进程时间。
这里和用户态一样会调用__qdisc_run发送数据。

static inline void qdisc_run(struct Qdisc *q)
{if (qdisc_run_begin(q))__qdisc_run(q);
}

然后也是进入qdisc_restart=>sch_direct_xmit,直到进入驱动程序函数dev_hard_start_xmit。

3.7 igb网卡驱动发送

无论是在用户进程的内核态还是对于软中断上下文,都会调用网络设备子系统中的dev_hard_start_xmit函数。在这个函数中会调用到驱动的发送函数igb_xmit_frame。
在驱动函数里,会将skb挂到RingBuffer上,驱动调用完毕,数据包将真正从网卡发送出去。流程图如下:
在这里插入图片描述

struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev,struct netdev_queue *txq, int *ret)
{struct sk_buff *skb = first; // 初始化 skb 为第一个数据包int rc = NETDEV_TX_OK; // 初始化返回码为 NETDEV_TX_OK,表示发送成功while (skb) { // 循环处理所有数据包,直到 skb 为 NULLstruct sk_buff *next = skb->next; // 保存下一个数据包的指针skb->next = NULL; // 将当前数据包的下一个指针设置为 NULL,因为硬件发送通常一次处理一个数据包rc = xmit_one(skb, dev, txq, next != NULL); // 调用 xmit_one 函数发送当前数据包if (unlikely(!dev_xmit_complete(rc))) { // 如果发送不完整(失败)skb->next = next; // 恢复下一个数据包的指针goto out; // 跳出循环}skb = next; // 移动到下一个数据包if (netif_xmit_stopped(txq) && skb) { // 如果队列已停止并且还有数据包rc = NETDEV_TX_BUSY; // 设置返回码为 NETDEV_TX_BUSY,表示设备忙break; // 跳出循环}}out:*ret = rc; // 将最终的返回码存储在 ret 指针指向的变量中return skb; // 返回当前处理的数据包(可能是最后一个数据包或因错误而停止的数据包)
}
static int xmit_one(struct sk_buff *skb, struct net_device *dev,struct netdev_queue *txq, bool more)
{unsigned int len;int rc;//tcpdump获取本机发送包工作的地方if (!list_empty(&ptype_all) || !list_empty(&dev->ptype_all))dev_queue_xmit_nit(skb, dev);len = skb->len;trace_net_dev_start_xmit(skb, dev);//发送数据包rc = netdev_start_xmit(skb, dev, txq, more);trace_net_dev_xmit(skb, rc, dev, len);return rc;
}
static inline netdev_tx_t netdev_start_xmit(struct sk_buff *skb, struct net_device *dev,struct netdev_queue *txq, bool more)
{//获取设备的回调函数集合const struct net_device_ops *ops = dev->netdev_ops;int rc;//继续发送rc = __netdev_start_xmit(ops, skb, dev, more);if (rc == NETDEV_TX_OK)txq_trans_update(txq);return rc;
}
static inline netdev_tx_t __netdev_start_xmit(const struct net_device_ops *ops,struct sk_buff *skb, struct net_device *dev,bool more)
{skb->xmit_more = more ? 1 : 0;return ops->ndo_start_xmit(skb, dev);
}

ndo_start_xmit是网卡驱动要实现的一个函数,是在net_device_ops中定义的。

drivers/net/ethernet/intel/igb/igb_main.c
static const struct net_device_ops igb_netdev_ops = {.ndo_open		= igb_open,	//当网络设备被激活(打开)时调用,通常用于初始化硬件。.ndo_stop		= igb_close,	//当网络设备被停用(关闭)时调用,用于停止设备的操作。.ndo_start_xmit		= igb_xmit_frame,	//用于发送数据包,调用驱动程序的发送函数。.ndo_get_stats64	= igb_get_stats64,	//获取网络设备的统计信息,如发送和接收的数据包数量等。.ndo_set_rx_mode	= igb_set_rx_mode,	//设置接收模式,如混杂模式、多播模式或单播模式。.ndo_set_mac_address	= igb_set_mac,	//设置网络设备的 MAC 地址。.ndo_change_mtu		= igb_change_mtu,	//更改网络设备的 MTU(最大传输单元)大小。.ndo_do_ioctl		= igb_ioctl,	//处理 IOCTL 命令,用于执行设备特定的操作。.ndo_tx_timeout		= igb_tx_timeout,	//处理发送超时,当设备在一定时间内没有发送数据包时调用。.ndo_validate_addr	= eth_validate_addr,	//验证硬件地址的有效性,通常由以太网设备通用层实现。...
};

对于网络设备层定义的ndo_start_xmit,igb的实现函数是igb_xmit_frame,这个函数是在网卡驱动初始化的时候被赋值的。

static netdev_tx_t igb_xmit_frame(struct sk_buff *skb,struct net_device *netdev)
{struct igb_adapter *adapter = netdev_priv(netdev); // 从网络设备结构体中获取私有数据...return igb_xmit_frame_ring(skb, igb_tx_queue_mapping(adapter, skb)); // 调用实际的发送函数
}
netdev_tx_t igb_xmit_frame_ring(struct sk_buff *skb,struct igb_ring *tx_ring)
{struct igb_tx_buffer *first;int tso;u32 tx_flags = 0;unsigned short f;u16 count = TXD_USE_COUNT(skb_headlen(skb)); // 计算所需的描述符数量__be16 protocol = vlan_get_protocol(skb); // 获取 VLAN 协议u8 hdr_len = 0; // 头部长度// 计算所需的描述符数量,包括数据、填充和上下文描述符for (f = 0; f < skb_shinfo(skb)->nr_frags; f++)count += TXD_USE_COUNT(skb_shinfo(skb)->frags[f].size);if (igb_maybe_stop_tx(tx_ring, count + 3)) { // 检查是否需要停止发送/* this is a hard error */return NETDEV_TX_BUSY; // 如果需要停止,则返回忙状态}// 获取TX Queue中下一个可用的缓冲区信息first = &tx_ring->tx_buffer_info[tx_ring->next_to_use];first->skb = skb;first->bytecount = skb->len;first->gso_segs = 1;if (unlikely(skb_shinfo(skb)->tx_flags & SKBTX_HW_TSTAMP)) { // 检查是否需要硬件时间戳struct igb_adapter *adapter = netdev_priv(tx_ring->netdev);if (!test_and_set_bit_lock(__IGB_PTP_TX_IN_PROGRESS, &adapter->state)) {skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS;tx_flags |= IGB_TX_FLAGS_TSTAMP;adapter->ptp_tx_skb = skb_get(skb);adapter->ptp_tx_start = jiffies;if (adapter->hw.mac.type == e1000_82576)schedule_work(&adapter->ptp_tx_work);}}skb_tx_timestamp(skb); // 记录数据包的时间戳if (skb_vlan_tag_present(skb)) { // 检查是否存在 VLAN 标签tx_flags |= IGB_TX_FLAGS_VLAN;tx_flags |= (skb_vlan_tag_get(skb) << IGB_TX_FLAGS_VLAN_SHIFT);}// 记录初始标志和协议first->tx_flags = tx_flags;first->protocol = protocol;tso = igb_tso(tx_ring, first, &hdr_len); // 尝试进行 TSO(TCP Segmentation Offload)if (tso < 0)goto out_drop; // 如果 TSO 失败,则丢弃数据包else if (!tso)igb_tx_csum(tx_ring, first); // 如果不进行 TSO,则计算校验和// 准备给设备发送的数据(给Tx Queue建立映射关系)igb_tx_map(tx_ring, first, hdr_len); ...
}
static void igb_tx_map(struct igb_ring *tx_ring,struct igb_tx_buffer *first,const u8 hdr_len)
{struct sk_buff *skb = first->skb; // 获取要发送的数据包struct igb_tx_buffer *tx_buffer;union e1000_adv_tx_desc *tx_desc; // 传输描述符struct skb_frag_struct *frag;dma_addr_t dma; // DMA 地址unsigned int data_len, size;u32 tx_flags = first->tx_flags; // 传输标志u32 cmd_type = igb_tx_cmd_type(skb, tx_flags); // 命令类型u16 i = tx_ring->next_to_use; // 下一个要使用的描述符索引tx_desc = IGB_TX_DESC(tx_ring, i); // 获取当前描述符// 设置描述符的 olinfo_status 字段igb_tx_olinfo_status(tx_ring, tx_desc, tx_flags, skb->len - hdr_len);size = skb_headlen(skb); // 数据包头部长度data_len = skb->data_len; // 数据包数据长度// 为skb->data构造内存映射,以允许设备通过DMA从RAM中读取数据dma = dma_map_single(tx_ring->dev, skb->data, size, DMA_TO_DEVICE);tx_buffer = first; // 初始化 tx_buffer 为第一个缓冲区//遍历该数据包所有分片,为skb的每个分片生成有效for (frag = &skb_shinfo(skb)->frags[0];; frag++) { // 遍历所有数据包片段if (dma_mapping_error(tx_ring->dev, dma)) // 检查 DMA 映射是否出错goto dma_error;// 记录长度和 DMA 地址dma_unmap_len_set(tx_buffer, len, size);dma_unmap_addr_set(tx_buffer, dma, dma);tx_desc->read.buffer_addr = cpu_to_le64(dma); // 设置描述符的缓冲区地址// 处理每个描述符的最大数据长度限制while (unlikely(size > IGB_MAX_DATA_PER_TXD)) {tx_desc->read.cmd_type_len = cpu_to_le32(cmd_type ^ IGB_MAX_DATA_PER_TXD);i++;tx_desc++;if (i == tx_ring->count) { // 如果到达描述符环的末尾,回到开始tx_desc = IGB_TX_DESi = 0;}tx_desc->read.olinfo_status = 0;dma += IGB_MAX_DATA_PER_TXD;size -= IGB_MAX_DATA_PER_TXD;tx_desc->read.buffer_addr = cpu_to_le64(dma);}if (likely(!data_len)) // 如果没有更多数据要发送,跳出循环break;tx_desc->read.cmd_type_len = cpu_to_le32(cmd_type ^ size);i++;tx_desc++;if (i == tx_ring->count) { // 如果到达描述符环的末尾,回到开始tx_desc = IGB_TX_DESC(tx_ring, 0);i = 0;}tx_desc->read.olinfo_status = 0;size = skb_frag_size(frag); // 获取片段大小data_len -= size;// 将数据包片段映射到 DMAdma = skb_frag_dma_map(tx_ring->dev, frag, 0, size, DMA_TO_DEVICE);tx_buffer = &tx_ring->tx_buffer_info[i]; // 更新 tx_buffer 为当前片段}// 设置最后一个描述符的 RS 和 EOP 位cmd_type |= size | IGB_TXD_DCMD;tx_desc->read.cmd_type_len = cpu_to_le32(cmd_type);// 更新网络设备发送统计信息netdev_tx_sent_queue(txring_txq(tx_ring), first->bytecount);...
}

当所有需要的描述符都已建好,且skb的所有数据都映射到DMA地址后,驱动就进入最后一步,触发真实的发送。

4、RingBuffer内存回收

当数据发送完成时,内存还没有清理。在发送完成时,网卡设备会触发硬中断来释放内存。

static inline void ____napi_schedule(struct softnet_data *sd,struct napi_struct *napi)
{list_add_tail(&napi->poll_list, &sd->poll_list);__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

在这里可以发现,无论是接收数据还是发送完成通知,从硬中断触发的软中断都是NET_RX_SOFTIRQ。
接着进入软中断的回调函数igb_poll。

static int igb_poll(struct napi_struct *napi, int budget)
{struct igb_q_vector *q_vector = container_of(napi,struct igb_q_vector,napi);bool clean_complete = true;int work_done = 0;...if (q_vector->tx.ring)clean_complete = igb_clean_tx_irq(q_vector, budget);...
}
static bool igb_clean_tx_irq(struct igb_q_vector *q_vector, int napi_budget)
{struct igb_adapter *adapter = q_vector->adapter; // 获取适配器实例struct igb_ring *tx_ring = q_vector->tx.ring; // 获取发送队列struct igb_tx_buffer *tx_buffer; // 指向当前处理的 tx_bufferunion e1000_adv_tx_desc *tx_desc; // 指向当前处理的 tx_descunsigned int total_bytes = 0, total_packets = 0; // 用于统计发送的总字节和数据包数量unsigned int budget = q_vector->tx.work_limit; // NAPI 预算unsigned int i = tx_ring->next_to_clean; // 从哪里开始清理if (test_bit(__IGB_DOWN, &adapter->state)) // 如果设备已经关闭return true; // 返回 true 表示没有更多的工作要做tx_buffer = &tx_ring->tx_buffer_info[i]; // 获取当前要清理的 tx_buffertx_desc = IGB_TX_DESC(tx_ring, i); // 获取当前要清理的 tx_desci -= tx_ring->count; // 调整索引do {union e1000_adv_tx_desc *eop_desc = tx_buffer->next_to_watch; // 获取这个数据包的最后一个描述符/* if next_to_watch is not set then there is no work pending */if (!eop_desc) // 如果没有工作待处理,则退出循环break;/* prevent any other reads prior to eop_desc */read_barrier_depends(); // 确保 eop_desc 的读取不会乱序/* if DD is not set pending work has not been completed */if (!(eop_desc->wb.status & cpu_to_le32(E1000_TXD_STAT_DD))) // 如果 DD 位未设置,表示工作未完成break;/* clear next_to_watch to prevent false hangs */tx_buffer->next_to_watch = NULL; // 清除 next_to_watch 以避免假挂起/* update the statistics for this packet */total_bytes += tx_buffer->bytecount; // 更新发送的总字节数total_packets += tx_buffer->gso_segs; // 更新发送的总数据包数//释放skbnapi_consume_skb(tx_buffer->skb, napi_budget); /* unmap skb header data */dma_unmap_single(tx_ring->dev, // 取消映射 skb 头部数据dma_unmap_addr(tx_buffer, dma),dma_unmap_len(tx_buffer, len),DMA_TO_DEVICE);/* clear tx_buffer data */dma_unmap_len_set(tx_buffer, len, 0); // 清除 tx_buffer 数据/* clear last DMA location and unmap remaining buffers */while (tx_desc != eop_desc) { // 清除剩余的 DMA 映射tx_buffer++;tx_desc++;i++;if (unlikely(!i)) {i -= tx_ring->count;tx_buffer = tx_ring->tx_buffer_info;tx_desc = IGB_TX_DESC(tx_ring, 0);}/* unmap any remaining paged data */if (dma_unmap_len(tx_buffer, len)) {dma_unmap_page(tx_ring->dev,dma_unmap_addr(tx_buffer, dma),dma_unmap_len(tx_buffer, len),DMA_TO_DEVICE);dma_unmap_len_set(tx_buffer, len, 0);}}/* move us one more past the eop_desc for start of next pkt */tx_buffer++; // 移动到下一个数据包tx_desc++;i++;if (unlikely(!i)) {i -= tx_ring->count;tx_buffer = tx_ring->tx_buffer_info;tx_desc = IGB_TX_DESC(tx_ring, 0);}/* issue prefetch for next Tx descriptor */prefetch(tx_desc); // 预取下一个 Tx 描述符/* update budget accounting */budget--; // 更新预算} while (likely(budget)); // 如果预算还有剩余,继续处理netdev_tx_completed_queue(txring_txq(tx_ring), // 更新网络设备的完成统计total_packets, total_bytes);i += tx_ring->count; // 调整索引tx_ring->next_to_clean = i; // 设置新的清理位置u64_stats_update_begin(&tx_ring->tx_syncp); // 开始更新统计信息tx_ring->tx_stats.bytes += total_bytes; // 更新发送的总字节数tx_ring->tx_stats.packets += total_packets; // 更新发送的总数据包数u64_stats_update_end(&tx_ring->tx_syncp); // 结束更新统计信息q_vector->tx.total_bytes += total_bytes; // 更新 q_vector 的总字节数q_vector->tx.total_packets += total_packets; // 更新 q_vector 的总数据包数...
}

http://www.mrgr.cn/news/33391.html

相关文章:

  • 文档布局内容检测系统源码分享
  • 连续数组问题
  • STL之list
  • c语言-数据类型
  • C++:数组与字符串
  • Git从了解到操作
  • 【homebrew安装】踩坑爬坑教程
  • Renesas R7FA8D1BH (Cortex®-M85) 生成4路PWM
  • 【ArcGIS微课1000例】0123:数据库中要素类批量转为shapefile
  • 数据结构之堆(优先级队列)
  • 2024/9/22周报
  • 【面经】查找中常见的树数据结构
  • 8. Data Member的绑定
  • 国产游戏技术能否引领全球【终稿】
  • CompletableFuture如何优雅处理异步任务超时!妙就完了
  • 国人卖家可折叠无线充电器发起TRO专利维权,功能相同可能侵权
  • 【深入学习Redis丨第六篇】Redis哨兵模式与操作详解
  • 图神经网络的新篇章:通用、强大、可扩展的图变换器
  • WebGL基础知识快速入门
  • 空栈压数 - 华为OD统一考试(E卷)