Linux是怎么发送一个网络包的?

news2025/2/22 10:23:30

目录

摘要

1 从 send 开始

2 传输层

3 网络层

4 网络接口层

4.1 邻居子系统

4.2 网络设备子系统

4.3 软中断发送剩余的 skb

4.4  硬中断又触发软中断

总结


摘要

        一个网络包的发送,始于应用层,经层层协议栈的封装,终于网卡。今天来循着一个网络包的足迹👣,深入学习一下 Linux 下发送数据的处理流程。

文中引用 Linux 内核源码基于版本 2.6.34,并做了一些删减以提高可读性。

        当你手头正好有一个 scoket ,并且开辟了一个 buf,就会情不自禁的想要把这个 buf 塞给 socket 发送出去。虽然我们有多个方法可用,但请从 send 开始吧~

1 从 send 开始

         嗯,send 系统调用做的事非常的简单,就是调了一下 sys_sendto。从这里就可以看出来了,sys_send  调用封装了 sys_sendto,两者只有参数的差别:

// net/socket.c
SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,
		unsigned, flags)
{
	return sys_sendto(fd, buff, len, flags, NULL, 0);
}

SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
		unsigned, flags, struct sockaddr __user *, addr,
		int, addr_len)
{
	struct socket *sock;

    // 查找 socket
	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (!sock)
		goto out;

	iov.iov_base = buff;
	iov.iov_len = len;
	msg.msg_name = NULL;
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	msg.msg_control = NULL;
	msg.msg_controllen = 0;
	msg.msg_namelen = 0;
	if (addr) {
		err = move_addr_to_kernel(addr, addr_len, (struct sockaddr *)&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;
    // 发送数据
	err = sock_sendmsg(sock, &msg, len);
}

        在 sendto 系统调用中,主要是把 socket 查出来,然后调用 sock_sendmsg,并在其内部一层层调用封装后的函数,并最终通过 inet_sendmsg 将数据丢到协议栈就完事儿~

2 传输层

        inet_sendms 是 AF_INET 协议族提供的一个通用函数,内部会区分根据不同的 socket 类型,调用其提前注册好的回调函数。

// net\ipv4\af_inet.c
int inet_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
		 size_t size)
{
	struct sock *sk = sock->sk;

	/* We may need to bind the socket. */
	if (!inet_sk(sk)->inet_num && inet_autobind(sk))
		return -EAGAIN;

	return sk->sk_prot->sendmsg(iocb, sk, msg, size);
}

        我们要看的是 tcp 协议,所以这里的 sendmsg 自然就是 tcp 的 tcp_sndmsg 方法啦:

// net\ipv4\tcp.c
int tcp_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
		size_t size)
{
	flags = msg->msg_flags;
	timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);

    // 检查连接状态
	if ((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT))
		if ((err = sk_stream_wait_connect(sk, &timeo)) != 0)
			goto out_err;

    // 获取最大报文段长度
	mss_now = tcp_send_mss(sk, &size_goal, flags);

	// 获取用户传递的数据和 flag
	iovlen = msg->msg_iovlen;
	iov = msg->msg_iov;
	copied = 0;

    // 遍历用户传递的每块数据
	while (--iovlen >= 0) {
		int seglen = iov->iov_len;
		unsigned char __user *from = iov->iov_base;  // 数据块地址

		iov++;

		while (seglen > 0) {
			int copy = 0;
			int max = size_goal;

            // 获取tcp socket 的发送队列
			skb = tcp_write_queue_tail(sk);
			if (tcp_send_head(sk)) {
				if (skb->ip_summed == CHECKSUM_NONE)
					max = mss_now;
				copy = max - skb->len;
			}

            // 构造 skb,涉及数据拷贝
			if (copy <= 0) {
new_segment:
				if (!sk_stream_memory_free(sk))
					goto wait_for_sndbuf;

				skb = sk_stream_alloc_skb(sk,
							  select_size(sk, sg),
							  sk->sk_allocation);
				if (!skb)
					goto wait_for_memory;

                // 待发送 skb 入发送队列
				skb_entail(sk, skb);
			}

			if (skb_tailroom(skb) > 0) {
				if (copy > skb_tailroom(skb))
					copy = skb_tailroom(skb);
                // 把用户空间数据拷贝到内核空间:第一次拷贝
				if ((err = skb_add_data(skb, from, copy)) != 0)
					goto do_fault;
			}
   
            // 检查是否可以发送数据包
            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;

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

			if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
				goto do_error;

			mss_now = tcp_send_mss(sk, &size_goal, flags);
		}
	}
}

        tcp_sendmsg 很长,主要涉及以下几件事情:

  1. 构造 skb,拷贝用户态数据到 skb 中
  2. 将 skb 加入 socket 的发送队列
  3. 判断发送条件是否成立决定是否发送

         tcp_sndmsg中有两个发送方法:

