本文在前半部分叙述一个听起来十分吸引人且合理的故事,然后紧接着告诉你这个美好的故事事实上几乎不会发生,最后来个总结。在接下来的一篇文章中,我提出一个比较自我的方案。

第一部分:美好的故事

在 xtables-addons中,有一个特别有意思的小模块,那就是xt_SYSRQ,它作为一个iptables的target加载进内核,可以在远程 为本机发送sysrq命令,这个功能可谓强大。在去年的项目中中,我已经将其部署到了实际的产品中,然而今日再看,发现还是有些美中不足,确实需要改进:

1.原版的xt_SYSRQ没有反馈机制

虽然你可以远程触发一个sysrq命令,但是你得不到任何反馈信息,包括,命令执行成功与否,内核环形缓冲区的内容。有的时候,我仅仅是需要看看dmesg的信息而已,而有的时候,我想看看内核stack的dump信息。我要做的并不仅仅是重启机器这么简单的事。

2.原版的xt_SYSRQ的认证机制有点乱

在 addons中,SYSRQ模块实现一个简单的认证机制,即password认证,为了防止有人攻破password,使用了序列号窗口机制。然而你要知 道,这种防护是很容易攻破的,第一,数据包使用UDP传输,第二,内核中实在不便于做强认证,特别是真的panic的情况下。因此,更好的做法是将这种认 证放在外部。
以上是我发现的两个问题,但是解决它们的时候切记要点到为止,这是为什么呢?
        仔细想想,难道真的需要这种方式来获取远程的dmesg信息吗?如果系统还活着,用SSH的方式不是更好吗?因此,正如xtables-addons文档 中所述,这个功能只是在系统死掉的情况下才会是一种选择方式。知识点只有一个,那就是系统挂起以及panic的时候,中断或许还可以继续被响应,而 Netfilter HOOK的执行作为中断后的软中断也会被继续响应。可能有些人会问,为何系统都panic了,还会响应中断。答案在于你把panic想的过于严重 了,panic只是系统处于恐慌而不知所措的状态,并没有死去。导致系统不知所措的原因可能是系统级的操作bug导致了内存混乱或者别的混乱,系统继续运 行下去将会导致不可知的后果,此时最好的办法就是原地不动,这就是panic。由于中断的响应逻辑是封闭的,所以中断是依然可以响应的,然而如果中断处理 相关的内存被破坏,就彻底没有机会了。
        接下来再看一下远程SYSRQ的安全机制,其实作者原生的方式很安全,再次地,没有必要将其向复杂化路线上扩展太多。系统已然挂起,此时的处理应该尽可能 地简单,安全前提是需要保证的。首先网络上没有传输password的明文,而只传输了其摘要值,这种策略在网络认证领域已经烂大街了,其次,为了防止重 放***以及防止碰撞密码,采用了窗口机制,也不失为一个小巧的技巧,于是只要加一个反馈机制,就可以了,上面的问题2基本上不是问题。
        正当一切都已经就绪,知道了该做什么不做什么的时候,当你用Sysrq-c故意把系统Crash之后,你会发现一切瞬间停止,网卡中断完全不再响应...到底发生了什么?

第二部分:现实的情况

在《panic与BUG_ON》 那篇文章中,我曾经说“即使panic了,也还是可以从外部ping通这台机器”,但是不得不说,那仅仅是一种“最好的情况”,99%的情况下,中断将会 停止响应,要想理解这个似乎也不难,应为作为一个操作系统,用户态的进程只是它的一种执行流,还有内核线程,中断,中断上下文的中断下半部,线程上下文的 中断下半部等,并且,经由本机forward的数据包以及arp处理数据包几乎全部都是在中断或者软中断中被处理的,另外,很多的软中断都是可以在中断上 下文被处理(硬中断完成的irq_exit中)的,如果任由这些执行流继续下午,那还叫panic吗?因此最好的办法就是停掉所有这一切!似乎复杂性还不 止这些,如果你在一个中断上下文中引发了一个panic,那么很显然,软中断便不会在irq_exit中被执行,因此,即使你没有在panic后停掉所有 的中断响应,软中断也不是每次panic后都会在中断上下文中被执行的,取决于panic是否在中断上下文中被引发。
        如果你想知道panic之后到底发生了什么,也不是特别复杂,事实上,panic之后的序列如下:
