运维知识
悠悠
2026年5月16日

CGI到底是个啥?从原理到演进,一次讲透

CGI,全称 Common Gateway Interface,通用网关接口。注意这几个字——"通用"说明不绑定任何编程语言,"网关"说明是个中间人角色,"接口"说明它是一套规范协议,不是什么具体的软件。

说白了,CGI就是一份"合同",规定了Web服务器跟外部程序怎么通信。没有这份合同之前,Web服务器只能干一件事:你请求什么文件,我就把那个文件原样扔回去。全是静态HTML,写死的内容,谁访问都一样。

有了CGI之后就不一样了。Web服务器可以"外包"了——收到一个请求自己处理不了,就按照CGI协议把请求数据打包,交给外部脚本去跑,脚本跑完把结果送回来,服务器再转交给浏览器。页面内容就是动态生成的,不同用户不同时间看到的都不一样。

打个比方,Web服务器就像一个前台接待,平时就负责发发宣传册(静态文件)。来了个客户问"我上个月的订单啥情况",前台自己答不上来,按照CGI这个"转接规范",把客户问题记下来转给后边的业务部门(外部脚本),业务部门查完系统把答案给前台,前台再告诉客户。CGI就是那个"转接规范"。

CGI在Web服务器里干了啥

核心就三个词:处理、传递、扩展

处理HTTP请求。 静态文件服务器干不了的事——查数据库、做计算、根据用户身份返回不同内容——这些活儿全得靠CGI调用的外部程序来完成。你在网页上点了个"查询"按钮,后端真正去数据库里捞数据的,就是那个被CGI调起来的脚本。

数据传递。 CGI最核心的角色就是"桥梁"。浏览器发过来的数据(表单参数、URL查询串、Cookie什么的),Web服务器自己不处理,通过CGI协议把这些数据"喂"给外部脚本;脚本跑完输出的内容(一般是HTML,也可以是JSON、图片二进制),再由Web服务器"端"回浏览器。整个过程中,Web服务器不关心脚本内部怎么实现的,脚本也不关心自己是在Apache下面跑还是Nginx下面跑,大家只认CGI这个协议。这就是"接口"的威力——解耦。

扩展服务器功能。 这点特别关键。你想给Web服务器加个新功能,不用去改它的源码重新编译,写个脚本就行。今天想用Perl处理请求,写个Perl脚本;明天想用Python,写个Python脚本;后天想用C写个编译型程序跑,也行。Web服务器一视同仁,只要你的程序符合CGI规范,它就调用。这种可插拔的架构设计,放到今天看也是相当优雅的。

一次CGI请求的完整流程

光说概念不够,咱们把一次完整的请求流程走一遍。假设用户在浏览器里提交了一个表单,表单的action指向一个CGI脚本,比如/cgi-bin/query.pl

浏览器发请求。 用户点了提交,浏览器往服务器发一个HTTP请求,可能是GET也可能是POST,URL里带着脚本路径和参数。

Web服务器识别CGI请求。 服务器收到请求一看,这个路径在/cgi-bin/下面,按照配置规则,得交给CGI处理。服务器开始准备环境——把HTTP请求头里的信息变成环境变量(QUERY_STRINGREQUEST_METHODCONTENT_TYPE这些),如果是POST请求还得把请求体通过标准输入(stdin)传给脚本。

这里插一句,CGI的数据传递机制就两种:环境变量和标准输入。URL参数走环境变量QUERY_STRING,POST数据走stdin。脚本那边通过读环境变量和stdin就能拿到所有请求数据,简单粗暴。

CGI脚本执行。 Web服务器fork出一个新进程,在这个进程里执行对应的CGI脚本。脚本拿到数据后开始干活——可能查数据库,可能做一堆计算,可能调别的API。处理完了,脚本把结果输出到标准输出(stdout),一般第一行是Content-Type: text/html这样的HTTP头,空一行之后是HTML正文。

Web服务器返回响应。 服务器拿到脚本的stdout输出,封装成HTTP响应发回浏览器。用户就看到了动态生成的页面。

整个流程走下来,最要命的一步就是fork新进程那一步。每次请求都要fork,请求完了进程就销毁。这个后面细说。

实际搭一个CGI环境看看

光说不练假把式。我之前在一台CentOS 7上搭过CGI环境,拿Apache做的,过程很简单。

装Apache:

yum install httpd -y
systemctl start httpd

Apache默认就带了mod_cgi模块,CGI脚本的存放目录一般是/var/www/cgi-bin/。去/etc/httpd/conf/httpd.conf里能看到这么一段:

ScriptAlias /cgi-bin/ "/var/www/cgi-bin/"
<Directory "/var/www/cgi-bin">
    AllowOverride None
    Options None
    Require all granted
</Directory>

这表示/cgi-bin/路径下的请求都会被当作CGI脚本处理。

写个最简单的Perl脚本,存成/var/www/cgi-bin/hello.pl

#!/usr/bin/perl
print "Content-Type: text/html\n\n";
print "<html><body><h1>Hello from CGI!</h1></body></html>";

给执行权限:

