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

signal

信号与中断流程介绍

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

  1. 中断源(中断信号产生位置):
  1. 终端设备驱动
  1. 在终端设备按下按键产生的中断信号 ctrl+cctrl+zctrl+\
  2. 硬件异常
  1. 软件产生
  1. 在终端使用kill 命令 来发送中断信号
  2. php中 posix_kill 函数 pcntl_alarm 函数
  3. SIGURG(TCP/IP),SIGALRM
  1. 中断响应(对信号的处理)
  1. 直接忽略 。但是有两种信号永远不会被忽略,一个是SIGSTOP,另一个是SIGKILL,因为这两个信号提供了向内核最后的可靠的结束进程的办法。
  2. 执行中断处理函数(信号处理函数,信号捕捉函数)
  3. 执行系统默认。大多数的系统默认响应就是终止进程。
  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]+ 是作业编号 可以使用 jobs 命令查看当前终端作业
在这里插入图片描述

中断信号处理程序

  1. pcntl_signal() 信号处理函数
  1. 第一个参数$signo 信号编号 | 信号名字
  2. 第二个参数 $handler 中断信号处理程序 | 信号处理函数 | 信号处理程序
  3. 第三个参数 $restart_syscall = true 这个参数表示是否要重启被中断的系统调用
    php解释器执行程序都是调用库函数或者系统调用函数由系统内核提供

中断系统调用概念
当进程正在执行系统调用的时候,接收到中断信号,那么这个系统调用就会被中断。比如进程正在写文件途中接收到中断信号,写操作将无法恢复。
如果能恢复中断前正在执行的函数我们称为:可重入函数,否则就是非可重入函数 如果中断后不能恢复,系统调用函数会返回 -1 ,errno 会设置为EINTR 中断错误,在编写网络程序时比较常用。

# 安装信号处理器;
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 函数捕获信号

注意pcntl_signal 回调函数内建议不要写过多业务逻辑,可以造成错误,一般我们把中断信号用于通知。

每个信号都有相应的动作(信号处理程序)

  1. 用户自定义的中断信号处理程序
  2. SIG_DEL 系统默认动作 pcntl_signal(SIGINT, SIG_DEL);
  3. 忽略 SIG_IGN(ignore)pcntl_signal(SIGINT, SIG_IGN);
  4. SIGKILL SIGSTOP 信号无法捕捉,忽略

进程启动的时候,信号的动作默认是系统行为,如果你在编写信号对应的处理程序,就会覆盖掉原来的动作。但是SIGKILL SIGSTOP 信号无法覆盖

在这里插入图片描述

  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
  1. 第一个参数$how 是设置信号阻塞还是移除信号阻塞
  2. 第二个参数 $set 阻塞信号集合
  3. 第三个参数 $oldset 获取旧的阻塞信号集合
# 安装信号处理器;
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.发送信号的方式

  1. kill -s 信号编号 | 信号名字 进程pid
  2. 在程序中使用 posix_kill 给一个指定的进程或是进程组发送信号。
  3. pcntl_alarm 函数产生 SIGALRM 信号
  4. 在终端按下特殊键 ctrl+c, ctrl+z ,ctrl+\
  5. 网络端对端通讯产生SIGURG信号,读写管道文件也会产生SIGPIPE信号,SIGCHLD(当子进程结束时候产生),这些信号由内核,操作系统发送

posix_kill

第一个参数:
pid 大于0 的情况是指定某个进程发送信号
pid 等于0 的情况是发送信号给进程组中每个进程
pid 等于 -1 的情况可以自己在虚拟机上试试,千万别在线上环境测试,可能造成系统出现问题

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
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux中的信号处理是通过信号机制实现的。当一个进程接收到一个信号时,它会根据事先定义好的处理方式来处理这个信号信号的处理方式包括终止进程、忽略信号、终止进程并生成core文件、停止进程和继续运行进程等不同的动作。 在Linux中,信号的处理是通过设置信号的处理函数来完成的。当一个信号到达时,内核会调用相应的处理函数来处理这个信号。可以通过系统提供的函数来设置自定义的信号处理函数。 信号的发送可以通过多种方式,包括按键产生、终端按键产生、系统调用产生、软件条件产生和硬件异常产生等。不同的事件会触发不同的信号发送。例如,按下Ctrl+C会发送SIGINT信号,而按下Ctrl+Z会发送SIGTSTP信号。 对于进程来说,接收到信号后,不管正在执行什么代码,都会暂停运行,去处理信号。这种处理方式类似于硬件中断,被称为“软中断”。对于用户来说,由于信号的实现方式,信号的延迟时间非常短,几乎不可察觉。 总而言之,Linux中的信号处理是通过信号机制实现的,程序在接收到信号后会根据事先定义好的处理方式来处理这个信号。这种处理方式可以通过设置信号的处理函数来自定义。信号的发送可以通过多种方式,不同的事件会触发不同的信号发送。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Linux信号signal)](https://blog.csdn.net/weixin_43408582/article/details/115523424)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值