linux php 进程初探(四)进程退出

进程状态

根据进程的定义,我们知道进程是代码运行的实体,而进程有可能是正在运行的,也可能是已经停止的,这就是进程的状态。

  1. 查看Linux源码。进程状态的定义在fs/proc/array.c文件中。
/*
 * The task state array is a strange "bitmap" of
 * reasons to sleep. Thus "running" is zero, and
 * you can test for combinations of others with
 * simple bit tests.
 */
static const char * const task_state_array[] = {
	"R (running)",		/*   0 */
	"S (sleeping)",		/*   1 */
	"D (disk sleep)",	/*   2 */
	"T (stopped)",		/*   4 */
	"t (tracing stop)",	/*   8 */
	"X (dead)",		/*  16 */
	"Z (zombie)",		/*  32 */
};

查看状态

通过ps aux可以看到进程的状态。
O:进程正在处理器运行,这个状态从来没有见过
S:休眠状态(sleeping)
R:等待运行(runable) R Running or rumnable (onrun queue)进程处于运行或就绪状态
I:空闲状态(idle)
Z:僵尸状态(zombie)
T:跟踪状态(Traced)
B:进程正在等待更多的内存页
D:不可中断的深度睡眠,一般由10引起,同步IO在做读或写操作时, cpu不能做其它事情,只能等待,这时进程处于这种状态,如果程序采用异步IO,这种状态应该就很少见到了

  1. 其中就绪状态表示进程已经分配到除CPU以外的资源,等CPU调度它时就可以马上执行了。
  2. 运行状态就是正在运行了,获得包括CPU在内的所有资源。
  3. 等待状态表示因等待某个事件而没有被执行,这时候不耗CPU时间,而这个时间有可能是等待IO、申请不到足够的缓冲区或者在等待信号。

进程退出

进程转换

  1. 进程的运行过程也就是进程状态转换的过程。例如就绪状态的进程只要等到CPU调度它时就马上转为运行状态,一旦它需要的IO操作还没有返回时,进程状态也就转换成等待状态。进程状态间转换还有很多

基本概念

  1. 一个程序启动之后,会变成一个进程,进程在什么情况下会退出呢?

以下情况下进程会退出:

  1. 运行到最后一行语句
  2. 运行时遇到return
  3. 运行时遇到exit()函数的时候
  4. 程序异常的时候
  5. 进程接收到中断信号
    c/c++中还有更多进程退出的情况,本文暂且不表,我们这里只关注php中进程退出的情况
  1. 一个进程要么是正常结束,要么是异常结束(异常结束大多跟信号有关),不管是何种方式导致进程退出,它都有一个终止状态码,进程结束时并不会真的退出,还会驻留在内存中,父进程可以使用wait(php中使用 pcntl_wait 函数)来获取进程终止状态码,同时该函数会释放终止进程的内存空间。否则会容易造成 僵尸进程,占用大量内存空间。

  2. 下面们来编写一个会生成僵尸进程的脚本

//fork 一个子进程
$pid = pcntl_fork();

if($pid > 0){
//父进程运行逻辑

//修改进程名称,方便区分
cli_set_process_title("php parent process");
$process_pid = posix_getpid();
printf("我是父进程,pid=%d \n", $process_pid);
//让主进程死循环
while(1){
    ;
}
}else if($pid == 0){
//子进程运行逻辑
cli_set_process_title("php child process");
$child_pid = posix_getpid();
printf("我是子进程,pid=%d \n", $child_pid);
}else{
echo "进程fork错误!".PHP_EOL;
}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  1. 执行脚本,然后执行pstree -ap 查看进程树中进程pid为20074的进程有个括号,这个代表子进程20074为僵尸进程,或者也可以使用 ps -aux 查看20074子进程状态为Z+ 代表为僵尸进程后面还有个<defunct> 单词意思是死者也表示该进程已经是僵尸进程。

