标签归档:PHP源码

如何在用户中断时停止程序的运行

当我们以WEB的方式运行PHP脚本时,默认情况下,即使你关闭当前页面,程序也会继续执行,直接程序结束或超时。如果我们想在用户关闭页面或点击了停止页面运行时就中断程序,我们需要做些什么呢?上周和小毅同学讨论了这个问题,从而也引出了今天我们这篇文章。

我们知道HTTP协议是基于TCP/IP协议,对于一个PHP页面的请求就是一个HTTP请求(假设我们是Apache服务器),从而会创建TCP连接,当用户中断请求时,会给服务器一个abort状态。这个abort状态就是今天我们要讲的关键点。

在PHP中有一个函数与abort状态有关:ignore_user_abort函数
ignore_user_abort() 函数设置与客户机断开时是否会终止脚本的执行。它返回 user-abort 之前设置的布尔值。它的参数可选。如果设置为 true,则忽略与用户的断开,如果设置为 false,会导致脚本停止运行。

PHP 不会检测到用户是否已断开连接,直到尝试向客户机发送信息为止。因此如果我们只是使用echo语句,可能无法如实的看到abort的效果,因为PHP在输出时会有一个缓存,如果要刷新缓存,则可以使用flush() 函数。

如下代码t.php:

ignore_user_abort(TRUE);
set_time_limit(50);
 
while (1) {
    echo $i++, "\r\n";    
    flush();
 
    $fp = fopen("data.txt", 'a');
    fwrite($fp, $i . " \r\n");
    fclose($fp);
 
    sleep(1);
}

在浏览器中执行这段代码,过了大概两秒后,关闭请求的页面,50秒后,你会发现在data.txt文件中写入了至少50个数。这表示我们的中断操作是无效的。
如果我们改一下,把第一句改为:ignore_user_abort(FALSE);,重复上面的操作,你会发现,只写入了极少的数字,这表示我们的中断操作有效了。
现在通过ignore_user_abort函数,我们实现了用户中断就马上停止程序的操作。这里有一个问题,即我们需要不停的flush,通过flush函数来更新连接状态,当状态为abort时,程序根据ignore_user_abort的设置来判断是否中断程序。除此之外,我们也可以使用直接获取连接状态来check连接状态,并对特定的状态作出处理,如下代码:

ignore_user_abort(FALSE);
set_time_limit(50);
 
while (1) {
 
    echo $i++, "\r\n";
    flush();
 
     if (connection_status() != CONNECTION_NORMAL) {
        break;
    }
 
    $fp = fopen("data.txt", 'a');
    fwrite($fp, $i . ":" . connection_status() . " \r\n");
    fclose($fp);
 
    sleep(1);
}

这里的connection_status函数的作用是获取连接的状态,当连接的状态非normal时,我们就中断循环,从而也达到了中断程序的操作。这个示例与前面的示例不同之处在于中断操作是由我们自己控制,而不是通过flush操作直接exit。如果在用户中断后还有一些其它的操作,这种方式会更合适一些。当然,这里的flush操作依旧不可少,我们还是需要通过这个函数做check操作。

ignore_user_abort函数和connection_status函数都实现了我们的目的,这两个函数的实现有没有关联?我们在ext/standard/basic_functions.c文件中找到这两个函数的实现如下:

/* {{{ proto int connection_aborted(void)
 
Returns true if client disconnected */
PHP_FUNCTION(connection_aborted)
{
    RETURN_LONG(PG(connection_status) & PHP_CONNECTION_ABORTED);
}
/* }}} */
 
/* {{{ proto int connection_status(void)
Returns the connection status bitfield */
PHP_FUNCTION(connection_status)
{
    RETURN_LONG(PG(connection_status));
}
/* }}} */
 
/* {{{ proto int ignore_user_abort([string value])
Set whether we want to ignore a user abort event or not */
PHP_FUNCTION(ignore_user_abort)
{
    char *arg = NULL;
    int arg_len = 0;
    int old_setting;
 
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|s", &arg, &arg_len) == FAILURE) {
        return;
    }
 
    old_setting = PG(ignore_user_abort);
 
    if (arg) {
        zend_alter_ini_entry_ex("ignore_user_abort", sizeof("ignore_user_abort"), arg, arg_len, PHP_INI_USER,     PHP_INI_STAGE_RUNTIME, 0 TSRMLS_CC);
    }
 
    RETURN_LONG(old_setting);
}
/* }}} */

