Deep Dive: Linux虚机挂起的前前后后

之前遇到一个case,用户在VMware ESXi上面部署了一台RedHat Linux虚机,虚机里面跑的是Oracle的业务。

用户发现,每次在启动Oracle的service时(service start xxxx)虚机就会被挂起。只要在界面上点‘resume’就可以把虚机恢复,后续的其他Oracle的操作都运行良好,不会有问题。

不管什么时候,只要运行该服务的启动脚本就会导致虚机挂起。用户很不爽,因为这样每次都需要重新‘resume’虚机才能运行正常的业务。

因为虚机挂起了,所以可以分析RedHat Linux虚机当时到底发生了什么。结果发现,一个进程访问了/dev/port,而正是这个操作导致了虚机的挂起。

原来用户访问的刚好就是负责电源管理的端口(power management),而这个端口的定义就是将系统置于S3的状态。在虚拟机环境下,ESXi会把虚机挂起,从而模拟S3的状态(VMware还是支持的非常完善的)。

用户得知了问题的原因之后就给Oracle开了bug,Oracle怎么处理的暂且不提,反正用户的问题没有解决。

事情到此,按理说就结束了,问题原因找到了,冤有头债有主,谁污染谁治理。

但是因为用户的问题最终没有得到解决,可见用户在使用过程中遇到了很大的问题,用户体验很差。

作为整个环节的一名参与者和开发者,有没有可能解决类似的问题呢?

作为上层应用的Orcale的服务是没法修改的,作为底层的ESXi的处理逻辑也是不能修改的。那么剩下的就只能是夹在中间的guest OS了,在Linux VM里面想想办法了。

用户部署的Linux VM不只一台,修改编译内核,再重新部署的代价也不小。可能的办法是做个kernel module。

OK,那么目前这个题应该怎么解?

已知用户态的一个进程X,其访问/dev/port设备文件,对其中的一个端口P进行了写操作。那么如何避免在这样的条件下触发ESXi对虚机进行挂起操作?限制条件是不要修改内核代码,不能修改进程P的运行逻辑。

由X86的硬件辅助虚拟化和之前的分析得到,对端口P的写操作导致了一次VMExit,该事件导致了ESXi对该虚机进行了挂起操作。

因此只要防止这个写port P的操作实际发生就可以了。这里是我后来做的一个hack。

这个hack是在阅读https://blog.csdn.net/qq_21792169/article/details/84583275的时候被它启发的(这个链接的代码一共有4个问题,我一一修复。然后陆陆续续总结出另外9个问题)。

这个hack的方法简单描述如下。

首先定位write_port()函数,然后按照上面文章的逻辑安装一个hook,把对它的调用捕获。在hook函数当中判断current process == X && write to port P。如果是,那么就直接返回用户态而什么都不做(不实际写port P)。如果不是,那么直接再执行原来write_port()函数的操作。

把上述功能做成一个kernel module,在有问题的虚机上自动加载之后再启动Oracle的服务就不会发生虚机挂起的情况。

下面是测试程序(对port 378的操作)和内核log,供参考。

#include

#include

#include

#include

#include

int main(int argc, char *argv[])

{

    int fd1=-1;

    unsigned char data=0xaa;

    fd1=open("/dev/port",O_RDWR|O_NDELAY);

    if ( fd1 < 0 ) return fd1;

    lseek(fd1,(0x378+0x00),SEEK_SET);

    write(fd1,&data,1);

    return 0;

}

编译成为testwriteport可执行文件。该程序向port 378写入一个字节0xAA。

[18760.918653] Orig_write_port    =  ffffffff81533bb0

[18760.920604] Hook_write_port    =  ffffffffc0535020

[18763.654453] testwriteport [13402] called write_port to port 0x378, 1 bytes

[18763.658643] AA

可见内核模块的hook函数捕获了对应的操作,并且获取了相应操作的信息(进程,端口号,读写操作,和数据)。因此可以进行有针对性的处理,而不是霰弹枪一样,一打一大片,把正常的通过/dev/port的操作也干扰了就太糟糕了。

这个方法的毫无疑问地会引入一定的开销,好在/dev/port的写操作并不多见。

总结&think further

上面的分析和hack可以完美的解决这个问题。可是我喜欢凡事总要再问几个为什么。这样的解决方法够了吗?有没有其他的方法,更好的方法?

上述hack是自己写的hook机制和处理方法,但是这个方法其实是fragile的,它依赖于内核把ftrace的支持打开。而且只能针对X86平台。

其实,更好的方法是利用内核提供的机制来完成hook和相应的处理。这个内核机制就是kprobe。当然需要内核打开对kprobe的支持。有了kprobe的支持,就可以利用内核提供的标准接口来完成类似的操作。但是如何避免真正的写port P还是需要进一步的工作。

当然,如果去掉不能修改编译内核源代码的限制条件,另一个方法就是直接修改write_port()函数完成判断和处理,但是显然这个方法不如之前的hack来的方便。因为修改了内核,如何从RedHat获取技术支持,在需要提供kernel-debuginfo进行分析的时候也只能提供用户自行修改后的包而不是RedHat官方发布的包,这些都带了很多后续的问题,不推荐。

写在最后,一个问题的出现总是有原因的,找到原因就可以想办法修复。而找到一个解决办法要考虑很多因素。一个方法找到了,还要继续挖挖看有没有更好的办法。

知识和技能的积累其实就是靠 Dive deeper and wider!

你可能感兴趣的:(Deep Dive: Linux虚机挂起的前前后后)