PHP特性
一、用PHP进行HTTP认证
PHP 的 HTTP 认证机制仅在 PHP 以 Apache 模块方式运行时才有效,因此该功能不适用于 CGI 版本。在 Apache 模块的 PHP 脚本中,可以用 header()函数来向客户端浏览器发送“Authentication Required”信息,使其弹出一个用户名/密码输入窗口。当用户输入用户名和密码后,包含有 URL 的 PHP 脚本将会加上预定义变量PHP_AUTH_USER,PHP_AUTH_PW 和 AUTH_TYPE被再次调用,这三个变量分别被设定为用户名,密码和认证类型。预定义变量保存在 $_SERVER 或者 $HTTP_SERVER_VARS 数组中。支持“Basic”和“Digest”(自 PHP 5.1.0 起)认证方法
二、Cookie
PHP 透明地支持 HTTP cookie。cookie 是一种在远程浏览器端储存数据并以此来跟踪和识别用户的机制。可以用 setcookie() 或 setrawcookie() 函数来设置 cookie。cookie 是 HTTP 标头的一部分,因此 setcookie() 函数必须在其它信息被输出到浏览器前调用,这和对 header() 函数的限制类似。可以使用输出缓冲函数来延迟脚本的输出,直到按需要设置好了所有的 cookie 或者其它 HTTP 标头。
如果 variables_order中包括“C”,则任何从客户端发送的 cookie 都会被自动包括进 $_COOKIE自动全局数组。如果希望对一个 cookie 变量设置多个值,则需在 cookie 的名称后加 [] 符号。
根据 register_globals的设置,可以从 cookie 建立普通的 PHP 变量。但是不推荐依赖于此特性,因为出于安全原因此选项通常是关闭的。在早期的 PHP 版本中,当 track_vars配置选项打开时(此选项自 PHP 4.0.3 后总是打开的),系统还会设定 $HTTP_COOKIE_VARS。
三、会话
会话机制(Session)在 PHP 中用于保存并发访问中的一些数据。这使可以帮助创建更为人性化的程序,增加站点的吸引力。关于会话机制的更多信息
四、处理XForms
XForms 定义了一种传统 web 表单的变种,它可以用于更多的平台和浏览器,甚至非传统的媒体
五、文件上传处理
* Post方法上传
可以上传文本和二进制文件
相关设置见php.ini 的 file_uploads,upload_max_filesize,upload_tmp_dirpost_max_size以及 max_input_time
确保文件上传表单的属性是 enctype="multipart/form-data",否则文件上传不了
* 全局变量$_FILES数组
其可包含所有上传的文件信息。
$_FILES['userfile']['name']
客户端机器文件的原名称。
$_FILES['userfile']['type']
文件的 MIME 类型,如果浏览器提供此信息的话。一个例子是“image/gif”。不过此 MIME 类型在 PHP 端并不检查,因此不要想当然认为有这个值。
$_FILES['userfile']['size']
已上传文件的大小,单位为字节。
$_FILES['userfile']['tmp_name']
文件被上传后在服务端储存的临时文件名。
$_FILES['userfile']['error']
和该文件上传相关的错误代码。
* 上传临时目录
文件被上传后,默认地会被储存到服务端的默认临时目录中,除非 php.ini 中的 upload_tmp_dir设置为其它的路径。服务端的默认临时目录可以通过更改 PHP 运行环境的环境变量 TMPDIR 来重新设置,但是在 PHP 脚本内部通过运行 putenv()函数来设置是不起作用的。该环境变量也可以用来确认其它的操作也是在上传的文件上进行的。
* 使用上传文件生效
函数 is_uploaded_file() 和 move_uploaded_file()
* 错误信息说明
PHP 将随文件信息数组一起返回一个对应的错误代码。该代码可以在文件上传时生成的文件数组中的 error 字段中被找到,也就是 $_FILES['userfile']['error']。
UPLOAD_ERR_OK
其值为 0,没有错误发生,文件上传成功。
UPLOAD_ERR_INI_SIZE
其值为 1,上传的文件超过了 php.ini 中 upload_max_filesize 选项限制的值。
UPLOAD_ERR_FORM_SIZE
其值为 2,上传文件的大小超过了 HTML 表单中 MAX_FILE_SIZE 选项指定的值。
UPLOAD_ERR_PARTIAL
其值为 3,文件只有部分被上传。
UPLOAD_ERR_NO_FILE
其值为 4,没有文件被上传。
UPLOAD_ERR_NO_TMP_DIR
其值为 6,找不到临时文件夹。PHP 4.3.10 和 PHP 5.0.3 引进。
UPLOAD_ERR_CANT_WRITE
其值为 7,文件写入失败。PHP 5.1.0 引进
* 常见缺陷
对 MAX_FILE_SIZE 设置的值,不能大于 ini 设置中 upload_max_filesize选项设置的值。其默认值为 2M 字节。
如果内存限制设置被激活,可能需要将 memory_limit 设置的更大些,请确认 memory_limit 的设置足够的大。
如果 max_execution_time设置的值太小,脚本运行的时间可能会超过该设置。因此,也请保证 max_execution_time 足够的大
max_execution_time仅仅只影响脚本本身运行的时间。任何其它花费在脚本运行之外的时间,诸如用函数 system() 对系统的调用、sleep()函数的使用、数据库查询、文件上传等,在计算脚本运行的最大时间时都不包括在内。
max_input_time以秒为单位设定了脚本接收输入的最大时间,包括文件上传。对于较大或多个文件,或者用户的网速较慢时,可能会超过默认的 60 秒。
如果 post_max_size设置的值太小,则较大的文件会无法被上传。因此,请保证 post_max_size 的值足够的大
* 上传多个文件
可以对 input 域使用不同的 name 来上传多个文件,并将它们的信息自动以数组的形式组织。要完成这项功能,需要在 HTML 表单中对文件上传域使用和多选框与复选框相同的数组式提交语法
* 对Put方法的支持
/* PUT data comes in on the stdin stream */ $putdata = fopen("php://stdin", "r"); /* Open a file for writing */ $fp = fopen("myputfile.ext", "w"); /* Read the data 1 KB at a time and write to the file */ while ($data = fread($putdata, 1024)) fwrite($fp, $data); /* Close the streams */ fclose($fp); fclose($putdata);
让 Apache 或者 PHP 自动允许所有人覆盖 web 目录树下的任何文件显然是很不明智的。因此,要处理类似的请求,必须先告诉 web 服务器需要用特定的 PHP 脚本来处理该请求。在 Apache 下,可以用 Script 选项来设置。它可以被放置到 Apache 配置文件中几乎所有的位置。通常我们把它放置在 <Directory> 区域或者 <Virtualhost> 区域。
Script PUT /put.php
这将告诉 Apache 将所有对 URI 的 PUT 请求全部发送到 put.php 脚本,这些 URI 必须和 PUT 命令中的内容相匹配。当然,这是建立在 PHP 支持 .php 扩展名,并且 PHP 已经在运行的假设之上。
这将会把文件拷贝到远程客户端请求的位置。可能希望在文件拷贝之前进行一些检查或者对用户认证之类的操作。这里唯一的问题是,当 PHP 接受到 PUT 方法的请求时,它将会把上传的文件储存到和其它用 POST 方法处理过的文件相同的临时目录。在请求结束时,临时文件将被删除。因此,用来处理 PUT 的 PHP 脚本必须将该文件拷贝到其它的地方。该临时文件的文件名被储存在变量 $PHP_PUT_FILENAME 中,也可以通过 $REQUEST_URI 变量获得建议的目标文件名(在非 Apache web 服务器上可能会有较大的变化)。该目标文件名是由远程客户端指定的。也可以不听从改客户端的信息,而把所有上传的文件存储到一个特殊的上传目录下
六、使用远程文件
只要在 php.ini 文件中激活了 allow_url_fopen选项,就可以在大多数需要用文件名作为参数的函数中使用 HTTP 和 FTP 的 URL 来代替文件名。同时,也可以在 include()、include_once()、require() 及 require_once() 语句中使用 URL。
Example #1 获取远程文件的标题
$file = fopen ("http://www.example.com/", "r"); if (!$file) { echo "<p>Unable to open remote file.\n"; exit; } while (!feof ($file)) { $line = fgets ($file, 1024); /* This only works if the title and its tags are on one line */ if (eregi ("<title>(.*)</title>", $line, $out)) { $title = $out[1]; break; } } fclose($file);
Example #2 将数据保存到远程服务器
$file = fopen ("ftp://ftp.example.com/incoming/outputfile", "w"); if (!$file) { echo "<p>Unable to open remote file for writing.\n"; exit; } /* Write the data here. */ fwrite ($file, $_SERVER['HTTP_USER_AGENT'] . "\n"); fclose ($file);
七、连接处理
在 PHP 内部,系统维护着连接状态,其状态有三种可能的情况:
■0 - NORMAL(正常)
■1 - ABORTED(异常退出)
■2 - TIMEOUT(超时)
当 PHP 脚本正常地运行 NORMAL 状态时,连接为有效。当远程客户端中断连接时,ABORTED 状态的标记将会被打开。远程客户端连接的中断通常是由用户点击 STOP 按钮导致的。当连接时间超过 PHP 的时限(请参阅 set_time_limit() 函数)时,TIMEOUT 状态的标记将被打开。
可以决定脚本是否需要在客户端中断连接时退出。有时候让脚本完整地运行会带来很多方便,即使没有远程浏览器接受脚本的输出。默认的情况是当远程客户端连接中断时脚本将会退出。该处理过程可由 php.ini 的 ignore_user_abort 或由 httpd.conf设置中对应的“php_value ignore_user_abort”以及 ignore_user_abort() 函数来控制。如果没有告诉 PHP 忽略用户的中断,脚本将会被中断,除非通过 register_shutdown_function()设置了关闭触发函数。通过该关闭触发函数,当远程用户点击 STOP 按钮后,脚本再次尝试输出数据时,PHP 将会检测到连接已被中断,并调用关闭触发函数。
脚本也有可能被内置的脚本计时器中断。默认的超时限制为 30 秒。这个值可以通过设置 php.ini 的 max_execution_time或 httpd.conf设置中对应的“php_value max_execution_time”参数或者 set_time_limit()函数来更改。当计数器超时的时候,脚本将会类似于以上连接中断的情况退出,先前被注册过的关闭触发函数也将在这时被执行。在该关闭触发函数中,可以通过调用 connection_status()函数来检查超时是否导致关闭触发函数被调用。如果超时导致了关闭触发函数的调用,该函数将返回 2。
需要注意的一点是 ABORTED 和 TIMEOUT 状态可以同时有效。这在告诉 PHP 忽略用户的退出操作时是可能的。PHP 将仍然注意用户已经中断了连接但脚本仍然在运行的情况。如果到了运行的时间限制,脚本将被退出,设置过的关闭触发函数也将被执行。在这时会发现函数 connection_status() 返回 3。
八、数据库永久连接
永久的数据库连接是指在脚本结束运行时不关闭的连接。当收到一个永久连接的请求时。PHP 将检查是否已经存在一个(前面已经开启的)相同的永久连接。如果存在,将直接使用这个连接;如果不存在,则建立一个新的连接。所谓“相同”的连接是指用相同的用户名和密码到相同主机的连接。
对 web 服务器的工作和分布负载没有完全理解的读者可能会错误地理解永久连接的作用。特别的,永久连接不会在相同的连接上提供建立“用户会话”的能力,也不提供有效建立事务的能力。实际上,从严格意义上来讲,永久连接不会提供任何非永久连接无法提供的特殊功能。
为什么?
这和 web 服务器工作的方式有关。web 服务器可以用三种方法来利用 PHP 生成 web 页面。
第一种方法是将 PHP 用作一个“外壳”。以这种方法运行,PHP 会为向 web 服务器提出的每个 PHP 页面请求生成并结束一个 PHP 解释器线程。由于该线程会随每个请求的结束而结束,因此任何在这个线程中利用的任何资源(例如指向 SQL 数据库服务器的连接)都会随线程的结束而关闭。在这种情况下,使用永久连接不会获得任何地改变——因为它们根本不是永久的。
第二,也是最常用的方法,是把 PHP 用作多进程 web 服务器的一个模块,这种方法目前只适用于 Apache。对于一个多进程的服务器,其典型特征是有一个父进程和一组子进程协调运行,其中实际生成 web 页面的是子进程。每当客户端向父进程提出请求时,该请求会被传递给还没有被其它的客户端请求占用的子进程。这也就是说当相同的客户端第二次向服务端提出请求时,它将有可能被一个不同的子进程来处理。在开启了一个永久连接后,所有请求 SQL 服务的后继页面都能够重新使用这个已经建立的 SQL Server 连接。
最后一种方法是将 PHP 用作多线程 web 服务器的一个插件。目前 PHP 4 已经支持 ISAPI、WSAPI 和 NSAPI(在 Windows 环境下),这些使得 PHP 可以被用作诸如 Netscape FastTrack (iPlanet)、Microsoft's Internet Information Server (IIS) 和 O'Reilly's WebSite Pro 等多线程 web 服务器的插件。永久连接的行为和前面所描述的多过程模型在本质上是相同的。注意 PHP 3 不支持 SAPI。
如果永久连接并没有任何附加的功能,那么使用它有什么好处?
答案非常简单——效率。当客户端对 SQL 服务器的连接请求非常频繁时,永久连接将更加高效。连接请求频繁的标准取决于很多因素。例如,数据库的种类,数据库服务和 web 服务是否在同一台服务器上,SQL 服务器如何加载负载等。但我们至少知道,当连接请求很频繁时,永久连接将显著的提高效率。它使得每个子进程在其生命周期中只做一次连接操作,而非每次在处理一个页面时都要向 SQL 服务器提出连接请求。这也就是说,每个子进程将对服务器建立各自独立的永久连接。例如,如果有 20 个不同的子进程运行某脚本建立了永久的 SQL 服务器永久连接,那么实际上向该 SQL 服务器建立了 20 个不同的永久连接,每个进程占有一个。
注意,如果永久连接的子进程数目超过了设定的数据库连接数限制,系统将会产生一些缺陷。如果数据库的同时连接数限制为 16,而在繁忙会话的情况下,有 17 个线程试图连接,那么有一个线程将无法连接。如果这个时候,在脚本中出现了使得连接无法关闭的错误(例如无限循环),则该数据库的 16 个连接将迅速地受到影响。请查阅使用的数据库的文档,以获取关于如何处理已放弃的及闲置的连接的方法。
在使用永久连接时还有一些特别的问题需要注意。例如在永久连接中使用数据表锁时,如果脚本不管什么原因无法释放该数据表锁,其随后使用相同连接的脚本将会被永久的阻塞,使得需要重新启动 httpd 服务或者数据库服务。另外,在使用事务处理时,如果脚本在事务阻塞产生前结束,则该阻塞也会影响到使用相同连接的下一个脚本。不管在什么情况下,都可以通过使用 register_shutdown_function() 函数来注册一个简单的清理函数来打开数据表锁,或者回滚事务。或者更好的处理方法,是不在使用数据表锁或者事务处理的脚本中使用永久连接,这可以从根本上解决这个问题(当然还可以在其它地方使用永久连接)。
永久连接是为通常连接建立一对一的分布而设计的。这意味着必须能够保证在将永久连接替换为非永久连接时,脚本的行为不会改变。使用永久连接将(非常)有可能改变脚本的效率,但不改变其行为!
参见 fbsql_pconnect(),ibase_pconnect(),ifx_pconnect(),ingres_pconnect(),msql_pconnect(),mssql_pconnect(),mysql_pconnect(),ociplogon(),odbc_pconnect(),ora_plogon(),pfsockopen(),pg_pconnect()和 sybase_pconnect()。
九、安全模式
PHP 的安全模式是为了试图解决共享服务器(shared-server)安全问题而设立的。在结构上,试图在 PHP 层上解决这个问题是不合理的,但修改 web 服务器层和操作系统层显得非常不现实。因此许多人,特别是 ISP,目前使用安全模式
* 保安措施和安全模式
名字 | 默认 | 可修改范围 | 更新日志 |
---|---|---|---|
safe_mode | "0" | PHP_INI_SYSTEM | |
safe_mode_gid | "0" | PHP_INI_SYSTEM | 自 PHP 4.1.0 起可用 |
safe_mode_include_dir | NULL | PHP_INI_SYSTEM | 自 PHP 4.1.0 起可用 |
safe_mode_exec_dir | "" | PHP_INI_SYSTEM | |
safe_mode_allowed_env_vars | "PHP_" | PHP_INI_SYSTEM | |
safe_mode_protected_env_vars | "LD_LIBRARY_PATH" | PHP_INI_SYSTEM | |
open_basedir | NULL | PHP_INI_SYSTEM | |
disable_functions | "" | 仅 php.ini | 自 PHP 4.0.1 起可用 |
disable_classes | "" | 仅 php.ini | 自 PHP 4.3.2 起可用 |
有关 PHP_INI_* 样式的更多详情与定义,见 Where a configuration setting may be set。
这是配置指令的简短说明。
safe_mode
boolean-
是否启用 PHP 的安全模式。
safe_mode_gid
boolean-
默认情况下,安全模式在打开文件时会做 UID 比较检查。如果想将其放宽到 GID 比较,则打开 safe_mode_gid。是否在文件访问时使用 UID(
FALSE
)或者 GID(TRUE
)来做检查。 safe_mode_include_dir
string-
当从此目录及其子目录(目录必须在 include_path中或者用完整路径来包含)包含文件时越过 UID/GID 检查。
从 PHP 4.2.0 开始,本指令可以接受和 include_path指令类似的风格用冒号(Windows 中是分号)隔开的路径,而不只是一个目录。 指定的限制实际上是一个前缀,而非一个目录名。这也就是说“safe_mode_include_dir = /dir/incl”将允许访问“/dir/include”和“/dir/incls”,如果它们存在的话。如果希望将访问控制在一个指定的目录,那么请在结尾加上一个斜线,例如:“safe_mode_include_dir = /dir/incl/”。 如果本指令的值为空,在 PHP 4.2.3 中以及 PHP 4.3.3 起具有不同 UID/GID的文件将不能被包含。在较早版本中,所有文件都能被包含。 safe_mode_exec_dir
string-
如果 PHP 使用了安全模式,system() 和其它程序执行函数将拒绝启动不在此目录中的程序。必须使用 / 作为目录分隔符,包括 Windows 中。
safe_mode_allowed_env_vars
string-
设置某些环境变量可能是潜在的安全缺口。本指令包含有一个逗号分隔的前缀列表。在安全模式下,用户只能改变那些名字具有在这里提供的前缀的环境变量。默认情况下,用户只能设置以 PHP_ 开头的环境变量(例如 PHP_FOO = BAR)。
Note:
如果本指令为空,PHP 将使用户可以修改任何环境变量!
safe_mode_protected_env_vars
string-
本指令包含有一个逗号分隔的环境变量的列表,最终用户不能用 putenv() 来改变这些环境变量。甚至在 safe_mode_allowed_env_vars 中设置了允许修改时也不能改变这些变量。
参见 register_globals,display_errors 和 log_errors。
当 safe_mode 设置为 on,PHP 将通过文件函数或其目录检查当前脚本的拥有者是否和将被操作的文件的拥有者相匹配。例如:
-rw-rw-r-- 1 rasmus rasmus 33 Jul 1 19:20 script.php -rw-r--r-- 1 root root 1116 May 26 18:01 /etc/passwd
运行 script.php
<?php
readfile('/etc/passwd');
?>
如果安全模式被激活,则将会导致以下错误:
Warning: SAFE MODE Restriction in effect. The script whose uid is 500 is not allowed to access /etc/passwd owned by uid 0 in /docroot/script.php on line 2
同时,或许会存在这样的环境,在该环境下,宽松的 GID检查已经足够,但严格的 UID检查反而是不适合的。可以用 safe_mode_gid选项来控制这种检查。如果设置为 On则进行宽松的 GID 检查;设置为 Off(默认值)则进行 UID 检查。
除了 safe_mode 以外,如果设置了 open_basedir选项,则所有的文件操作将被限制在指定的目录下。例如:
<Directory /docroot> php_admin_value open_basedir /docroot </Directory>
如果在设置了 open_basedir选项后运行同样的 script.php,则其结果会是:
Warning: open_basedir restriction in effect. File is in wrong directory in /docroot/script.php on line 2
也可以单独地屏蔽某些函数。请注意 disable_functions选项不能在 php.ini 文件外部使用,也就是说无法在 httpd.conf文件的按不同虚拟主机或不同目录的方式来屏蔽函数。如果将如下内容加入到 php.ini 文件:
disable_functions readfile,system
则会得到如下的输出:
Warning: readfile() has been disabled for security reasons in /docroot/script.php on line 2
当然,这些 PHP 限制不适用于可执行文件。
* 被安全模式限制或屏蔽的函数
函数名 | 限制 |
---|---|
dbmopen() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 |
dbase_open() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 |
filepro() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 |
filepro_rowcount() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 |
filepro_retrieve() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 |
ifx_* | sql_safe_mode 限制, (!= safe mode) |
ingres_* | sql_safe_mode 限制, (!= safe mode) |
mysql_* | sql_safe_mode 限制, (!= safe mode) |
pg_loimport() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 |
posix_mkfifo() | 检查被操作的目录是否与被执行的脚本有相同的 UID(所有者)。 |
putenv() | 遵循 ini 设置的 safe_mode_protected_env_vars 和 safe_mode_allowed_env_vars 选项。请参考 putenv() 函数的有关文档。 |
move_uploaded_file() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 |
chdir() | 检查被操作的目录是否与被执行的脚本有相同的 UID(所有者)。 |
dl() | 当 PHP 运行在 安全模式 时,不能使用此函数。 |
backtick operator | 当 PHP 运行在 安全模式 时,不能使用此函数。 |
shell_exec()(在功能上和 backticks 函数相同) | 当 PHP 运行在 安全模式 时,不能使用此函数。 |
exec() | 只能在 safe_mode_exec_dir设置的目录下进行执行操作。基于某些原因,目前不能在可执行对象的路径中使用 ..。escapeshellcmd() 将被作用于此函数的参数上。 |
system() | 只能在 safe_mode_exec_dir设置的目录下进行执行操作。基于某些原因,目前不能在可执行对象的路径中使用 ..。escapeshellcmd() 将被作用于此函数的参数上。 |
passthru() | 只能在 safe_mode_exec_dir设置的目录下进行执行操作。基于某些原因,目前不能在可执行对象的路径中使用 ..。escapeshellcmd() 将被作用于此函数的参数上。 |
popen() | 只能在 safe_mode_exec_dir设置的目录下进行执行操作。基于某些原因,目前不能在可执行对象的路径中使用 ..。escapeshellcmd() 将被作用于此函数的参数上。 |
fopen() | 检查被操作的目录是否与被执行的脚本有相同的 UID(所有者)。 |
mkdir() | 检查被操作的目录是否与被执行的脚本有相同的 UID(所有者)。 |
rmdir() | 检查被操作的目录是否与被执行的脚本有相同的 UID(所有者)。 |
rename() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 检查被操作的目录是否与被执行的脚本有相同的 UID(所有者)。 |
unlink() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 检查被操作的目录是否与被执行的脚本有相同的 UID(所有者)。 |
copy() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 检查被操作的目录是否与被执行的脚本有相同的 UID(所有者)。 (on source and target ) |
chgrp() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 |
chown() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 |
chmod() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 另外,不能设置 SUID、SGID 和 sticky bits |
touch() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 检查被操作的目录是否与被执行的脚本有相同的 UID(所有者)。 |
symlink() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 检查被操作的目录是否与被执行的脚本有相同的 UID(所有者)。 (注意:仅测试 target) |
link() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 检查被操作的目录是否与被执行的脚本有相同的 UID(所有者)。 (注意:仅测试 target) |
apache_request_headers() | 在安全模式下,以“authorization”(区分大小写)开头的标头将不会被返回。 |
header() | 在安全模式下,如果设置了 WWW-Authenticate,当前脚本的 uid 将被添加到该标头的 realm 部分。 |
PHP_AUTH 变量 | 在安全模式下,变量 PHP_AUTH_USER、PHP_AUTH_PW 和 PHP_AUTH_TYPE 在 $_SERVER 中不可用。但无论如何,您仍然可以使用 REMOTE_USER 来获取用户名称(USER)。(注意:仅 PHP 4.3.0 以后有效) |
highlight_file(), show_source() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 检查被操作的目录是否与被执行的脚本有相同的 UID(所有者)。 (注意,仅在 4.2.1 版本后有效) |
parse_ini_file() | 检查被操作的文件或目录是否与被执行的脚本有相同的 UID(所有者)。 检查被操作的目录是否与被执行的脚本有相同的 UID(所有者)。 (注意,仅在 4.2.1 版本后有效) |
set_time_limit() | 在安全模式下不起作用。 |
max_execution_time | 在安全模式下不起作用。 |
mail() | 在安全模式下,第五个参数被屏蔽。(注意,仅自 PHP 4.2.3 起受影响) |
任何使用 php4/main/fopen_wrappers.c 的函数 | ?? |
十、PHP命令行模式
PHP 提供了一种新类型的 CLI SAPI(Server Application Programming Interface,服务端应用编程端口)支持,名为 CLI,意为 Command Line Interface,即命令行接口。顾名思义,该 CLI SAPI 模块主要用作 PHP 的开发外壳应用。CLI SAPI和其它 CLI SAPI模块相比有很多的不同之处,我们将在本章中详细阐述。值得一提的是,CLI和 CGI 是不同的 SAPI,尽管它们之间有很多共同的行为。
CLI SAPI 最先是随 PHP 4.2.0 版本发布的,但仍旧只是一个实验性的版本,并需要在运行 ./configure 时加上 --enable-cli参数。从 PHP 4.3.0 版本开始,CLI SAPI 成为了正式模块,--enable-cli参数会被默认得设置为 on,也可以用参数 --disable-cli来屏蔽。
从 PHP 4.3.0开始,CLI/CGI 二进制执行文件的文件名、位置和是否存在会根据 PHP 在系统上的安装而不同。在默认情况下,当运行 make 时,CGI 和 CLI 都会被编译并且分别放置在 PHP 源文件目录的 sapi/cgi/php 和 sapi/cli/php 下。可以注意到两个文件都被命名为了 php。在 make install的过程中会发生什么取决于配置行。如果在配置的时候选择了一个 SAPI 模块,如 apxs,或者使用了 --disable-cgi参数,则在 make install 的过程中,CLI 将被拷贝到 {PREFIX}/bin/php,除非 CGI 已经被放置在了那个位置。因此,例如,如果在配置行中有 --with--apxs,则在 make install 的过程中,CLI 将被拷贝到 {PREFIX}/bin/php。如果希望撤销 CGI 执行文件的安装,请在 make install之后运行 make install-cli。或者,也可以在配置行中加上 --disable-cgi参数。
Note:
由于 --enable-cli和 --enable-cgi同时默认有效,因此,不必再配置行中加上 --enable-cli来使得 CLI 在 make install 过程中被拷贝到 {PREFIX}/bin/php。
在 PHP 4.2.0 到 PHP 4.2.3 之间的 Windows 发行包中,CLI 的文件名为 php-cli.exe,相同文件夹下的 php.exe 为 CGI。从 PHP 4.3.0 版本开始,Windows 的发行包中 CLI 的执行文件为 php.exe,被放置在一个单独的名为 cli 的文件夹下,即 cli/php.exe。在 PHP 5 中,CLI 存在于主文件夹中,名为 php.exe,而 CGI 版本名为 php-cgi.exe。
从 PHP 5 起,一个名为 php-win.exe的新文件随包发布。它相当于 CLI 版本,但是 php-win 不输出任何内容,便不提供控制台(不会弹出“DOS 窗口”)。这种方式类似于 php-gtk。需要使用 --enable-cli-win32选项来配置它。
Note: 如何得知自己使用的是哪个 SAPI?
在命令行下,运行 php -v 便能得知该 php 是 CGI 还是 CLI。请参考函数 php_sapi_name() 以及常量
PHP_SAPI
。
Note:
在 PHP 4.3.2 中加入了 Unix 的 man 页面。可以在命令行中键入 man php 来查看。
以下为 CLI SAPI 和其它 CLI SAPI 模块相比的显著区别:
-
与 CGI SAPI 不同,其输出没有任何头信息。
尽管 CGI SAPI 提供了取消 HTTP 头信息的方法,但在 CLI SAPI 中并不存在类似的方法以开启 HTTP 头信息的输出。
CLI 默认以安静模式开始,但为了保证兼容性,-q和 --no-header参数为了向后兼容仍然保留,使得可以使用旧的 CGI 脚本。
在运行时,不会把工作目录改为脚本的当前目录(可以使用 -C和 --no-chdir参数来兼容 CGI 模式)。
出错时输出纯文本的错误信息(非 HTML 格式)。
-
CLI SAPI 强制覆盖了 php.ini中的某些设置,因为这些设置在外壳环境下是没有意义的。
覆盖 php.ini 设置选项 设置选项 CLI SAPI 默认值 备注 html_errors FALSE
无意义的 HTML 标记符会使得出错信息很凌乱,所以在外壳下阅读报错信息是十分困难的。因此将该选项的默认值改为 FALSE
。implicit_flush TRUE
在命令行模式下,所有来自 print() 和 echo() 的输出将被立即写到输出端,而不作任何地缓冲操作。如果希望延缓或控制标准输出,仍然可以使用 output buffering 设置项。 max_execution_time 0(无限值) 鉴于在外壳环境下使用 PHP 的无穷的可能性,最大运行时间被设置为了无限值。为 web 开发的应用程序可能只需运行几秒钟时间,而外壳应用程序的运行时间可能会长的多。 register_argc_argv TRUE
由于该设置为
TRUE
,将总是可以在 CLI SAPI中访问到 argc(传送给应用程序参数的个数)和 argv(包含有实际参数的数组)。对于 PHP 4.3.0,在使用 CLI SAPI 时,PHP 变量 $argc 和 $argv已被注册并且设定了对应的值。而在这之前的版本,这两个变量在 CGI 或者 模块 版本中的建立依赖于将 PHP 的设置选项 register_globals设为 on。除了版本和 register_globals 设定以外,可以随时通过调用 $_SERVER或者 $HTTP_SERVER_VARS来访问它们。例如:$_SERVER['argv']
Note:
这些设置无法在设置文件 php.ini或任何指定的其它文件中被初始化为其它值。这些默认值被限制在所有其它的设置文件被解析后改变。不过,它们的值可以在程序运行的过程中被改变(尽管对于该运行过程来说,这些设置项是没有意义的)。
-
为了减轻外壳环境下的工作,我们定义了如下常量:
CLI 专用常量 常量名称 描 述 STDIN
一个已打开的指向 stdin的流。可以用如下方法来调用: <?php
$stdin = fopen('php://stdin', 'r');
?><?php
$line = trim(fgets(STDIN)); // 从 STDIN 读取一行
fscanf(STDIN, "%d\n", $number); // 从 STDIN 读取数字
?>STDOUT
一个已打开的指向 stdout的流。可以用如下方式来调用: <?php
$stdout = fopen('php://stdout', 'w');
?>STDERR
一个已打开的指向 stderr的流。可以用如下方式来调用: <?php
$stderr = fopen('php://stderr', 'w');
?>有了以上常量,就无需自己建立指向诸如 stderr的流,只需简单的使用这些常量来代替流指向:
php -r 'fwrite(STDERR, "stderr\n");'
-
CLI SAPI不会将当前目录改为已运行的脚本所在的目录。
以下范例显示了本模块与 CGI SAPI 模块之间的不同:
<?php
// 名为 test.php 的简单测试程序
echo getcwd(), "\n";
?>在使用 CGI 版本时,其输出为
$ pwd /tmp $ php-cgi -f another_directory/test.php /tmp/another_directory
明显可以看到 PHP 将当前目录改成了刚刚运行过的脚本所在的目录。
使用 CLI SAPI 模式,得到:
$ pwd /tmp $ php -q another_directory/test.php /tmp
Note:
可以在命令行运行时给该 CGI SAPI 加上 -C参数,使其支持 CLI SAPI 的功能。
以下是 PHP 二进制文件(即 php.exe程序)提供的命令行模式的选项参数,随时可以运行带 -h参数的 PHP 命令来查询这些参数。
Usage: php [options] [-f] <file> [--] [args...] php [options] -r <code> [--] [args...] php [options] [-B <begin_code>] -R <code> [-E <end_code>] [--] [args...] php [options] [-B <begin_code>] -F <file> [-E <end_code>] [--] [args...] php [options] -- [args...] php [options] -a -a Run interactively -c <path>|<file> Look for php.ini file in this directory -n No php.ini file will be used -d foo[=bar] Define INI entry foo with value 'bar' -e Generate extended information for debugger/profiler -f <file> Parse <file>. -h This help -i PHP information -l Syntax check only (lint) -m Show compiled in modules -r <code> Run PHP <code> without using script tags <?..?> -B <begin_code> Run PHP <begin_code> before processing input lines -R <code> Run PHP <code> for every input line -F <file> Parse and execute <file> for every input line -E <end_code> Run PHP <end_code> after processing all input lines -H Hide any passed arguments from external tools. -s Display colour syntax highlighted source. -v Version number -w Display source with stripped comments and whitespace. -z <file> Load Zend extension <file>. args... Arguments passed to script. Use -- args when first argument starts with - or script is read from stdin
CLI SAPI 模块有以下三种不同的方法来获取要运行的 PHP 代码:
-
让 PHP 运行指定文件。
php my_script.php php -f my_script.php
以上两种方法(使用或不使用 -f参数)都能够运行给定的 my_script.php 文件。可以选择任何文件来运行,指定的 PHP 脚本并非必须要以 .php为扩展名,它们可以有任意的文件名和扩展名。
-
在命令行直接运行 PHP 代码。
php -r 'print_r(get_defined_constants());'
在使用这种方法时,请注意外壳变量的替代及引号的使用。
Note:
请仔细阅读以上范例,在运行代码时没有开始和结束的标记符!加上 -r参数后,这些标记符是不需要的,加上它们会导致语法错误。
-
通过标准输入(stdin)提供需要运行的 PHP 代码。
以上用法提供了非常强大的功能,使得可以如下范例所示,动态地生成 PHP 代码并通过命令行运行这些代码:
$ some_application | some_filter | php | sort -u >final_output.txt
以上三种运行代码的方法不能同时使用。
和所有的外壳应用程序一样,PHP 的二进制文件(php.exe文件)及其运行的 PHP 脚本能够接受一系列的参数。PHP 没有限制传送给脚本程序的参数的个数(外壳程序对命令行的字符数有限制,但通常都不会超过该限制)。传递给脚本的参数可在全局变量 $argv 中获取。该数组中下标为零的成员为脚本的名称(当 PHP 代码来自标准输入获直接用 -r参数以命令行方式运行时,该名称为“-”)。另外,全局变量 $argc 存有 $argv数组中成员变量的个数(而非传送给脚本程序的参数的个数)。
只要传送给脚本的参数不是以 -符号开头,就无需过多的注意什么。向脚本传送以 - 开头的参数会导致错误,因为 PHP 会认为应该由它自身来处理这些参数。可以用参数列表分隔符 -- 来解决这个问题。在 PHP 解析完参数后,该符号后所有的参数将会被原样传送给脚本程序。
# 以下命令将不会运行 PHP 代码,而只显示 PHP 命令行模式的使用说明: $ php -r 'var_dump($argv);' -h Usage: php [options] [-f] <file> [args...] [...] # 以下命令将会把“-h”参数传送给脚本程序,PHP 不会显示命令行模式的使用说明: $ php -r 'var_dump($argv);' -- -h array(2) { [0]=> string(1) "-" [1]=> string(2) "-h" }
除此之外,还有另一个方法将 PHP 用于外壳脚本。可以在写一个脚本,并在第一行以 #!/usr/bin/php 开头,在其后加上以 PHP 开始和结尾标记符包含的正常的 PHP 代码,然后为该文件设置正确的运行属性(例如:chmod +x test)。该方法可以使得该文件能够像外壳脚本或 PERL 脚本一样被直接执行。
#!/usr/bin/php
<?php
var_dump($argv);
?>
假设改文件名为 test 并被放置在当前目录下,可以做如下操作:
$ chmod +x test $ ./test -h -- foo array(4) { [0]=> string(6) "./test" [1]=> string(2) "-h" [2]=> string(2) "--" [3]=> string(3) "foo" }
正如所看到的,在向该脚本传送以 - 开头的参数时,脚本仍然能够正常运行。
PHP 4.3.3 以来有效的长选项:
选项名称 | 长名称 | 说明 |
---|---|---|
-a | --interactive |
交互式运行 PHP。如果编译 PHP 时加入了 Readline 扩展(Windows 下不可用),那将会得到一个很好的外壳,包括一个自动完成的功能(例如可以在键入变量名的时候,按下 TAB 键,PHP 会自动完成该变量名)以及命令历史记录,可以用上下键来访问。历史记录存在 ~/.php_history 文件中。
|
-c | --php-ini |
用该参数,可以指定一个放置 php.ini文件的目录,或者直接指定一个自定义的 INI文件(其文件名可以不是 php.ini),例如: $ php -c /custom/directory/ my_script.php $ php -c /custom/directory/custom-file.ini my_script.php
|
-n | --no-php-ini |
完全忽略 php.ini。此参数在 PHP 4.3.0 以后有效。 |
-d | --define |
用该参数可以自行设置任何可以在 php.ini文件中设置的配置选项的值,其语法为: -d configuration_directive[=value]
例子(因版面原因而折行显示): # 取值部分被省略,将会把配置选项设为 "1" $ php -d max_execution_time -r '$foo = ini_get("max_execution_time"); var_dump($foo);' string(1) "1" # 取值部分为空白,将会把配置选项设为 "" php -d max_execution_time= -r '$foo = ini_get("max_execution_time"); var_dump($foo);' string(0) "" # 配置选项将被设置成为任何 '=' 字符之后的值 $ php -d max_execution_time=20 -r '$foo = ini_get("max_execution_time"); var_dump($foo);' string(2) "20" $ php -d max_execution_time=doesntmakesense -r '$foo = ini_get("max_execution_time"); var_dump($foo);' string(15) "doesntmakesense"
|
-e | --profile-info |
激活扩展信息模式,被用于调试/测试。 |
-f | --file |
解析并运行 -f选项给定的文件名。该参数为可选参数,可以省略,仅指明需要运行的文件名即可。 |
-h and -? | --help and --usage | 使用该参数,可以得到完整的命令行参数的列表及这些参数作用的简单描述。 |
-i | --info | 该命令行参数会调用 phpinfo()函数并显示出结果。如果 PHP 没有正常工作,建议执行 php -i命令来查看在信息表格之前或者对应的地方是否有任何错误信息输出。请注意当使用 CGI 摸索时,输出的内容为 HTML格式,因此输出的信息篇幅较大。 |
-l | --syntax-check |
该参数提供了对指定 PHP 代码进行语法检查的方便的方法。如果成功,则向标准输出写入 No syntax errors detected in <filename>字符串,并且外壳返回值为 0。如果失败,则输出 Errors parsing <filename>以及内部解析器错误信息到标准输出,同时外壳返回值将别设置为 255。 该参数将无法检查致命错误(如未定义函数),如果也希望检测致命错误,请使用 -f参数。
|
-m | --modules |
使用该参数,PHP 将打印出内置以及已加载的 PHP 及 Zend 模块: $ php -m [PHP Modules] xml tokenizer standard session posix pcre overload mysql mbstring ctype [Zend Modules]
|
-r | --run |
使用该参数可以在命令行内运行单行 PHP 代码。无需加上 PHP 的起始和结束标识符(<?php 和 ?>),否则将会导致语法解析错误。
|
-B | --process-begin |
在处理 stdin 之前先执行 PHP 代码。PHP 5 新加。 |
-R | --process-code |
对每个输入行都执行 PHP 代码。PHP 5 新加。 此模式下有两个特殊变量:$argn和 $argi。$argn包含 PHP 当前处理的行内容,而 $argi 则包含该行号。 |
-F | --process-file |
对每个输入行都执行 PHP 文件。PHP 5 新加。 |
-E | --process-end |
在处理完输入后执行的 PHP 代码。PHP 5 新加。 使用 -B,-R和 -E选项来计算一个项目总行数的例子。 $ find my_proj | php -B '$l=0;' -R '$l += count(@file($argn));' -E 'echo "Total Lines: $l\n";' Total Lines: 37328
|
-s | --syntax-highlight and --syntax-highlight |
显示有语法高亮色彩的源代码。 该参数使用内建机制来解析文件并为其生成一个 HTML高亮版本并将结果写到标准输出。请注意该过程所做的只是生成了一个 <code> [...] </code> 的 HTML 标记的块,并不包含任何的 HTML 头。
|
-v | --version |
将 PHP,PHP SAPI 和 Zend 的版本信息写入标准输出。例如: $ php -v PHP 4.3.0 (cli), Copyright (c) 1997-2002 The PHP Group Zend Engine v1.3.0, Copyright (c) 1998-2002 Zend Technologies
|
-w | --strip |
显示除去了注释和多余空白的源代码。
|
-z | --zend-extension |
加载 Zend 扩展库。如果仅给定一个文件名,PHP 将试图从当前系统扩展库的默认路径(在 Linux 系统下,该路径通常由 /etc/ld.so.conf指定)加载该扩展库。如果用一个绝对路径指定文件名,则不会使用系统的扩展库默认路径。如果用相对路径指定的文件名,则 PHP 仅试图在当前目录的相对目录加载扩展库。 |
PHP 的命令行模式能使得 PHP 脚本能完全独立于 web 服务器单独运行。如果使用 Unix 系统,需要在 PHP 脚本的最前面加上一行特殊的代码,使得它能够被执行,这样系统就能知道用哪个程序去运行该脚本。在 Windows 平台下可以将 php.exe 和 .php文件的双击属性相关联,也可以编写一个批处理文件来用 PHP 执行脚本。为 Unix 系统增加的第一行代码不会影响该脚本在 Windows 下的运行,因此也可以用该方法编写跨平台的脚本程序。以下是一个简单的 PHP 命令行程序的范例。
Example #1 试图以命令行方式运行的 PHP 脚本(script.php)
#!/usr/bin/php
<?php
if ($argc != 2 || in_array($argv[1], array('--help', '-help', '-h', '-?'))) {
?>
This is a command line PHP script with one option.
Usage:
<?php echo $argv[0]; ?> <option>
<option> can be some word you would like
to print out. With the --help, -help, -h,
or -? options, you can get this help.
<?php
} else {
echo $argv[1];
}
?>
在以上脚本中,用第一行特殊的代码来指明该文件应该由 PHP 来执行。在这里使用 CLI 的版本,因此不会有 HTTP 头信息输出。在用 PHP 编写命令行应用程序时,可以使用两个参数:$argc和 $argv。前面一个的值是比参数个数大 1 的整数(运行的脚本本身的名称也被当作一个参数)。第二个是包含有参数的数组,其第一个元素为脚本的名称,下标为数字 0($argv[0])。
以上程序中检查了参数的个数是大于 1 个还是小于 1 个。此外如果参数是 --help,-help,-h或 -?时,打印出帮助信息,并同时动态输出脚本的名称。如果还收到了其它参数,将其显示出来。
如果希望在 Unix 下运行以上脚本,需要使其属性为可执行文件,然后简单的运行 script.php echothis 或 script.php -h。在 Windows 下,可以为此编写一个批处理文件:
Example #2 运行 PHP 命令行脚本的批处理文件(script.bat)
@C:\php\php.exe script.php %1 %2 %3 %
假设将上述程序命名为 script.php,且 CLI 版的 php.exe 文件放置在 c:\php\cli\php.exe,该批处理文件会帮助将附加的参数传给脚本程序:script.bat echothis 或 script.bat -h。
请参阅 Readline扩展模块的有关文档,以获取更多的函数的信息。这些函数可以帮助完善 PHP 命令行应用程序。
十一、垃圾回收机制
引用计数基本知识
每个php变量存在一个叫"zval"的变量容器中。一个zval变量容器,除了包含变量的类型和值,还包括两个字节的额外信息。第一个是"is_ref",是个bool值,用来标识这个变量是否是属于引用集合(reference set)。通过这个字节,php引擎才能把普通变量和引用变量区分开来,由于php允许用户通过使用&来使用自定义引用,zval变量容器中还有一个内部引用计数机制,来优化内存使用。第二个额外字节是"refcount",用以表示指向这个zval变量容器的变量(也称符号即symbol)个数。所有的符号存在一个符号表中,其中每个符号都有作用域(scope),那些主脚本(比如:通过浏览器请求的的脚本)和每个函数或者方法也都有作用域。
当一个变量被赋常量值时,就会生成一个zval变量容器,如下例这样:
Example #1 Creating a new zval container
<?php
$a = "new string";
?>
在上例中,新的变量a,是在当前作用域中生成的。并且生成了类型为 string 和值为new string的变量容器。在额外的两个字节信息中,"is_ref"被默认设置为 FALSE
,因为没有任何自定义的引用生成。"refcount" 被设定为 1,因为这里只有一个变量使用这个变量容器. 注意到当"refcount"的值是1时,"is_ref"的值总是FALSE
. 如果你已经安装了» Xdebug,你能通过调用函数 xdebug_debug_zval()显示"refcount"和"is_ref"的值。
Example #2 Displaying zval information
<?php
xdebug_debug_zval('a');
?>
以上例程会输出:
a: (refcount=1, is_ref=0)='new string'
把一个变量赋值给另一变量将增加引用次数(refcount).
Example #3 Increasing refcount of a zval
<?php
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );
?>
以上例程会输出:
a: (refcount=2, is_ref=0)='new string'
这时,引用次数是2,因为同一个变量容器被变量 a 和变量 b关联.当没必要时,php不会去复制已生成的变量容器。变量容器在”refcount“变成0时就被销毁. 当任何关联到某个变量容器的变量离开它的作用域(比如:函数执行结束),或者对变量调用了函数 unset()时,”refcount“就会减1,下面的例子就能说明:
Example #4 Decreasing zval refcount
<?php
$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
unset( $b, $c );
xdebug_debug_zval( 'a' );
?>
以上例程会输出:
a: (refcount=3, is_ref=0)='new string' a: (refcount=1, is_ref=0)='new string'
如果我们现在执行 unset($a);,包含类型和值的这个变量容器就会从内存中删除。
复合类型(Compound Types)
当考虑像 array和object这样的复合类型时,事情就稍微有点复杂. 与 标量(scalar)类型的值不同,array和 object类型的变量把它们的成员或属性存在自己的符号表中。这意味着下面的例子将生成三个zval变量容器。
Example #5 Creating a array zval
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );
?>
以上例程的输出类似于:
a: (refcount=1, is_ref=0)=array ( 'meaning' => (refcount=1, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42 )
Or graphically
这三个zval变量容器是: a,meaning和 number。增加和减少”refcount”的规则和上面提到的一样. 下面, 我们在数组中再添加一个元素,并且把它的值设为数组中已存在元素的值:
Example #6 Adding already existing element to an array
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
xdebug_debug_zval( 'a' );
?>
以上例程的输出类似于:
a: (refcount=1, is_ref=0)=array ( 'meaning' => (refcount=2, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42, 'life' => (refcount=2, is_ref=0)='life' )
Or graphically
从以上的xdebug输出信息,我们看到原有的数组元素和新添加的数组元素关联到同一个"refcount"2的zval变量容器. 尽管 Xdebug的输出显示两个值为'life'的 zval 变量容器,其实是同一个。 函数xdebug_debug_zval()不显示这个信息,但是你能通过显示内存指针信息来看到。
删除数组中的一个元素,就是类似于从作用域中删除一个变量. 删除后,数组中的这个元素所在的容器的“refcount”值减少,同样,当“refcount”为0时,这个变量容器就从内存中被删除,下面又一个例子可以说明:
Example #7 Removing an element from an array
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
unset( $a['meaning'], $a['number'] );
xdebug_debug_zval( 'a' );
?>
以上例程的输出类似于:
a: (refcount=1, is_ref=0)=array ( 'life' => (refcount=1, is_ref=0)='life' )
现在,当我们添加一个数组本身作为这个数组的元素时,事情就变得有趣,下个例子将说明这个。例中我们加入了引用操作符,否则php将生成一个复制。
Example #8 Adding the array itself as an element of it self
<?php
$a = array( 'one' );
$a[] =& $a;
xdebug_debug_zval( 'a' );
?>
以上例程的输出类似于:
a: (refcount=2, is_ref=1)=array ( 0 => (refcount=1, is_ref=0)='one', 1 => (refcount=2, is_ref=1)=... )
Or graphically
能看到数组变量 (a) 同时也是这个数组的第二个元素(1) 指向的变量容器中“refcount”为 2。上面的输出结果中的"..."说明发生了递归操作, 显然在这种情况下意味着"..."指向原始数组。
跟刚刚一样,对一个变量调用unset,将删除这个符号,且它指向的变量容器中的引用次数也减1。所以,如果我们在执行完上面的代码后,对变量$a调用unset, 那么变量 $a 和数组元素 "1" 所指向的变量容器的引用次数减1, 从"2"变成"1". 下例可以说明:
Example #9 Unsetting $a
(refcount=1, is_ref=1)=array ( 0 => (refcount=1, is_ref=0)='one', 1 => (refcount=1, is_ref=1)=... )
Or graphically
清理变量容器的问题(Cleanup Problems)
尽管不再有某个作用域中的任何符号指向这个结构(就是变量容器),由于数组元素“1”仍然指向数组本身,所以这个容器不能被清除 。因为没有另外的符号指向它,用户没有办法清除这个结构,结果就会导致内存泄漏。庆幸的是,php将在请求结束时清除这个数据结构,但是在php清除之前,将耗费不少空间的内存。如果你要实现分析算法,或者要做其他像一个子元素指向它的父元素这样的事情,这种情况就会经常发生。当然,同样的情况也会发生在对象上,实际上对象更有可能出现这种情况,因为对象总是隐式的被引用。
如果上面的情况发生仅仅一两次倒没什么,但是如果出现几千次,甚至几十万次的内存泄漏,这显然是个大问题。在长时间运行的脚本,比如请求基本上不会结束的守护进程(deamons)或者单元测试中的大的套件(sets)中,在给 eZ 组件库的模板组件做单元测试时,后者(指单元测试中的大的套件)就会出现问题.它将需要耗用2GB的内存,而一般的测试服务器没有这么大的内存空间。
回收周期(Collecting Cycles)
传统上,像以前的 php 用到的引用计数内存机制,无法处理循环的引用内存泄漏。然而 5.3.0 PHP 使用文章» 引用计数系统中的同步周期回收(Concurrent Cycle Collection in Reference Counted Systems)中的同步算法,来处理这个内存泄漏问题。
对算法的完全说明有点超出这部分内容的范围,将只介绍其中基础部分。首先,我们先要建立一些基本规则,如果一个引用计数增加,它将继续被使用,当然就不再在垃圾中。如果引用计数减少到零,所在变量容器将被清除(free)。就是说,仅仅在引用计数减少到非零值时,才会产生垃圾周期(garbage cycle)。其次,在一个垃圾周期中,通过检查引用计数是否减1,并且检查哪些变量容器的引用次数是零,来发现哪部分是垃圾。
为避免不得不检查所有引用计数可能减少的垃圾周期,这个算法把所有可能根(possible roots 都是zval变量容器),放在根缓冲区(root buffer)中(用紫色来标记),这样可以同时确保每个可能的垃圾根(possible garbage root)在缓冲区中只出现一次。仅仅在根缓冲区满了时,才对缓冲区内部所有不同的变量容器执行垃圾回收操作。看上图的步骤 A。
在步骤 B 中,算法使用深度优先搜索查找所有可能的根,找到后将每个变量容器中的引用计数减“1",为确保不会对同一个变量容器减两次"1",用灰色标记已减过“1”的。在步骤 C 中,算法再一次对每个根节点使用深度优先搜索,检查每个变量容器的引用计数。如果引用计数是 0 ,变量容器用白色来标记(图中的蓝色)。如果引用次数大于0,则恢复在这个点上使用深度优先搜索而将引用计数减”1“的操作(即引用计数加“1”),然后将它们重新用黑色标记。在最后一步 D 中,算法遍历根缓冲区以从那里删除变量容器根(zval roots),同时,检查是否有在上一步中被白色标记的变量容器。每个被白色标记的变量容器都被清除。
现在,你已经对这个算法有了基本了解,我们回头来看这个如何与PHP集成。默认的,PHP的垃圾回收机制是打开的,然后有个 php.ini 设置允许你修改它:zend.enable_gc。
当垃圾回收机制打开时,每当根缓存区存满时,就会执行上面描述的循环查找算法。根缓存区有固定的大小,可存10,000个可能根,当然你可以通过修改PHP源码文件Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES,然后重新编译PHP,来修改这个10,000值。当垃圾回收机制关闭时,循环查找算法永不执行,然而,可能根将一直存在根缓冲区中,不管在配置中垃圾回收机制是否激活。
当垃圾回收机制关闭时,如果根缓冲区存满了可能根,更多的可能根显然不会被记录。那些没被记录的可能根,将不会被这个算法来分析处理。如果他们是循环引用周期的一部分,将永不能被清除进而导致内存泄漏。
即使在垃圾回收机制不可用时,可能根也被记录的原因是,相对于每次找到可能根后检查垃圾回收机制是否打开而言,记录可能根的操作更快。不过垃圾回收和分析机制本身要耗不少时间。
除了修改配置zend.enable_gc,也能通过分别调用gc_enable() 和 gc_disable()函数来打开和关闭垃圾回收机制。调用这些函数,与修改配置项来打开或关闭垃圾回收机制的效果是一样的。即使在可能根缓冲区还没满时,也能强制执行周期回收。你能调用gc_collect_cycles()函数达到这个目的。这个函数将返回使用这个算法回收的周期数。
允许打开和关闭垃圾回收机制并且允许自主的初始化的原因,是由于你的应用程序的某部分可能是高时效性的。在这种情况下,你可能不想使用垃圾回收机制。当然,对你的应用程序的某部分关闭垃圾回收机制,是在冒着可能内存泄漏的风险,因为一些可能根也许存不进有限的根缓冲区。因此,就在你调用gc_disable()函数释放内存之前,先调用gc_collect_cycles()函数可能比较明智。因为这将清除已存放在根缓冲区中的所有可能根,然后在垃圾回收机制被关闭时,可留下空缓冲区以有更多空间存储可能根
性能方面考虑的因素
在上一节我们已经简单的提到:回收可能根有细微的性能上影响,但这是把PHP 5.2与PHP 5.3比较时才有的。尽管在PHP 5.2中,记录可能根相对于完全不记录可能根要慢些,而PHP 5.3中对 PHP run-time 的其他修改减少了这个性能损失。
这里主要有两个领域对性能有影响。第一个是内存占用空间的节省,另一个是垃圾回收机制执行内存清理时的执行时间增加(run-time delay)。我们将研究这两个领域。
内存占用空间的节省
首先,实现垃圾回收机制的整个原因是为了,一旦先决条件满足,通过清理循环引用的变量来节省内存占用。在PHP执行中,一旦根缓冲区满了或者调用gc_collect_cycles() 函数时,就会执行垃圾回收。在下图中,显示了下面脚本分别在PHP 5.2 和 PHP 5.3环境下的内存占用情况,其中排除了脚本启动时PHP本身占用的基本内存。
Example #1 Memory usage example
<?php
class Foo
{
public $var = '3.1415962654';
}
$baseMemory = memory_get_usage();
for ( $i = 0; $i <= 100000; $i++ )
{
$a = new Foo;
$a->self = $a;
if ( $i % 500 === 0 )
{
echo sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "\n";
}
}
?>
在这个很理论性的例子中,我们创建了一个对象,这个对象中的一个属性被设置为指回对象本身。在循环的下一个重复(iteration)中,当脚本中的变量被重新复制时,就会发生典型性的内存泄漏。在这个例子中,两个变量容器是泄漏的(对象容器和属性容器),但是仅仅能找到一个可能根:就是被unset的那个变量。在10,000次重复后(也就产生总共10,000个可能根),当根缓冲区满时,就执行垃圾回收机制,并且释放那些关联的可能根的内存。这从PHP 5.3的锯齿型内存占用图中很容易就能看到。每次执行完10,000次重复后,执行垃圾回收,并释放相关的重复使用的引用变量。在这个例子中由于泄漏的数据结构非常简单,所以垃圾回收机制本身不必做太多工作。从这个图表中,你能看到 PHP 5.3的最大内存占用大概是9 Mb,而PHP 5.2的内存占用一直增加。
执行时间增加(Run-Time Slowdowns)
垃圾回收影响性能的第二个领域是它释放已泄漏的内存耗费的时间。为了看到这个耗时时多少,我们稍微改变了上面的脚本,有更多次数的重复并且删除了循环中的内存占用计算,第二个脚本代码如下:
Example #2 GC performance influences
<?php
class Foo
{
public $var = '3.1415962654';
}
for ( $i = 0; $i <= 1000000; $i++ )
{
$a = new Foo;
$a->self = $a;
}
echo memory_get_peak_usage(), "\n";
?>
我们将运行这个脚本两次,一次通过配置zend.enable_gc打开垃圾回收机制时,另一次是它关闭时。
Example #3 Running the above script
time php -dzend.enable_gc=0 -dmemory_limit=-1 -n example2.php # and time php -dzend.enable_gc=1 -dmemory_limit=-1 -n example2.php
在我的机器上,第一个命令持续执行时间大概为10.7秒,而第二个命令耗费11.4秒。时间上增加了7%。然而,执行这个脚本时内存占用的峰值降低了98%,从931Mb 降到 10Mb。这个基准不是很科学,或者并不能代表真实应用程序的数据,但是它的确显示了垃圾回收机制在内存占用方面的好处。好消息就是,对这个脚本而言,在执行中出现更多的循环引用变量时,内存节省的更多的情况下,每次时间增加的百分比都是7%。
PHP内部 GC 统计信息
在PHP内部,可以显示更多的关于垃圾回收机制如何运行的信息。但是要显示这些信息,你需要先重新编译PHP使benchmark和data-collecting code可用。你需要在按照你的意愿运行./configure前,把环境变量CFLAGS设置成-DGC_BENCH=1。下面的命令串就是做这个事:
Example #4 Recompiling PHP to enable GC benchmarking
export CFLAGS=-DGC_BENCH=1 ./config.nice make clean make
当你用新编译的PHP二进制文件来重新执行上面的例子代码,在PHP执行结束后,你将看到下面的信息:
Example #5 GC statistics
GC Statistics ------------- Runs: 110 Collected: 2072204 Root buffer length: 0 Root buffer peak: 10000 Possible Remove from Marked Root Buffered buffer grey -------- -------- ----------- ------ ZVAL 7175487 1491291 1241690 3611871 ZOBJ 28506264 1527980 677581 1025731
主要的信息统计在第一个块。你能看到垃圾回收机制运行了110次,而且在这110次运行中,总共有超过两百万的内存分配被释放。只要垃圾回收机制运行了至少一次,根缓冲区峰值(Root buffer peak)总是10000.
结论
通常,PHP中的垃圾回收机制,仅仅在循环回收算法确实运行时会有时间消耗上的增加。但是在平常的(更小的)脚本中应根本就没有性能影响。
然而,在平常脚本中有循环回收机制运行的情况下,内存的节省将允许更多这种脚本同时运行在你的服务器上。因为总共使用的内存没达到上限。
这种好处在长时间运行脚本中尤其明显,诸如长时间的测试套件或者daemon脚本此类。同时,对通常比Web脚本运行时间长的» PHP-GTK应用程序,新的垃圾回收机制,应该会大大改变一直以来认为内存泄漏问题难以解决的看法。