最近的大半年中,编程语言从PHP换到了Golang后,就很少接触PHP,当然,更多的还是恋恋不舍。尽管如此,每当有人在群里聊起PHP的话题时,我总是想插几句,怀念怀念,同时也温故温故知识点,可不能把她给忘了。
昨天朋友tywei问我一个关于PHP奇怪问题,查到原因解决后,没有详细的解释。夜里睡觉时,老是回想这事,早上醒来,决定还是认真记录一下这些问题。也让自己回归正常状态,多写点博客,总结自己,记录自己。
问题描述
PHP+nginx的环境,任何PHP处理的结果,都是空白页面。OS是ubuntu 14.10 ,nginx 1.6.2 ,PHP5.5.12, 问任何PHP的页面,返回的HTTP状态200,但页面内容是空的,什么都没有,不管PHP页面里写的是什么,正常响应。初步怀疑这是拓展的问题,处理请求后,输出内容有冲突,输出为空之类。想查看拓展列表,看看加载了哪些拓展,但任何PHP代码都返回空,万不得已,在CLI模式下运行,确定ini是同一个。粗略的看了加载的模块,列表如下:
Configuration File (php.ini) Path => /etc/php5/cli Loaded Configuration File => /etc/php5/cli/php.ini Scan this dir for additional .ini files => /etc/php5/cli/conf.d Additional .ini files parsed => /etc/php5/cli/conf.d/05-opcache.ini, /etc/php5/cli/conf.d/10-mysqlnd.ini, /etc/php5/cli/conf.d/10-pdo.ini, /etc/php5/cli/conf.d/20-curl.ini, /etc/php5/cli/conf.d/20-gd.ini, /etc/php5/cli/conf.d/20-imagick.ini, /etc/php5/cli/conf.d/20-json.ini, /etc/php5/cli/conf.d/20-memcache.ini, /etc/php5/cli/conf.d/20-memcached.ini, /etc/php5/cli/conf.d/20-mysql.ini, /etc/php5/cli/conf.d/20-mysqli.ini, /etc/php5/cli/conf.d/20-pdo_mysql.ini, /etc/php5/cli/conf.d/20-readline.ini, /etc/php5/cli/conf.d/20-redis.ini, /etc/php5/cli/conf.d/20-xdebug.ini
大约如上的模块加载,怀疑xdebug跟opcache冲突,尝试关闭后,仍未解决。
strace看系统调用信息
10:02:03.432445 accept(0, {sa_family=AF_INET, sin_port=htons(49617), sin_addr=inet_addr("127.0.0.1")}, [16]) = 4 10:02:06.125142 times({tms_utime=0, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 1718358917 10:02:06.125177 poll([{fd=4, events=POLLIN}], 1, 5000) = 1 ([{fd=4, revents=POLLIN}]) 10:02:06.125332 read(4, "\1\1\0\1\0\10\0\0", 8) = 8 10:02:06.125363 read(4, "\0\1\0\0\0\0\0\0", 8) = 8 10:02:06.125381 read(4, "\1\4\0\1\3)\7\0", 8) = 8 10:02:06.125394 read(4, "\f\0QUERY_STRING\16\3REQUEST_METHODGET\f\0CONTENT_TYPE\16\0CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\v\1REQUEST_URI/\f\nDOCUMENT_URI/index.php\r\22DOCUMENT_ROOT/data/web/test.com\17\10SERVER_PROTOCOLHTTP/1.1\21\7GATEWAY_INTERFACECGI/1.1\17\vSERVER_SOFTWAREnginx/1.6.2\v\10REMOTE_ADDR10.0.2.2\v\5REMOTE_PORT50057\v\tSERVER_ADDR10.0.2.15\v\2SERVER_PORT80\v\10SERVER_NAMEtest.com\17\3REDIRECT_STATUS200\t\10HTTP_HOSTtest.com\17\nHTTP_CONNECTIONkeep-alive\22\tHTTP_CACHE_CONTROLmax-age=0\vJHTTP_ACCEPTtext/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\17yHTTP_USER_AGENTMozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36\24\23HTTP_ACCEPT_ENCODINGgzip, deflate, sdch\24\27HTTP_ACCEPT_LANGUAGEzh-CN,zh;q=0.8,en;q=0.6\v\6HTTP_RA_VER2.10.1\v&HTTP_RA_SIDB4A714D2-20150327-061728-7c932d-97f1f0\0\0\0\0\0\0\0", 816) = 816 10:02:06.125416 read(4, "\1\4\0\1\0\0\0\0", 8) = 8 10:02:06.125439 setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={60, 0}}, NULL) = 0 10:02:06.125468 rt_sigaction(SIGPROF, {0x6ec3d0, [PROF], SA_RESTORER|SA_RESTART, 0x7f3c26740da0}, {0x6ec3d0, [PROF], SA_RESTORER|SA_RESTART, 0x7f3c26740da0}, 8) = 0 10:02:06.125508 rt_sigprocmask(SIG_UNBLOCK, [PROF], NULL, 8) = 0 10:02:06.125570 times({tms_utime=0, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 1718358917 10:02:06.125601 setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={0, 0}}, NULL) = 0 10:02:06.125637 fcntl(3, F_SETLK, {type=F_UNLCK, whence=SEEK_SET, start=0, len=0}) = 0 10:02:06.125668 write(4, "\1\6\0\1\0@\0\0X-Powered-By: PHP/5.5.12-2ubuntu4.4\r\nContent-type: text/html\r\n\r\n\1\3\0\1\0\10\0\0\0\0\0\0\0\0\0\0", 88) = 88 10:02:06.125697 shutdown(4, SHUT_WR) = 0 10:02:06.125713 recvfrom(4, "\1\5\0\1\0\0\0\0", 8, 0, NULL, NULL) = 8 10:02:06.125728 recvfrom(4, "", 8, 0, NULL, NULL) = 0 10:02:06.125797 close(4) = 0
觉得好简短,好奇怪,访问的是SCRIPT_NAME index.php,怎么都没 lstat \open 这个PHP文件呢?直接返回了?起码要判断SCRIPT_FILENAME是否存在吧,要读取SCRIPT_FILENAME,解析里面的代码吧? 等下…..SCRIPT_FILENAME全部地址是啥?发来的CGI协议包中怎么没有SCRIPT_FILENAME?
仔细看下CGI包的内容
QUERY_STRING REQUEST_METHODGET CONTENT_TYPE CONTENT_LENGTH SCRIPT_NAME /index.php REQUEST_URI / DOCUMENT_URI /index.php DOCUMENT_ROOT /data/web/test.com SERVER_PROTOCOL HTTP/1.1 GATEWAY_INTERFACE CGI/1.1 SERVER_SOFTWARE nginx/1.6.2 REMOTE_ADDR 10.0.2.2 REMOTE_PORT 50057 SERVER_ADDR 10.0.2.15 SERVER_PORT 80 SERVER_NAME test.com REDIRECT_STATUS 200 HTTP_HOST test.com HTTP_CONNECTION keep-alive
缺少:
SCRIPT_FILENAME //PATH_TRANSLATED //这个暂时无视
那么问题来了
- PHP-FPM接收CGI请求时,如果没有SCRIPT_FILENAME怎么处理的?
- 发来的CGI协议包中,为啥没有SCRIPT_FILENAME? (SCRIPT_FILENAME是什么,干啥用的,这个就不要问了吧。)
问题1:PHP-FPM接收CGI请求时,如果没有SCRIPT_FILENAME怎么处理的?
fpm源码里(PHP5.5.x 为例)
//fpm_main.c /* {{{ main */ int main(int argc, char *argv[]) { //... //fpm_main.c 1820行 while (fcgi_accept_request(&request) >= 0) { request_body_fd = -1; SG(server_context) = (void *) &request; init_request_info(); //这里对应986行左右的的init_request_info 函数中代码 char *primary_script = NULL; fpm_request_info(); /* request startup only after we've done all we can to * get path_translated */ if (php_request_startup() == FAILURE) { fcgi_finish_request(&request, 1); SG(server_context) = NULL; php_module_shutdown(); return FPM_EXIT_SOFTWARE; } /* check if request_method has been sent. * if not, it's certainly not an HTTP over fcgi request */ if (!SG(request_info).request_method) {//这里判断request.method是否存在,在init_request_info方法里,最上面设置了默认的NULL goto fastcgi_request_done; } //... fastcgi_request_done: //结束当前request请求,给及响应 } }
同样是 fpm_main.c中init_request_info函数的代码如下:
//fpm_main.c 986行 static void init_request_info(void) { char *env_script_filename = sapi_cgibin_getenv("SCRIPT_FILENAME", sizeof("SCRIPT_FILENAME") - 1); char *env_path_translated = sapi_cgibin_getenv("PATH_TRANSLATED", sizeof("PATH_TRANSLATED") - 1); char *script_path_translated = env_script_filename; char *ini; int apache_was_here = 0; /* some broken servers do not have script_filename or argv0 * an example, IIS configured in some ways. then they do more * broken stuff and set path_translated to the cgi script location */ if (!script_path_translated && env_path_translated) { script_path_translated = env_path_translated; } /* initialize the defaults */ SG(request_info).path_translated = NULL; SG(request_info).request_method = NULL; SG(request_info).proto_num = 1000; SG(request_info).query_string = NULL; SG(request_info).request_uri = NULL; SG(request_info).content_type = NULL; SG(request_info).content_length = 0; SG(sapi_headers).http_response_code = 200; // 这里默认给了200的响应 /* script_path_translated being set is a good indication that * we are running in a cgi environment, since it is always * null otherwise. otherwise, the filename * of the script will be retreived later via argc/argv */ if (script_path_translated) { if (CGIG(fix_pathinfo)) { //对pathinfo做处理,剥离出SCRIPT_FILENAME,并重置SCRIPT_FILENAME } else { } if (is_valid_path(script_path_translated)) { //这里如果script_path_translated是合法路径,就给转化一下,赋值给SG(request_info).path_translated SG(request_info).path_translated = estrdup(script_path_translated); } SG(request_info).request_method = sapi_cgibin_getenv("REQUEST_METHOD", sizeof("REQUEST_METHOD") - 1);//这里从CGI包李获取method,赋值给request_method // ... } }
从代码里看出,script_path_translated变量就是cgi协议包中SCRIPT_FILENAME的结果,其中1115行左右,判断如果script_path_translated为空,并且env_path_translated不为空,则用env_path_translated赋值到script_path_translated上。
之后,对request_info的几个属性给予了默认值,包括request_method为null,以及http_response_code默认200的http响应。
在后面的代码中if (script_path_translated) ,因为CGI包中没有SCRIPT_FILENAME,也没有PATH_TRANSLATED,即script_path_translated为空,故没有对request_method进行赋值,其默认值为NULL。
回到main函数中1839行附近if (!SG(request_info).request_method) ,则直接goto到了fastcgi_request_done,直接结束当前request请求,由于之前有设置过默认的http 响应状态为200 ,也就导致了每次返回http状态200成功响应的空白页面的问题。 同时也解释了strace系统调用中,没出现lstat、open 操作SCRIPT_FILENAME的记录了。
问题2,发来的CGI协议包中,为啥没有SCRIPT_FILENAME?
PHP-FPM接收到的CGI协议包都是来自前面nginx的,cgi协议包中没有这个,肯定是nginx没发来,查看nginx配置看到fastcgi_params中没有这项。加上后就可以了。
解决办法:
在nginx配置的 fastcgt_params中加上SCRIPT_FILENAME的配置(在ubuntu的apt-get形式安装nginx配置中,默认是有这条的),比如
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
题外话:
在FPM的fpm_main.c文件的main函数中,1848行对SG(request_info).path_translated的判断,晚于1849行SG(request_info).request_method的判断。而在init_request_info函数中,对SG(request_info).path_translated的赋值却是比SG(request_info).request_method早的。 而且,明明找不到需要执行的脚本,却还返回200的http响应,很奇怪,也不方便排查。我觉得把对SG(request_info).request_method的判断放到SG(request_info).path_translated后面更合适一些。或者若找不到SCRIPT_FILENAME的话,http响应状态改为404,同时,写入LOG日志,便于排查。
后记:
从fpm代码里可以看到,其实作者是有考虑到没有SCRIPT_FILENAME的问题的,只是判断顺序搞错了。所以,我觉得这应该是个bug,就提了一下:BUG #69625:php-fpm return http 200 response on nginx without SCRIPT_FILENAME,不知道官方是否认为这是个BUG。
新的问题:
- 为什么如果SCRIPT_FILENAME不存在时,用PATH_TRANSLATED来代替它?PATH_TRANSLATED是每个CGI前端都要发送的吗?
这个问题,后来认真看了下,感觉还挺复杂,跟CGI客户端有关,PHPFPM针对IIS\APACHE\NGINX的处理都不一样。以后再写吧。
参考资料:
RFC3875 – The Common Gateway Interface (CGI) Version 1.1
如果你在阅读PHP源码,或者阅读PHP SAPI、PHP拓展源码,可以关注一下PHP源码执行流程图 关于FPM的执行流程,可以看下下面这幅图:
2018年07月09日更新:
Fix bugs #75120 and #69625 #3226里修复了我在2015年3月提的 bug report Fix bug #69625 php-fpm return http 200 response without SCRIPT_FILENAME #1270
CFC4N的博客 由 CFC4N 创作,采用 署名—非商业性使用—相同方式共享 4.0 进行许可。基于https://www.cnxct.com上的作品创作。转载请注明转自:nginx上,http状态200响应,PHP空白返回的问题
虽然看不懂,也跪读完了
装逼高手啊……
Son of a gun, this is so heuplfl!
楼主精通各种装逼,能把这个问题解释成这样……这装逼功夫也算到家了
=_=!
为什么我的nginx配置有fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
还会出现这种情况
多谢楼主, 按照你的方法解决了