10/01
10
PHP中有几个可以执行命令的函数:
system
shell_exec
exec
popen
..
除popen外的几个函数,执行命令后会等待命令的返回,再执行下一行程序,如下面的代码:
切入正题,PHP中的管道和进程间通讯及其问题,相关的函数包括popen,pclose等。
popen等函数是PHP中实现管道的几个函数,能很好的进行进程间通讯,和LINUX的常规管道一致,单向的,popen执行后不会等待返回而会接着执行下面的代码,这会利用用PHP来编写相应的脚本来处理相应的程序,但是和管道相关的事项也必须注意。
两段代码。
A:shell.php
sleep(1);
echo 'test';
sleep(1000);
echo 'hello';
?>
B:popen.php
popen('php shell.php &', 'r');
?>
执行相应的程序:
php popen.php
我们会发现程序执行到第二行后就没有执行下去了,我们通过
ps -ef|grep shell.php
早就中途退出了,程序并没有执行完成,而是中途退出了。
修改popen.php的代码如下:
B:popen.php
popen('php shell.php 2>&1 > log.log &', 'r');
?>
正常执行,再修改成这样:
B:popen.php
$out = popen('php shell.php &', 'r');
?>
也是正常执行,我们再将修改成这样:
B:popen.php
popen('php shell.php 2 > log.log &', 'r');
?>
我们会发现,所有的输出都进入了log.log,但是程序也是正常执行。
问题的关键,管道和PHP中的进程间通讯的实现:
popen函数的实现:/php-5.2.2/ext/standard/file.c 行885-959定义了popen的实现:
PHP_FUNCTION(popen)
{
zval **arg1, **arg2;
FILE *fp;
char *p, *tmp = NULL;
char *b, *buf = 0;
php_stream *stream;
if (ZEND_NUM_ARGS() != 2 || zend_get_parameters_ex(2, &arg1, &arg2) == FAILURE) {
WRONG_PARAM_COUNT;
}
convert_to_string_ex(arg1);
convert_to_string_ex(arg2);
p = estrndup(Z_STRVAL_PP(arg2), Z_STRLEN_PP(arg2));
#ifndef PHP_WIN32
{
char *z = memchr(p, 'b', Z_STRLEN_PP(arg2));
if (z) {
memmove(p + (z - p), z + 1, Z_STRLEN_PP(arg2) - (z - p));
}
}
#endif
if (PG(safe_mode)){
b = strchr(Z_STRVAL_PP(arg1), ' ');
if (!b) {
b = strrchr(Z_STRVAL_PP(arg1), '/');
} else {
char *c;
c = Z_STRVAL_PP(arg1);
while((*b != '/') && (b != c)) {
b--;
}
if (b == c) {
b = NULL;
}
}
if (b) {
spprintf(&buf, 0, "%s%s", PG(safe_mode_exec_dir), b);
} else {
spprintf(&buf, 0, "%s/%s", PG(safe_mode_exec_dir), Z_STRVAL_PP(arg1));
}
tmp = php_escape_shell_cmd(buf);
fp = VCWD_POPEN(tmp, p);
efree(tmp);
if (!fp) {
php_error_docref2(NULL TSRMLS_CC, buf, p, E_WARNING, "%s", strerror(errno));
efree(p);
efree(buf);
RETURN_FALSE;
}
efree(buf);
} else {
fp = VCWD_POPEN(Z_STRVAL_PP(arg1), p);
if (!fp) {
php_error_docref2(NULL TSRMLS_CC, Z_STRVAL_PP(arg1), p, E_WARNING, "%s", strerror(errno));
efree(p);
RETURN_FALSE;
}
}
stream = php_stream_fopen_from_pipe(fp, p);
if (stream == NULL) {
php_error_docref2(NULL TSRMLS_CC, Z_STRVAL_PP(arg1), p, E_WARNING, "%s", strerror(errno));
RETVAL_FALSE;
} else {
php_stream_to_zval(stream, return_value);
}
efree(p);
}
我们继续查找源,/php-5.2.2/TSRM/tsrm_virtual_cwd.c 行1027-1028:
....
memcpy(ptr, command, command_length+1);
retval = popen(command_line, type);
...
到这我们终于找到PHP的popen其实是封装了系统中的popen函数,而系统popen函数的执行流程又是怎么样的呢?
1、使用pipe系统调用创建一个管道
2、创建一个新的进程,将拷贝标准输入或者标准输出到文件描述符。
3、关闭pipe返回的文件描述符。
4、调用execve系统调用来执行命令。
……
再来看popen.php和shell.php
A:shell.php
sleep(1);
echo 'test';
sleep(1000);
echo 'hello';
?>
B:popen.php
popen('php shell.php &', 'r');
?>
再管道中会打开没有系统影射的文件描述符做为进程间的通讯管道,一个做为写端子,一个为读端子,PHP的popen会将系统zval的 return_value关联到C调用popen返回的文件描述符,OK,如果此文件描述符被释放了,会发生怎么样的情况,呵,正是前面所描述到的问题。 echo ‘test’;后就不再执行了,即该程序在写一个读端被关闭的管道,会产生SIGPIPE信号,虽然没有仔细看PHP处理SIGPIPE信号的代码,但结果已经告诉我们,退出了。
总结:
1、shell_exec、exec、system等函数在执行命令后会等待返回后再执行下一行代码,而popen不会。
2、popen封装了C调用popen来实现管道,管道中会打开两个文件描述符做为进程间通讯用,如果写端或者读端关闭都会导致系统产生错误,而PHP会把这个管道描述符做为popen的返回,如果没有将该文件描述符进行保存,将会被PHP释放,导导致出现管道一端关闭的情况而中途退出。
3、在程序的执行过程中必须保持该管道(即该文件描述符),不能被释放或者重写,这就意味着不能对管道文件描述符重复赋值,一个文件描述符只能做为单独的一个管道使用。
4、当然,我们也可以将进程的标准输出重定向来解决这个问题。
system
shell_exec
exec
popen
..
除popen外的几个函数,执行命令后会等待命令的返回,再执行下一行程序,如下面的代码:
切入正题,PHP中的管道和进程间通讯及其问题,相关的函数包括popen,pclose等。
popen等函数是PHP中实现管道的几个函数,能很好的进行进程间通讯,和LINUX的常规管道一致,单向的,popen执行后不会等待返回而会接着执行下面的代码,这会利用用PHP来编写相应的脚本来处理相应的程序,但是和管道相关的事项也必须注意。
两段代码。
A:shell.php
sleep(1);
echo 'test';
sleep(1000);
echo 'hello';
?>
B:popen.php
popen('php shell.php &', 'r');
?>
执行相应的程序:
php popen.php
我们会发现程序执行到第二行后就没有执行下去了,我们通过
ps -ef|grep shell.php
早就中途退出了,程序并没有执行完成,而是中途退出了。
修改popen.php的代码如下:
B:popen.php
popen('php shell.php 2>&1 > log.log &', 'r');
?>
正常执行,再修改成这样:
B:popen.php
$out = popen('php shell.php &', 'r');
?>
也是正常执行,我们再将修改成这样:
B:popen.php
popen('php shell.php 2 > log.log &', 'r');
?>
我们会发现,所有的输出都进入了log.log,但是程序也是正常执行。
问题的关键,管道和PHP中的进程间通讯的实现:
popen函数的实现:/php-5.2.2/ext/standard/file.c 行885-959定义了popen的实现:
PHP_FUNCTION(popen)
{
zval **arg1, **arg2;
FILE *fp;
char *p, *tmp = NULL;
char *b, *buf = 0;
php_stream *stream;
if (ZEND_NUM_ARGS() != 2 || zend_get_parameters_ex(2, &arg1, &arg2) == FAILURE) {
WRONG_PARAM_COUNT;
}
convert_to_string_ex(arg1);
convert_to_string_ex(arg2);
p = estrndup(Z_STRVAL_PP(arg2), Z_STRLEN_PP(arg2));
#ifndef PHP_WIN32
{
char *z = memchr(p, 'b', Z_STRLEN_PP(arg2));
if (z) {
memmove(p + (z - p), z + 1, Z_STRLEN_PP(arg2) - (z - p));
}
}
#endif
if (PG(safe_mode)){
b = strchr(Z_STRVAL_PP(arg1), ' ');
if (!b) {
b = strrchr(Z_STRVAL_PP(arg1), '/');
} else {
char *c;
c = Z_STRVAL_PP(arg1);
while((*b != '/') && (b != c)) {
b--;
}
if (b == c) {
b = NULL;
}
}
if (b) {
spprintf(&buf, 0, "%s%s", PG(safe_mode_exec_dir), b);
} else {
spprintf(&buf, 0, "%s/%s", PG(safe_mode_exec_dir), Z_STRVAL_PP(arg1));
}
tmp = php_escape_shell_cmd(buf);
fp = VCWD_POPEN(tmp, p);
efree(tmp);
if (!fp) {
php_error_docref2(NULL TSRMLS_CC, buf, p, E_WARNING, "%s", strerror(errno));
efree(p);
efree(buf);
RETURN_FALSE;
}
efree(buf);
} else {
fp = VCWD_POPEN(Z_STRVAL_PP(arg1), p);
if (!fp) {
php_error_docref2(NULL TSRMLS_CC, Z_STRVAL_PP(arg1), p, E_WARNING, "%s", strerror(errno));
efree(p);
RETURN_FALSE;
}
}
stream = php_stream_fopen_from_pipe(fp, p);
if (stream == NULL) {
php_error_docref2(NULL TSRMLS_CC, Z_STRVAL_PP(arg1), p, E_WARNING, "%s", strerror(errno));
RETVAL_FALSE;
} else {
php_stream_to_zval(stream, return_value);
}
efree(p);
}
我们继续查找源,/php-5.2.2/TSRM/tsrm_virtual_cwd.c 行1027-1028:
....
memcpy(ptr, command, command_length+1);
retval = popen(command_line, type);
...
到这我们终于找到PHP的popen其实是封装了系统中的popen函数,而系统popen函数的执行流程又是怎么样的呢?
1、使用pipe系统调用创建一个管道
2、创建一个新的进程,将拷贝标准输入或者标准输出到文件描述符。
3、关闭pipe返回的文件描述符。
4、调用execve系统调用来执行命令。
……
再来看popen.php和shell.php
A:shell.php
sleep(1);
echo 'test';
sleep(1000);
echo 'hello';
?>
B:popen.php
popen('php shell.php &', 'r');
?>
再管道中会打开没有系统影射的文件描述符做为进程间的通讯管道,一个做为写端子,一个为读端子,PHP的popen会将系统zval的 return_value关联到C调用popen返回的文件描述符,OK,如果此文件描述符被释放了,会发生怎么样的情况,呵,正是前面所描述到的问题。 echo ‘test’;后就不再执行了,即该程序在写一个读端被关闭的管道,会产生SIGPIPE信号,虽然没有仔细看PHP处理SIGPIPE信号的代码,但结果已经告诉我们,退出了。
总结:
1、shell_exec、exec、system等函数在执行命令后会等待返回后再执行下一行代码,而popen不会。
2、popen封装了C调用popen来实现管道,管道中会打开两个文件描述符做为进程间通讯用,如果写端或者读端关闭都会导致系统产生错误,而PHP会把这个管道描述符做为popen的返回,如果没有将该文件描述符进行保存,将会被PHP释放,导导致出现管道一端关闭的情况而中途退出。
3、在程序的执行过程中必须保持该管道(即该文件描述符),不能被释放或者重写,这就意味着不能对管道文件描述符重复赋值,一个文件描述符只能做为单独的一个管道使用。
4、当然,我们也可以将进程的标准输出重定向来解决这个问题。
PHP程序员最易犯10种错误
TIOBE编程社区指数2010年1月


