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;// 查找 socketsock = 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);// 获取用户传递的数据和 flagiovlen = 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;// 依次处理待发送的 skbwhile ((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);elseskb = 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);elseiph->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);elsereturn 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);elsegoto 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:// 选择一个发送队列:RingBuftxq = 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)) {// 如果需要被调度出去,则延迟发送剩余的 skbif (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)
{// 清理发送队列 RingBufif (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.mzph.cn/news/792045.shtml

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

相关文章

ubuntu18.04图形界面卡死,鼠标键盘失灵, 通过MAC共享网络给Ubuntu解决!

ubuntu18.04图形界面卡死&#xff0c;鼠标键盘失灵&#xff0c; 通过MAC共享网络给Ubuntu解决&#xff01; 1. 尝试从卡死的图形界面切换到命令行界面2. 进入bios和grub页面3. 更改Grub中的设置&#xff0c;以进入命令行4. 在命令行页面解决图形界面卡死的问题5. Mac共享WI-FI网…

【MySQL】数据库的基本操作

目录 一、数据库的库操作 二、数据库的表操作 一、数据库的库操作 数据库的创建 create database (if not exists) 库名 这里的if not exists 是一个判断用的&#xff0c;如果数据库存在&#xff0c;就不执行语句&#xff0c;如果数据库不存在&#xff0c;则执行该语句。 创建…

vulhub中Apache Solr Velocity 注入远程命令执行漏洞复现 (CVE-2019-17558)

Apache Solr 是一个开源的搜索服务器。 在其 5.0.0 到 8.3.1版本中&#xff0c;用户可以注入自定义模板&#xff0c;通过Velocity模板语言执行任意命令。 访问http://your-ip:8983即可查看到一个无需权限的Apache Solr服务。 1.默认情况下params.resource.loader.enabled配置…

C++实现vector

目录 前言 1.成员变量 2.成员函数 2.1构造函数 2.2析构函数 2.3begin,end 2.4获取size和capacity 2.5函数重载【】 2.6扩容reserve 2.7resize 2.8insert 2.9删除 2.10尾插、尾删 3.0拷贝构造函数 3.1赋值运算符重载 前言 自主实现C中vector大部分的功能可以使我们更好的理解并使…

红黑树介绍与模拟实现(insert+颜色调整精美图示超详解哦)

红黑树 引言红黑树的介绍实现结点类insert搜索插入位置插入调整当parent为gparent的左子结点当parent为gparent的右子结点 参考源码测试红黑树是否合格总结 引言 在上一篇文章中我们认识了高度平衡的平衡二叉树AVL树&#xff1a;戳我看AVL树详解哦 &#xff08;关于旋转调整的…

Java 7、Java 8常用新特性

目录 Java 8 常用新特性1、Lambda 表达式2、方法引用2.1 静态方法引用2.2 特定对象的实例方法引用2.3 特定类型的任意对象的实例方法引用2.4 构造器引用 3、接口中的默认方法4、函数式接口4.1 自定义函数式接口4.2 内置函数式接口 5、Date/Time API6、Optional 容器类型7、Stre…

(四) 序列化器类使用整理

从一、序列化器类中&#xff0c;或 视图集源码 中&#xff0c; 可以得知&#xff1a; 序列化器类可以接收一个instance &#xff0c;和一个data serializer_obj XxxxSerializer(instance,datarequest.data) &#xff08;更新时&#xff0c;instance相当于原…

云原生技术精选:探索腾讯云容器与函数计算的最佳实践

文章目录 写在前面《2023腾讯云容器和函数计算技术实践精选集》深度解读案例集特色&#xff1a;腾讯云的创新实践与技术突破精选案例分析——Stable Diffusion云原生部署的最佳实践精选集实用建议分享总结 写在前面 在数字化转型的浪潮下&#xff0c;云计算技术已成为企业运营…

Kafka入门到实战-第五弹

Kafka入门到实战 Kafka常见操作官网地址Kafka概述Kafka的基础操作更新计划 Kafka常见操作 官网地址 声明: 由于操作系统, 版本更新等原因, 文章所列内容不一定100%复现, 还要以官方信息为准 https://kafka.apache.org/Kafka概述 Apache Kafka 是一个开源的分布式事件流平台&…

基于springboot+vue实现的酒店客房管理系统

作者主页&#xff1a;Java码库 主营内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。 收藏点赞不迷路 关注作者有好处 文末获取源码 技术选型 【后端】&#xff1a;Java 【框架】&#xff1a;spring…

昇腾训练执行与推理部署系列 入门: 1.开启异腾AI之旅

一、1认识CANN 1、昇腾AI基础软硬件平台介绍2、CANN逻辑架构介绍 1、昇腾AI基础软硬件平台介绍 2、CANN逻辑架构介绍

普联一面4.2面试记录

普联一面4.2面试记录 文章目录 普联一面4.2面试记录1.jdk和jre的区别2.java的容器有哪些3.list set map的区别4.get和post的区别5.哪个更安全6.java哪些集合类是线程安全的7.创建线程有哪几种方式8.线程的状态有哪几种9.线程的run和start的区别10.什么是java序列化11.redis的优…

商品购买过程中,库存的抵扣过程是怎样的?如何防止超卖?

在商品购买的过程中&#xff0c;库存的抵扣过程&#xff0c;一般操作如下&#xff1a; 1、select根据商品id查询商品的库存。 2、根据下单的数量&#xff0c;计算库存是否足够&#xff0c;如果存库不足则抛出库存不足的异常&#xff0c;如果库存足够&#xff0c;则减去扣除的…

mysql+keepalive+lvs搭建的数据库集群实验

前提条件&#xff1a;准备5台计算机&#xff0c;且网络互通 1、客户端 yum groups -y install mariadb-client ip 192.168.0.5 2、lvs1 yum-y install ipvsadm keepalived ip 192.168.0.1 keepalivedvip 192.168.0.215 /etc/hosts 解析192.168.0.1 主机名 3、lvs2 yum-y i…

前视声呐目标识别定位(五)-代码解析之修改声呐参数

前视声呐目标识别定位&#xff08;一&#xff09;-基础知识 前视声呐目标识别定位&#xff08;二&#xff09;-目标识别定位模块 前视声呐目标识别定位&#xff08;三&#xff09;-部署至机器人 前视声呐目标识别定位&#xff08;四&#xff09;-代码解析之启动识别模块 …

DHT11温湿度传感器使用视频教程分享

下载地址&#xff1a; 温湿度计(STCDHT11): https://url83.ctfile.com/d/45573183-60623983-9b7f6c?p7526 (访问密码: 7526)

Java常用类和基础API

文章目录 1. 字符串相关类之不可变字符序列&#xff1a;String1.1 String的特性1.2 String的内存结构1.2.1 概述1.2.2 练习类型1&#xff1a;拼接1.2.3 练习类型2&#xff1a;new1.2.4 练习类型3&#xff1a;intern() 1.3 String的常用API-11.3.1 构造器1.3.2 字符串对象的比较…

解决酷狗官网无法播放音乐问题

播放歌曲页面无法播放歌曲&#xff0c;有CORS错误&#xff0c;困扰我很久了。 谷歌浏览器优雅的关闭cors跨域_浏览器关闭跨域-CSDN博客 "C:\Program Files\CatsxpSoftware\Catsxp-Browser\Application\catsxp.exe" --disable-web-security --user-data-dirE:\catsxp…

【学习笔记】java项目—苍穹外卖day10

文章目录 苍穹外卖-day10课程内容1. Spring Task1.1 介绍1.2 cron表达式1.3 入门案例1.3.1 Spring Task使用步骤1.3.2 代码开发1.3.3 功能测试 2.订单状态定时处理2.1 需求分析2.2 代码开发2.3 功能测试 3. WebSocket3.1 介绍3.2 入门案例3.2.1 案例分析3.2.2 代码开发3.2.3 功…

Fusion360修改嘉立创EDA专业版生成的3D外壳文件

需要第三方软件的原因 嘉立创EDA专业版生成电路板的3D外壳文件是比较快捷的&#xff0c;但如果侧面精密开孔或者添加其它非常规的元素还是有些局限。嘉立创EDA专业版可以把3D外壳文件导出&#xff0c;这就大大方便了第三方软件的修改。 本文是利用Fusion360修改3D外壳文件&…