PHP如何实现daemon守护进程和master-woker模式进程

一、PHP多进程及其实现
每个进程都有一个父进程,子进程退出,父进程能得到子进程退出的状态。每个进程都属于一个进程组,每个进程组都有一个进程组号,该号等于该进程组组长的PID。

场景一:

日常任务中,有时需要通过php脚本执行一些日志分析,队列处理等任务,当数据量比较大时,可以使用多进程来处理。

场景二:

如果一个任务被分解成多个进程执行,就会减少整体的耗时。比如有一个比较大的数据文件要处理,这个文件由很多行组成。如果单进程执行要处理的任务,量很大时要耗时比较久。这时可以考虑多进程。多进程处理分解任务,每个进程处理文件的一部分,这样需要均分割一下这个大文件成多个小文件(进程数和小文件的个数等同就可以)。

PHP不存在多线程,只有多进程,PHP多进程需要pcntl,posix扩展支持,可以通过 php - m 查看,没安装的话需要重新编译php,加上参数--enable-pcntl,posix一般默认会有。

创建子进程的函数fork

pcntl_fork — 在当前进程当前位置产生分支(子进程)。fork是创建了一个子进程,父进程和子进程 都从fork的位置开始向下继续执行,不同的是父进程执行过程中,得到的fork返回值为子进程号,而子进程得到的是0。

<?php
$pid = pcntl_fork();//父进程和子进程都会执行下面代码
if ($pid == -1) {
//错误处理:创建子进程失败时返回-1.
die(‘could not fork‘);
} else if ($pid) {
//父进程会得到子进程号,所以这里是父进程执行的逻辑
pcntl_wait($status);
file_put_contents(‘/tmp/swh.log‘, "父进程执行".PHP_EOL, FILE_APPEND);
//等待子进程中断,防止子进程成为僵尸进程。
} else {
//子进程得到的$pid为0, 所以这里是子进程执行的逻辑。
file_put_contents(‘/tmp/swh.log‘, "子进程执行".PHP_EOL, FILE_APPEND);
}
file_put_contents(‘/tmp/swh.log‘, posix_getpid().PHP_EOL, FILE_APPEND);
运行程序,得出输出:

父进程执行
12714
子进程执行
12715
几个重要的和多线程编程相关函数

posix_kill($pid, SIGTERM); //用来发信号给进程进行操作

posix_getpid() //获取当前进程的进程id

pcntl_wait($status); //wait函数刮起当前进程的执行直到一个子进程退出或接收到一个信号要求中断当前进程或调用一个信号处理函数。

二、PHP如何实现守护进程
守护进程(Daemon)是一种运行在后台的常驻进程服务, 很常见,例如 PHP-FPM, NGINX,REDIS,都需要一个父进程来支持整个服务。

守护进程独立于终端,并在后台周期性的执行任务或等待处理某些发生的事件。(Daemon独立于终端是为了避免进程在执行过程中的信息在终端上显示或者因收到终端上所产生的终端信息而中断。在Linux中从该控制终端开始运行的进程会依附于该控制终端,当控制终端被关闭时,相应的进程都会自动关闭,所以守护进程必须脱离控制终端)

守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机才随之一起停止运行;

守护进程一般都以root用户权限运行,因为要使用某些特殊的端口或者资源;

它们由init进程启动,并且没有控制终端,是一种执行日常事务的进程。所有的提供服务的进程基本上都是守护进程,通常也可以称为服务

它不需要用户输入就能够运行且提供某种服务,不是对整个系统就是对某个用户程序服务。

查看守护进程

#ps -efj
守护进程基本上都是以超级用户启动,所以UID为0

没有控制终端,所以TTY为?

2.1 借助 nohup 和 &  配合使用
在命令后面加上 & 符号, 可以让启动的进程转到后台运行,而不占用控制台,控制台还可以再运行其他命令。

<?php
while(true){
echo time().PHP_EOL;
sleep(3);
}
用 & 方式来启动该进程

[[email protected] ~]$ ps -ef | grep ‘test‘
dev 31095 1 0 11:37 ? 00:00:00 php test.php
dev 31112 31012 0 11:37 pts/2 00:00:00 grep --color=auto test
[[email protected] ~]$
我们发现在后台运行,不影响正常运行别的命令, 但是有一个问题,我们关闭终端或退出后,脚本也就结束了。

在命令之前加上 nohup ,启动的进程将会忽略linux的挂起信号 (SIGHUP)。

