撤消进程

来源:互联网 发布:amazo。it 编辑:程序博客网 时间:2024/06/02 11:33

很多进程终止了它们本该执行的代码,从这种意义上说,这些进程“死”了。当这种情况发生是,必须通知内核以便内核释放进程所拥有的资源,包括内存,打开文件及其它我们在本书中讲到的零碎东西,如信号量。


进程终止的一般方式是调用exit()库函数,该函数释放C函数库所分配的资源,执行编程者所注册的每个函数,并结束从系统回收进程的那个系统调用。exit()函数可能由编程者显式的插入。另外,C编译程序总是把exit()函数插入到main()函数的最后一条语句之后。


内核可以有选择地强迫整个线程组死掉。这发生在以下两种典型情况下:当进程接收到一个不能处理或忽略的信号时,或者当内核正在代表进程运行时在内核态产生一个不可恢复的CPU异常时。


进程终止


linux2.6中有两个终止用户态应用的系统调用:

  1. exit_group()系统调用,它终止整个线程组,即整个基于多线程的应用。do_group_exit()是实现这个系统调用的主要内核函数。这是C库函数exit()应该调用的系统调用。

  2. exit()系统调用,它终止某一个线程,而不管线程所属线程组中的所有其它进程,do_exit()是实现这个系统调用的主要内核函数。这是被诸如pthread_exit()Linux线程库的函数所调用的系统调用。


do_group_exit()函数


do_group_exit()函数杀死属于current线程组的所有进程。它接受进程终止代码作为参数,进程终止代号可能是系统调用exit_group()指定的一个值,也可能是内核提供的一个错误代号。该函数执行下述操作:

  1. 检查退出进程的SIGNAL_GROUP_EXIT标志是否不为0,如果不为0,说明内核已经开始为线性组执行退出的过程。在这种情况下,就把存放在current->signal->group_exit_code的值当作退出码,然后跳转到第4步。

  2. 否则,设置进程的SIGNAL_GROUP_EXIT标志并把终止代号放到current->signal->group_exit_code字段。

  3. 调用zap_other_threads()函数杀死current线程组中的其它进程。为了完成这个步骤,函数扫描与current->tgid对应的PIDTYPE_TGID类型的散列表中的每PID链表,向表中所有不同于current的进程发送SIGKILL信号,结果,所有这样的进程都将执行do_exit()函数,从而被杀死。

  4. 调用do_exit()函数,把进程的终止代码传递给它。正如我们将在下面看到的,do_exit()杀死进程而且不再返回。


do_exit()函数


所有进程的终止都是由do_exit()函数来处理的,这个函数从内核数据结构中删除对终止进程的大部分引用。do_exit()函数接受进程的终止代号作为参数并执行下列操作:

  1. 把进程描述符的flag字段设置为PF_EXITING标志,以表示进程正在被删除。

  2. 如果需要,通过函数del_timer_sync()从动态定时器队列中删除进程描述符。

  3. 分别调用exit_mm()exit_sem()__exit_files()__exit_fs()exit_namespace()exit_thread()函数从进程描述符中分离出与分页、信号量、文件系统、打开文件描述符、命名空间以及I/O权限位图相关的数据结构。如果没有其它进程共享这些数据结构,那么这些函数还删除所有这些数据结构中。

  4. 如果实现了被杀死进程的执行域和可执行格式的内核函数包含在内核模块中,则函数递减它们的使用计数器。

  5. 把进程描述符的exit_code字段设置成进程的终止代号,这个值要么是_exit()exit_group()系统调用参数,要么是由内核提供的一个错误代码。

  6. 调用exit_notify()函数执行下面的操作:

a更新父进程和子进程的亲属关系。如果同一线程组中有正在运行的进程,就让终止进程所创建的所有子进程都变成同一线程组中另外一个进程的子进程,否则让它们成为init的子进程

b检查被终止进程其进程描述符的exit_signal字段是否不等于-1,并检查进程是否是其所属进程组的最后一个成员。在这种情况下,函数通过给正被终止进程的父进程发送一个信号,以通知父进程子进程死亡。

