美文网首页
PHP7内核知识整理

PHP7内核知识整理

作者: 影帆 | 来源:发表于2017-06-24 12:05 被阅读0次

    php7基础逻辑

    worker进程

    FPM_REQUEST_ACCEPTING: 等待请求阶段
    FPM_REQUEST_READING_HEADERS: 读取fastcgi请求header阶段
    FPM_REQUEST_INFO: 获取请求信息阶段,此阶段是将请求的method、query stirng、request uri等信息保存到各worker进程的fpm_scoreboard_proc_s结构中,此操作需要加锁,因为master进程也会操作此结构
    FPM_REQUEST_EXECUTING: 执行请求阶段
    FPM_REQUEST_END: 没有使用
    FPM_REQUEST_FINISHED: 请求处理完成 
    

    进程管理方式

    • static:启动master的时候按照pm.max_children配置fork出相应数量worker,worker进程数固定
    • dynamic:动态进程管理,pm.start_servers 初始化一定量worker,低于pm.min_spare_servers配置数则会fork worker进程,但总的不能超过pm.max_children,如果master发现worker数
      超过pm.max_spare_servers则会杀掉一些worker,避免用过多的资源,master通过这4个值来控制worker数
    • ondemand:这种方式在启动的时候不分配worker,等到请求了再通知master进程fork worker进程,总得worker数不超过pm.max_children,处理完成后worker进程不会立即退出,当空闲时间超过pm.process_idle_timeout后再退出

    fpm_init相关操作
    
    - fpm_conf_init_main
    - fpm_scoreboard_init_main
    - fpm_signals_init_main
    - fpm_sockets_init_main
    - fpm_event_init_main
    

    fpm_got_signal 逻辑

    • SIGINT/SIGTERM/SIGQUIT: 退出fpm,在master收到退出信号后将向所有的worker进程发送退出信号,然后master退出
    • SIGUSR1: 重新加载日志文件,生产环境中通常会对日志进行切割,切割后会生成一个新的日志文件,如果fpm不重新加载将无法继续写入日志,这个时候就需要向master发送一个USR1的信号
    • SIGUSR2: 重启fpm,首先master也是会向所有的worker进程发送退出信号,然后master会调用execvp()重新启动fpm,最后旧的master退出
    • SIGCHLD: 这个信号是子进程退出时操作系统发送给父进程的,子进程退出时,内核将子进程置为僵尸状态,这个进程称为僵尸进程,它只保留最小的一些内核数据结构,以便父进程查询子进程的退出状态,只有当父进程调用wait或者waitpid函数查询子进程退出状态后子进程才告终止,fpm中当worker进程因为异常原因(比如coredump了)退出而非master主动杀掉时master将受到此信号,这个时候父进程将调用waitpid()查下子进程的退出,然后检查下是不是需要重新fork新的worker

    php-fpm的进程中request_terminate_timeout负责心跳检查,当大于0的时候走以下函数逻辑:

    fpm_pctl_heartbeat逻辑

    这个事件是用于限制worker处理单个请求最大耗时的,php-fpm.conf中有一个request_terminate_timeout的配置项,如果worker处理一个请求的总时长超过了这个值那么master将会向此worker进程发送kill -TERM信号杀掉worker进程,此配置单位为秒,默认值为0表示关闭此机制,另外fpm打印的slow log也是在这里完成的。


    fpm_pctl_perform_idle_server_maintenance_heartbeat逻辑

    这是进程管理实现的主要事件,master启动了一个定时器,每隔1s触发一次,主要用于dynamic、ondemand模式下的worker管理,master会定时检查各worker pool的worker进程数,通过此定时器实现worker数量的控制


    除了上面这几个事件外还有一个没有提到,那就是ondemand模式下master监听的新请求到达的事件,因为ondemand模式下fpm启动时是不会预创建worker的,有请求时才会生成子进程,所以请求到达时需要通知master进程,这个事件是在fpm_children_create_initial()时注册的,事件处理函数为fpm_pctl_on_socket_accept()

    image.png

    php数组

    数组结构

    存放记录的数组称做散列表,这个数组用来存储value,而value具体在数组中的存储位置由映射函数根据key计算确定,映射函数可以采用取模的方式,key可以通过一些譬如“times 33”的算法得到一个整形值,然后与数组总大小取模得到在散列表中的存储位置。这是一个普通散列表的实现,PHP散列表的实现整体也是这个思路,只是有几个特殊的地方,下面就是PHP中HashTable的数据结构:

    //Bucket:散列表中存储的元素
    typedef struct _Bucket {
        zval              val; //存储的具体value,这里嵌入了一个zval,而不是一个指针
        zend_ulong        h;   //key根据times 33计算得到的哈希值,或者是数值索引编号
        zend_string      *key; //存储元素的key
    } Bucket;
    
    //HashTable结构
    typedef struct _zend_array HashTable;
    struct _zend_array {
        zend_refcounted_h gc;
        union {
            struct {
                ZEND_ENDIAN_LOHI_4(
                        zend_uchar    flags,
                        zend_uchar    nApplyCount,
                        zend_uchar    nIteratorsCount,
                        zend_uchar    reserve)
            } v;
            uint32_t flags;
        } u;
        uint32_t          nTableMask; //哈希值计算掩码,等于nTableSize的负值(nTableMask = -nTableSize)
        Bucket           *arData;     //存储元素数组,指向第一个Bucket
        uint32_t          nNumUsed;   //已用Bucket数
        uint32_t          nNumOfElements; //哈希表有效元素数
        uint32_t          nTableSize;     //哈希表总大小,为2的n次方
        uint32_t          nInternalPointer;
        zend_long         nNextFreeElement; //下一个可用的数值索引,如:arr[] = 1;arr["a"] = 2;arr[] = 3;  则nNextFreeElement = 2;
        dtor_func_t       pDestructor;
    };
    

    HashTable中有两个非常相近的值:nNumUsednNumOfElementsnNumOfElements表示哈希表已有元素数,那这个值不跟nNumUsed一样吗?为什么要定义两个呢?实际上它们有不同的含义,当将一个元素从哈希表删除时并不会将对应的Bucket移除,而是将Bucket存储的zval修改为IS_UNDEF,只有扩容时发现nNumOfElements与nNumUsed相差达到一定数量(这个数量是:ht->nNumUsed - ht->nNumOfElements > (ht->nNumOfElements >> 5))时才会将已删除的元素全部移除,重新构建哈希表。所以nNumUsed>=nNumOfElements

    HashTable中另外一个非常重要的值arData,这个值指向存储元素数组的第一个Bucket,插入元素时按顺序 依次插入 数组,比如第一个元素在arData[0]、第二个在arData[1]...arData[nNumUsed]。PHP数组的有序性正是通过arData保证的,这是第一个与普通散列表实现不同的地方。

    实际上这个散列表也在arData中,比较特别的是散列表在ht->arData内存之前,分配内存时这个散列表与Bucket数组一起分配,arData向后移动到了Bucket数组的起始位置,并不是申请内存的起始位置,这样散列表可以由arData指针向前移动访问到,即arData[-1]、arData[-2]、arData[-3]......散列表的结构是uint32_t,它保存的是value在Bucket数组中的位置。

    imgimg

    映射函数

    映射函数(即:散列函数)是散列表的关键部分,它将key与value建立映射关系,一般映射函数可以根据key的哈希值与Bucket数组大小取模得到,即key->h % ht->nTableSize,但是PHP却不是这么做的:

    nIndex = key->h | ht->nTableMask;
    

    哈希碰撞

    哈希碰撞是指不同的key可能计算得到相同的哈希值(数值索引的哈希值直接就是数值本身),但是这些值又需要插入同一个散列表。一般解决方法是将Bucket串成链表,查找时遍历链表比较key。

    PHP的实现也是如此,只是将链表的指针指向转化为了数值指向,即:指向冲突元素的指针并没有直接存在Bucket中,而是保存到了value的zval中:

    struct _zval_struct {
        zend_value        value;            /* value */
        ...
        union {
            uint32_t     var_flags;
            uint32_t     next;                 /* hash collision chain */
            uint32_t     cache_slot;           /* literal cache slot */
            uint32_t     lineno;               /* line number (for ast nodes) */
            uint32_t     num_args;             /* arguments number for EX(This) */
            uint32_t     fe_pos;               /* foreach position */
            uint32_t     fe_iter_idx;          /* foreach iterator index */
        } u2;
    };
    

    当出现冲突时将原value的位置保存到新value的zval.u2.next中,然后将新插入的value的位置更新到散列表,也就是后面冲突的value始终插入header。所以查找过程类似:

    zend_ulong h = zend_string_hash_val(key);
    uint32_t idx = ht->arHash[h & ht->nTableMask];
    while (idx != INVALID_IDX) {
        Bucket *b = &ht->arData[idx];
        if (b->h == h && zend_string_equals(b->key, key)) {
            return b;
        }
        idx = Z_NEXT(b->val); //移到下一个冲突的value
    }
    return NULL;
    

    扩容

    散列表可存储的value数是固定的,当空间不够用时就要进行扩容了。

    PHP散列表的大小为2^n,插入时如果容量不够则首先检查已删除元素所占比例,如果达到阈值(ht->nNumUsed - ht->nNumOfElements > (ht->nNumOfElements >> 5),则将已删除元素移除,重建索引,如果未到阈值则进行扩容操作,扩大为当前大小的2倍,将当前Bucket数组复制到新的空间,然后重建索引。

    //zend_hash.c
    static void ZEND_FASTCALL zend_hash_do_resize(HashTable *ht)
    {
    
        if (ht->nNumUsed > ht->nNumOfElements + (ht->nNumOfElements >> 5)) {
            //只有到一定阈值才进行rehash操作
            zend_hash_rehash(ht); //重建索引数组
        } else if (ht->nTableSize < HT_MAX_SIZE) {
            //扩容
            void *new_data, *old_data = HT_GET_DATA_ADDR(ht);
            //扩大为2倍,加法要比乘法快,小的优化点无处不在...
            uint32_t nSize = ht->nTableSize + ht->nTableSize;
            Bucket *old_buckets = ht->arData;
    
            //新分配arData空间,大小为:(sizeof(Bucket) + sizeof(uint32_t)) * nSize
            new_data = pemalloc(HT_SIZE_EX(nSize, -nSize), ...);
            ht->nTableSize = nSize;
            ht->nTableMask = -ht->nTableSize;
            //将arData指针偏移到Bucket数组起始位置
            HT_SET_DATA_ADDR(ht, new_data);
            //将旧的Bucket数组拷到新空间
            memcpy(ht->arData, old_buckets, sizeof(Bucket) * ht->nNumUsed);
            //释放旧空间
            pefree(old_data, ht->u.flags & HASH_FLAG_PERSISTENT);
            
            //重建索引数组:散列表
            zend_hash_rehash(ht);
            ...
        }
        ...
    }
    
    #define HT_SET_DATA_ADDR(ht, ptr) do { \
            (ht)->arData = (Bucket*)(((char*)(ptr)) + HT_HASH_SIZE((ht)->nTableMask)); \
        } while (0)
    

    重建散列表

    当删除元素达到一定数量或扩容后都需要重建散列表,因为value在Bucket位置移动了或哈希数组nTableSize变化了导致key与value的映射关系改变,重建过程实际就是遍历Bucket数组中的value,然后重新计算映射值更新到散列表,除了更新散列表之外,这里还有一个重要的处理:移除已删除的value,开始的时候我们说过,删除value时只是将value的type设置为IS_UNDEF,并没有实际从Bucket数组中删除,如果这些value一直存在那么将浪费很多空间,所以这里会把它们移除,操作的方式也比较简单:将后面未删除的value依次前移,具体过程如下:

    //zend_hash.c
    ZEND_API int ZEND_FASTCALL zend_hash_rehash(HashTable *ht)
    {
        Bucket *p;
        uint32_t nIndex, i;
        ...
        i = 0;
        p = ht->arData;
        if (ht->nNumUsed == ht->nNumOfElements) { //没有已删除的直接遍历Bucket数组重新插入索引数组即可
            do {
                nIndex = p->h | ht->nTableMask;
                Z_NEXT(p->val) = HT_HASH(ht, nIndex);
                HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(i);
                p++;
            } while (++i < ht->nNumUsed);
        } else {
            do {
                if (UNEXPECTED(Z_TYPE(p->val) == IS_UNDEF)) {
                    //有已删除元素则将后面的value依次前移,压实Bucket数组
                    ......
                    while (++i < ht->nNumUsed) {
                        p++;
                        if (EXPECTED(Z_TYPE_INFO(p->val) != IS_UNDEF)) {
                            ZVAL_COPY_VALUE(&q->val, &p->val);
                            q->h = p->h;
                            nIndex = q->h | ht->nTableMask;
                            q->key = p->key;
                            Z_NEXT(q->val) = HT_HASH(ht, nIndex);
                            HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(j);
                            if (UNEXPECTED(ht->nInternalPointer == i)) {
                                ht->nInternalPointer = j;
                            }
                            q++;
                            j++;
                        }
                    }
                    ......
                    ht->nNumUsed = j;
                    break;
                }
                
                nIndex = p->h | ht->nTableMask;
                Z_NEXT(p->val) = HT_HASH(ht, nIndex);
                HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(i);
                p++;
            }while(++i < ht->nNumUsed);
        }
    }
    

    静态变量

    PHP中局部变量分配在zend_execute_data结构上,每次执行zend_op_array都会生成一个新的zend_execute_data,局部变量在执行之初分配,然后在执行结束时释放,这是局部变量的生命周期,而局部变量中有一种特殊的类型:静态变量,它们不会在函数执行完后释放,当程序执行离开函数域时静态变量的值被保留下来,下次执行时仍然可以使用之前的值。

    PHP中的静态变量通过static关键词创建

    function my_func(){
        static $count = 4;
        $count++;
        echo $count,"\n";
    }
    my_func();
    my_func();
    ===========================
    5
    6
    

    静态变量的存储

    静态变量既然不会随执行的结束而释放,那么很容易想到它的保存位置:zend_op_array->static_variables,这是一个哈希表,所以PHP中的静态变量与普通局部变量不同,它们没有分配在执行空间zend_execute_data上,而是以哈希表的形式保存在zend_op_array中。

    静态变量只会初始化一次,注意:它的初始化发生在编译阶段而不是执行阶段,上面这个例子中:static $count = 4;是在编译阶段发现定义了一个静态变量,然后插进了zend_op_array->static_variables中,并不是执行的时候把static_variables中的值修改为4,所以上面执行的时候会输出5、6,再次执行并没有重置静态变量的值。

    这个特性也意味着静态变量初始的值不能是变量,比如:static $count = $xxx;这样定义将会报错。

    局部变量通过编译时确定的编号进行读写操作,而静态变量通过哈希表保存,这就使得其不能像普通变量那样有一个固定的编号,有一种可能是通过变量名索引的,那么究竟是否如此呢?我们分析下其编译过程。

    静态变量编译的语法规则:

    statement:
        ...
        |   T_STATIC static_var_list ';'    { $$ = $2; }
        ...
    ;
    
    static_var_list:
            static_var_list ',' static_var { $$ = zend_ast_list_add($1, $3); }
        |   static_var { $$ = zend_ast_create_list(1, ZEND_AST_STMT_LIST, $1); }
    ;
    
    static_var:
            T_VARIABLE          { $$ = zend_ast_create(ZEND_AST_STATIC, $1, NULL); }
        |   T_VARIABLE '=' expr { $$ = zend_ast_create(ZEND_AST_STATIC, $1, $3); }
    ;
    

    语法解析后生成了一个ZEND_AST_STATIC语法树节点,接着再看下这个节点编译为opcode的过程:zend_compile_static_var。

    void zend_compile_static_var(zend_ast *ast)
    {
        zend_ast *var_ast = ast->child[0];
        zend_ast *value_ast = ast->child[1];
        zval value_zv;
    
        if (value_ast) {
            //定义了初始值
            zend_const_expr_to_zval(&value_zv, value_ast);
        } else {
            //无初始值
            ZVAL_NULL(&value_zv);
        }
    
        zend_compile_static_var_common(var_ast, &value_zv, 1);
    }
    

    这里首先对初始化值进行编译,最终得到一个固定值,然后调用:zend_compile_static_var_common()处理,首先判断当前编译的zend_op_array->static_variables是否已创建,未创建则分配一个HashTable,接着将定义的静态变量插入:

    //zend_compile_static_var_common():
    if (!CG(active_op_array)->static_variables) {
        ALLOC_HASHTABLE(CG(active_op_array)->static_variables);
        zend_hash_init(CG(active_op_array)->static_variables, 8, NULL, ZVAL_PTR_DTOR, 0);
    }
    //插入静态变量
    zend_hash_update(CG(active_op_array)->static_variables, Z_STR(var_node.u.constant), value);
    

    插入静态变量哈希表后并没有完成,接下来还有一个重要操作:

    //生成一条ZEND_FETCH_W的opcode
    opline = zend_emit_op(&result, by_ref ? ZEND_FETCH_W : ZEND_FETCH_R, &var_node, NULL);
    opline->extended_value = ZEND_FETCH_STATIC;
    
    if (by_ref) {
        zend_ast *fetch_ast = zend_ast_create(ZEND_AST_VAR, var_ast);
        //生成一条ZEND_ASSIGN_REF的opcode
        zend_emit_assign_ref_znode(fetch_ast, &result);
    }
    

    后面生成了两条opcode:

    • ZEND_FETCH_W: 这条opcode对应的操作是创建一个IS_INDIRECT类型的zval,指向static_variables中对应静态变量的zval
    • ZEND_ASSIGN_REF: 它的操作是引用赋值,即将一个引用赋值给CV变量

    通过上面两条opcode可以确定静态变量的读写过程:首先根据变量名在static_variables中取出对应的zval,然后将它修改为引用类型并赋值给局部变量,也就是说static $count = 4;包含了两个操作,严格的将$count并不是真正的静态变量,它只是一个指向静态变量的局部变量,执行时实际操作是:$count = & static_variables["count"];。上面例子$count与static_variables["count"]间的关系如图所示。

    imgimg

    相关文章

      网友评论

          本文标题:PHP7内核知识整理

          本文链接:https://www.haomeiwen.com/subject/shcjfxtx.html