那什么情况下会触发linux下SIGHUP信号呢,SIGHUP会在以下3种情况下被发送给相应的进程:

1、终端关闭时,该信号被发送到session首进程以及作为job提交的进程(即用 & 符号提交的进程)
2、session首进程退出时,该信号被发送到该session中的前台进程组中的每一个进程
3、若父进程退出导致进程组成为孤儿进程组,且该进程组中有进程处于停止状态(收到SIGSTOP或SIGTSTP信号),该信号会被发送到该进程组中的每一个进程。

[[email protected] ~]$ nohup php test.php &
[1] 31626
[[email protected] ~]$ nohup: 忽略输入并把输出追加到"nohup.out"

[1]+ Exit 1 nohup php test.php
[[email protected] ~]$
可以看出,echo输出被重新定义到nohup.out中,终端关闭后,该进程还是存在的。

[[email protected] tmp]$ ps -ef | grep ‘test‘
dev 31817 1 0 11:50 ? 00:00:00 php test.php
dev 32090 31744 0 11:54 pts/2 00:00:00 grep --color=auto test
当我们组合 nohup 和 & 两种方式时,启动的进程不会占用控制台,也不依赖控制台,控制台关闭之后进程被1号进程收养,成为孤儿进程,这就和守护进程的机制非常类似了。

2.2 根据守护进程的规则和特点通过代码来实现
守护进程最大的特点就是脱离了用户终端和会话。

创建守护进程步骤

(1)创建子进程,退出父进程

为了脱离终端,需要退出父进程,使得子进程可以在后台执行。在Linux中父进程先于子进程退出会导致子进程变为孤儿进程,而每当系统发现一个孤儿进程,就由有1号进程(init)收养它,这样,原先的子进程就会变成了init进程的子进程。

(2)在子进程中创建新的会话

先介绍三个概念:

进程组:是一个或多个进程的集合。进程组由进程组ID来唯一标识。除了进程号PID之外,进程组ID也是一个进程的必备属性。每个进程组都有一个组长进程,其组长进程的进程号等于进程组ID,且该进程组ID不会因组长进程的退出而受到影响。

会话:多个进程组组成一个会话。

控制终端:每个会话可能会拥有一个控制终端(可以理解成我们常见的黑窗口,命令行),建立与控制终端连接的会话首进程叫做控制进程

每个进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。由于在调用fork函数时,子进程拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,为了使子进程不再受到它们的影响,需要调用setsid()来创建一个新的会话。

setsid() :用于创建一个新的会话,并且让调用该函数的进程成为该会话组的组长。对于子进程来说其主要有三个作用:

让子进程摆脱原会话的控制。

让子进程摆脱原进程组的控制。

让子进程摆脱原控制终端的控制。

(3)改变当前目录为根目录

使用fork创建的子进程继承了父进程的当前的工作目录。由于在进程运行中,当前目录所在的文件系统(如“/mnt/usb”)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让根目录”/”作为守护进程的当前工作目录。这样就可以避免上述的问题。如有特殊的需求,也可以把当前工作目录换成其他的路径。改变工作目录的方法是使用chdir函数。

(4)重设文件权限掩码

文件权限掩码:是指屏蔽掉文件权限中的对应位。

例如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限(对应二进制为,rwx, 101)。由于fork函数创建的子进程继承了父进程的文件权限掩码,这就给子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为0(即,不屏蔽任何权限),可以增强该守护进程的灵活性。设置文件权限掩码的函数是umask。通常的使用方法为umask(0)。

(5)再fork()一个子进程并终止父进程

现在,进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端,所以需要再次fork()一个子进程,并终止父进程。打开一个控制终端的前提条件是该进程必须为会话组组长,而我们通过第二次fork,确保了第二次fork出来的子进程不会是会话组组长。

(6)关闭文件描述符

用fork创建的子进程也会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸载。在使用setsid调用之后,守护进程已经与所属的控制终端失去了联系,因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1、2(即,标准输入、标准输出、标准错误输出)的三个文件已经失去了存在的价值,也应该关闭。

(7)守护进程退出处理

当用户需要外部停止守护进程时,通常使用kill命令停止该守护进程。所以,守护进程中需要编码来实现kill发出的signal信号处理,达到进程正常退出。

具体实现代码:

