0
点赞
收藏
分享

微信扫一扫

linux php 进程进阶(五) signal(信号)

夕颜合欢落 2022-02-13 阅读 62
linuxphp

linux php 进程进阶(五) signal(信号)

signal

信号与中断流程介绍

信号 是指软件中断信号,简称软中断

  1. 中断源(中断信号产生位置):
  1. 软件产生
  1. 中断响应(对信号的处理)
  1. 中断返回:中断信号处理程序(信号处理函数,信号捕捉函数)完成后,就会返回继续执行主函数

  2. 中断处理过程图
    中断源向进程发出中断信号称为 中断请求信号
    进程接收到中断信号可以进行响应,称为 中断响应
    为中断服务的程序称为 中断处理程序 或叫 中断处理函数 | 中断服务程序
    执行完中断处理函数后放回,中断放回

常用中断信号

信号介绍
SIGTSTP交互停止信号,终端挂起键 ctrl+z 终端驱动产生此信号 【终端停止符】 终止+core(core 文件是用来记录进程异常或者错误的文件方便使用gdb来调试)
SIGTERM可以被捕捉,让程序先清理一些工作再终止
SIGSTOP作业控制信号,也是停止一个进程,跟SIGSTSP 一样
SIGQUIT退出键 ctrl+\ 终端驱动程序产生此信号,同时产生core文件(终端退出符)
SIGINT中断键 delete / ctrl+c (终端中断符)
SIGCHLD子进程终止时返回 (发送信号到父进程)
SIGUSR1,SIGUSR2用户自定义信号
SIGKILL,SIGSTOP不能被捕捉及忽略的,主要用于让进程可靠的终止和停止

可以编写一个php脚本使用 kill 命令来测试上面的信号

//demo1.php
fprintf(STDOUT,"PID: ".posix_getpid().PHP_EOL);
while(1){
    ;
}
  1. 自定义信号 SIGUSR1
    在这里插入图片描述

  2. 作业控制信号 SIGSTOP 停止进程(停止并不是结束 使用 ps -aux|grep php 发现进程只是停止状态)

中断信号处理程序

  1. pcntl_signal() 信号处理函数
# 安装信号处理器;
pcntl_signal(SIGINT, function($signo){
    fprintf(STDOUT,"我接收到一个信号,编号为:%d \n", $signo);
});
// 循环等待信号
while(1){
 # 分发
  pcntl_signal_dispatch();
  fprintf(STDOUT,posix_getpid()."进程运行中\n");
  sleep(1);
}

执行php脚本 按下ctrl+c 或者使用 kill -s SIGINT 31193 发送信号,pcntl_signal 函数捕获信号

在这里插入图片描述

  1. 我们知道 fork() 函数通过系统调用创建一个与原来进程几乎完全相同的进程,那父进程的信号处理程序 子进程会继承吗?
function sigHandler($signo){
    fprintf(STDOUT,"pid:".posix_getpid()."接收到一个信号,编号为:%d \n", $signo);
}
# 安装信号处理器;
pcntl_signal(SIGINT, 'sigHandler');
//fork 进程
$pid = pcntl_fork();
// 循环等待信号
while(1){
 # 分发
  pcntl_signal_dispatch();
  fprintf(STDOUT,posix_getpid()."进程运行中\n");
  sleep(1);
}

通过执行上面脚本 按下ctrl+c 或者对子进程单独发送信号,子进程是可以捕获的。所以说当父进程创建一个子进程的时候,子进程是继承父进程的中断信号程序的。
在这里插入图片描述

function sigHandler($signo){
    fprintf(STDOUT,"pid:".posix_getpid()."接收到一个信号,编号为:%d \n", $signo);
}
# 安装信号处理器;
pcntl_signal(SIGINT, 'sigHandler');
//fork 进程
$pid = pcntl_fork();
if($pid == 0){
//已经重设信号处理
pcntl_signal(SIGINT, function($signo){
    fprintf(STDOUT,"pid:".posix_getpid()."我是子进程我接收到一个信号,编号为:%d \n", $signo);
});
}
// 循环等待信号
while(1){
 # 分发
  pcntl_signal_dispatch();
  fprintf(STDOUT,posix_getpid()."进程运行中\n");
  sleep(1);
}

在这里插入图片描述

信号集

  1. 信号集是指信号的集合
  2. 主进程可以选择某些信号,被阻塞的信号集称为阻塞信号集,或者叫信号屏蔽字Block
  3. 当进程阻塞了某个信号(php 通过 pcntl_sigprocmask 来设置信号屏蔽字)
  4. pcntl_sigprocmask
# 安装信号处理器;
pcntl_signal(SIGINT, function($signo){
    fprintf(STDOUT,"我接收到一个信号,编号为:%d \n", $signo);
});
// 定义阻塞信号集
$sigset = [SIGINT,SIGUSR1];
//设置阻塞
pcntl_sigprocmask(SIG_BLOCK, $sigset);
// 循环等待信号
while(1){
 # 分发
  pcntl_signal_dispatch();
  fprintf(STDOUT,posix_getpid()."进程运行中\n");
  sleep(1);
}

