内核是如何发送数据包
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++) {
//有几个队列就构造几个RingBuffer
err = 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找到socket
sock = sockfd_lookup_light(fd, &err, &fput_needed);
//2.构造msghdr
msg.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_sendmsg
int 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_buff
if (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_buff
if (!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);
//申请skb
skb = 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;
}
...
//更新seq
tp->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);
else
tcp_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);
else
skb = 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);
else
tcp_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);
else
iph->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_output
return 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_ACT
skb->tc_at_ingress = 0;
// 如果需要处理出口数据
# ifdef CONFIG_NET_EGRESS
if (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);
else
skb_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)
{
...
//取出一个要发送的skb
skb = 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); // 验证要发送的 skb
if (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); // 调用驱动程序尝试直接发送 skb
HARD_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 - 重新排队 skb
if (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; // 如果设备被冻结或停止,返回 0
return 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); // 如果数据包已被消耗,则跟踪
else
trace_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 为 NULL
struct 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_DES
i = 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;
// 将数据包片段映射到 DMA
dma = 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_buffer
union e1000_adv_tx_desc *tx_desc; // 指向当前处理的 tx_desc
unsigned 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_buffer
tx_desc = IGB_TX_DESC(tx_ring, i); // 获取当前要清理的 tx_desc
i -= 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; // 更新发送的总数据包数
//释放skb
napi_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 的总数据包数
...
}