<?php
class Daemon{

private $info_dir="/tmp";
private $pid_file="";
private $terminate=false; //是否中断
private $workers_count=0;
private $gc_enabled=null;
private $workers_max=8; //最多运行8个进程

public function __construct($is_sington=false,$user=‘nobody‘,$output="/dev/null"){

$this->is_sington=$is_sington; //是否单例运行,单例运行会在tmp目录下建立一个唯一的PID
$this->user=$user;//设置运行的用户 默认情况下nobody
$this->output=$output; //设置输出的地方
$this->checkPcntl();
}
//检查环境是否支持pcntl支持
public function checkPcntl(){
if ( ! function_exists(‘pcntl_signal_dispatch‘)) {
// PHP < 5.3 uses ticks to handle signals instead of pcntl_signal_dispatch
// call sighandler only every 10 ticks
declare(ticks = 10);
}

// Make sure PHP has support for pcntl
if ( ! function_exists(‘pcntl_signal‘)) {
$message = ‘PHP does not appear to be compiled with the PCNTL extension. This is neccesary for daemonization‘;
$this->_log($message);
throw new Exception($message);
}
//信号处理
pcntl_signal(SIGTERM, array(__CLASS__, "signalHandler"),false);
pcntl_signal(SIGINT, array(__CLASS__, "signalHandler"),false);
pcntl_signal(SIGQUIT, array(__CLASS__, "signalHandler"),false);

// Enable PHP 5.3 garbage collection
if (function_exists(‘gc_enable‘))
{
gc_enable();
$this->gc_enabled = gc_enabled();
}
}

// daemon化程序
public function daemonize(){

global $stdin, $stdout, $stderr;
global $argv;

set_time_limit(0);

// 只允许在cli下面运行
if (php_sapi_name() != "cli"){
die("only run in command line mode\n");
}

// 只能单例运行
if ($this->is_sington==true){

$this->pid_file = $this->info_dir . "/" .__CLASS__ . "_" . substr(basename($argv[0]), 0, -4) . ".pid";
$this->checkPidfile();
}

umask(0); //把文件掩码清0

if (pcntl_fork() != 0){ //是父进程,父进程退出
exit();
}

posix_setsid();//设置新会话组长,脱离终端

if (pcntl_fork() != 0){ //是第一子进程,结束第一子进程
exit();
}

chdir("/"); //改变工作目录

$this->setUser($this->user) or die("cannot change owner");

//关闭打开的文件描述符
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);

$stdin = fopen($this->output, ‘r‘);
$stdout = fopen($this->output, ‘a‘);
$stderr = fopen($this->output, ‘a‘);

if ($this->is_sington==true){
$this->createPidfile();
}

}
//--检测pid是否已经存在
public function checkPidfile(){

if (!file_exists($this->pid_file)){
return true;
}
$pid = file_get_contents($this->pid_file);
$pid = intval($pid);
if ($pid > 0 && posix_kill($pid, 0)){
$this->_log("the daemon process is already started");
}
else {
$this->_log("the daemon proces end abnormally, please check pidfile " . $this->pid_file);
}
exit(1);

}
//----创建pid
public function createPidfile(){

if (!is_dir($this->info_dir)){
mkdir($this->info_dir);
}
$fp = fopen($this->pid_file, ‘w‘) or die("cannot create pid file");
fwrite($fp, posix_getpid());
fclose($fp);
$this->_log("create pid file " . $this->pid_file);
}

//设置运行的用户
public function setUser($name){

$result = false;
if (empty($name)){
return true;
}
$user = posix_getpwnam($name);
if ($user) {
$uid = $user[‘uid‘];
$gid = $user[‘gid‘];
$result = posix_setuid($uid);
posix_setgid($gid);
}
return $result;

}
//信号处理函数
public function signalHandler($signo){

switch($signo){

//用户自定义信号
case SIGUSR1: //busy
if ($this->workers_count < $this->workers_max){
$pid = pcntl_fork();
if ($pid > 0){
$this->workers_count ++;
}
}
break;
//子进程结束信号
case SIGCHLD:
while(($pid=pcntl_waitpid(-1, $status, WNOHANG)) > 0){
$this->workers_count --;
}
break;
//中断进程
case SIGTERM:
case SIGHUP:
case SIGQUIT:

$this->terminate = true;
break;
default:
return false;
}

}
/**
*开始开启进程
*$count 准备开启的进程数
*/
public function start($count=1){

$this->_log("daemon process is running now");
pcntl_signal(SIGCHLD, array(__CLASS__, "signalHandler"),false); // if worker die, minus children num
while (true) {
if (function_exists(‘pcntl_signal_dispatch‘)){

pcntl_signal_dispatch();
}

if ($this->terminate){
break;
}
$pid=-1;
if($this->workers_count<$count){

$pid=pcntl_fork();
}

if($pid>0){

$this->workers_count++;

}elseif($pid==0){

// 这个符号表示恢复系统对信号的默认处理
pcntl_signal(SIGTERM, SIG_DFL);
pcntl_signal(SIGCHLD, SIG_DFL);
if(!empty($this->jobs)){
while($this->jobs[‘runtime‘]){
if(empty($this->jobs[‘argv‘])){
call_user_func($this->jobs[‘function‘],$this->jobs[‘argv‘]);
}else{
call_user_func($this->jobs[‘function‘]);
}
$this->jobs[‘runtime‘]--;
sleep(2);
}
exit();

}
return;

}else{

sleep(2);
}

}

$this->mainQuit();
exit(0);

}

