授权与合规声明
本文全部操作对象均为自建隔离靶场(本机容器或隔离虚拟机),涉及安全测试的环节必须以取得合法授权为前提。未经授权的渗透测试违反《中华人民共和国网络安全法》与《刑法》相关条款,须承担相应法律责任。本文只讲环境配置、版本对照与靶场隔离,不含任何攻击步骤、利用载荷与绕过手法,请勿将文中环境指向任何非自有系统。
一、先问一句:这个请求最后落在哪个进程上
1.1 一个页面,其实是两段路
在靶场里打开一个 PHP 页面,页面出来了,这件事在体感上是一整块。但在机器内部,它至少经过两段完全不同的路。
第一段是 Apache 接住这个连接。这一段由 MPM 负责——它监听端口、接受请求,然后从自己维护的一批子进程里挑一个出来招呼这个连接。第二段是 PHP 真正执行。如果 PHP 是以 FastCGI 方式跑的,那么 Apache 只是把请求转交给另一组进程,也就是 PHP-FPM 的 master 与 worker,由 FPM 的某个 worker 去执行那个脚本。
所以「谁在处理这个请求」这个问题,答案不是唯一的,而是分两段、每段各有一个答案。第一段是「某个 Apache 子进程」,第二段是「某个 FPM 的 worker」。两段各有各的上限与计数方式,而且不是同一个东西。
本文就是把这两段拆开,看清「谁在被派活、规则写在哪儿、那条线用什么单位数」。
1.2 为什么这个问题最容易被跳过
因为它不影响你「跑起来」。
默认安装、默认配置,页面就是能开。能开的时候没人会问「刚才是哪个进程在干活」。只有当页面开始变慢、开始时而正常时而不正常,这个问题才会浮上来——而那时候,你已经习惯了从代码、从日志、从配置文件里找原因。
问题在于,「这个请求落在谁身上」不在任何一份配置文件的某个字段里。它不在php.ini里,也不在你写的代码里。它是几个模块协作出来的结果:Apache 编进了哪个 MPM、那个 MPM 用什么单位数人头、FPM 的池里那个数字设成了多少。三者分散在三处,谁也不会主动告诉你它们是一套的。
于是最常见的误判就出现了:把「Apache 那边满了」当成「PHP 那边满了」,或者反过来。两边的表现都是「请求排队、页面变慢」,但你要去看的地方完全不同。分不清这两段,你改的那一处大概率不是卡住的那一处。
把两段路的差别先摆出来,后面几章就有了骨架。
| 阶段 | 谁来接住请求 | 这条线由谁定义 | 这条线管的是什么 |
|---|---|---|---|
| 第一段 | Apache 的某个子进程(或子进程里的某个线程) | MPM 模块 | 同时能招呼多少个连接 |
| 第二段 | PHP-FPM 池里的某个 worker 进程 | FPM 池配置 | 同时能执行多少个脚本 |
1.3 本文的三条边界
写清楚边界,比写清楚结论更重要,因为这类文章最容易被当成「操作手册」照抄。
第一条,只讲进程模型与默认值,不讲任何调参方法。你会看到很多「默认是多少」,但看不到一句「所以应该改成多少」——该设成多少,取决于那台机器上还跑着什么。
第二条,不出现任何量级数字。不给承载能力的估计,也不画负载曲线——本文关心的是「这个数字在数什么」,不是「它应该多大」。
第三条,能实测的就实测,测不了的就标待验证。本文有两处是本机真实跑出来的输出,我在文里明确标了「本机实测」;凡是我没有真正跑过的命令,都挂了待验证标记。这条规矩看起来啰嗦,但它决定了你照抄的时候会不会踩空。
二、MPM 做的三件事:绑端口、接连接、派子进程
2.1 官方定义里的三个动词
Apache 2.4 的文档里,对 MPM 的定义只有一句话,但这句话信息量很大。原文逐字为:
The server ships with a selection of Multi-Processing Modules (MPMs) which are responsible for binding to network ports on the machine, accepting requests, and dispatching children to handle the requests.
拆开看,它说的是三件事:绑定端口(binding to network ports)、接受请求(accepting requests)、派子进程去处理(dispatching children to handle the requests)。
前两个动作是「守门」,第三个是「派活」。注意第三个的措辞——不是 handle,而是dispatch:MPM 自己不处理请求,只负责把请求交给一个子进程。这是理解全文的钥匙:MPM 是调度者,不是执行者。
接受这个定位,很多现象就顺了:MPM 的配置项全是「多少个进程」「多少个空闲的」这类词,因为它管的是「有多少人手」,不是「活儿怎么干」;换个 MPM,页面的业务逻辑一行都不用改,因为业务逻辑不在它这一层。
2.2 同一时刻只能有一个 MPM
紧接着的一句话同样关键,原文逐字为:
The main difference is that one and only one MPM must be loaded into the server at any time.
有且只有一个。这句话排除了一整类误解:prefork、worker、event 不是三个可以叠加的加速器,而是三个互斥的替代品。你不是「把 event 也开上」,而是「把 prefork 换成 event」。
这也说明查看当前 MPM 很有意义:它决定了后面所有数字的单位。同样是MaxRequestWorkers,prefork 下数进程,worker 下数线程,含义差得很远。
想看当前编进了哪个 MPM,一条只读命令就够。这条命令我在本机跑过-V和-M,但下面这条-l的写法我没有单独留存输出,所以按规矩挂标记。
⚠️代码待验证
# 只读:列出编译进 httpd 的模块,从输出里找 mpm_ 开头的模块httpd-l|grep-impm2.3 为什么要「派」而不是「自己干」
这里值得多问一层:既然 MPM 已经在接连接了,为什么不让它顺手把请求也处理掉?
因为「接连接」和「跑业务」是两种节奏。接连接必须一直在,而且要快——它只是在等新连接出现,等到就往下传,不该被任何具体的事情拖住。跑业务则可能很慢,可能卡在读写文件或等数据库回应上。
两者由同一个执行体来做,「等」就会占住「接」:一个人埋头算题,门口来了新客人,他听不见。解决办法不是让他算快一点,而是分成两个人——一个专门守门,一个专门干活。守门的永远清醒,干活的有多少可以按需增减。
这就是 MPM 的设计动机。prefork 的文档里有一句把这件事说得很直白,原文逐字为:
Apache httpd always tries to maintain several spare or idle server processes, which stand ready to serve incoming requests.
注意「stand ready」这个词。空闲进程不是浪费,它是预备队。它的存在意义就是让新请求不必等一个人腾出手来。
三、三种 MPM 的差别只在「谁来处理」
三种 MPM 常被讲成三档性能,这是最省事也最没用的讲法。真正的差别只有一个:一个请求进来,最后是被一个进程接住,还是被一个进程里的某个线程接住。
3.1 prefork:一个进程,一次一个请求
prefork 的模块页描述只有一句:Implements a non-threaded, pre-forking web server——非线程的、预派生的 Web 服务器。说明里有一句把定位讲得很清楚,原文逐字为:
This Multi-Processing Module (MPM) implements a non-threaded, pre-forking web server. Each server process may answer incoming requests, and a parent process manages the size of the server pool. It is appropriate for sites that need to avoid threading for compatibility with non-thread-safe libraries. It is also the best MPM for isolating each request, so that a problem with a single request will not affect any other.
三个要点:每个进程自己就能应答请求(不需要线程);由一个父进程管理池子的大小;最适合把每个请求隔离开——一个请求出问题,波及不到另一个。
隔离性是 prefork 最实在的优点:两个请求落在两个进程里,连内存都不共享,一个把内存写坏了,另一个毫不知情。这也是它至今仍是很多环境默认选择的原因——尤其在要兼容「不是线程安全」的老库时。
它的工作方式,文档写得更具体:
A single control process is responsible for launching child processes which listen for connections and serve them when they arrive.
一个控制进程负责启动子进程,这些子进程自己去监听连接,连接到了就服务。注意这里有个容易看漏的细节:监听这件事是子进程在做,不是父进程接住了再往下发。父进程只管「生」和「收」。
3.2 worker:进程里再开线程
worker 走的是另一条路。原文逐字为:
This Multi-Processing Module (MPM) implements a hybrid multi-process multi-threaded server. By using threads to serve requests, it is able to serve a large number of requests with fewer system resources than a process-based server. However, it retains much of the stability of a process-based server by keeping multiple processes available, each with many threads.
「混合的多进程多线程」——进程还在,但每个进程里开了一批线程,请求由线程来招呼。它的取舍写得很坦白:用线程,是为了用更少的系统资源招呼更多的请求;保留多进程,是为了保住进程模型的那份稳定性。
它的工作方式是这样分配的:
Each child process creates a fixed number of server threads as specified in the ThreadsPerChild directive, as well as a listener thread which listens for connections and passes them to a server thread for processing when they arrive.
每个子进程按ThreadsPerChild创建固定数量的线程,另外还有一个监听线程,负责听连接,并把连接交给某个工作线程去处理。
「固定数量」四个字值得留意:worker 的伸缩不是靠「进程里线程忽多忽少」,而是靠「进程有几个」。进程是粗粒度旋钮,线程是进程内部的固定编制。
3.3 event:线程只留给「正在干活」的连接
event 的定位,文档一句话就说完:它是 worker 的变体,目标是只为正在活跃处理的连接占用线程(consuming threads only for connections with active processing)。
不解释很难看出它解决了什么。它解决的是keep-alive 问题,原文逐字为:
This MPM tries to fix the ‘keep alive problem’ in HTTP. After a client completes the first request, it can keep the connection open, sending further requests using the same socket and saving significant overhead in creating TCP connections. However, Apache HTTP Server traditionally keeps an entire child process/thread waiting for data from the client, which brings its own disadvantages.
说白了:HTTP 允许一个连接用完不关,留着下次接着用,省掉重复建连接的开销。但传统模型里,这条留着的连接依然占着一个完整的进程或线程,哪怕它这会儿一个字都没在传。
这就是「占着人手不干活」。空闲连接越多,被白占的人手越多。
event 的解法是换一个角色分工:
To solve this problem, this MPM uses a dedicated listener thread for each process to handle both the Listening sockets, all sockets that are in a Keep Alive state, sockets where the handler and protocol filters have done their work and the ones where the only remaining thing to do is send the data to the client.
每个进程有一个专门的监听线程,它统管几类套接字:正在监听的、处于 keep-alive 状态在等的、以及那些「活已经干完只差把数据发出去」的。换句话说,「等」这件事被集中到了一个线程里,不再各自占一个执行体。
支撑它的底层是两样东西:非阻塞套接字,以及 APR 暴露的现代内核特性(比如 Linux 的 epoll)。直接结果是——不再需要mpm-accept那个互斥锁去避免「惊群」。
3.4 三种 MPM 的对照
把三者的差别压成一张表,「谁来处理」这个问题就有答案了。
| MPM | 谁来接住一个请求 | 一个执行体能同时招呼几个 | 代价 |
|---|---|---|---|
| prefork | 一个独立进程 | 一个 | 隔离性最好,但每个请求都要占一个完整进程 |
| worker | 进程里的一个线程 | 一个线程一个 | 资源开销小,但线程共享地址空间,隔离性弱于 prefork |
| event | 由监听线程分派,只在真正处理时才占线程 | 一个线程一个,但「等」的时候不占 | 结构最复杂,专治 keep-alive 白占人手 |
官方给的选型口径,原文逐字为:
sites that need a great deal of scalability can choose to use a threaded MPM like worker or event, while sites requiring stability or compatibility with older software can use a prefork.
要「可伸缩」,选带线程的 worker 或 event;要「稳定」或「兼容老软件」,用 prefork。这两句话不是性能排名,是取舍方向的说明。
四、Apache 侧的那条线:一个指令,两种单位
4.1 prefork 里,这个数字数的是进程
prefork 页面里有一句关于容量的话,值得逐字读:
Most important is that MaxRequestWorkers be big enough to handle as many simultaneous requests as you expect to receive, but small enough to assure that there is enough physical RAM for all processes.
这句话给了两个方向的约束,而不是一个方向。一边要「够大」,大到能接住你预期会来的那些同时请求;另一边要「够小」,小到物理内存能装下所有这些进程。
「small enough」这半句最容易被跳过。很多人只记住前半句,觉得越大越好;但官方在同一句话里就说了,它同时被内存压着——prefork 的每个执行体是一个完整进程,各占一份内存。
4.2 worker 里,同一个名字,数的是线程
worker 页面提到同一个指令时,措辞变了,原文逐字为:
The most important directives used to control this MPM are ThreadsPerChild, which controls the number of threads deployed by each child process and MaxRequestWorkers, which controls the maximum total number of threads that may be launched.
MaxRequestWorkers在这里控制的是「可能被启动的线程总数」。而在 prefork 里,它控制的是「进程数」。
同一个指令名,两种单位。这是本文最想让你记住的一处细节,误读的后果很具体:你以为设的是「进程上限」,实际设的是「线程上限」;或者你按进程的体积去估内存,实际跑的是线程模型。单位错了,算出来的东西一定错。
| 指令 | 在 prefork 下数什么 | 在 worker 下数什么 | 误读的后果 |
|---|---|---|---|
MaxRequestWorkers | 子进程的个数 | 所有子进程里线程的总数 | 拿进程的体积去套线程模型,估出来的内存对不上 |
ThreadsPerChild | 不适用(prefork 无线程) | 每个子进程固定开多少线程 | 以为是「上限」,其实是「每个进程的固定编制」 |
4.3 「默认用哪个」没有统一答案
「Apache 默认用哪个 MPM」没有全局答案,官方给的是一张按平台分的表:
| 平台 | 默认 MPM |
|---|---|
| Unix | prefork、worker 或 event,取决于该平台的能力 |
| Windows | mpm_winnt |
「depending on platform capabilities」——取决于平台能力。同一份 Apache 装在不同系统上,默认 MPM 可能不同。所以第六章要拿本机实测一次:与其背结论,不如在自己机器上问一句。
想看运行时的实际配置(包括那个指令的当前取值),可以用这条导出命令。这条我没有在本机留存输出,按规矩挂标记。
⚠️代码待验证
# 只读:导出运行时配置,其中包含 MPM 相关的指令当前取值httpd-t-DDUMP_RUN_CFG注意这条命令是-t开头的,它做的是配置语法检查并顺带导出,不会启动或重启服务。这是本文所有命令的共同前提:只读、不改状态。
五、PHP-FPM 侧:一个池,一个数字
5.1 FPM 是另一组进程,不属于 Apache
Apache 的 MPM 只管到「把请求交出去」为止。如果这个请求是 PHP,它还要落到另一组进程上——PHP-FPM。
这组进程有自己的结构:一个 master 管着一批 worker。master 不执行业务,只按池的配置把 worker 生出来、收回去。「同时能执行多少个 PHP 脚本」,是这组进程决定的,不是 Apache 决定的。
这意味着排「请求变慢」时面对的是两个独立的名额池:Apache 那边满了,请求进不来;FPM 那边满了,请求进来了但没人在跑。现象相似,位置完全不同。
想看清这台机器上有没有这组进程、有几个,用ps就够了。本机没有装 php-fpm,所以下面这条我无法实测。
⚠️代码待验证
# 只读:看 FPM 的 master 与 worker 进程(本机无 php-fpm,未实测)ps-eopid,ppid,comm|grep-iphp-fpm如果输出里能看到一个父进程带着若干子进程,那就是 master 与 worker 的关系;如果一条都没有,说明这台机器上的 PHP 不是以 FPM 方式跑的——那第二段路根本不存在,你的问题只在第一段。
5.2 一个池,一个数字:pm.max_children
FPM 的池配置文件(发行版里通常叫www.conf,由源码里的www.conf.in生成)里,有一个数字承担了「同时能服务几个请求」这件事。
官方配置里的注释逐字为:
The number of child processes to be created when pm is set to ‘static’ and the maximum number of child processes when pm is set to ‘dynamic’ or ‘ondemand’. This value sets the limit on the number of simultaneous requests that will be served. Equivalent to the ApacheMaxClients directive with mpm_prefork.
这句话里最值钱的是中间那句:这个值设定了「同时会被服务的请求数」的上限。FPM 侧那条线,最终就落在pm.max_children这一个数字上。
最后一句也值得看:这个值等价于 prefork 下的ApacheMaxClients。官方自己就把两条线对照起来了——它们数的是同一件事,只是分处两段路。
至于这个值的默认数,截至 2026-10-20,源码里的www.conf.in给的是5。同一个文件里还有几项默认值:
| 配置项 | 默认值 | 说明 |
|---|---|---|
pm | dynamic | 池的进程管理方式 |
pm.max_children | 5 | 同时能被服务的请求数上限 |
pm.start_servers | 2 | dynamic 模式下的起步进程数 |
pm.min_spare_servers | 1 | 空闲进程的下限 |
pm.max_spare_servers | 3 | 空闲进程的上限 |
;pm.max_requests | 500 | 默认被注释,即未启用 |
;pm.process_idle_timeout | 10s | 默认被注释,即未启用 |
最后两行前面的分号不是排版错误,是注释符号。它造成了一类很典型的误读:你在配置文件里明明看到「500」,就以为「处理 500 个请求后重启」在生效,实际前面有个分号,根本没生效。
「写了」和「生效了」是两件事,中间隔着一个注释符。这个坑不只 FPM 有,但 FPM 这里是重灾区,因为默认值往往就处在被注释掉的状态。
5.3 同一个数字,在三种模式下含义不同
pm有三种取值:static、dynamic、ondemand。它们共用pm.max_children这一个数字,但这个数字在三种模式下的身份不一样。
从上面那段注释就能读出来:在static下,它是「要创建多少个子进程」;在dynamic或ondemand下,它是「子进程最多能有多少个」。前者是固定编制,后者是天花板。
这个区别在观察时很关键:pm是static时数出来的进程数应当稳定;是dynamic时会随负载浮动,但不会越过那条线。同样是「数出几个进程」,两种模式下解释完全不同。
dynamic模式下另外三项(start_servers、min_spare_servers、max_spare_servers)从名字就能看出方向:起步多少、空闲的下限与上限。它们管「平时留多少人在待命」,max_children管「最多能叫多少人」。具体调度行为以官方池配置说明为准,本文不逐条展开。
完整版对照表:把这篇文章里 Apache 侧与 FPM 侧的两条线——谁在数、数的是什么单位、默认值是多少——压成了一页对照。放在资料包里,扫码即可获取:
六、本机实测:同一台机器上的 MPM 是什么
6.1httpd -V读的是编译与运行信息
前面说「默认 MPM 取决于平台」,那就在本机上问一句。下面摘出这条命令最关键的四行(本机实测,macOS,2026-10-20):
本机实测(macOS,2026-10-20):
$ /usr/sbin/httpd-VServer version: Apache/2.4.67(Unix)Server MPM: prefork threaded: no forked:yes(variable process count)四行里,真正有用的是后三行。
Server MPM: prefork直接回答了「用哪个」。threaded: no说明它是非线程的,和第三章的定位完全对得上。forked: yes (variable process count)里的variable process count说明进程数可变,这正是「父进程管池子大小」的体现。
第一行Apache/2.4.67 (Unix)是截至 2026-10-20 本机这份 Apache 的版本。版本号本身不重要,重要的是旁边的(Unix)与后面那个prefork是一组信息,印证了第四章表里「Unix 上可能是 prefork」这一行。
6.2httpd -M读的是加载了哪些模块
-V告诉你「用的是哪个」,-M告诉你「装了哪些」。
本机实测(macOS,2026-10-20):
$ /usr/sbin/httpd-M|grep-impm mpm_prefork_module(shared)一行,就一个模块。这一行同时验证了两件事:
第一,MPM 是以模块形式存在的,所以「换 MPM」在操作上就是「换一个模块加载」。这和第二章那句「有且只有一个」正好对上——列表里只有一个mpm_开头的模块,不是巧合,是规则。
第二,(shared)说明它是动态加载的共享模块,不是编译进核心的静态模块。排查时有用:如果-l(静态列表)和-M(已加载列表)对不上,说明有人动过加载项。
6.3 这两条输出读得出什么、读不出什么
实测的价值不只在于「得到了什么」,也在于「知道它没告诉你什么」。
读得出的:这台机器上 Apache 用的是 prefork,非线程模型,MPM 是共享模块,版本 2.4.67。
读不出的:MaxRequestWorkers的实际取值。-V和-M都不打印这个数,得从运行时配置里导出(第四章那条命令)。待验证:某台具体机器上这个指令的实际取值,以及它是否被conf/extra下的文件覆盖过——本文未逐台核对,属待验证范围。
更读不出的:PHP-FPM 的存在与配置。httpd的任何命令都不讲 FPM 那边的事,那是另一个程序。本机没有 php-fpm,这一块只能依据官方文档讲机制。
待验证:FPM 池配置文件在发行版里的实际路径——Debian 系与 RHEL 系放的位置不同,本文不给具体路径,你按自己那台机器去找,别照抄别人博客里的路径。
一句话收这一章:在自己机器上问一句,永远比背一个结论可靠。同一份 Apache,换个系统,这两条命令的输出可能完全不同。
七、排查顺序:从「谁在处理」往下问五步
7.1 五步,每步只回答一个问题
前面六章讲机制,落到排查上可以收成一个固定的提问顺序,设计原则是:每一步只回答一个问题,答案必须来自一次观察,不能来自猜测。
| 步骤 | 要回答的问题 | 去看哪里 | 如果这一步的答案是「否」 |
|---|---|---|---|
| 1 | 这个请求是静态资源还是动态脚本 | 请求的路径与后缀 | 静态资源不经过 FPM,只需看第一段 |
| 2 | Apache 用的是哪个 MPM | httpd -V的Server MPM一行 | 单位还没确定,后面两步都无从谈起 |
| 3 | 这条线数的单位是进程还是线程 | 由第 2 步决定,对照第四章的表 | 单位错了,算出来的容量一定是错的 |
| 4 | FPM 池里pm.max_children是多少 | 池配置文件;先确认那一行没被注释 | 若被注释,用的是内置默认,不是你看到的那个数 |
| 5 | 这两段的上限是不是同一个「同时」 | 把第 3、4 步放在一起看 | 两段是串联的,各自的「满」是两件事 |
第三步和第五步最容易做错。
第三步错在单位:在 worker 模型里按「进程数」去理解MaxRequestWorkers,会同时高估内存、低估容量,两个方向都错。
第五步错在「串联」:请求要先过第一段才到第二段,所以「第一段没满、第二段满了」完全可能,反过来也可能。它们不是同一个池子,不能把一个的余量当成另一个的。
7.2 一套只读的观察组合
把前面散落的命令按顺序摆一下。这一组我只有-V和-M两行在本机跑过,其余按规矩整体挂标记。
⚠️代码待验证
# 1) 当前用的是哪个 MPM,是否线程模型httpd-V# 2) 加载了哪些模块,确认 mpm_ 只有一项httpd-M|grep-impm# 3) 运行时配置里那条上限的当前取值httpd-t-DDUMP_RUN_CFG# 4) FPM 侧:master 与 worker 的进程关系ps-eopid,ppid,comm|grep-iphp-fpm四条全部只读。第 1、2 条回答「第一段是什么模型」,第 3 条回答「第一段的上限是多少」,第 4 条回答「第二段在不在」。
附带作用是确认某一层到底存不存在:第 4 条若什么都没输出,就不用再纠结 FPM 的配置了——那段路在这台机器上根本没走。
7.3 一句话记住这篇
如果只带走一句,就是这句:别问「能扛多少」,先问「这一个请求落在谁身上」。
前一个问法要你先有数字,后一个只要看一眼。而且更实际的是:不知道「落在谁身上」时,任何数字都无法解释——你不知道它在数进程、数线程,还是数 worker。
本文没有给任何一个「应该设成多少」的答案,这有意为之:进程模型的价值不在调参,而在于让你在动手之前知道自己在动哪一层。看清楚之前不动手,本身就是一种能力。
环境自查流程图:从「这是静态还是动态」问到「两段的上限是不是同一个同时」,五步走完的判定路径我画成了一张图,也放在资料包里,扫码即可获取:
附表 A:本文引用事实与官方出处对照表
| 事实陈述 | 出处 | 核验日期 | 本文位置 |
|---|---|---|---|
| MPM 负责绑定端口、接受请求、派子进程处理请求(原文 “binding to network ports…accepting requests, and dispatching children”) | httpd.apache.org《Multi-Processing Modules (MPM)》mpm.html | 2026-10-20 | 2.1 |
| 任意时刻必须有且只有一个 MPM 被加载(原文 “one and only one MPM”) | 同上,mpm.html | 2026-10-20 | 2.2 |
| 需要可伸缩选 worker 或 event,需要稳定或兼容老软件选 prefork | 同上,mpm.html | 2026-10-20 | 3.4 |
| 默认 MPM:Unix 为 prefork/worker/event,取决于平台能力;Windows 为 mpm_winnt | 同上,mpm.html 默认 MPM 表 | 2026-10-20 | 4.3 |
| prefork 实现非线程、预派生的 Web 服务器;每个进程可应答请求,父进程管理池大小;最适合隔离单个请求 | httpd.apache.org《Apache MPM prefork》 | 2026-10-20 | 3.1 |
| prefork 始终设法维持若干备用或空闲进程(“stand ready”) | 同上,How it Works 段 | 2026-10-20 | 2.3 |
| prefork 的 MaxRequestWorkers 要大到够用、小到物理内存装得下所有进程 | 同上 | 2026-10-20 | 4.1 |
| worker 是混合的多进程多线程模型,每个子进程按 ThreadsPerChild 开固定线程,另有监听线程 | httpd.apache.org《Apache MPM worker》 | 2026-10-20 | 3.2 |
| worker 的 MaxRequestWorkers 控制「可被启动的线程总数」 | 同上 | 2026-10-20 | 4.2 |
| event 目标为只对活跃处理的连接占用线程;为解决 keep-alive 问题,每进程设专用监听线程统管各类套接字 | httpd.apache.org《Apache MPM event》 | 2026-10-20 | 3.3 |
| event 基于非阻塞套接字与 APR 暴露的内核特性(如 epoll),不再需要 mpm-accept 互斥锁 | 同上 | 2026-10-20 | 3.3 |
pm.max_children默认 5;该项设定「同时被服务的请求数」上限,等价于 prefork 下的 ApacheMaxClients | php.net《PHP-FPM 配置》install.fpm.configuration,含 php-src 的www.conf.in | 2026-10-20 | 5.2 |
www.conf.in默认:pm=dynamic、start_servers=2、min_spare=1、max_spare=3;;pm.max_requests=500与;pm.process_idle_timeout=10s默认被注释 | 同上,php-src 仓库www.conf.in | 2026-10-20 | 5.2 |
本机实测:httpd -V显示 Apache/2.4.67 (Unix)、Server MPM: prefork、threaded: no、forked: yes (variable process count) | 本机实测(macOS,2026-10-20) | 2026-10-20 | 6.1 |
本机实测:httpd -M过滤 MPM 后输出mpm_prefork_module (shared) | 本机实测(macOS,2026-10-20) | 2026-10-20 | 6.2 |
| 待验证:某台机器上 MaxRequestWorkers 的实际取值、FPM 池配置文件在发行版中的实际路径 | 本文未实测,标注为待验证 | 2026-10-20 | 6.3、7.1 |
附表 B:术语速查表
| 术语 | 一句话解释 | 易混点 |
|---|---|---|
| MPM | 多处理模块,负责绑端口、接请求、派子进程 | 它是调度者,不执行业务逻辑 |
| prefork | 非线程、预派生的模型,一个进程招呼一个请求 | 隔离性最好,但每个请求占一个完整进程 |
| worker | 多进程加多线程的混合模型,请求由线程招呼 | 进程数靠父进程调,线程数在进程内是固定编制 |
| event | worker 的变体,只为活跃处理的连接占线程 | 解决的是 keep-alive 白占人手,不是「更快」 |
| keep-alive | 一个连接用完不关,留着下次接着用 | 省的是建连接的开销,代价是连接会一直占着 |
MaxRequestWorkers | Apache 侧同时处理请求的上限 | 在 prefork 下数进程,在 worker 下数线程 |
ThreadsPerChild | 每个子进程固定开多少线程 | 是「编制」不是「上限」 |
mpm_winnt | Windows 平台的默认 MPM | 平台不同默认就不同,没有全局答案 |
| PHP-FPM | 独立于 Apache 的一组进程,由 master 管 worker | 不属于 Apache,Apache 只是把请求转过去 |
pm.max_children | FPM 侧同时被服务的请求数上限,默认 5 | static 下是「创建多少」,dynamic 下是「最多多少」 |
pm模式 | static/dynamic/ondemand三种池管理方式 | 同一个数字在三种模式下身份不同 |
写在最后:这篇用到的资料
写这篇文章时,我把 Apache 2.4 的 MPM 总览页和 prefork/worker/event 三个模块页对着读了一遍,又把 PHP-FPM 池配置里的那几个默认值逐个核了,最大的收获不是记住了哪个数,而是发现「同时能服务几个请求」这件事在两段路上各有一个答案,而这两个答案的单位还不一样——顺手也整理了几份配套的东西:
- 靶场环境对照表:DVWA、upload-labs 在 Windows / macOS / Linux 三平台的可行性与推荐路径
- Web 安全学习路线图:从基础打牢到安全管理,四个阶段各学什么
- 常用靶场清单:每个靶场练什么、适合哪个阶段
资料是我自己整理的,放在下面这个码上,扫码即可获取:
添加时备注「靶场」,优先通过。
拿到之后建议先看靶场环境对照表那一份,下次页面变慢时,先问一句「这个请求落在谁身上」,比先去改任何一个数字都管用。