// net\ipv4\tcp.c
void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
			       int nonagle)
{
	/* If we are closed, the bytes will have to remain here.
	 * In time closedown will finish, we empty the write queue and
	 * all will be happy.
	 */
	if (unlikely(sk->sk_state == TCP_CLOSE))
		return;

	if (tcp_write_xmit(sk, cur_mss, nonagle, 0, GFP_ATOMIC))
		tcp_check_probe_timer(sk);
}

// net\ipv4\tcp_output.c
void tcp_push_one(struct sock *sk, unsigned int mss_now)
{
	struct sk_buff *skb = tcp_send_head(sk);

	BUG_ON(!skb || skb->len < mss_now);

	tcp_write_xmit(sk, mss_now, TCP_NAGLE_PUSH, 1, sk->sk_allocation);
}

        可见其还是殊途同归,都调用了 tcp_write_xmit 方法,只不过参数有点差异罢了,普通发送流程默认是启用了 nagle 算法的:

// net\ipv4\tcp_output.c
static int 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);
	struct sk_buff *skb;

    // 依次处理待发送的 skb
	while ((skb = tcp_send_head(sk))) {
		unsigned int limit;

		tso_segs = tcp_init_tso_segs(sk, skb, mss_now);
		BUG_ON(!tso_segs);

        // 测试拥塞窗口是否满足发送条件
		cwnd_quota = tcp_cwnd_test(tp, skb);
		if (!cwnd_quota)
			break;

        // 测试发送窗口是否满足发送条件
		if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))
			break;

		if (tso_segs == 1) {
            // 测试 nagle 算法是否满足发送条件
			if (unlikely(!tcp_nagle_test(tp, skb, mss_now,
						     (tcp_skb_is_last(sk, skb) ?
						      nonagle : TCP_NAGLE_PUSH))))
				break;
		}

        ...

        // 发送
		if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
			break;

		/* Advance the send_head.  This one is sent out.
		 * This call will increment packets_out.
		 */
		tcp_event_new_data_sent(sk, skb);

		tcp_minshall_update(tp, mss_now, skb);
		sent_pkts++;

		if (push_one)
			break;
	}

	if (likely(sent_pkts)) {
		tcp_cwnd_validate(sk);
		return 0;
	}
	return !tp->packets_out && tcp_send_head(sk);
}

        tcp_write_xmit 中处理了 tcp 的拥塞控制、流量控制窗口条件、nagle 算法满足发送要求,就继续调用 tcp_transmit_skb 方法了:

