diff --git a/contrib/criu-service-client/.gitignore b/contrib/criu-service-client/.gitignore new file mode 100644 index 000000000..7558a76f1 --- /dev/null +++ b/contrib/criu-service-client/.gitignore @@ -0,0 +1,6 @@ +criu-service-client +*.o +.install/ +criu-service.socket +criu-service.log +criu-service.pid diff --git a/contrib/criu-service-client/Makefile b/contrib/criu-service-client/Makefile new file mode 100644 index 000000000..e6971a6ed --- /dev/null +++ b/contrib/criu-service-client/Makefile @@ -0,0 +1,78 @@ +CC ?= gcc + +CFLAGS ?= -Wall -Wextra -O2 +LDFLAGS ?= +LDLIBS := -lcriu + +TARGET := criu-service-client +SRC := criu-service-client.c +OBJ := $(SRC:.c=.o) + +# By default, build libcriu from source into a local staging directory. +# Set LOCAL=0 to use the system-installed libcriu and criu instead: +# make LOCAL=0 +LOCAL ?= 1 + +INSTALL_DIR := $(CURDIR)/.install +ifeq ($(LOCAL),1) +CRIU_INCLUDE := $(INSTALL_DIR)/include +CRIU_LIB := $(INSTALL_DIR)/lib +CFLAGS += -I$(CRIU_INCLUDE) +LDFLAGS += -L$(CRIU_LIB) +endif + +all: $(TARGET) +.PHONY: all + +$(TARGET): $(OBJ) + $(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS) + +ifeq ($(LOCAL),1) +%.o: %.c | $(CRIU_INCLUDE) + $(CC) $(CFLAGS) -c -o $@ $< + +$(CRIU_LIB)/libcriu.so: + $(MAKE) -C ../../ install-lib \ + DESTDIR=$(INSTALL_DIR) PREFIX=/ LIBDIR=/lib \ + SKIP_PIP_INSTALL=1 + +$(CRIU_INCLUDE): $(CRIU_LIB)/libcriu.so + @: +else +%.o: %.c + $(CC) $(CFLAGS) -c -o $@ $< +endif + +ifeq ($(LOCAL),1) +CRIU_BIN := $(CURDIR)/../../criu/criu +else +CRIU_BIN := criu +endif + +CRIU_SERVICE_SOCKET ?= $(CURDIR)/criu-service.socket +CRIU_SERVICE_LOG ?= $(CURDIR)/criu-service.log +CRIU_SERVICE_PIDFILE ?= $(CURDIR)/criu-service.pid + +start-service: + $(CRIU_BIN) service --address $(CRIU_SERVICE_SOCKET) \ + --log-file $(CRIU_SERVICE_LOG) \ + --pidfile $(CRIU_SERVICE_PIDFILE) -d -v4 + @echo "CRIU service started on $(CRIU_SERVICE_SOCKET) (PID $$(cat $(CRIU_SERVICE_PIDFILE)))" +.PHONY: start-service + +stop-service: + @if [ -s $(CRIU_SERVICE_PIDFILE) ]; then \ + kill $$(cat $(CRIU_SERVICE_PIDFILE)) 2>/dev/null || true; \ + rm -f $(CRIU_SERVICE_PIDFILE); \ + else \ + echo "CRIU service not running (no pidfile at $(CRIU_SERVICE_PIDFILE))"; \ + fi +.PHONY: stop-service + +clean: + rm -f $(TARGET) $(OBJ) +.PHONY: clean + +distclean: clean + rm -rf $(INSTALL_DIR) +.PHONY: distclean diff --git a/contrib/criu-service-client/README.md b/contrib/criu-service-client/README.md new file mode 100644 index 000000000..2502a30e1 --- /dev/null +++ b/contrib/criu-service-client/README.md @@ -0,0 +1,104 @@ +# CRIU service client + +An example CRIU service client that uses `libcriu` to checkpoint and restore +processes. It demonstrates how to use the libcriu C API as an alternative to +invoking the `criu` command-line tool directly. + +The option names mirror those of the `criu` CLI so that switching between the +two is straightforward. + +## Building + +By default, `make` builds `libcriu` from the source tree so there is +no need to install anything first: + +``` +make +``` + +If you already have `libcriu` installed on your system (via the +`criu-devel` RPM or `libcriu-dev` DEB package), you can skip the +source build: + +``` +make LOCAL=0 +``` + +## Usage + +``` +criu-service-client [OPTIONS] +``` + +### Dump + +Checkpoint a running process into an image directory: + +``` +sudo criu-service-client dump -t -D /tmp/imgs -j -v4 +``` + +### Pre-dump + +Pre-dump captures only the modified memory pages while the process keeps +running. This reduces final dump downtime by transferring most memory ahead +of time. Use `--prev-images-dir` on subsequent pre-dumps or the final dump +to point to the previous image directory (relative to `-D`): + +``` +mkdir -p /tmp/imgs/1 /tmp/imgs/2 /tmp/imgs/3 + +sudo criu-service-client pre-dump -t -D /tmp/imgs/1 --track-mem -v4 +sudo criu-service-client pre-dump -t -D /tmp/imgs/2 --prev-images-dir ../1 --track-mem -v4 +sudo criu-service-client dump -t -D /tmp/imgs/3 --prev-images-dir ../2 --track-mem -j -v4 +``` + +### Restore + +Restore a process from a previously created checkpoint: + +``` +sudo criu-service-client restore -D /tmp/imgs -j -v4 +``` + +### Connecting to a CRIU service + +By default the client launches `criu` internally via libcriu. To talk to an +already-running `criu service` daemon, start the service first and then pass +`--service-address`: + +``` +sudo make start-service +sudo criu-service-client dump -t -D /tmp/imgs --service-address ./criu-service.socket +``` + +The `start-service` target daemonizes `criu service` with a pidfile. Stop +it with `make stop-service`. The socket, log, and pidfile paths are +configurable: + +``` +sudo make start-service CRIU_SERVICE_SOCKET=/var/run/criu.socket CRIU_SERVICE_LOG=/var/log/criu.log +sudo make stop-service +``` + +## Testing + +Run the test suite (requires root for dump/restore operations): + +``` +sudo make test +``` + +This runs three tests: + +- **run.sh** - iterative pre-dump, dump, and restore using a loop workload +- **run-shell-job.py** - dump and restore of a process with a controlling + terminal (`--shell-job`) +- **run-service.sh** - pre-dump, dump, and restore through a `criu service` + daemon (`--service-address`) + +To test against the system-installed CRIU instead of building from source: + +``` +sudo make LOCAL=0 test +``` diff --git a/contrib/criu-service-client/criu-service-client.c b/contrib/criu-service-client/criu-service-client.c new file mode 100644 index 000000000..2acc73101 --- /dev/null +++ b/contrib/criu-service-client/criu-service-client.c @@ -0,0 +1,384 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum action { + ACT_DUMP, + ACT_PRE_DUMP, + ACT_RESTORE +}; + +static struct option long_opts[] = { + { "tree", required_argument, 0, 't' }, + { "images-dir", required_argument, 0, 'D' }, + { "prev-images-dir", required_argument, 0, 7 }, + { "shell-job", no_argument, 0, 'j' }, + { "leave-running", no_argument, 0, 'R' }, + { "tcp-established", no_argument, 0, 1 }, + { "tcp-close", no_argument, 0, 6 }, + { "ext-unix-sk", no_argument, 0, 'x' }, + { "file-locks", no_argument, 0, 'l' }, + { "manage-cgroups", no_argument, 0, 2 }, + { "track-mem", no_argument, 0, 3 }, + { "auto-dedup", no_argument, 0, 4 }, + { "log-file", required_argument, 0, 'o' }, + { "verbosity", optional_argument, 0, 'v' }, + { "service-address", required_argument, 0, 5 }, + { "help", no_argument, 0, 'h' }, + { 0, 0, 0, 0 } +}; + +static void print_usage(FILE *out) +{ + fprintf(out, "Usage: criu-service-client [OPTIONS]\n\n"); + fprintf(out, "Options:\n"); + fprintf(out, " -t, --tree Target PID to checkpoint (dump/pre-dump only)\n"); + fprintf(out, " -D, --images-dir Directory to store/load checkpoint images\n"); + fprintf(out, " --prev-images-dir Path to parent images from a previous dump\n"); + fprintf(out, " -j, --shell-job Allow dumping processes with TTY\n"); + fprintf(out, " -R, --leave-running Leave the process running after dump\n"); + fprintf(out, " --tcp-established Dump established TCP connections\n"); + fprintf(out, " --tcp-close Close TCP connections on restore\n"); + fprintf(out, " -x, --ext-unix-sk Dump external Unix sockets\n"); + fprintf(out, " -l, --file-locks Dump file locks\n"); + fprintf(out, " --manage-cgroups Manage cgroups\n"); + fprintf(out, " --track-mem Track memory changes\n"); + fprintf(out, " --auto-dedup Deduplicate memory pages automatically\n"); + fprintf(out, " -o, --log-file Log file path (default: criu.log)\n"); + fprintf(out, " -v[N], --verbosity[=N] Log level 0-4 (default: 2); -v raises by 1\n"); + fprintf(out, " --service-address CRIU service Unix socket path\n"); + fprintf(out, " -h, --help Show this help message\n"); +} + +static void print_criu_error(const char *op, int ret) +{ + const char *msg; + + switch (ret) { + case -EBADE: + msg = "RPC returned failure (check the CRIU log)"; + break; + case -ECONNREFUSED: + msg = "Unable to connect to CRIU service"; + break; + case -ECOMM: + msg = "Unable to send/recv message to/from CRIU"; + break; + case -EINVAL: + msg = "CRIU rejected the request (bad sequence or unsupported request type)"; + break; + case -EBADMSG: + msg = "Unexpected response from CRIU (consider updating CRIU)"; + break; + default: + msg = "Unknown error"; + break; + } + + if (errno) + fprintf(stderr, "CRIU %s failed: %s (ret=%d, errno=%d: %s)\n", + op, msg, ret, errno, strerror(errno)); + else + fprintf(stderr, "CRIU %s failed: %s (ret=%d)\n", + op, msg, ret); +} + +static int parse_int(const char *str, const char *name, int *val) +{ + char *end; + long v; + + errno = 0; + v = strtol(str, &end, 10); + if (errno || *end != '\0' || end == str || v < INT_MIN || v > INT_MAX) { + fprintf(stderr, "Invalid value for %s: '%s'\n", name, str); + return -1; + } + + *val = (int)v; + return 0; +} + +int main(int argc, char *argv[]) +{ + enum action action; + int opt, option_index = 0; + int pid = -1; + const char *images_dir = NULL; + const char *prev_images_dir = NULL; + const char *log_file = "criu.log"; + int log_level = CRIU_LOG_WARN; + const char *service_socket = NULL; + int dirfd = -1; + int ret = 1; + + bool shell_job = false; + bool leave_running = false; + bool tcp_established = false; + bool tcp_close = false; + bool ext_unix_sk = false; + bool file_locks = false; + bool manage_cgroups = false; + bool track_mem = false; + bool auto_dedup = false; + + if (argc < 2) { + print_usage(stderr); + return 1; + } + + if (strcmp(argv[1], "dump") == 0) { + action = ACT_DUMP; + } else if (strcmp(argv[1], "pre-dump") == 0) { + action = ACT_PRE_DUMP; + } else if (strcmp(argv[1], "restore") == 0) { + action = ACT_RESTORE; + } else if (strcmp(argv[1], "help") == 0 || strcmp(argv[1], "--help") == 0) { + print_usage(stdout); + return 0; + } else { + fprintf(stderr, "Unknown command '%s'; expected dump, pre-dump, restore, or help\n", + argv[1]); + return 1; + } + + /* Skip the subcommand so getopt_long() only sees the options. */ + argc--; + argv++; + + while ((opt = getopt_long(argc, argv, "t:D:jRxlo:v::h", long_opts, + &option_index)) != -1) { + switch (opt) { + case 't': + if (parse_int(optarg, "tree", &pid)) + return 1; + if (pid <= 0) { + fprintf(stderr, "PID must be positive\n"); + return 1; + } + break; + case 'D': + images_dir = optarg; + break; + case 'j': + shell_job = true; + break; + case 'R': + leave_running = true; + break; + case 1: + tcp_established = true; + break; + case 6: + tcp_close = true; + break; + case 'x': + ext_unix_sk = true; + break; + case 'l': + file_locks = true; + break; + case 2: + manage_cgroups = true; + break; + case 3: + track_mem = true; + break; + case 4: + auto_dedup = true; + break; + case 7: + prev_images_dir = optarg; + break; + case 'o': + log_file = optarg; + break; + case 'v': + if (optarg) { + if (optarg[0] == 'v') + log_level += strlen(optarg) + 1; + else if (parse_int(optarg, "verbosity", &log_level)) + return 1; + } else { + log_level++; + } + break; + case 5: + service_socket = optarg; + break; + case 'h': + print_usage(stdout); + return 0; + default: + print_usage(stderr); + return 1; + } + } + + if (!images_dir) { + fprintf(stderr, "Images directory required (-D)\n"); + return 1; + } + + if (action != ACT_RESTORE && pid <= 0) { + fprintf(stderr, "%s requires -t \n", + action == ACT_DUMP ? "Dump" : "Pre-dump"); + return 1; + } + + if (log_level < CRIU_LOG_MSG || log_level > CRIU_LOG_DEBUG) { + fprintf(stderr, "Verbosity must be in the range 0-4\n"); + return 1; + } + + if (criu_init_opts() < 0) { + perror("criu_init_opts"); + return 1; + } + + if (service_socket && criu_set_service_address(service_socket)) { + perror("criu_set_service_address"); + goto out; + } + + dirfd = open(images_dir, O_RDONLY | O_DIRECTORY); + if (dirfd < 0) { + fprintf(stderr, "Failed to open images dir '%s': %s\n", + images_dir, strerror(errno)); + goto out; + } + + criu_set_images_dir_fd(dirfd); + if (prev_images_dir && criu_set_parent_images(prev_images_dir)) { + perror("criu_set_parent_images"); + goto out; + } + if (criu_set_log_file(log_file)) { + perror("criu_set_log_file"); + goto out; + } + criu_set_log_level(log_level); + + if (shell_job) + criu_set_shell_job(true); + if (leave_running) + criu_set_leave_running(true); + if (tcp_established) + criu_set_tcp_established(true); + if (tcp_close) + criu_set_tcp_close(true); + if (ext_unix_sk) + criu_set_ext_unix_sk(true); + if (file_locks) + criu_set_file_locks(true); + if (manage_cgroups) + criu_set_manage_cgroups(true); + if (track_mem) + criu_set_track_mem(true); + if (auto_dedup) + criu_set_auto_dedup(true); + + switch (action) { + case ACT_PRE_DUMP: + criu_set_pid(pid); + fprintf(stderr, "Pre-dumping PID %d to '%s'\n", pid, images_dir); + ret = criu_pre_dump(); + break; + case ACT_DUMP: + criu_set_pid(pid); + fprintf(stderr, "Dumping PID %d to '%s'\n", pid, images_dir); + ret = criu_dump(); + break; + case ACT_RESTORE: + fprintf(stderr, "Restoring from '%s'\n", images_dir); + ret = criu_restore_child(); + if (ret <= 0) + break; + + pid = ret; + ret = 0; + + /* + * When the restored process shares our session and + * stdin is a terminal, hand the terminal over to the + * restored process so it can read input and receive + * signals (Ctrl-C, etc.). We temporarily ignore + * SIGTTOU because tcsetpgrp() would otherwise stop + * this process since it is no longer in the foreground. + * + * Skip this when the restored process lives in a + * different session since tcsetpgrp() only works + * within the same session. + */ + if (shell_job && isatty(STDIN_FILENO) && + getsid(pid) == getsid(0)) { + struct sigaction sa, old_sa; + pid_t pgrp; + + pgrp = getpgid(pid); + if (pgrp < 0) { + perror("getpgid"); + ret = 1; + goto out; + } + + sa.sa_handler = SIG_IGN; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + if (sigaction(SIGTTOU, &sa, &old_sa) < 0) { + perror("sigaction"); + ret = 1; + goto out; + } + + if (tcsetpgrp(STDIN_FILENO, pgrp) < 0) + perror("tcsetpgrp"); + + sigaction(SIGTTOU, &old_sa, NULL); + } + + if (waitpid(pid, &ret, 0) < 0) { + perror("waitpid"); + ret = 1; + goto out; + } + + if (WIFEXITED(ret)) + ret = WEXITSTATUS(ret); + else if (WIFSIGNALED(ret)) + ret = 128 + WTERMSIG(ret); + else + ret = 1; + goto out; + } + + if (ret < 0) { + const char *names[] = { + [ACT_DUMP] = "dump", + [ACT_PRE_DUMP] = "pre-dump", + [ACT_RESTORE] = "restore", + }; + + print_criu_error(names[action], ret); + ret = 1; + goto out; + } + + ret = 0; +out: + if (dirfd >= 0) + close(dirfd); + return ret; +}