
socket系统调用通过陷入内核态交由socket子系统处理,send/sendto/write触发协议栈下行;TCP走tcp_sendmsg()、UDP走udp_sendmsg(),经路由查找、ARP解析后生成sk_buff,经qdisc、驱动ndo_start_xmit提交至网卡DMA发送。
应用层调用 socket()、bind()、connect() 或 sendto() 时,并不直接操作硬件,而是陷入内核态,由内核的 socket 子系统接管。关键点在于:这些调用本身不发包,只是准备数据结构和状态;真正触发网络栈下行的是 send() / sendto() / write() 这类写操作。
常见误区是认为 connect() 就发 SYN 包——其实它只是设置 socket 状态为 CONNECTING,真正发包发生在第一次 send()(阻塞模式)或内核在后续软中断中调度发送队列(非阻塞 + EPOLLOUT 场景)。
AF_INET + SOCK_STREAM 对应 TCP 协议栈入口,走 inet_stream_ops → tcp_sendmsg()
udp_sendmsg(),不建连接,路径更短,但依然要经过路由查找(ip_route_output_flow())和邻居子系统(ARP)send() 可能被阻塞在 ARP 请求完成前(尤其在 SOCK_DGRAM 首次发包时)内核通过 fib_lookup() 查路由表(FIB),结果不是“目标 IP”,而是 struct rtable 或 struct fib_result,其中包含:
oif:出接口索引,对应具体网卡(如 eth0 的 dev->ifindex)gateway:若非直连,
dst:最终封装用的目标 IP(可能与原始 send 目标不同,比如经策略路由或 NAT 后)拿到出接口和下一跳 IP 后,进入邻居子系统(neighbour subsystem):如果目标是直连网段,查 arp_table 获取 MAC;如果是网关,则查该网关 IP 对应的 MAC。查不到就发 ARP 请求并临时挂起 sk_buff 在 skb->dst->neighbour->arp_queue 上,等响应回来再重发。
注意:ip route get 可验证实际选路结果,而 ip neigh show 能看到当前 ARP 缓存——很多“ping 通但应用连不上”的问题,根源是 ARP 表老化或被防火墙丢弃了请求。
数据包经 TCP/UDP/IP 封装后,变成一个 sk_buff 结构体,最终调用 dev_queue_xmit() 进入设备层。这里的关键跳转是:
dev_queue_xmit() → 检查 dev->flags & IFF_UP → 进入 qdisc(如 pfifo_fast)→ __qdisc_run() → sch_direct_xmit() → dev_hard_start_xmit() → 网卡驱动的 ndo_start_xmit 回调
skb_is_gso() 为真),由网卡硬件完成分片,此时 sk_buff 携带的是大包 + gso_size
tcp_tso_segment() 提前分片,生成多个小 sk_buff
dev->xmit_lock 是 per-CPU 锁,高并发下锁竞争可能成为瓶颈,可通过 ethtool -L eth0 combined N 调整队列数缓解驱动的 ndo_start_xmit 实现因芯片而异,但通用流程是:将 sk_buff 数据地址和长度写入网卡 DMA 描述符环(descriptor ring),触发 tx_doorbell 告知硬件取包。此时 CPU 不等待发送完成,而是继续处理其他任务。
真正发出信号靠网卡硬件:它读取描述符,用 DMA 把数据搬进自己的 FIFO,按以太网帧格式(含 preamble、SFD、DA/SA、type、FCS)串行输出到 PHY 层,PHY 再转成电信号(RJ45)或光信号(SFP)。
容易忽略的点:
/proc/net/dev 中的 tx_dropped 不代表线缆没信号,可能是驱动 ring 满(tx_fifo_errors)、DMA 映射失败(tx_aborted_errors)或校验错误ethtool -S eth0 可查看芯片级计数器,比如 tx_packets(驱动提交数) vs tx_unicast(PHY 实际发出单播帧数),差值过大说明链路层丢包CONFIG_NET_RX_BUSY_POLL 或 XDP 程序时,部分路径会绕过传统 softirq,需确认是否影响你观察的统计点整个流程里,最易被当成“黑盒”而掩盖真实瓶颈的,其实是邻居子系统和 qdisc 队列——它们不报错,但会让包在内存里滞留几十毫秒,且不会出现在 tcpdump 中。