// net\ipv4\tcp_output.c
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
			    gfp_t gfp_mask)
{
    // 克隆 skb: 实际上只克隆了 skb 元数据,即头部(第二次拷贝,仅头部)
    // 数据部分跟原 skb 共享,毕竟内核不会修改用户数据,拷贝它干啥?
	if (likely(clone_it)) {
		if (unlikely(skb_cloned(skb)))
			skb = pskb_copy(skb, gfp_mask);
		else
			skb = skb_clone(skb, gfp_mask);
		if (unlikely(!skb))
			return -ENOBUFS;
	}

	...

	// 设置 tcp 头中各字段
	th = tcp_hdr(skb);
	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->flags);

	if (unlikely(tcb->flags & TCPCB_FLAG_SYN)) {
		/* RFC1323: The window in SYN & SYN/ACK segments
		 * is never scaled.
		 */
		th->window	= htons(min(tp->rcv_wnd, 65535U));
	} else {
		th->window	= htons(tcp_select_window(sk));
	}
	th->check		= 0;
	th->urg_ptr		= 0;

	/* The urg_mode check is necessary during a below snd_una win probe */
	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 = 0xFFFF;
			th->urg = 1;
		}
	}

	tcp_options_write((__be32 *)(th + 1), tp, &opts);
	if (likely((tcb->flags & TCPCB_FLAG_SYN) == 0))
		TCP_ECN_send(sk, skb, tcp_header_size);

	icsk->icsk_af_ops->send_check(sk, skb->len, skb);

	if (likely(tcb->flags & TCPCB_FLAG_ACK))
		tcp_event_ack_sent(sk, tcp_skb_pcount(skb));

	if (skb->len != tcp_header_size)
		tcp_event_data_sent(tp, skb, sk);

	if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq)
		TCP_INC_STATS(sock_net(sk), TCP_MIB_OUTSEGS);

    // 调用网络层发送接口
	err = icsk->icsk_af_ops->queue_xmit(skb, 0);
	if (likely(err <= 0))
		return err;

	tcp_enter_cwr(sk, 1);

    
	return net_xmit_eval(err);
}

        在这各方法中,克隆了一个 skb 出来,为什么需要克隆?因为网络层发送 skb 之后,底层最终会释放掉这个 skb,而 tcp 是可靠连接,在传输层维护了发送队列,如果对端没有响应,是要进行丢包重传的,所以原始 skb,tcp 要自己留着。故而这里是第二次拷贝了,只不过需要注意的是,这里并不涉及用户数据的拷贝,而是 skb 元数据的拷贝,提升了效率。

3 网络层

        数据到了网络层。queue_xmit 也是个回调方法,由网络层注入到传输层的。实际对应的方法是 ip_queue_xmit :

int ip_queue_xmit(struct sk_buff *skb, int ipfragok)
{
	// 检查路由表是否已经有了
	rt = skb_rtable(skb);
	if (rt != NULL)
		goto packet_routed;

	// 没有路由项则进行填充
	rt = (struct rtable *)__sk_dst_check(sk, 0);
	if (rt == NULL) {
		__be32 daddr;

		/* Use correct destination address if we have options. */
		daddr = inet->inet_daddr;
		if(opt && opt->srr)
			daddr = opt->faddr;

		{
            ...
			security_sk_classify_flow(sk, &fl);
			if (ip_route_output_flow(sock_net(sk), &rt, &fl, sk, 0))
				goto no_route;
		}
		sk_setup_caps(sk, &rt->u.dst);
	}
    // 填充路由项
	skb_dst_set(skb, dst_clone(&rt->u.dst));

packet_routed:
	if (opt && opt->is_strictroute && rt->rt_dst != rt->rt_gateway)
		goto no_route;

	// ip 包头设置
	skb_push(skb, sizeof(struct iphdr) + (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->u.dst) && !ipfragok)
		iph->frag_off = htons(IP_DF);
	else
		iph->frag_off = 0;
	iph->ttl      = ip_select_ttl(inet, &rt->u.dst);
	iph->protocol = sk->sk_protocol;
	iph->saddr    = rt->rt_src;
	iph->daddr    = rt->rt_dst;
	/* Transport layer set skb->h.foo itself. */

	if (opt && opt->optlen) {
		iph->ihl += opt->optlen >> 2;
		ip_options_build(skb, opt, inet->inet_daddr, rt, 0);
	}

	ip_select_ident_more(iph, &rt->u.dst, sk,
			     (skb_shinfo(skb)->gso_segs ?: 1) - 1);

	skb->priority = sk->sk_priority;
	skb->mark = sk->sk_mark;

    // 走发送流程
	return ip_local_out(skb);

}

        网络层通过查询路由表,将路由项缓存到 skb 中,这样数据包就知道下一步怎么走了,然后设置完 ip 包头就走到了 ip_local_out:

// net\ipv4\ip_output.c
int ip_local_out(struct sk_buff *skb)
{
	int err;

    // 流经 netfilter 框架的 local_out    
	err = __ip_local_out(skb);
	if (likely(err == 1))
        // 发送数据
		err = dst_output(skb);

	return err;
}

int __ip_local_out(struct sk_buff *skb)
{
	struct iphdr *iph = ip_hdr(skb);

	iph->tot_len = htons(skb->len);
	ip_send_check(iph);
	return nf_hook(PF_INET, NF_INET_LOCAL_OUT, skb, NULL, skb_dst(skb)->dev,
		       dst_output);
}

        经过 netfilter 的 local_out 的蹂躏,如果 skb 幸存了下来,那就会接着通过 dst_output 进行发送了:

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

        dst_output 就是通过 skb 的路由项,找到对应的路由方法 output,这里实际是调用的是 ip_output 了:

