PHP `pcntl_fork` 结合 `posix_kill` 实现优雅停机与进程间通信

各位观众,各位朋友,欢迎来到“PHP 进程管理与优雅停机”特别节目!我是你们的老朋友,今天咱就来聊聊 PHP 里那些“不安分”的进程,以及如何让它们乖乖听话,优雅地退休。

咱今天要讲的核心就是 pcntl_forkposix_kill 这对黄金搭档,它们能让你的 PHP 代码拥有多进程的能力,还能实现进程间的通信和优雅停机。

一、 为什么要用多进程?

在开始之前,咱们先来聊聊为什么要用多进程。PHP 擅长处理 Web 请求,但有些任务特别耗时,比如:

  • 发送大量的邮件
  • 处理复杂的图像或视频
  • 进行大数据分析
  • 执行外部命令(比如调用 ffmpeg)

如果这些任务都在主进程里执行,那你的网站可能就卡死了,用户体验直线下降。这时候,多进程就能派上大用场了。你可以把这些耗时的任务交给子进程去处理,主进程继续响应用户的请求,互不干扰,效率杠杠的。

二、 pcntl_fork:进程的“分身术”

pcntl_fork 是 PHP 里创建子进程的关键函数。它就像一个“分身术”,能复制出一个和当前进程一模一样的子进程。

<?php

$pid = pcntl_fork();

if ($pid == -1) {
    // Fork 失败
    die("Could not fork");
} else if ($pid) {
    // 父进程
    echo "我是父进程,我的 PID 是 " . getmypid() . ", 子进程的 PID 是 " . $pid . PHP_EOL;
    pcntl_wait($status); // 等待子进程结束,避免产生僵尸进程
} else {
    // 子进程
    echo "我是子进程,我的 PID 是 " . getmypid() . ", 我的父进程的 PID 是 " . posix_getppid() . PHP_EOL;
    exit(0); // 子进程执行完任务后必须退出
}

?>

这段代码执行后,你会看到父进程和子进程都输出了信息。注意几个关键点:

  • pcntl_fork() 返回值:
    • -1:Fork 失败
    • > 0:父进程中,返回子进程的 PID
    • 0:子进程中,返回 0
  • 父进程需要用 pcntl_wait() 来等待子进程结束,避免产生僵尸进程(zombie process)。僵尸进程会占用系统资源,影响性能。
  • 子进程执行完任务后一定要 exit(),否则它会继续执行父进程的代码,造成混乱。

三、 posix_kill:进程的“遥控器”

posix_kill 函数可以向指定的进程发送信号。信号就像一个“遥控器”,可以控制进程的行为。

<?php

$pid = pcntl_fork();

if ($pid == -1) {
    die("Could not fork");
} else if ($pid) {
    // 父进程
    echo "我是父进程,我的 PID 是 " . getmypid() . ", 子进程的 PID 是 " . $pid . PHP_EOL;

    sleep(5); // 等待一段时间,让子进程执行一些任务

    echo "父进程准备发送 SIGTERM 信号给子进程 " . $pid . PHP_EOL;
    posix_kill($pid, SIGTERM); // 发送 SIGTERM 信号,请求子进程终止

    pcntl_wait($status); // 等待子进程结束
    echo "子进程已经结束" . PHP_EOL;

} else {
    // 子进程
    echo "我是子进程,我的 PID 是 " . getmypid() . ", 我的父进程的 PID 是 " . posix_getppid() . PHP_EOL;

    // 注册信号处理函数
    pcntl_signal(SIGTERM, function ($signal) {
        echo "子进程接收到 SIGTERM 信号,准备退出..." . PHP_EOL;
        // 在这里可以做一些清理工作,比如关闭数据库连接、释放资源等
        exit(0);
    });

    while (true) {
        echo "子进程正在努力工作中..." . PHP_EOL;
        sleep(1);
        pcntl_signal_dispatch(); // 检查是否有信号需要处理
    }

    exit(0);
}

?>

这段代码演示了如何使用 posix_kill 发送 SIGTERM 信号给子进程,让它优雅地退出。注意几个关键点:

  • SIGTERM 信号:这是一个“终止请求”信号,告诉进程“你可以准备退休了”。进程收到这个信号后,可以选择清理资源,然后退出。
  • pcntl_signal():这个函数用于注册信号处理函数。当进程收到指定的信号时,就会执行这个函数。
  • pcntl_signal_dispatch():这个函数用于分发信号。在循环中调用它可以让进程及时响应信号,避免错过信号。

