TCP 的 send 函数 tcp_sendmsg/tcp_sendpage,要调用 lock_sock(sk)。
TCP 的 recv 函数 tcp_recvmsg,也要调用 lock_sock(sk):
void lock_sock_nested(struct sock *sk, int subclass) { might_sleep();
spin_lock_bh(&sk->sk_lock.slock); if (sk->sk_lock.owned) __lock_sock(sk);
sk->sk_lock.owned = 1; spin_unlock(&sk->sk_lock.slock); /* * The sk_lock has
mutex_lock() semantics here: */ mutex_acquire(&sk->sk_lock.dep_map, subclass,
0, _RET_IP_); local_bh_enable(); }
__lock_sock 的核心即睡眠等锁。
这意味着 TCP socket 的 send 和 recv 互斥。睡眠等锁期间,CPU 时间不属于该 socket。
不光如此,软中断上下文 tcp_v4_rcv 处理 TCP 接收间,无论 send 还是 recv 均会在一个 spinlock 自旋,这意味着软中断
TCP 接收间,send 无法进行,recv 亦无法读取已按序齐整排入 receive queue 的数据。
这就是我常说的,TCP 称全双工传输,但 Linux TCP 实现却是半双工。我并不认为这是高尚的:
Linux TCP并不是全双工的
很多人怼我,说我根本不懂双工的概念,涉及到物理层,信道。我想他们并没有理解我在说什么。再解释也苍白,引用 iperf-2.0.14a 的 manual
来解释 socket 双工:
–full-duplex
run a full duplex test, i.e. traffic in both transmit and receive directions
using the same socket
在我看来,高尚的做法是只保护读写共享的数据,细化锁粒度:
* sk_write_queue:发送队列,socket 进程上下文写入,tcp_write_xmit 进程上下文或软中断上下文摘除。
* tcp_rtx_queue:重传队列,socket 进程上下文或软中间上下文写入,tcp_clean_rtx_queue 软中断上下文摘除。
* sk_receive_queue:接收队列,socket 进程上下文 release_sock 或软中断上下文写入,socket 进程上下文摘除。
保护好这些数据结构,再小心翼翼处理一下其它共享元数据,send/recv/tcp_v4_rcv 即可并行。事情会高尚很多。
迄今为止,我想因为 Linux TCP 单机性能尚未触及瓶颈,拆锁重构工作量成本不小,却没有眼见的收益,社区没人闲着做这事。如今 100 Gbps
网卡越来越多,这些细节早晚会被盯上。
但还有一条路,若 DPDK 可实现好用的 TCP,内核协议栈可能就这么放着永远不动了:
* DPDK 提供高性能版本 TCP。
* Kernel 提供通用完备 TCP。
外说一句,曾经 UDP 也是类似 lock_sock,可能也是因为一把梭哈实现简单,后面随着 UDP 应用逐步推广,可能是 QUIC 加持也可能不是,UDP
的 lock_sock 消失了,锁粒度细化到保护特定的共享 queue,最终,连 queue 都拆成了两个,便于批量处理:
* Linux内核UDP收包为什么效率低?能做什么优化?
* Linux内核UDP收包为什么效率低?能做什么优化?
这也是历史发展的轨迹。
弄蟹!80块钱弄蟹,两大四小。
浙江温州皮鞋湿,下雨进水不会胖。