php 的一个bug

 

Bug描述
在使用file_get_contents($ur);请求一个$url 时,如果server在http请求头还没有完全输出时就意外关闭了连接,则file_get_contents($ur)将因无法读取完整的数据而陷入死循环,这时,进程占用cpu约 100%

影响版本
本次故障出现在PHP-5.3.3 ,相信此前的版本中应该也存在;以后的版本就不得而知了

Bug重现方法

server.php

 

client.php

 

在A终端模拟server端:

 

在B终端执行client.php

在C终端观察client.php 的执行情况

使用gstack查看堆栈状况:

使用gdb分析,参看脚本: php-5.3.3/ext/standard/http_fopen_wrapper.c

可以通过: https://svn.php.net/repository/php/php-src/tags/ 直接查看,在php5.4中还没有修复该bug

 

 


解决办法

提交bug 到 php.net, bug地址: https://bugs.php.net/bug.php?id=63338

关于从终端执行程序的问题

通常在一个终端执行一个耗时很长的程序时,我们使用nohup来执行,避免终端关闭时程序退出; 程序退出多半是程序需要输出,而输出终端已经不存在了,导致IO错误。

下面观察了一下test.php 的程序在终端执行的情况:
<?php
while(1) {
    echo "hello world\n";
    sleep(5);
}

————————————–
在一个终端启动程序:
php test.php
# ltrace  -p 19197  
write(1, "hello world\n", 12)                                                     = 12
fflush(0x8515c0)                                                                  = 0
sleep(5)                                                                          = 0
write(1, "hello world\n", 12)                                                     = 12

在另一个终端使用gdb调试该进程,当前一个终端关闭时,程序因写失败而进入函数php_handle_aborted_connection ; 这里将连接状态设置为abort,然后检查是否忽略连接abort,因为我设置为不忽略,所以程序退出了。修改程序,添加 ignore_user_abort(true); 这样的话,终端退出后,进程也不会退出了。但是,我们就又发现另外一个现象,这时候使用ltrace跟踪时,write调用没有了,因为php使用函数:php_ub_body_write_no_header 输出时,检查了变量output_globals.disable_output, 而该变量在发现连接断开时被置为 1 , 所以再也看不到输出了。

奇怪的是,使用ltrace跟踪的使用,命名是使用write系统调用输出的,gdb调试时,给write函数设置断点,居然不能拦住????

关于反向地址解析

PTR (Pointer Recore),指针记录,是电子邮件系统中的一种数据类型,被互联网标准文件RFC1035所定义。与其相对应的是A记录、地址记录。二者组成邮件交换记录。
  A记录解析名字到地址,而PTR记录解析地址到名字。地址是指一个客户端的IP地址,名字是指一个客户的完全合格域名。
   PTR记录被用于电子邮件发送过程中的反向地址解析。当正向域名解析完成后,还应当向您的线路接入商(ISP)申请做反向地址解析,以减少被国外机构退信的可能性。
   因为反向地址解析是需要“钱”的,所以除了用于发邮件的IP做反向地址解析,提供web服务的IP一般是不做反向地址解析的,所以很难根据IP地址反向解析到域名

实例:
提供邮件服务的IP的反向地址解析:

提供web服务的IP的反向地址解析:

PHP中使用文件锁

 
  1. <?php
  2. $lock_file = "/tmp/logging.lock";
  3. if($should_do_sth) {
  4.     $fp = @fopen($lock_file"w+");
  5.     if ($fp && flock($fp, LOCK_EX)) {
  6.         do_sth();
  7.         flock($fp, LOCK_UN);
  8.         fclose($fp);
  9.         @unlink($lock_file);  // 1.  这里删除自己创建的文件,避免因属主权限问题导致其他进程无法打开该所文件 2. 这里也可能多进程间重复删除,所以使用 @
  10.     }
  11. }

form 表单的text/plain 编码方式

form表单的enctype属性可以设置表单提交时表单内容的编码方式,可用的编码方式有:

  enctype
When the value of the method attribute is post, this attribute is the MIME type of content that is used to submit the form to the server. Possible values are:

  • application/x-www-form-urlencoded: The default value if the attribute is not specified.
  • multipart/form-data: Use this value if you are using an <input>  element with the type attribute set to "file".
  • text/plain (HTML5)

其中, text/plain 很少使用,其编码方式如下:

关于PHP中popen使用的一点注意

引子:
PHP的popen可以并发调起多个进程吗?

我们做如下测试:
测试1

分析1
我们发现,这里只输出了"a", 没有输出"b",说明sleep 100;执行了, sleep 200; 没有立即执行

测试2

分析2
这里a、b都输出了,说明两条命令都被调起执行了;同时,我们也发现,主进程并没有立即退出,而是在等待所有子进程执行完毕,如果使用 "&" 将启动的命令推倒后台,主进程也就不等待了。

测试3

分析3
这里a没有输出,说明如果不将popen赋值给一个变量,则进程阻塞,如果使用 "&" 将启动的命令推倒后台,则亦不阻塞。

关于ttserver的异常退出

