主页
软件技术
返回
PHP内核解密

对大多数用户来说,PHP已经足够剽悍了。只有在一些牛叉的企业级应用中PHP的瓶颈才会显现出来,运行速度达不到要求、缺少必要的功能等等。由于语言上的限制,有些功能可能压根儿没法用PHP实现(比如精确到纳秒级别的计时);维护一个每个脚本都需要使用的一个超大函数库也是一件令人头疼的事情(这句翻译的不太好,原文:inconveniences that arise when having to carry around a huge library of default code appended to every single script),所以我们必须要找到一种方法来克服这些障碍。一旦我们的应用有了这样的需求,是时候看看PHP内核源码啦~看看C语言是如何让PHP跑起来的。

扩展PHP,说得容易做得难。 而今,PHP已经是一个功能完善、源码达数兆的庞然大物了。想要对这样一个系统加点儿料,咱们得弄明白一些很基本的东西。我们采用边学边做的方法进行学习,也许这并不是最科学、最专业的方式,但是我确信这种方式是最有效果的,同时你会得到很多乐趣。我们接下来会看下最最简单的扩展是如何跑起来的,之后我们来看看Zend所提供的一些高级功能。当然我们也可以先对Zend的功能、设计、技巧分别进行学习、在动手实践之前学习各种理论,不用做任何hack。但是这样学院派的“好”方法是非常令人不爽的,既费时又费力,所以我们还是边学边做吧!

注意尽管这章我们要尽可能理解PHP内部各个部分的工作原理,我们也不可能覆盖到扩展PHP的方方面面。师傅领进门,学艺在个人。只有勤学苦“练”你才能完全了解它内部的各种机制,所以我们鼓励你多和源码打交道哦~亲~

Zend是神马?PHP又是神马?

Zend其实是指PHP语言的引擎,PHP的核心。PHP是指包裹这层核心的一个完整系统。乍一听这有点儿丈二和尚摸不着头脑,但是这并不复杂(往下看)。一个网络脚本解释器由如下三部分组成。

 

1、解释器解析输入代码,翻译并将其执行。

 

2、函数库实现语言的各个功能。

 

3、和服务器打交道的接口。

 

Zend完整地实现了第一部分,部分实现了第二和第三部分。这三部分共同组成了完整的PHP。Zend是语言核心,使用一些预定义函数构建了PHP的地基。PHP的各种模块才是使得它绽放万丈光芒的原因。

 

下面我们来看看PHP在哪些地方可以扩展,又是怎么做到的。

 

[扩展可行性分析]:

 

如上图所示,PHP可以在三个地方进行扩展:外部模块、内部模块、Zend引擎。接下来我们分别对这三种方法进行学习。

 

[外部模块]:

 

外部模块可以在脚本运行时由dl()函数进行加载。dl()函数将一个动态链接库从磁盘加载到内存,使得绑定在这个库上的函数得以被脚本调用。当脚本运行完毕时,外部模块被请出内存,释放其所占用的空间。这种方法有利有弊

 

 

外部模块不需要重新编译PHP

 

PHP在大小不发生变化的情况下扩展了新的功能。

 

 

每次执行脚本都需要加载,要知道磁盘IO是很慢地。

 

你的磁盘会被额外的文件占满。。。

 

注:每个需要使用外部模块的脚本都必须调用dl()函数指定所需模块,要么就必须在php.ini中注明需要加载的动态链接库(windows是.dll,linux和mac是.so)。

 

总结起来,外部模块适用于PHP的第三方产品,不常用的小扩展或者专为测试时使用。如果你要快速开发扩展,外部模块应该是你的首选。然而对于那些常用的、大型应用以及复杂代码来说,就是弊大于利了。

 

[内部模块]:

 

内部模块是那些和PHP一起进行编译,每次执行脚本时都会使用、加载的模块。它们所提供的函数每个脚本都可以直接调用。和外部模块一样,内部莫卡也是有利弊的。

 

利:

 

不用指定加载文件,功能都是内置的。

 

不会占用额外的空间,一切都已经在PHP二进制文件中了。

 

弊:

 

任何改动都需要重新编译PHP

 

PHP二进制文件会占用更多内存。

 

内部模块适用于那些相对不变的库、较高的性能要求或是项目中很多脚本需要调用且频繁使用的情况。重新编译PHP的不快很快就随着使用方便和飞快的速度所带来的享受而烟消云散。但是内部模块不适合应用在那些需要快速开发的小扩展上。

 