1.禁止抢占(如果内核编译时没有打开抢占,相当于什么都没有做);
2.打印信息和堆栈;
3.调用kexec逻辑;
4.通知其它的CPU停止所有工作;
5.调用panic通知链进行善后;
6.如果panic timeout被设置,则等待后重启机器;
7.如果panic timeout没有被设置,则进入"闪灯"状态,直到永远。
其 中最关键的是第4步,这里也是今后系统是否还会处理中断和软中断的关键,在详细解释之前,先说一下为何要通知其它CPU停止工作。因为panic在本 CPU被触发,而在多个CPU间共享的内核数据结构此时可能已经乱掉了,此时就要通知别的CPU在这一瞬间停止。那么停止需要做哪些工作呢?主要集中在和 对应CPU相关的外部总线以及中断控制器操作,典型的说就是关闭掉它们,等于说把这些CPU尽可能和外部事件隔离起来,这也是一种安全的做法,如果一下子 掉电,可能会在热重启后遇到电平不一致的问题,因此采用安全的关闭序列总是要好一些的。那么有没有直接一点的做法呢?当然有!
        如果我们设置机器重启的方式为冷重启(板子几乎都支持),那么就不存在电平不一致的情况了,此时就可以将CPU安全地一下子掉电,因此也就没有必要遵循安 全关闭序列来关闭CPU了,为了更高效,在明知系统马上就会重新启动的前提下(这很重要),根本没有必要关闭中断控制器,而此时的CPU便会继续响应中 断,是否会继续处理软中断取决于panic是否在非中断上下文中被触发。由于已经禁止了抢占且没有任何执行流会返回用户态,因此此时不可能发生task切 换,故而即便是软中断会被处理,也不会在softirqd上下文被处理,而是在irq_exit中的中断上下文被处理。
        一切就是这么惨!
        具体来讲,要想在panic之后还能响应中断,你需要设置一个内核启动参数:reboot=f,c。含义是强制(force)冷(cold)重启,此时不 会执行停止CPU本地APIC中断控制器的操作,要想继续处理软中断,请别在中断上下文panic,这就好像告诉某个人请别死一样,这是无法控制的。即便 能保证中断上下文不会panic,由于无法调度softirqd,在中断上下文中来不及处理(只有MAX_SOFTIRQ_RESTART次机会)的软中 断将会被彻底淹没。另外,非要让系统在panic继续执行中断响应和软中断处理是极其不对的想法,那样会破坏更多的内核数据结构,万一破坏了磁盘 cache/buffer,或者误写了某个地址/寄存器,后果将是不可预料的...一切就是这么惨,只因为内核panic!

第三部分:正确的做法

正 确的做法不是设置一个iptables策略等待外部远程触发SYSRQ,这意味着中断不能被关闭,且软中断必须被处理...正确的做法是在panic之后 尽可能马上重启,而在重启之前需要做点善后操作,当然,如果你配置了kexec,那更好,但是如果你不准备调试它,那就完全没必要。以下是正确的做法:
1.启动时设置reboot=f,c参数,panic后不会禁止中断控制器工作;
2.系统启动后,设置sysctl -w kernel.panic=5,争取马上重启系统;
3.注册一个panic通知链,向外以广播地址封装以太头,广播内核环形缓冲区的内容(包括堆栈等信息)。
为 何采用广播我要说一下,因为我不希望它发送ARP请求然后再等ARP回应,因为那会平添一次交互(由于ARP回应的处理在软中断中进行,既然不能保证 panic不在中断中被触发,就不能保证软中断一定会被执行),再者说,发给谁呢?诚然可以配置一个接收IP地址,但多一个配置不说,这个IP毕竟保存在 内存,只要是使用了多一点的内存,panic之后取到错误数据的可能性就更大,panic之后你要想办法使用最少的信息,虽然“发送数据包”这种事场面已 经够庞大了,但是也是没有办法。