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 也就是说 256 和 0 是相同的。
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() 函数获取当前进程的资源消耗。