四、 优雅停机:让进程好好说再见

优雅停机是指在停止进程之前,让它有机会清理资源,完成未完成的任务,避免数据丢失或损坏。使用 pcntl_forkposix_kill 可以实现优雅停机。

以下是一些优雅停机的最佳实践:

  • 使用 SIGTERM 信号: 这是最常用的终止信号,进程可以选择忽略它,或者执行清理操作后退出。
  • 注册信号处理函数: 在信号处理函数中,可以关闭数据库连接、释放文件锁、保存未完成的数据等。
  • 设置超时时间: 如果进程在收到 SIGTERM 信号后一段时间内没有退出,可以发送 SIGKILL 信号强制终止它。SIGKILL 信号是“立即终止”信号,进程无法捕获或忽略它。
<?php

$pid = pcntl_fork();

if ($pid == -1) {
    die("Could not fork");
} else if ($pid) {
    // 父进程
    echo "我是父进程,我的 PID 是 " . getmypid() . ", 子进程的 PID 是 " . $pid . PHP_EOL;

    sleep(5); // 等待一段时间,让子进程执行一些任务

    echo "父进程准备发送 SIGTERM 信号给子进程 " . $pid . PHP_EOL;
    posix_kill($pid, SIGTERM); // 发送 SIGTERM 信号,请求子进程终止

    // 设置超时时间
    $timeout = 10;
    $start = time();

    while (pcntl_waitpid($pid, $status, WNOHANG) == 0) {
        if ((time() - $start) > $timeout) {
            echo "子进程超时未退出,发送 SIGKILL 信号强制终止 " . $pid . PHP_EOL;
            posix_kill($pid, SIGKILL); // 发送 SIGKILL 信号,强制终止
            break;
        }
        sleep(1);
    }

    echo "子进程已经结束" . PHP_EOL;

} else {
    // 子进程
    echo "我是子进程,我的 PID 是 " . getmypid() . ", 我的父进程的 PID 是 " . posix_getppid() . PHP_EOL;

    // 注册信号处理函数
    pcntl_signal(SIGTERM, function ($signal) {
        echo "子进程接收到 SIGTERM 信号,准备退出..." . PHP_EOL;
        // 在这里可以做一些清理工作,比如关闭数据库连接、释放资源等
        // 模拟耗时操作
        sleep(3);
        echo "子进程清理完成,退出" . PHP_EOL;
        exit(0);
    });

    while (true) {
        echo "子进程正在努力工作中..." . PHP_EOL;
        sleep(1);
        pcntl_signal_dispatch(); // 检查是否有信号需要处理
    }

    exit(0);
}

?>

五、 进程间通信:让进程“唠嗑”

多进程之间需要进行通信,才能协同完成任务。PHP 提供了多种进程间通信的方式,比如:

  • 共享内存: 多个进程可以访问同一块内存区域,进行数据交换。
  • 消息队列: 进程可以向消息队列发送消息,其他进程可以从消息队列接收消息。
  • 信号量: 用于控制多个进程对共享资源的访问,避免竞争条件。
  • 管道: 进程可以通过管道进行单向或双向的通信。

这里我们介绍一种简单的方式:使用共享内存。

<?php

// 创建一个共享内存段
$shm_key = ftok(__FILE__, 't');
$shm_id = shm_attach($shm_key, 1024, 0666);

if ($shm_id === false) {
    die("Could not attach to shared memory");
}

$pid = pcntl_fork();

if ($pid == -1) {
    die("Could not fork");
} else if ($pid) {
    // 父进程
    echo "我是父进程,我的 PID 是 " . getmypid() . ", 子进程的 PID 是 " . $pid . PHP_EOL;

    // 从共享内存中读取数据
    $data = shm_get_var($shm_id, 1);
    echo "父进程从共享内存中读取到的数据: " . $data . PHP_EOL;

    pcntl_wait($status); // 等待子进程结束

    // 删除共享内存段
    shm_remove($shm_id);
    shm_detach($shm_id);

} else {
    // 子进程
    echo "我是子进程,我的 PID 是 " . getmypid() . ", 我的父进程的 PID 是 " . posix_getppid() . PHP_EOL;

    // 向共享内存中写入数据
    shm_put_var($shm_id, 1, "Hello from child process!");
    echo "子进程向共享内存中写入了数据" . PHP_EOL;

    exit(0);
}