connection_status函数直接返回PG(connection_status)的值,

ignore_user_abort函数重新设置PG(ignore_user_abort)的值,

不管是因为缓存满自动调用或通过flush函数调用的flush操作,其最终都会根据连接状态判断是否执行php_handle_aborted_connection函数,如果是abort状态,则执行。

其代码如下:

/* {{{ php_handle_aborted_connection
*/
PHPAPI void php_handle_aborted_connection(void)
{
    TSRMLS_FETCH();
 
    PG(connection_status) = PHP_CONNECTION_ABORTED;
    php_output_set_status(0 TSRMLS_CC);
 
    if (!PG(ignore_user_abort)) {
        zend_bailout();
    }
}
/* }}} */

在PG(ignore_user_abort)为假时,即不忽略用户的中断行为时,如果调用了此函数,则使用zend_bailout函数跳出程序直接exit。

在默认情况下ignore_user_abort为0,即不忽略用户的中断行为。

如果你是ubuntu的默认apache环境下,可能上面的代码会无效。这是由于此环境下的apache开启了zip,在没有达到预定的大小时,服务器不会与客户端通信,从而也就无法获取客户端的状态,即使使用了flush函数也是一样。

PHP中计算字符串相似度的函数

上次reeze提到similar_text函数,这个真心没用过。
在手册上查找其说明如下:
similar_text — 计算两个字符串的相似度
int similar_text ( string $first , string $second [, float &$percent ] )
$first 必需。规定要比较的第一个字符串。
$second 必需。规定要比较的第二个字符串。
$percent 可选。规定供存储百分比相似度的变量名。

两个字符串的相似程度计算依据 Oliver [1993] 的描述进行。注意该实现没有使用 Oliver 虚拟码中的堆栈,但是却进行了递归调用,这个做法可能会导致整个过程变慢或变快。也请注意,该算法的复杂度是 O(N**3),N 是最长字符串的长度。

比如我们想找字符串abcdefg和字符串aeg的相似度:

$first = "abcdefg";
$second = "aeg";
 
echo similar_text($first, $second);

结果输出3.如果想以百分比显示,则可使用它的第三个参数,如下:

$first = "abcdefg";
$second = "aeg";
 
similar_text($first, $second, $percent);
echo $percent;

这里的相似度的算法是什么呢?本来是想看看Oliver[1993]对于这个算法的具体描述,各种google后,只找到这是从Ian Oliver1993年出版的书《Programming classics: implementing the world’s best algorithms》中记载,没有找到这本书的电子版。

直接代码,在string.c文件中我们找到了similar_text的实现PHP_FUNCTION(similar_text),其最终调用php_similar_cha获取两个字符串的相似度,如下代码:

static int php_similar_char(const char *txt1, int len1, const char *txt2, int len2)
{
    int sum;
    int pos1, pos2, max;
 
    php_similar_str(txt1, len1, txt2, len2, &pos1, &pos2, &max);
    if ((sum = max)) {
        if (pos1 && pos2) {
            sum += php_similar_char(txt1, pos1, txt2, pos2);
        }
        if ((pos1 + max < len1) && (pos2 + max < len2)) { 
             sum += php_similar_char(txt1 + pos1 + max, len1 - pos1 - max, 
                                               txt2 + pos2 + max, len2 - pos2 - max);
        }
    }
 
    return sum;
}

首先我们看php_similar_str函数的作用,从函数名和参数名我们可以大致猜测它的作用是求两个字符串的相似子串,具体代码如下:

