工作中同事需要做一个简单的工作进程,需要在进程结束时不能被硬生生的掐断当前正在执行的工作流程,需要等一个处理流程跑完了再结束,所以这时候就需要用到pcntl的信号量来工作了,主要的设计思路:

  1. 捕获系统发给进程的中断的信号量
  2. 在handler代码中设置标志位变量
  3. 在一个业务循环处理完成之后判断标志位变量,如果接收到过终止请求,则跳出整个工作循环。 主要的处理逻辑:
  • 注册绑定函数:
1
2
3
4
5
6
7
    protected function regist_sig_handler()
    {
        declare(ticks = 1);
        pcntl_signal(SIGTERM, [$this, 'sig_handler']);
        pcntl_signal(SIGHUP, [$this, 'sig_handler']);
        pcntl_signal(SIGINT, [$this, 'sig_handler']);
    }
  • 在函数中设置标志位:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
protected function sig_handler($signo)
    {
        switch ($signo) {
            case SIGTERM:
            case SIGHUP:
            case SIGINT:
                self::$_MYDAEMON_SHOULD_STOP = true;
                // ...
                break;
        }
    }
  • 构建退出处理函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 protected function shouldStop()
    {
        if (self::$_MYDAEMON_SHOULD_STOP) {
            // before exit staff code
            // ...
            return true;
        }

        return false;
    }
  • 在主循环中一次处理完成后判断是否要终止退出
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
        while (true) {
            if ($this->shouldStop()) {
                break;
            }
            try {
                $jobStatusInfo = $this->execute([]);
            } catch (Exception $ex) {
                $this->log($ex->getMessage());
            }
            usleep(10);
        }

附上完整代码(loop是示例需要长时间处理的业务逻辑,memoryusage超限也会退出,外部可以由supervisor/daemontools等进程监控程序控制)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/usr/bin/env php
<?php
/**
 * Description
 *
 * @project mydevkit
 * @package mydevkit
 * @author nickfan <nickfan81@gmail.com>
 * @link http://www.axiong.me
 * @version $Id$
 * @lastmodified: 2015-07-08 09:37
 *
 */
@set_time_limit(0);

class myDaemon
{
    const DEBUG = true;
    protected static $_MYDAEMON_SHOULD_STOP = false;
    public static $memLimit = 5242880;
    private $loop = 10;
    protected $logpath = '';

    public function __construct($option = [])
    {
        $option += [
            'loop' => 10,
            'memLimit' => 5242880,
            'logpath' => '/tmp/daemon_kill.log',
        ];
        $this->loop = $option['loop'];
        $this->logpath = $option['logpath'];
        self::$memLimit = $option['memLimit'];
        $this->regist_sig_handler();
    }

    public function __destruct()
    {
        $this->log('worker progress ending...');
    }

    protected function regist_sig_handler()
    {
        declare(ticks = 1);
        pcntl_signal(SIGTERM, [$this, 'sig_handler']);
        pcntl_signal(SIGHUP, [$this, 'sig_handler']);
        pcntl_signal(SIGINT, [$this, 'sig_handler']);
    }

    protected function sig_handler($signo)
    {
        switch ($signo) {
            case SIGTERM:
            case SIGHUP:
            case SIGINT:
                self::$_MYDAEMON_SHOULD_STOP = true;
                $signoStr = '';
                if ($signo == SIGTERM) {
                    $signoStr = 'SIGTERM';
                } elseif ($signo == SIGHUP) {
                    $signoStr = 'SIGHUP';
                } elseif ($signo == SIGINT) {
                    $signoStr = 'SIGINT';
                }
                $content = date('Y-m-d H:i:s') . ' ' . $signoStr . PHP_EOL;
                file_put_contents($this->logpath, $content, FILE_APPEND);
                break;
        }
    }

    protected function shouldStop()
    {
        if (self::$_MYDAEMON_SHOULD_STOP) {
            // before exit staff code
            // ...
            return true;
        }
        return false;
    }

    protected function log()
    {
        $args = func_get_args();
        $retstr = date('[Y-m-d H:i:s] ') . implode(' ', $args) . PHP_EOL;
        if (self::DEBUG == true) {
            echo $retstr;
        } else {
            return $retstr;
        }
    }

    public function execute($data = [])
    {
        $this->log('[EXECUTE] process lot of data.');
        for ($i = 0; $i < $this->loop; $i++) {
            $this->log('step:' . ($i + 1));
            sleep(1);
        }
        return rand(1, $this->loop);
    }

    public function run()
    {
        $this->log('[RUN] worker starting to run ...');
        while (true) {
            if ($this->shouldStop()) {
                break;
            }
            $this->log('processJobData Start ...');
            try {
                $jobStatusInfo = $this->execute([]);
                $this->log('processJobData end with: ' . var_export($jobStatusInfo, true));
                unset($jobStatusInfo);
            } catch (Exception $ex) {
                $this->log($ex->getMessage());
            }
            $memory = memory_get_usage();
            $this->log('memory usage:' . sprintf('%.2fMB', round($memory / 1048576, 2)));
            if ($memory > self::$memLimit) {
                $this->log('exiting run due to memory limit');
                exit;
            }
            //sleep(1);
            usleep(10);
        }
        $this->log('[RUN] worker run quit.');
    }
}

$gotMyParam = 10;
if (isset($argv[1])) {
    $gotMyParam = intval(trim(strip_tags($argv[1])));
}
$worker = new myDaemon(['loop' => $gotMyParam, 'memLimit' => 1048576 * 128,]);
$worker->run();

测试:

在class代码中的构造函数中注释掉

1
//$this->regist_sig_handler();

运行daemon代码,在执行execute的for循环时ctrl+c或者用 kill 杀死当前进程

Snip20150708_1

进程直接结束,没有执行完execute的一个完整流程就直接退出了,

把注释去掉重新执行daemon代码:

Snip20150708_2

在执行过程中按ctrl+c或者kill命令杀死前进程

观察daemon运行的结果是直到整个execute循环执行完了以后才退出程序的