?>

这段代码演示了如何使用共享内存进行进程间通信。注意几个关键点:

  • ftok():这个函数用于生成一个唯一的共享内存键值。
  • shm_attach():这个函数用于连接到共享内存段。
  • shm_get_var():这个函数用于从共享内存中读取数据。
  • shm_put_var():这个函数用于向共享内存中写入数据。
  • shm_remove():这个函数用于删除共享内存段。
  • shm_detach():这个函数用于断开与共享内存段的连接。

六、 实际案例:异步任务处理

咱们来一个实际的案例,演示如何使用 pcntl_forkposix_kill 来实现异步任务处理。假设你的网站需要发送大量的邮件,你可以把发送邮件的任务交给子进程去处理,主进程继续响应用户的请求。

<?php

function send_email($email, $subject, $message) {
    // 模拟发送邮件
    echo "正在发送邮件给 " . $email . ", 主题是 " . $subject . PHP_EOL;
    sleep(2); // 模拟发送邮件的耗时
    echo "邮件发送成功" . PHP_EOL;
}

function handle_email_task($email, $subject, $message) {
    $pid = pcntl_fork();

    if ($pid == -1) {
        error_log("Could not fork");
        return false;
    } else if ($pid) {
        // 父进程
        echo "父进程创建了一个子进程来发送邮件,子进程的 PID 是 " . $pid . PHP_EOL;
        return true; // 任务已提交
    } else {
        // 子进程
        send_email($email, $subject, $message);
        exit(0); // 子进程执行完任务后必须退出
    }
}

// 模拟接收用户请求,并提交邮件发送任务
$emails = [
    "[email protected]",
    "[email protected]",
    "[email protected]",
];

foreach ($emails as $email) {
    $subject = "欢迎注册我们的网站";
    $message = "感谢您注册我们的网站,祝您使用愉快!";

    if (handle_email_task($email, $subject, $message)) {
        echo "邮件发送任务已提交" . PHP_EOL;
    } else {
        echo "邮件发送任务提交失败" . PHP_EOL;
    }
}

echo "主进程继续处理其他请求..." . PHP_EOL;

// 可以选择等待所有子进程结束
while (pcntl_waitpid(0, $status) > 0) {
    // 等待所有子进程结束
}

echo "所有邮件发送任务已完成" . PHP_EOL;

?>

这段代码演示了如何使用 pcntl_fork 创建子进程来异步发送邮件。主进程可以继续处理其他请求,而不用等待邮件发送完成。

七、 注意事项:

  • 信号处理: 务必注册信号处理函数,并使用 pcntl_signal_dispatch() 来分发信号,确保进程能够及时响应信号。
  • 避免僵尸进程: 父进程需要使用 pcntl_wait()pcntl_waitpid() 来等待子进程结束,避免产生僵尸进程。
  • 资源竞争: 在多进程环境下,需要注意资源竞争的问题,可以使用信号量或互斥锁来保护共享资源。
  • 错误处理: 务必进行错误处理,避免程序崩溃。
  • 扩展依赖: 确保你的 PHP 环境安装了 pcntl 扩展。

八、 总结:

pcntl_forkposix_kill 是 PHP 中进行进程管理和优雅停机的利器。掌握它们,你可以编写出更加高效、稳定的 PHP 代码。

函数/信号 功能 备注
pcntl_fork() 创建一个子进程 返回值:-1(失败),>0(父进程,返回子进程 PID),0(子进程)
posix_kill() 向指定进程发送信号 常用的信号:SIGTERM(终止请求),SIGKILL(强制终止)
pcntl_wait() 等待子进程结束,避免产生僵尸进程
pcntl_signal() 注册信号处理函数
pcntl_signal_dispatch() 分发信号,让进程能够及时响应信号
SIGTERM 终止请求信号,进程可以选择清理资源后退出
SIGKILL 强制终止信号,进程无法捕获或忽略它 谨慎使用,可能导致数据丢失
共享内存 一种进程间通信方式,多个进程可以访问同一块内存区域,进行数据交换 需要使用 shm_attach()shm_get_var()shm_put_var() 等函数

今天就到这里了,希望大家有所收获。记住,进程管理就像驯服野马,需要耐心和技巧。祝大家在 PHP 的世界里玩得开心!感谢大家的收看,我们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注