// net\ipv4\ip_output.c
int ip_output(struct sk_buff *skb)
{
	struct net_device *dev = skb_dst(skb)->dev;

	IP_UPD_PO_STATS(dev_net(dev), IPSTATS_MIB_OUT, skb->len);

	skb->dev = dev;
	skb->protocol = htons(ETH_P_IP);

	return NF_HOOK_COND(PF_INET, NF_INET_POST_ROUTING, skb, NULL, dev,
			    ip_finish_output,
			    !(IPCB(skb)->flags & IPSKB_REROUTED));
}

        这里设置了 skb 的协议信息,然后又被 netfilter 框架的 post_routing 蹂躏一番,如果 skb 侥幸通过了 post_routing,那么就会走到 ip_finish_output 了:

static int ip_finish_output(struct sk_buff *skb)
{
    // 根据 mtu 判断是否需要分片
	if (skb->len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb))
		return ip_fragment(skb, ip_finish_output2);
	else
		return ip_finish_output2(skb);
}

        ip_finish_output 分两种情况,大于 mtu 的要经过分片再发送,小于 mtu 的可以直接发送。为了简单起见,我们直接看发送流程:

static inline int ip_finish_output2(struct sk_buff *skb)
{
	/* Be paranoid, rather than too clever. */
	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 == NULL) {
			kfree_skb(skb);
			return -ENOMEM;
		}
		if (skb->sk)
			skb_set_owner_w(skb2, skb->sk);
		kfree_skb(skb);
		skb = skb2;
	}

    // 传递给邻居子系统
	if (dst->hh)
		return neigh_hh_output(dst->hh, skb);
	else if (dst->neighbour)
		return dst->neighbour->output(skb);

    ...
}

        ip_finish_output2 中把数据传输到了邻居子系统,实际就是四层协议栈中的网络接口层了。

4 网络接口层

4.1 邻居子系统

        邻居子系统位于数据链路层与网络层直接,在这里主要是查找或者创建邻居项,在创造邻居项的时候,有可能会发出实际的 arp 请求。然后封装一下 MAC 头,将发送过程再传递到更下层的网络设备子系统。

// net\core\neighbour.c
int neigh_resolve_output(struct sk_buff *skb)
{
	struct dst_entry *dst = skb_dst(skb);
	struct neighbour *neigh;

	__skb_pull(skb, skb_network_offset(skb));

    // 没有邻居的 mac 地址的话还需要发送 arp 进行解析
	if (!neigh_event_send(neigh, skb)) {
		int err;
		struct net_device *dev = neigh->dev;
		if (dev->header_ops->cache && !dst->hh) {
			write_lock_bh(&neigh->lock);
			if (!dst->hh)
				neigh_hh_init(neigh, dst, dst->ops->protocol);
            // 把 mac 地址(neigh->ha) 设置套帧头
			err = dev_hard_header(skb, dev, ntohs(skb->protocol),
					      neigh->ha, NULL, skb->len);
			write_unlock_bh(&neigh->lock);
		}
		if (err >= 0)
            // 数据帧交给网络子系统发送
			rc = neigh->ops->queue_xmit(skb);
		else
			goto out_kfree_skb;
	}

}

4.2 网络设备子系统

        邻居子系统已经填好了 skb 头的 mac 地址,接下来就是真正的发送逻辑了:

// net\core\dev.c
int dev_queue_xmit(struct sk_buff *skb)
{
	struct net_device *dev = skb->dev;
	struct netdev_queue *txq;
	struct Qdisc *q;

	...
	// 将 skb 各片段线性化到一个连续缓冲区
	if (skb_needs_linearize(skb, dev) && __skb_linearize(skb))
		goto out_kfree_skb;

	// 如果数据包的部分校验和还未完成,那么在这里完成校验和计算
	if (skb->ip_summed == CHECKSUM_PARTIAL) {
		skb_set_transport_header(skb, skb->csum_start -
					      skb_headroom(skb));
		if (!dev_can_checksum(dev, skb) && skb_checksum_help(skb))
			goto out_kfree_skb;
	}

gso:
    // 选择一个发送队列:RingBuf
	txq = dev_pick_tx(dev, skb);
	q = rcu_dereference_bh(txq->qdisc);

    // 有队列,就继续发送
	if (q->enqueue) {
		rc = __dev_xmit_skb(skb, q, dev, txq);
		goto out;
	}

    // 没有队列说明是回环设备或者隧道设备
	if (dev->flags & IFF_UP) {
		...
	}
}

        由于现代网卡为了提升性能大多支持多队列,所以这里当然要选择一个合适的队列了,选择好之后就通过 __dev_xmit_skb 进行发送了:

