BLCR(Berkeley Lab Checkpoint/Restart)介绍及Checkpoint架构剖析
BLCR(Berkeley Lab Checkpoint/Restart)简单地讲是一个对进程做Checkpoint/Restart的套件,实现了用户态的libcr库和kernel module来完成相关的Checkpoint/Restart工作,最近在阅读BLCR的代码,也简单地hack过代码,写这篇文章来记录下我对于BLCR的理解,先暂时只写Checkpoint相关的BLCR架构流程。
1. BLCR的用法
对于一个进程如果需要对它进行Checkpoint,那么它首先需要具备以下两个条件之一:
- 进程通过cr_run启动,如cr_run ./test
- 进程在编译时链接了libcr库,如gcc -o test test.c -lcr
上述两程方法是等价的,即如果test在链接时未链入libcr,那用cr_run启动它也可以进行Checkpoint,同样,如果一个进程在编译时链接了libcr,那么启动时无需使用cr_run进程启动也可以进行Checkpoint.
假设刚启动的进程pid为3030,那么对该进程做Checkpoint可以简单地通过下面的命令来完成:
$ cr_checkpoint 3030
BLCR在完成后默认会生成一个context.3030的文件,里面包含了进程运行的几乎所有信息,可以通过该文件Restart该进程,如:
$ cr_restart context.3030
BLCR支持四种范围的Checkpoint:
-T 指定一个进程pid,它会Checkpoint整个进程树,这是BLCR的默认行为
-p 指定一个进程pid,它会Checkpoint指定的这单个进程
-g 指定一个group id,它会Checkpoint整个进程组的所有进程
-s 指定一个session id,它会Checkpoint整个会话里面的所有进程
2. BLCR的基本原理
BLCR的用户态工具(cr_checkpoint/cr_restart)通过proc file跟kernel进行交互,cr_module在初始化的时候创建了proc entry /proc/checkpoint/ctrl,并定义了该entry的open/release/ioctl/poll方法,open和release主要做一些和CR相关的初始化工作和资源释放工作,用户态工具与kernel的交互主要通过ioctl来完成。
BLCR通过在kernel中向要被Checkpoint的进程发送一个信号来通知进程对自己做Checkpoint,BLCR使用了signum=64的实时信号,这个信号在用户进程中是不能被重写的,假如我们在某进程中对64号信号重新注册了sighandler,那么cr_checkpoint就会hang住,因为这个sighandler会陷入内核来完成当前进程的Checkpoint工作,然后cr_checkpoint会通过proc entry的poll接口来等待POLLIN事件,sighandler被重写之后Checkpoint根本不会被执行,POLLIN事件也自然就不会被唤醒了。
这就引入了另一个问题,如何为用户态进程的64号信号注册一个指定的sighandler,也就是上面提到的两种方法,一种是使用cr_run执行,一种是给程序链入libcr。
BLCR编译完成后会生成几个共享库文件,libcr.so,libcr_run.so和libcr_omit.so,先不讨论omit.so,libcr.so和libcr_run.so中都存在一个初始化函数,函数的声明如下:
static void __attribute__((constructor)) cri_init(void)
这个函数声明为constructor,也就是这个函数会在其它函数开始执行前执行,BLCR在这个函数里面对进程的64号信号注册sighandler,因此链接了libcr的程序在开始执行的时候64号信号的sighandler就会被注册为指定的sighandler,而对于通常没用链接libcr的程序而言就使用了另一种方法,使用cr_run执行要被Checkpoint的程序,cr_run其实是一个简单的bash脚本,它做的主要工作就是设置一个环境变量LD_PRELOAD,简单地讲这个变量会让程序在运行前优先加载某个动态链接库,cr_run默认是把LD_PRELOAD设置为libcr_run.so,这个库中和libcr这个库一样使用了同一个cri_init()函数,只不过他们注册的sighandler略有不同。
2.1 Checkpoint入口和出口
cr_checkpoint中最主要的函数cr_request_checkpoint(),它会构造一个checkpoint request(cr_chkpt_reqs),这个request中包含用户所指定的一些Checkpoint参数,然后通过ioctl陷入内核,并将这个request通过ioctl参数传递给内核,这时候的ioctl所使用的request code为CR_OP_CHKPT_REQ,这个request code在cr_module中对应的处理函数为cr_chkpt_req(),接下来的Checkpoint逻辑对于用户态的cr_checkpoint来讲就是异步进行的了,具体的逻辑后面再详细写,先写一下cr_checkpoint如何同步地等待Checkpoint的完成,前面说过用户态进程与kernel的交互都是通过 proc entry来完成的,在进程的始终都会记录打开的proc entry file的fd,在发送完CR请求后便会等待CR的完成,这里的等待就是通过对刚才那个fd进行select来实现的,而select/epoll/poll在内核中对应的都是sys_poll,在cr_module初始化的时候就给这个proc entry的ops注册了poll callback,贴一下这个callback的定义吧:
static unsigned int ctrl_poll(struct file *filp, poll_table *wait) { unsigned int mask; cr_pdata_t *priv; CR_KTRACE_FUNC_ENTRY(); priv = filp->private_data; if (priv && priv->rstrt_req) { mask = cr_rstrt_poll(filp, wait); } else if (priv && priv->chkpt_req) { mask = cr_chkpt_poll(filp, wait); } else { mask = POLLERR; } return mask; } unsigned int cr_chkpt_poll(struct file *filp, poll_table *wait) { cr_pdata_t *priv; cr_chkpt_req_t *req; priv = filp->private_data; if (!priv) { return POLLERR; } req = priv->chkpt_req; if (!req) { return POLLERR; } else if (req == CR_CHKPT_RESTARTED) { return POLLIN | POLLRDNORM; } poll_wait(filp, &req->wait, wait); return check_done(req) ? (POLLIN | POLLRDNORM) : 0; }
由cr_checkpoint构造中的Request被塞到了proc file的privite_data中,poll其实就是在等待req->wait被wake up,这个queue什么时候被wake up也有两种方法,具体还是取决于要被Checkpoint的进程是用前面讨论的两种方法中的哪一种启动的,刚才提到两种方法给进程的64号信号注册的sighandler略有不同,BLCR定义了好多handler函数,我只写一下两种情况下的默认行为:
1. 程序链接了libcr.so
在这种情况下的sighandler函数向内核发送指令还是通过proc entry的ioctl完成的,具体组合为:
request code = CP_OP_HAND_CHKPT,flags = 0
而发送完这个指令后程序陷入内核把进程的相关信息dump出来,这个过程是同步进行的,所以在没有完成之前ioctl会hang在那里等待过程的完成,因此ioctl返回后便可以知道dump过程已经完成,接下来在sighandler函数中再通过ioctl发送一个request code = CP_OP_HAND_DONE的指令,这个请求对应在kernel的handler最终会把req->wait这个queue给激活,从而导致在用户态的cr_checkpoint的select返回POLLIN事件。
2. 程序通过LD_PRELOAD动态链接了libcr_run.so
这个sighandler默认是用汇编来写的,具体说来就是一个ioctl syscall:
request code = CP_OP_HAND_CHKPT,flags = _CR_CHECKPOINBT_STUB
与第一种情况不同的是,这个syscall完成以后并没有再调用另一个syscall发送CP_OP_HAND_DONE事件,这时候对于Checkpoint过程完成事件的通知就依赖于这个_CR_CHECKPOINT_STUB flags,在执行dump操作的核心函数cr_dump_self的最后有这样一句话:
if (req->die || result || (flags & _CR_CHECKPOINT_STUB)) { // this task will not call the HAND_DONE ioctl, so finish up now. cr_chkpt_task_complete(cr_task, 1); }
先不管req->die和result这两个,只要flags设置了_CR_CHECKPOINT_STUB,这个函数便会执行cr_chkpt_task_complete,这个函数在确定了所有要进行Checkpoint的进程全都dump完成后会激活req->wait:
if (list_empty(&req->tasks)) { wake_up(&req->wait); }
2.2 Checkpoint的请求结构
在cr_checkpoint的cr_request_checkpoint()这个函数通过ioctl向内核中发送了CR_OP_CHKPT_REQ请求,内核对于这个请求的处理函数中做了如下三件事情:
1. 将用户态传过来的checkpoint request对象塞到已打开的proc entry的private_data中。
2. 调用build_req()对要进行Checkpoint的进程/进程组/会话中的每个进程/线程创建request对象,build_req又根据前面提到的Checkpoint Scope选择要构建Request的进程的范围,默认的Scope是进程树,因此build_req()又调用build_req_tree()这个函数,对一颗进程树进行广度优先遍历,对每一个task_struct创建一个cr_task对象,再对每一个mm_struct创建一个cr_chkpt_proc_req_t,搞个图就容易理解这个结构了:
对进程进行Checkpoint最主要是把进程的内存dump到文件中去,针对于每个mm来创建一个proc_req是非常合理的,这个proc_req里面主要包含一些barrier,来保证共用同一个mm的进程在dump之前可以进行同步。
3. 调用cr_trigger_phase1来触发各个进程进行Checkpoint,这里面涉及到phase为PHASE1,PHASE2的proc_req,这些是在cr_checkpoint这个进程被Checkpoint的时候才会涉及到的,可以说BLCR考虑到了很多种情况,如果cr_checkpoint这个进程在Checkpoint另一个进程的时候,自己又被别人Checkpoint了,那这种情况也是需要处理了,虽然可能现实意义不大,但BLCR也是花了大篇幅的代码去实现了这种情况,这部分代码我就不写了,不过也确实花了不少时间才弄明白它的意图,要说一点的是普通进程的proc_req的phase都是0.
cr_trigger_phase1做的事情归纳起来就是遍历刚刚创建好的req->tasks这个链表,然后向每个cr_task发送64号信号,具体的task在收到这个信号之后就调用之前注册好的用户态的sighandler,这个handler里面做的工作就是再调用ioctl向kernel发送一个CR_OP_HAND_CHKPT,这个请求会在kernel中触发cr_dump_self,这个函数是对某一个进程进行Checkpoint的核心函数。
上面从Checkpoint的角度分析了BLCR的软件架构,没有写最核心的做dump操作的那部分代码,这部分代码完全是在内核态运行的,涉及到进程的各种状态,包括进程的PID/PGID,虚拟内存映射,打开的文件,寄存器的状态,credentials,timers,信号状态等等。
要提一下的是昨天淘宝内核组的炳天大神给我看了一篇LWN上的文章,http://lwn.net/Articles/525675/,这个东西叫CRIU(CheckPoint/Restart in user space),是在用户态实现进程的Checkpoint/Restart,虽然是用户态的CR,但这种事情没有kernel的配合肯定是做不来的,我没有仔细去研究CRIU的实现机制,只是看了下简单的介绍,貌似是添加了一些相关的系统调用,否则这种事情单靠用户态程序肯定是办不到的,比方说PID做Checkpoint的时候有getpid()这样式syscall,可以得到进程的pid然后保存在文件里面就可以,但Restart的时候可没有setpid()这样的syscall可以用,当然CRIU和kernel进行通信也用到了/proc文件系统,相比之下BLCR和kernel通信完全只用了/proc文件系统,之后就把CR所有的工作都扔给内核去做了,在kernel中进行Checkpoint最核心的函数是cr_dump_self(),也就是进程通过ioctl向kernel发送CR_OP_HAND_CHKPT请求之后kernel对应的处理函数,接下来仔细讨论下这个函数。
对应于进程的每一个子进程或者线程都会执行这个cr_dump_self(),也就是每个task_struct对于一个cr_dump_self(),对于共享mm_struct的线程而言,这些函数只有其中一个dump mm即可,其它的只是保存一下进程的寄存器状态便可以了。
首先要获取将要dump到的文件的file struct
dest_filp = cr_loc_get(&req->dest, &shared); if (IS_ERR(dest_filp)) { return PTR_ERR(dest_filp); }
关于这个shared标志,意思就是多个进程是否共享这一个目标文件,cr_checkpoint提供了一个-d参数用来指定dump出来的文件可以放在这个目录下面,并且对每个进程都单独创建一个文件,这种时候shared就不是共享的了,每个进程对应的目标文件都是相互独立的,也就无须进行同步了,同时我也发现这个-d参数虽然有,但却标志着unimplemented,也就是说我前面说的这几句话都是废话,这个功能BLCR虽然打算提供但尚未实现,默认只能将所有进程的信息都dump到同一个文件中,shared标志为1,这时候在完成初始化之后在dump进程数据的时候就需要加锁了。
接下来BLCR把除SIGKILL之外的所有信号都block了(包括SIGSTOP),否则来个什么信号进程上下文又变了,那我们Checkpoint的数据就不可靠了。
初始化工作,给这个dump文件写一个file header,这个头写一个就够了,所有这些进程都去抢req->serial_mutex这个锁,谁抢到谁干这个事情:
down(&req->serial_mutex); if (!test_and_set_bit(0, &req->done_header)) { result = cr_save_file_header(req, dest_filp); if (result < 0) { req->result = result; } } up(&req->serial_mutex);
至于这个文件的内容就没什么好说的了,几个标志。
接下来做的工作是把共享同一个mm_struct的进程同步,保证大家在dump前停在同一个位置,这个同步就是用到了proc_req里面的barrier变量来完成的,涉及到kernel函数就是wait_event_interruptible()
// Synchronize to ensure all tasks in the current process have stopped running. once = cr_signal_predump_barrier(cr_task, /* block= */ 1); if (once < 0) { goto cleanup_unlocked; }
第一个wakeup的进程once返回1,这个返回值在暂停itimers的时候会用到,也是为了保证只有一个人把itimers暂停就可以了,谁先醒来谁就去暂停一下,其它人就不要动了。
// One task pauses the itimers if (once) { cr_pause_itimers(proc_req->itimers); // vmadump_barrier ensures this is written before reading }
接下来根据刚才提到的dump file的shared标志加锁,进入cr_do_dump()这个函数来dump进程的信息,几个共享mm_struct还是会去抢一个锁,谁先抢到谁就会成为这组进程的leader,只有leader进程才会去保存mm_struct相关的一些信息,其它的进程只是保存一下各自的寄存器信息就可以了。
接下来针对于这个proc_req(一个proc_req对应一个mm_struct)也可保存一个header,这个header主要内容是一共有几个线程在用这个mm_struct,线程的clone_flags是多少,也是几个task去抢一把锁,谁先抢到谁写这个东东。
接下来保存process linkage信息,进程的pgid,pgrp,tgid,sessionid等等这些信息。完了后进入cr_freeze_threads()这个函数,这个函数做很多dump工作,主要工作其实都是刚才选出来的那个leader来做的,其它不是leader的进程只是保存了下寄存器信息,leader做如下工作,下面就涉及到非常多的细节了,每一个都可以拿出来写一篇文章,有些太过于细节的东西我也没仔细看,就简单介绍一下:
1. 写一个头信息:
static struct vmadump_header header ={VMAD_MAGIC, VMAD_FMT_VERS, VMAD_ARCH, (LINUX_VERSION_CODE >> 16) & 0xFF, (LINUX_VERSION_CODE >> 8) & 0xFF, LINUX_VERSION_CODE & 0xFF };
2. 保存PID
3. 保存Credentials, cr_save_creds()
4. 保存cpu状态信息, vmadump_store_cpu()
5. 保存signal信息,所有的进程都会把sigblock和sigpending信息dump出来,但只有leader进程会把sigaction给dump出来。
6. 保存current->clear_child_id, current->personality
7. 保存memory信息,这部分信息是只有leader进程才会去保存的,vmadump这块是比较复杂的,主要是遍历mm_struct的vma,BLCR可以控制哪里vma可以被dump出来,cr_checkpoint提供了如下的参数:
Options to save optional portions of memory:
-save-exe
save the executable file.
-save-private
save private mapped files. (executables and libraries are mapped this way)
-save-shared
save shared mapped files. (System V IPC is mapped this way).
-save-all
save all of the above.
-save-none
save none of the above (the default).
首先非0匿名页是肯定需要保存的,其它的像可执行文件,mmap进来的库文件,和mmap的共享文件都是可以根据用户指定的checkpoint选项来dump的。
从cr_freeze_threads()出来后继续在cr_do_vmadump()这个函数往下跑,接下来做如下几件事情:
1. 保存fs信息(cr_save_fs_struct()),主要保存umask,root path和current path.
2. 保存mmap() table (cr_save_mmaps_maps())
3. 保存itimers() (cr_save_itimers())
4. 保存mmaped() pages (cr_save_mmaps_data())
5. 保存打开的文件 (cr_save_all_files())
以上每一步都可以展开写很多东西,细节太多了,也鉴于我本人水平有限,有些东西也可能理解的不对就暂时不往细里写了,有时间我把vmadump这块仔细整理下。
跑到这里对于某一个proc_req的dump基本就完了,再退回到上层函数写一个tailer就OK了,然后把停掉的itimers打开,再把block的信号给恢复过来就完成了。
可以说整个这个过程需要对内核有非常深入的理解才能完成,我这种小白只是拿过来别人的代码学习一下,也确实通过这个东西对kernel多了很多理解,过段时间有空了把Restart过程写一下,相比于Checkpoint,Restart会有很多技巧
建议继续学习:
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:levin 来源: basic coder
- 标签: Berkeley BLCR Checkpoint
- 发布时间:2012-12-07 23:54:02
- [67] Go Reflect 性能
- [65] Oracle MTS模式下 进程地址与会话信
- [62] 如何拿下简短的域名
- [60] 【社会化设计】自我(self)部分――欢迎区
- [60] android 开发入门
- [58] 图书馆的世界纪录
- [58] IOS安全–浅谈关于IOS加固的几种方法
- [54] 视觉调整-设计师 vs. 逻辑
- [48] 界面设计速成
- [47] 给自己的字体课(一)——英文字体基础