一个进程运行时,会动态在/proc/文件夹生成进程目录
在这里插入图片描述

进入僵尸进程pid目录 cd 20074,查看命令行启动文件cat cmdline 返回为空,因为进程已经死亡,但是进程文件还存在服务器中并没有清除,如果存在大量僵尸进程可能挤满内存和存储空间
在这里插入图片描述

进入一个正常进程目录 cd 20073 ,查看命令行启动文件cat cmdline 是正常返回
在这里插入图片描述

  1. 怎么解决这个问题呢?
  1. 使用pcntl_wait 解决
  2. 使用pcntl_wait 存在下面几种情况
  1. 如果没有子进程,调用pcntl_wait可能返回错误
  2. 如果子进程还没有结束,调用pcntl_wait就会阻塞父进程
  3. 给pcntl_wait函数传递第三个参数option可以让父进程不阻塞。
//fork 一个子进程
$pid = pcntl_fork();

if($pid > 0){
//父进程运行逻辑

//修改进程名称,方便区分
cli_set_process_title("php parent process");
$process_pid = posix_getpid();
printf("我是父进程,pid=%d \n", $process_pid);
$exitPid = pcntl_wait($status);
if($exitPid > 0){
printf("pid=%d,子进程已经挂了,它的终止状态码:%d,并且已经完全释放了它占用的资源 \n", $pid, $status);
//阻塞父进程方便查看子进程状态
while(1){
    sleep(3);
    printf("执行中... \n");
}
}else{
printf("waut error...\n");
}
}else if($pid == 0){
//子进程运行逻辑
cli_set_process_title("php child process");
$child_pid = posix_getpid();
printf("我是子进程,pid=%d \n", $child_pid);
}else{
echo "进程fork错误!".PHP_EOL;
}

在这里插入图片描述

进入子进程目录查看是否回收,发现文件不存在,代表已经回收
在这里插入图片描述

  1. 接下来咱们使用 exit() 函数来手动定义退出状态码
//fork 一个子进程
$pid = pcntl_fork();

if($pid > 0){
//父进程运行逻辑

//修改进程名称,方便区分
cli_set_process_title("php parent process");
$exitPid = pcntl_wait($status);
if($exitPid > 0){
printf("pid=%d,子进程已经挂了,它的终止状态码:%d,并且已经完全释放了它占用的资源 \n", $pid, $status);
//阻塞父进程方便查看子进程状态
while(1){
    sleep(3);
    printf("执行中... \n");
}
}else{
printf("waut error...\n");
}
}else if($pid == 0){
//子进程运行逻辑
cli_set_process_title("php child process");
$child_pid = posix_getpid();
printf("我是子进程,pid=%d \n", $child_pid);
//测试进程退出状态码
exit(10); //0表示成功 -1表示失败 最大255
}else{
echo "进程fork错误!".PHP_EOL;
}

执行后发现终止状态码并不是10,而是2560,,这是为什么?
在这里插入图片描述
这是因为我们不能直接打印status,必须使用 pcntl_wexitstatus函数配合使用,执行结果就是我们定义进程退出码10
在这里插入图片描述

$pid = pcntl_fork();

if($pid > 0){
//父进程运行逻辑

//修改进程名称,方便区分
cli_set_process_title("php parent process");

$process_pid = posix_getpid();
printf("我是父进程,pid=%d \n", $process_pid);
$exitPid = pcntl_wait($status);
if($exitPid > 0){
printf("pid=%d,子进程已经挂了,它的终止状态码:%d,并且已经完全释放了它占用的资源 \n", $pid, pcntl_wexitstatus($status));
}else{
printf("waut error...\n");
}

//阻塞父进程方便查看子进程状态
while(1){
    sleep(3);
    printf("执行中... \n");
}

}else if($pid == 0){
//子进程运行逻辑
cli_set_process_title("php child process");
$child_pid = posix_getpid();
printf("我是子进程,pid=%d \n", $child_pid);
exit(10); //0表示成功 -1表示失败 最大255
}else{
echo "进程fork错误!".PHP_EOL;
}
  1. 接下来我们思考一下,因为父进程中pcntl_wait函数是阻塞的,子进程如果也阻塞,会导致什么情况?
  1. 父进程将一直阻塞无法做其他操作
  2. 所以我们需要在pcntl_wait函数中加第二个参数 WNOHANG