static void php_similar_str(const char *txt1, int len1, const char *txt2, int len2, int *pos1, int *pos2, int *max)
{
    char *p, *q; 
    char *end1 = (char *) txt1 + len1;
    char *end2 = (char *) txt2 + len2;
    int l;
 
    *max = 0;
    for (p = (char *) txt1; p < end1; p++) {
        for (q = (char *) txt2; q < end2; q++) {
            for (l = 0; (p + l < end1) && (q + l < end2) && (p[l] == q[l]); l++); //我是分号
            if (l > *max) {
                *max = l;
                *pos1 = p - txt1;
                *pos2 = q - txt2;
            }
        }
    }
}

真心很直白的三重循环,求两个字符串的最大相似子串的长度,以及这两个子串相等的开始位置。

在了解了php_similar_str的作用后,回到php_similar_char函数。这是一个很直白的二分算法。以当前两个字符串的最大相似子串的位置为分隔,向两边二分查找相似子串,最终得到所有的相似子串长度的总和,这也就是我们这个函数的相似度算法:从最长子串开始,依次统计所有的子串长度。

那么这里的百分比是如何计算的呢?

在PHP_FUNCTION(similar_text)的函数体中,如下代码:

sim = php_similar_char(t1, t1_len, t2, t2_len);
 
if (ac > 2) {
    Z_DVAL_PP(percent) = sim * 200.0 / (t1_len + t2_len);
}

sim是相似度的值,百分比是直接 sim * 200 / 两个字符串的长度。

关于那本书:

名称 Programming classics: implementing the world’s best algorithms
作者 Ian Oliver
出版商 Prentice Hall, 1993
出处: 密歇根大学
数字化处理时间 2007年11月15日
ISBN 0131004131, 9780131004139
页数 386 页
这里也许可以下载到

http://filecom.net/8EMrrcoyc8/

http://ebooks-files.org/download/programming-classics-implementing-the-worlds-best-algorithms.html

wordpress升级到3.3引起的血案

某位哥哥因为看着wordpress3.3的升级提示激动了,一不小心就将它升级了,升级后一切正常,只是之前可用的附件上传功能现在不能用了,显示报错如下:

Warning: touch() [function.touch]: SAFE MODE Restriction in effect.
 The script whose uid is 10041 is not allowed to access /tmp owned by uid 0 in ......

各种纠结……

依据提示我们知道是由于PHP开启了安全模式的原因,最简单的办法:修改php.ini文件,将safe_mode设置为off,将安全模式关闭。

只是这是生产环境,关闭安全模式不太靠谱。只能换方案。
依据刚才的错误提示,我们知道是安全模式在检查执行程序的文件的UID与需要写入的文件夹的UID不一样,这是因为在PHP的安全模式中,对于部分函数是需要检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者),很不幸,touch就是这样的函数之一。当然,有这个规则就应该有跳过这个规则的办法,在php.ini文件中,我们可以设置safe_mode_include_dir参数,多个路径以冒号隔开(Windows是分号),在此参数内设置的目录及其子目录都将会越过 UID/GID 检查。

除此之外,安全模式相关的还有一个open_basedir设置项,PHP 所能打开的文件限制在open_basedir指定的目录树,包括文件本身。本指令不受安全模式打开或者关闭的影响。

safe_mode_include_dir和open_basedir指定的限制实际上是一个前缀,而非一个目录名。这也就是说“safe_mode_include_dir = /tmp”将允许访问“/tmp和“/template”(如果它们存在的话),。如果希望将访问控制在一个指定的目录,那么请在结尾加上一个斜线,例如:“safe_mode_include_dir = /tmp”。

这两个与本次安全问题相关的内容都添加了/tmp和当前操作的目录,发现依旧报错。

追根溯源,从源码中开始吧,如果对于PHP源码不太熟悉,那么我们可以从本次事件的关键点safe_mode_include_dir开始。

在PHP源码中搜索safe_mode_include_dir,除了与此配置项相关的初始化操作,我们可以找到验证此项的函数php_check_safe_mode_include_dir。现在,我们可以用GDB debug的方法查看,给php_check_safe_mode_include_dir函数增加断点,运行一个你认为一定会经过此参数检查的PHP函数调用,如fopen等。运行,发现会在断点处停下,如果我们运行一个只包含了touch函数的代码,会发现程序会执行完,即touch函数根本就没有调用php_check_safe_mode_include_dir函数,即在安全模式下,根本无法跳过UID的检测。