[Zend引擎]

 

当然,扩展也可以直接实现在Zend引擎内部。这种方法常用于修改PHP语言,使得它满足特殊需要。总的来说,我们应该尽量避免直接修改Zend引擎。要知道你在引擎里的任何改动都会导致你所使用的PHP语言无法兼容新版本。。。如果进行升级,它才不会检查你现在跑着的PHP有啥不同呢,它会直接覆盖你的代码,编译然后安装!因此这种方法真是不咋地,我们也不会在本书里讲到这种方法。(当然学习完这一系列内容,你应该有这样的本事,对PHP源码进行改动)

 

[源码结构]

 

在我们学习任何源码的时候,你都必须先对源码树有所了解,以便你能快速对PHP的文件进行划分。这是编写扩展所需的最基本能力。

 

php-src 

PHP中主要的.c文件和.h头文件;你可以在这里看到PHP全部API、宏等内容的定义

 

php-src/ext                             

用于存放动态链接库(模块)和内部模块;默认情况下,这个文件夹里的内容是PHP官方模块,已经内置到PHP源码中。并且从PHP4.0开始,你已经可以采用动态加载的方法编译安装它们。(通过--enable-shared)

 

php-src/main                         

用于存放PHP各种宏定义

 

php-src/pear                           

PHP扩展和应用库(PEAR),该文件夹存放着PEAR核心文件

 

php-src/sapi                           

各种和服务器交互的接口

 

TSRM                                     

Zend和PHP的安全线程资源管理器

 

ZendEngine2                         

用于存放Zend引擎;这里你可以看到全部的Zend API定义、宏定义等等

 

我们可不是在学习PHP源码,所以就不在此展开了。但是你应该看看以下几个文件:

 

php-src/main/php.h,该文件定义了大多数PHP宏和API

 

php-src/Zend/Zend.h,该文件定义了大多数Zend引擎的宏

 

php-src/Zend/zend_API.h,该文件定义了Zend引擎的API

 

同时你也应该看看这些文件,包括Zend executor,PHP初始化文件等等。看过这些文件之后试试找出各个文件和模块之间的依赖关系。它们之间有什么关系,如何相互使用的。在这个过程中你可以学到PHP官方的编码规范。为了扩展PHP,你应该尽快熟悉这种规范。

 

[扩展规范]

 

Zend有着各种命名规范;为了避免和Zend标准相冲突,你应该遵守如下几条规范。

 

[宏定义]

 

对于每项重要的工作(比如分配内存、动态加载链接库啥的),使用Zend自己预定义的宏是相当方便的。下面的章节我们来看看一些非常基本的函数、结构体和宏。记住你可以在zend.h和zend_API.h中找到这些定义。我们强烈建议您看过本章后仔细阅读这两个文件。(当然你可以现在就读,尽管有些内容你可能并不太明白)

 

[内存管理]

 

资源管理从来都是非常重要的环节,特别是在服务器端软件。最重要的资源之一是内存,所以需要额外注意那些用于管理内存的程序。在Zend中,内存管理被单独抽象出来,你应该特别注意这一点,因为Zend可以完全控制内存分配。Zend可以判断一块内存是否正被使用,自动释放不再使用的块以及空引用指向的块,从而有可能造成内存泄露。你可以使用如下函数管理内存。

emalloc() 代替 malloc().

 

efree()   代替 free().

 

estrdup() 代替 strdup().

 

estrndup() 代替 strndup(). 比 estrdup() 快并且更安全,在已知字符串字节长度的情况下,你应该用这个函数去复制

 

ecalloc() 代替 calloc().

 

erealloc()  代替 realloc().

 

emalloc(), estrdup(), estrndup(), ecalloc(), 和 erealloc() 用于分配内存。 efree() 释放这些被分配的内存。由E开头的函数分配的内存,会在当前进程执行脚本结束后自动释放。

 

注意:你也可以用malloc和free去分配释放内存,但是这些内存是不会随脚本结束而释放的,所以有可能会造成内存泄露。

 

Zend还提供了一个线程安全的资源管理器,以便更好地支持多线程服务器。它要求你为全局变量分配好本地内存,供当前线程使用。因为本文撰写时线程安全资源管理器还木有写好,所以我们在此就不做讨论了。。。

 

[文件夹和文件函数]

 

