工业设计专业怎么样,南宁网站seo推广优化公司,做网站的公司怎么赚钱吗,企业设计网站系统目录 摘要
1 从 send 开始
2 传输层
3 网络层
4 网络接口层
4.1 邻居子系统
4.2 网络设备子系统
4.3 软中断发送剩余的 skb
4.4 硬中断又触发软中断
总结 摘要 一个网络包的发送#xff0c;始于应用层#xff0c;经层层协议栈的封装#xff0c;终于网卡。今天来循…目录 摘要
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 是可靠连接在传输层维护了发送队列如果对端没有响应是要进行丢包重传的所以原始 skbtcp 要自己留着。故而这里是第二次拷贝了只不过需要注意的是这里并不涉及用户数据的拷贝而是 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 才会通过发送软中断取发送。 [rootcentos ~]# 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_sndmsgtcp_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 中已发送数据的清理