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()
函数获取当前进程的资源消耗。