使用Tokyotrant有相当一段时间了,确实有几次莫名的退出,今天再次出现,查了一下系统日志,报错如下:
$grep ttserver /var/log/messages
Jun 18 22:00:42 localhost kernel: [2463228.773583] ttserver[32282] general protection rip:2b6557922dcb rsp:4a0b2c08 error:0

 

又出现一次;

Jul 18 07:00:21 localhost kernel: [11461817.641400] ttserver[612]: segfault at 000000001ffc2000 rip 0000003f5407c08f rsp 000000004732ec08 error 4

关于PHP中配置文件的定义

定义方法1
—– conf.php ——

  1. <?php
  2. $conf = "hello world";
  3. ?>

引用配置文件:
—– test.php ——

  1. <?php
  2. test();
  3. function test() {
  4.     include("conf.php");
  5.     echo $conf;
  6. }
  7. ?>

———————
分析
如果代码就这么简单,我们发现程序工作的很好,但是:
1. 如果$conf文件很大,每次都include势必浪费很多时间
2. 如果把include修改为include_once,则情况就会变得怪异了; 如果外部不小心include的了一次conf.php ,则test中不再执行conf.php,则test() 不能正常工作; 如果保证外面没有include("conf.php"); 则test() 第一次可以工作正常,第二次就无法正常工作,因为第二次执行test()函数并没有执行conf.php,也就没有$conf变量
3. conf.php 中 $conf 变量的作用域是不固定的,依赖于conf.php 的执行环境,所以简单地把 $conf看做是global的,更是容易犯错误的

结论: 这样写配置文件很容易犯错误,所以不要这样定义配置文件

定义方法2
——- conf.php —–

  1. <?php
  2. $GLOBALS["conf"] = "hello phpor";
  3. ?>

引用配置文件:
——- test.php ——-

  1. <?php
  2. test();
  3. function test() {
  4.     include_once("conf.php");
  5.     echo $GLOBALS["conf"];
  6. }
  7. ?>

分析
1. 配置文件包含进来之后,将一直占用内存空间,所以配置文件不宜太大,不过一般都不太太大
2. 为了提高效率,这里最好使用include_once, 而不是include
3. 这种写法不管怎么调用都不会有问题
4. 需要注意一点的是: 如果配置文件确实很大,解析该配置文件需要10毫秒, 而且不是每次请求都会用到大部分配置,或许整个请求之间只用到了其中1/10的配置,但是还是不得不耗费10ms来解析整个配置文件; 如果确实这这种情况,不妨考虑第三种配置方法

定义方法3
——- conf.php ——

 
  1. <?php
  2. class conf {
  3.     private static $cache = array();
  4.     public static function get($alias) {
  5.         if (isset(self::$cache[$alias])) return self::$cache[$alias];
  6.         $method = "get_$alias";
  7.         if (!method_exists(self, $method)) return array();
  8.         return self::$method();
  9.     }
  10.     private static function get_conf1() {
  11.         return self::$cache["conf1"] = "hello phpor";
  12.     }
  13.     private static function get_conf2() {
  14.         return self::$cache["conf2"] = "maybe this is a big array";
  15.     }
  16. }
  17. ?>

引用配置文件:
——- test.php ——-

  1. <?php
  2. include_once("conf.php");  // 对于应用了PHP加速器的程序来讲,因为conf.php中没有包含就执行的语句,所以使用include或者include_once也没什么区别
  3. test();
  4. function test() {
  5.    var_dump(conf::get("conf1"));
  6. }
  7. ?>

分析
1. 这种办法似乎规避了前两种写法的所有缺点
2. 我们还可以在配置文件中添加一些逻辑,避免代码的重复

关于PHP中的变量作用于问题

引子:
有文件1.php:

  1. <?php
  2. $conf = array("hello world");
  3. ?>

文件2.php

  1. <?php
  2. include_once("1.php");
  3. function do_sth() {
  4.      global $conf;
  5.      print_r($conf);
  6. }
  7. ?>

文件3.php

 
  1. <?php
  2. include_once("2.php");
  3. do_sth(); // 这里能不能打印出数组内容取决于3.php的上下文环境
  4. ?>

直接执行 3.php 可以打印出数组内容。

下面使用文件4.php来include文件3.php
文件4.php

  1. <?php
  2. wrap(); // 这里不能打印出$conf数组的内容
  3. function wrap() {
  4.     include_once("3.php");
  5. }
  6. ?>

分析
1) 因为1.php 中的$conf数组没有使用global声明,所以1.php中 $conf数组的作用于取决于 1.php的上下文环境;
2) 3.php 直接执行的话,工作的很好,但是一旦被一个函数给包裹住,则$conf 数组不再是一个全局变量,这样do_str() 也就不能按照预期执行了
3) 如果在1.php中也使用global声明一下 $conf , 然后再赋值,就比较靠谱了

结论
1. 不要指望在任何函数外部声明的变量就是全局的
2. 虽然使用global 声明后同样可以确保是一个全局变量,但是这里还是建议使用 $GLOBALS 数组来访问全局变量