默默的一块石头
分类: LINUX
2020-11-24 14:11:57
近日有人问我为什么在PREROUTING这个NF HOOK点的function里需要使用spin_lock/unlock_bh而不是spin_lock/unlock来保护临界区。近日有人问我为什么在PREROUTING这个NF HOOK点的function里需要使用spin_lock/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即可:
进程上下文C1在PREROUTING function中调用spin_lock(Lx)进入临界区。
尚未出临界区,C1所运行CPU被中断,随即调度softirq执行net_rx_action。
在软中断上下文C2中进入PREROUTING function,调用spin_lock(Lx)企图进入临界区。
由于C1已经获取spinlock Lx,C2开始自旋,等待C1释放Lx。
由于C1被C2抢占,而C2已经自旋,因此妥妥死锁!
但问题是,在什么情况下,进程上下文能到PREROUTING呢??
记得2015年大概也是这个时候,写过一篇文章:
https://blog.csdn.net/dog250/article/details/48770481
该文章中的case是进程上下文中执行数据包接收的场景,数据包接收的过程中肯定是穿过PREROUTING点的。
我来摘抄一下该文章相关的描述:
一个连接本机的TCP数据包最终到达了loopback的xmit发送函数,其中简单的调度了本CPU上的一个软中断处理,然后会在下一次中断结束后调度其执行,这有很大几率是在当前发送进程的上下文中进行的,也就是说,发送进程在其上下文中进行了发送操作,而此时软中断借用了其上下文触发了接收操作,…
但是,有问题啊,什么叫 "这有很大几率是在当前发送进程的上下文中进行的" 我感觉这不严谨,所以今天我要深入探究一下这个问题:
为什么loopback网卡的发送和接收逻辑在同一个进程上下文中进行?
为此,需要在本地通过loopback进行ping通信的时候,打印出stack:
#!/usr/local/bin/stap -g
function dump()
%{
dump_stack();
%}
probe kernel.function("icmp_rcv") {
dump();
//print_backtrace();
// 这个不知为何不好使..
}
以下是一次ping后的结果:
[34197.319729] [
[34197.319732] [
[34197.319735] [
[34197.319738] [
[34197.319741] [
[34197.319744] [
[34197.319747] [
[34197.319752] [
[34197.319755] [
[34197.319757] [
[34197.319760] [
[34197.319765] [
[34197.319768] [
[34197.319769]
[34197.319777] [
[34197.319780] [
[34197.319783] [
[34197.319786] [
[34197.319789] [
[34197.319791] [
[34197.319793] [
[34197.319797] [
[34197.319802] [
[34197.319805] [
[34197.319811] [
[34197.319814] [
[34197.319817] [
[34197.319820] [
[34197.319823] [
哈哈,真相大白了!我在2015年的分析是错误的:
发送进程在其上下文中进行了发送操作,而此时软中断借用了其上下文触发了接收操作,…
根本就不是什么 "借用了其上下文" ,而是实实在在就是在该上下文中主动调用的net_rx_action啊!
其调用逻辑如下:
ip_output_finish
rcu_read_lock_bh ...
dev_queue_xmit
loopback_xmit
netif_rx
enqueue_to_backlog # 这里将skb入队列 raise_softirq_irqoff(NET_RX_SOFTIRQ) ... ... ... ... ...
rcu_read_unlock_bh # unlock操作触发进程上下文中处理接收操作 local_bh_enable
do_softirq
__do_softirq
net_rx_action # 这里对队列中的skb进行处理 ...
ip_rcv_finish
icmp_rcv ... ... ... ... ... ...
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网卡发送和接收数据包奇怪且有意思的流程:
发送逻辑尚未返回,接收逻辑先返回。
这意味着什么?不得而知,但如果碰到一些本机连本机过程中莫名其妙的问题,可以从此入手来排查。