(一)进程的五种正常终止方式有:
- 在main函数执行return语句。(等效于调用exit)
- 调用exit函数。
- 调用_exit或_Exit函数。
- 进程的最后一个线程在其启动历程中执行返回语句。但是,该线程的返回值不会用作进程的返回值。当最后一个线程从其启动历程返回时,该进程一终止状态0返回。
- 进程的最后一个线程调用pthread_exit函数。
三种异常终止方式如下:
- 调用abort。产生SIGABRT信号,这是一种异常终止的一种特例。
- 当进程接收到某些信号时。(信号可由进程自身、其他进程或内核产生)
- 最后一个线程对“取消”请求做出响应。按系统默认,“取消”以延迟方式发生:一个线程要求取消另一个线程,一段时间之后,目标线程终止。
不论进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。
一般来说,进程的析构是自身引起的。正常终结:它发生在进程调用exit()系统调用时,既可能显示地调用这个系统调用,也可能隐式地从某个程序的主函数返会(main函数的返回点后面调用exit())。异常终结:进程接收到它既不能处理也不能忽略的信号或异常是,它还可能被动地终结。不管进程是怎么终结的,该任务大部分都要考do_exit()来完成,它要做下面这些繁琐的工作:
- 将tast_struct中的标志成员flags设置为PF_EXITING。
- 调用del_timer_sync()删除任一内核定时器。根据返回的结果,它确保没有定时器在排队,也没有定时器处理程序在运行。
- 如果BSD的进程记账功能是开启的,do_exit()调用acct_updata_integrals()来输出记账信息。
- 然后调用exit_mm()函数释放进程占用的mm_struct ,如果没有别的进程使用它们(这个地址空间没有被共享),就彻底释放它们。
- 接下来调用sem_exit()函数。若果进程排队等候IPC信号,它则离开队伍。
- 调用exit_files()和exit_fs(),以分别递减文件描述符、文件系统数据的引用计数。如果其中某个引用计数的数值降为零,那么代表没有进程在使用相应的资源,此时可以释放。
- 接着把存放在task_struct的exit_code成员中的任务退出码置为由exit()提供的退出代码,或者去完成任何其他由内核机制规定的退出动作。退出代码存放在这里供范金成随时检索。
- 调用exit_notify()性父进程发送信号,给子进程重新找养父,养父为新城组中的其他线程或init进程,并把进程状态(存放在task——struct结构中的exit_state中)设成EXIT-ZOMBIE。
- do_exit()调用shedule()切换到新进程。因为处于EXIT-ZOMBIE状态的进程不会再被调度,所以这是进程所执行的最后一段代码。do_exit()永不返回。
至此,与进程相关联的所有资源都被释放掉了(假设该进程是这些资源的唯一使用者)。进程不可运行(实际上也没有地址空间让它运行)并处于EXIT-ZOMBIE退出状态。他占用的所有内存就是内核栈、thread_info结构和task_struct结构。此时进程存在唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核那些无关的信息后,有进程所持有的剩余内存被释放,归还给系统使用。
当一个进程终结时,内核必须释放它所占有的资源并把这一不幸告知其父进程。但是如果父进程没有获取子进程的退出状态,或者父进程先于子进程终结会产生什么?
僵死进程:父进程未结束,子进程结束,并且父进程未获取子进程的退出状态的这种进程,进程主体释放,只有PCB还未释放。
内核为每个终止子进程保存了一定量的信息,所以当终止的父进程调用wait或waitpid时,可以得到这些信息(进程ID、该进程的终止状态、以及该进程使用CPU时间总量)。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。
孤儿进程:父进程结束,子进程未结束。孤儿进程会被系统守护进程init收养,并为他们完成状态收集工作。
对于父进程已经终止的所有进程,它们的父进程都改变为init进程(被init进程邻养)。大致过程如下:在一个进程终止时,内核逐个检查所有活动进程,以判断他是否要终止进程的子进程,如果是,则该进程的父进程ID更改为1(init进程的ID)。这种处理方式保证保证每一个进程都有一个父进程。
一个由init进程领养的进程终止时会发生什么?它会不会变成一个僵死进程?
答案是“否”,因为init被编写成无论如何只要有一个子进程中终止,init就会调用一个wait函数取得其终止状态。防止了系统有很多僵死进程。
(二)删除进程描述符
在调用do_exit()之后,尽管进程已经僵死不能再运行,但是系统还保留了她的进程描述符。这样做是可以让系统有办法在子进程终结后仍能获得它的信息。因此进程终结时需要的清理工作和进程描述符删除被分开执行。当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是一个异步事件所以这种信号也是内核向其父进程发的异步通知。父进程可以忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。对于这种信号系统默认动作是忽略它。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。
三种僵死进程的处理方法有以下:
- 父进程中调用wait或waitpid获取子进程的退出状态,这种方式可能导致父进程在wait或waitpid调用处阻塞运行,直到自己成退出。
- 父进程调用signal(SIGCHLD,SIG_IGN),来忽略SIGCHLD信号,这样子进程结束后会由内核释放资源。
- 对子进程的退出捕获他们的退出信号SIGHLD,父进程退出信号时,在信号处理函数中调用wait或waitpid操作释放他们的资源。
现在需要知道的是调用wait或waitpid的进程可能会发生什么情况:
- 如果其所有子进程都还在运行,则阻塞。
- 如果一个子进程已终止,正在等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
- 如果它没有任何子进程,则立即出错返回。
- 如果进程由于接受到SIGCHLD信号而调用wait,则可期望wait会立即返回。但是如果在任意时刻调用wait,则进程可能会阻塞。
#include<sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid,int *statloc,int options);
两个函数返回值:若成功则返回进程ID,出错则返回-1
statloc是一个整型指针。如果statloc不是一个空指针,则终止进程的终止状态就存放它所指向的单元内。
区别:
waitpid可以等待指定进程而wait返回任一终止子进程状态。
waitpid提供了一个wait的非阻塞版本
waitpid支持作业控制
这两个函数的区别如下:
- 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞。
- waitpid并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。
waitpid函数中参数pid作用的解释;
waitpid函数中的参数options:
wait()这一族函数都是通过唯一的一个系统调用wait4()来实现的。它的标准动作是挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程的PID。此外,调用该函数是提供的指针会包含子函数退出时的退出代码。
当最终需要释放进程描述符时,release_task()会被调用,用来完成以下工作:
- 它调用_exit_signal(),该函数调用_unhash_process(),后者又调用detach_pid()从pidhash上删除该进程,同时也要从任务列表中删除该过程。
- _exit_signal()释放目前僵死进程所使用的所有剩余资源,并进行最终统计和记录。
- 如果这个进程是线程组最后一个进程,并且领头进程已经死掉,那么release_task()就要通知僵死的领头进程的父进程,
- release_task()调用put_task_struct()释放进程内核栈和thread_info结构所占的页,并释放task_struct()所占的slab高速缓存。
至此,进程描述符和所有进程独享的资源就全部释放掉了。