$pid = pcntl_fork();

if($pid > 0){
//父进程运行逻辑

//修改进程名称,方便区分
cli_set_process_title("php parent process");

$process_pid = posix_getpid();
printf("我是父进程,pid=%d \n", $process_pid);

//阻塞父进程方便查看子进程状态
while(1){
//如果pcntl_wait函数以不阻塞方式运行,子进程退出返回退出的子进程
//如果没有子进程退出返回0
$exitPid = pcntl_wait($status, WNOHANG);
if($exitPid > 0){
printf("pid=%d,子进程已经挂了,它的终止状态码:%d,并且已经完全释放了它占用的资源 \n", $pid, pcntl_wexitstatus($status));
}else if(0 == $exitPid){
printf("等待子进程退出... \n");
sleep(1);
}else{
printf("waut error...\n");
}
}
printf("运行中... \n");
}else if($pid == 0){
//模拟子进程阻塞
while(1){
    ;
}
//子进程运行逻辑
cli_set_process_title("php child process");
$child_pid = posix_getpid();
printf("我是子进程,pid=%d \n", $child_pid);
exit(10); //0表示成功 -1表示失败 最大255
}else{
echo "进程fork错误!".PHP_EOL;
}
  1. 我们也可以直接使用break 直接使进程退出
$pid = pcntl_fork();

if($pid > 0){
//父进程运行逻辑

//修改进程名称,方便区分
cli_set_process_title("php parent process");

$process_pid = posix_getpid();
printf("我是父进程,pid=%d \n", $process_pid);

//阻塞父进程方便查看子进程状态
while(1){
//如果pcntl_wait函数以不阻塞方式运行,子进程退出返回退出的子进程
//如果没有子进程退出返回0
$exitPid = pcntl_wait($status, WNOHANG);
if($exitPid > 0){
printf("pid=%d,子进程已经挂了,它的终止状态码:%d,并且已经完全释放了它占用的资源 \n", $pid, pcntl_wexitstatus($status));
break;
}else if(0 == $exitPid){
printf("等待子进程退出... \n");
sleep(1);
}else{
printf("waut error...\n");
}
}
printf("运行中... \n");
}else if($pid == 0){
//子进程运行逻辑
cli_set_process_title("php child process");
$child_pid = posix_getpid();
printf("我是子进程,pid=%d \n", $child_pid);
exit(10); //0表示成功 -1表示失败 最大255
}else{
echo "进程fork错误!".PHP_EOL;
}

在这里插入图片描述

  1. 接下来我们演示pcntl_wait() 没有子进程返回-1的情况
$exitPid = pcntl_wait($status);
printf("没有子进程pcntl_wait退出返回: $exitPid");

在这里插入图片描述

还可以组合使用pcntl_errno()pcntl_strerror() 函数查看准确出错内容

<?php
$exitPid = pcntl_wait($status);
$errno = pcntl_errno();
printf("具体错误:".pcntl_strerror($errno).PHP_EOL);
printf("没有子进程pcntl_wait退出返回: $exitPid \n");

在这里插入图片描述

  1. 信号影响进程退出方式
  1. 我们可以通过函数判断进程退出方式,可以获取中断信号编号
  2. 在终端可以使用kill 命令发送信号,kill 命令是用来发送信号并不是杀死进程用的,使用 kill -l查看支持信号列表,我们经常使用的信号都是编号31以下的
    在这里插入图片描述
    pcntl_wifexited 函数只能判断正常退出情况,所有我们需要使用 pcntl_wifsignaled 函数来判断中断信号退出的情况,中断状态编号也需要使用 pcntl_wtermsig 函数获取