// net\core\dev.c
static inline int __dev_xmit_skb(struct sk_buff *skb, struct Qdisc *q,
				 struct net_device *dev,
				 struct netdev_queue *txq)
{
        ...
	    // 排队发送
		rc = qdisc_enqueue_root(skb, q);
		qdisc_run(q);
}

        qdisc_run 是 __qdisc_run 的简单封装:

void __qdisc_run(struct Qdisc *q)
{
	unsigned long start_time = jiffies;

    // 依次从队列中取 skb 进行发送
	while (qdisc_restart(q)) {
		// 如果需要被调度出去,则延迟发送剩余的 skb
		if (need_resched() || jiffies != start_time) {
			__netif_schedule(q);
			break;
		}
	}
}

        在这里,实际的发送还一直都是占用原进程对应的系统态时间,只有当进程需要被调度出去的时候,才会通过软中断将剩余的 skb 发送出去。

 通过 /proc/softirqs 看到的接收软中断要比发送软中断高几个数量级,这里就是第一个原因:大部分数据包的发送占用原进程的系统时间进行发送,只有被调度出去后,剩余的 skb 才会通过发送软中断取发送。

[root@centos ~]# cat /proc/softirqs 
                    CPU0       CPU1       
          HI:          0          1
       TIMER:   87639029   63839412
      NET_TX:          0          0
      NET_RX:    3495365    3180870

// net\sched\sch_generic.c
static inline int qdisc_restart(struct Qdisc *q)
{
	struct netdev_queue *txq;
	struct net_device *dev;
	spinlock_t *root_lock;
	struct sk_buff *skb;

	// skb 出队
	skb = dequeue_skb(q);
	if (unlikely(!skb))
		return 0;

	root_lock = qdisc_lock(q);
	dev = qdisc_dev(q);
	txq = netdev_get_tx_queue(dev, skb_get_queue_mapping(skb));

    // 发送
	return sch_direct_xmit(skb, q, dev, txq, root_lock);
}

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

// net\sched\sch_generic.c
int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,
		    struct net_device *dev, struct netdev_queue *txq,
		    spinlock_t *root_lock)
{
	if (!netif_tx_queue_stopped(txq) && !netif_tx_queue_frozen(txq))
		ret = dev_hard_start_xmit(skb, dev, txq);

	return ret;
}

// net\core\dev.c
int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,
			struct netdev_queue *txq)
{
        ...
		rc = ops->ndo_start_xmit(skb, dev);
		if (rc == NETDEV_TX_OK)
			txq_trans_update(txq);
}

        最终走到网卡驱动注册的回调方法 ndo_start_xmit 进行发送。

4.3 软中断发送剩余的 skb

        前面 __qdisc_run 中在当前进程被调度出去后,发送队列剩余的包怎么处理呢?继续看下看这里的处理流程。__netif_schdule 内部最终调用了 __netif_reschedule:

// net\core\dev.c
static inline void __netif_reschedule(struct Qdisc *q)
{
	struct softnet_data *sd;
	unsigned long flags;

	local_irq_save(flags);
	sd = &__get_cpu_var(softnet_data);
	q->next_sched = sd->output_queue;
	sd->output_queue = q;
	raise_softirq_irqoff(NET_TX_SOFTIRQ);
	local_irq_restore(flags);
}

        可以看到,在 __netif_reschedule 中,触发了一个 NET_TX_SOFTIRQ 软中断,即剩下的包要走软中断发送了。NET_TX_SOFTIRQ 软中断对应的中端处理函数是 net_tx_action:

