由spin_lock_bh想到的一些事

待我称王封你为后i 2022-12-13 12:45 342阅读 0赞

近日有人问我为什么在PREROUTING这个NF HOOK点的function里需要使用spin_lock_bh/unlock_bh而不是spin_lock/unlock来保护临界区。

面对这个问题,有点懵,说到spin_lock族,有很多系列接口:

  • spin_lock/spin_unlock
  • spin_lock_bh/spin_unlock_bh
  • spin_lock_irq/spin_unlock_irq
  • spin_lock_irqsave/spin_unlock_irqrestore

之所以有这么多,说白了就是为了 防止关闭了抢占的临界区被同一个CPU的高优先级序列打断而重入时造成死锁。

但还是要给出一个具体的case才能让人信服,而不仅仅是理论上如此。

其实只需要给出一个进程上下文调用PREROUTING function的case即可:

  1. 进程上下文C1在PREROUTING function中调用spin_lock(Lx)进入临界区。
  2. 尚未出临界区,C1所运行CPU被中断,随即调度softirq执行net_rx_action。
  3. 在软中断上下文C2中进入PREROUTING function,调用spin_lock(Lx)企图进入临界区。
  4. 由于C1已经获取spinlock Lx,C2开始自旋,等待C1释放Lx。
  5. 由于C1被C2抢占,而C2已经自旋,因此妥妥死锁!

但问题是,在什么情况下,进程上下文能到PREROUTING呢??

记得2015年大概也是这个时候,写过一篇文章:
https://blog.csdn.net/dog250/article/details/48770481
该文章中的case是进程上下文中执行数据包接收的场景,数据包接收的过程中肯定是穿过PREROUTING点的。

我来摘抄一下该文章相关的描述:

一个连接本机的TCP数据包最终到达了loopback的xmit发送函数,其中简单的调度了本CPU上的一个软中断处理,然后会在下一次中断结束后调度其执行,这有很大几率是在当前发送进程的上下文中进行的,也就是说,发送进程在其上下文中进行了发送操作,而此时软中断借用了其上下文触发了接收操作,…

但是,有问题啊,什么叫 “这有很大几率是在当前发送进程的上下文中进行的” 我感觉这不严谨,所以今天我要深入探究一下这个问题:

  • 为什么loopback网卡的发送和接收逻辑在同一个进程上下文中进行?

为此,需要在本地通过loopback进行ping通信的时候,打印出stack:

  1. #!/usr/local/bin/stap -g
  2. function dump()
  3. %{
  4. dump_stack();
  5. %}
  6. probe kernel.function("icmp_rcv")
  7. {
  8. dump();
  9. //print_backtrace(); // 这个不知为何不好使..
  10. }

以下是一次ping后的结果:

  1. [34197.319729] [<ffffffff8159a145>] ? icmp_rcv+0x5/0x380
  2. [34197.319732] [<ffffffff81561b84>] ? ip_local_deliver_finish+0xb4/0x1f0
  3. [34197.319735] [<ffffffff81561e69>] ip_local_deliver+0x59/0xd0
  4. [34197.319738] [<ffffffff81561ad0>] ? ip_rcv_finish+0x350/0x350
  5. [34197.319741] [<ffffffff815617fd>] ip_rcv_finish+0x7d/0x350
  6. [34197.319744] [<ffffffff81562196>] ip_rcv+0x2b6/0x410
  7. [34197.319747] [<ffffffff81561780>] ? inet_del_offload+0x40/0x40
  8. [34197.319752] [<ffffffff815267b2>] __netif_receive_skb_core+0x582/0x7d0
  9. [34197.319755] [<ffffffff81526a18>] __netif_receive_skb+0x18/0x60
  10. [34197.319757] [<ffffffff815276ee>] process_backlog+0xae/0x180
  11. [34197.319760] [<ffffffff81526ed2>] net_rx_action+0x152/0x240
  12. [34197.319765] [<ffffffff8107e02f>] __do_softirq+0xef/0x280
  13. [34197.319768] [<ffffffff81646b1c>] call_softirq+0x1c/0x30
  14. [34197.319769] <EOI> [<ffffffff81017155>] do_softirq+0x65/0xa0
  15. [34197.319777] [<ffffffff8107d924>] local_bh_enable+0x94/0xa0
  16. [34197.319780] [<ffffffff81566a00>] ip_finish_output+0x1f0/0x7d0
  17. [34197.319783] [<ffffffff81567cff>] ip_output+0x6f/0xe0
  18. [34197.319786] [<ffffffff81566810>] ? ip_fragment+0x8b0/0x8b0
  19. [34197.319789] [<ffffffff81565971>] ip_local_out_sk+0x31/0x40
  20. [34197.319791] [<ffffffff81568746>] ip_send_skb+0x16/0x50
  21. [34197.319793] [<ffffffff815687b3>] ip_push_pending_frames+0x33/0x40
  22. [34197.319797] [<ffffffff81590fbe>] raw_sendmsg+0x59e/0x620
  23. [34197.319802] [<ffffffff810af1a9>] ? ttwu_do_wakeup+0x19/0xd0
  24. [34197.319805] [<ffffffff8159f604>] inet_sendmsg+0x64/0xb0
  25. [34197.319811] [<ffffffff8150cc90>] sock_sendmsg+0xb0/0xf0
  26. [34197.319814] [<ffffffff8150d201>] SYSC_sendto+0x121/0x1c0
  27. [34197.319817] [<ffffffff8150e221>] ? __sys_recvmsg+0x51/0x90
  28. [34197.319820] [<ffffffff8150dc8e>] SyS_sendto+0xe/0x10
  29. [34197.319823] [<ffffffff81645189>] system_call_fastpath+0x16/0x1b

