pcntl_fork在CLI模式正常Apache环境报未定义函数如何解决
我有一个对响应速度要求较高的关键业务Web应用,需要执行部分可能耗时数秒的代码,因此尝试使用pcntl_fork实现进程派生。使用如下简单测试脚本时:
<?PHP $pid = pcntl_fork(); if ($pid == -1) { die('could not fork'); } else if ($pid) { // 父进程逻辑 echo "We are the parent!!"; pcntl_wait($status); // 避免产生僵尸进程 } else { // 子进程逻辑 sleep (5); echo "Child here!!"; } ?>
CLI模式下运行完全正常,执行输出如下:
[dovid@k1 php.d]# php /var/www/html/test.php We are the parent!!Child here!![dovid@k1 php.d]#
通过php -i命令检查确认pcntl扩展已启用:
[dovid@k1 php.d]# php -i | grep pcntl pcntl pcntl support => enabled [dovid@k1 php.d]#
但通过Apache访问该页面时返回500错误,Apache错误日志内容如下:
[Mon Jun 13 05:20:53.953818 2022] [:error] [pid 30457] [client XX.XX.XX.XX:57500] PHP Fatal error: Call to undefined function pcntl_fork() in /var/www/html/test.php on line 2
当前运行环境为CentOS7衍生发行版Sangoma Linux,安装的PHP版本为5.6,已安装包列表如下:
[dovid@k1 php.d]# rpm -qa | grep php php56w-5.6.40-1.sng7.x86_64 php56w-xml-5.6.40-1.sng7.x86_64 php56w-pear-1.10.1-1.w7.noarch php56w-intl-5.6.40-1.sng7.x86_64 php-digium_register-4.0-1.sng7.x86_64 php56w-pdo-5.6.40-1.sng7.x86_64 php56w-pecl-redis-2.2.7-1.w7.x86_64 php56w-gd-5.6.40-1.sng7.x86_64 php56w-pecl-ssh2-0.13-1.sng7.x86_64 php56w-pecl-igbinary-1.2.1-2.w7.x86_64 php56w-odbc-5.6.40-1.sng7.x86_64 php56w-process-5.6.40-1.sng7.x86_64 php56w-mbstring-5.6.40-1.sng7.x86_64 php56w-soap-5.6.40-1.sng7.x86_64 php56w-mysqlnd-5.6.40-1.sng7.x86_64 php56w-ldap-5.6.40-1.sng7.x86_64 php56w-common-5.6.40-1.sng7.x86_64 php56w-cli-5.6.40-1.sng7.x86_64 php56w-bcmath-5.6.40-1.sng7.x86_64 [dovid@k1 php.d]# rpm -qa | grep httpd httpd-tools-2.4.6-93.el7.centos.x86_64 httpd-2.4.6-93.el7.centos.x86_64 [dovid@k1 php.d]#
咨询问题:
- 是否是Apache配置存在缺失?
- PHP环境下是否有更优的进程派生方案(例如使用exec加&的方式)?
Apache环境下pcntl_fork未定义的原因
这不是Apache配置缺失的问题。pcntl扩展本身是PHP官方明确仅支持CLI命令行场景使用的扩展,在Web SAPI(包括Apache mod_php、php-fpm)环境下,发行版打包的PHP默认会在Web环境的配置中禁用pcntl系列函数,检查/etc/php.d/目录下pcntl对应的ini配置,就能看到disable_functions中包含了pcntl_fork等相关函数。
即便手动去掉函数禁用,也绝对不推荐在Web请求中直接使用pcntl_fork:fork出的子进程会完整继承父进程的数据库连接、网络套接字、文件句柄等资源,父进程请求结束后会自动释放这些资源,子进程继续使用这些资源时会触发各种难以排查的异常,同时还存在僵尸进程、服务进程崩溃的风险,生产环境用这种方式非常容易出故障。
更适合Web场景的耗时任务处理方案
临时方案:exec后台执行
提到的exec加&的方式可以实现不阻塞Web请求的后台执行,正确写法需要重定向输出流,避免PHP等待进程退出:
// 所有传入命令的参数都要用escapeshellarg过滤,避免命令注入 $taskArg = escapeshellarg($userInput); exec("/usr/bin/php /path/to/your/task_script.php {$taskArg} > /dev/null 2>&1 &");
这个方案的缺点是没有任务管控能力,高并发场景下短时间触发大量后台进程会直接打满服务器CPU、内存资源,只适合低并发的临时场景使用。
生产环境推荐方案:任务队列
这是Web场景处理耗时任务的标准方案,稳定性远高于直接在请求中fork进程:
- Web收到请求后,只需要把耗时任务的参数存入Redis、MySQL等存储实现的队列中,即可立刻返回响应给用户,耗时完全不受任务执行影响
- 用crontab或者systemd托管一个常驻的PHP CLI消费脚本,持续从队列中拉取任务执行,可以灵活控制并发数、实现失败重试、日志记录等能力
- 完全规避了Web进程fork带来的资源继承、僵尸进程、服务不稳定的问题
内容的提问来源于stack exchange,提问作者Dovid Bender

