"criu dump重要步骤的深入研究三"

  "criu checkpoint六"

Posted by Xu on June 12, 2017

CRIU,DUMP重要步骤解析

在之前的章节,我们分析了在dump的信息收集过程中的进程树信息收集和进程树id信息收集两个步骤,今天进入信息收集的第三个部分:对网络的锁操作

network_lock

network_lock():首先写好ip路由表配置文件conf,再切换到指定的net命名空间,然后新建一个管道pfd,并向pfd[1]写入配置文件,然后fork()子进程,设置好信号屏蔽字后,并将管道输出端pfd[0]重定向到标准的输入端,然后在该子进程下执行iptable-restore命令来根据conf配置文件设置iptable路由表,达到网络锁的目的

int network_lock(void)
{
	pr_info("Lock network\n");

	/* Each connection will be locked on dump */
	if  (!(root_ns_mask & CLONE_NEWNET))
		return 0;

	if (run_scripts(ACT_NET_LOCK))
		return -1;

	return network_lock_internal();
}

int network_lock_internal()
{
	char conf[] =	"*filter\n"
				":CRIU - [0:0]\n"
				"-I INPUT -j CRIU\n"
				"-I OUTPUT -j CRIU\n"
				"-A CRIU -m mark --mark " __stringify(SOCCR_MARK) " -j ACCEPT\n"
				"-A CRIU -j DROP\n"
				"COMMIT\n";//这个就是要写入ip路由表的配置信息
	int ret = 0, nsret;

	if (switch_ns(root_item->pid->real, &net_ns_desc, &nsret))
	//切换命名空间
		return -1;


	ret |= iptables_restore(false, conf, sizeof(conf) - 1);//恢复ip路由表
	if (kdat.ipv6)
		ret |= iptables_restore(true, conf, sizeof(conf) - 1);

	if (restore_ns(nsret, &net_ns_desc))//恢复命名空间
		ret = -1;

	return ret;
}

switch_ns():切换命名空间,打开指定命名空间文件得到命名空间文件描述符,然后依据命名空间文件描述符调用setns()设置指定命名空间

int switch_ns(int pid, struct ns_desc *nd, int *rst)
{
	int nsfd;
	int ret;

	nsfd = open_proc(pid, "ns/%s", nd->str);//打开指定pid命名空间文件信息,在这里就是net_ns_desc,网络命名空间。
	if (nsfd < 0)
		return -1;

	ret = switch_ns_by_fd(nsfd, nd, rst);//根据命名空间文件描述符切换命名空间

	close(nsfd);

	return ret;
}

int switch_ns_by_fd(int nsfd, struct ns_desc *nd, int *rst)
{
	int ret = -1;

	if (rst) {
		*rst = open_proc(PROC_SELF, "ns/%s", nd->str);//打开自身的配置文件下的命名空间文件
		if (*rst < 0)
			goto err_ns;
	}

	ret = setns(nsfd, nd->cflag);//setns()将调用的进程和一个特定的命名空间解除关系并将该进程和一个同类型的命名空间相关联,nsfd参数指明了关联的命名空间,其是指向了 /proc/PID/ns 目录下一个符号链接的文件描述符,可以通过打开这些符号链接指向的文件或者打开一个绑定到符号链接的文件来获得文件描述符
	if (ret < 0) {
		pr_perror("Can't setns %d/%s", nsfd, nd->str);
		goto err_set;
	}

	return 0;

err_set:
	if (rst)
		close(*rst);
err_ns:
	return -1;
}

iptables_restore():恢复ip路由表,新建管道,写入配置文件信息conf,调用cr_system新建子进程执行命令

static int iptables_restore(bool ipv6, char *buf, int size)
{
	int pfd[2], ret = -1;
	char *cmd4[] = {"iptables-restore",  "--noflush", NULL};//两个命令行字符串数组,iptables-restore的作用是通过STDIN(标准输入)上指定的数据来恢复IP Table。
由你的shell提供的I/O重定向功能从一个文件中读取。--noflush不删除表中以前的内容。
	char *cmd6[] = {"ip6tables-restore", "--noflush", NULL};
	char **cmd = ipv6 ? cmd6 : cmd4;//根据ipv6参数决定使用哪一行命令,我们只研究ipv4的命令行

	if (pipe(pfd) < 0) {//新建一个管道,在pfd[1]上写,在pfd[0]上读内容
		pr_perror("Unable to create pipe");
		return -1;
	}

	if (write(pfd[1], buf, size) < size) {//向pfd[1]写入buf,也就是conf的内容,在之后的pfd[0]可以读取]
		pr_perror("Unable to write iptables configugration");
		goto err;
	}
	close_safe(&pfd[1]);

	ret = cr_system(pfd[0], -1, -1, cmd[0], cmd, 0);
err:
	close_safe(&pfd[1]);
	close_safe(&pfd[0]);
	return ret;
}

