1. 从一次部署翻车说起:index.cgi 报错的真实面目
如果你在 Linux 服务器上部署过 Bugzilla,大概率见过这个让人血压升高的提示:登录页面能打开,输入管理员账号密码点提交,页面直接甩出一行couldn't create child process: 720002: index.cgi。更让人抓狂的是,Apache 的错误日志里往往只有这一行,没有堆栈、没有行号,看起来像是 Bugzilla 自己崩了,又像是 Web 服务器出了问题,排查方向一时半会儿理不清。
先把结论摆在前面:这个报错几乎从来不是 Bugzilla 代码本身的 bug,而是Web 服务器以某个系统用户身份去执行 index.cgi 时,进程创建环节失败了。720002这个数字是 Apache 在 Windows 平台上的错误码体系里常见的返回值,但在 Linux 环境下出现类似格式的报错,通常意味着底层fork/exec调用没有成功,或者 CGI 脚本的执行环境不满足要求。换句话说,Apache 想帮你跑这个 Perl 脚本,但它连"启动这个进程"这一步都没走通。
这篇文章面向的是正在自建缺陷跟踪系统、第一次接触 Bugzilla 部署的运维和开发同学,也适合那些已经装完但卡在登录环节、反复重启服务却毫无进展的人。我会把这类报错拆成几条独立的排查链路:文件权限与属主、SELinux 与 AppArmor 的拦截、Perl 解释器路径与依赖、Apache 的 CGI 执行配置、以及日志里那些容易被忽略的线索。每一条都会给出可复现的操作命令和判断依据,而不是泛泛地说"检查权限"。
需要提前说明的是,Bugzilla 是一个历史相当悠久的 Perl CGI 应用,它的运行模型和现在流行的前后端分离架构完全不同。它依赖 Apache 直接把.cgi文件当可执行程序来跑,这就意味着操作系统的进程创建权限、文件系统权限、安全模块策略三者必须同时放行,缺一个就会在登录这种"第一次真正触发 CGI 执行"的动作上暴露出来。首页能打开,往往只是因为静态资源或重定向没走到 CGI,一旦提交表单,问题立刻现形。
2. 为什么首页正常、一登录就炸:CGI 执行链路拆解
2.1 从浏览器请求到 index.cgi 被拉起的完整过程
要理解这个报错,得先搞清楚一次登录请求到底经历了什么。用户在登录页填写邮箱和密码,点击提交后,浏览器向服务器发送一个 POST 请求,目标地址通常是/bugzilla/index.cgi。Apache 收到请求后,根据配置判断这个路径属于 CGI 脚本目录,于是不会把它当静态文件返回,而是要启动一个新的进程来执行这个脚本。
这个"启动新进程"的动作,在 Apache 内部大致分几步:先确认执行该脚本所需的用户身份(通常是apache、www-data或httpd这类运行 Web 服务的账号),然后调用系统的进程创建接口,把 Perl 解释器和脚本路径作为参数传进去,最后把脚本的标准输出收集起来返回给浏览器。couldn't create child process这句提示,直译就是"无法创建子进程",说明卡在了"调用系统接口启动进程"这一步,脚本内容甚至还没开始执行。
这就解释了一个很关键的现象:为什么首页能打开。首页可能是一个静态 HTML,或者是一个不涉及 CGI 执行的跳转,Apache 直接读文件返回即可,根本不需要创建子进程。而登录动作必然触发index.cgi,于是进程创建失败的问题第一次被触发,报错也就第一次出现。所以看到这个提示,不要怀疑 Bugzilla 的 Perl 代码写错了,方向应该锁定在"为什么系统不允许 Apache 启动这个进程"。
2.2 720002 这个数字到底在说什么
很多同学会被720002这个数字吓到,以为是什么神秘的内部错误码。实际上在 Apache 的错误处理体系里,这类数字通常是操作系统返回的错误经过封装后的结果。在 Linux 上,进程创建失败最常见的底层原因是EACCES(权限不足)和ENOENT(文件或解释器不存在),前者对应"你没有权限执行它",后者对应"我要执行的东西根本找不到"。
720002在不同平台和不同 Apache 版本里的具体映射会有差异,所以不要死记这个数字,而要把它当成一个信号:进程创建被系统拒绝了。真正有价值的信息在 Apache 的错误日志里,那一行报错通常会带上更具体的描述,比如Permission denied、No such file or directory,或者安全模块的拒绝记录。把日志级别调高,往往能看到比浏览器提示多得多的细节。
我个人的习惯是,遇到这类报错第一件事不是改配置,而是先把日志打开看清楚。盲目地chmod 777一通操作,可能暂时让报错消失,但把整个目录的权限开成任何人都能读写执行,是给自己埋了一颗安全地雷。正确的做法是先定位到底是哪一类拒绝,再针对性地放行。
2.3 为什么这个坑在初次登录时才集中爆发
这里有个容易被忽略的时间线问题。安装 Bugzilla 的过程中,很多步骤是以 root 身份或者安装者自己的账号执行的,比如解压、运行checksetup.pl、生成配置文件。这些操作不会经过 Apache,所以权限问题不会暴露。等到服务启动、用户访问,Apache 以它自己的运行账号去碰这些文件时,问题才浮出水面。
更微妙的是,checksetup.pl在初始化时会创建localconfig文件、数据目录、以及一些临时目录。如果这些文件是以 root 身份创建的,属主就是 root,而 Apache 运行账号是apache或www-data,两者不一致,Apache 就没有权限去读取或执行。登录动作需要写会话、读配置、执行 CGI,任何一个环节被拒都会导致进程创建失败。所以这个报错本质上是"安装时的身份"和"运行时的身份"没有对齐。
理解了这一点,排查思路就清晰了:凡是 Apache 运行账号需要访问的路径,属主和权限都必须对它友好。下面几节就按这个逻辑,把最常见的几类原因逐一拆开。
3. 权限与属主:最容易被忽视的第一嫌疑
3.1 用 ps 确认 Apache 到底以谁的身份运行
排查权限问题,第一步永远是搞清楚"谁在跑"。不同发行版给 Apache 配置的运行账号不一样,Debian/Ubuntu 系通常是www-data,CentOS/RHEL 系通常是apache,有些自定义环境可能是nobody或别的名字。你可以用下面这条命令直接看主进程和子进程的属主:
ps -eo user,pid,ppid,cmd | grep -E 'apache|httpd' | grep -v grep输出里第一列就是运行账号。记下这个名字,后面所有权限调整都围绕它来做。我见过不少案例,安装文档里写的是apache,但实际环境因为改过配置,跑的是www-data,结果按文档改的权限全都没生效,白白折腾半天。
确认账号之后,还要注意一个细节:Apache 可能配置了User和Group两个指令,分别指定运行用户和运行组。有些环境下用户对了但组不对,而文件权限是按组来控制的,同样会失败。用apachectl -S或者直接看主配置文件里的User、Group指令,把这两个值都确认清楚。
3.2 逐层检查 Bugzilla 目录的属主与权限
Bugzilla 的目录结构里,有几个位置是 Apache 必须能访问的:整个安装目录需要可读可进入,index.cgi需要可执行,localconfig需要可读,数据目录和临时目录需要可写。任何一层出问题都会导致进程创建失败。可以用一条命令快速扫一遍关键路径:
ls -ld /var/www/html/bugzilla ls -l /var/www/html/bugzilla/index.cgi ls -l /var/www/html/bugzilla/localconfig ls -ld /var/www/html/bugzilla/data ls -ld /var/www/html/bugzilla/data/tmp判断标准很直接:目录的属主或属组应该是 Apache 运行账号,或者至少对它有读和执行权限;index.cgi必须有执行位;localconfig和data目录必须对 Apache 账号可读写。如果发现属主是 root,而权限又是700这种只有属主能进的形式,Apache 连目录都进不去,自然创建不了进程。
调整属主最稳妥的方式是递归改给 Apache 账号,同时保留合理的权限位:
chown -R apache:apache /var/www/html/bugzilla find /var/www/html/bugzilla -type d -exec chmod 755 {} \; find /var/www/html/bugzilla -type f -exec chmod 644 {} \; chmod 755 /var/www/html/bugzilla/*.cgi这里要特别提醒:不要图省事用chmod -R 777。把所有文件开成任何人可读写执行,短期内报错确实会消失,但等于把整个系统的安全边界拆掉了。Bugzilla 里存的是缺陷数据、用户信息,权限放开后任何本地账号都能篡改,风险极高。按上面的方式精确授权,才是可持续的做法。
3.3 localconfig 与 data 目录的写入权限陷阱
localconfig是 Bugzilla 的核心配置文件,里面记录了数据库连接信息、站点 URL、以及各种路径。这个文件在checksetup.pl运行时生成,如果当时是以 root 身份执行的,属主就是 root,权限可能是600。Apache 以apache账号去读它,直接被拒,进程创建失败。
data目录同理,它存放附件、临时文件、会话数据等,Apache 必须能写。如果这个目录属主是 root 且权限是755,Apache 能进但写不了,登录时创建会话文件就会失败。这两个位置是初次登录报错的高频元凶,值得优先检查。
一个实用的验证方法是切换到 Apache 账号,手动尝试读取和写入:
sudo -u apache cat /var/www/html/bugzilla/localconfig sudo -u apache touch /var/www/html/bugzilla/data/tmp/testfile如果第一条命令报Permission denied,说明localconfig权限有问题;如果第二条报错,说明data/tmp不可写。这种"以目标身份实测"的方式,比盯着权限数字猜要可靠得多,因为权限还受父目录、ACL、安全模块等多重因素影响。
4. SELinux 与 AppArmor:看不见的拦截者
4.1 权限都对了还是报错,先怀疑安全模块
有一种情况特别折磨人:你把属主、权限、执行位全都改对了,用sudo -u apache手动跑index.cgi也能出结果,但通过浏览器访问依然报couldn't create child process。这时候大概率不是文件权限的问题,而是强制访问控制模块在拦截。CentOS/RHEL 系默认启用 SELinux,Ubuntu 系默认启用 AppArmor,它们会在传统权限之外再加一层策略检查。
SELinux 的拦截有个特点:它不会体现在ls -l看到的权限位上,普通权限看起来完全正常,但内核层面就是拒绝。判断方法很简单,先看 SELinux 当前状态:
getenforce如果返回Enforcing,那它就在生效。再看有没有相关的拒绝记录:
ausearch -m avc -ts recent或者直接查审计日志里和 httpd 相关的条目。如果能看到denied字样,并且涉及httpd_t对某个文件类型的访问,基本可以确认是 SELinux 在拦。
4.2 用 audit2allow 定位被拦的具体操作
确认是 SELinux 拦截后,不要急着直接关掉它。关掉 SELinux 虽然能解决问题,但等于放弃了系统的一层重要防护,在生产环境里是不推荐的做法。更合理的思路是:先看清楚它到底拦了什么,再决定是调整文件的安全上下文,还是补充一条精细的策略。
audit2allow这个工具能把审计日志里的拒绝记录翻译成人类可读的说明,甚至生成策略建议:
ausearch -m avc -ts recent | audit2allow -a输出会告诉你"哪个进程想对哪个类型的文件做什么操作被拒绝了"。常见的 Bugzilla 场景是:Apache 想执行位于非标准路径下的 CGI 脚本,但该路径的文件安全上下文不是httpd_sys_script_exec_t,于是被拒。解决办法是给相关文件打上正确的上下文标签:
semanage fcontext -a -t httpd_sys_script_exec_t "/var/www/html/bugzilla(/.*)?" restorecon -Rv /var/www/html/bugzilla这两条命令的意思是:先声明这个路径下的文件应该属于"可被 httpd 执行的脚本"类型,然后让系统按这个声明重新打标签。执行完之后再访问,往往就通了。如果还不行,再考虑用audit2allow -M生成一个自定义策略模块并加载,但这一步要谨慎,确保生成的规则范围足够小。
4.3 AppArmor 环境下的对应处理思路
Ubuntu 系用的是 AppArmor,机制和 SELinux 不同,但排查逻辑类似。先看 AppArmor 状态:
aa-status如果 httpd 或 apache2 的 profile 处于 enforce 模式,它就会按预定义的规则限制 Apache 能访问哪些路径。Bugzilla 如果装在 profile 允许范围之外的目录,就会被拦。查看拒绝记录可以看内核日志:
dmesg | grep -i apparmor或者查/var/log/syslog里的相关条目。处理方式有两种:一是把 Bugzilla 目录加入 profile 的允许列表,二是如果这个 profile 本身限制过严且不影响其他服务,可以调整为 complain 模式观察。但同样不建议直接禁用整个 AppArmor,那会削弱系统整体防护。
提示:安全模块的调整一定要在测试环境先验证,确认策略生效且不影响其他站点后再上生产。改完记得用
restorecon或重载 profile 让配置生效,光改文件不重载是没用的。
5. Perl 解释器与依赖:进程启动前的最后一道坎
5.1 shebang 行与解释器路径必须真实存在
CGI 脚本的第一行通常是 shebang,形如#!/usr/bin/perl或#!/usr/bin/env perl。这一行告诉系统"用哪个解释器来跑我"。如果这个路径指向的解释器不存在,系统在创建进程时就会失败,报错信息里往往带No such file or directory。排查方法很直接:
head -1 /var/www/html/bugzilla/index.cgi which perl ls -l /usr/bin/perl如果 shebang 写的是/usr/bin/perl,但实际 Perl 装在/usr/local/bin/perl,那就对不上。解决办法要么是修正 shebang 指向真实路径,要么是在/usr/bin/perl建一个软链接。用env perl的写法相对灵活,它会在 PATH 里找 perl,但要注意 Apache 运行环境的 PATH 可能和你登录 shell 的 PATH 不一样,所以显式写绝对路径往往更稳。
还有一种隐蔽情况:shebang 行末尾带了 Windows 风格的\r(回车符)。这种文件在 Linux 下执行时,系统会把\r当成路径的一部分,导致找不到解释器。用file命令或cat -A能看到行尾的^M。修复方式是转换文件格式:
sed -i 's/\r$//' /var/www/html/bugzilla/index.cgi5.2 用命令行直接跑脚本,把问题从 Apache 里剥离出来
当浏览器报错信息太少时,最有效的办法是绕过 Apache,直接用 Apache 的运行账号在命令行执行脚本,看它到底报什么:
sudo -u apache /usr/bin/perl /var/www/html/bugzilla/index.cgi如果这条命令能正常输出 HTML,说明脚本本身和 Perl 环境没问题,问题在 Apache 的配置或安全模块。如果它报出具体的 Perl 错误,比如缺少某个模块、找不到某个库,那就顺着这个错误去补依赖。这种"分层剥离"的排查思路,能把一个模糊的进程创建失败,拆解成一个个具体可验证的小问题。
常见的 Perl 依赖缺失表现是Can't locate XXX.pm in @INC。Bugzilla 依赖大量 Perl 模块,安装时如果漏装,运行到某个功能就会报错。可以用 Bugzilla 自带的检查脚本确认依赖是否齐全,缺什么补什么。注意补装模块时要用和 Apache 同一套 Perl 环境,避免出现"命令行能跑、Apache 跑不了"的错位。
5.3 环境变量与 PATH 在 CGI 上下文中的差异
CGI 进程的环境变量和交互式 shell 差别很大。你在终端里echo $PATH看到的一长串路径,在 Apache 启动的 CGI 进程里可能被精简成很短的一段。如果脚本或它调用的程序依赖 PATH 里的某个路径,在 CGI 环境下就可能找不到。这也是为什么显式写绝对路径更可靠。
可以在 Bugzilla 的配置里检查是否有依赖外部命令的地方,确保这些命令用的是绝对路径。另外,Apache 的SetEnv指令可以给 CGI 进程注入环境变量,必要时用它补上缺失的 PATH 或自定义变量。但要注意,注入的环境变量不要包含敏感信息,也不要引入安全风险。
6. Apache 配置:CGI 执行开关与目录授权
6.1 确认 CGI 模块已加载、脚本目录已授权
Apache 要执行 CGI,必须加载相应的模块,并且对脚本所在目录显式授权。如果模块没加载,或者目录没有ExecCGI权限,请求会被拒绝。检查模块是否加载:
apachectl -M | grep cgi应该能看到cgi_module或cgid_module。如果没看到,需要在配置里启用。目录授权方面,Bugzilla 目录的配置块里应该有类似这样的内容:
<Directory /var/www/html/bugzilla> AddHandler cgi-script .cgi Options +ExecCGI AllowOverride None Require all granted </Directory>Options +ExecCGI是关键,没有它 Apache 不会把.cgi当程序执行。AddHandler则告诉 Apache 哪些扩展名按 CGI 处理。这两项缺一不可。改完配置记得用apachectl configtest检查语法,再平滑重载服务。
6.2 Suexec 与运行身份的交互影响
有些环境启用了mod_suexec,它允许 CGI 脚本以文件属主的身份运行,而不是统一用 Apache 账号。这个机制本意是提升隔离性,但如果配置不当,反而会导致进程创建失败。比如脚本属主是一个没有登录权限的系统账号,或者 Suexec 的目标用户配置有误,都会让进程起不来。
判断是否启用了 Suexec,可以看模块列表里有没有suexec_module,以及配置里有没有SuexecUserGroup指令。如果启用了,就要确保脚本属主和 Suexec 配置的用户一致,并且该用户对脚本和依赖路径都有访问权限。如果不需要这种隔离,也可以考虑关掉 Suexec,让脚本统一以 Apache 账号运行,简化权限模型。两种方式各有取舍,关键是保持"运行身份"和"文件属主"的一致性。
6.3 日志级别调高,让报错说出更多细节
默认的日志级别可能只记录一行笼统的报错,把级别调高能看到更多上下文。在 Apache 配置里把LogLevel设为debug或info,然后重现一次登录操作,再看错误日志:
tail -f /var/log/httpd/error_log调高日志级别后,通常能看到进程创建失败的具体原因,比如是权限被拒、解释器找不到,还是安全模块拦截。定位到原因后,记得把日志级别调回正常值,避免长期输出大量调试信息拖慢服务、占满磁盘。这个"临时调高、定位后调回"的习惯,在排查各种疑难问题时都很实用。
7. 一套可复用的排查顺序与实战心得
7.1 从外到内的五步排查法
把前面几节的内容串起来,我总结出一套从外到内的排查顺序,按这个顺序走,基本能覆盖绝大多数couldn't create child process场景:
| 步骤 | 检查对象 | 关键命令 | 典型结论 |
|---|---|---|---|
| 1 | Apache 运行账号 | ps -eo user,cmd | grep httpd | 确认是 apache 还是 www-data |
| 2 | 文件属主与权限 | ls -l index.cgi localconfig | 属主是否匹配、执行位是否存在 |
| 3 | 以目标身份实测 | sudo -u apache perl index.cgi | 脚本本身能否跑通 |
| 4 | 安全模块状态 | getenforce/aa-status | 是否被 SELinux/AppArmor 拦截 |
| 5 | Apache 配置与日志 | apachectl -M/tail error_log | CGI 模块、ExecCGI、具体报错 |
这个顺序的逻辑是"先确认身份,再确认权限,再剥离 Apache 验证脚本,最后看安全模块和配置"。每一步都有明确的验证手段,避免盲目猜测。实际排查时,往往在第二步或第三步就能定位到问题,后面的步骤作为兜底。
7.2 几个我踩过的真实坑
第一个坑是属主改了但没改组。有次我把 Bugzilla 目录属主改成了apache,但属组还是 root,而某些文件的权限是750,组用户没有读权限,结果 Apache 能进目录却读不了文件。后来把属组也一起改掉才通。所以改权限时,用户和组要一起看。
第二个坑是SELinux 上下文没恢复。我用cp把 Bugzilla 文件复制到 Web 目录,文件内容没问题,但安全上下文还是源目录的标签,导致 Apache 执行被拒。用restorecon重新打标签后立刻恢复。这个坑的隐蔽性在于,普通权限看起来完全正常,只有安全模块知道不对劲。
第三个坑是Perl 模块装在了错误的路径。我在命令行用cpan装了模块,命令行跑脚本没问题,但 Apache 跑就报找不到模块。原因是命令行用的 Perl 和 Apache 用的 Perl 不是同一套,模块装到了另一套的@INC路径里。后来统一了 Perl 环境才解决。这个教训是:永远用 Apache 的身份去验证,而不是用你自己的登录身份。
7.3 预防胜于救火:部署时的几个习惯
与其等报错再排查,不如在部署阶段就把这些坑避开。我的习惯是:安装 Bugzilla 时就用目标运行账号来执行checksetup.pl,或者安装完立刻把整个目录的属主递归改成 Apache 账号;装完先不急着开浏览器,而是用sudo -u apache手动跑一遍index.cgi,确认脚本层面没问题;然后再启动服务、访问登录页。这样能把问题在早期就暴露出来,而不是等到用户登录时才炸。
另外,部署文档里最好记录清楚当前环境的 Apache 运行账号、SELinux 状态、Perl 路径这几个关键信息。下次迁移或重装时,直接对照这些信息配置,能省下大量重复排查的时间。Bugzilla 这类老牌 CGI 应用,对运行环境的"身份一致性"要求很高,把这一点吃透,后面遇到类似报错都能快速定位。
最后分享一个判断小技巧:如果报错只在登录时出现,首页正常,那问题几乎一定在"CGI 执行"这条链路上,而不是数据库或网络。顺着权限、安全模块、解释器、Apache 配置这四条线查,基本不会跑偏。真正难缠的从来不是报错本身,而是不知道从哪查起。把排查顺序固定下来,再奇怪的报错也能一步步收敛到根因。