static void net_tx_action(struct softirq_action *h)
{
    // 获取每 cpu 上的发送队列
	struct softnet_data *sd = &__get_cpu_var(softnet_data);

	...

    // 如果有 output_queue,说明不是回环或隧道设备
	if (sd->output_queue) {
		struct Qdisc *head;

		local_irq_disable();
		head = sd->output_queue;
		sd->output_queue = NULL;
		local_irq_enable();

        // 遍历 qdiscs 列表
		while (head) {
			struct Qdisc *q = head;
			spinlock_t *root_lock;

			head = head->next_sched;

			root_lock = qdisc_lock(q);
			if (spin_trylock(root_lock)) {
				smp_mb__before_clear_bit();
				clear_bit(__QDISC_STATE_SCHED,
					  &q->state);
                // 一样是调用 qdisc_run 进行发送
				qdisc_run(q);
				spin_unlock(root_lock);
			} else {
				if (!test_bit(__QDISC_STATE_DEACTIVATED,
					      &q->state)) {
					__netif_reschedule(q);
				} else {
					smp_mb__before_clear_bit();
					clear_bit(__QDISC_STATE_SCHED,
						  &q->state);
				}
			}
		}
	}
}

        发送软中断这里首先会获取 softnet_data,跟我们在接收软中断中看到的操作类似。随后又是调用 qdisc_run 进行数据发送,这一点有跟在进程内核态中发送数据的流程一样了。

4.4  硬中断又触发软中断

        网卡具体的发送处理流程就不一一细看了,不是我们关注的重点。重点看下发送完成后是怎么清理的?

// net\core\dev.c
void __napi_schedule(struct napi_struct *n)
{
	unsigned long flags;

	local_irq_save(flags);
	list_add_tail(&n->poll_list, &__get_cpu_var(softnet_data).poll_list);
	__raise_softirq_irqoff(NET_RX_SOFTIRQ);
	local_irq_restore(flags);
}

        发送完成后,网卡发出了硬中断,硬中断中触发软中断就只有这一个流程:还是通过 __napi_schedule 来触发。这就有意思了,发送完成后触发清理的软中断仍然是 NET_RX_SOFTIRQ,这是 bug 吗?进入软中断的回调函数 igb_poll 看一下:

static int igb_poll(struct napi_struct *napi, int budget)
{
    // 清理发送队列 RingBuf
	if (q_vector->tx_ring)
		tx_clean_complete = igb_clean_tx_irq(q_vector);

    // 接收处理
	if (q_vector->rx_ring)
		igb_clean_rx_irq_adv(q_vector, &work_done, budget);
}

        没毛病,确实是在接收软中断中清理了发送队列。

还记得前面那个问题吗?通过 /proc/softirqs 看到的接收软中断要比发送软中断高几个数量级,这里就是第二个原因:发送完成的清理操作发的的接收软中断,而不是发送软中断!

总结

        看了这么多,有必要总结下形成更好的记忆。发送一个数据包的过程中,干了这么几件事:

  1. 用户态调用 send 方法发送数据
  2. 内核态调用系统调用,sys_sendto 中将 skb 给到 AF_INET 协议族
  3. 协议族发包方法中根据 socket 类型调用对应的处理方法:对于 tcp 则是 tcp_sndmsg
  4. tcp_sndmsg 中构造好 skb(拷贝数据),将 skb 加入 socket 发送队列,判断满足发送条件就进行发送
  5. 对于需要发送的包,还要进一步检查拥塞窗口、流控窗口、nagle 算法是否满足条件,满足条件的才进一步发送给网络层
  6. 网络层对 skb 进行克隆(拷贝 skb 头),查询路由表,填充 ip 包头,通过 ip_local_out 发送,这里需要经过 netfilter 框架 local_out 点
  7. 接着还是网络层,查包头中的路由项,找到对应的路由方法 output,在这里需要经过 netfilter 框架的 post_routing 点
  8. 接着判断 skb 是否需要分片(如果需要分片,会涉及到用户数据的拷贝)
  9. 随后数据被交给邻居子系统,填充 mac 地址,这里可能需要发送 arp 协议
  10. 接着数据到达网络设备子系统,选择合适的发送队列进行数据的发送操作
  11. 大部分的数据包在进程内核态被发送完,占用进程内核态时间。当进程需要被调度出去,触发发送软中断 NET_TX_SOFTIRQ
  12. 发送软中断中会对剩余的 skb 接着进行发送
  13. 网卡发送完成,发出硬中断,硬中断触发接收软中断 NET_RX_SOFTIRQ
  14. 在接收软中断中,完成对 RingBuf 中已发送数据的清理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1560022.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java_21 完成一半题目