chmod +x /var/www/cgi-bin/hello.pl

浏览器访问http://你的IP/cgi-bin/hello.pl,就能看到页面了。

再写个能接收参数的Python版本,存成/var/www/cgi-bin/greet.py

#!/usr/bin/python3
import os

query = os.environ.get('QUERY_STRING', '')
name = query.split('=')[1] if '=' in query else 'Stranger'

print("Content-Type: text/html\n\n")
print(f"<html><body><h1>Hello, {name}!</h1></body></html>")

访问http://你的IP/cgi-bin/greet.py?name=张三,页面上就会显示"Hello, 张三!"。

你看,CGI的编程模型就是这么直白——读环境变量和stdin,往stdout写内容。没有任何框架,没有任何魔法。这跟现在动不动就Spring Boot、Django一大套框架的画风完全不一样,但正是这种朴素,让你能看清楚Web服务器跟后端程序之间到底是怎么交互的。

CGI的致命问题:性能

前面说了,CGI每次请求都要fork一个新进程。这个事有多严重,算一笔账就知道了。

假设一个CGI脚本用Python写的,Python解释器启动一次大概50-100ms(取决于机器性能和加载的模块)。并发100个请求,服务器就得同时fork出100个Python进程,每个进程启动都要50ms+,内存占用一个进程少说30MB,100个就是3GB。还只是一个简单的脚本,要是脚本里还import了一堆第三方库,启动时间更长,内存更大。

我之前维护过一个老系统,用的Perl写的CGI脚本跑在Apache上。平时访问量不大,几十QPS还能撑住。有一次搞活动,流量突然涨到几百QPS,服务器直接load飙到几十,SSH都登不上去。最后只能临时加机器硬扛。

问题就出在fork进程这个机制上。进程创建和销毁的系统开销太大了,而且每个进程都是独立的,不能共享数据库连接、不能共享内存里的缓存,什么资源都得从头初始化一遍。

这就好比一个餐厅,每来一个客人就临时雇一个厨师,厨师做完这道菜就解雇。来100个客人就雇100个厨师,厨房根本塞不下。正常做法难道不是雇几个常驻厨师,客人来了排队做菜吗?

而且还有个问题,进程fork出来的时间是不确定的。系统负载低的时候可能几毫秒就完事,负载一高,fork本身就可能卡住。再加上Python/Perl这些解释型语言的启动开销,一个请求光在"准备阶段"就花掉大几百毫秒,用户体验能好才怪。

FastCGI:CGI的进化形态

上面那个"常驻厨师"的思路,就是FastCGI。

FastCGI的核心改进就一句话:进程不销毁,常驻内存,处理完一个请求接着处理下一个。

FastCGI的工作模式是这样的:启动的时候预先fork出一组worker进程(数量可配),这些进程一直活着,通过Unix Socket或TCP连接跟Web服务器通信。请求来了,Web服务器把数据通过这个连接发给worker,worker处理完把结果发回来,连接不断,继续等下一个请求。

对比一下:

维度CGIFastCGI
进程生命周期请求来时创建,请求完销毁常驻,处理多个请求
进程间通信环境变量 + stdin/stdoutSocket连接
数据库连接每次重新建立可复用
并发处理每请求一进程固定数量进程池
内存占用随并发线性增长相对固定

拿Nginx + PHP-FPM来说,PHP-FPM就是一个FastCGI进程管理器。它管理着一组PHP-CGI worker进程,Nginx收到PHP请求后通过FastCGI协议转发给PHP-FPM,PHP-FPM分配一个空闲的worker处理,处理完归还到进程池。这套架构到现在还是PHP世界的主流方案。

Nginx配置FastCGI的写法大概长这样:

location ~ \.php$ {
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME  /var/www/html$fastcgi_script_name;
    include        fastcgi_params;
}

fastcgi_pass就是告诉Nginx,PHP请求往127.0.0.1:9000这个地址转,PHP-FPM就监听在这个端口上。

FastCGI还有个好处是worker进程可以部署在不同的机器上。Web服务器跟FastCGI进程之间是Socket通信,那就意味着可以是TCP Socket,天然支持远程部署。CGI就不行,脚本必须在Web服务器本机,因为它是靠fork本地进程来执行的。这点在需要做水平扩展的时候差别就很大了。

不过FastCGI也不是银弹。worker进程数是有限的,请求量突然暴增超过worker处理能力,请求就会排队,响应时间变长。PHP-FPM的max_children参数调不好,要么资源浪费要么502报错,这个踩过坑的都懂。

Servlet:Java的方案

Java走的是另一条路。Servlet不fork进程,而是在Web服务器(或者Servlet容器,比如Tomcat)的进程内部,用线程来处理请求。线程比进程轻量得多,创建和切换的开销小很多。

Servlet的运行机制是这样的:Tomcat启动的时候加载Servlet类,实例化之后常驻内存。请求来了,Tomcat从线程池里拿一个线程,调用Servlet的service()方法(内部根据HTTP方法分发给doGet()doPost()),处理完线程归还。Servlet实例始终在内存里,所有线程共享同一个实例(所以Servlet要注意线程安全)。

