TCP 传输时 sk_buff 的 clone 和 unclone

周一有位朋友咨询个问题,问题本身不重要,但牵扯出的细节却是非常有趣。

Linux 内核协议栈的 skb 设计非常高效和精巧,多个 skb 可以指向同一块 data,这就是 clone,当 data 不止一个 skb 指示时,任何一个 skb 要修改 data 时,必须 unclone,其内部实现就是一个简单的 cow(copy on write),这就是 unclone,原理如下:

clone 和 unclone 的含义简述如下:

  • clone,复制一个新 skb,指向相同 data,dataref 递增;
  • unclone,复制一份新 data,原 dataref 递减,skb 指向新 data;

值得注意的是 skb 的 clone 和 share 的区别:

  • clone,针对 skb 的 data;
  • share,针对 skb 结构体本身;

因此,kfree_skb 就非常清晰了:

  • 先递减 share 计数,自己是最后一个才继续释放 skb 的其它内容;
  • 再递减 data 的 dataref,自己是最后一个才彻底释放 data 本身;

至于 skb_copy,pskb_copy,自然就不必说,理解 skb_clone,skb_unclone,kfree_skb 就够了。

下面是 TCP 传输和重传过程的 clone,unclone 序列:

TCP 传输和重传时,红黑树(解释理由)上的本体 skb 结构体始终未变,变的是 clone skb 结构体和 data:

  • 每次传输或重传时,均会 clone 一份本体 skb 实际传输,clone 过程本体 skb 的 data 不变,dataref 递增;
  • 每次重传时,如果 dataref 大于 1,本体 skb 会 unclone,复制一份 data ,原 data 的 dataref 递减,回到 1;

unclone 后留下的原 data 并未游离,因为还有上下文引用它们,可能驱动尚未发送完毕,等发完了一般会有中断通知,那就留给 clone 它的上下文去 kfree 了。

以下是上面知识在处理朋友问题时的一个应用,该应用过程反过来也能加深上面知识的理解和内化。

问题是这样的。理论上 TCP 重传时要从重传队列 head 开始,可抓包却发现先重传了最后 fin,再次重传时才重传队列更前面的 data。

初步猜测是 tlp,过年期间写过一个关于 tlp 的,详见 探秘 TCP TLP:从背景到实现。为帮朋友定位问题,我写了下面的 pdrill 复现脚本,并打开 tlp:

c 复制代码
   // 开启 tlp
   //sysctl -q net.ipv4.tcp_early_retrans=4 net.ipv4.tcp_recovery=1
   0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
  +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
  +0 bind(3, ..., ...) = 0
  +0 listen(3, 1) = 0

  +0 < S 0:0(0) win 32792 <mss 1460,sackOK, nop, nop, nop,wscale 7>
  +0 > S. 0:0(0) ack 1 <mss 1460,nop, nop, sackOK, nop, wscale 7>
  +0 < . 1:1(0) ack 1 win 257
  +0 accept(3, ..., ...) = 4

  +0 write(4, ..., 1360) = 1360
  +0 close(4) = 0
  +10 < . 1:1(0) ack 2821 win 257

通篇没收到任何 ack, 1360 字节数据和 close 的 fin 均要被重传,会先重传最后的 fin,但理论上后续重传 1360 字节时会将 fin 合并带上,几乎可以完全复现场景:

然而抓包却是这样:

并没有合并捎带 fin。

不合并就不合并了,不是什么大事,本身 tcp collapse 触发条件就不止一个。但作为 pdrill 本地测试,最基本的场景,立场应该是想让它 collapse 它就得 collapse,一定是哪里出了问题才导致 1360 字节的报文没有 collapse fin。

tcp 重传 collapse 的条件是没有其它上下文引用该 skb 的 data,即 shinfo->dataref == 1,以下是详细观测 skb shinfo->dataref 的脚本:

c 复制代码
bpftrace -e '
//kprobe:__tcp_transmit_skb  // 尚未 clone,dataref = 1
//kprobe:__ip_queue_xmit     // 已经 clone,dataref = 2
//kprobe:__dev_queue_xmit
//kprobe:dev_hard_start_xmit
//kprobe:dev_queue_xmit_nit // PACKET 套接字等抓包程序会再次 clone,dataref = 3,处理完成后 dataref = 2
//kprobe:网卡 xmit 回调      // 传输完毕由驱动负载 kfree_skb,dataref = 1
{
    $skb = (struct sk_buff *)arg0;
    $dev = (struct net_device *)($skb->dev);

    if (strncmp($dev->name, "tun0", 4) == 0) {
        $shinfo = (struct skb_shared_info *)($skb->head + $skb->end);
        $dataref = $shinfo->dataref.counter;
        printf("tun0 skb=%p dataref=%d\n", $skb, $dataref & 0xffff);
    }
}
'

