Zend扩展
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
中。
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
的启动基本完成,下面我们主要来看下对编译函数的 hook persistent_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
中后返回。