你可以在编写与文件或者文件夹相关的程序中使用如下函数。它们用起来其实和C语言中的亲戚没啥区别,只是额外在线程级别提供了虚拟路径。

 

Zend 函数                                C语言函数

 

V_GETCWD()                          getcwd()

 

V_FOPEN()                              fopen()

 

V_OPEN()                                open()

 

V_CHDIR()                              chdir()

 

V_GETWD()                            getwd()

 

V_CHDIR_FILE()       以文件路径为参数,切换当前工作路径到那个路径。(V_CHDIR_FILE("/") = cd /)

 

V_STAT()                                stat()

 

V_LSTAT()                              lstat()

 

[字符串操作]

 

字符串操作起来和其它值,如整数、布尔变量有些不同,你不用分配内存去存储字符串。如果你想在函数中返回一个字符串值,可以在符号表里插入一个字符串变量,或者使用其它相似的办法,总之你得保证分配给字符串的内存在之前已经分配过了,记得使用e开头的函数哦~亲~(可能现在你看不懂介是啥意思,没事儿,有个印象就成,一会儿会解释的)

 

[复杂类型]

 

复杂数据类型如数组、对象需要特殊对待。Zend对此提供了一个简单API——将它们存储于哈西表中。

 

说说数组和对象的这个哈西表

 

注:为了降低复杂性,我们先使用一些简单的数据类型,如整数。稍后我们再看更高级的数据类型如何使用。

 

[PHP自建系统]

 

PHP4提供了一个非常灵活的自建系统。所有的模块都被放置于ext文佳夹中的子文件夹中(一般文件夹名就是模块名)。除了模块源码之外,每个模块都含有一个叫做config.m4的文件,用于配置模块。

 

所有的stub文件(万分抱歉我实在不知道这个词是啥意思,中文有对应翻译吗?)由ext文件夹中的一个叫ext_skel的脚本都是自动生成的,它将你所想创建的模块的名称作为参数,创建出同名文件夹,并将stub文件放置于文件夹中。

 

过程是酱紫的:

 

:~/cvs/php4/ext:> ./ext_skel --extname=my_module

 

Creating directory my_module

 

Creating basic files: config.m4 .cvsignore my_module.c php_my_module.h CREDITS EXPERIMENTAL tests/001.phpt my_module.php [done].

 

为了使用新扩展,你需要执行如下命令

 

1.  $ cd ..

 

2.  $ vi ext/my_module/config.m4

 

3.  $ ./buildconf

 

4.  $ ./configure --[with|enable]-my_module

 

5.  $ make

 

6.  $ ./php -f ext/my_module/my_module.php

 

7.  $ vi ext/my_module/my_module.c

 

8.  $ make

 

反复执行步骤3-6直到你对/ext/my_module/config.m4文件满意为止,步骤6使得你的扩展直接被编译进PHP。最后你可以通过反复修改并执行最后两步达到最佳扩展效果

 

上述命令创建了之前提到的文件。要想自动配置新扩展,你需要执行buildconf,通过搜索ext文件夹下的全部config.m4文件,重新生成配置脚本。

 

以下是一个默认的config.m4文件,有点儿小复杂:

 

例1:默认的config.m4文件

 

dnl $Id: build.xml 297078 2010-03-29 16:25:51Z degeberg $

 

dnl config.m4 for extension my_module

 

dnl Comments in this file start with the string 'dnl'.

 

dnl Remove where necessary. This file will not work

 

dnl without editing.

 

dnl If your extension references something external, use with:

 

dnl PHP_ARG_WITH(my_module, for my_module support,

 

dnl Make sure that the comment is aligned:

 

dnl [  --with-my_module            Include my_module support])

 

dnl Otherwise use enable:

 

dnl PHP_ARG_ENABLE(my_module, whether to enable my_module support,

 

dnl Make sure that the comment is aligned:

 

dnl [  --enable-my_module          Enable my_module support])

 

if test "$PHP_MY_MODULE" != "no"; then

 

dnl Write more examples of tests here...

 

dnl # --with-my_module -> check with-path

 

dnl SEARCH_PATH="/usr/local /usr"    # you might want to change this

 

dnl SEARCH_FOR="/include/my_module.h"  # you most likely want to change this

 

dnl if test -r $PHP_MY_MODULE/; then # path given as parameter

 

dnl  MY_MODULE_DIR=$PHP_MY_MODULE

 

dnl else # search default path list

 

dnl  AC_MSG_CHECKING([for my_module files in default path])

 