对比CGI,Servlet省掉了进程创建的开销,还能共享内存状态(数据库连接池、缓存),性能优势非常明显。这也是Java在企业级Web开发中长期占据主导地位的原因之一。

我之前做过一个项目从CGI迁移到Servlet的改造。原来那个系统是C写的CGI程序,每次请求fork进程查数据库,QPS上到200就扛不住了。改成Servlet之后,连接池复用数据库连接,线程处理并发,同样的硬件配置QPS直接翻了十倍不止。当然这个对比不完全公平,因为连接池的贡献很大,但进程模型跟线程模型的差距确实是数量级的。

WSGI:Python的方案

Python世界有自己的规范叫WSGI(Web Server Gateway Interface)。跟CGI的思想有点像,都是定义一个接口标准,但实现方式完全不同。

WSGI不是基于进程fork的,它是基于函数调用的。写一个Python函数,接收两个参数(environ字典和start_response回调),返回可迭代对象,这就是一个WSGI应用。Web服务器(或者中间件)直接在进程内调用这个函数,没有进程创建的额外开销。

def application(environ, start_response):
    status = '200 OK'
    headers = [('Content-Type', 'text/html')]
    start_response(status, headers)
    return [b'<h1>Hello from WSGI!</h1>']

你看这个environ字典,跟CGI的环境变量是不是很像?其实WSGI设计的时候就参考了CGI的规范,environ里面那些REQUEST_METHODQUERY_STRINGCONTENT_TYPE的key跟CGI环境变量基本一一对应。可以说WSGI是CGI在Python世界里"脱胎换骨"的产物——保留了接口标准化的思想,抛弃了fork进程的包袱。

Gunicorn、uWSGI这些应用服务器都是基于WSGI规范实现的,它们用多worker(多进程或多线程)的方式处理并发,每个worker常驻,跟FastCGI的思路类似。Gunicorn默认用的是pre-fork模型,启动时先fork好worker进程,请求来了分配给空闲worker,跟FastCGI简直是一个模子刻出来的。

Node.js:更激进的方案

Node.js又走了一条不同的路——单线程事件循环。不用多进程也不用多线程,一个进程一个线程,靠异步I/O和事件驱动来处理并发。

这个模型跟CGI的差距就更大了。CGI是"一个请求一个进程",Node.js是"所有请求一个线程"。但本质上解决的问题是一样的:怎么高效地处理大量并发请求。Node.js的方案在I/O密集型场景下表现特别好,因为I/O操作是异步的,不会阻塞事件循环。但CPU密集型任务就会卡住整个进程,所以得靠cluster模块或者拆分成多个进程来兜底。

CGI现在还在哪出现

说了这么多替代方案,CGI是不是彻底没用了?也不完全是。

在一些嵌入式设备、路由器的管理界面里,CGI还在用。那些环境资源有限,跑不动什么应用服务器,一个轻量的CGI脚本反而最合适。我之前帮人调试过一个OpenWrt路由器,它的Web管理界面就是用Lua写的CGI脚本跑在uhttpd上的,简单高效,占用的内存小到可以忽略不计。

还有一些遗留系统,九十年代和2000年代初用C或Perl写的CGI程序,现在还在跑着。你没法轻易迁移,因为业务逻辑都写死在那些脚本里了,没人敢动。这种系统的运维就很头疼,性能差但又不能停,只能靠前面加缓存、加负载均衡来缓解。

另外CGI的环境变量传递模型其实影响很深远。你去看Nginx的fastcgi_params文件,里面定义的那些参数,跟CGI标准里的环境变量几乎一模一样。FastCGI协议本质上就是CGI协议的"网络版"——把环境变量和stdin/stdout搬到了Socket上传输。所以理解了CGI,FastCGI的配置参数你一看就懂,不用死记硬背。

从CGI到现代Web架构的演进脉络

把这条线串起来看就很清晰了:

CGI每次请求fork进程 → 性能太差 → FastCGI用常驻进程池解决 → 但进程还是重 → Servlet用线程替代进程 → 更轻量 → WSGI用函数调用替代协议通信 → 更Pythonic → Node.js用单线程事件循环 → 另一种思路

每一步演进都是在解决上一步的性能瓶颈或者易用性问题。理解了这个演进逻辑,不管以后出现什么新技术,你都能快速理解它"为什么这么设计"。


CGI这个东西,说它过时吧确实过时了,说它没用吧还真不是。它是Web动态内容处理这条线上的起点,从CGI到FastCGI到WSGI到Servlet,一脉相承。把这些串起来理解,不管后面技术怎么迭代,底层逻辑你都能抓得住。

觉得这篇有帮助的话,转发给需要的朋友,让更多人少走弯路。有问题欢迎留言讨论,咱们下期见。


公众号:耕云躬行录

个人博客:躬行笔记

文章目录

博主介绍

热爱技术的云计算运维工程师,Python全栈工程师,分享开发经验与生活感悟。
欢迎关注我的微信公众号@运维躬行录,领取海量学习资料

微信二维码