October 20, 2018

libcriu でプロセスの checkpoint と restore をやってみる

やはり俺の青春ラブコメはまちがっている。(13)11月刊行予定、了解!


そういえば libcriu って使ったことないなぁと思ったのでメモがてら…

CRIU

CRIU(stands for Checkpoint and Restore in Userspace) とは Linux のプロセスを checkpoint / restore できるツールである。
checkpoint とは実行中のプロセスの状態をファイルに保存することである。対し、 restore とはその保存されたプロセスを再開することを指す。

CRIU については以下の記事が参考になる。

CRIU を使うことで以下の技術が可能となる。

  • コンテナの Live Migration
  • 起動が遅いプロセスの高速化(起動した段階でチェックポイントしておく)

CRIU を試す

CRIU は CLI コマンドとして提供されているので、サクッと試すことができる。

checkpoint

vagrant@ubuntu-bionic:~/shared/criu$ cat loop.sh
#!/bin/bash

for i in `seq 1000`
do
  echo $i
  sleep 1
done

vagrant@ubuntu-bionic:~/shared/criu$ ./loop.sh
1
2
3
4
5
6
7
8
9
vagrant@ubuntu-bionic:~/shared/criu/out$ ps aux| grep loop.sh
vagrant   8679  0.0  0.3  13444  3172 pts/1    S+   16:05   0:00 /bin/bash ./loop.sh
vagrant@ubuntu-bionic:~/shared/criu/out$ sudo criu dump -t 8679 --shell-job -o ./dump.log
vagrant@ubuntu-bionic:~/shared/criu/out$ ls
cgroup.img     dump.log      fs-8679.img   ids-8700.img   mm-8700.img       pagemap-8711.img  stats-dump
core-8679.img  fdinfo-2.img  fs-8700.img   ids-8711.img   mm-8711.img       pages-1.img       tty-info.img
core-8700.img  fdinfo-3.img  fs-8711.img   inventory.img  pagemap-8679.img  pages-2.img
core-8711.img  files.img     ids-8679.img  mm-8679.img    pagemap-8700.img  pstree.img
vagrant@ubuntu-bionic:~/shared/criu/out$ ps aux| grep loop.sh
# プロセスは kill されている

vagrant@ubuntu-bionic:~/shared/criu/out$ file *
cgroup.img:       CRIU image file v1.1
core-8679.img:    CRIU image file v1.1
core-8700.img:    CRIU image file v1.1
core-8711.img:    CRIU image file v1.1
dump.log:         ASCII text
fdinfo-2.img:     CRIU image file v1.1
fdinfo-3.img:     CRIU image file v1.1
files.img:        CRIU image file v1.1
fs-8679.img:      CRIU image file v1.1
fs-8700.img:      CRIU image file v1.1
fs-8711.img:      CRIU image file v1.1
ids-8679.img:     CRIU image file v1.1
ids-8700.img:     CRIU image file v1.1
ids-8711.img:     CRIU image file v1.1
inventory.img:    CRIU inventory
mm-8679.img:      CRIU image file v1.1
mm-8700.img:      CRIU image file v1.1
mm-8711.img:      CRIU image file v1.1
pagemap-8679.img: CRIU image file v1.1
pagemap-8700.img: CRIU image file v1.1
pagemap-8711.img: CRIU image file v1.1
pages-1.img:      ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, missing section headers
pages-2.img:      ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, stripped
pstree.img:       CRIU image file v1.1
stats-dump:       CRIU service file
tty-info.img:     CRIU image file v1.1

イメージのファイルフォーマットについては下記に記述がある。

restore

vagrant@ubuntu-bionic:~/shared/criu/out$ sudo criu restore --shell-job
10
11
12
13

libcriu

CRIU を扱うための API が libcriu として用意されている。

CRIU は RPC でやり取りをしており、libcriu はそれを扱うための Wrapper といったところか。

ここでは簡単に checkpoint / restore を行うプログラムを書いてみる。

checkpoint

まずは checkpoint を行うプログラム。

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <criu.h>

int main(int argc,char *argv[]) {
  int ret, pid;

  pid = atoi(argv[1]);

  criu_init_opts();
  if (criu_check() == -1) {
    exit(errno);
  }

  criu_set_pid(pid);
  criu_set_shell_job(true);

  criu_set_log_file("dump.log");
  criu_set_log_level(4);

  int fd = open("./out/", O_DIRECTORY);
  criu_set_images_dir_fd(fd);

  printf("Start dump\n");
  ret = criu_dump();

  if (ret < 0) {
    exit(errno);
  }

  printf("Dumped!\n");

  return 0;
}