dnl  for i in $SEARCH_PATH ; do

 

dnl    if test -r $i/$SEARCH_FOR; then

 

dnl      MY_module_DIR=$i

 

dnl      AC_MSG_RESULT(found in $i)

 

dnl    fi

 

dnl  done

 

dnl fi

 

dnl

 

dnl if test -z "$MY_module_DIR"; then

 

dnl  AC_MSG_RESULT([not found])

 

dnl  AC_MSG_ERROR([Please reinstall the my_module distribution])

 

dnl fi

 

dnl # --with-my_module -> add include path

 

dnl PHP_ADD_INCLUDE($MY_MODULE_DIR/include)

 

dnl # --with-my_module -> chech for lib and symbol presence

 

dnl LIBNAME=my_module # you may want to change this

 

dnl LIBSYMBOL=my_module # you most likely want to change this

 

dnl PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,

 

dnl [

 

dnl  PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $MY_MODULE_DIR/lib, MY_MODULE_SHARED_LIBADD)

 

dnl  AC_DEFINE(HAVE_MY_MODULELIB,1,[ ])

 

dnl ],[

 

dnl  AC_MSG_ERROR([wrong my_module lib version or lib not found])

 

dnl ],[

 

dnl  -L$MY_module_DIR/lib -lm -ldl

 

dnl ])

 

dnl

 

dnl PHP_SUBST(MY_module_SHARED_LIBADD)

 

PHP_NEW_EXTENSION(my_module, my_module.c, $ext_shared)

 

fi

 

如果你不熟悉M4文件,扎一看会有点儿晕,但其实它还是挺容易理解的。

 

注意:dnl开头的行是注释~

 

在配置过程中,config.m4文件主要负责生成configure命令的控制选项。也就是说它也要负责检测需要用到的外部依赖,做些基本的配置工作。

 

默认会在configure脚本中创建两个配置选项:--with-my_module 和 --enable-my_module。第一个是在你需要用到外部文件的时候使用的(比如 --with-apache 代表了使用Apache目录中的文件)。第二个顾名思义,可以用它控制是否让你的模块可用。你不能同时使用这两个选项,选了--with-my_module你就不能用第二个,反之亦然。

 

默认情况下,ext_skel脚本生成的config.m4文件同时含有这两个指令,并默认将你的扩展设置为可用。通过一个名为PHP_EXTENSION的宏你的扩展被默认设为可用。如果你要改变默认设置,将你的模块直接添加到PHP可执行文件中(在配置PHP时显示设置--enable-my_module或者--with-my_module),请将$PHP_MY_MODULE测试设置为“是”

 

if test "$PHP_MY_MODULE" == "yes"; then dnl

 

Action.. PHP_EXTENSION(my_module, $ext_shared)

 

fi

 

如果这样你必须在每次重新配置编译PHP的时候使用 --enable-my_module。

 

注:务必记得在改动config.m4之后使用buildconf

 

稍后我们会学习更多M4文件中的宏。现在我们只用这个最简单的默认配置就可以了。

 

[创建模块]

 

我们将从创建最简单模块的开始,它的唯一功能就是接收一个整形参数并将其返回。看下源码。

 

例2:简单模块

 

/* include standard header */

 

#include "php.h"

 

/* declaration of functions to be exported */

 

ZEND_FUNCTION(first_module);

 

/* compiled function list so Zend knows what's in this module */

 

zend_function_entry firstmod_functions[] =

 

{

 

ZEND_FE(first_module, NULL)

 

{NULL, NULL, NULL}

 

};

 

/* compiled module information */

 

zend_module_entry firstmod_module_entry =

 

{

 

STANDARD_MODULE_HEADER,

 

"First Module",

 

firstmod_functions,

 

NULL,

 

NULL,

 

NULL,

 

NULL,

 

NULL,

 

NO_VERSION_YET,

 

STANDARD_MODULE_PROPERTIES

 

};

 

/* implement standard "stub" routine to introduce ourselves to Zend */

 

#if COMPILE_DL_FIRST_MODULE

 

ZEND_GET_MODULE(firstmod)

 

#endif

 

/* implement function that is meant to be made available to PHP */

 

ZEND_function(first_module)

 

{

 

long parameter;

 

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", ¶meter) == FAILURE) {

 

return;

 

}

 

RETURN_LONG(parameter);

 

}

 

麻雀虽小,五脏俱全!不管你信步信,这已经是一个完整的模块了。我们稍后将对其进行详细的讲解,咱们先来看看编译流程。

 

