日期:2014-05-17  浏览次数:20630 次

PHP扩展编写第一步:PHP和Zend介绍

-----------------
介绍

  如果你在读这篇入门文章,那么你可能对写PHP扩展有点兴趣。如果不是… 好吧,那么等我们写完这篇文章,你将会发现一个之前自己完全不知道,但是非常有趣的东西。
这篇入门文章假设你对PHP语言和以及PHP的编写语言C语言都有一定的熟悉。
让我们以“为什么你需要写一个PHP扩展”作为开始。
  因为PHP语言本身抽象程度有限,有一些库或者操作系统级别的调用,不能用PHP直接调用。
  你想给PHP添加一些与众不同的行为。
  你已经写了一些PHP代码,但是当运行的时候你知道它可以更快,更小,消耗的内存更少。
  你有一部分程序想出售,你可以把它写成扩展,这样程序是可以执行的,但是别人却无法看到源码。
这儿有很多完美的原因,但是要想创建一个扩展,你首先要需要明白什么是扩展。

-----------------
什么是扩展?

  如果你用过PHP,那么你就用过扩展。除了一些极少的特殊情况之外,PHP语言中的每个用户空间函数都是以组的形式分布在一个或多个扩展之中。这些函数中的大部分是位于标准扩展中的 – 总共超过400个。PHP源码中包含86个扩展,平均每个扩展中有30个函数。算一下,大概有2500个函数。如果这个不够用,PECL仓库还提供了超过100个其他扩展,或者还可以在互联网上找到更多的扩展。
  「PHP除了扩展中的这些函数之外,剩下的是什么」我听到了你的疑问「扩展是什么?PHP的核心又是什么?」

  PHP的核心是由两个独立的部分组成的。在最底层是Zend Engine (ZE)。ZE 负责把人类可以理解的脚本解析成机器可以理解的符号(token),然后在一个进程空间内执行这些符号。ZE还负责内存管理,变量作用域,以及函数调用的调度。另一部分是PHP。PHP负责与SAPI层(Server Application Programming Interface,经常被用来与Apache, IIS, CLI, CGI等host环境进行关联)的交互以及绑定。它也为safe_mode和open_basedir检查提供了一个统一的控制层,就像streams层把文件和网络I/O与用户空间函数(例如fopen(),fread()和fwrite())关联起来一样。

-----------------
生命周期

  当一个给定的SAPI启动后,以/usr/local/apache/bin/apachectl start的响应为例,PHP便以初始化它的核心子系统作为开始。随着SAPI启动程序的结束,PHP开始加载每个扩展的代码,然后调用它们的模块初始化(MINIT)程序。这就给每个扩展机会用来初始化内部变量,申请资源,注册资源处理器,并且用ZE注册自己的函数,这样如果一个脚本调用这些函数中的一个,ZE就知道执行哪些代码。
  接下来,PHP会等待SAPI层的页面处理请求。在CGI或者CLI SAPI情况下,这个请求会立即发生并且只执行一次。在Apache, IIS, 或者其他成熟的web服务器SAPI中,请求处理会在远程用户发起请求的时候发生,并且会重复执行很多次,也可能是并发的。不管请求是怎么进来的,PHP以让ZE来建立脚本可以运行的环境作为开始,然后调用每个扩展的请求初始化(RINIT)函数。RINIT给了扩展一个机会,让其可以建立指定的环境变量,分配请求指定的资源,或者执行其他任务例如审计。关于RINIT函数调用最典型的例子是在session扩展中,如果session.auto_start选项是开启的,RINIT会自动触发用户空间的session_start()函数并且预先填充$_SESSION变量。
  当请求一旦被初始化,ZE便把PHP脚本翻译成符号(token),最终翻译成可以进行单步调试和执行的opcode。如果这些opcode中的一个需要调用一个扩展函数,ZE将会给那个函数绑定参数,并且临时放弃控制权直到函数执行完成。
  当一个脚本完成了执行之后,PHP将会调用每个扩展的请求结束(RSHUTDOWN)函数来执行最后的清理工作(比如保存session变量到磁盘上)。接下来,ZE执行一个清理过程(熟知的垃圾回收),实际上是对上次请求过程中使用的变量调用unset()函数。
  一旦完成,PHP等待SAPI发起另一个文档请求或者一个关闭信号。在CGI和CLI SAPI的情况下,没有所谓的“下一个请求”,所以SAPI会立刻执行关闭流程。在关闭过程中,PHP又让每个扩展调用自己的模块关闭(MSHUTDOWN)函数,最后关闭自己的核心子系统。
  这个过程第一次听令人有些费解,但是一旦你深入到一个扩展的开发过程中,它就会逐渐的清晰起来。