执行脚本使用 kill -s SIGINT 3009 发送信号并没有执行捕获而是阻塞挂起了。
在这里插入图片描述

  1. 解除信号屏蔽
pcntl_signal(SIGINT, function($signo){
    fprintf(STDOUT,"我接收到一个信号,编号为:%d \n", $signo);
});
// 定义阻塞信号集
$sigset = [SIGINT,SIGUSR1];
//设置阻塞
pcntl_sigprocmask(SIG_BLOCK, $sigset);
// 循环等待信号
$i = 10;
while($i--){
 # 分发
  pcntl_signal_dispatch();
  fprintf(STDOUT,posix_getpid()."进程运行中\n");
  sleep(1);
  if($i == 5){
  fprintf(STDOUT,"屏蔽已解除\n");
  //解除信号屏蔽
  //$oldset 会返回之前阻塞的信号集| 信号屏蔽字
  pcntl_sigprocmask(SIG_UNBLOCK,[SIGINT,SIGUSR1], $oldset);
  print_r($oldset);
  }
}

可以看到执行结果,在移除屏蔽时使用ctrl+c 没有捕获,屏蔽移除后,使用 ctrl+c 可以捕获到信号发送,并且得到了之前阻塞的信号集
在这里插入图片描述

发送信号

1.发送信号的方式

posix_kill

pcntl_signal(SIGINT,function($signo){
    fprintf(STDOUT,"PID %d 接收到 %d 信号 \n",posix_getpid(),$signo);
});
//$mapPid 里面是兄弟进程关系
$mapPid = [];
$pid = pcntl_fork();
if($pid > 0){
    $mapPid[] = $pid;

    $pid = pcntl_fork();
    if($pid > 0){
        $mapPid[] = $pid;
        while(1){
            pcntl_signal_dispatch();
           //posix_kill 第一个参数pid大于0指定某个进程发送
           //posix_kill pid 等于0 发送信号给进程组中每个进程
            posix_kill($mapPid[0],SIGINT);
            sleep(2);
        }
        exit(0);
    }
}

// 这里是子进程代码
while(1){
pcntl_signal_dispatch();
    fprintf(STDOUT, "pid=%d ppid=%d pgid=%d ...\n",posix_getpid(),posix_getppid(),posix_getpgrp());
sleep(2);
}

执行上面代码只有第一个子进程收到信号,两个子进程为兄弟进程关系
在这里插入图片描述

  1. 设置 posix_kill 函数 pid 等于0 发送信号给进程组中每个进程,兄弟进程与父进程同属一个进程组,
pcntl_signal(SIGINT,function($signo){
    fprintf(STDOUT,"PID %d 接收到 %d 信号 \n",posix_getpid(),$signo);
});
//$mapPid 里面是兄弟进程关系
$mapPid = [];
$pid = pcntl_fork();
if($pid > 0){
    $mapPid[] = $pid;

    $pid = pcntl_fork();
    if($pid > 0){
        $mapPid[] = $pid;
        while(1){
            pcntl_signal_dispatch();
           //posix_kill 第一个参数pid大于0指定某个进程发送
           //posix_kill pid 等于0 发送信号给进程组中每个进程
            posix_kill(0,SIGINT);
            sleep(2);
        }
        exit(0);
    }
}

// 这里是子进程代码
while(1){
pcntl_signal_dispatch();
    fprintf(STDOUT, "pid=%d ppid=%d pgid=%d ...\n",posix_getpid(),posix_getppid(),posix_getpgrp());
sleep(2);
}

父子进程都捕获到了信号
在这里插入图片描述

SIGALRM 信号

  1. 一个定时信号 php中由 pcntl_alarm 函数实现
# 安装信号处理器;
pcntl_signal(SIGALRM, function($signo){
    fprintf(STDOUT,"我接收到一个信号,编号为:%d \n", $signo);
});
pcntl_alarm(2);
// 循环等待信号
while(1){
 # 分发
  pcntl_signal_dispatch();
  fprintf(STDOUT,posix_getpid()."进程运行中\n");
  sleep(1);
}

执行脚本两秒后捕获到SIGALRM信号
在这里插入图片描述
2. 实现一个每两秒执行一次的函数

function sigHandler($signo){
fprintf(STDOUT,"我接收到一个信号,编号为:%d \n", $signo);
//再次设置定时信号
pcntl_alarm(2);
}
# 安装信号处理器;
pcntl_signal(SIGALRM, 'sigHandler');
//设置定时信号
pcntl_alarm(2);
// 循环等待信号
while(1){
 # 分发
  pcntl_signal_dispatch();
  fprintf(STDOUT,posix_getpid()."进程运行中\n");
  sleep(1);
}

执行脚本得到有规律的信号发送,2秒一次
在这里插入图片描述
3. 需要注意 每次对 pcntl_alarm() 函数的调用都会取消之前设置的alarm信号。也就是说只会执行最后一个 pcntl_alarm() 函数的调用,之前设置的无效