//整个进程退出
public function mainQuit(){

if (file_exists($this->pid_file)){
unlink($this->pid_file);
$this->_log("delete pid file " . $this->pid_file);
}
$this->_log("daemon process exit now");
posix_kill(0, SIGKILL);
exit(0);
}

// 添加工作实例,目前只支持单个job工作
public function setJobs($jobs=array()){

if(!isset($jobs[‘argv‘])||empty($jobs[‘argv‘])){

$jobs[‘argv‘]="";

}
if(!isset($jobs[‘runtime‘])||empty($jobs[‘runtime‘])){

$jobs[‘runtime‘]=1;

}

if(!isset($jobs[‘function‘])||empty($jobs[‘function‘])){

$this->log("你必须添加运行的函数!");
}

$this->jobs=$jobs;

}
//日志处理
private function _log($message){
printf("%s\t%d\t%d\t%s\n", date("c"), posix_getpid(), posix_getppid(), $message);
}

}
调用方法

//调用方法1
$daemon=new Daemon(true);
$daemon->daemonize();
$daemon->start(2);//开启2个子进程工作
work();

//调用方法2
$daemon=new Daemon(true);
$daemon->daemonize();
$daemon->addJobs(array(‘function‘=>‘work‘,‘argv‘=>‘‘,‘runtime‘=>1000));//function 要运行的函数,argv运行函数的参数,runtime运行的次数
$daemon->start(2);//开启2个子进程工作

//具体功能的实现
function work(){
echo "测试1";
}
 特殊说明

// 获取进程ID

var_dump(posix_getpid());

// 获取进程组ID

var_dump(posix_getpgid(posix_getpid()));

// 获取进程会话ID

var_dump(posix_getsid(posix_getpid()));
三者结果相同,说明了该进程即使进程组的组长,也是会话首领。

三、如何实现基于master-woker模式的守护进程
<?php

class Worker{

public static $count = 2;

public static function runAll(){
static::runMaster();
static::moniProcess();
}

//开启主进程
public static function runMaster(){
//确保进程有最大操作权限
umask(0);
$pid = pcntl_fork();
if($pid > 0){
echo "主进程进程 $pid \n";
exit;
}else if($pid == 0){
if(-1 === posix_setsid()){
throw new Exception("setsid fail");
}

for ($i=0; $i < self::$count; $i++) {
static::runWorker();
}

@cli_set_process_title("master_process");
}else{
throw new Exception("创建主进程失败");
}
}

//开启子进程
public static function runWorker(){
umask(0);
$pid = pcntl_fork();
if($pid > 0){
// echo "创建子进程 $pid \n";
}else if($pid == 0){
if(-1 === posix_setsid()){
throw new Exception("setsid fail");
}
@cli_set_process_title("worker_process");
while(1){
sleep(1);
}
}else{
throw new Exception("创建子进程失败");
}
}
//监控worker进程
public static function moniProcess(){
while( $pid = pcntl_wait($status)){
if($pid == -1){
break;
}else{
static::runWorker();
}
}
}
}

Worker::runAll();

原文地址:https://www.cnblogs.com/liliuguang/p/12619029.html

时间: 2024-08-14 10:22:15

PHP如何实现daemon守护进程和master-woker模式进程的相关文章

&lt;spark&gt; error:启动spark后查看进程,进程中master和worker进程冲突