上記のコードを実行すると ./out/ 配下にイメージが保存される。

vagrant@ubuntu-bionic:~/shared/criu$ gcc -I/usr/local/include/criu dump.c -lcriu
vagrant@ubuntu-bionic:~/shared/criu$ ps aux | grep loop
vagrant  27383  0.0  0.3  13444  3216 pts/1    S+   03:47   0:00 /bin/bash ./loop.sh
vagrant@ubuntu-bionic:~/shared/criu$ sudo ./dump 27383
Warn  (criu/kerndat.c:808): Stale /run/criu.kdat file
Warn  (criu/net.c:2780): Unable to get tun network namespace
Looks good.
Start dump
Dumped!
vagrant@ubuntu-bionic:~/shared/criu$ ls out/
cgroup.img      fdinfo-2.img  fs-27431.img   mm-27383.img       pages-1.img  stats-dump
core-27383.img  fdinfo-3.img  ids-27383.img  mm-27431.img       pages-2.img  tty-info.img
core-27431.img  files.img     ids-27431.img  pagemap-27383.img  pstree.img
dump.log        fs-27383.img  inventory.img  pagemap-27431.img  seccomp.img

criu_init_opts

criu_init_opts で初期化を行う。
この関数は criu_check 以外の libcriu 内の関数を呼び出す前に実行しなければいけない。

criu_set_shell_job

今回のように checkpoint 対象のアプリ (loop.sh) がシェルから直接呼ばれている場合はシェルとは異なった session id と tty を使用するのでチェックポイントはできない。
そこで、CRIU ではセッションリーダーと pty のマスタを無視して checkpoint を行う。そして restore するときに既存の tty に貼り直すなどをしているらしい。
この操作を criu_set_shell_jobtrue を渡すだけでやってくれる。魔法だ…
これは CLI における --shell-job に相当する。

なので例えば setsid ./loop.sh < /dev/null &> test.log & のようにして新しいセッションで tty を使用しないデーモンプロセスとして動作させれば、このオプションは使用しなくてもよい。

restore

では先程 checkpoint したプロセスを restore してみる。

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <criu.h>

int main(int argc,char *argv[]) {
  int pid;
  criu_init_opts();
  if (criu_check() == -1) {
    exit(errno);
  }

  criu_set_shell_job(true);

  criu_set_log_file("restore.log");
  criu_set_log_level(4);

  int fd = open("./out/", O_DIRECTORY);
  criu_set_images_dir_fd(fd);

  printf("Start restore\n");
  pid = criu_restore_child();

  if (pid < 0) {
    exit(errno);
  }

  printf("Restored pid %d\n", pid);

  return 0;
}

上記のコードを実行すると ./out 以下のプロセスイメージを restore される。

vagrant@ubuntu-bionic:~/shared/criu$ sudo ./restore
Looks good.
Start restore
Restored pid 27383
vagrant@ubuntu-bionic:~/shared/criu$ 23
24
25
26
27
...
vagrant@ubuntu-bionic:~/shared/criu$ ps aux | grep loop
vagrant  27383  0.3  0.2  13444  2132 pts/2    S    04:09   0:00 /bin/bash ./loop.sh

TCP

TCP でコネクションが確立している場合でも C/R 可能。

Linux Kernel 3.5 で TCP_REPAIR というオプションが入ることで実現できるようになった。
connect() が呼ばれると強制的に ESTABLISHED になるし、 bind() が呼ばれると競合関係なしで与えられたアドレスで LISTEN を行う。
シーケンスについても TCP_REPAIR_QUEUETCP_QUEUE_SEQ が入り、いい感じにできるようになったらしい。 CRIU ではこれらを用いてソケット情報を保存、その際に netfilter で RST を送信しないようだとかをゴニョゴニョやってくれて、再開できるようになる。ヤバイ。

以下の動画では動画をストリーミング配信しているコンテナを CRIU によって Live Migration しつつ、ストリーミングが一切途切れない様子を観ることができる。

所感

この技術は Forensics や マルウェア解析に活用できるなーと考えていたら既に参考実装付きで USENIX で発表されていた。

このエントリーをはてなブックマークに追加

© Kouhei Morita 2018