目录
摘要
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 很长,主要涉及以下几件事情:
- 构造 skb,拷贝用户态数据到 skb 中
- 将 skb 加入 socket 的发送队列
- 判断发送条件是否成立决定是否发送
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 看到的接收软中断要比发送软中断高几个数量级,这里就是第二个原因:发送完成的清理操作发的的接收软中断,而不是发送软中断!
总结
看了这么多,有必要总结下形成更好的记忆。发送一个数据包的过程中,干了这么几件事:
- 用户态调用 send 方法发送数据
- 内核态调用系统调用,sys_sendto 中将 skb 给到 AF_INET 协议族
- 协议族发包方法中根据 socket 类型调用对应的处理方法:对于 tcp 则是 tcp_sndmsg
- tcp_sndmsg 中构造好 skb(拷贝数据),将 skb 加入 socket 发送队列,判断满足发送条件就进行发送
- 对于需要发送的包,还要进一步检查拥塞窗口、流控窗口、nagle 算法是否满足条件,满足条件的才进一步发送给网络层
- 网络层对 skb 进行克隆(拷贝 skb 头),查询路由表,填充 ip 包头,通过 ip_local_out 发送,这里需要经过 netfilter 框架 local_out 点
- 接着还是网络层,查包头中的路由项,找到对应的路由方法 output,在这里需要经过 netfilter 框架的 post_routing 点
- 接着判断 skb 是否需要分片(如果需要分片,会涉及到用户数据的拷贝)
- 随后数据被交给邻居子系统,填充 mac 地址,这里可能需要发送 arp 协议
- 接着数据到达网络设备子系统,选择合适的发送队列进行数据的发送操作
- 大部分的数据包在进程内核态被发送完,占用进程内核态时间。当进程需要被调度出去,触发发送软中断 NET_TX_SOFTIRQ
- 发送软中断中会对剩余的 skb 接着进行发送
- 网卡发送完成,发出硬中断,硬中断触发接收软中断 NET_RX_SOFTIRQ
- 在接收软中断中,完成对 RingBuf 中已发送数据的清理