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

目录

摘要

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 中已发送数据的清理

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C语言】贪吃蛇【附源码】

欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 一、游戏说明&#xff1a; 一个基于C语言链表开发的贪吃蛇游戏&#xff1a; 1. 按方向键上下左右&#xff0c;可以实现蛇移动方向的改变。 2. 短时间长按方向键上下左右其中之一&#xff0c;可实现蛇向该方向的短时间…

AndroidStudio出现类似 Could not create task ‘:app:ToolOperatorDemo.main()‘. 错误

先看我们的报错 翻译过来大概意思是:无法创建任务:app:ToolOperatorDemo.main()。 没有找到名称为“main”的源集。 解决方法&#xff1a; 在.idea文件夹下的gradle.xml文件中 <GradleProjectSettings>标签下添加<option name"delegatedBuild" value"f…

指针强化练习(详解)

更多学习内容 结构体内存对齐 和 位段-CSDN博客指针初级&#xff08;基础知识&#xff09;-CSDN博客指针进阶(深入理解)-CSDN博客 目录 1.sizeof与strlen的区别 2.一维数组 3.字符指针 4.二维数组 5.指针运算(笔试题) 6.函数指针 1.sizeof与strlen的区别 请思考以下运行结…

IntelliJ IDEA 2023 for Mac 好用的Java开发工具

IntelliJ IDEA 2023是一款由JetBrains开发的强大的集成开发环境&#xff08;IDE&#xff09;软件&#xff0c;适用于多个编程语言。它旨在提高开发人员的生产力和代码质量&#xff0c;具有以下多种特色功能&#xff1a; 软件下载&#xff1a;IntelliJ IDEA 2023 v2023.3.6中文激…

mixly编译esp32程序缺失问题bits/c++config.h

错误信息&#xff1a; 编译中... 使用平台的 ‘esp32’ 开发板&#xff0c;在列出的文件夹中&#xff1a;D:\windowsoftInstalled\mixly\mixly2.0-win32-x64\arduino-cli\Arduino15\packages\esp32\hardware\esp32\2.0.7 使用平台的 ‘esp32’ 代码&#xff0c;在列出的文件夹…

【MySQL】DQL-排序查询-语法&排序方式&注意事项&可cv例题语句

前言 大家好吖&#xff0c;欢迎来到 YY 滴MySQL系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C Linux的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的…

使用python实现i茅台自动预约

使用python实现i茅台自动预约[仅限于学习,不可商用] 运行: 直接运行 imtApi.py 打包:切换到imt脚本目录,执行打包命令: pyinstaller --onefile imtApi.py这个应用程序可以帮助你进行茅台自动化配置。以下是一些使用说明: 平台注册账号(可用i茅台)不用登录,你可以进行…

【数据分析面试】5.地址信息数据清洗合并 (Python)

题目 有两个数据框。一个包含有关地址的信息&#xff0c;另一个包含了不同城市和州之间的关系。 示例&#xff1a; df_addresses address4860 Sunset Boulevard, San Francisco, 941053055 Paradise Lane, Salt Lake City, 84103682 Main Street, Detroit, 482049001 Casca…

hackmyvm———Quick4之tar命令注入

目录 1.信息收集 2.目录扫描 3.文件上传 ​4.提权 简介&#xff1a;HackMyVm是一个集合了大量靶机的平台&#xff0c;通过下载压缩包后导入Vm虚拟机来使用&#xff0c;推荐使用VirtualBox来导入加载漏洞虚拟机。类似于Vulnhub、HackTheBox&#xff0c;但是相比于Vulnhub而言…

利用TensorRT在jetson orin 上加速YOLOv5

一、第一种方法&#xff0c;需要下载各种包&#xff1a; 要用到一个大佬的开源&#xff0c;GitHub地址如下&#xff1a; https://github.com/wang-xinyu/tensorrtx/tree/master/yolov51. 安装pycuda&#xff0c;在线安装pycuda pip3 install pycuda 或者&#xff1a; sudo…

