进程调度之5:系统调用exit与wait4
2018-07-16 18:54
337 查看
date: 2014-10-27 10:16
两个exit函数都带有一个int类型的参数,称之为终止状态或退出状态(exit_status)。main函数中返回一个整型值与用该值调用exit是等价的。于是main函数中,exit(0)等价于return 0。
此外,进程也可能因为其他一些情况而异常终止(比如收到一个越界访问的信号SIGSEGV)。不管进程如何终止,最后都会执行内核中的同一段代码(此即后面要讨论的系统调用exit的内核代码)。这段代码关闭进程所有打开的文件描述,并释放掉进程所占用的资源。
不管进程如何终止,我们都希望进程能通知其父进程,可以理解为子进程去世时给父进程发一个“报丧”信号,告之自己是如何终止的。父进程可以调用wait函数获取子进程的退出状态。
讨论下面三个特别的问题:
如果父进程在子进程退出之前退出呢?子进程退出时该把报丧信号发给谁?这种情况下将由init进程“领养”父进程的所有子进程。
如果子进程已经终止了,但父进程没有调用wait函数获取它的终止状态又如何?内核为每个终止进程保存了一定量的信息,包括子进程的ID、进程终止状态以及进程使用的CPU时间总量,可以理解为子进程虽已去世,但还留着“尸体”等着父进程“收尸”。尸体要保留到父进程调用wait函数来收尸为止,在此之前,该子进程便成为一个僵尸进程(zombile)。
如果被init进程“领养”的进程终止了,系统中岂不会有大量的僵尸进程?不用担心,init进程被设计成“无论何时,只要有一个子进程终止,init就会调用wait函数来为之收尸”,从而防止了在系统中有很多僵尸进程。
wait用来等待任一子进程终止,waitpid可用来等待特定的子进程退出(当然也可以等待任意子进程退出),wait3多了一个rusage参数,要求内核返回由终进程及其所有子进程使用的资源汇总。这三个函数都是通过系统调用wait4来实现,我们来分析下wait4的参数:
这四个函数的返回值都是对应终止子进程的pid,父进程据此可以知道哪个子进程终止了。
每个进程组都可以有一个组长进程。组长进程的标识是,其进程ID等于其进程组ID。
一个进程可以调用setpgid来加入一个现有组(作为组的成员)或者创建一个新的进程组(作为组长)。
一个用户login到系统中以后,可能会启动许多不同的进程(组),所有这些进程使用同一个控制终端(或用来模拟一个终端的窗口),这些使用同一个控制终端的进程(组)属于同一个会话(session)。
会话可以是一个或多个进程组的集合。通常由shell的管道线将几个进程编程一组。一个会话中的几个进程组可以分为一个前台进程组以及若干个后台进程组。比如如下shell命令
将构成一个会话,该会话中有三个进程组:
其一、前台进程组即{proc3, proc4, proc5},它们在控制终端的前台运行;
其二、后台进程组{proc1, proc2};
其三、shell进程单成一个后台进程组。
一个会话也有一个唯一标识session ID,类似进程组ID,也可以存放在pid_t数据类型中。进程task_struct结构中session成员即表示进程所属会话。一个会话有一个会话首进程(session leader),会话首进程是创建该会话的进程,其task_struct结构中的leader成员非0。
根据进程的财产登记表卡task_struct结构清算进程财产并回收;
解散进程的家谱,将该进程的子进程交由init进程“领养”。
保留尸体(task_struct结构本身以及task_struct结构所在的两个页面)并给父进程发报丧信号,等到父进程来收尸。
当前进程终止了,当然需要调度器启动一次调度。
另外进程调用exit表示进程要最终退出历史舞台了,意即当前进程在exit函数的执行过程中逐步走向消亡,不会从这个函数中返回了。
可见其核心是do_exit,do_exit的主要流程如下:
关于关于流程图,在重点讨论下几个问题。
有趣的是,在判断当前进程所在的进程组是否为孤儿进程组、在给父进程发报丧信号时以及将子进程加入新的家谱时(在此之前,已经将子进程的p_pptr设置为子进程的p_opptr),都只认监护人p_pptr,而很少关注其生父p_opptr。看来进程行事时只认其监护人而不认其亲生父亲,与现实世界何其相似也。
了解了wait4的原语,理解其内核实现应该很容易了。wait4的内核入口是sys_wait,同样定义在exit.c文件中。其主要流程如下:
函数的主题为两层循环,如果当前进程为线程,外层循环则遍历同线程组所有进程。内存循环是变量进程的所有子进程。还记得进程的家谱吗,通过进程的家谱则可以遍历所有子进程。当满足下列条件之一时,通过goto end_wait4来结束这个系统调用:
所等待的子进程状态为TASK_STOPPED或者为TASK_ZOMBIE;
所等待的子进程存在,但不在上述两个状态;而入参options设置了WNOHANG标志,表示非阻塞。
所等待的子进程不存在(进程号为pid的进程或者不存在,或者不是当前进程的子进程)。
否则,当前进程将自己的状态设置为TASK_INTERRUPTIBLE,并再循环外调用schedule来进入浅度睡眠而让其他的进程先运行。别忘了,在此之前(sys_wait4函数开始处)定义了一个等待节点wait,并加入了当前进程的等待队列头wait_chldexit。此后,如果有子进程退出,子进程调用do_notify_parent来通知父进程。父进程被唤醒后,继续从repeat标号处重新开始执行。
等待队列节点wait_queue_t类型以及等待队列头wait_queue_head_t类型定义在<include/linux/wait.h>中:
等待队列节点通过task_list链入到等待队列头所领衔的链表中,同时每个等待队列节点都关联了一个进程的task_struct结构,当通过wake_up系列函数来唤醒等待队列头所领衔的等待队列时,将唤醒所有或者其中一个等待节点(如果传入WQ_FLAG_EXCLUSIVE标志将独占唤醒,只唤醒其中一个节点)所关联的进程。
1 进程控制原语
这部分详情请参考APUE(第2版)第8章1.1 进程退出
有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。
此外,进程也可能因为其他一些情况而异常终止(比如收到一个越界访问的信号SIGSEGV)。不管进程如何终止,最后都会执行内核中的同一段代码(此即后面要讨论的系统调用exit的内核代码)。这段代码关闭进程所有打开的文件描述,并释放掉进程所占用的资源。
不管进程如何终止,我们都希望进程能通知其父进程,可以理解为子进程去世时给父进程发一个“报丧”信号,告之自己是如何终止的。父进程可以调用wait函数获取子进程的退出状态。
讨论下面三个特别的问题:
如果父进程在子进程退出之前退出呢?子进程退出时该把报丧信号发给谁?这种情况下将由init进程“领养”父进程的所有子进程。
如果子进程已经终止了,但父进程没有调用wait函数获取它的终止状态又如何?内核为每个终止进程保存了一定量的信息,包括子进程的ID、进程终止状态以及进程使用的CPU时间总量,可以理解为子进程虽已去世,但还留着“尸体”等着父进程“收尸”。尸体要保留到父进程调用wait函数来收尸为止,在此之前,该子进程便成为一个僵尸进程(zombile)。
如果被init进程“领养”的进程终止了,系统中岂不会有大量的僵尸进程?不用担心,init进程被设计成“无论何时,只要有一个子进程终止,init就会调用wait函数来为之收尸”,从而防止了在系统中有很多僵尸进程。
1.2 等待子进程终止
有4个wait相关的函数(此外还有一个waitid函数,这里没列出来,具体参考APUE)<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的参数:
这四个函数的返回值都是对应终止子进程的pid,父进程据此可以知道哪个子进程终止了。
1.3 进程组与会话
每个进程除了有一个进程ID之外,还属于一个进程组。进程组是一个或多个进程的集合,每个进程组有一个唯一标识进程组ID,进程组ID类似于进程ID,可存放在pid_t数据类型中。进程task_struct结构中pgrp成员即表示进程所属进程组的ID。每个进程组都可以有一个组长进程。组长进程的标识是,其进程ID等于其进程组ID。
一个进程可以调用setpgid来加入一个现有组(作为组的成员)或者创建一个新的进程组(作为组长)。
一个用户login到系统中以后,可能会启动许多不同的进程(组),所有这些进程使用同一个控制终端(或用来模拟一个终端的窗口),这些使用同一个控制终端的进程(组)属于同一个会话(session)。
会话可以是一个或多个进程组的集合。通常由shell的管道线将几个进程编程一组。一个会话中的几个进程组可以分为一个前台进程组以及若干个后台进程组。比如如下shell命令
proc1 | proc2 & proc3 | proc4 | proc5
将构成一个会话,该会话中有三个进程组:
其一、前台进程组即{proc3, proc4, proc5},它们在控制终端的前台运行;
其二、后台进程组{proc1, proc2};
其三、shell进程单成一个后台进程组。
一个会话也有一个唯一标识session ID,类似进程组ID,也可以存放在pid_t数据类型中。进程task_struct结构中session成员即表示进程所属会话。一个会话有一个会话首进程(session leader),会话首进程是创建该会话的进程,其task_struct结构中的leader成员非0。
2 系统调用exit
根据对进程控制原语的了解,以及进程创建过程的了解,不难想象出exit所要做的工作:根据进程的财产登记表卡task_struct结构清算进程财产并回收;
解散进程的家谱,将该进程的子进程交由init进程“领养”。
保留尸体(task_struct结构本身以及task_struct结构所在的两个页面)并给父进程发报丧信号,等到父进程来收尸。
当前进程终止了,当然需要调度器启动一次调度。
另外进程调用exit表示进程要最终退出历史舞台了,意即当前进程在exit函数的执行过程中逐步走向消亡,不会从这个函数中返回了。
2.1 主要流程
exit系统调用内核入口为sys_exit:<kernel/exit.c> asmlinkage long sys_exit(int error_code) { do_exit((error_code&0xff)<<8); }
可见其核心是do_exit,do_exit的主要流程如下:
关于关于流程图,在重点讨论下几个问题。
2.2 进程的p_opptr与p_pptr
task_struct结构中有两个成员用来指向其父进程,p_oppt和p_pptr,前者可以理解为进程的生父(orginal parent),后者可以理解为进程的养父或者监护人。在进程创建之初,进程的生父与监护人一致。但在运行中,进程的监护人可以暂时改变。比如一个进程通过系统调用ptrace来跟踪另一进程时,被跟踪继承的p_pptr将被设置为跟踪进程,跟踪进程暂时成了被跟踪进程的监护人,而被跟踪进程的生父仍然不变。有趣的是,在判断当前进程所在的进程组是否为孤儿进程组、在给父进程发报丧信号时以及将子进程加入新的家谱时(在此之前,已经将子进程的p_pptr设置为子进程的p_opptr),都只认监护人p_pptr,而很少关注其生父p_opptr。看来进程行事时只认其监护人而不认其亲生父亲,与现实世界何其相似也。
2.3 为什么要让父进程来收尸(task_struct结构以及其所在的系统空间的两个页面),而不是子进程自行消亡?##
一方面,进程的task_struct结构中有很多统计信息,比如CPU使用时间等,让父进程来料理后事可以将这些信息并入父进程的统计信息而不至于丢失;另一方面,也是更重要的一方面,无论如何系统必须得有一个当前进程,在中断以及异常的服务程序中要用到当前进程的系统空间堆栈。如果在下一个进程投入运行之前,就把当前进程的系统空间回收,这样就存在一个空档,如果恰巧此时有中断发生就会造成问题。3 系统调用wait4
进程在调用exit之后,系统还保留着进程的尸体等待其父进程来料理后事,父进程正在wait4中等着哩。了解了wait4的原语,理解其内核实现应该很容易了。wait4的内核入口是sys_wait,同样定义在exit.c文件中。其主要流程如下:
函数的主题为两层循环,如果当前进程为线程,外层循环则遍历同线程组所有进程。内存循环是变量进程的所有子进程。还记得进程的家谱吗,通过进程的家谱则可以遍历所有子进程。当满足下列条件之一时,通过goto end_wait4来结束这个系统调用:
所等待的子进程状态为TASK_STOPPED或者为TASK_ZOMBIE;
所等待的子进程存在,但不在上述两个状态;而入参options设置了WNOHANG标志,表示非阻塞。
所等待的子进程不存在(进程号为pid的进程或者不存在,或者不是当前进程的子进程)。
否则,当前进程将自己的状态设置为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标志将独占唤醒,只唤醒其中一个节点)所关联的进程。
相关文章推荐
- 操作系统实践(9)——进程、多进程、系统调用、进程调度
- 【进程管理】系统调用exit()
- 内核线程&&系统调用exit&&wait4&&撤销进程
- 调度进程的系统调用
- 【进程管理】系统调用wait4()
- Linux内核学习之四--进程、进程调度、系统调用、proc文件系统和内核异常分析
- 进程调度之3:系统调用fork、vfork与clone
- 进程调度之 4:系统调用execve
- 4.5 进程调度_与调度相关的系统调用
- 关闭系统进程,以及如何调用cmd并执行命令
- Linux下进程相关的系统调用
- 进程与系统调用、进程间通信--Head First C读书笔记
- 基于Linux系统中进程调度分析
- exit和_exit 进程终止有5种方法: 1正常终止 (1)从main函数返回 (2)调用exit (3)调用_exit 2异常终止 (1)调用abort (2)由一个信号来终止 exi
- 时间系统、进程的调度与切换
- Linux用户进程与系统调用
- 避免linux系统调用fork后产生僵死进程
- 进程优先级之getpriority系统调用
- Linux系统进程控制编程---system系统调用
- SYSEXIT——快速系统调用的快速返回