Tini 源码分析(2)

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

Tini 源码分析(2)

Tini 源码分析


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

Tini 源码分析

父进程死亡,此进程收到的信号


if (parent_death_signal && prctl(PR_SET_PDEATHSIG, parent_death_signal)) {
		PRINT_FATAL("Failed to set up parent death signal");
		return 1;
	 }

这段代码就比较简单了,int prctl ( int option,unsigned long arg2 ) 这个系统调用指令是为进程制定而设计的,明确的选择取决于option,PR_SET_PDEATHSIG :arg2作为处理器信号pdeath被输入,正如其名,如果父进程不能再用,进程接受这个信号。

检测是否能收割


void reaper_check () {
	/* 检查我们是否能正确收割僵尸进程 */
#if HAS_SUBREAPER
	int bit = 0;
#endif

	if (getpid() == 1) {
		return;
	}

#if HAS_SUBREAPER
	if (prctl(PR_GET_CHILD_SUBREAPER, &bit)) {
		PRINT_DEBUG("Failed to read child subreaper attribute: %s", strerror(errno));
	} else if (bit == 1) {
		return;
	}
#endif

	PRINT_WARNING(reaper_warning);
}

此函数检测自己是否能正确的收割僵尸进程的。判断条件1,如果此进程为 init 进程,则可以收割僵尸进程,直接返回;如果不满足,第二个判断条件,是否设置了参数或者环境变量,使此进程开启子进程收割者的命令,如果可以则直接返回;否则,打印警告信息,该进程无法正常收割僵尸进程。

创建子进程


// 创建子进程
int spawn(const signal_configuration_t* const sigconf_ptr, char* const argv[], int* const child_pid_ptr) {
	pid_t pid;

	// TODO: check if tini was a foreground process to begin with (it's not OK to "steal" the foreground!")

	pid = fork();
	if (pid < 0) {
		PRINT_FATAL("fork failed: %s", strerror(errno));
		return 1;
	} else if (pid == 0) {

		// 把子进程放在一个进程组中,如果有tty的话,让它成为前台进程。
		if (isolate_child()) {
			return 1;
		}

		// 将所有的信号处理程序恢复到我们 触碰 它们之前的样子。
		if (restore_signals(sigconf_ptr)) {
			return 1;
		}

		execvp(argv[0], argv);

		// execvp只会在出错时返回,所以要确保我们检查errno,并为我们遇到的错误提供正确的返回状态。
		// See: http://www.tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF
		int status = 1;
		switch (errno) {
			case ENOENT:
				status = 127;
				break;
			case EACCES:
				status = 126;
				break;
		}
		PRINT_FATAL("exec %s failed: %s", argv[0], strerror(errno));
		return status;
	} else {
		// Parent
		PRINT_INFO("Spawned child process '%s' with pid '%i'", argv[0], pid);
		*child_pid_ptr = pid;
		return 0;
	}
}

这里面主进程 fork() 了一个子进程出来。后面则是主进程和子进程的不同路线了,如果 pid 小于 0 则表示进程创建失败,打印错误信息并返回。

如果 pid == 0 则表示此进程为子进程,会进入以下的代码块:

