Tini 源码分析(1)

举报
东坡爱吃肘子 发表于 2022/11/12 13:17:47 2022/11/12
【摘要】 我会从程序在 main 函数中执行的顺序,来一步一步的分析源码。每一个函数我都会贴上源码,然后进行分析。我把 main 函数的大概流程画了出来,大家可以参考一下。文章太长发不了,只能分割成两章了,这是第一章。我注释过的代码文件也放在了附件中,有需要的可以自行下载。

Tini 源码分析(1)

(奇怪,我是一个写 Go 的,为什么要来分析 C 代码的项目啊!)

Tini 简介


Tini 是一个超轻量级的 init 进程管理器,被设计作为容器的 1 号进程。

Tini 只会做以下的事情:

  • 只生成一个子进程(这意味着 Tini 应该运行在容器中),并等待子进程退出
  • 收割僵尸进程
  • 执行信号转发

Tini 只能管理一个进程,容器的最佳实践一般都是一个容器即一个进程,因此 Tini 在容器化场景足够使用了。

Tini 的使用


如果你使用的是 Docker 1.13 或更高版本, Tini 已包含在 Docker 中。包括所以版本的 Docker CE。如果要启用 Tini,只需将 --init 参数传递给 Docker run 即可。

Tini 的优势

使用 Tini 有几个好处:

  • 它可以保护你应用免受僵尸进程的侵害,这可能会随着时间而推移,使你的整个系统缺少可分配的 PID(并使其不可用)。
  • 它确保***默认信号处理程序***适用于你运行的 Docker 镜像中的应用。例如,使用 Tini 可以让 SIGTERM 正确终止你的进程,即使没有显式的编写信号处理程序。
  • 它运行起来是完全透明的。没有 Tini 的 Docker 镜像和有 Tini 的Docker 镜像使用起来是没有区别的。

如果你想详细了解 Tini 的好处,可以查看:What is advantage of Tini?

Tini 源码分析


我会从程序在 main 函数中执行的顺序,来一步一步的分析源码。每一个函数我都会贴上源码,然后进行分析。我把 main 函数的大概流程画了出来,大家可以参考一下。文章太长发不了,只能分割成两章了,这是第一章。我注释过的代码文件也放在了附件中,有需要的可以自行下载。

int main(int argc, char *argv[]) {
    // 用于存放 子进程的pid
	pid_t child_pid;

	// 这些被传递到函数中,以获得一个exitcode。
	int child_exitcode = -1;  // 这不是有效的exitcode;让我们判断子进程是否已经退出。
	int parse_exitcode = 1;   // 默认情况下,如果解析失败,则返回1。

	/* 解析命令行参数 */
	char* (*child_args_ptr)[];
	int parse_args_ret = parse_args(argc, argv, &child_args_ptr, &parse_exitcode);
	if (parse_args_ret) {
		return parse_exitcode;
	}

	/* 解析环境 */
	if (parse_env()) {
		return 1;
	}

	/* 配置信号 */
	sigset_t parent_sigset, child_sigset;
	struct sigaction sigttin_action, sigttou_action;
	memset(&sigttin_action, 0, sizeof sigttin_action);
	memset(&sigttou_action, 0, sizeof sigttou_action);

	signal_configuration_t child_sigconf = {
		.sigmask_ptr = &child_sigset,
		.sigttin_action_ptr = &sigttin_action,
		.sigttou_action_ptr = &sigttou_action,
	};

	if (configure_signals(&parent_sigset, &child_sigconf)) {
		return 1;
	}

	/* 当父进程退出时,该进程上会触发的信号。 */
	if (parent_death_signal && prctl(PR_SET_PDEATHSIG, parent_death_signal)) {
		PRINT_FATAL("Failed to set up parent death signal");
		return 1;
	 }

#if HAS_SUBREAPER
	/* If available and requested, register as a subreaper */
	if (register_subreaper()) {
		return 1;
	};
#endif

	/* 我们可以正常收割僵尸进程吗?如果没有,警告。 */
	reaper_check();

	/* 创建子进程 */
	int spawn_ret = spawn(&child_sigconf, *child_args_ptr, &child_pid);
	if (spawn_ret) {
		return spawn_ret;
	}
	free(child_args_ptr);

	while (1) {
		/* 等待信号并转发信号 */
		if (wait_and_forward_signal(&parent_sigset, child_pid)) {
			return 1;
		}

		/* 现在,收割僵尸进程 */
		if (reap_zombies(child_pid, &child_exitcode)) {
			return 1;
		}

		if (child_exitcode != -1) {
			PRINT_TRACE("Exiting: child has exited");
			return child_exitcode;
		}
	}
}

