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:
              ……

    }
    ……
}

主要操作步骤如下。

  1. 初始化一个全局变量 accel_globals

  2. 注册内部模块 accel

  3. 校验 opcache 是否支持当前的 sapi

  4. 分配并初始化共享内存。

  5. 初始化全局变量 accel_shared_globals 指向的结构体 zend_accel_shared_globals

  6. 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:进程使用的共享内存大小。

  • enabledOpcahce 是否可用。

  • locked:线程是否获得了互斥锁。

  • bind_hashHashTable,初始化时分配了 10 个元素。

  • accel_directivesopcache 相关的配置,在函数 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;

主要字段说明如下。

  • scriptzend_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 的大小。

第二步,注册内部模块 accelopcache 的相关函数都注册在这个内部模块下:

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 是否支持当前的 sapiopcache 只支持下面几种 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 种,即 mmapshmposix 以及 win32,对应的函数名分别是 zend_alloc_mmap_handlerszend_alloc_shm_handlerszend_alloc_posix_handlerszend_alloc_win32_handlers。申请内存时,优先使用 php.inimemory_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);
}

具体步骤如下。

  1. 校验 opcache 是否可用,是否已初始化完成,如果不可用或者初始化未成功,则执行原来的编译逻辑。

  2. 判断是否开启了文件路径验证(ZCG(accel_directives).revalidate_path):

    opcache.revalidate_path = 1

    默认状态为关闭。如果关闭,则查找 cache 时的索引,只能通过文件名、当前工作路径和 ZCG(include_path) 来生成(ZCG(include_path) 的值在 php.ini 中的 include_path 字段配置);如果已开启,则直接使用文件的全路径来查找 cache

  3. 判断是否开启缓存的有效期验证(ZCG(accel_directives).validate_timestamps):

    opcache.validate_timestamps = 1

    默认状态为开启。如果关闭,则必须使用 opcache_set 或者 opcache_invalidate 函数来手动重置 opcache,也可以通过重启 Web 服务器来使文件系统更改生效;如果开启,则每隔 opcache.revalidate_freq 秒检查一次文件是否有更新,如果有更新则重新编译。

  4. 校验 cache 是否合法。为了防止在读取 cache 的过程中数据被其他进程修改,导致读取到的 cache 数据异常,需对 cache 进行校验。

    进行校验的算法为 Adler-32Adler-32 是 Mark Adler 发明的校验和算法,和 32 位 CRC 校验算法一样,用于保护数据以防止其被意外更改,但是这个算法较容易被伪造,所以是不安全的。但是比起 CRC,它的计算速度很快。这个算法是在 Fletcher 校验和算法的基础上修改而成的,原始的算法形式略快,但是可依赖性并不高。

  5. 返回 cache 中的数据或者把重新编译生成的结果存入 cache 中后返回。