Linux 进程退出码

2018-11-04 kernel linux

Linux 下进程的退出包括了正常退出和异常退出,正常退出包括了 A) main() 函数中通过 return 返回;B) 调用 exit() 或者 _exit() 退出。异常退出包括了 A) abort() 函数;B) 收到了信号退出。

不管是哪种退出方式,系统最终都会执行内核中的同一代码,并将进程的退出方式以返回码的方式保存下来。

简介

当进程正常或异常终止时,内核就向其父进程发送 SIGCHLD 信号,对于 wait() 以及 waitpid() 进程可能会出现如下场景:

  • 如果其所有子进程都在运行则阻塞;
  • 如果某个子进程已经停止,则获取该子进程的退出状态并立即返回;
  • 如果没有任何子进程,则立即出错返回。

如果进程由于接收到 SIGCHLD 信号而调用 wait,则一般会立即返回;但是,如果在任意时刻调用 wait 则进程可能会阻塞。

等待子进程退出

父进程可以通过 wait() 或者 waitpid() 获取子进程的状态码,详细可以通过 man 3 wait 查看,其声明如下。

如果下面参数中的 status 不是 NULL,那么会把子进程退出时的状态返回,该返回值保存了是否为正常退出、正常结束的返回值、被那个信号终止等。

#include <sys/wait.h>

pid_t wait(int *status);
pit_t waitpid(pid_t pid, int *status, int options);

当要等待一特定进程退出时,可调用 waitpid() 函数,其中第一个入参 pid 的入参含义如下:

  • pid=-1 等待任一个子进程,与 wait 等效。
  • pid>0 等待其进程 ID 与 pid 相等的子进程。
  • pid==0 等待其进程组 ID 等于调用进程组 ID 的任一个子进程。
  • pid<-1 等待其进程组 ID 等于 pid 绝对值的任一子进程。

waitpid 返回终止子进程的进程 ID,并将该子进程的终止状态保存在 status 中,其中 waitpid() 第三个入参指定了一些行为,如下是常见的参数列表:

  • WNOHANG 没有任何已经结束的子进程则立刻返回,不等待。
  • WUNTRACED 子进程进入暂停执行情况则马上返回, 不会关心子进程的推出状态。

退出码

子进程结束后,其最终的状态信息保存在 status ,在 sys/wait.h 中有对相关宏定义的实现,其中退出码有效为 16Bits 主要由三部分组成,包括了:

  • Bits 8~15 通过 exit() 接口退出进程,也就是意味着错误码最大为 255 ,如果是 256 那么实际上是 0 ;
  • Bit 7 用来标示是否有生成 core 文件。
  • Bits 0~6 对应了接受到的信号,注意这里还通过 127 0x7f 定义了 STOP 状态。

头文件中提供的宏定义包括了:

  • WIFEXITED 判断是否通过 exit() return 正常退出,然后通过 WEXITSTATUS 获取具体的退出码。
  • WIFSIGNALED 判断是否是因为接受到了信号而停止,包括 core 的方式实际上也是通过信号完成,此时可通过 WTERMSIG 读取信号,WCOREDUMP 是否生成 coredump 文件。
  • WIFSTOPPED 判断子进程是否处于暂停执行状态,一般只有使用 WUNTRACED 参数时会返回该值。

其中示例代码如下。

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/ptrace.h>

#define log_info(...)  do { printf("info : " __VA_ARGS__); putchar('\n'); } while(0);
#define log_error(...) do { printf("error: " __VA_ARGS__); putchar('\n'); } while(0);

#define REPO_PATH "/tmp/examples/linux/process/exitcode"

int main(void)
{
    int status;
    pid_t pid;
    char *argv[] = {(char *)"/bin/bash", (char *)"-c", (char *)"exit 1", NULL};

    pid = fork();
    if (pid < 0) {
        log_error("fork failed, %s.", strerror(errno));
        exit(EXIT_FAILURE); /* 1 */
    } else if (pid == 0) { /* child */
        //ptrace(PTRACE_TRACEME, 0, NULL, NULL);
        if (execvp(argv[0], argv) < 0) {
            log_error("execl failed, %s.", strerror(errno));
            return 0;
        }
    }

    if (waitpid(pid, &status, WUNTRACED) < 0) {
        log_error("waitpid error, %s.", strerror(errno));
        return 0;
    }
    log_info("process #%d exit with %d.", pid, status);
    if (WIFEXITED(status)) {
        log_info("normal termination, exit status = %d.", WEXITSTATUS(status));
    } else if (WIFSIGNALED(status)) {
        log_info("abnormal termination, signal number = %d%s.",
                    WTERMSIG(status), WCOREDUMP(status) ? " (core file generated)" : "");
    } else if (WIFSTOPPED(status)) {
        log_info("child stopped, signal number = %d.", WSTOPSIG(status));
    } else {
        log_info("unknown exit code %d.", status);
	}
    exit(0);
}

测试场景

1. 正常退出

满足 WIFEXITED 宏定义的条件,也就是通过 exit() 或者 return 这种接口退出,此时可以直接通过 WEXITSTATUS 获取具体的错误码。

char *argv[] = {(char *)"/bin/bash", (char *)"-c", (char *)"exit 1", NULL};
char *argv[] = {(char *)REPO_PATH "/exitcode", (char *)"1", NULL};

注意,退出码只能是 8bits 也就是说 2560 是相同的。

2. 异常退出

满足 WIFSIGNALED() 宏定义的判断条件。

Core 掉

常见的是除零错误 ,这里会有两种方式:A) 生成了 Core 文件,对应的返回码是 136 = 1000 1000;B) 没有生成 Core 文件,则是 8 = 1000

char *argv[] = {(char *)REPO_PATH "/coredump", NULL};

这里的 8 实际上对应了 SIGFPE 信号量。

是否生成 core file 文件可以通过 ulimit 命令进行查看 (ulimit -c)、开启 (ulimit -c unlimited)、关闭 (ulimit -c 0)。

发送信号

通过执行 sleep 1000 命令,然后通过 kill -SIGTERM <PID> 或者 kill -15 <PID> 手动发送信号。

注意,如果注册了信号的回调函数,而在回调函数里是通过 exit() 退出的,那么实际上仍然被认为是 exit() 的退出方式。

3. 停止执行

实际上对应了 WIFSTOPPED() 宏定义,此时需要在子进程中调用 ptrace() 接口,默认返回的信号是 SIGTRAP

示例如下。

char *argv[] = {(char *)"/usr/bin/sleep", (char *)"1", NULL};

pid = fork();
if (pid < 0) {
	log_error("fork failed, %s.", strerror(errno));
	exit(EXIT_FAILURE); /* 1 */
} else if (pid == 0) { /* child */
	ptrace(PTRACE_TRACEME, 0, NULL, NULL);
	if (execvp(argv[0], argv) < 0) {
		log_error("execl failed, %s.", strerror(errno));
		return 0;
	}
}

进程统计信息

在等待进程退出的时候,常用的有几个 API 调用,例如 wait() waitpid() wait3() wait4() 等,其中后两者还会获取到进程在运行时的一些统计信息。

实际上返回的是一个 struct rusage 结构体,也可以通过 getrusage() 函数获取当前进程的资源消耗。