else if (pid == 0) {

		// 把子进程放在一个进程组中,如果有tty的话,让它成为前台进程。
		if (isolate_child()) {
			return 1;
		}

		// 将所有的信号处理程序恢复到我们 触碰 它们之前的样子。
		if (restore_signals(sigconf_ptr)) {
			return 1;
		}

		execvp(argv[0], argv);

		// execvp只会在出错时返回,所以要确保我们检查errno,并为我们遇到的错误提供正确的返回状态。
		// See: http://www.tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF
		int status = 1;
		switch (errno) {
			case ENOENT:
				status = 127;
				break;
			case EACCES:
				status = 126;
				break;
		}
		PRINT_FATAL("exec %s failed: %s", argv[0], strerror(errno));
		return status;

首先是 isolate_child() 这个函数:

int isolate_child() {
	// 把子进程放到一个新的进程组中。 setpgid(0, 0) 与 setpgid(getpid(), getpid()) 等价
	if (setpgid(0, 0) < 0) {
		PRINT_FATAL("setpgid failed: %s", strerror(errno));
		return 1;
	}

	// 如果有一个tty,把它分配给这个新的进程组。
	// 我们可以在子进程中做到这一点,因为我们正在阻止SIGTTIN / SIGTTOU。

	// 如果Tini调用Tini,在子进程中这样做可以避免出现竞争条件的情况
	// 在子进程中这样做可以避免出现竞争条件的情况进程组,而实际的子进程最终在后台!)。

    // getpgrp()用来取得目前进程所属的组识别码。. 此函数相当于调用 getpgid (0);返回目前进程所属的组识别码。.
    // tcsetpgrp 函数---设置前台进程组ID
    // int tcsetpgrp(int fd, pid_t pgrpid);
    // 函数功能:使用 pgrpid 设置前台进程组ID,fd 必须引用该会话的控制终端,0 代表当前正在使用的终端
    // 返回值:成功返回 0,出错返回 -1
	if (tcsetpgrp(STDIN_FILENO, getpgrp())) {
		if (errno == ENOTTY) {
			PRINT_DEBUG("tcsetpgrp failed: no tty (ok to proceed)");
		} else if (errno == ENXIO) {
			// can occur on lx-branded zones
			PRINT_DEBUG("tcsetpgrp failed: no such device (ok to proceed)");
		} else {
			PRINT_FATAL("tcsetpgrp failed: %s", strerror(errno));
			return 1;
		}
	}

	return 0;
}

其中这段代码

if (setpgid(0, 0) < 0) {
		PRINT_FATAL("setpgid failed: %s", strerror(errno));
		return 1;
	}

int setpgid(pid_t pid,pid_t pgid);

函数作用:将pid进程的进程组ID设置成pgid,创建一个新进程组或加入一个已存在的进程组。

函数性质:

性质1:一个进程只能为自己或子进程设置进程组ID,不能设置其父进程的进程组ID。

性质2:if(pid == pgid),由pid指定的进程变成进程组长;即进程pid的进程组ID pgid=pid。

性质3:if(pid==0),将当前进程的pid作为进程组ID。

性质4:if(pgid==0),将pid作为进程组ID。

setpgid(0, 0) 与 setpgid(getpid(), getpid()) 等价

所以这段代码的含义就是把这个进程(子进程)放到一个新的进程组中,并设置为这个进程组的组长,进程组号就是这个进程的 pid 。

这段代码

// 如果有一个tty,把它分配给这个新的进程组。
	// 我们可以在子进程中做到这一点,因为我们正在阻止SIGTTIN / SIGTTOU。

	// 如果Tini调用Tini,在子进程中这样做可以避免出现竞争条件的情况
	// 在子进程中这样做可以避免出现竞争条件的情况进程组,而实际的子进程最终在后台!)。

    // getpgrp()用来取得目前进程所属的组识别码。. 此函数相当于调用 getpgid (0);返回目前进程所属的组识别码。
    // tcsetpgrp 函数---设置前台进程组ID
    // int tcsetpgrp(int fd, pid_t pgrpid);
    // 函数功能:使用 pgrpid 设置前台进程组ID,fd 必须引用该会话的控制终端,0 代表当前正在使用的终端
    // 返回值:成功返回 0,出错返回 -1
	if (tcsetpgrp(STDIN_FILENO, getpgrp())) {
		if (errno == ENOTTY) {
			PRINT_DEBUG("tcsetpgrp failed: no tty (ok to proceed)");
		} else if (errno == ENXIO) {
			// can occur on lx-branded zones
			PRINT_DEBUG("tcsetpgrp failed: no such device (ok to proceed)");
		} else {
			PRINT_FATAL("tcsetpgrp failed: %s", strerror(errno));
			return 1;
		}
	}

getpgrp()用来取得目前进程所属的组识别码。. 此函数相当于调用 getpgid (0);返回目前进程所属的组识别码。

int tcsetpgrp(int fd, pid_t pgrpid);

函数功能:使用 pgrpid 设置前台进程组ID,fd 必须引用该会话的控制终端,0 代表当前正在使用的终端。而 STDIN_FILEND 在 stdio.h 中的定义就是 #define STDIN_FILENO 0

返回值:成功返回 0,出错返回 -1。

所以这段代码的含义就是把前台进程组的 pgid 设置成 此进程的 pgid。

然后是restore_signals(sigconf_ptr) 这个函数

// 恢复原来的信号响应
int restore_signals(const signal_configuration_t* const sigconf_ptr) {
	if (sigprocmask(SIG_SETMASK, sigconf_ptr->sigmask_ptr, NULL)) {
		PRINT_FATAL("Restoring child signal mask failed: '%s'", strerror(errno));
		return 1;
	}

	if (sigaction(SIGTTIN, sigconf_ptr->sigttin_action_ptr, NULL)) {
		PRINT_FATAL("Restoring SIGTTIN handler failed: '%s'", strerror((errno)));
		return 1;
	}

	if (sigaction(SIGTTOU, sigconf_ptr->sigttou_action_ptr, NULL)) {
		PRINT_FATAL("Restoring SIGTTOU handler failed: '%s'", strerror((errno)));
		return 1;
	}

	return 0;
}

在上面将配置主进程信号的时候解释过 sigprovmask() 这个函数的作用,这里就不过多赘述了。

在前面配置主进程信号configure_signals()这个函数中,将主进程最原始的响应信号集保存了下来。而在这个函数中就是将原来的响应信号集复原到这个进程(子进程)中。此处将 SIGTTIN 和 SIGTTON 也都一一复原。

等待并转发信号


int wait_and_forward_signal(sigset_t const* const parent_sigset_ptr, pid_t const child_pid) {
	siginfo_t sig;

	if (sigtimedwait(parent_sigset_ptr, &sig, &ts) == -1) {
		switch (errno) {
			case EAGAIN:
				break;
			case EINTR:
				break;
			default:
				PRINT_FATAL("Unexpected error in sigtimedwait: '%s'", strerror(errno));
				return 1;
		}
	} else {
		/* 这里有一个信号需要处理 */
		switch (sig.si_signo) {
			case SIGCHLD:
				/* 特殊情况下,因为我们不转发SIGCHLD。相反,我们将陷入收割过程。 */
				PRINT_DEBUG("Received SIGCHLD");
				break;
			default:
				PRINT_DEBUG("Passing signal: '%s'", strsignal(sig.si_signo));
				/* 转发其他所有信号 */
				if (kill(kill_process_group ? -child_pid : child_pid, sig.si_signo)) {
					if (errno == ESRCH) {
						PRINT_WARNING("Child was dead when forwarding signal");
					} else {
						PRINT_FATAL("Unexpected error when forwarding signal: '%s'", strerror(errno));
						return 1;
					}
				}
				break;
		}
	}

	return 0;
}

这个函数是在主进程死循环里面的一个函数,在我的上一篇文章提到过这段函数。

int sigtimedwait(const sigset_t *set, soirnfo_t *info, const struct timespec *timeout),

这个函数从待处理信号集中删除信号,并返回信号编号作为其功能结果。如果info参数不是NULL,则它指向的缓冲区将用于返回soirnfo_t类型的结构,其中包含有关信号的信息。参数timeout
,该参数指定线程挂起等待信号的间隔。变量 ts 在开头有过定义 static struct timespec ts = { .tv_sec = 1, .tv_nsec = 0 }; 即为 1 秒。

成功时,sigtimedwait() 返回信号号(即,大于零的值)。失败时,两个调用均返回-1,并设置errno来指示错误。

错误码

  • EAGAIN :sigtimedwait()指定的超时期限内,没有信号置入待处理状态;
  • **EINTR :**等待被信号处理程序中断;
  • **EINVAL :**超时无效。

所以 sigtimedwait(parent_sigset_ptr, &sig, &ts) == -1 的意思就是等待信号,如果 sigtimedwait(parent_sigset_ptr, &sig, &ts) 的返回值为 -1 的话就执行下面的代码:

switch (errno) {
			case EAGAIN:
				break;
			case EINTR:
				break;
			default:
				PRINT_FATAL("Unexpected error in sigtimedwait: '%s'", strerror(errno));
				return 1;
		}

判断 errno 的信息,如果是 EAGAIN 表示 1 秒内没有信号处于待处理的状态,则直接返回;如果是 EINTR 则表示等待被信号处理程序中断,此处理程序用于信号而不是集合中的信号之一。如果不是这两个值,则表示有其他不可预料的错误,打印错误信息,并直接返回。

如果 sigtimedwait(parent_sigset_ptr, &sig, &ts) 的返回值不为 -1 就执行 else 后面的语句:

/* 这里有一个信号需要处理 */
		switch (sig.si_signo) {
			case SIGCHLD:
				/* 特殊情况下,因为我们不转发SIGCHLD。相反,我们将陷入收割过程。 */
				PRINT_DEBUG("Received SIGCHLD");
				break;
			default:
				PRINT_DEBUG("Passing signal: '%s'", strsignal(sig.si_signo));
				/* 转发其他所有信号 */
				if (kill(kill_process_group ? -child_pid : child_pid, sig.si_signo)) {
					if (errno == ESRCH) {
						PRINT_WARNING("Child was dead when forwarding signal");
					} else {
						PRINT_FATAL("Unexpected error when forwarding signal: '%s'", strerror(errno));
						return 1;
					}
				}
				break;
		}

int kill(pid_t pid, int sig); 函数可以给对应的 pid 发送 指定的信号。

参数:pid:可能选择有以下四种:

  1. pid大于零时,pid是信号欲送往的进程的标识。
  2. pid等于零时,信号将送往所有与调用kill()的那个进程属同一个使用组的进程。
  3. pid等于-1时,信号将送往所有调用进程有权给其发送信号的进程,除了1 号(init) 进程。
  4. pid小于-1时,信号将送往以-pid为组标识的进程。

sig:准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在执行。

返回值说明: 成功执行时,返回0。失败返回-1,

errno被设为以下的某个值:

  • EINVAL:指定的信号码无效(参数 sig 不合法(INVAL:invalid))
  • EPERM;权限不够无法传送信号给指定进程 (PERM:permission权限)
  • ESRCH:参数 pid 所指定的进程或进程组不存在(SRCH:search)

这里的 sig.si_signo 就是前面等待接收到的信号了,如果信号是 SIGCHLD,SIGCHLD 这个信号在子进程状态变更了,例如停止、继续、退出等,都会发送这个信号通知父进程。不过我们都不用处理,如果是子进程退出了,reap_zombies() 会帮我们收割僵尸进程的。

如果是其他信号,则会进行转发,也就是调用 kill(),发送给 子进程还是子进程所在的进程组,则取决于 kill_process_group

kill_process_group 这个值上面我们讲过,默认为 0,如果在命令行加了 -g 参数,或者在环境变量中配置了 KILL_PROCESS_GROUP_GROUP_ENV_VAR 都会使其 加 1。则会使这里的 kill() 函数发送给 -child_pid 即子进程所在的进程组。

收割僵尸进程


int reap_zombies(const pid_t child_pid, int* const child_exitcode_ptr) {
	pid_t current_pid;
	int current_status;

	while (1) {
		current_pid = waitpid(-1, &current_status, WNOHANG);

		switch (current_pid) {

			case -1:
				if (errno == ECHILD) {
					PRINT_TRACE("No child to wait");
					break;
				}
				PRINT_FATAL("Error while waiting for pids: '%s'", strerror(errno));
				return 1;

			case 0:
				PRINT_TRACE("No child to reap");
				break;

			default:
				/* 一个孩子被收割了。检查它是否是主进程。
				 * 如果是,那么设置exit_code,这将导致我们在收割完其他所有人后退出。
				 */
				PRINT_DEBUG("Reaped child with pid: '%i'", current_pid);
				if (current_pid == child_pid) {
					if (WIFEXITED(current_status)) {
						/* 我们的进程正常退出。 */
						PRINT_INFO("Main child exited normally (with status '%i')", WEXITSTATUS(current_status));
						*child_exitcode_ptr = WEXITSTATUS(current_status);
					} else if (WIFSIGNALED(current_status)) {
						/* 我们的进程被终止了。模仿sh / bash 的做法,即返回128 + 信号数。 */
						PRINT_INFO("Main child exited with signal (with signal '%s')", strsignal(WTERMSIG(current_status)));
						*child_exitcode_ptr = 128 + WTERMSIG(current_status);
					} else {
						PRINT_FATAL("Main child exited for unknown reason");
						return 1;
					}

					// 安全起见,确保退出码 在0和255之间。
					*child_exitcode_ptr = *child_exitcode_ptr % (STATUS_MAX - STATUS_MIN + 1);

					// 如果这个退出码 被重新设置,那么就把它设置为0。
					INT32_BITFIELD_CHECK_BOUNDS(expect_status, *child_exitcode_ptr);
					if (INT32_BITFIELD_TEST(expect_status, *child_exitcode_ptr)) {
						*child_exitcode_ptr = 0;
					}
				} else if (warn_on_reap > 0) {
					PRINT_WARNING("Reaped zombie process with pid=%i", current_pid);
				}

				// 检查其他子进程是否已被收割。
				continue;
		}

		/* 如果我们能到这里,那是因为我们没有 continue in the switch case. */
		break;
	}

	return 0;
}

了解这段代码,我们还需要知道 waitpid() 这个函数的含义和用法。

pid_t waitpid(pid_t pid,int *status,int options)。如果在调用waitpid()函数时,当指定等待的子进程已经停止运行或结束了,则waitpid()会立即返回;但是如果子进程还没有停止运行或结束,则调用waitpid()函数的父进程则会被阻塞,暂停运行。

pid_t pid

参数pid为欲等待的子进程识别码,其具体含义如下:

  • pid < -1,表示等待进程组号为 pid 绝对值的任何子进程。
  • pid = -1,表示等待任何子进程。
  • pid = 0,表示等待进程组号与目前进程相同的任何子进程,也就是说任何和调用 waitpid() 函数的进程在同一个进程组的进程。
  • pid > 0,表示等待进程号为 pid 的子进程。

*int status

这个参数将保存子进程的状态信息,有了这个信息父进程就可以了解子进程为什么会推出,是正常推出还是出了什么错误。如果status不是空指针,则状态信息将被写入
器指向的位置。当然,如果不关心子进程为什么推出的话,也可以传入空指针。
Linux提供了一些非常有用的宏来帮助解析这个状态信息,这些宏都定义在sys/wait.h头文件中。主要有以下几个:

  • WIFEXITED(status),表示如果子进程正常结束,它就返回真;否则返回假。
  • WEXITSTATUS(status),表示如果WIFEXITED(status)为真,则可以用该宏取得子进程exit()返回的结束代码。
  • WIFSIGNALED(status),表示如果子进程因为一个未捕获的信号而终止,它就返回真;否则返回假。
  • WTERMSIG(status),表示如果WIFSIGNALED(status)为真,则可以用该宏获得导致子进程终止的信号代码。

init options

参数options提供了一些另外的选项来控制waitpid()函数的行为。如果不想使用这些选项,则可以把这个参数设为0。主要使用的有以下两个选项:

  • WNOHANG,表示如果pid指定的子进程没有结束,则waitpid()函数立即返回 0,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号。
  • WUNTRACED,表示如果子进程进入暂停状态,则马上返回。

如果waitpid()函数执行成功,则返回子进程的进程号;如果有错误发生,则返回-1,并且将失败的原因存放在errno变量中。
失败的原因主要有:没有子进程(errno设置为ECHILD),调用被某个信号中断(errno设置为EINTR)或选项参数无效(errno设置为EINVAL)。

知道 waitpid() 这个函数后,我们就就可以看这段代码了。

在死循环里面 通过 waitpid() 来回收僵尸进程。查看其返回值:

  • 如果为 -1 则代表出错了,检查错误信息,如果错误信息是 ECHILD,则表示没有子进程,打印 trace 信息并跳出死循环;如果错误信息不是 ECHILD,则表示不可预期的错误,直接返回。
  • 如果错误信息如果为 0,则表示没有子进程退出,也就没有僵尸进程需要回收,则跳出死循环。
  • 如果为其他值,则表示有一个子进程被收割了。switch 进入 default 下面的代码,并会再次循环:
/* 一个孩子被收割了。检查它是否是主进程。
				 * 如果是,那么设置exit_code,这将导致我们在收割完其他所有人后退出。
				 */
				PRINT_DEBUG("Reaped child with pid: '%i'", current_pid);
				if (current_pid == child_pid) {
					if (WIFEXITED(current_status)) {
						/* 我们的进程正常退出。 */
						PRINT_INFO("Main child exited normally (with status '%i')", WEXITSTATUS(current_status));
						*child_exitcode_ptr = WEXITSTATUS(current_status);
					} else if (WIFSIGNALED(current_status)) {
						/* 我们的进程被终止了。模仿sh / bash 的做法,即返回128 + 信号数。 */
						PRINT_INFO("Main child exited with signal (with signal '%s')", strsignal(WTERMSIG(current_status)));
						*child_exitcode_ptr = 128 + WTERMSIG(current_status);
					} else {
						PRINT_FATAL("Main child exited for unknown reason");
						return 1;
					}

					// 安全起见,确保退出码 在0和255之间。
					*child_exitcode_ptr = *child_exitcode_ptr % (STATUS_MAX - STATUS_MIN + 1);

					// 如果这个退出码 被重新设置,那么就把它设置为0。
					INT32_BITFIELD_CHECK_BOUNDS(expect_status, *child_exitcode_ptr);
					if (INT32_BITFIELD_TEST(expect_status, *child_exitcode_ptr)) {
						*child_exitcode_ptr = 0;
					}
				} else if (warn_on_reap > 0) {
					PRINT_WARNING("Reaped zombie process with pid=%i", current_pid);
				}

				// 检查其他子进程是否已被收割。
				continue;

这里的 current_pid 表示的是此进程创建的子进程的 pid,如果 current_pid == child_pid ,则表示被收割的僵尸进程,就是自己创建的子进程,会执行下面的代码:

if (WIFEXITED(current_status)) {
						/* 我们的进程正常退出。 */
						PRINT_INFO("Main child exited normally (with status '%i')", WEXITSTATUS(current_status));
						*child_exitcode_ptr = WEXITSTATUS(current_status);
					} else if (WIFSIGNALED(current_status)) {
						/* 我们的进程被终止了。模仿sh / bash 的做法,即返回128 + 信号数。 */
						PRINT_INFO("Main child exited with signal (with signal '%s')", strsignal(WTERMSIG(current_status)));
						*child_exitcode_ptr = 128 + WTERMSIG(current_status);
					} else {
						PRINT_FATAL("Main child exited for unknown reason");
						return 1;
					}

					// 安全起见,确保退出码 在0和255之间。
					*child_exitcode_ptr = *child_exitcode_ptr % (STATUS_MAX - STATUS_MIN + 1);

					// 如果这个退出码 被重新设置,那么就把它设置为0。
					INT32_BITFIELD_CHECK_BOUNDS(expect_status, *child_exitcode_ptr);
					if (INT32_BITFIELD_TEST(expect_status, *child_exitcode_ptr)) {
						*child_exitcode_ptr = 0;
					}

首先判断子进程是否是正常退出 WIFEXITED(current_status) ,如果是正常退出则设置子进程退出码为 WEXITSTATUS(current_status) 子进程退出的返回代码。

如果不是正常退出,则再判断子进程是否是因为一个未捕获的信号而退出的 WIFSIGNALED(current_status) ,如果是,则打印是哪一个信号暂停的子进程 strsignal(WTERMSIG(current_status))) 。并模仿 sh / bash 的做法,将子进程退出码赋值为 128 + WTERMSIG(current_status) 即 128 + 子进程退出的返回码。