# 安装信号处理器;
pcntl_signal(SIGALRM, function($signo){
    fprintf(STDOUT,"我接收到一个信号,编号为:%d \n", $signo);
});
pcntl_alarm(1);
pcntl_alarm(3);
pcntl_alarm(5);
// 循环等待信号
while(1){
 # 分发
  pcntl_signal_dispatch();
  fprintf(STDOUT,posix_getpid()."进程运行中\n");
  sleep(1);
}

可以看到脚本执行到第5秒,捕获到信号,而不是第1秒或者第3秒
在这里插入图片描述
4. 如果 pcntl_alarm() 函数 参数为0,则之前设置的闹钟信号会被取消,并不会触发信号捕获函数
5. 翻看workerman定时器源码发现它也是使用SIGALRM 闹钟信号实现

SIGCHLD信号

  1. SIGCHLD 信号默认忽略
  2. 可以解决僵尸进程问题,及时回收子进程。
# 安装信号处理器;
pcntl_signal(SIGCHLD, function($signo){
    fprintf(STDOUT,"我接收到一个信号,编号为:%d \n", $signo);
    $pid = pcntl_waitpid(-1, $status, WNOHANG);
    if($pid > 0){
        fprintf(STDOUT,"PID=%d 子进程退出了",$pid);
    }
});
$pid = pcntl_fork();
if($pid > 0){
// 循环等待信号
while(1){
 # 分发
  pcntl_signal_dispatch();
  fprintf(STDOUT,posix_getpid()."进程运行中\n");
  sleep(1);
}
}else{
fprintf(STDOUT,"PID=%d 子进程结束 \n",posix_getpid());
exit(10);
}

pcntl_signal 缺点(下面是引用韩天峰大佬的文章 原文地址 PHP官方的pcntl_signal性能极差)

  1. PHP官方的pcntl_signal性能不好,因为php的信号处理函数是基于ticks来实现的,而不是注册到真正系统底层的信号处理函数中。而如果使用ticks的话,比如delare ticks=1, 那么每执行一条php语句都会调用上面的函数一次。而实际大部分时间里面并没有信号需要处理,所以这会造成极大的浪费。

  2. 如果一个服务器程序1秒中接收1000次请求,平均每个请求要执行1000行PHP代码。那么PHP的pcntl_signal,就带来了额外的 1000 * 1000,也就是100万次空的函数调用。这样会浪费大量的CPU资源。

  3. 通过查看pcntl.c的源码实现发现。pcntl_signal的实现原理是,触发信号后先将信号加入一个队列中。然后在PHP的ticks回调函数中不断检查是否有信号,如果有信号就执行PHP中指定的回调函数,如果没有则跳出函数。

PHP_MINIT_FUNCTION(pcntl)
{
	php_register_signal_constants(INIT_FUNC_ARGS_PASSTHRU);
	php_pcntl_register_errno_constants(INIT_FUNC_ARGS_PASSTHRU);
	php_add_tick_function(pcntl_signal_dispatch TSRMLS_CC);

	return SUCCESS;
}

pcntl_signal_dispatch 函数的实现:

void pcntl_signal_dispatch()
{
	//.... 这里略去一部分代码,queue即是信号队列
	while (queue) {
		if ((handle = zend_hash_index_find(&PCNTL_G(php_signal_table), queue->signo)) != NULL) {
			ZVAL_NULL(&retval);
			ZVAL_LONG(&param, queue->signo);

			/* Call php signal handler - Note that we do not report errors, and we ignore the return value */
			/* FIXME: this is probably broken when multiple signals are handled in this while loop (retval) */
			call_user_function(EG(function_table), NULL, handle, &retval, 1, &param TSRMLS_CC);
			zval_ptr_dtor(&param);
			zval_ptr_dtor(&retval);
		}
		next = queue->next;
		queue->next = PCNTL_G(spares);
		PCNTL_G(spares) = queue;
		queue = next;
	}
}
  1. 比较好的做法是去掉ticks,转而使用pcntl_signal_dispatch,在代码循环中自行处理信号。workerman 就没有使用declare ticks,而是在主事件循环中调用pcntl_signal_dispatch函数实现,这样就把pcntl_signal_dispatch 的调用频率下降了很多,而且还保证能达到近似实时的信号处理。
  2. 事实上,一般需要信号处理的代码都是后端服务程序,而一般的后端服务程序都是按照事件处理的结构来编写的,也就是说,这种程序里面必定会有个主事件循环。在事件循环的每次循环中主动调用pcntl_signal_dispatch,就能基本实时的把信号处理掉,而且还能保证一个比较好的性能。
  3. 而swoole中因为底层是C实现的,信号处理不受PHP的影响。swoole使用了目前Linux系统中最先进的signalfd来处理信号,几乎是没有任何额外消耗的。
  4. 所有最好使用swoole正确的编写需要php处理信号功能的代码。

如有表述错误,请提示我更正,一起进步

举报

相关推荐

0 条评论