Tini 源码分析(2)
Tini 源码分析(2)
Tini 源码分析
我会从程序在 main 函数中执行的顺序,来一步一步的分析源码。每一个函数我都会贴上源码,然后进行分析。我把 main 函数的大概流程画了出来,大家可以参考一下。文章太长发不了,只能分割成两章了,这是第二章。源代码的注释我也放在了附件中,大家需要的可以自行下载。
父进程死亡,此进程收到的信号
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:可能选择有以下四种:
- pid大于零时,pid是信号欲送往的进程的标识。
- pid等于零时,信号将送往所有与调用kill()的那个进程属同一个使用组的进程。
- pid等于-1时,信号将送往所有调用进程有权给其发送信号的进程,除了1 号(init) 进程。
- 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, ¤t_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 源码分析完了,可能分析的不太全面,请见谅。我会继续努力,后续写出更有价值的博客。
- 点赞
- 收藏
- 关注作者
评论(0)