或者我们直接查看touch函数的PHP源码实现,其代码在/ext/standard/filestat.c文件649行。在其代码中我们可以清楚的看到其实现过程在处理了相关参数后,就直接判断完全模式和检测UID了,如下代码:

PHP_FUNCTION(touch)
{
 
...
 
/* Safe-mode */
if (PG(safe_mode) && (!php_checkuid(filename, NULL, CHECKUID_CHECK_FILE_AND_DIR))) {
RETURN_FALSE;
}
 
/* Check the basedir */
if (php_check_open_basedir(filename TSRMLS_CC)) {
RETURN_FALSE;
}
 
...
 
}

找到了问题的根源,也许是PHP的BUG,也许是其它考量,待确认。

那么这个问题怎么解决呢?

比较wordpress3.3和wordpress3.2的代码,我们发现wordpress在处理附件上传时最终都会调用wp_handle_upload函数(/wp-admin/include/file.c)。在此函数中wordpress3.3版本多了一句:

$tmp_file = wp_tempnam($filename);

在wp_tempnam函数中,最终会通过touch生成临时文件,从前面我们可以知道我们暂时无法解决在此环境下的touch函数的问题,那么我们只能适应这个函数,也就是说我们需要将新创建的临时文件的目录UID和执行的PHP文件的UID设置为一样,那么这个临时文件的目录是否可以改变?我们查看wp_tempnam函数,发现其最先取的临时目录的地址为常量WP_TEMP_DIR的值,此值默认是没有设置的,如果这个值没有才会取其它值。此时,解决方案就浮出水面了:在wp-config.php文件中设置常量WP_TEMP_DIR的值为网站目录下某个UID和PHP脚本一样UID的目录即可。

附gdb bt显示的内容

#0 php_check_safe_mode_include_dir (path=0x86f4fcc "/tmp/aaa43243.tmp")
at /home/martin/project/c/phpsrc/main/fopen_wrappers.c:319
#1 0x0829d97b in php_plain_files_stream_opener (wrapper=0x85eb624, 
path=0x86f4fcc "/tmp/aaa43243.tmp", mode=0x86f6b38 "a", options=4, 
opened_path=0x0, context=0x86f5560)
at /home/martin/project/c/phpsrc/main/streams/plain_wrapper.c:991
#2 0x0829842d in _php_stream_open_wrapper_ex (
path=0x86f4fcc "/tmp/aaa43243.tmp", mode=0x86f6b38 "a", options=12, 
opened_path=0x0, context=0x86f5560)
at /home/martin/project/c/phpsrc/main/streams/streams.c:1867
#3 0x08229b73 in php_if_fopen (ht=2, return_value=0x86f5544, 
return_value_ptr=0x0, this_ptr=0x0, return_value_used=1)
at /home/martin/project/c/phpsrc/ext/standard/file.c:909
#4 0x0817b088 in phar_fopen (ht=2, return_value=0x86f5544, 
return_value_ptr=0x0, this_ptr=0x0, return_value_used=1)
at /home/martin/project/c/phpsrc/ext/phar/func_interceptors.c:418
#5 0x0831b9a1 in zend_do_fcall_common_helper_SPEC (execute_data=0x8727eb8)
at /home/martin/project/c/phpsrc/Zend/zend_vm_execute.h:313
#6 0x082f4bc6 in execute (op_array=0x86f545c)
at /home/martin/project/c/phpsrc/Zend/zend_vm_execute.h:104
#7 0x082d2256 in zend_execute_scripts (type=8, retval=0x0, file_count=3)
---Type <return> to continue, or q <return> to quit---
at /home/martin/project/c/phpsrc/Zend/zend.c:1194
#8 0x08281b70 in php_execute_script (primary_file=0xbffff2a0)
at /home/martin/project/c/phpsrc/main/main.c:2225
#9 0x08351b97 in main (argc=4, argv=0xbffff414)
at /home/martin/project/c/phpsrc/sapi/cli/php_cli.c:1190