哈哈,真相大白了!我在2015年的分析是错误的:

发送进程在其上下文中进行了发送操作,而此时软中断借用了其上下文触发了接收操作,…

根本就不是什么 “借用了其上下文” ,而是实实在在就是在该上下文中主动调用的net_rx_action啊!

其调用逻辑如下:

  1. ip_output_finish
  2. rcu_read_lock_bh
  3. ...
  4. dev_queue_xmit
  5. loopback_xmit
  6. netif_rx
  7. enqueue_to_backlog # 这里将skb入队列
  8. raise_softirq_irqoff(NET_RX_SOFTIRQ)
  9. ...
  10. ...
  11. ...
  12. ...
  13. ...
  14. rcu_read_unlock_bh # unlock操作触发进程上下文中处理接收操作
  15. local_bh_enable
  16. do_softirq
  17. __do_softirq
  18. net_rx_action # 这里对队列中的skb进行处理
  19. ...
  20. ip_rcv_finish
  21. icmp_rcv
  22. ...
  23. ...
  24. ...
  25. ...
  26. ...
  27. ...
  28. ip_output_finish return

OK,现在,这就是一个非常清晰的进程上下文执行数据包接收逻辑的case,也就是说:

  • 既然软中断函数net_rx_action可能会在进程上下文中执行,为了防止死锁,其中的临界区一定要用_bh版本的spinlock保护!

类似rcu_read_unlock_bh这种在unlock过程中做很多事情的操作,内核中还有很多:

  • spin_unlock可能会触发schedule进而发生task切换。
  • spin_unlock_bh可能会触发do_softirq进而执行软中断例程。
  • release_sock可能会执行sk_backlog_rcv进而处理收包。

这是一种 补偿 效应,既然lock操作到unlock操作之间禁止了一些行为,那么在unlock时就要尽可能地去补偿这些不得不延后的行为,尽量让它们马上执行。这个设计还是比较巧妙的。

另外,还有一个典型的进程上下文执行数据包接收逻辑的case,即TUN/TAP网卡从进程上下文调用tun_get_user,然后直接调用netif_rx_ni来收包的case。

我们再来看看这个loopback网卡发送和接收数据包奇怪且有意思的流程:

  • 发送逻辑尚未返回,接收逻辑先返回。

这意味着什么?不得而知,但如果碰到一些本机连本机过程中莫名其妙的问题,可以从此入手来排查。

发表评论

表情:
评论列表 (有 0 条评论,342人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Java ClassLoader一些

    在Java中,存在着类加载的机制。   除了委托机制以外,还遵循以下规则: 1、java命令启动main线程时,会使用该线程的类加载器去加载第一个类; 2、以后的类加载

    相关 清华百年所想到

    清华百年校庆,还是过年回家的时候听在清华读书的老同学提的,比较有印象的是他当时说到同时要举行百对新人婚礼之类的话。 再回溯一点,很久以前一群高中校友回到母校,有老师问到清华如