Tini 源码分析

解析命令行参数



// 解析命令行参数
int parse_args(const int argc, char* const argv[], char* (**child_args_ptr_ptr)[], int* const parse_fail_exitcode_ptr) {
	char* name = argv[0];

  // 如果 --version 是 *唯一* 提供的参数,则输出版本信息。
	if (argc == 2 && strcmp("--version", argv[1]) == 0) {
		*parse_fail_exitcode_ptr = 0;
		fprintf(stdout, "%s\n", TINI_VERSION_STRING);
		return 1;
	}

#ifndef TINI_MINIMAL
	int c;
	while ((c = getopt(argc, argv, OPT_STRING)) != -1) {
		switch (c) {

      // 打印帮助信息并退出
			case 'h':
				print_usage(name, stdout);
				*parse_fail_exitcode_ptr = 0;
				return 1;
#if HAS_SUBREAPER
			case 's':
				subreaper++;
				break;
#endif
      //设置父进程死亡时会触发的信号
			case 'p':
				if (set_pdeathsig(optarg)) {
					PRINT_FATAL("Not a valid option for -p: %s", optarg);
					*parse_fail_exitcode_ptr = 1;
					return 1;
				}
				break;

			case 'v':
				verbosity++;
				break;

			case 'w':
				warn_on_reap++;
				break;

      // 打开 kill 进程组选项,可以将 kill 发送给 进程组,而不是单个进程
			case 'g':
				kill_process_group++;
				break;

      // 添加期望标识
			case 'e':
				if (add_expect_status(optarg)) {
					PRINT_FATAL("Not a valid option for -e: %s", optarg);
					*parse_fail_exitcode_ptr = 1;
					return 1;
				}
				break;

      // 打印许可证并退出
			case 'l':
				print_license(stdout);
				*parse_fail_exitcode_ptr = 0;
				return 1;

      // 参数中包含无效的选项字符,打印帮助信息在 错误信息中 并退出。
			case '?':
				print_usage(name, stderr);
				return 1;
			default:
				/* 不应该发生 */
				return 1;
		}
	}
#endif

	*child_args_ptr_ptr = calloc(argc-optind+1, sizeof(char*));
	if (*child_args_ptr_ptr == NULL) {
		PRINT_FATAL("Failed to allocate memory for child args: '%s'", strerror(errno));
		return 1;
	}

	int i;
	for (i = 0; i < argc - optind; i++) {
		(**child_args_ptr_ptr)[i] = argv[optind+i];
	}
	(**child_args_ptr_ptr)[i] = NULL;

	if (i == 0) {
		/* 用户忘记提供参数! */
		print_usage(name, stderr);
		return 1;
	}

	return 0;
}

其中这一段函数

// 如果 --version 是 *唯一* 提供的参数,则输出版本信息。
	if (argc == 2 && strcmp("--version", argv[1]) == 0) {
		*parse_fail_exitcode_ptr = 0;
		fprintf(stdout, "%s\n", TINI_VERSION_STRING);
		return 1;
	}

就如注释所说,如果参数仅仅只有 --version 参数的话,就输出版本信息,然后退出。

argc 和 argv[] 是 main 函数的入参,int main(int argc, char *argv[])

其中 argc 表示的是传入参数的个数,

char* argv[],是字符串数组,用来存放指向的字符串参数的指针数组,每一个元素指向一个参数。

argv[0]:指向程序运行的全路径名;

argv[1]:指向执行程序名后的第一个字符串 ,表示真正传入的第一个参数;

argv[n]:指向执行程序名后的第n个字符串 ,表示传入的第n个参数

规定:argv[argc] 为 NULL,表示参数的结尾。

int strcmp(const char *str1, const char *str2) 把 str1 所指向的字符串和 str2 所指向的字符串进行比较。如果返回值小于 0,则表示 str1 小于 str2。 如果返回值大于 0,则表示 str1 大于 str2。 如果返回值等于 0,则表示 str1 等于 str2。