启动hadoop再启动spark后jps,发现master进程和worker进程同时存在,调试了半天配置文件. 测试发现,当我关闭hadoop后 worker进程还是存在, 但是,当我再关闭spark之后再jps,发现worker进程依旧存在 于是想起了在~/spark/conf/slaves 中配置的slave1 slave2 上面还有个localhost,直接删去localhost,然后kill -s 9  worker进程. 初次测试这样解决了error,但是不知道是不是暂时的,如若有问题

Daemon守护进程

Daemon守护进程 在linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程,都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭. 守护进程(Daemon)是运行在后台的一种特殊进程.它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件.守护进程是一种很有用的进程.Linux的大多数服务器就是用守护进程实现的.比如,Internet服务器inetd,Web服务器httpd等.同时,守护进程完成许多系统任务,比

Daemon——守护进程

守护进程,也就是通常说的Daemon进程,是Linux中的后台服务进程.它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件.守护进程常常在系统引导装入时启动,在系统关闭时终止.Linux系统有很多守护进程,大多数服务都是通过守护进程实现的,同时,守护进程还能完成许多系统任务,例如,作业规划进程crond.打印进程lqd等(这里的结尾字母d就是Daemon的意思). 由于在Linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都

linux下daemon守护进程的实现(以nginx代码为例)

ngx_int_t ngx_daemon(ngx_log_t *log) { int fd; // 让init进程成为新产生进程的父进程: // 调用fork函数创建子进程后,使父进程立即退出.这样,产生的子进程将变成孤儿进程,并被init进程接管, // 同时,所产生的新进程将变为在后台运行. switch (fork()) { case -1: ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "fork() failed"); return

2进程之间的关系:进程组,会话,守护进程

 1进程组 一个或过个进程的集合,进程组ID是一个正整数.用来获得当前进程组ID的函数. pid_t getpgid(pid_t pid) pid_t getpgrp(void) 获得父子进程进程组 运行结果: 组长进程标识:其进程组ID=其进程ID 组长进程可以创建一个进程组,创建该进程组中的进程,然后终止,只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关. 进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组) 一个进程可以为自己或子进程设置进程组ID i

Linux 进程(二):进程关系及其守护进程

进程关系 进程组 进程组是一个或多个进程的集合.通常,它们是在同一作业中结合起来的,同一进程组中的各进程接收来自同一终端的各种信号,每个进程组有一个唯一的进程组ID.每个进程组有一个组长进程,该组长进程的ID等于进程组ID.从进程组创建开始到最后一个进程离开为止的时间称为进程组的生命周期. #include <unistd.h> pid_t getpgrp(void); 返回值:调用进程的进程组ID int setpgid(pid_t pid, pid_t pgid); 返回值:成功,返回0:

nginx进程模型 master/worker

nginx有两类进程,一类称为master进程(相当于管理进程),另一类称为worker进程(实际工作进程).启动方式有两种: (1)单进程启动:此时系统中仅有一个进程,该进程既充当master进程的角色,也充当worker进程的角色. (2)多进程启动:此时系统有且仅有一个master进程,至少有一个worker进程工作. master进程主要进行一些全局性的初始化工作和管理worker的工作:事件处理是在worker中进行的. 首先简要的浏览一下nginx的启动过程,如下图: 2.实现原理

nginx源码分析--master和worker进程模型

一.Nginx整体架构 正常执行中的nginx会有多个进程,最基本的有master process(监控进程,也叫做主进程)和woker process(工作进程),还可能有cache相关进程. 一个较为完整的整体框架结构如图所示: 二.核心进程模型 启动nginx的主进程将充当监控进程,而由主进程fork()出来的子进程则充当工作进程. nginx也可以单进程模型执行,在这种进程模型下,主进程就是工作进程,没有监控进程. Nginx的核心进程模型框图如下: master进程 监控进程充当整个进

写一个Windows上的守护进程(8)获取进程路径

写一个Windows上的守护进程(8)获取进程路径 要想守护某个进程,就先得知道这个进程在不在.我们假设要守护的进程只会存在一个实例(这也是绝大部分情形). 我是遍历系统上的所有进程,然后判断他们的路径和要守护的进程是否一致,以此来确定进程是否存在. 遍历进程大家都知道用CreateToolhelp32Snapshot系列API,但是他们最后取得的是进程exe名称,不是全路径,如果仅依靠名称就可以达到目的也就罢了,但是有的时候还是得取到全路径,这样会更靠谱一些. 那么问题来了,如何取到进程全路径