完成一半题目 有 N 位扣友参加了微软与力扣举办了「以扣会友」线下活动。主办方提供了 2*N 道题目&#xff0c;整型数组 questions 中每个数字对应了每道题目所涉及的知识点类型。 若每位扣友选择不同的一题&#xff0c;请返回被选的 N 道题目至少包含多少种知识点类型。 示例…

Acrobat Pro DC 2023 for Mac PDF编辑管理软件

Acrobat Pro DC 2023 for Mac是一款功能强大的PDF编辑和管理软件&#xff0c;旨在帮助用户轻松处理PDF文件。它提供了丰富的工具和功能&#xff0c;使用户可以创建、编辑、转换和注释PDF文件&#xff0c;以及填写和签署PDF表单。 软件下载&#xff1a;Acrobat Pro DC 2023 for …

机器学习全攻略:概念、流程、分类与行业应用案例集锦

目录 1.引言 2.从零开始认识机器学习&#xff1a;基本概念与重要术语 3.五步走&#xff1a;掌握机器学习项目执行的完整流程 3.1.问题定义与数据收集 3.2.数据预处理与特征工程 3.3.模型选择与训练 3.4.模型评估与优化 3.5.模型部署与监控 4.深入了解各类机器学习方法…

Monkey 和 TextMonkey ---- 论文阅读

文章目录 Monkey贡献方法增强输入分辨率多级描述生成多任务训练 实验局限结论 TextMonkey贡献方法移位窗口注意&#xff08;Shifted Window Attention&#xff09;图像重采样器&#xff08;Image Resampler&#xff09;Token Resampler位置相关任务&#xff08;Position-Relate…

云计算探索-如何在服务器上配置RAID(附模拟器)

一&#xff0c;引言 RAID&#xff08;Redundant Array of Independent Disks&#xff09;是一种将多个物理硬盘组合成一个逻辑单元的技术&#xff0c;旨在提升数据存取速度、增大存储容量以及提高数据可靠性。在服务器环境中配置RAID尤其重要&#xff0c;它不仅能够应对高并发访…

实景三维技术:开启自然资源管理的新篇章

随着科技的不断进步&#xff0c;实景三维技术已经在多个领域得到了广泛的应用。而在自然资源管理领域&#xff0c;实景三维技术更是发挥着越来越重要的作用。本文将介绍实景三维在自然资源管理领域的应用&#xff0c;探讨其带来的优势和变革。一、什么是实景三维技术&#xff1…

MHA高可用-解决MySQL主从复制的单点问题

目录 一、MHA的介绍 1&#xff0e;什么是 MHA 2&#xff0e;MHA 的组成 2.1 MHA Node&#xff08;数据节点&#xff09; 2.2 MHA Manager&#xff08;管理节点&#xff09; 3&#xff0e;MHA 的特点 4. MHA工作原理总结如下&#xff1a; 二、搭建 MySQL MHA 实验环境 …

文献阅读:使用 CellChat 推理和分析细胞-细胞通信

文献介绍 「文献题目」 Inference and analysis of cell-cell communication using CellChat 「研究团队」 聂青&#xff08;加利福尼亚大学欧文分校&#xff09; 「发表时间」 2021-02-17 「发表期刊」 Nature Communications 「影响因子」 16.6 「DOI」 10.1038/s41467-0…

DevSecOps安全工具链介绍

目录 一、概述 二、安全工具链在平台中的定位 2.1 概述 2.2 分层定位 2.2.1 不同阶段的安全工具 2.2.2 安全工具金字塔 2.3 安全流水线集成概览 2.3.1 概述 2.3.2 标准流水线集成安全工具链概览图 三、安全工具链分类 3.1 概述 3.2 威胁建模类 3.2.1 威胁建模的概念…

47 vue 常见的几种模型视图不同步的问题

前言 这里主要是来看一下 关于 vue 中的一些场景下面 可能会出现 模型和视图 不同步更新的情况 然后 这种情况主要是 vue 中的对象 属性没有响应式的 setter, getter 然后 我们这里就来看一下 大多数的情况下的一个场景, 和一些处理方式 当然 处理方式主要是基于 Vue.set, …