结果是到了 tun_net_xmit 时,dataref = 3,但在 dev_hard_start_xmit 时 dataref 还是 2,这中间只有 PACKET 套接字,类似抓包路径了。而抓包需要 copy data,肯定是需要 clone skb 的。还果然是这个原因:

bash 复制代码
root@vbox:~# strace -e trace=socket packetdrill ./tlp-with-fin.pkt
...
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=6624, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=6626, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) = 6

也可在 packetdrill 运行期间通过 procfs 确认:

bash 复制代码
root@vbox:~# cat /proc/net/packet
sk               RefCnt Type Proto  Iface R Rmem   User   Inode
00000000dafae456 3      2    0003   0     1 0      0      42872

但即使减少了 PACKET 套接字的 1 个 ref,还有 1 个 ref 直到 tun_net_xmit 也没释放,原因在于 packetdrill 并未 read tun0fd。

为了避免这种本地测试工具不处理 read 造成的额外混乱,我觉得 tun 驱动应该增加一个 "低效率" 模式,即 skb 进入 tun 驱动的 tun_net_xmit 回调时直接转交掉,多一次复制,少一份混乱:

c 复制代码
static netdev_tx_t tun_net_xmit(struct sk_buff *skb, struct net_device *dev)
{
    struct tun_struct *tun = netdev_priv(dev);
    enum skb_drop_reason drop_reason;
    int txq = skb->queue_mapping;
    struct netdev_queue *queue;
    struct tun_file *tfile;
    int len = skb->len;
    struct sk_buff *nskb = skb;

    rcu_read_lock();

    if (tun-> & 低效) {
        skb = skb_copy(nskb, GFP_ATOMIC);
        kfree_skb(nskb);
        // 这里顺带帮 PACKET 套接字解除一个 ref,实际中不需要
        atomic_dec(&skb_shinfo(nskb)->dataref);
    }
    tfile = rcu_dereference(tun->tfiles[txq]);

再次运行,结果就符合预期了:

如果 skb 发到真实网卡,当网卡中断通知发送完成,该 skb 即可得到释放,但本地环回的 skb 生命周期可是要长得多,比如 loopback_xmit,直接用 tx 路径的 skb 调用 netif_rx 了。

不出本机协议栈的数据包环回处理最好在 ndo_start_xmit 中都转交 copy 一下 skb,毕竟本地处理不要求性能(要求高性能的抓包也不用 skb),这样可避免循环依赖而伤害到协议栈的固有行为,比如我用 pdrill 几乎无法验证 collapse 机制。

多年以前我曾因为 tun 网卡没有调用 sk_mem_uncharge 导致了通过 tun 网卡的 socket 异常行为,但忘记题目了,文章找不到了。

不管这世界多么无知和操蛋,只要给我一本历史书,给我一个 TCP 疑难问题,或带我到一座可以攀登的山脚下,我就马上元气爆满!

浙江温州皮鞋湿,下雨进水不会胖。

相关推荐
1892280486115 分钟前
NW728NW733美光固态闪存NW745NW746
大数据·服务器·网络·人工智能·性能优化
蜡笔小炘1 小时前
HCIA--- OSPF动态路由实验
网络
计算机小手5 小时前
内网穿透系列九:开源的网络穿透与组网工具 EasyTier,支持多种数据传输通道,去中心化,兼具高效与安全
网络·经验分享·开源软件
YC运维6 小时前
网络配置综合实验全攻略(对之前学习的总结)
linux·服务器·网络
yqcoder7 小时前
13. https 是绝对安全的吗
网络协议·安全·https
Xi-Xu7 小时前
隆重介绍 Xget for Chrome:您的终极下载加速器
前端·网络·chrome·经验分享·github
孙克旭_8 小时前
day051-ansible循环、判断与jinja2模板
linux·运维·服务器·网络·ansible
悟空胆好小9 小时前
分音塔科技(BABEL Technology) 的公司背景、股权构成、产品类型及技术能力的全方位解读
网络·人工智能·科技·嵌入式硬件
DoraBigHead9 小时前
《电磁波的浪漫,铜线上的灵魂》——计算机网络·物理层全解版
网络协议
ssswywywht10 小时前
OSPF实验
网络