------------------
内存分配
  为了避免写的很糟糕的扩展泄露内存,ZE以自己内部的方式来进行内存管理,通过用一个附加的标志来指明持久化。一个持久化分配的内存比单个页面请求存在的时间要长。一个非持久化分配的内存,相比之下,在请求结束的时候就会被释放,不管free函数是否被调用。例如用户空间变量,都是非持久化分配的内存,因为在请求结束之后这些变量都没有用了。
  一个扩展理论上可以依靠ZE在每个页面请求结束后自动释放非持久化的内存,但这是不被推荐的。在请求结束的时候,分配的内存不会被立即被回收,并且会持续一段时间,所以和那块内存关联的资源将不会被恰当的关闭,这是一个很糟的做法,因为如果不能适当的清理的话,这会产生混乱。就像你即将要看见的,确定所有分配的数据被恰当的清除了是非常的简单。

让我们把常规的内存分配函数(只应该当和内部库一起工作的时候才会用到)和PHP ZE中的持久化和非持久化内存分配函数进行一个对比。
Traditional ??? Non-Persistent ??? Persistent
malloc(count)
calloc(count, num) ??? emalloc(count)
ecalloc(count, num) ??? pemalloc(count, 1)*
pecalloc(count, num, 1)
strdup(str)
strndup(str, len) ??? estrdup(str)
estrndup(str, len) ??? pestrdup(str, 1)
pemalloc() & memcpy()
free(ptr) ??? efree(ptr) ??? pefree(ptr, 1)
realloc(ptr, newsize) ??? erealloc(ptr, newsize) ??? perealloc(ptr, newsize, 1)
malloc(count * num + extr)** ??? safe_emalloc(count, num, extr) ??? safe_pemalloc(count, num, extr)
* The pemalloc() family include a ‘persistent’ flag which allows them to behave like their non-persistent counterparts.
For example: emalloc(1234) is the same as pemalloc(1234, 0)
** safe_emalloc() and (in PHP 5) safe_pemalloc() perform an additional check to avoid integer overflows

----------------------
建立一个开发环境

  现在你已经掌握了一些关于PHP和ZE的工作原理,我估计你希望要深入进去,并且开始写些什么。无论如何在你能做之前,你需要收集一些必要的开发工具,并且建立一个满足自己目标的环境。

第一你需要PHP本身,以及构建PHP所需要的开发工具集合。如果你对于从源码编译PHP不熟悉,我建议你看看http://www.php.net/install.unix。(开发windows下的PHP扩展在以后的文章会介绍)。使用适合自己发行版的PHP二进制包是很诱人的,但是这些版本总是会忽略两个重要的
./configure
选项,这两个选项在开发过程中非常方便。第一个是--enable-debug。这个选项将会用附加符号信息来编译PHP所以,如果一个段错误发生,那么你将可以从PHP收集到一个核心dump信息,然后使用gdb来跟踪这个段错误是在哪里发生的,为什么会发生。另一个选项依赖于你将要进行扩展开发的PHP版本。在PHP4.3这个选项叫--enable-experimental-zts,在PHP5和以后的版本中叫--enable-maintainer-zts。这个选项将会让PHP思考在多线程环境中的行为,并且可以让你捕获常见的程序错误,这些错误在非线程