date: 2014-10-27 10:16linux
这部分详情请参考APUE(第2版)第8章shell
有2个函数用来正常终止一个进程:_exit当即进入内核,exit则先执行一些清理工做,包括调用执行各终止处理程序(经过atexit函数注册)和关闭全部标准I/O流(为全部打开流执行fclose函数),而后调用_exit进入内核。编程
<unistd.h> void _exit(int status); <stdlib.h> void exit(int status);
两个exit函数都带有一个int类型的参数,称之为终止状态或退出状态(exit_status)。main函数中返回一个整型值与用该值调用exit是等价的。因而main函数中,exit(0)等价于return 0。session
此外,进程也可能由于其余一些状况而异常终止(好比收到一个越界访问的信号SIGSEGV)。无论进程如何终止,最后都会执行内核中的同一段代码(此即后面要讨论的系统调用exit的内核代码)。这段代码关闭进程全部打开的文件描述,并释放掉进程所占用的资源。函数
无论进程如何终止,咱们都但愿进程能通知其父进程,能够理解为子进程去世时给父进程发一个“报丧”信号,告之本身是如何终止的。父进程能够调用wait函数获取子进程的退出状态。线程
讨论下面三个特别的问题:设计
有4个wait相关的函数(此外还有一个waitid函数,这里没列出来,具体参考APUE)code
<wait.h> pid_t wait4 (pid_t pid, int *status, int options, struct rusage *rusage); pid_t wait( int* status ); pid_t wait3(int* status, int options, struct rusage* rusage); pid_t waitpid(pid_t pid, int* status, int options);
wait用来等待任一子进程终止,waitpid可用来等待特定的子进程退出(固然也能够等待任意子进程退出),wait3多了一个rusage参数,要求内核返回由终进程及其全部子进程使用的资源汇总。这三个函数都是经过系统调用wait4来实现,咱们来分析下wait4的参数:blog
这四个函数的返回值都是对应终止子进程的pid,父进程据此能够知道哪一个子进程终止了。继承
每一个进程除了有一个进程ID以外,还属于一个进程组。进程组是一个或多个进程的集合,每一个进程组有一个惟一标识进程组ID,进程组ID相似于进程ID,可存放在pid_t数据类型中。进程task_struct结构中pgrp成员即表示进程所属进程组的ID。
每一个进程组均可以有一个组长进程。组长进程的标识是,其进程ID等于其进程组ID。
一个进程能够调用setpgid来加入一个现有组(做为组的成员)或者建立一个新的进程组(做为组长)。
一个用户login到系统中之后,可能会启动许多不一样的进程(组),全部这些进程使用同一个控制终端(或用来模拟一个终端的窗口),这些使用同一个控制终端的进程(组)属于同一个会话(session)。
会话能够是一个或多个进程组的集合。一般由shell的管道线将几个进程编程一组。一个会话中的几个进程组能够分为一个前台进程组以及若干个后台进程组。好比以下shell命令
proc1 | proc2 & proc3 | proc4 | proc5
将构成一个会话,该会话中有三个进程组:
一个会话也有一个惟一标识session ID,相似进程组ID,也能够存放在pid_t数据类型中。进程task_struct结构中session成员即表示进程所属会话。一个会话有一个会话首进程(session leader),会话首进程是建立该会话的进程,其task_struct结构中的leader成员非0。
根据对进程控制原语的了解,以及进程建立过程的了解,不难想象出exit所要作的工做:
另外进程调用exit表示进程要最终退出历史舞台了,意即当前进程在exit函数的执行过程当中逐步走向消亡,不会从这个函数中返回了。
exit系统调用内核入口为sys_exit:
<kernel/exit.c> asmlinkage long sys_exit(int error_code) { do_exit((error_code&0xff)<<8); }
可见其核心是do_exit,do_exit的主要流程以下:
关于关于流程图,在重点讨论下几个问题。
task_struct结构中有两个成员用来指向其父进程,p_oppt和p_pptr,前者能够理解为进程的生父(orginal parent),后者能够理解为进程的养父或者监护人。在进程建立之初,进程的生父与监护人一致。但在运行中,进程的监护人能够暂时改变。好比一个进程经过系统调用ptrace来跟踪另外一进程时,被跟踪继承的p_pptr将被设置为跟踪进程,跟踪进程暂时成了被跟踪进程的监护人,而被跟踪进程的生父仍然不变。
有趣的是,在判断当前进程所在的进程组是否为孤儿进程组、在给父进程发报丧信号时以及将子进程加入新的家谱时(在此以前,已经将子进程的p_pptr设置为子进程的p_opptr),都只认监护人p_pptr,而不多关注其生父p_opptr。看来进程行事时只认其监护人而不认其亲生父亲,与现实世界何其类似也。
一方面,进程的task_struct结构中有不少统计信息,好比CPU使用时间等,让父进程来料理后事能够将这些信息并入父进程的统计信息而不至于丢失;另外一方面,也是更重要的一方面,不管如何系统必须得有一个当前进程,在中断以及异常的服务程序中要用到当前进程的系统空间堆栈。若是在下一个进程投入运行以前,就把当前进程的系统空间回收,这样就存在一个空档,若是恰巧此时有中断发生就会形成问题。
进程在调用exit以后,系统还保留着进程的尸体等待其父进程来料理后事,父进程正在wait4中等着哩。
了解了wait4的原语,理解其内核实现应该很容易了。wait4的内核入口是sys_wait,一样定义在exit.c文件中。其主要流程以下:
函数的主题为两层循环,若是当前进程为线程,外层循环则遍历同线程组全部进程。内存循环是变量进程的全部子进程。还记得进程的家谱吗,经过进程的家谱则能够遍历全部子进程。当知足下列条件之一时,经过goto end_wait4来结束这个系统调用:
不然,当前进程将本身的状态设置为TASK_INTERRUPTIBLE,并再循环外调用schedule来进入浅度睡眠而让其余的进程先运行。别忘了,在此以前(sys_wait4函数开始处)定义了一个等待节点wait,并加入了当前进程的等待队列头wait_chldexit。此后,若是有子进程退出,子进程调用do_notify_parent来通知父进程。父进程被唤醒后,继续从repeat标号处从新开始执行。
等待队列节点wait_queue_t类型以及等待队列头wait_queue_head_t类型定义在<include/linux/wait.h>中:
struct __wait_queue { unsigned int flags; #define WQ_FLAG_EXCLUSIVE 0x01 struct task_struct * task; struct list_head task_list; #if WAITQUEUE_DEBUG long __magic; long __waker; #endif }; typedef struct __wait_queue wait_queue_t; struct __wait_queue_head { wq_lock_t lock; struct list_head task_list; #if WAITQUEUE_DEBUG long __magic; long __creator; #endif }; typedef struct __wait_queue_head wait_queue_head_t;
等待队列节点经过task_list链入到等待队列头所领衔的链表中,同时每一个等待队列节点都关联了一个进程的task_struct结构,当经过wake_up系列函数来唤醒等待队列头所领衔的等待队列时,将唤醒全部或者其中一个等待节点(若是传入WQ_FLAG_EXCLUSIVE标志将独占唤醒,只唤醒其中一个节点)所关联的进程。