所以, if 条件中 argc == 2 则表示 只有一个参数,argv[1] 则表示第一个参数,strcmp(”–version, argv[1]) == 0” 就表示 唯一的那个参数是 ”–version ”。

这段代码

#ifndef TINI_MINIMAL
	int c;
	while ((c = getopt(argc, argv, OPT_STRING)) != -1) {
		switch (c) {

            // 打印帮助信息并退出
			case 'h':
				print_usage(name, stdout);
				*parse_fail_exitcode_ptr = 0;
				return 1;
#if HAS_SUBREAPER
			case 's':
				subreaper++;
				break;
#endif
            //设置父进程死亡时会触发的信号
			case 'p':
				if (set_pdeathsig(optarg)) {
					PRINT_FATAL("Not a valid option for -p: %s", optarg);
					*parse_fail_exitcode_ptr = 1;
					return 1;
				}
				break;

			case 'v':
				verbosity++;
				break;

			case 'w':
				warn_on_reap++;
				break;

            // 打开 kill 进程组选项,可以将 kill 发送给 进程组,而不是单个进程
			case 'g':
				kill_process_group++;
				break;

            // 添加期望标识
			case 'e':
				if (add_expect_status(optarg)) {
					PRINT_FATAL("Not a valid option for -e: %s", optarg);
					*parse_fail_exitcode_ptr = 1;
					return 1;
				}
				break;

            // 打印许可证并退出
			case 'l':
				print_license(stdout);
				*parse_fail_exitcode_ptr = 0;
				return 1;

            // 参数中包含无效的选项字符,打印帮助信息在 错误信息中 并退出。
			case '?':
				print_usage(name, stderr);
				return 1;
			default:
				/* 不应该发生 */
				return 1;
		}
	}
#endif

ifndef XXX endif 和 if XXX endif 是成对存在的,这是宏定义的一种,它可以根据是否已经定义了一个变量来进行分支选择。和我们用的if-else一样 ifndef 后面的常量如果没有被定义过,则运行 ifndef 下面的语句,否则则运行 else 下面的代码,没有则忽略, endif 表示结束。if-endif 就是 if 后面的条件非零则运行 if 下面的代码 endif 表结束。

int getopt(int argc, char * const argv[], const char *optstring) 方法是用来分析命令行参数的。

这个函数调用一次,返回一个选项。 在命令行选项参数再也检查不到optstring中包含的选项时,返回-1,同时optind储存第一个不包含选项的命令行参数。

getopt 参数说明:

  • argc:通常由 main 函数直接传入,表示参数的数量
  • argv:通常也由 main 函数直接传入,表示参数的字符串变量数组
  • optstring:一个包含正确的参数选项字符串,用于参数的解析。例如 “abc:”,其中 -a,-b 就表示两个普通选项,-c 表示一个必须有参数的选项,因为它后面有一个冒号。这里的 optstring 定义的是 #define OPT_STRING "p:hvwgle

外部变量说明:

  • optarg:如果某个选项有参数,这包含当前选项的参数字符串
  • optind:argv 的当前索引值
  • opterr:正常运行状态下为 0。非零时表示存在无效选项或者缺少选项参数,并输出其错误信息
  • optopt:当发现无效选项字符时,即 getopt() 方法返回 ? 字符,optopt 中包含的就是发现的无效选项字符。

所以 while ((c = getopt(argc, argv, OPT_STRING)) != -1) 表示取到符合的参数就执行一次循环,下面的 stitch 就是判断 c 的值了。

  • 如果选项是 h,就打印帮助信息并退出,print_usage(name, stdout);就是打印信息的函数,里面基本上都是 println(),就不展示了,有兴趣可以点击这里
  • 如果 HAS_SUBREAPER的值不为零,且选项为 s,则subreaper++,会在register_subreaper ()函数中将此进程设置为子收割者。
  • 如果选项是 p,则后面必须跟一个参数,调用set_pdeathsig() 将后面的参数设置为父进程死亡时会触发的信号。失败则设置错误码并退出。
  • 如果选项是 v,则 verbosity++ ,verbosity 表示打印信息冗余程度,verbosity的值越高,输出的信息就越详细。该值:大于 0 则打印 warning ; 大于 1 还会打印 info 信息; 大于 2 还会打印 debug 信息;大于 3 还会再打印 trace 信息。
  • 如果选项是 w,则warn_on_reap++ ,他会在后面让僵尸进程被收割时,可以打印一条警告。
  • 如果选项是 g,则kill_process_group++ ,他会在后面,选择将信号 kill 给一个进程组,而不是单个进程。
  • 如果选项是 e,则后面必须跟一个参数,这个参数是退出码,可以将这个退出码映射为 0。
  • 如果选项是 l,则print_license(stdout) ,打印许可证信息并退出。
  • 如果选项是其他字符,则参数中包含无效的选项字符,打印帮助信息在错误信息中并退出。