53 v-bind 和 v-model 的实现和区别

前言 这个主要的来源是 偶尔的情况下 出现的问题 就比如是 el-select 中选择组件之后, 视图不回显, 然后 model 不更新等等 这个 其实就是 vue 中 视图 -> 模型 的数据同步, 我们通常意义上的处理一般是通过 模型 -> 数据 的数据同步, 比如 我们代码里面更新了 model.…

正多边形拓扑与泛函

&#xff08;原创&#xff1a;Daode3056&#xff09; 也许&#xff0c;关于“拓扑”&#xff0c;“泛函”几本书上的内容与实例都是大同小异&#xff0c;总是那么点内容&#xff0c;数学要开拓一些新领域与新内容才能满足不断发展的社会与工业各种需要。本文就以人工智能生成对…

鸿蒙OS开发实例:【ArkTS 实现MQTT协议】

介绍 MQTT是物联网中的一种协议&#xff0c;在HarmonyOS API9平台&#xff0c;解决方案以C库移植为实现方案。 遥遥领先的平台&#xff0c;使用MQTT怎能不遥遥领先呢&#xff01; 新年快乐&#xff0c;本篇将带领你手把手实现HarmonyOS ArkTS语言的MQTT协议。 准备 阅读…

阿里云通用算力型u1云服务器配置性能评测及价格参考

阿里云服务器u1是通用算力型云服务器&#xff0c;CPU采用2.5 GHz主频的Intel(R) Xeon(R) Platinum处理器&#xff0c;ECS通用算力型u1云服务器不适用于游戏和高频交易等需要极致性能的应用场景及对业务性能一致性有强诉求的应用场景(比如业务HA场景主备机需要性能一致)&#xf…

如何使用极狐GitLab 自定义 Pages 根域名

本文作者&#xff1a;徐晓伟 GitLab 是一个全球知名的一体化 DevOps 平台&#xff0c;很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门为中国程序员服务。可以一键式部署极狐GitLab。 本文主要讲述了极狐GitLab Pages …

JS实现正则匹配文本中的URL地址

如何利用JS正则表达式&#xff0c;提取文本中的URL地址呢&#xff1f; 目录 一、程序代码 二、运行结果 一、程序代码 function extractUrls(text) {var urlRegex /(https?:\/\/[^\s])/g;return text.match(urlRegex); }var text "3.02 复制打开抖音&#xff0c;看…

YOLOV8逐步分解(3)_trainer训练之模型加载

yolov8逐步分解(1)--默认参数&超参配置文件加载 yolov8逐步分解(2)_DetectionTrainer类初始化过程 接上2篇文章&#xff0c;继续讲解yolov8训练过程中的模型加载过程。 使用默认参数完成训练器trainer的初始化后&#xff0c;执行训练函数train()开始YOLOV8的训练。 1. t…

SaaS 电商设计 (十) 记一次 5000kw 商品数据ES迁移 (详细的集群搭建以及线上灰度过程设计)

目录 一.背景二.技术目标三.技术方案3.1 整体流程3.2 ES 切换前:完成整体新集群的搭建.i:拓扑结构设计ii: 如何选择整体的 **ES** 集群配置. 3.3 **ES** 版本切换中3.3.1 多client版本兼容3.3.2 Router的设计 3.4 ES 切换后3.5 开箱即用 四.总结 专栏系列 -SaaS 电商设计 (一) …

每日面经分享(pytest装饰器)

pytest装饰器 a. pytest.mark.parametrize&#xff1a;这个装饰器用于标记测试函数&#xff0c;并为其提供多组参数进行参数化测试。可以使用元组、列表、字典等形式来指定参数组合。 import pytestpytest.mark.parametrize("num1, num2, expected", [(2, 2, 4), (5…

2024 3.23~3.29周报

上周工作 SVInvNet论文研读 本周计划 加入DenseNet&#xff0c;修改网络架构&#xff0c;跑代码 总结 DenseNet 密集块&#xff1a;DenseNet将网络分成多个密集块&#xff08;Dense Block)。在每个密集块内&#xff0c;每一层都连接到前面所有的层。这种跳跃连接有助于解…