【问题标题】:How to prevent PHP script running more than once?如何防止 PHP 脚本多次运行?
【发布时间】:2012-11-29 22:52:54
【问题描述】:

目前,我试图阻止onlytask.php 脚本多次运行:

$fp = fopen("/tmp/"."onlyme.lock", "a+");
if (flock($fp, LOCK_EX | LOCK_NB)) {
  echo "task started\n";
  //
    while (true) {
      // do something lengthy
      sleep(10);
    }
  //
  flock($fp, LOCK_UN);
} else {
  echo "task already running\n";
}
fclose($fp);

并且有一个cron作业每分钟执行一次上述脚本:

* * * * * php /usr/local/src/onlytask.php

它工作了一段时间。几天后,当我这样做时:

ps auxwww | grep onlytask

我发现有 两个 实例正在运行!不是三个或更多,不是一个。我杀死了其中一个个体。几天后,又有两个实例。

代码有什么问题?还有其他替代方法可以限制 onlytask.php 的一个实例正在运行吗?

附言我的/tmp/ 文件夹未清理。 ls -al /tmp/*.lock 显示锁定文件是在第一天创建的:

-rw-r--r--  1 root root    0 Dec  4 04:03 onlyme.lock

【问题讨论】:

  • 非常有趣,但似乎这应该是正确的......你的代码框架就是这样吗?我确实想知道如果通过 false 而不是资源会发生什么。当然它返回false。如果不这样做,那将是非常非常奇怪的,但我已经看到 PHP 做出了一些非常奇怪的选择。此外,flock 的内部可能存在缺陷(或者使用的系统调用——尽管这在很久以前就已经被注意到了)。
  • 代码几乎 100% 像真实代码。 // do something lengthy 实际上是 exec() 另一个 .php 是应该定期运行的脚本,但不超过一次。
  • 第二个进程将等待第一个进程完成。见错误:stackoverflow.com/questions/5524073/lock-nb-ignored

标签: php linux cron centos


【解决方案1】:

你应该在打开锁文件时使用x标志:

<?php

$lock = '/tmp/myscript.lock';
$f = fopen($lock, 'x');
if ($f === false) {
  die("\nCan't acquire lock\n");
} else {
  // Do processing
  while (true) {
    echo "Working\n";
    sleep(2);
  }
  fclose($f);
  unlink($lock);
}

来自PHP manual 的注释

'x' - 创建和打开仅供书写;将文件指针放在 文件的开头。如果文件已经存在,则 fopen() 调用 将通过返回 FALSE 并生成 level 错误而失败 E_WARNING。如果该文件不存在,请尝试创建它。这是 相当于为底层指定 O_EXCL|O_CREAT 标志 open(2) 系统调用。

这是来自man pageO_EXCL解释:

O_EXCL - 如果设置了 O_CREAT 和 O_EXCL,open() 将失败,如果文件 存在。检查文件是否存在并创建 如果文件不存在,应该是原子的 执行 open() 的线程在同一文件中命名相同的文件名 设置了 O_EXCL 和 O_CREAT 的目录。如果设置了 O_EXCL 和 O_CREAT, 和路径名称符号链接,open() 将失败并将 errno 设置为 [EEXIST],不管符号链接的内容。如果 O_EXCL 已设置而 O_CREAT 未设置,则结果未定义。

更新

更可靠的方法 - 运行主脚本,获取锁,运行工作脚本并释放锁。

<?php
// File: main.php

$lock = '/tmp/myscript.lock';
$f = fopen($lock, 'x');
if ($f === false) {
  die("\nCan't acquire lock\n");
} else {
  // Spawn worker which does processing (redirect stderr to stdout)
  $worker = './worker 2>&1';
  $output = array();
  $retval = 0;
  exec($worker, $output, $retval);
  echo "Worker exited with code: $retval\n";
  echo "Output:\n";
  echo implode("\n", $output) . "\n";
  // Cleanup the lock
  fclose($f);
  unlink($lock);
}

工人来了。让我们在其中提出一个假的致命错误:

#!/usr/bin/env php
<?php
// File: worker (must be executable +x)
for ($i = 0; $i < 3; $i++) {
  echo "Processing $i\n";
  if ($i == 2) {
    // Fake fatal error
    trigger_error("Oh, fatal error!", E_USER_ERROR);
  }
  sleep(1);
}

这是我得到的输出:

galymzhan@atom:~$ php main.php 
Worker exited with code: 255
Output:
Processing 0
Processing 1
Processing 2
PHP Fatal error:  Oh, fatal error! in /home/galymzhan/worker on line 8
PHP Stack trace:
PHP   1. {main}() /home/galymzhan/worker:0
PHP   2. trigger_error() /home/galymzhan/worker:8

重点是锁定文件已正确清理,因此您可以再次运行main.php 而不会出现问题。

【讨论】:

  • 如果代码在位置echo "Working\n"; 处因错误而死掉,.lock 文件将保留在磁盘上,不会被任何人清理,因此不会进行更多处理。跨度>
  • @ohho 任何依赖锁定文件的工具都是如此。您可以在主脚本中获取锁,然后将工作脚本作为子进程运行,然后在主脚本中删除锁文件
  • @galymzhan LOCK会在进程死亡时释放,文件不会在进程死亡时被删除。请检查文档。
  • 如果main.phpunlink 之前死了怎么办?之后什么都不会做。
  • 例如,exec($worker, $output, $retval); 正在运行时服务器会重新启动。
【解决方案2】:

现在我通过ps 检查进程是否正在运行,并通过bash 脚本扭曲 php 脚本:

 #!/bin/bash

 PIDS=`ps aux | grep onlytask.php | grep -v grep`
 if [ -z "$PIDS" ]; then
     echo "Starting onlytask.php ..."
     php /usr/local/src/onlytask.php >> /var/log/onlytask.log &
 else
     echo "onlytask.php already running."
 fi

并每分钟通过cron 运行bash 脚本。

【讨论】:

  • 这通常是我(系统管理员)这样做的方式。但是,我遇到了这样一种情况,当从 cron 运行的 BASH 脚本调用时,一个 php 脚本执行时没有错误并且返回值正确。然而,结果并不如预期。就我而言,php 应该使用 ssh 调用在远程服务器上创建一个目录。当 php 脚本直接运行时,它会创建目录。但是,通过 BASH 脚本,它不会创建它。 @galymzhan 回答解决了 php 应该在没有重复过程的情况下自行运行的部分。我还没有通过 cron 进行测试。
【解决方案3】:
<?php

$sLock = '/tmp/yourScript.lock';

if( file_exist($sLock) ) {
 die( 'There is a lock file' );
}

file_put_content( $sLock, 1 );

// A lot of code

unlink( $sLock );

您可以通过编写 pid 添加额外的检查,然后在 file_exist-statement 中检查它。 为了更加安全,您可以通过“ps 传真”获取所有正在运行的应用程序,并检查此文件是否在列表中。

【讨论】:

  • 在脚本即将创建文件时调用 file_exists 是可能的,因此它被创建/写入两次。这行不通。 (此外,就实际文件而言,取消设置文件并没有任何意义。)
  • 我想这个应用程序在 php 中不是多线程的,所以这不会是一个问题。将未设置更改为取消链接(拼写错误)
  • 如果你手动移除锁(例如 rm -rf /tmp/yourScript.lock),你应该责备自己。
  • 问题是file_exists 和file_put_contents 不是一个原子单位。你有两个操作:x 然后 y。假设您必须处理。理想情况下,它们会像 x1 y1 x1 y2 一样(尽管 y2 不会发生)。 可以发生的是 x1 x2 y1 y2。这意味着检查刚刚失败。
  • 如果您在同一时刻多次运行脚本,可以。但这是一项 cron 工作。
【解决方案4】:

尝试使用文件的存在而不是它的群标志:

$lockFile = "/tmp/"."onlyme.lock";
if (!file_exists($lockFile)) {

  touch($lockFile); 

  echo "task started\n";
  //
  // do something lengthy
  //

  unlink($lockFile); 

} else {
  echo "task already running\n";
}

【讨论】:

  • 它在问题的上下文中工作,因为脚本创建自己的锁定文件,因此永远不会出现竞争条件。使用相同锁定文件的多个脚本 = 不同的故事。
  • 不,与脚本文件无关。与一次运行的多个进程有关。不保证 file_exists/touch/unlink 链不会因处理器调度而对每个进程不间断地发生。 (虽然我必须让步,但 Niclas 刚刚提出了一个非常有效的观点,即这两个进程永远不会以足够快的速度产生影响,所以是的,我想这会奏效。不过,这取决于产生的间隔。)
  • “检查文件是否存在”不起作用。如果进程在 bug 中死掉,.lock 文件将保留在磁盘中并阻止 cron 作业启动另一个进程。
【解决方案5】:

您可以使用锁定文件,正如一些人所建议的,但您真正需要的是 PHP Semaphore 函数。这些有点像文件锁,但专为您的工作而设计,限制对共享资源的访问。

【讨论】:

  • 来自文档:“sem_acquire() 是阻塞的,这意味着具有相同信号量的后续调用将无限期阻塞,直到信号量被释放。这确保了序列化,但如果你想要的话它不是很实用要做的是检查你是否应该继续。不幸的是,PHP 还不支持以非阻塞方式查询信号量状态的任何方法。"
【解决方案6】:

切勿对锁定文件或重命名等其他功能使用取消链接。它破坏了 Linux 上的 LOCK_EX。例如,在取消链接或重命名锁定文件后,任何其他脚本总是从flock() 中获取true。

检测先前有效结束的最佳方法 - 在结束锁定之前写入锁定文件几个字节,在 LOCK_UN 处理之前。 LOCK_EX 从锁文件中读取几个字节并 ftruncate 句柄之后。

重要提示:所有测试均在 Linux 上的 PHP 5.4.17 和 Windows 7 上的 5.4.22 上测试。

示例代码:

设置信号量:

$handle = fopen($lockFile, 'c+');
if (!is_resource($handle) || !flock($handle, LOCK_EX | LOCK_NB)) {
    if (is_resource($handle)) {
        fclose($handle);
    }
    $handle = false;
    echo SEMAPHORE_DENY;
    exit;
} else {
    $data = fread($handle, 2);
    if ($data !== 'OK') {
        $timePreviousEnter = fileatime($lockFile);
        echo SEMAPHORE_ALLOW_AFTER_FAIL;
    } else {
        echo SEMAPHORE_ALLOW;
    }
    fseek($handle, 0);
    ftruncate($handle, 0);
}

离开信号量(在关闭处理程序中更好地调用):

if (is_resource($handle)) {
    fwrite($handle, 'OK');
    flock($handle, LOCK_UN);
    fclose($handle);
    $handle = false;
}

【讨论】:

  • 我不太明白您的主要声明 - 如果您要取消锁定文件的链接,这意味着您已经完成了该过程中的锁定部分。同样,如果文件描述符因任何原因关闭,则意味着锁定已完成。那时其他脚本应该能够继续。为什么会被破坏?
  • @JosipRodin 我也这么认为,直到我遇到问题。您可以尝试测试此行为。也许这取决于系统及其实施。在 Gentoo 上我遇到了问题。如果上面的代码有效,我看不出有任何理由寻找原因。关于文件的删除 - 删除文件不会导致文件描述符的关闭。 inode 仍然可用。
  • 我想您指的是stackoverflow.com/questions/17708885/… 中描述的竞争条件。更简单的方法应该更好的原因是它们会导致更少的 I/O,这在具有大量数据的系统中可能很重要锁争用和同一设备上的大量 I/O。
  • @JosipRodin 也许你是对的。但是Windows 上的fstat(或stat)总是有inode == 0。我需要在两个系统中都可以使用的解决方案。而且我不喜欢while(true) - 它有时会导致高负载。例如在高负载系统上。
【解决方案7】:

在 galimzhan 的回答中添加了对旧锁的检查(没有足够的 *s 来评论),这样如果进程死亡,旧的锁文件将在三分钟后被清除,让 cron 再次启动进程。这就是我使用的:

<?php
$lock = '/tmp/myscript.lock';
if(time()-filemtime($lock) > 180){
    // remove stale locks older than 180 seconds
    unlink($lock);
}
$f = fopen($lock, 'x');
if ($f === false) {
  die("\nCan't acquire lock\n");
} else {
    // Do processing
    while (true) {
    echo "Working\n";
        sleep(2);
    }
    fclose($f);
    unlink($lock);
}

您还可以为 cron 作业添加超时,以便 php 进程在 60 秒后被终止,例如:

* * * * * user timeout -s 9 60 php /dir/process.php >/dev/null

【讨论】:

    猜你喜欢
    • 2015-02-10
    • 2014-02-05
    • 1970-01-01
    • 1970-01-01
    • 2012-12-21
    • 2014-12-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多