注:源码使用了些PHP 4.1.0之后的API,所以使用PHP 4.0.X的童鞋有可能无法编译。

 

[编译模块]

 

有两种编译模块的方法:

 

1、使用ext文件夹提供的make机制,可以动态加载模块

 

2、手动编译

 

我们强烈推荐第一种方法。从PHP4.0开始,这一复杂的编译过程成为了标准。不幸的是,它复杂到你一开始可能无法理解它是如何工作的。我们稍后再对它做一个详细介绍吧,现在我们用默认配置文件把模块编译出来就成了。

 

第二种方法适合那些没有完整PHP源码的小盆友。虽然这个方法不怎么常用,但是为了保证完整性,我们也会对这种方法进行介绍。

 

[使用make]:

 

使用标准机制进行编译:将模块所在的文件夹复制到PHP源码的ext文件夹下。运行buildconf,将新模块的配置项更新到configure脚本。

 

运行完buildconf之后,使用configure --help你将会看到如下内容

 

--enable-array_experiments  BOOK: Enables array experiments

 

--enable-call_userland      BOOK: Enables userland module

 

--enable-cross_conversion    BOOK: Enables cross-conversion module

 

--enable-first_module        BOOK: Enables first module

 

--enable-infoprint          BOOK: Enables infoprint module

 

--enable-reference_test      BOOK: Enables reference test module

 

--enable-resource_test      BOOK: Enables resource test module

 

--enable-variable_creation  BOOK: Enables variable-creation module

 

你可以使用--enable-first_module 或 --enable-first_module=yes.将模块置为可用

 

译者注:在这里我没有使用buildconf,而是用了phpize。。。windows上是没有这个东西的,所以如果在win上还是用这个标准的方法进行开发吧。还有一点需要特别注意的是如果你也使用phpize,一定要在.c文件开头添加

 

#ifdef HAVE_CONFIG_H

 

#include "config.h"

 

#endif

 

这三行。否则你会遇到

 

PHP Warning:  PHP Startup: Invalid library (maybe not a PHP library) 'xxx.so' in Unknown on line 0

 

[手动编译]:

 

手动编译只需要运行如下命令:

 

Action                                      Command

 

Compiling                                cc -fpic -DCOMPILE_DL_FIRST_MODULE=1 -I/usr/local/include -I. -I.. -I../Zend -c -o first_module.o first_module.c

 

Linking                                    cc -shared -L/usr/local/lib -rdynamic -o first_module.so first_module.o

 

译者注:我在链接的时候使用了cc -bundle -flat_namespace -undefined suppress -o first_module.so first_module.o

 

如果你在链接的时候出现了问题,只需要用上面这条命令就可以了。因为使用了-undefined suppress选项,忽略了链接时找不到的外部文件,不用担心,因为你在命令行运行php命令时,libphp5.so会加载到内存,几乎一切你需要用到的函数都在内存里了,所以运行是木有任何问题的。

 

编译指令使编译器编译时生成不定地址代码。(显示添加-fpic)而-DCOMPILE_DL_FIRST_MODULE则告诉编译器生成动态加载模块。而后指定了一系列include路径用于编译源码。

 

注:所有的相对路径都是基于ext文件夹的。如果你是从别的文件夹编译的,记得改变路径。译者的编译命令:cc -fPIC -DCOMPILE_DL_FIRST_MODULE=1 -I/usr/local/include -I ../../../ -I ../../../main -I ../../../TSRM/ -I ../../../Zend -c -o first_module.o first_mod.c

 

相对路径在ext/hello/src_first_mod

 

同时链接指令也指出链接器应该将模块作为动态模块链接。

 

注:由于将模块作为静态模块直接编译进php的二进制文件中的指令会非常非常非常长所以我们就不在此做介绍了,感兴趣的童鞋可以自学~~



一级建造师考试《市政公用工程》精华辅导(106)
常雅飞:绿色建筑需“不拘小节”
2015年一级建造师考试《法规及相关知识》考点分析(46)
智慧城市是城市信息化向高端发展
2008年-2015年二级建造师执业资格考试试卷及答案汇总表
2015年一级建造师考试《法规及相关知识》辅导资料(43)
根据《仲裁法》,仲裁裁决可能被撤销的情形有
探讨油气勘探项目管理基本框架和相关问题
信息发布:名易软件http://www.myidp.net