如果 TINI_MINIMAL 这个常量被定义了,则执行下面的函数:

*child_args_ptr_ptr = calloc(argc-optind+1, sizeof(char*));
	if (*child_args_ptr_ptr == NULL) {
		PRINT_FATAL("Failed to allocate memory for child args: '%s'", strerror(errno));
		return 1;
	}

	int i;
	for (i = 0; i < argc - optind; i++) {
		(**child_args_ptr_ptr)[i] = argv[optind+i];
	}
	(**child_args_ptr_ptr)[i] = NULL;

	if (i == 0) {
		/* 用户忘记提供参数! */
		print_usage(name, stderr);
		return 1;
	}

	return 0;

这里面的函数都是前面解释过的了,这段函数的含义就是把 main函数传入的参数,赋值给 **child_args_ptr_ptr[]。

解析环境


int parse_env() {
#if HAS_SUBREAPER
	if (getenv(SUBREAPER_ENV_VAR) != NULL) {
		subreaper++;
	}
#endif

    // 如果配置的环境变量 TINI_KILL_PROCESS_GROUP,那么就让 发送的信号转发给进程组
	if (getenv(KILL_PROCESS_GROUP_GROUP_ENV_VAR) != NULL) {
		kill_process_group++;
	}

    // 如果配置了 TINI_VERBOSITY,那么就将 值 赋给 verbosity,
    // verbosity 表示打印信息冗余程度,该值:大于 0 则打印 warning ; 大于 1 还会打印 info 信息; 大于 2 还会打印 debug 信息;大于 3 还会再打印 trace 信息
	char* env_verbosity = getenv(VERBOSITY_ENV_VAR);
	if (env_verbosity != NULL) {
		verbosity = atoi(env_verbosity);
	}

	return 0;
}

这里面的参数前面也说过,这段函数的含义就是,如果设置了

  • SUBREAPER_ENV_VAR ,就让此进程成为子进程收割者
  • KILL_PROCESS_GROUP_GROUP_ENV_VAR ,就让 kill 信号发送给整个进程组,而不是单个进程
  • VERBOSITY_ENV_VAR ,就将该值设置为 verbosity 的值,该值越大,输出信息越详细,最高为 3

配置信号