如果都不是,则表示是未知的理由退出的,直接打印错误信息,并返回。

为了确保安全,退出码在 0 - 255 之间

#define STATUS_MAX 255
#define STATUS_MIN 0

*child_exitcode_ptr = *child_exitcode_ptr % (STATUS_MAX - STATUS_MIN + 1);

STATUS_MAX 的值是 255, STATUS_MIN 的值是 0。通过求余来确保退出码的范围。

#define INT32_BITFIELD_CHECK_BOUNDS(F, i) do {  assert(i >= 0); assert(ARRAY_LEN(F) > (uint) (i / 32)); } while(0)
#define INT32_BITFIELD_TEST(F, i)    ( F[(i / 32)] &   (1 << (i % 32)) )
static int32_t expect_status[(STATUS_MAX - STATUS_MIN + 1) / 32];

// 如果这个退出码 被重新设置,那么就把它设置为0。
					INT32_BITFIELD_CHECK_BOUNDS(expect_status, *child_exitcode_ptr);
					if (INT32_BITFIELD_TEST(expect_status, *child_exitcode_ptr)) {
						*child_exitcode_ptr = 0;
					}

INT32_BITFIELD_CHECK_BOUNDS() 通过断言子进程退出码在 expect_status 范围中的 (也就是 0- 255)。

INT32_BITFIELD_TEST() 判断子进程退出码是否是之前定义过的,也就是我们在输入命令时,加上 -e 选项后带的参数。如果子进程退出码是我们之前设置过的,就将退出码设置为 0。

else if (warn_on_reap > 0) {
					PRINT_WARNING("Reaped zombie process with pid=%i", current_pid);
				}

如果被收割的僵尸进程不是该进程创建的子进程,则打印警告信息,表明已经收割了 pid 为多少的子进程。

进程退出

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

child_exitcode 的默认值就是 -1。所以如果不重新给 child_exitcode 赋值,那么这个进程就会一直处于死循环中。而 child_exitcode 的赋值操作仅在收割僵尸进程 reap_zombies()函数中赋值,赋值的条件就是收割了自己创建的子进程。

换句话说,就是当此进程创建出来的子进程退出后,就会使 child_exitcode != -1 成立,从而跳出死循环,进而此进程也会跟着退出。

到这里,就已经将 Tini 源码分析完了,可能分析的不太全面,请见谅。我会继续努力,后续写出更有价值的博客。

    附件下载

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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