BLCR(Berkeley Lab Checkpoint/Restart)介绍及Checkpoint架构剖析

来源:互联网 发布:淘宝网男装毛衫 编辑:程序博客网 时间:2024/04/30 03:12

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 intcr_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的核心函数。

关于cr_dump_self这个函数所做的操作接下来找时间再写吧


转载自basic coder


0 0