$pid = pcntl_fork();

if($pid > 0){
//父进程运行逻辑

//修改进程名称,方便区分
cli_set_process_title("php parent process");

$process_pid = posix_getpid();

//阻塞父进程方便查看子进程状态
while(1){
//如果pcntl_wait函数以不阻塞方式运行,子进程退出返回退出的子进程
//如果没有子进程退出返回0
$exitPid = pcntl_wait($status, WNOHANG);
//pcntl_wifexited函数  检查状态代码是否代表一个正常的退出。
if($exitPid > 0){
if(pcntl_wifexited($status)){
printf("pid=%d,子进程已经挂了,属于正常退出,它的终止状态码:%d \n", $pid, pcntl_wexitstatus($status));
}else if(pcntl_wifsignaled($status)){
printf("pid=%d,子进程已经挂了,属于中断信号退出,它的信号编号是:%d \n", $pid, pcntl_wtermsig($status));
}
}else if(0 == $exitPid){
printf("我是父进程,pid=%d \n", $process_pid);

}else{
printf("waut error...\n");
}
sleep(1);
}
}else if($pid == 0){
//子进程运行逻辑
cli_set_process_title("php child process");
$child_pid = posix_getpid();
while(1){
printf("我是子进程,pid=%d \n", $child_pid);
sleep(2);
}
}else{
echo "进程fork错误!".PHP_EOL;
}

在这里插入图片描述

可以看到执行结果,脚本捕获到了编号为12的这个信号,也就是 SIGUSR2
需要注意的是还有一种进程停止信号需要额外做判断,需要使用 pcntl_wstopsigpcntl_wifstopped 这两函数做处理, 并且pcntl_wait 函数的第二个参数必须为 WUNTRACED,也就是阻塞模式,才能有效捕获

$pid = pcntl_fork();

if($pid > 0){
//父进程运行逻辑

//修改进程名称,方便区分
cli_set_process_title("php parent process");

$process_pid = posix_getpid();

//阻塞父进程方便查看子进程状态
while(1){
//如果pcntl_wait函数以不阻塞方式运行,子进程退出返回退出的子进程
//如果没有子进程退出返回0
$exitPid = pcntl_wait($status, WUNTRACED);
//pcntl_wifexited函数  检查状态代码是否代表一个正常的退出。
if($exitPid > 0){
if(pcntl_wifexited($status)){//正常退出判断
printf("pid=%d,子进程已经挂了,属于正常退出,它的终止状态码:%d \n", $pid, pcntl_wexitstatus($status));
}else if(pcntl_wifsignaled($status)){//中断退出判断
printf("pid=%d,子进程已经挂了,属于中断信号退出1,它的信号编号是:%d \n", $pid, pcntl_wtermsig($status));
}else if(pcntl_wifstopped($status)){//进程停止信号判断 一般是发送SIGSTOP SIGTSTP
printf("pid=%d,子进程已经停止,属于中断信号退出2,它的信号编号是:%d \n", $pid, pcntl_wstopsig($status));
}
}else if(0 == $exitPid){
printf("我是父进程,pid=%d \n", $process_pid);

}else{
printf("waut error...\n");
}
sleep(1);
}
}else if($pid == 0){
//子进程运行逻辑
cli_set_process_title("php child process");
$child_pid = posix_getpid();
while(1){
printf("我是子进程,pid=%d \n", $child_pid);
sleep(2);
}
}else{
echo "进程fork错误!".PHP_EOL;
}

通过图片可以看到脚本收到进程停止信号,子进程从运行状态转为停止状态
在这里插入图片描述

我们还可以使用 kill -s SIGCONT 29390 信号恢复进程运行状态
在这里插入图片描述

如有错误请指正,一起进步

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值