int cr_system(int in, int out, int err, char *cmd, char *const argv[], unsigned flags)
{
	return cr_system_userns(in, out, err, cmd, argv, flags, -1);
}

cr_system_userns:在指定的用户空间创建子进程执行系统命令行并将pipe读取端pfd[0]重定向到标准输入端来根据conf配置文件恢复ip路由表。

int cr_system_userns(int in, int out, int err, char *cmd,
			char *const argv[], unsigned flags, int userns_pid)
{
	sigset_t blockmask, oldmask;
	int ret = -1, status;
	pid_t pid;

	sigemptyset(&blockmask);//sigemptyset用来将参数blockmask信号集初始化并清空
	sigaddset(&blockmask, SIGCHLD);//sigaddset将SIGCHLD信号加入到blockmask信号集中去
	if (sigprocmask(SIG_BLOCK, &blockmask, &oldmask) == -1) {
	//设置阻塞信号集,阻塞信号集包含了所有不能传递给当前进程的信号信息,将blockmask中的信号添加到阻塞信号集中,屏蔽这些信号
		pr_perror("Can not set mask of blocked signals");
		return -1;
	}

	pid = fork();
	if (pid == -1) {
		pr_perror("fork() failed");
		goto out;
	} else if (pid == 0) {
	    //创建的子进程
		if (userns_pid > 0) {
		      //切换该子进程的用户user_ns到指定的userns_pid命名空间下去
			if (switch_ns(userns_pid, &user_ns_desc, NULL))
				goto out_chld;
			if (setuid(0) || setgid(0)) {
				pr_perror("Unable to set uid or gid");
				goto out_chld;
			}
		}

		if (out < 0)
		//一个进程标准应该有三个fd文件描述符:标准输入和输出还有错误内容输出#define	STDIN_FILENO	0	/* Standard input.  */ #define	STDOUT_FILENO	1	/* Standard output.  */ #define	STDERR_FILENO	2	/* Standard error output.  */
			out = DUP_SAFE(log_get_fd(), out_chld);//调用dup()复制文件描述符,dup复制失败则跳转到out_chld执行
		if (err < 0)
			err = DUP_SAFE(log_get_fd(), out_chld);

		/*
		 * out, err, in should be a separate fds,
		 * because reopen_fd_as() closes an old fd
		 */
		if (err == out || err == in)
			err = DUP_SAFE(err, out_chld);

		if (out == in)
			out = DUP_SAFE(out, out_chld);

		if (move_fd_from(&out, STDIN_FILENO) ||
		    move_fd_from(&err, STDIN_FILENO))
			goto out_chld;

		if (in < 0) {
			close(STDIN_FILENO);
		} else {
			if (reopen_fd_as_nocheck(STDIN_FILENO, in))
			//调用dup2()将in,即pfd[0]管道读取端重定向到标准输入STDIN_FILENO
				goto out_chld;
		}

		if (move_fd_from(&err, STDOUT_FILENO))
			goto out_chld;

		if (reopen_fd_as_nocheck(STDOUT_FILENO, out))
			goto out_chld;

		if (reopen_fd_as_nocheck(STDERR_FILENO, err))
			goto out_chld;

		execvp(cmd, argv);//执行cmd命令,来恢复ip路由表,cmd为cmd[0],argv为cmd[0]之后的参数

		pr_perror("exec failed");
out_chld:
		_exit(1);
	}
//fork()的子进程结束调用
	while (1) {
		ret = waitpid(pid, &status, 0);//在父进程中,fork返回的pid的值为子进程的pid,在子进程中,返回的值为0,所以在此处父进程等待子进程的返回,并返回子进程的状态值status
		if (ret == -1) {
			pr_perror("waitpid() failed");
			goto out;
		}
//对子进程返回的状态进行解析
		if (WIFEXITED(status)) {
			if (!(flags & CRS_CAN_FAIL) && WEXITSTATUS(status))
				pr_err("exited, status=%d\n", WEXITSTATUS(status));
			break;
		} else if (WIFSIGNALED(status)) {
			pr_err("killed by signal %d: %s\n", WTERMSIG(status),
				strsignal(WTERMSIG(status)));
			break;
		} else if (WIFSTOPPED(status)) {
			pr_err("stopped by signal %d\n", WSTOPSIG(status));
		} else if (WIFCONTINUED(status)) {
			pr_err("continued\n");
		}
	}

	ret = status ? -1 : 0;
out:
	if (sigprocmask(SIG_SETMASK, &oldmask, NULL) == -1) {
		pr_perror("Can not unset mask of blocked signals");
		BUG();
	}

	return ret;
}