1. 打开一个文件(如果存在)
2. 写/读文件
3. 关闭这个文件
l打开文件
在打开文件文件之前,我们需要知道这个文件的路径,以及此文件是否存在。
用$_SERVER[“DOCUMENT_ROOT”]内置全局变量,来获得站点的相对路径。如下:
$root = $_SERVER[“DOCUMENT_ROOT”];
在用函数file_exists()来检测文件是否存在。如下:
If(!file_exists("$root/order.txt")){echo ‘文件不存在';}
接下来用fopen()函数打开这个文件。
$fp = fopen("$root/order.txt",'ab');
fopen()函数,接受2个或3个或4个参数。
第一个参数为文件路径,第二个为操作方式(读/写/追加等等),必选参数。
$fp = fopen("$root/order.txt",'ab');
第三个为可选参数,如果需要PHP在include_path中搜索一个文件,就可以使用它,不需要提供目录名或路径。
$fp = fopen("order.txt",'ab',true);
第四个也为可选参数,允许文件名称以协议名称开始(如http://)并且在一个远程的位置打开这个文件,也支持一些其他的协议,比如ftp等等。
如果fopen()成功的打开一个文件,就返回一个指向此文件的指针。在上面我们保存到了$fp变量中。
附文件模式图
写文件
在PHP中写文件比较简单。直接用fwrite()函数即可。
fwrite()的原型如下
int fwrite(resource handle,string string [,int length]);
第三个参数是可选的,表明写入文件的最大长度。
可以通过内置strlen()函数获得字符串的长度,如下:
fwrite($fp,$outputinfo,strlen($outputinfo));
此函数告诉PHP将$outputinfo中的信息保存到$fp指向的文件中。
l读文件
1. 以只读模式打开文件
仍然使用fopen()函数,但只读模式打开文件,就用“rb”文件模式。如下:
$fp = fopen(“$root/order.txt”,'rb');
2. 知道何时读完文件
我们用while循环来读取文件内容,用feof()函数,作为循环条件的终止条件。如下:
while(!feof($fp)){
//要处理的信息
}
3.每次读取一行记录
fgets()函数可以从文本文件中读取一行内容。如下:
$fp = fopen("$root/order.txt",'rb');
while(!feof($fp)){
$info = fgets($fp,999);
echo $info.'<br />';
}
fclose($fp);
这样,他将不断的读入数据,直到读取一个换行符(\n)或者文件结束符EOF,或者是从文件中读取了998B,可以读取的最大长度为指定的长度减去1B。
4.读取整个文件
PHP提供了4中不同的方式来读取整个文件。
a).readfile()函数
它可以不用先fopen($path)文件和关闭文件,也不用echo,直接使用即可。如下:
readfile(“$root/order.txt”);
它会自动把文件的信息,输出到浏览器中。它的原型如下:
Int readfile(string filename,[int use_include_path[,resource context]]);
第二个可选参数指定了PHP是否在include_path中查找文件,这一点于fopen函数一样,返回值为从文件中读取的字节总数。
注:直接使用,不用fopen或fclose
b).fpassthru()函数
要使用这个函数,必须先fopen()打开一个文件。然后将文件的指针作为参数传递给fpassthru(),这样就可以把文件指针所指向的文件内容输出。然后再将这个文件关闭。如下:
$fp = fopen(“$root/order.txt”,'rb');
fpassthru($fp);
fclose($fp);
返回值同样为从文件中读取的字节总数。
注:必须fopen和fclose
c).file()函数
除了将文件输出到浏览器中外,他和readfile()函数是一样的,它把结果发送到一个数组中。如下:
$fileArray = file(“$root/order.txt”);
文件中的每一行,将作为数组的每一个元素。
注:直接使用,不用fopen和fclose
d).file_get_contents()函数
于readfile()相同,但是该函数将以字符串的形式返回文件内容,而不是将文件内容直接输出到浏览器中,也就是必须使用echo 输出,如下:
echo file_get_contents(“$root/order.txt”);
注:直接使用,不用fopen和fclose
5.读取一个字符
fgetc()函数从一个文件中一次读取一个字符,它具有一个文件指针函数,这也是唯一的参数,而且它返回下一个字符。如下:
$fp = fopen("$root/order.txt",'rb');
while(!feof($fp)){
$char = fgetc($fp);
if(!feof($fp)){
echo ($char == "\n" ? '<br />' : $char);
}
}
fclose($fp);
注:fgetc()函数的一个缺点就是它返回文件的结束符EOF,而fgets()则不会。读取字符后还需要判断feof()。
6. 读取任意长度
fread()函数即为从文件中读取任一长度的字节,函数原型如下:
string fread(resource fp,int length);
使用该函数时,它或者是读满了length参数所指定的字节数,或者是读到了文件的结束。
$fp = fopen("$root/order.txt",'rb');
echo fread($fp,10); //读取10个字节
fclose($fp);
l关闭文件
关闭文件比较简单,直接调用fclose()函数即可,如果返回true,则表明成功,反之。如下:
fclose($fp);
l删除文件
unlink()函数(没有名为delete的函数),如下:
unlink("$root/order.txt");
l确定文件大小
可以使用filesize()函数来查看一个文件的大小(字节为单位),如下:
echo filesize("$root/order.txt");
大家也可以参考下面的文章
以下是一篇关于文件基本读写操作的文章,我曾经就是看了这篇文章后学会文件基本操作的,在这里发出来与大家共享:
读文件:
PHP代码:
1. <?php
2.
3. $file_name = "data.dat";
4. // 要读取的文件的绝对路径: homedata.dat
5.
6. $file_pointer = fopen($file_name, "r");
7. // 打开文件,8. "r" 是一种模式,9. 或者说我们要进行的操作方法,10. 详见本文后面的介绍
11.
12. $file_read = fread($file_pointer, filesize($file_name));
13. // 通过文件指14. 针读取文件内容
15.
16. fclose($file_pointer);
17. // 关闭文件
18.
19. print "读取到的文件内容是: $file_read";
20. // 显示文件内容
21. ?>
22.
写文件:
PHP代码:
1. <?php
2.
3. $file_name = "data.dat";
4. // 绝对路径: homedata.dat
5.
6. $file_pointer = fopen($file_name, "w");
7. // "w"是一种模式,8. 详见后面
9.
10. fwrite($file_pointer, "what you wanna write");
11. // 先把文件剪切12. 为0字节大小,13. 然后写入
14.
15. fclose($file_pointer);
16. // 结束
17.
18. print "数据成功写入文件";
19.
20. ?>
21.
追加到文件后面:
PHP代码:
1. <?php
2.
3. $file_name = "data.dat";
4. // 绝对路径: homedata.dat
5.
6. $file_pointer = fopen($file_name, "a");
7. // "w"模式
8.
9. fwrite($file_pointer, "what you wanna append");
10. // 不11. 把文件剪切12. 成0字节,13. 把数据追加到文件最后
14.
15. fclose($file_pointer);
16. // 结束
17.
18. print "数据成功追加到文件";
19.
20. ?>
21.
以上只是简单介绍,下面我们要讨论一些更深层的。
有时候会发生多人写入的情况(最常见是在流量较大的网站),会产生无用的数据写入文件, 例如:
info.file文件内容如下 ->
|1|Mukul|15|Male|India (n)
|2|Linus|31|Male|Finland (n)
现在两个人同时注册,引起文件破坏->
info.file ->
|1|Mukul|15|Male|India
|2|Linus|31|Male|Finland
|3|Rob|27|Male|USA|
Bill|29|Male|USA
上例中当PHP写入Rob的信息到文件的时候,Bill正好也开始写入,这时候正好需要写入Rob纪录的'n',引起文件破坏。
我们当然不希望发生这样的情况, 所以让我们看看文件锁定:
PHP代码:
1. <?php
2.
3. $file_name = "data.dat";
4.
5. $file_pointer = fopen($file_name, "r");
6.
7. $lock = flock($file_pointer, LOCK_SH);
8. // 我使用4.0.2,9. 所以用LOCK_SH,10. 你可能需要直接写成 1.
11.
12. if ($lock) {
13.
14. $file_read = fread($file_pointer, filesize($file_name));
15. $lock = flock($file_pointer, LOCK_UN);
16. // 如果版本小于PHP4.0.2,17. 用 3 代替 LOCK_UN
18.
19. }
20.
21. fclose($file_pointer);
22.
23. print "文件内容为 $file_read";
24.
25. ?>
26.
上例中,如果两个文件read.php和read2.php都要存取该文件,那么它们都可以读取,但是当一个程序需要写入的时候,它必须等待,直到读操作完成,文件所释放。
PHP代码:
1. <?php
2.
3. $file_name = "data.dat";
4.
5. $file_pointer = fopen($file_name, "w");
6.
7. $lock = flock($file_pointer, LOCK_EX);
8. // 如果版本低于PHP4.0.2,9. 用 2 代替 LOCK_EX
10.
11. if ($lock) {
12.
13. fwrite($file_pointer, "what u wanna write");
14. flock($file_pointer, LOCK_UN);
15. // 如果版本低于PHP4.0.2,16. 用 3 代替 LOCK_UN
17.
18. }
19.
20. fclose($file_pointer);
21.
22. print "数据成功写入文件";
23.
24. ?>
25.
虽然"w"模式用来覆盖文件, 单我觉得不适用。
PHP代码:
1. <?php
2.
3. $file_name = "data.dat";
4.
5. $file_pointer = fopen($file_name, "a");
6.
7. $lock = flock($file_pointer, LOCK_EX);
8. // 如果版本低于PHP4.0.2,9. 用 2 代替 LOCK_EX
10.
11. if ($lock) {
12.
13. fseek($file_pointer, 0, SEEK_END);
14. // 如果版本小于PHP4.0RC1,15. 使用 fseek($file_pointer, filsize($file_name));
16.
17. fwrite($file_pointer, "what u wanna write");
18. flock($file_pointer, LOCK_UN);
19. // 如果版本低于PHP4.0.2,20. 用 3 代替 LOCK_UN
21.
22. }
23.
24. fclose($file_pointer);
25.
26. print "数据成功写入文件";
27.
28. ?>
29.
Hmmm..., 对于追加数据与其他操作有点不同,就是FSEEK! 确认文件指针在文件尾部总是一个好习惯。
如果是在Windows系统下, 上面的文件中文件名前面需要加上''.
FLOCK杂谈:
Flock()只在文件打开后才锁定。 在上列中文件打开后才获得锁定,现在文件的内容只是在当时的内容, 而不反映出别的程序操作的结果,因此不只是在文件追加操作,就是对读取操作也应该使用fseek。
(此处翻译可能不是很确切, 但我想意思到了)。
关于模式:
'r' - 只读方式打开, 文件指针置于文件头
'r+' - 读写方式打开,文件指针置于文件头
'w' - 只写打开,文件指针置于文件头, 文件被剪切为0字节, 如果文件不存在, 尝试建立文件
'w+' - 读写打开,文件指针置于文件头, 文件大小被剪切为0字节,如果文件不存在, 尝试建立文件
'a' - 只写方式打开,文件指针置于文件尾,如果文件不存在,尝试建立文件
'a+' - 读写打开,文件指针置于文件尾,如果文件不存在, 尝试建立文件
顺便说一下创建文件目录的代码
//创建类似"../../../xxx/xxx.txt"的目录
function createdirs($path, $mode = 0777) //mode 077
{
$dirs = explode('/',$path);
$pos = strrpos($path, ".");
if ($pos === false) { // note: three equal signs
// not found, means path ends in a dir not file
$subamount=0;
}
else {
$subamount=1;
}
for ($c=0;$c < count($dirs) - $subamount; $c++) {
$thispath="";
for ($cc=0; $cc <= $c; $cc++) {
$thispath.=$dirs[$cc].'/';
}
if (!file_exists($thispath)) {
//print "$thispath";
mkdir($thispath,$mode); //mkdir函数创建目录
}
}
}
//调用如createdirs("xxx/xxxx/xxxx",);
//原函数中使用$GLOBALS["dirseparator"]我改成了'/'
function recur_mkdirs($path, $mode = 0777) //mode 0777
{
//$GLOBALS["dirseparator"]
$dirs = explode($GLOBALS["dirseparator"],$path);
$pos = strrpos($path, ".");
if ($pos === false) { // note: three equal signs
// not found, means path ends in a dir not file
$subamount=0;
}
else {
$subamount=1;
}
这些只是一些基本的关于文件的操作代码,相信对初学者很有用,在此贴出来,希望有抛砖引玉之功能!
在PHP中,填充一个字符串变量相当简单,这只需要一个语句"<?php $str = 'hello world '; ?>"即可,并且该字符串能够被自由地修改、拷贝和移动。而在C语言中,尽管你能够编写例如"char *str = "hello world ";"这样的一个简单的静态字符串;但是,却不能修改该字符串,因为它生存于程序空间内。为了创建一个可操纵的字符串,你必须分配一个内存块,并且通过一个函数(例如strdup())来复制其内容。
char *str;
str = strdup("hello world");
if (!str) {
fprintf(stderr, "Unable to allocate memory!");
}
}
由于后面我们将分析的各种原因,传统型内存管理函数(例如malloc(),free(),strdup(),realloc(),calloc(),等等)几乎都不能直接为PHP源代码所使用。
二、 释放内存
在几乎所有的平台上,内存管理都是通过一种请求和释放模式实现的。首先,一个应用程序请求它下面的层(通常指"操作系统"):"我想使用一些内存空间"。如果存在可用的空间,操作系统就会把它提供给该程序并且打上一个标记以便不会再把这部分内存分配给其它程序。
当应用程序使用完这部分内存,它应该被返回到OS;这样以来,它就能够被继续分配给其它程序。如果该程序不返回这部分内存,那么OS无法知道是否这块内存不再使用并进而再分配给另一个进程。如果一个内存块没有释放,并且所有者应用程序丢失了它,那么,我们就说此应用程序"存在漏洞",因为这部分内存无法再为其它程序可用。
在一个典型的客户端应用程序中,较小的不太经常的内存泄漏有时能够为OS所"容忍",因为在这个进程稍后结束时该泄漏内存会被隐式返回到OS。这并没有什么,因为OS知道它把该内存分配给了哪个程序,并且它能够确信当该程序终止时不再需要该内存。
而对于长时间运行的服务器守护程序,包括象Apache这样的web服务器和扩展php模块来说,进程往往被设计为相当长时间一直运行。因为OS不能清理内存使用,所以,任何程序的泄漏-无论是多么小-都将导致重复操作并最终耗尽所有的系统资源。
现在,我们不妨考虑用户空间内的stristr()函数;为了使用大小写不敏感的搜索来查找一个字符串,它实际上创建了两个串的各自的一个小型副本,然后执行一个更传统型的大小写敏感的搜索来查找相对的偏移量。然而,在定位该字符串的偏移量之后,它不再使用这些小写版本的字符串。如果它不释放这些副本,那么,每一个使用stristr()的脚本在每次调用它时都将泄漏一些内存。最后,web服务器进程将拥有所有的系统内存,但却不能够使用它。
你可以理直气壮地说,理想的解决方案就是编写良好、干净的、一致的代码。这当然不错;但是,在一个象PHP解释器这样的环境中,这种观点仅对了一半。
三、 错误处理
为了实现"跳出"对用户空间脚本及其依赖的扩展函数的一个活动请求,需要使用一种方法来完全"跳出"一个活动请求。这是在Zend引擎内实现的:在一个请求的开始设置一个"跳出"地址,然后在任何die()或exit()调用或在遇到任何关键错误(E_ERROR)时执行一个longjmp()以跳转到该"跳出"地址。
尽管这个"跳出"进程能够简化程序执行的流程,但是,在绝大多数情况下,这会意味着将会跳过资源清除代码部分(例如free()调用)并最终导致出现内存漏洞。现在,让我们来考虑下面这个简化版本的处理函数调用的引擎代码:
zend_function *fe;
char *lcase_fname;
/* PHP函数名是大小写不敏感的,
*为了简化在函数表中对它们的定位,
*所有函数名都隐含地翻译为小写的
*/
lcase_fname = estrndup(fname, fname_len);
zend_str_tolower(lcase_fname, fname_len);
if (zend_hash_find(EG(function_table),lcase_fname, fname_len + 1, (void **)&fe) == FAILURE) {
zend_execute(fe->op_array TSRMLS_CC);
} else {
php_error_docref(NULL TSRMLS_CC, E_ERROR,"Call to undefined function: %s()", fname);
}
efree(lcase_fname);
}
当执行到php_error_docref()这一行时,内部错误处理器就会明白该错误级别是critical,并相应地调用longjmp()来中断当前程序流程并离开call_function()函数,甚至根本不会执行到efree(lcase_fname)这一行。你可能想把efree()代码行移动到zend_error()代码行的上面;但是,调用这个call_function()例程的代码行会怎么样呢?fname本身很可能就是一个分配的字符串,并且,在它被错误消息处理使用完之前,你根本不能释放它。
注意,这个php_error_docref()函数是trigger_error()函数的一个内部等价实现。它的第一个参数是一个将被添加到docref的可选的文档引用。第三个参数可以是任何我们熟悉的E_*家族常量,用于指示错误的严重程度。第四个参数(最后一个)遵循printf()风格的格式化和变量参数列表式样。
四、 Zend内存管理器
在上面的"跳出"请求期间解决内存泄漏的方案之一是:使用Zend内存管理(ZendMM)层。引擎的这一部分非常类似于操作系统的内存管理行为-分配内存给调用程序。区别在于,它处于进程空间中非常低的位置而且是"请求感知"的;这样以来,当一个请求结束时,它能够执行与OS在一个进程终止时相同的行为。也就是说,它会隐式地释放所有的为该请求所占用的内存。图1展示了ZendMM与OS以及PHP进程之间的关系。
图1.Zend内存管理器代替系统调用来实现针对每一种请求的内存分配。
除了提供隐式内存清除功能之外,ZendMM还能够根据php.ini中memory_limit的设置控制每一种内存请求的用法。如果一个脚本试图请求比系统中可用内存更多的内存,或大于它每次应该请求的最大量,那么,ZendMM将自动地发出一个E_ERROR消息并且启动相应的"跳出"进程。这种方法的一个额外优点在于,大多数内存分配调用的返回值并不需要检查,因为如果失败的话将会导致立即跳转到引擎的退出部分。
把PHP内部代码和OS的实际的内存管理层"钩"在一起的原理并不复杂:所有内部分配的内存都要使用一组特定的可选函数实现。例如,PHP代码不是使用malloc(16)来分配一个16字节内存块而是使用了emalloc(16)。除了实现实际的内存分配任务外,ZendMM还会使用相应的绑定请求类型来标志该内存块;这样以来,当一个请求"跳出"时,ZendMM可以隐式地释放它。
经常情况下,内存一般都需要被分配比单个请求持续时间更长的一段时间。这种类型的分配(因其在一次请求结束之后仍然存在而被称为"永久性分配"),可以使用传统型内存分配器来实现,因为这些分配并不会添加ZendMM使用的那些额外的相应于每种请求的信息。然而有时,直到运行时刻才会确定是否一个特定的分配需要永久性分配,因此ZendMM导出了一组帮助宏,其行为类似于其它的内存分配函数,但是使用最后一个额外参数来指示是否为永久性分配。
如果你确实想实现一个永久性分配,那么这个参数应该被设置为1;在这种情况下,请求是通过传统型malloc()分配器家族进行传递的。然而,如果运行时刻逻辑认为这个块不需要永久性分配;那么,这个参数可以被设置为零,并且调用将会被调整到针对每种请求的内存分配器函数。
例如,pemalloc(buffer_len,1)将映射到malloc(buffer_len),而pemalloc(buffer_len,0)将被使用下列语句映射到emalloc(buffer_len):
#define pemalloc(size, persistent) ((persistent)?malloc(size): emalloc(size))
所有这些在ZendMM中提供的分配器函数都能够从下表中找到其更传统的对应实现。
表格1展示了ZendMM支持下的每一个分配器函数以及它们的e/pe对应实现:
表格1.传统型相对于PHP特定的分配器。
分配器函数 e/pe对应实现 void *malloc(size_t count); void *emalloc(size_t count);void *pemalloc(size_t count,char persistent); void *calloc(size_t count); void *ecalloc(size_t count);void *pecalloc(size_t count,char persistent); void *realloc(void *ptr,size_t count); void *erealloc(void *ptr,size_t count);
void *perealloc(void *ptr,size_t count,char persistent); void *strdup(void *ptr); void *estrdup(void *ptr);void *pestrdup(void *ptr,char persistent); void free(void *ptr); void efree(void *ptr);
void pefree(void *ptr,char persistent);
你可能会注意到,即使是pefree()函数也要求使用永久性标志。这是因为在调用pefree()时,它实际上并不知道是否ptr是一种永久性分配。针对一个非永久性分配调用free()能够导致双倍的空间释放,而针对一种永久性分配调用efree()有可能会导致一个段错误,因为内存管理器会试图查找并不存在的管理信息。因此,你的代码需要记住它分配的数据结构是否是永久性的。
除了分配器函数核心部分外,还存在其它一些非常方便的ZendMM特定的函数,例如:
该函数能够分配len+1个字节的内存并且从ptr处复制len个字节到最新分配的块。这个estrndup()函数的行为可以大致描述如下:
{
char *dst = emalloc(len + 1);
memcpy(dst, ptr, len);
dst[len] = 0;
return dst;
}
在此,被隐式放置在缓冲区最后的NULL字节可以确保任何使用estrndup()实现字符串复制操作的函数都不需要担心会把结果缓冲区传递给一个例如printf()这样的希望以为NULL为结束符的函数。当使用estrndup()来复制非字符串数据时,最后一个字节实质上都浪费了,但其中的利明显大于弊。
void *safe_pemalloc(size_t size, size_t count,size_t addtl,char persistent);
这些函数分配的内存空间最终大小是((size*count)+addtl)。你可以会问:"为什么还要提供额外函数呢?为什么不使用一个emalloc/pemalloc呢?"原因很简单:为了安全。尽管有时候可能性相当小,但是,正是这一"可能性相当小"的结果导致宿主平台的内存溢出。这可能会导致分配负数个数的字节空间,或更有甚者,会导致分配一个小于调用程序要求大小的字节空间。而safe_emalloc()能够避免这种类型的陷井-通过检查整数溢出并且在发生这样的溢出时显式地预以结束。
注意,并不是所有的内存分配例程都有一个相应的p*对等实现。例如,不存在pestrndup(),并且在PHP 5.1版本前也不存在safe_pemalloc()。
五、 引用计数
慎重的内存分配与释放对于PHP(它是一种多请求进程)的长期性能有极其重大的影响;但是,这还仅是问题的一半。为了使一个每秒处理上千次点击的服务器高效地运行,每一次请求都需要使用尽可能少的内存并且要尽可能减少不必要的数据复制操作。请考虑下列PHP代码片断:
$a = 'Hello World';
$b = $a;
unset($a);
?>
在第一次调用之后,只有一个变量被创建,并且一个12字节的内存块指派给它以便存储字符串"Hello World",还包括一个结尾处的NULL字符。现在,让我们来观察后面的两行:$b被置为与变量$a相同的值,然后变量$a被释放。
如果PHP因每次变量赋值都要复制变量内容的话,那么,对于上例中要复制的字符串还需要复制额外的12个字节,并且在数据复制期间还要进行另外的处理器加载。这一行为乍看起来有点荒谬,因为当第三行代码出现时,原始变量被释放,从而使得整个数据复制显得完全不必要。其实,我们不妨再远一层考虑,让我们设想当一个10MB大小的文件的内容被装载到两个变量中时会发生什么。这将会占用20MB的空间,此时,10已经足够了。引擎会把那么多的时间和内存浪费在这样一种无用的努力上吗?
你应该知道,PHP的设计者早已深谙此理。
记住,在引擎中,变量名和它们的值实际上是两个不同的概念。值本身是一个无名的zval*存储体(在本例中,是一个字符串值),它被通过zend_hash_add()赋给变量$a。如果两个变量名都指向同一个值,会发生什么呢?
zval *helloval;
MAKE_STD_ZVAL(helloval);
ZVAL_STRING(helloval, "Hello World", 1);
zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),&helloval, sizeof(zval*), NULL);
zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),&helloval, sizeof(zval*), NULL);
}
此时,你可以实际地观察$a或$b,并且会看到它们都包含字符串"Hello World"。遗憾的是,接下来,你继续执行第三行代码"unset($a);"。此时,unset()并不知道$a变量指向的数据还被另一个变量所使用,因此它只是盲目地释放掉该内存。任何随后的对变量$b的存取都将被分析为已经释放的内存空间并因此导致引擎崩溃。
这个问题可以借助于zval(它有好几种形式)的第四个成员refcount加以解决。当一个变量被首次创建并赋值时,它的refcount被初始化为1,因为它被假定仅由最初创建它时相应的变量所使用。当你的代码片断开始把helloval赋给$b时,它需要把refcount的值增加为2;这样以来,现在该值被两个变量所引用:
zval *helloval;
MAKE_STD_ZVAL(helloval);
ZVAL_STRING(helloval, "Hello World", 1);
zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),&helloval, sizeof(zval*), NULL);
ZVAL_ADDREF(helloval);
zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),&helloval,sizeof(zval*),NULL);
}
现在,当unset()删除原变量的$a相应的副本时,它就能够从refcount参数中看到,还有另外其他人对该数据感兴趣;因此,它应该只是减少refcount的计数值,然后不再管它。
六、 写复制(Copy on Write)
通过refcounting来节约内存的确是不错的主意,但是,当你仅想改变其中一个变量的值时情况会如何呢?为此,请考虑下面的代码片断:
$a = 1;
$b = $a;
$b += 5;
?>
通过上面的逻辑流程,你当然知道$a的值仍然等于1,而$b的值最后将是6。并且此时,你还知道,Zend在尽力节省内存-通过使$a和$b都引用相同的zval(见第二行代码)。那么,当执行到第三行并且必须改变$b变量的值时,会发生什么情况呢?
回答是,Zend要查看refcount的值,并且确保在它的值大于1时对之进行分离。在Zend引擎中,分离是破坏一个引用对的过程,正好与你刚才看到的过程相反:
{
zval **varval, *varcopy;
if (zend_hash_find(EG(active_symbol_table),varname, varname_len + 1, (void**)&varval) == FAILURE) {
/* 变量根本并不存在-失败而导致退出*/
return NULL;
}
if ((*varval)->refcount < 2) {
/* varname是唯一的实际引用,
*不需要进行分离
*/
return *varval;
}
/* 否则,再复制一份zval*的值*/
MAKE_STD_ZVAL(varcopy);
varcopy = *varval;
/* 复制任何在zval*内的已分配的结构*/
zval_copy_ctor(varcopy);
/*删除旧版本的varname
*这将减少该过程中varval的refcount的值
*/
zend_hash_del(EG(active_symbol_table), varname, varname_len + 1);
/*初始化新创建的值的引用计数,并把它依附到
* varname变量
*/
varcopy->refcount = 1;
varcopy->is_ref = 0;
zend_hash_add(EG(active_symbol_table), varname, varname_len + 1,&varcopy, sizeof(zval*), NULL);
/*返回新的zval* */
return varcopy;
}
现在,既然引擎有一个仅为变量$b所拥有的zval*(引擎能知道这一点),所以它能够把这个值转换成一个long型值并根据脚本的请求给它增加5。
七、 写改变(change-on-write)
引用计数概念的引入还导致了一个新的数据操作可能性,其形式从用户空间脚本管理器看来与"引用"有一定关系。请考虑下列的用户空间代码片断:
$a = 1;
$b = &$a;
$b += 5;
?>
在上面的PHP代码中,你能看出$a的值现在为6,尽管它一开始为1并且从未(直接)发生变化。之所以会发生这种情况是因为当引擎开始把$b的值增加5时,它注意到$b是一个对$a的引用并且认为"我可以改变该值而不必分离它,因为我想使所有的引用变量都能看到这一改变"。
但是,引擎是如何知道的呢?很简单,它只要查看一下zval结构的第四个和最后一个元素(is_ref)即可。这是一个简单的开/关位,它定义了该值是否实际上是一个用户空间风格引用集的一部分。在前面的代码片断中,当执行第一行时,为$a创建的值得到一个refcount为1,还有一个is_ref值为0,因为它仅为一个变量($a)所拥有并且没有其它变量对它产生写引用改变。在第二行,这个值的refcount元素被增加为2,除了这次is_ref元素被置为1之外(因为脚本中包含了一个"&"符号以指示是完全引用)。
最后,在第三行,引擎再一次取出与变量$b相关的值并且检查是否有必要进行分离。这一次该值没有被分离,因为前面没有包括一个检查。下面是get_var_and_separate()函数中与refcount检查有关的部分代码:
/* varname是唯一的实际引用,
* 或者它是对其它变量的一个完全引用
*任何一种方式:都没有进行分离
*/
return *varval;
}
这一次,尽管refcount为2,却没有实现分离,因为这个值是一个完全引用。引擎能够自由地修改它而不必关心其它变量值的变化。
八、 分离问题
尽管已经存在上面讨论到的复制和引用技术,但是还存在一些不能通过is_ref和refcount操作来解决的问题。请考虑下面这个PHP代码块:
$a = 1;
$b = $a;
$c = &$a;
?>
在此,你有一个需要与三个不同的变量相关联的值。其中,两个变量是使用了"change-on-write"完全引用方式,而第三个变量处于一种可分离的"copy-on-write"(写复制)上下文中。如果仅使用is_ref和refcount来描述这种关系,有哪些值能够工作呢?
回答是:没有一个能工作。在这种情况下,这个值必须被复制到两个分离的zval*中,尽管两者都包含完全相同的数据(见图2)。
图2.引用时强制分离
同样,下列代码块将引起相同的冲突并且强迫该值分离出一个副本(见图3)。
图3.复制时强制分离
$a = 1;
$b = &$a;
$c = $a;
?>
注意,在这里的两种情况下,$b都与原始的zval对象相关联,因为在分离发生时引擎无法知道介于到该操作当中的第三个变量的名字。
九、 总结
PHP是一种托管语言。从普通用户角度来看,这种仔细地控制资源和内存的方式意味着更为容易地进行原型开发并导致出现更少的冲突。然而,当我们深入"内里"之后,一切的承诺似乎都不复存在,最终还要依赖于真正有责任心的开发者来维持整个运行时刻环境的一致性。
我们先找一个例子图像(用Canon 550D拍的):
例子图片:butterfly.jpg
下面看看如何使用Imagick实现图像直方图:
<?php
$file = 'butterfly.jpg';
$size = array(
'width' => 256,
'height' => 100,
);
$image = new Imagick($file);
$histogram = array_fill_keys(range(0, 255), 0);
foreach ($image->getImageHistogram() as $pixel) {
$rgb = $pixel->getColor();
$histogram[$rgb['r']] += $pixel->getColorCount();
$histogram[$rgb['g']] += $pixel->getColorCount();
$histogram[$rgb['b']] += $pixel->getColorCount();
}
$max = max($histogram);
$threshold = ($image->getImageWidth() * $image->getImageHeight()) / 256 * 12;
if ($max > $threshold) {
$max = $threshold;
}
$image = new Imagick();
$draw = new ImagickDraw();
$image->newImage($size['width'], $size['height'], 'white');
foreach ($histogram as $x => $count) {
if ($count == 0) {
continue;
}
$draw->setStrokeColor('black');
$height = min($count, $max) / $max * $size['height'];
$draw->line($x, $size['height'], $x, $size['height'] - $height);
$image->drawImage($draw);
$draw->clear();
}
$image->setImageFormat('png');
$image->writeImage('histogram.png');
?>
注:代码中之所以加入$threshold这个阀值,是因为有时候某些色阶的值可能会非常大,如果不做处理会干扰最终的生成效果。至于为什么要先除256,接着又乘12,没有什么道理可言,都是我一拍脑袋决定的,你也可以使用别的方法。
最终生成的直方图和Photoshop的效果基本一样,这里就贴一下Photoshop的:
Photoshop生成的直方图
注:使用Photoshop打开图片后,选择窗口,然后选择直方图即可。
本文说的实际上只是RGB通道的直方图绘制方法,原理上,RGB直方图是红绿蓝直方图累加的结果,至于红绿蓝三原色各自的直方图,上面代码稍加修改即可。
注:XARG.ORG上有一个HTML5实现的图像直方图开源项目,效果不错,值得学习。
最后顺便说一下,如果你对摄影知识感兴趣,可参考:如何解读数码相机的直方图。