硅谷网9月27日评论 据《电脑知识与技术》杂志刊文,目前,国内外已对进程迁移做了较多的研究。有通过修改操作系统内核实现进程迁移机制的系统,如:MOSIX、Charlotte、V、Sprite和Mach等;也有在用户层实现进程迁移机制的系统,如:Condor、PRM、UPVM、DQS等。在这些已实现的进程迁移机制当中,其大部分是基于检查点机制来实现的。即在迁移进程之前,把进程的状态数据保存到检查点文件,然后把该检查点文件转移到另外的节点上,待目标节点收到整个检查点文件后,再根据检查点文件重新恢复迁移进程。
我们对进程迁移的研究也是基于Linux的检查点机制,并在此基础上实现了核外的进程恢复,进而实现进程迁移。
在要恢复的部分系统上下文中包括:进程描述符、内存描述符、文件描述符等内核数据,但是这些内容是没有必要被原样恢复的。因为恢复这些系统上下文的目的在于保持进程与文件的映射关系以及获得运行所需的内存资源,但这不是非得原样恢复原来的系统上下文才能做得到,可以通过让系统重新加载原可执行文件来自动完成。虽然这些内核数据与原来的不一样,但是当系统完成加载映射后,用户地址空间的安排和内核数据的分配已经完成,磁盘文件和内存虚拟空间也关联起来,形成了原来进程运行的基本框架。
而恢复用户上下文和寄存器上下文目的在于保证迁移进程与原进程中断时候数据的一致性,这也就是要恢复进程断点处全局变量和局部静态变量的值、堆栈的内容以及寄存器的内容。通过对ELF可执行文件及其执行过程的分析可以看到,对这部分信息的恢复,可以考虑在进程跳转到程序起始地址的时候,先让其执行一段恢复代码来恢复断点时候的数据信息,然后再跳转到断点处开始执行。
因此,整个构建新可执行文件的工作,可以由数据段的恢复、堆栈及寄存器的恢复、文件调整这几步来完成。
1、数据段的恢复
根据以上的分析,考虑到在整个进程的执行过程中,textsegment无论怎样都是不变的,所以直接引用原可执行文件的textsegment。对于datasegment,要考虑的有两个地方,一个是.datasection,一个是.bsssection,这两个地方分别存放的是已初始化和未初始化的全局变量、局部静态变量,进程在运行过程中可能会对其进行修改,故要用检查点文件(core)中对应的.data和.bsssection来做恢复。而datasegment的其他部分是和动态链接有关的,它们是提供给动态链接器在动态符号解析和重定位的时候使用的,用户程序在执行过程中不会对其进行修改,所以重新加载原文件后,这部分设置已经正确,没有必要修改了。
于是,我们仍然是直接引用原可执行文件的datasegment,再把检查点文件中对应.data和.bsssection的内容拷贝到新的可执行文件中。待新的可执行文件被加载到内存后,再用repmovsd指令把其复制到对应位置,这也就恢复了进程中断时的全局变量和局部静态变量的值。在拷贝.datasection和.bsssection的时候,都要记录下该数据被加载后的源地址、要恢复的目标地址、以及整个内容有多少双字大小,为使用repmovsd指令做准备。
动态共享库是用户进程不可或缺的一部分。在Linux中,动态共享库也是ELF格式,所以可以采用恢复用户程序的方法来对动态共享库做调整。也就是要对共享库的.datasection和.bsssection做恢复,因为共享库是存在于用户空间,当在执行恢复代码的时候,已经完成了映射和重定位,直接对其内容做操作是可行的。
在对动态共享库的处理中,确定共享库的.data和.bsssection在检查点文件中的对应部分,要稍微复杂些。这首先得从原可执行文件中找出所依赖的共享库,读取sectionheadertable进而确定.datasection、.bsssection与datasegment的相对位置关系,然后根据这个相对关系,再在检查点文件中找到保存该共享库datasegment的位置,从中提取出.data和.bsssection的内容,写到新的可执行文件中。
当所有相关内容都被提取后,接下来就要插入repmovsd等指令的机器码,来操作刚才这些数据,完成在运行的时候对目标虚拟地址空间内容的修改。
2、堆栈和寄存器的处理
下一步,就要恢复进程中断时堆栈和寄存器的值。但并不是整个堆栈段的内容都必须恢复,可以只恢复与进程运行相关的堆栈帧的部分。在我们的实现中,首先根据中断时esp指定的位置和堆栈帧中ebp为0的位置确定恢复区间,然后用从检查点文件中,依次以双字大小为单位来提取其中内容,再用push指令来完成堆栈的恢复。而寄存器的值,可以直接从检查点文件中读取,用mov指令来恢复。
应该指出的是,这里恢复的各个寄存器的值,是从检查点文件中保存的structpt_regs(asm/ptrace.h)结构中提取的。该结构位于内核栈栈顶,保存的是进程陷入内核时候的寄存器内容,所以仅仅恢复他们就可以使CPU的状态恢复到进程中断时候的内容。
Linux检查点文件没有保存内核栈的其他内容,但这样处理是有理由的。Linux是以信号处理机制来产生检查点的,当进程从内核函数返回的时候,Linux检查进程有没有收到信号,如果有接收到信号,就调用信号处理程序。这样的话,对信号的处理是在某个内核函数调用结束的时候,此时,该内核函数调用已经结束,功能已经完成,内核栈中的内容对进程已经没有意义,因而没有必要保存。
在以上所有的这些恢复代码中,都只能使用机器码。因为ELF可执行文件本身就是一个二进制文件,在不能重新编译、创建新可执行文件的情况下,只能够直接使用机器能识别的机器码。当然,这样可能会使程序的可移植性降低,但只要在使用的时候把各种机器码都封装起来,对外提供相同的接口,便不会有什么影响。
3、对构造文件的调整
插入的数据和恢复代码要能够被执行,就必须以某种方式进入新的可执行文件,并在系统加载文件的时候被一同加载到内存。这里采用的方法是,把它们整个合起来作为新可执行文件的一个单独的segment,在programheadertable中占用notesegment的programheader,同时设置该header为textsegment的属性。新添加段的起始虚拟地址为可执行文件datasegment的页对齐虚拟地址与新添加段相对文件起始的偏移值的和,这样处理将会使用户程序有两个textsegment,不过根据exec()加载程序分析判断是可行的(fs/binfmt_elf.c)。
这样的处理,是为了不改变ELF文件头。由于在ELF可执行文件中,其programheadertable大小是固定了的,随后的textsegment、datasegment也都是根据这个偏置来存放,更重要的是程序代码中也引用了这样的偏置,所以不能随意改变programheadertable大小,否则将引起程序混乱而不可执行。而ELF的notesegment保存的是一些附加信息,对程序的运行没有任何的用处,在重构ELF可执行文件的时候,完全可以占用这个段的programheader。
最后还要修改ELFheader中指定程序入口点entry,使这个入口点为恢复代码的起始虚拟地址。由前面的分析可以得到,在完成共享库的加载和解析之后,动态链接器会将控制权传递到ELFheader指定的入口点。这个时候,新进程已经拥有能正常运行的足够资源,再由恢复代码完成相关调整,来模拟原进程中断时刻的状态,然后再跳转到原进程断点处,就可以继续原进程的执行了。
4、结束语
在Linux核外实现进程恢复具有很突出的优点,它不依靠任何的内核代码,实现相对简单,而且不必修改任何的用户源程序,不需要额外的函数库,也不需要重新编译用户程序。原则上,大多数的类UNIX系统都可以支持。对那些非专用集群系统,要保持其软硬件平台的通用性,就比较适合采用在内核外进程恢复的方法。
不过,由于采用的是核外恢复,并且是利用Linux自身的检查点机制,使得这个方法受到较多情况的限制。比如对那些使用了进程间通信机制、正在进行某些I/O操作、运行环境依赖特定环境变量(PID、主机名)或者有未处理消息的进程就不能恢复,这就有可能要求用户选择适合的进程来进行迁移。
基于核外进程恢复的进程迁移特别适合处理需要占用大量CPU时间,而较少特定I/O操作的大计算量的科学计算任务。一般来说,进程的可迁移性主要与迁移开销与进程占用CPU时间有关,长时间占用CPU的进程迁移开销相对较小,这样的进程迁移会获得较为满意的效果。
王芳(1978.7—),女,研究生,湖北省洪湖市人,重庆商务职业学院讲师,主要研究方向:系统开发,网络出版
|