为何Perl qx在Mojolicious::Lite中挂起但普通脚本正常?
tr命令挂起的问题 我来帮你分析下这个诡异的现象,顺便给几个靠谱的解决办法:
为啥普通Perl脚本没问题,放到Mojolicious里就挂?
核心问题出在管道命令的信号处理和Mojolicious的事件循环环境上。
你用的命令链是tr -cd "[:alnum:]" < /dev/urandom | fold -w3 | head -n1,正常逻辑是:head读到第一行就退出,这时候fold会收到SIGPIPE信号(因为后面的管道没人读了),接着fold退出,tr也会收到SIGPIPE,停止读/dev/urandom,整个命令链就结束了。
但在Mojolicious的路由处理函数里,事件循环会接管进程的标准输入输出处理,导致SIGPIPE信号没有被正确传递给tr进程。tr根本不知道后面的fold和head已经跑没影了,还一个劲地从/dev/urandom读数据,自然就占着CPU不撒手,直到你手动中断。你测试cat也挂,就是因为同样的管道信号问题。
怎么解决?给你三个方案,按需选:
方案1:用Mojolicious自带的非阻塞子进程API
Mojolicious专门提供了Mojo::IOLoop::Subprocess来处理这种需要调用外部命令的场景,既能避免阻塞事件循环,还能正确处理进程信号:
use Mojolicious::Lite; use strict; use warnings; use Data::Dumper; post 'testit' => sub { my $c = shift; my $pass_length = 3; # 启动子进程执行命令,不阻塞主事件循环 Mojo::IOLoop::Subprocess->new->run( # 第一个回调:在子进程里执行命令 sub { my $exec = qq{tr -cd '[:alnum:]' < /dev/urandom | fold -w$pass_length | head -n1}; my $pass = `$exec`; chomp $pass; return $pass; }, # 第二个回调:子进程完成后处理结果 sub { my ($subprocess, $err, $pass) = @_; if ($err) { warn "生成密码出错:$err"; return $c->render(json => {error => '密码生成失败'}); } warn Dumper $pass; return $c->render(json => {foo => 'bar', password => $pass}); } ); # 告诉框架这个请求要异步处理,别直接返回 $c->render_later; }; app->secrets('foobar'); app->start;
方案2:修改命令,限制tr的读取量
既然tr会无限读,那我们给它加个限制,用head先读足够的随机字节再传给tr,这样tr处理完就自动退出:
my $exec = qq{head -c 100 /dev/urandom | tr -cd '[:alnum:]' | fold -w$pass_length | head -n1};
100字节足够生成N位的字母数字密码了,完全够用。
方案3:纯Perl生成随机密码(最推荐)
调用外部命令本来就容易出环境兼容问题,不如直接用Perl代码生成,高效又靠谱:
use Mojolicious::Lite; use strict; use warnings; use Data::Dumper; use List::Util qw(sample); # Perl 5.20+自带,比shuffle更高效 post 'testit' => sub { my $c = shift; my $pass_length = 3; # 定义所有可用的字母数字字符 my @chars = ('a'..'z', 'A'..'Z', 0..9); # 随机选$pass_length个字符拼接成密码 my $pass = join '', sample($pass_length, @chars); # 要是追求更安全的随机(比如符合密码学安全),可以直接读/dev/urandom: # open my $urand_fh, '<:raw', '/dev/urandom' or die "打不开/dev/urandom: $!"; # read $urand_fh, my $buf, $pass_length; # $pass = unpack('H*', $buf); # 转成十六进制字符串,或者自己映射成字母数字 warn Dumper $pass; return $c->render(json => {foo => 'bar', password => $pass}); }; app->secrets('foobar'); app->start;
最后总结下
这个坑的本质是Mojolicious事件循环环境下,外部管道命令的SIGPIPE信号传递异常,导致tr进程无法感知后续进程退出,进而无限读取/dev/urandom。三个方案里,纯Perl生成是最优解,避免了外部命令的依赖;要是非得用外部命令,就用Mojolicious的子进程API更稳妥。
内容的提问来源于stack exchange,提问作者ftumsh