c.否则,也就是exit_signal字段等于-1,或者线程组中还有其它进程,那么只要进程正在被跟踪,就向父进程发送一个SIGCHLD信号。

d.如果进程描述符的exit_signal字段等于-1,而且进程没有被跟踪,就把进程描述符的exit_state字段置为EXIT_DEAD,然后调用release_task()回收进程的其它数据结构占用的内存,并递减进程描述符的使用计数器,以使进程描述符本身正好不会被释放。

e.否则,如果进程描述符的exit_signal字段不等于-1,或进程正在被跟踪,就把exit_state字段置为EXIT_ZOMBIE

f.把进程描述符的flags字段设置为PF_DEAD标志。

  1. 调用schedule()函数选择一个新进程运行。调度程序忽略处于EXIT_ZOMBIE状态的进程,所以这种进程正好在schedule()中的宏switch_to被调用之后停止执行。


进程删除


Unix允许进程查询内核以获得其父进程的PID,或者其任何于进程的执行状态。例如,进程可以创建一个子进程来执行特定的任务,然后调用诸如wait()这样的一些库函数检查子进程是否终止。如果子进程已经终止,那么,它的终止代号将告诉父进程这个任务是否已成功地完成。


为了遵循这些设计选择,不允许Unix内核在进程一终止后就丢弃包含在进程描述符字段中的数据。只有父进程发出了与被终止的进程相关的wait()类系统调用之后,才允许这样做。这就是引入僵死状态的原因:尽管从技术上来说进程已死,但必须保存它的描述符,直到父进程得到通知。


如果父进程在子进程结束之前结束会发生什么情况呢?在这种情况下,系统中会到处是僵死的进程,而且它们的进程描述符永久占据着RAM。如前所述,必须强迫所有的孤儿进程成为init进程的子进程来解决这个问题。这样,init进程在用wait()类系统调用检查其合法的子进程终止时,就会撤消僵死的进程。


release_task()函数从僵死进程的描述符中分离出最后的数据结构;对僵死进程的处理有两种可能方式;如果父进程不需要接收来自子进程的信号,就调用do_exit();如果已经给父进程发送了一个信号,就调用wait4()waitpid()系统调用。在后一种情况下,函数还将回收进程描述符所占用的内存空间,而在前一种情况下,内存的回收将由进程调度程序来完成。该函数执行下述步骤:

  1. 递减终止进程拥有者的进程个数。这个值存放在本章前面提到的user_struct结构中。

  2. 如果进程正在被跟踪,函数将它从调试程序的ptrace_children链表中删除,并让该进程重新属于初始的父进程。

  3. 调用__exit_signal()删除所有的挂起信号并释放进程的signal_struct描述符。如果该描述符不再被其它的轻量级进程使用,函数进一步删除这个数据结构。此外,函数调用exit_itimers()从进程中剥离掉所有的POSIX时间间隔定时器。

  4. 调用__exit_sighand()删除信号处理函数。

  5. 调用__unhash_process(),该函数依次执行下面的操作:

a.变量nr_threads减。

b.两次调用detach_pid(),分别从PIDTYPE_PIDPIDTYPE_TGID类型的PID散列表中删除进程描述符。

c.如果进程是线程组的领头进程,那么再调用两次detach_pid(),PIDTYPE_PGIDPIDTYPE_SID类型的散列表中删除进程描述符。

d.用宏REMOVE_LINKS从进程链表中解除进程描述符的链接。

  1. 如果进程不是线程的领头进程,领头进程处于僵死状态,而且进程是线程组的最后一个成员,则该函数向领头进程的父进程发送一个信号,通知它进程已终止。

  2. 调用sched_exit()函数来调整父进程的时间片。

  3. 调用put_task_struct()递减进程描述符的使用计数器,如果计数器变为0,则函数终止所有残留的对进程的引用。

a.递减进程所有都的user_struct数据结构的使用计数器,如果使用计数器变成0,就释放该数据结构。

b.释放进程描述符以及thread_info描述符和内核堆栈所占用的内存区域。