进程终止方式

  • 正常终止
    • 从main返回
    • 调用exit
    • 调用_exit 或 _Exit
    • 最后一个线程从其启动例程返回
    • 从最后一个线程调用pthread_exit
  • 异常终止
    • 调用abort
    • 接收到一个信号
    • 最后一个线程对取消请求作出响应

不管是哪一种退出方式,最后都会执行内核中的同一代码,这段代码用来关闭进程所用到的已经打开的文件描述符所占用的内存和资源.

exit 与 _exit的区别

exit/_exit区别

io缓冲区操作

程序段1:调用exit

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    printf("hello itcast");
    exit(0);
}

结果: 输出hello itcast

程序段2: 调用_exit

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    printf("hello itcast");
    _exit(0);
}

结果:无输出

可见exit进行了缓冲区的刷新,而_exit并未做缓冲区处理, 可以使用fflush手动刷新缓冲区

exit会调用afexit注册的终止处理程序

atexit可以注册终止处理程序,ANSI C规定至少可以注册32个终止处理程序。
终止处理程序的调用与注册次序相反。

程序段1: 使用_exit

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void before_exit(void) {
    printf("before_exit\n");
}
void clean(void) {
    printf("before_clean\n");
}
int main(int argc, char *argv[]) {

    atexit(before_exit);
    atexit(clean);
    _exit(0);
}

结果分析: 无输出,未执行终止处理程序

程序段2: 使用exit

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void before_exit(void) {
    printf("before_exit\n");
}
void clean(void) {
    printf("before_clean\n");
}
int main(int argc, char *argv[]) {

    atexit(before_exit);
    atexit(clean);
    exit(0);
}

执行结果:

bogon:process xqhero$ ./atexit
before_clean
before_exit

结果分析: exit执行了终止处理程序

总结exit与_exit区别

  1. _exit是一个系统调用,exit是一个c库函数
  2. exit会执行刷新I/O缓存
  3. exit会执行调用终止处理程序

return 和 exit的区别

  1. exit是系统调用级别,它表示一个进程的结束;return是语言级别的返回,表示调用堆栈的返回
  2. exit用于结束正在运行的整个程序,它将参数返回给OS,把控制权交给操作系统;而return 是退出当前函数,返回函数值,把控制权交给调用函数。
  3. 在main函数结束时,会隐式地调用exit函数,所以一般程序执行到main()结尾时,则结束主进程。exit将删除进程使用的内存空间,同时把错误信息返回给父进程
  4. void exit(int status); 一般status为0,表示正常退出,非0表示非正常退出。

main 中使用return相当于间接调用了exit

进程终止时会执行的操作

  1. 不管进程如何终止,最后都会执行内核中的同一段代码:为相应进程关闭所有打开描述符,释放内存等等
  2. 若父进程在子进程之前终止了,则子进程的父进程将变为init进程,其PID为1;保证每个进程都有父进程。
  3. 当子进程先终止,父进程如何知道子进程的终止状态?事实上,内核为每个终止子进程保存了终止状态等信息,父进程调用wait等函数,就可获取该信息。
  4. 当父进程调用wait等函数后,内核将释放终止进程所使用的所有内存,关闭其打开的所有文件。
  5. 对于已经终止、但是其父进程尚未对其调用wait等函数的进程,被称为僵尸进程(即已经结束,但尚未释放资源的)。
  6. 对于父进程先终止,而被init领养的进程会是僵尸进程吗?init对每个终止的子进程,都会调用wait函数,获取其终止状态信息。

进程终止时的情况

  • 子进程在父进程调用fork后完成,进程将其终止状态返回给父进程,如果父进程在进程终止前终止,则子进程的父进程被改变成init进程,称这些进程由init进程收养, 操作的流程大致是: 在一个进程终止时,内核逐个检查所有活动进程,以判断它是否为正要终止进程的子进程,如果是,则该进程的父进程ID就更改为1

  • 子进程在父进程之前终止,那么父进程又如何能在做相应检查时得到子进程的终止状态呢? 如果子进程完全消失了,父进程在最终准备好检查子进程是否终止时是无法获取它的最终状态的。 内核为每个终止的子进程保存一定量的信息,所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息。这些信息至少包括进程ID,该进程的终止状态以及该进程使用的CPU时间总量。 内核可以释放终止进程所使用的所有存储区,关闭其所有打开的文件。

一个已经终止,但是其父进程尚未对其进行善后处理(获取终止进程的有关信息,释放它仍占用的资源)的进程被称为僵尸进程

一个被init进程收养的进程终止时会不会变成僵尸进程: 不会,因为init被编写成无论何时只要有一个子进程终止,init就会调用一个wait函数取得其终止状态。

进程终止状态

  • 进程终止后,由父进程回收PCB资源
  • 当一个进程正常或者异常终止时,内核就向父进程发送SIGCHLD信号。因为子进程终止是一个异步事件(可以在父进程运行的任何时刻发生),所以这种信号也是内核向父进程发的异步通知。
  • 内核向父进程发送SIGCHLD信号,通知父进程子进程结束了,你可以获取子进程的“进程终止状态”了。如果父进程没有调用wait函数的话,会忽略这个信号,表示不关心子进程的“进程终止状态”。如果父进程正在调用wait函数等待子进程的“进程终止状态”的话,wait会被SIGCHLD信号唤醒,并获取子进程“进程终止状态”。一般情况下,父进程都不关心子进程的终止状态是什么,所以我们经常看到的情况是,不管子进程返回什么返回值,其实都无所谓,因为父进程不关心。不过如果我们的程序是一个多进程的程序,而且父进程有获取子进程“终止状态”的需求,此时我们就可以使用wait函数来获取了

wait/waitpid用于父进程获取子进程的“进程终止状态”。

#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options)

wait 与 waitpid的区别:

  • wait 等待第一个终止的子进程,waitpid可以通过pid参数指定等待哪一个子进程
  • waitpid中pid=-1、option=0时,waitpid函数等同于wait,可以把wait看成是waitpid实现的特例。
  • waitpid函数提供了wait函数没有提供的三个功能:
  1. waitpid等待一个特定的进程,而wait则返回任一终止子进程的状态 。

  2. waitpid提供了一个 wait的非阻塞版本,有时希望取得一个子进程的状态, 但不想进程阻塞。

  3. waitpid支持作业控制。

文档更新时间: 2021-03-06 00:27   作者:周国强