数据结构之单链表实现(JAVA语言+C语言)

一、理论 1 单链表结构 2 增、删、查 、改思路 &#xff08;增&#xff09;直接添加放到最后即可。按顺序添加&#xff1a;找到要修改的节点的前一个节点&#xff0c;插入新节点&#xff08;&#xff09;。&#xff08;改&#xff09;要修改的节点修改内容即可。&#xff08;…

STM32+ MAX30102通过指尖测量心率+血氧饱和度

一、前言 重要的事情放在最前面&#xff1a;max30102只适用于指尖手指测量&#xff0c;不适用与手腕手指测量&#xff0c;如需做成可穿戴样式选择传感器的小伙伴请pass掉他&#xff0c;因为他只有红光和红外2种光&#xff0c;不够充足的数据源去运算。 由于一些原因&#xff0c…

阿基米德分牛问题及其Python求解

文章目录 题目大意sympy求解结果 题目大意 问 太阳神有一牛群&#xff0c;由白、黑、花、棕四种颜色的公、母牛组成&#xff0c;其间关系如下&#xff0c;求每种牛的个数。 公牛中&#xff0c;白牛多于棕牛&#xff0c;二者之差为黑牛的 1 2 1 3 \frac{1}{2}\frac{1}{3} 21​…

排序——交换排序(冒泡排序与快速排序)

本专栏和大家分享关于排序的算法,其中有插入排&#xff08;直接插入排序和希尔排序&#xff09;、选择排序&#xff08;直接选择排序和堆排&#xff09;、交换排序&#xff08;冒泡排序和快速排序&#xff09;、归并排序以及其他非基于比较的排序 本文与大家分享交换排序 目录 …

SAP FICO 银企直联

银企直联是指企业通过互联网或专线连接的方式&#xff0c;使企业的SAP系统与商业银行的业务系统通过特定的数据接口实现连接&#xff0c;在SAP系统中可以直接查询银行账户的余额和明细&#xff0c;实现付款、银企对账、自动出具余额调节表等功能。 在这主要介绍SAP相关CALLSS配…

C++—vector的介绍及使用 vector的模拟实现

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 文章目录 前言 一、vector的介绍及使用 1.1 vector的介绍 1.2 vector的使用 1.2.1 vector的定义 1.2.2 vector iterator 的使用 1.2.3 vector 空间增长问题 1.2.4 vecto…

Python绘制线图之plt.plot()的介绍以及使用

在Python中plt.plot是matplotlib库中的一个函数,用于绘制点和线,并对其样式进行控制,下面这篇文章主要给大家介绍了关于Python绘制线图之plt.plot()的介绍以及使用的相关资料,需要的朋友可以参考下 plt.plot() 是Matplotlib库中用于绘制线图&#xff08;折线图&#xff09;的主…

【递归】有序分数(SBT)

给定一个整数 N&#xff0c;请你求出所有分母小于或等于 N&#xff0c;大小在 [0,1][0,1] 范围内的最简分数&#xff0c;并按从小到大顺序依次输出。 例如&#xff0c;当 N5时&#xff0c;所有满足条件的分数按顺序依次为&#xff1a; 0/1,1/5,1/4,1/3,2/5,1/2,3/5,2/3,3/4,4…

Python | Leetcode Python题解之第1题两数之和

题目&#xff1a; 题解&#xff1a; class Solution:def twoSum(self, nums: List[int], target: int) -> List[int]:n len(nums)for i in range(n):for j in range(i 1, n):if nums[i] nums[j] target:return [i, j]return []

GT收发器PHY层设计(4)上板验证

文章目录 前言一、实验平台二、通道1收发数据三、通道2收发数据 前言 在前面三篇内容当中详细介绍了基于GT的PHY设计&#xff0c;本篇内容进行上板测试&#xff0c;主要查看接收数据是否能正确对齐 一、实验平台 俩个光口相互通信&#xff0c;即1发2收&#xff0c;2发1收 发…