经过前面几章的学习,我们对PHP的整个底层实现、执行有了深入的认识,本章我们来学习下扩展相关内容。我们在日常的PHP开发中,已经不知不觉使用了很多的扩展,如常见的时间格式化、JSON编码和解码、数组操作、数据库操作等,这些都是通过扩展来实现的。PHP的扩展按照归属可以分为四大类,分别如下:
- 核心扩展库:其实属于PHP内核的一部分,这些扩展不能通过编译选项排除掉,如array系列、date系列、string系列、反射等。
- 绑定的扩展库:常见的iconv、ftp、gd、json等都属于绑定的扩展库。
- 外部扩展库:常见的curl、dom、mysqli等都属于绑定的外部扩展库,这些扩展库已经包含在PHP源码中但是需要编译它们才能使用,并且可能需要额外的扩展库。
- PECL扩展库:常见的有Memcached、Yaf、Xhprof等,这些扩展来自PECL,它们可能需要额外的库。
PHP的扩展还可以分为两种,一种是PHP扩展,另外一种是Zend扩展。在php_module_start加载扩展时,通过extension=.so加载的扩展称为PHP扩展;通过zend_extension=.so加载的扩展称为Zend扩展。在某些文档中,根据扩展在PHP源码中的定义,把PHP扩展称为模块(PHP源码中有module关键字),把Zend扩展称为扩展(源码中有extension关键字)。
除了加载方式不同,两种扩展最根本的区别在于实现的不同。PHP扩展需要实现一个名为zend_module_entry的结构体,而Zend扩展需要实现一个名为zend_extension的结构体。扩展xdebug既实现了zend_module_entry,也实现了zend_extension,所以说其实这个扩展有点怪,既是PHP扩展,也是Zend扩展。
至于加载顺序的不同,我们后面会介绍。现在我们先来介绍扩展的实现原理。
14.1 扩展的实现原理
无论是PHP扩展还是Zend扩展,它们实现的基本原理都是开发者按照扩展规范和API,实现自己的功能,然后要么以静态编译方式编译到PHP的可执行文件,要么以动态编译方式生成动态链接库.so文件。加载扩展时,PHP将动态链接库文件加载到内存,校验其符合规范后,PHP即可以使用此扩展。
那么这里提到的库是什么呢?库可以看作可复用代码的二进制形式。在计算机世界中,每个程序都要依赖很多基础的底层库。库分为两种:静态链接库和动态链接库。在编译生成可执行程序时,一起被打包到可执行文件的库称为静态链接库,Linux下一般以.a为扩展名(Windows下为.lib);而生成可执行文件时并未被打包,在运行时才被载入的库,称为动态链接库,Linux下一般以.so为扩展名(Windows下为.dll)。动态链接库使用起来比静态链接库稍微麻烦,但有着非常明显的优势。
- 相对于静态链接库,使用动态链接库可以有效地缩小程序体积,节省空间,在同一个运行环境下,不同的程序可以调用相同的库。
- 程序更新时,使用了静态链接库的程序需要重新编译整个程序,用户也需要重新下载安装完整的程序,而使用了动态链接库的程序可以只更新库,实现增量更新。
- 有助于节省内存。当我们需要某个扩展时,才将其加载到内存中。
- 有助于资源共享。这里讲的资源共享,是指在多个进程中实现共享。下面我们来实现一个简单的动态链接库libhelloworld.so并调用:
#include <stdio.h>
void helloworld()
{
printf("hello, world! \n");
}
代码很简单,只有一个函数——helloworld,调用这个函数,会输出字符串“hello,world!”。我们将其编译生成动态链接库:
$ gcc -shared -fPIC -o libhelloworld.so helloworld.c
$ ll
-rw-r--r--1 vagrant vagrant 71 1月 25 14:15 helloworld.c
-rwxr-xr-x 1 vagrant vagrant 7.9K 1月 25 14:30 libhelloworld.so
现在我们显式加载此动态链接库:
#include <stdio.h>
#include <dlfcn.h>
int main(int argc, char* argv[])
{
void* handle = dlopen("./libhelloworld.so", RTLD_LAZY);
char* error = dlerror();
if(! handle || error)
{
printf("load so error! \n");
return 1;
}
void (*func)() = dlsym(handle, "helloworld");
if(! func)
{
printf("load func error! \n");
dlclose(handle);
return 1;
}
func();
dlclose(handle);
return 0;
}
编译、链接此代码生成可执行程序:
$ gcc test.c -L. -lhelloworld -ldl
$ ll
-rwxr-xr-x 1 vagrant vagrant 8.6K 1月 25 14:34 a.out
-rw-r--r--1 vagrant vagrant 71 1月 25 14:15 helloworld.c
-rwxr-xr-x 1 vagrant vagrant 7.9K 1月 25 14:30 libhelloworld.so
-rw-r--r--1 vagrant vagrant 459 1月 25 14:34 test.c
$ ./a.out
Hello, world!
可以看到已成功生成可执行文件a.out,运行后输出字符串“hello, world!”。由此可见,动态链接库libhelloworld.so中的函数执行成功。
仔细研究下这段代码,会发现它一共使用了4个函数来加载动态链接库并执行其中的函数,它们分别是dlopen、dlerror、dlsym、dlclose,分别用来加载动态链接库、获得相关错误信息、获得函数地址、关闭动态链接库。
PHP的扩展实现原理和这段代码极其相似,也是用这4个函数完成了扩展的加载和函数的调用。当在我们在PHP程序中动态通过dl函数或通过php.ini来动态加载扩展时,PHP会从extension_dir配置项指定的目录加载扩展,这个目录默认为/lib/php/extensions/--ZEND_MODULE_API_NO,如/usr/local/php/lib/php/extensions/debug-non-zts-20160303或/usr/local/php/lib/php/extensions/no-debug-zts-20160303。
14.2 PHP扩展
14.2.1 扩展的实现
前面已经讲了,PHP扩展需要实现一个结构体——zend_module_entry,现在我们来研究下这个结构体。
struct _zend_module_entry {
unsigned short size;
unsigned int zend_api;
unsigned char zend_debug;
unsigned char zts;
const struct _zend_ini_entry *ini_entry;
const struct _zend_module_dep *deps;
const char *name;
const struct _zend_function_entry *functions;
int (*module_startup_func)(INIT_FUNC_ARGS);
int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);
int (*request_startup_func)(INIT_FUNC_ARGS);
int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);
void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);
const char *version;
size_t globals_size;
void* globals_ptr;
void (*globals_ctor)(void *global);
void (*globals_dtor)(void *global);
int (*post_deactivate_func)(void);
int module_started;
unsigned char type;
void *handle;
int module_number;
const char *build_id;
};
主要字段说明如下。
- size:此结构体的大小,即sizeof(struct _zend_module_entry)。
- zend_api:PHP扩展版本号。
- zend_debug:是否开启debug模式。
- zts:线程是否安全。
- ini_entry:php.ini相关,详情请看第8章。
- name:扩展名称,不得与其他扩展名相同。
- functions:扩展提供的内部函数数组。
- module_startup_func:模块初始化阶段回调的钩子函数。
- module_shutdown_func:模块关闭阶段回调的钩子函数。
- request_startup_func:请求初始化阶段回调的钩子函数。
- request_shutdown_func:请求关闭阶段回调的钩子函数。
- info_func:被php_info函数调用的展示扩展信息的函数。
- version:扩展的版本号。
如果当前扩展是以动态链接库的方式来使用的,那么扩展除了要实现了struct_zend_module_entry结构外,还需要实现get_module函数来让内核获得当前扩展结构。此函数的实现有统一的规范,通过宏来实现:
#define ZEND_GET_MODULE(name) \
BEGIN_EXTERN_C()\
ZEND_DLEXPORT zend_module_entry *get_module(void) { return &name##_module_
entry; }\
END_EXTERN_C()
由此可以看出,实现的函数名为get_module,如果此扩展名为***,那么定义扩展的变量名必须为***_module_entry。例如,JSON扩展为json_module_entry。
从8.4节中知道,解析php.ini时,PHP扩展和Zend扩展相关配置都会被加载到全局变量extension_lists中,PHP扩展保存在extension_lists.functions中,而Zend扩展保存在extension_lists.engine中。
在PHP_MINIT阶段,通过php_ini_register_extensions函数完成扩展的注册:
void php_ini_register_extensions(void)
{
// Zend扩展
zend_llist_apply(&extension_lists.engine, php_load_zend_extension_cb);
// PHP扩展
zend_llist_apply(&extension_lists.functions, php_load_php_extension_cb);
zend_llist_destroy(&extension_lists.engine);
zend_llist_destroy(&extension_lists.functions);
}
在这个函数中,依次遍历extension_lists.engine和extension_lists.functions,调用php_load_php_extension_cb函数完成对PHP扩展的加载,调用php_load_zend_extension_cb函数完成对Zend扩展的加载:
static void php_load_php_extension_cb(void *arg)
{
#ifdef HAVE_LIBDL
php_load_extension(*((char **) arg), MODULE_PERSISTENT, 0);
#endif
}
由源码可知,PHP扩展加载的关键步骤在php_load_extension中完成,现在仔细研究下这个函数:
- 获取配置文件定义的扩展目录extension_dir。
- 调用dlopen打开对应的动态链接库.so文件。
- 调用dlsym找到get_module函数的地址,通过get_module方法获得此扩展的struct_zend_module_entry结构。
- zend api版本校验。
- 检查扩展依赖,没有错误的时候将扩展添加到全局变量module_registry中;如果存在内部函数,则将这些函数注册到全局变量EG(function_table)中。
至此完成了PHP扩展的加载。
14.2.2 JSON扩展
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。JSON采用完全独立于语言的文本格式,各类语言都有对其实现,包括C、C++、C#、Java、JavaScript、Perl、Python等,因此JSON格式是比较理想的数据交换语言。
对象是一个无序的键/值对集合。一个对象以“{”(左括号)开始,从“}”(右括号)结束。每个“名称”后跟一个“:”(冒号),名称/值对之间使用“,”(逗号)分隔。
数组是值(value)的有序集合。一个数组以“[”(左中括号)开始,以“]”(右中括号)结束。值之间使用“,”(逗号)分隔,值可以是双引号括起来的字符串(string)、数值(number)、true、false、null、对象(object)或者数组(array)。这些结构可以嵌套。
字符串(string)是由双引号括起来的任意数量unicode字符的集合,使用“\”(反斜杠)转义。一个字符(character)即一个单独的字符串(character string)。
除去未曾使用的八进制与十六进制格式,数值(number)与C或者Java语言的数值非常相似。我们来看下PHP中的JSON扩展,其扩展定义如下:
zend_module_entry json_module_entry = {
STANDARD_MODULE_HEADER,
"json",
json_functions,
PHP_MINIT(json),
NULL,
NULL,
NULL,
PHP_MINFO(json),
PHP_JSON_VERSION,
PHP_MODULE_GLOBALS(json),
PHP_GINIT(json),
NULL,
NULL,
STANDARD_MODULE_PROPERTIES_EX
};
此结构体的前6个字段是用宏STANDARD_MODULE_HEADER来定义的。多读几个PHP自带的PHP扩展源码,会发现绝大部分扩展用这个宏来定义前6个字段,实际结构如下:
sizeof(zend_module_entry), ZEND_MODULE_API_NO, ZEND_DEBUG, USING_ZTS, NULL, NULL
当前JSON扩展实际展开后如下(注意,部分参数根据实际的编译参数可能有所不同,如是否开启debug):
sizeof(zend_module_entry), 20160303, 0, 0, NULL, NULL
STANDARD_MODULE_HEADER之后是扩展名“json”,然后是扩展JOSN提供的内部函数数组json_functions:
static const zend_function_entry json_functions[] = {
PHP_FE(json_encode, arginfo_json_encode)
PHP_FE(json_decode, arginfo_json_decode)
PHP_FE(json_last_error, arginfo_json_last_error)
PHP_FE(json_last_error_msg, arginfo_json_last_error_msg)
PHP_FE_END
};
可以看出,JOSN扩展提供了4个内部函数,其各自实现不在此书的讨论范围之内。
结构体的后面是扩展JSON提供的模块初始化阶段回调的钩子函数PHP_MINFO_FUNCTION(json) 以及扩展版本等相关信息。
14.3 Zend扩展
14.3.1 Zend扩展的实现
Zend扩展必须要实现的结构体为zend_extension:
struct _zend_extension {
char *name;
char *version;
char *author;
char *URL;
char *copyright;
startup_func_t startup;
shutdown_func_t shutdown;
activate_func_t activate;
deactivate_func_t deactivate;
message_handler_func_t message_handler;
op_array_handler_func_t op_array_handler;
statement_handler_func_t statement_handler;
fcall_begin_handler_func_t fcall_begin_handler;
fcall_end_handler_func_t fcall_end_handler;
op_array_ctor_func_t op_array_ctor;
op_array_dtor_func_t op_array_dtor;
int (*api_no_check)(int api_no);
int (*build_id_check)(const char* build_id);
op_array_persist_calc_func_t op_array_persist_calc;
op_array_persist_func_t op_array_persist;
void *reserved5;
void *reserved6;
void *reserved7;
void *reserved8;
DL_HANDLE handle;
int resource_number;
};
主要字段说明如下。
- name:扩展名称。
- version:扩展版本号。
- author:作者。
- URL:扩展官方主页。
- copyright:版本声明。
- startup:扩展启动钩子函数。
- shutdown:扩展关闭钩子函数。
- activate:请求初始化阶段钩子函数。
- deactivate:请求结束阶段钩子函数。
前文我们提到解析php.ini时会把Zend扩展保存在extension_lists.engine中,在通过php_ini_register_extensions注册扩展时会先调用php_load_zend_extension_cb完成对Zend扩展的加载:
static void php_load_zend_extension_cb(void *arg)
{
……
if (IS_ABSOLUTE_PATH(filename, length)) {
zend_load_extension(filename);
} else {
……
zend_load_extension(libpath);
}
}
具体的加载流程是在zend_load_extension函数中实现的,其实现和PHP扩展的加载php_load_extension大同小异,不同之处是,内核最终将Zend扩展加载到全局变量zend_extensions中。
14.3.2 opcache扩展
通过对前几章的学习,我们了解了PHP的执行原理,简单地讲,就是PHP内核把PHP代码编译成opcode,然后再来执行opcode。这个过程看上去挺简单,没有可以优化的地方,其实不然。在PHP代码没有变化的情况下,内核生成的opcode是不会变的,那么我们可以把生成的opcode缓存到文件或内存中,这样可以避免重复执行生成opcode的过程,提高PHP的运行效率。
有人会想,既然如此,为何PHP不能像Java一样,把编译生成的opcode部署到线上,让PHP直接执行opcode,这样就不存在刚才说的重复执行生成opcode的过程了。这的确是一个好办法,不过PHP虽然是跨平台的,但是与Java的跨平台不同,PHP是语言跨平台,生成的opcode并不能跨平台执行,而Java生成的bytecode是可跨平台执行的,另外热部署是PHP的优势之一,直接部署opcode有悖于这一特性。所以PHP开发者最终选择了使用缓存opcode这一功能来优化PHP的执行。
现在我们来深入了解下opcache的运行原理,先来研究下此扩展实现的zend_extension结构。
ZEND_EXT_API zend_extension zend_extension_entry = {
ACCELERATOR_PRODUCT_NAME, /* name */
PHP_VERSION, /* version */
"Zend Technologies", /* author */
"http://www.zend.com/", /* URL */
"Copyright (c) 1999-2017", /* copyright */
accel_startup, /* startup */
NULL, /* shutdown */
accel_activate, /* per-script activation */
accel_deactivate, /* per-script deactivation */
NULL, /* message handler */
NULL, /* op_array handler */
NULL, /* extended statement handler */
NULL, /* extended fcall begin handler */
NULL, /* extended fcall end handler */
NULL, /* op_array ctor */
NULL, /* op_array dtor */
STANDARD_ZEND_EXTENSION_PROPERTIES
};
opcache的module startup方法为accel_startup。下面我们研究下accel_startup方法中的操作。
static int accel_startup(zend_extension *extension){
……
accel_globals_ctor(&accel_globals);
……
if (start_accel_module() == FAILURE) {
}
if (accel_find_sapi() == FAILURE) {
……
}
switch (zend_shared_alloc_startup(ZCG(accel_directives).memory_consumption))
{
case ALLOC_SUCCESS:
zend_accel_init_shm();
break;
case ALLOC_FAILURE:
……
case SUCCESSFULLY_REATTACHED:
……
case FAILED_REATTACHED:
……
}
……
}
主要操作步骤如下。
- 初始化一个全局变量accel_globals。
- 注册内部模块accel。
- 校验opcache是否支持当前的sapi。
- 分配并初始化共享内存。
- 初始化全局变量accel_shared_globals指向的结构体zend_accel_shared_globals。
- hook相关函数。
我们分别看下每一步都具体做了些什么。
第一步,初始化一个全局变量accel_globals,将其置为0,全局变量accel_globals类型为zend_accel_globals,其结构如下:
typedef struct _zend_accel_globals {
/* copy of CG(function_table) used for compilation scripts into cache */
/* initially it contains only internal functions */
HashTable function_table;
int internal_functions_count;
int counted; /* the process uses shared memory */
zend_bool enabled;
zend_bool locked; /* thread obtained exclusive lock */
HashTable bind_hash; /* prototype and zval lookup table */
zend_accel_directives accel_directives;
zend_string *cwd; /* current working directory or NULL */
zend_string *include_path; /* current value of "include_path" directive */
char include_path_key[32]; /* key of current "include_path" */
char cwd_key[32]; /* key of current working directory */
int include_path_key_len;
int include_path_check;
int cwd_key_len;
int cwd_check;
int auto_globals_mask;
time_t request_time;
time_t last_restart_time; /* used to synchronize SHM and in-process
caches */
char system_id[32];
HashTable xlat_table;
#ifndef ZEND_WIN32
zend_ulong root_hash;
#endif
/* preallocated shared-memory block to save current script */
void *mem;
void *arena_mem;
zend_persistent_script *current_persistent_script;
/* cache to save hash lookup on the same INCLUDE opcode */
const zend_op *cache_opline;
zend_persistent_script *cache_persistent_script;
/* preallocated buffer for keys */
int key_len;
char key[MAXPATHLEN * 8];
} zend_accel_globals;
主要字段说明如下。
-
function_table:注释很清晰,即把CG(function_table)复制到这,初始化时只把内部函数复制到这。
-
internal_functions_count:内部函数的个数。
-
counted:进程使用的共享内存大小。
-
enabled:Opcahce是否可用。
-
locked:线程是否获得了互斥锁。
-
bind_hash:HashTable,初始化时分配了10个元素。
-
accel_directives:opcache相关的配置,在函数opcache_get_configuration中完成初始化,主要存储php.ini中关于opcache的配置信息。
-
cwd:进程当前的工作路径。
-
include_path:当前文件的include_path。
-
auto_globals_mask:每次请求,都会在函数accel_activate中初始化为0,用来标记当次请求使用的全局变量。全局变量有以下4种。
static const struct jit_auto_global_info { const char *name; size_t len; } jit_auto_globals_info[] = { { "_SERVER", sizeof("_SERVER")-1}, { "_ENV", sizeof("_ENV")-1}, { "_REQUEST", sizeof("_REQUEST")-1}, { "GLOBALS", sizeof("GLOBALS")-1}, }; -
request_time:每次请求,都会在函数accel_activate中将其初始化为(time_t)sapi_get_request_time,记录此次请求的开始时间。
-
last_restart_time:在扩展初始化,或每次请求的初始化中进行判断,如果不等于共享内存中的last_restart_time,会将其置为共享内存中的last_restart_time。
-
system_id:存储一串特殊字符串的md5值,与缓存中的md5值进行比较,如果不同,说明有异常。
-
current_persistent_script:指向此次从cache获得的相关信息,指向一个结构体zend_persistent_script。
zend_persistent_script结构体如下:
typedef struct _zend_persistent_script {
zend_script script;
zend_long compiler_halt_offset; /* position of __HALT_COMPILER or -1 */
int ping_auto_globals_mask; /* which autoglobals are used by the
script */
accel_time_t timestamp; /* the script modification time */
zend_bool corrupted;
zend_bool is_phar;
void *mem; /* shared memory area used by script
structures */
size_t size; /* size of used shared memory */
void *arena_mem; /* part that should be copied into
process */
size_t arena_size;
/* All entries that shouldn't be counted in the ADLER32
* checksum must be declared in this struct
*/
struct zend_persistent_script_dynamic_members {
time_t last_used;
zend_ulong hits;
unsigned int memory_consumption;
unsigned int checksum;
time_t revalidate;
} dynamic_members;
} zend_persistent_script;
主要字段说明如下。
-
script:zend_script结构体,存储文件名、oparray、相关方法和类。
-
timestamp:文件的更新时间。
-
is_phar:文件是phar类型,判断方法如下。
new_persistent_script->is_phar = new_persistent_script->script.filename && strstr(ZSTR_VAL(new_persistent_script->script.filename), ".phar") && !strstr(ZSTR_VAL(new_persistent_script->script.filename), "://"); -
mem:指向所在共享内存的位置。
-
size:初始化为结构体zend_persistent_script的大小。
第二步,注册内部模块accel。opcache的相关函数都注册在这个内部模块下:
static zend_function_entry accel_functions[] = {
/* User functions */
ZEND_FE(opcache_reset, arginfo_opcache_none)
ZEND_FE(opcache_invalidate, arginfo_opcache_invalidate)
ZEND_FE(opcache_compile_file, arginfo_opcache_compile_file)
ZEND_FE(opcache_is_script_cached, arginfo_opcache_is_script_cached)
/* Private functions */
ZEND_FE(opcache_get_configuration, arginfo_opcache_none)
ZEND_FE(opcache_get_status, arginfo_opcache_get_status)
ZEND_FE_END
};
第三步,校验opcache是否支持当前的sapi。opcache只支持下面几种sapi。
static const char *supported_sapis[] = {
"apache",
"fastcgi",
"cli-server",
"cgi-fcgi",
"fpm-fcgi",
"isapi",
"apache2filter",
"apache2handler",
"litespeed",
"uwsgi",
NULL
};
看上去opcache不支持CLI模式,其实不然。如果想要opcache支持CLI,需要单独在php.ini中设置。
opcache.enable_cli = 1
第四步,分配并初始化共享内存。此步在函数zend_shared_alloc_startup中进行,共享内存的大小在php.ini中配置:
opcache.memory_consumption = 64
配置中的单位为MB,管理共享内存的全局变量为smm_shared_globals,其为指向结构体zend_smm_shared_globals的指针。
typedef struct _zend_smm_shared_globals {
/* Shared Memory Manager */
zend_shared_segment **shared_segments;
/* Number of allocated shared segments */
int shared_segments_count;
/* Amount of free shared memory */
size_t shared_free;
/* Amount of shared memory allocated by garbage */
size_t wasted_shared_memory;
/* No more shared memory flag */
zend_bool memory_exhausted;
/* Saved Shared Allocator State */
zend_shared_memory_state shared_memory_state;
/* Pointer to the application's shared data structures */
void *app_shared_globals;
} zend_smm_shared_globals;
主要字段说明如下。
-
shared_segments:二级指针,最终指向结构体zend_shared_segment。由于一次性申请内存大小的限制,php.ini中设置的内存大小可能被分成多块内存来存储在zend_shared_segment中:
typedef struct _zend_shared_segment { size_t size; // 此块内存的大小 size_t pos; // 此块内存在整块内存中的位置 void *p; // 指向此块内存的指针 } zend_shared_segment; -
shared_segments_count:大块内存分成的小块内存的个数,根据申请内存的方式不同而不同。
申请流程:首先抢占fcntl文件锁,保证此共享内存只会申请一次;真正实现内存分配的函数总共有4种,即mmap、shm、posix以及win32,对应的函数名分别是zend_alloc_mmap_handlers、zend_alloc_shm_handlers、zend_alloc_posix_handlers、zend_alloc_win32_handlers。申请内存时,优先使用php.ini中memory_model字段配置的申请模式,如果申请失败,则再依次使用全局变量handler_table配置的申请内存的模式重新申请,直到成功,如果遍历结束后仍然未申请成功,则返回申请失败;最后对全局变量smm_shared_globals指向的数据进行初始化。
int zend_shared_alloc_startup(size_t requested_size)
{
……
if (ZCG(accel_directives).memory_model && ZCG(accel_directives).memory_model[0]) {
……
}
if (! g_shared_alloc_handler) {
/* try memory handlers in order */
for (he = handler_table; he->name; he++) {
res = zend_shared_alloc_try(he, requested_size, &ZSMMG(shared_segments),
&ZSMMG(shared_segments_count), &error_in);
if (res) {
/* this model works! */
break;
}
}
}
……
}
第五步,初始化全局变量accel_shared_globals指向的结构体zend_accel_shared_globals,其结构体如下。
typedef struct _zend_accel_shared_globals {
/* Cache Data Structures */
zend_ulong hits;
zend_ulong misses;
zend_ulong blacklist_misses;
zend_ulong oom_restarts; /* number of restarts because of out of memory */
zend_ulong hash_restarts; /* number of restarts because of hash overflow */
zend_ulong manual_restarts; /* number of restarts scheduled by opcache_reset() */
zend_accel_hash hash; /* hash table for cached scripts */
/* Directives & Maintenance */
time_t start_time;
time_t last_restart_time;
time_t force_restart_time;
zend_bool accelerator_enabled;
zend_bool restart_pending;
zend_accel_restart_reason restart_reason;
zend_bool cache_status_before_restart;
zend_bool restart_in_progress;
/* Interned Strings Support */
char *interned_strings_start;
char *interned_strings_top;
char *interned_strings_end;
char *interned_strings_saved_top;
HashTable interned_strings;
} zend_accel_shared_globals;
首先初始化字段hash,其类型为_zend_accel_hash,这是opcache自已实现的HashTable,结构体如下:
typedef struct _zend_accel_hash {
zend_accel_hash_entry **hash_table;
zend_accel_hash_entry *hash_entries;
uint32_t num_entries;
uint32_t max_num_entries;
uint32_t num_direct_entries;
} zend_accel_hash;
struct _zend_accel_hash_entry {
zend_ulong hash_value;
char *key;
uint32_t key_length;
zend_accel_hash_entry *next;
void *data;
zend_bool indirect;
};
zend_accel_hash结构中的max_num_entries为可存储的元素最大个数,可以在php.ini中配置:
opcache.max_accelerated_files = 1024
注意,这个配置只是名义上最大的缓存个数,实际可缓存的个数是从下面的prime_numbers中找到的第一个大于配置的数字的素数:
static uint prime_numbers[] =
{5, 11, 19, 53, 107, 223, 463, 983, 1979, 3907, 7963, 16229, 32531, 65407, 130987,
262237, 524521, 1048793 };
然后初始化interned_strings,这里的HashTable为第5章介绍的_zend_array,用来存储内部字符串(interned strings)。此HashTable的元素个数与interned_strings_buffer字段相关,此字段在php.ini中配置:
opcache.interned_strings_buffer = 8
其单位为MB,代表缓存的内部字符串内存上限。opcache假定平均每个字符串长度为8,再加上PHP 7的字符串存储结构体zend_string中的其他字段,得到每个字符串的平均长度_ZSTR_STRUCT_SIZE(8),所以此HashTable的初始化元素个数为interned_strings_buffer× 1024× 1024 / _ZSTR_STRUCT_SIZE(8)。
字段interned_strings_start为存储缓存字符串的内存首地址,显然,此块内存的大小为interned_strings_buffer× 1024× 1024,字段interned_strings_top指向interned_strings_start的头部,interned_strings_end指向interned_strings_start的尾部。
第六步,hook相关函数:
/* Override compiler */
zend_compile_file = persistent_compile_file;
/* Override stream opener function (to eliminate open() call caused by
* include/require statements ) */
zend_stream_open_function = persistent_stream_open_function;
/* Override path resolver function (to eliminate stat() calls caused by
* include_once/require_once statements */
zend_resolve_path = persistent_zend_resolve_path;
/* Override chdir() function */
if ((func = zend_hash_str_find_ptr(CG(function_table), "chdir", sizeof("chdir")-1)) ! =
NULL &&
func->type == ZEND_INTERNAL_FUNCTION) {
orig_chdir = func->internal_function.handler;
func->internal_function.handler = ZEND_FN(accel_chdir);
}
/* Override "include_path" modifier callback */
if ((ini_entry = zend_hash_str_find_ptr(EG(ini_directives), "include_path", sizeof
("include_path")-1)) ! = NULL) {
ZCG(include_path) = ini_entry->value;
orig_include_path_on_modify = ini_entry->on_modify;
ini_entry->on_modify = accel_include_path_on_modify;
}
/* Override file_exists(), is_file() and is_readable() */
zend_accel_override_file_functions();
至此,opcache的启动基本完成,下面我们主要来看下对编译函数的hookpersistent_compile_file的实现。
zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type)
{
if (! file_handle->filename || ! ZCG(enabled) || ! accel_startup_ok) {
……
}
if (! ZCG(accel_directives).revalidate_path) {
/* try to find cached script by key */
key = accel_make_persistent_key(file_handle->filename, strlen(file_handle->
filename), &key_length);
if (! key) {
return accelerator_orig_compile_file(file_handle, type);
}
persistent_script = zend_accel_hash_str_find(&ZCSG(hash), key, key_length);
}
/* if turned on - check the compiled script ADLER32 checksum */
if (persistent_script && ZCG(accel_directives).consistency_checks
&& persistent_script->dynamic_members.hits % ZCG(accel_directives).consistency_
checks == 0) {
unsigned int checksum = zend_accel_script_checksum(persistent_script);
if (checksum ! = persistent_script->dynamic_members.checksum ) {
/* The checksum is wrong */
……
persistent_script = NULL;
}
}
return zend_accel_load_script(persistent_script, from_shared_memory);
}
具体步骤如下。
-
校验opcache是否可用,是否已初始化完成,如果不可用或者初始化未成功,则执行原来的编译逻辑。
-
判断是否开启了文件路径验证(ZCG(accel_directives).revalidate_path):
opcache.revalidate_path = 1默认状态为关闭。如果关闭,则查找cache时的索引,只能通过文件名、当前工作路径和ZCG(include_path)来生成(ZCG(include_path)的值在php.ini中的include_path字段配置);如果已开启,则直接使用文件的全路径来查找cache。
-
判断是否开启缓存的有效期验证(ZCG(accel_directives).validate_timestamps):
opcache.validate_timestamps = 1默认状态为开启。如果关闭,则必须使用opcache_set或者opcache_invalidate函数来手动重置opcache,也可以通过重启Web服务器来使文件系统更改生效;如果开启,则每隔opcache.revalidate_freq秒检查一次文件是否有更新,如果有更新则重新编译。
-
校验cache是否合法。为了防止在读取cache的过程中数据被其他进程修改,导致读取到的cache数据异常,需对cache进行校验。
进行校验的算法为Adler-32。Adler-32是Mark Adler发明的校验和算法,和32位CRC校验算法一样,用于保护数据以防止其被意外更改,但是这个算法较容易被伪造,所以是不安全的。但是比起CRC,它的计算速度很快。这个算法是在Fletcher校验和算法的基础上修改而成的,原始的算法形式略快,但是可依赖性并不高。
-
返回cache中的数据或者把重新编译生成的结果存入cache中后返回。
14.4 自定义扩展
前边我们介绍了PHP扩展的实现原理,本节我们来看下如何编写一个自己的扩展。
假定我们要经常获取文件的总行数,命令行下可以直接通过“wc -lfilepath”获取,但是我们希望可以做成PHP内置函数来使用,例如:
<?php
$line = wcl('test.txt');
14.4.1 初始化
首先需要生成扩展的基本框架。PHP官方提供了一个构造器ext_skel以帮助我们生成扩展所必要的文件和基本框架,该文件位于源码目录下的ext目录:
$ ll ext_skel
-rwxr-xr-x 1 vagrant vagrant 8.5K 4月 112017 ext_skel
$ ./ext_skel -help
./ext_skel --extname=module [--proto=file] [--stubs=file] [--xml[=file]]
[--skel=dir] [--full-xml] [--no-help]
ext_skel有几个参数,其中,--extname参数用来指定要创建的扩展名称,是一个全为小写字母的标识符,仅包含字母和下划线,需要在ext目录下保持唯一;--proto用来指定函数原型(这一步可以省略)。例如,我们创建一个名为wcl的PHP扩展,首先定义函数原型文件:
$ touch wcl.def
$ cat wcl.def
int wcl(string filename)
接下来生成扩展框架:
$ ./ext_skel --extname=wcl --proto=wcl.def
Creating directory wcl
Creating basic files: config.m4 config.w32 .gitignore wcl.c php_wcl.h CREDITS
EXPERIMENTAL tests/001.phpt wcl.php [done].
To use your new extension, you will have to execute the following steps:
1. $ cd ..
2. $ vi ext/wcl/config.m4
3. $ ./buildconf
4. $ ./configure --[with|enable]-wcl
5. $ make
6. $ ./sapi/cli/php -f ext/wcl/wcl.php
7. $ vi ext/wcl/wcl.c
8. $ make
Repeat steps 3-6 until you are satisfied with ext/wcl/config.m4 and
step 6 confirms that your module is compiled into PHP. Then, start writing
code and repeat the last two steps as often as necessary.
$ ll wcl
-rw-r--r--1 vagrant vagrant 2.0K 1月 25 15:20 config.m4
-rw-r--r--1 vagrant vagrant 334 1月 25 15:20 config.w32
-rw-r--r--1 vagrant vagrant 3 1月 25 15:20 CREDITS
-rw-r--r--1 vagrant vagrant 0 1月 25 15:20 EXPERIMENTAL
-rw-r--r--1 vagrant vagrant 2.3K 1月 25 15:20 php_wcl.h
drwxr-xr-x 2 vagrant vagrant 21 1月 25 15:20 tests
-rw-r--r--1 vagrant vagrant 5.3K 1月 25 15:20 wcl.c
-rw-r--r--1 vagrant vagrant 493 1月 25 15:20 wcl.php
命令执行完,可以看到在当前目录下已经有了一个名为wcl的目录,目录中有很多文件。
- config.m4:autoconf语法规则的编译配置文件,它可以指定扩展支持的configure选项以及扩展需要的额外的库,包含哪些源文件等。
- config.w32:Windows平台下的编译配置文件,它的作用同config.m4,但是它是使用JavaScript编写的。
- CREDITS:用纯文本格式列出了扩展的贡献者和维护者。文件的第一行应保存扩展的名称,第二行是用逗号分隔的贡献者名单。
- EXPERIMENTAL:实验功能说明文件。
- php_wcl.h:当将扩展作为静态模块构建并放入PHP二进制包时,构建系统要求用php_ 加扩展的名称命名的头文件包含一个对扩展模块结构的指针定义。就像其他头文件,此文件经常包含附加的宏、原型和全局变量。
- tests:测试脚本目录。
- wcl.c:扩展的主要源文件,通常,此文件名就是扩展的文件名。此文件包含模块结构定义、ini配置项、扩展提供的函数和其他扩展所需的内容。
- wcl.php:测试脚本,可以输出扩展支持的函数列表以及当前扩展是否已经被编译到PHP。
14.4.2 编译配置
框架初始化之后,我们来看下如何修改编译配置文件。
这里补充一个小细节:读者可能注意到我们在编译安装PHP的时候,会指定一些编译配置选项,有些是-with,有些是-enable,这里有什么区别呢?一般来说,enable表示某个内置功能是否开启,而with表示是否需要添加某个功能,通常需要指定依赖的外部库。
wcl扩展不依赖外部组件,所以这里选择enable方式。修改config.m4,去掉PHP_ARG_ENABLE和--enable-wcl两行前面的dnl(在autoconf语法中,dnl表示注释)。修改后如下:
PHP_ARG_ENABLE(wcl, whether to enable wcl support,
dnl Make sure that the comment is aligned:
[ --enable-wcl Enable wcl support])
if test "$PHP_WCL" ! = "no"; then
PHP_NEW_EXTENSION(wcl, wcl.c, $ext_shared, , -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1)
fi
宏说明如下。
- PHP_ARG_ENABLE宏:第一个参数表示扩展名称;第二个参数在执行./configure处理到该扩展时,显示该参数的内容;第三个参数是执行./configure -help的输出信息。
- PHP_NEW_EXTENSION宏:声明了扩展的名称、源文件列表(多个文件的时候在文件名称后边加空格,如果需要换行还需加上反斜杠“\”)、此扩展是动态库还是静态库,扩展是否只能在CLI或CGI模式下运行等。
更多的宏定义参见源码目录下的acinclude.m4文件。在14.2.1节中,我们提到使用动态链接库方式的扩展还需要实现get_module方法,这里exk_skel帮我们做了这个工作:
#ifdef COMPILE_DL_WCL
#ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
#endif
ZEND_GET_MODULE(wcl)
#endif
14.4.3 功能实现
我们在生成扩展框架的时候已经指定了函数原型,ext_skel会自动生成该函数的基本定义,并通过宏PHP_FE把函数注册到zend_function_entry。如果不指定原型函数,那么这两步需要我们手动完成。另外,每个扩展都会注册一个名为confirm_扩展名称_compiled的函数用来输出当前扩展是否已经被编译到PHP:
/* {{{ proto int wcl(string filepath)
*/
PHP_FUNCTION(wcl)
{
char *filepath = NULL;
int argc = ZEND_NUM_ARGS();
size_t filepath_len;
if (zend_parse_parameters(argc, "s", &filepath, &filepath_len) == FAILURE) {
return;
}
php_error(E_WARNING, "wcl: not yet implemented");
}
/* }}} */
/* {{{ wcl_functions[]
*
* Every user visible function must have an entry in wcl_functions[].
*/
const zend_function_entry wcl_functions[] = {
PHP_FE(confirm_wcl_compiled, NULL) /* For testing, remove later. */
PHP_FE(wcl, NULL)
PHP_FE_END /* Must be the last line in wcl_functions[] */
};
/* }}} */
扩展中函数的定义由PHP_FUNCTION宏来完成,实际展开如下:
void zif_wcl(zend_execute_data *execute_data, zval *return_value)
zend_parse_parameters用来对函数参数做校验并获取函数的参数。第一个参数为传递给函数的参数个数,通常ZEND_NUM_ARGS来获取;第二个参数指定函数的参数类型,其后是要解析的参数。
接下来我们修改PHP_FUNCTION(wcl) 实现功能即可,打开wcl.c,修改这个函数为以下代码:
PHP_FUNCTION(wcl)
{
char *filepath = NULL;
int argc = ZEND_NUM_ARGS();
size_t filepath_len;
char ch;
FILE *fp;
Zend_long lcount = 0;
if (zend_parse_parameters(argc, "s", &filepath, &filepath_len) == FAILURE)
{
return;
}
/* php_error(E_WARNING, "wcl: not yet implemented"); */
if ((fp = fopen(filepath, "r")) == NULL) {
RETURN_FALSE;
}
while ((ch = fgetc(fp)) ! = EOF) {
if (ch == '\n') {
lcount++;
}
}
fclose(fp);
RETURN_LONG(lcount);
}
这个函数在无法打开文件的时候返回false,正常情况下遍历文件内容判断换行符,如果当前字符为换行符,则计数加1,最终返回文件总行数。
这里的RETURN_FALSE和RETURN_LONG是用于从函数中返回值的宏,分别表示返回false和long类型的返回值,类似的还有RETVAL_NULL、RETVAL_BOOL、RETVAL_TRUE、RETVAL_DOUBLE、RETVAL_STRING、RETVAL_STRINGL、RETVAL_RESOURCE,见名思义,这里就不再逐一解释了。
14.4.4 注册配置
项如果不考虑各种边界问题,这个扩展的基本功能就完成了,后边只需要安装并启用这个扩展即可,但是这里我们稍微扩展下:注册一个ini配置项来控制是否计算空行(暂时只考虑首字符为换行符的情况)。
注意
配置解析相关内容请参考第8章,这里不再赘述。
在php_wcl.h中声明扩展内的全局变量:
ZEND_BEGIN_MODULE_GLOBALS(wcl)
Zend_long filter_blank;
ZEND_END_MODULE_GLOBALS(wcl)
在wcl.c中添加配置项:
ZEND_DECLARE_MODULE_GLOBALS(wcl)
PHP_INI_BEGIN()
STD_PHP_INI_ENTRY("wcl.filter_blank", "0", PHP_INI_ALL, OnUpdateBool, filter_
blank, zend_wcl_globals, wcl_globals)
PHP_INI_END()
以上代码表示为当前扩展注册了一个配置项“wcl.filter_blank”,其默认值为0。
接下来,我们修改前边的PHP_FUNCTION(wcl),在函数中获取这个配置项:
PHP_FUNCTION(wcl)
{
char *filepath = NULL;
int argc = ZEND_NUM_ARGS();
size_t filepath_len;
char ch, pre = '\n';
FILE *fp;
zend_long lcount = 0;
if (zend_parse_parameters(argc, "s", &filepath, &filepath_len) == FAILURE)
{
return;
}
/* php_error(E_WARNING, "wcl: not yet implemented"); */
if ((fp = fopen(filepath, "r")) == NULL) {
RETURN_FALSE;
}
while ((ch = fgetc(fp)) ! = EOF) {
if (ch == '\n') {
if (WCL_G(filter_blank) && pre == ch) {
continue;
}
lcount++;
}
pre = ch;
}
fclose(fp);
RETURN_LONG(lcount);
}
扩展内可以通过注册到WCL_G的配置项来获取配置。到这里还差一步就可以真正使用它了。前面我们学习了PHP有五大阶段,每个扩展都会通过实现这五个阶段的钩子函数来完成相关工作,具体如下:
/* {{{ wcl_module_entry
*/
zend_module_entry wcl_module_entry = {
STANDARD_MODULE_HEADER,
"wcl",
wcl_functions,
PHP_MINIT(wcl),
PHP_MSHUTDOWN(wcl),
PHP_RINIT(wcl), /* Replace with NULL if there's nothing to do at request start */
PHP_RSHUTDOWN(wcl), /* Replace with NULL if there's nothing to do at request end */
PHP_MINFO(wcl),
PHP_WCL_VERSION,
STANDARD_MODULE_PROPERTIES
};
/* }}} */
其中,配置项的注册是在模块初始化阶段PHP_MINIT这一步完成的,在wcl.c文件找到PHP_MINIT_FUNCTION函数,可以看到里边已经有如下注释(我们只需要打开这个注释即可,wcl扩展在启动的时候会自动注册当前扩展的配置项):
/* {{{ PHP_MINIT_FUNCTION
*/
PHP_MINIT_FUNCTION(wcl)
{
/* If you have INI entries, uncomment these lines
REGISTER_INI_ENTRIES();
*/
return SUCCESS;
}
/* }}} */
相应地,在模块关闭阶段PHP_MSHUTDOWN中,也需要注销配置项,在PHP_MSHUTDOWN_FUNCTION函数中打开对应的注释即可。
14.4.5 编译、安装
完成了扩展功能的实现,并注册了相应的配置项后,只需要编译并安装就可以在PHP程序中使用了:
$ pwd
/home/vagrant/php-7.1.0/output/ext/wcl
$ /home/vagrant/php-7.1.0/output/bin/phpize
Configuring for:
PHP Api Version: 20160303
Zend Module Api No: 20160303
Zend Extension Api No: 320160303
$ ./configure --with-php-config=/home/vagrant/php-7.1.0/output/bin/php-config
// 编译过程略
$ make && make install
Installing shared extensions: /home/vagrant/php-7.1.0/output/lib/php/
extensions/no-debug-non-zts-20160303/
$ ll /home/vagrant/php-7.1.0/output/lib/php/extensions/no-debug-non-zts-20160303/
-rwxr-xr-x 1 root root 33K 1月 27 05:49 wcl.so
可以看到,扩展对应的目录下已经有对应的wcl.so文件了,之后的步骤想必读者已经很熟悉了,只需要打开php.ini文件启用扩展并配置对应的配置项即可。配置完成以后,执行以下命令:
$ php -f ext/wcl/wcl.php
Functions available in the test extension:
confirm_wcl_compiled
wcl
Congratulations! You have successfully modified ext/wcl/config.m4. Module wcl is now compiled into PHP.
可以看到,wcl已经成功编译到PHP,并且该扩展提供了两个函数。现在我们可以在PHP代码中直接使用wcl函数了。
14.5 本章小结
本章从扩展的原因,讲到扩展的两种分类——PHP扩展和Zend扩展及其异同,分别讲了两种扩展的实现原理,并仔细研究了一个实现,最后又详细描述了一个简易PHP扩展开发示例。其实PHP官方提供的扩展开发工具不是特别好用,在此推荐一款更加灵活好用的第三方开源工具——PHP-X。通过此工具,开发者可以更加高效地开发PHP扩展。