Tini 源码分析(1)
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;
}
}
}
解析命令行参数
// 解析命令行参数
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 这两个信号进行了忽略。
到此,此进程的信号就已经配置好了。
- 点赞
- 收藏
- 关注作者
评论(0)