// 配置信号
int configure_signals(sigset_t* const parent_sigset_ptr, const signal_configuration_t* const sigconf_ptr) {
	/* 屏蔽所有要由主循环收集的信号 */
	if (sigfillset(parent_sigset_ptr)) {
		PRINT_FATAL("sigfillset failed: '%s'", strerror(errno));
		return 1;
	}

	// 这些不应该被主循环收集
	uint i;
	int signals_for_tini[] = {SIGFPE, SIGILL, SIGSEGV, SIGBUS, SIGABRT, SIGTRAP, SIGSYS, SIGTTIN, SIGTTOU};
	for (i = 0; i < ARRAY_LEN(signals_for_tini); i++) {
		if (sigdelset(parent_sigset_ptr, signals_for_tini[i])) {
			PRINT_FATAL("sigdelset failed: '%i'", signals_for_tini[i]);
			return 1;
		}
	}

	if (sigprocmask(SIG_SETMASK, parent_sigset_ptr, sigconf_ptr->sigmask_ptr)) {
		PRINT_FATAL("sigprocmask failed: '%s'", strerror(errno));
		return 1;
	}
    

    // 分别处理SIGTTIN和SIGTTOU。由于Tini使子进程组成为前台进程组,Tini有可能最终不能控制tty。
    // 如果在tty上设置了TOSTOP,这可能会阻止Tini写调试信息。我们不希望这样。忽略这些信号。

	struct sigaction ign_action;
	memset(&ign_action, 0, sizeof ign_action);

	ign_action.sa_handler = SIG_IGN;
	sigemptyset(&ign_action.sa_mask);

	if (sigaction(SIGTTIN, &ign_action, sigconf_ptr->sigttin_action_ptr)) {
		PRINT_FATAL("Failed to ignore SIGTTIN");
		return 1;
	}

	if (sigaction(SIGTTOU, &ign_action, sigconf_ptr->sigttou_action_ptr)) {
		PRINT_FATAL("Failed to ignore SIGTTOU");
		return 1;
	}

	return 0;
}
  • int sigfillset(sigset_t *set) 该函数的作用是把信号集初始化包含所有已定义的信号。
  • int sigdelset(sigset_t *set, int signo) 该函数的作用是把信号signo从信号集set中删除,成功时返回0,失败时返回-1。
  • int sigpromask(int how, const sigset_t *set, sigset_t *oset) 该函数可以根据参数指定的方法修改进程的信号屏蔽字。新的信号屏蔽字由参数set(非空)指定,而原先的信号屏蔽字将保存在oset(非空)中。如果set为空,则how没有意义,但此时调用该函数,如果oset不为空,则把当前信号屏蔽字保存到oset中。如果sigpromask成功完成返回0,如果how取值无效返回-1,并设置errno为EINVAL。how 的不同取值及操作如下所示:
    • SIG_BLOCK :把参数 set 中的信号添加到信号字中
    • SIG_SETMASK :把信号屏蔽字设置为参数 set 中的信号
    • SIG_UNBLOCK :从信号屏蔽字中删除参数 set 中的信号
  • int sigemptyset(sigset_t *set) 该函数的作用是将信号集初始化为空。
  • int sigaction(int sig, const struct sigaction *act, struct sigaction *oact) 该函数与signal()函数一样,用于设置与信号sig关联的动作,而oact如果不是空指针的话,就用它来保存原先对该信号的动作的位置,act则用于设置指定信号的动作。

知道上述函数的意义之后,我们就可以分析代码了。

if (sigfillset(parent_sigset_ptr)) {
		PRINT_FATAL("sigfillset failed: '%s'", strerror(errno));
		return 1;
	}

这一段代码将 父进程信号集初始化包含所有已定义的信号。

// 这些不应该被主循环收集
	uint i;
	int signals_for_tini[] = {SIGFPE, SIGILL, SIGSEGV, SIGBUS, SIGABRT, SIGTRAP, SIGSYS, SIGTTIN, SIGTTOU};
	for (i = 0; i < ARRAY_LEN(signals_for_tini); i++) {
		if (sigdelset(parent_sigset_ptr, signals_for_tini[i])) {
			PRINT_FATAL("sigdelset failed: '%i'", signals_for_tini[i]);
			return 1;
		}
	}

这一段代码将 SIGFPE, SIGILL, SIGSEGV, SIGBUS, SIGABRT, SIGTRAP, SIGSYS, SIGTTIN, SIGTTOU 这些信号从父进程信号集中删除。使其不被父进程收集。

if (sigprocmask(SIG_SETMASK, parent_sigset_ptr, sigconf_ptr->sigmask_ptr)) {
		PRINT_FATAL("sigprocmask failed: '%s'", strerror(errno));
		return 1;
	}

这段代码将父进程信号集设置为此进程的信号屏蔽集。

struct sigaction ign_action;
	memset(&ign_action, 0, sizeof ign_action);

	ign_action.sa_handler = SIG_IGN;
	sigemptyset(&ign_action.sa_mask);

	if (sigaction(SIGTTIN, &ign_action, sigconf_ptr->sigttin_action_ptr)) {
		PRINT_FATAL("Failed to ignore SIGTTIN");
		return 1;
	}

	if (sigaction(SIGTTOU, &ign_action, sigconf_ptr->sigttou_action_ptr)) {
		PRINT_FATAL("Failed to ignore SIGTTOU");
		return 1;
	}

	return 0;

这段代码则是将 SIGTTIN 和 SIGTTOU 这两个信号进行了忽略。

到此,此进程的信号就已经配置好了。

    附件下载

  • tini.zip 6.74KB 下载次数:0
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。