php内核学习-生命周期与执行过程


layout: post
title: php内核学习-生命周期与执行过程
date: 2016-03-28
categories: php
tags: [php内核]
description: 总结下php源码学习过程中的一些点(载入简书)


随意转载,请注明出处

PHP四层架构

开始之前,先看看PHP的核心架构,如图:

我们自顶向下的来看这架构:

  • 首先是Application,就是我们的上层应用——平时写的PHP程序,可以是web应用或者php脚本;
  • 接着是SAPI(Server Application Programming Interface),服务端应用编程接口,sapi通过一系列钩子函数,使得php可以和外围交互数据,并且通过sapi成功的将php本身和上层应用解耦隔离,php可以不再考虑如何针对不同的应用进行兼容,而应用本身也可以针对自己的特点实现不同的处理方式;
  • 再下来是Extensions——扩展层,围绕着Zend引擎,扩展层通过组件式的方式提供各种基础服务,我们常见的各种内置函数(如array系列)、标准库等都是通过扩展来实现,我们自己也能自定义一些扩展来达到扩展功能、优化性能等目的;
  • 最下面就是Zend了,整体用纯c实现,是php的内核部分,在这一层,它将php代码翻译成opcode并提供相应的处理方法、实现了基本的数据结构、内存分配及管理、提供了相应的api方法工外部调用,时一切的核心,所有的外围功能均围绕zend实现。

PHP的生命周期

结合php四层架构,我们来看一看一个php程序从执行开始都需要经历什么。

最常见的四种启动php的方式如下:

  • 直接以CLI/CGI模式调用
  • 多线程模块
  • 多进程模块
  • Embedded(嵌入式,在自己c程序中调用Zend Engine)

无论用哪种方式启动的,除了执行脚本本身逻辑之外,都会依次经过Module init、Request init、Request shutdown、Module shutdown四个过程。两种init和两种shutdown各会执行多少次,各自执行的频率有多少,取决于php是用什么SAPI与宿主通信的。

以命令行运行一个PHP程序为例:

图中可以看出,在命令行敲下 “php -f test.php” 之后,会有这些操作:

  • MINIT 这个过程在扩展被载入时调用,是模块初始化阶段(MINIT),回调所有模块的MINIT函数,模块在这个阶段可以进行一些初始化工作,如注册常量,定义模块使用的类等。在整个SAPI生命周期内,该过程只进行一次
  • RINIT 每次请求之前,都会进行模块激活(RINIT请求开始),当请求到达以后,PHP会初始化执行脚本的基本环境,例如创建一个执行环境,包括保存PHP运行过程中变量名称和变量值内容的符号表,以及当前所有的函数以及类等信息的符号表。然后PHP会调用所有模块的RINIT函数,在这个阶段,各个模块也可以执行一些相关的操作。一个经典的例子是Session模块的RINIT,如果在php.ini中启用了Session模块,那在调用该模块的RINIT时就会初始化$_SESSION变量,并将相关内容读入;RINIT方法可以看作是一个准备过程, 在程序执行之间就会自动启动。
  • Execute test.php 执行test.php阶段,主要是把PHP文件翻译成Opcodes,然后再PHP虚拟机下执行。
  • RSHUTDOWN & MSHUTDOWN 请求处理完后就进入了结束阶段,一般脚本执行到末尾或者通过调用exit()或die()函数, PHP都将进入结束阶段。和开始阶段对应,结束阶段也分为两个环节,一个在请求结束后停用模块(RSHUWDOWN,对应RINIT), 一个在SAPI生命周期结束(Web服务器退出或者命令行脚本执行完毕退出)时关闭模块(MSHUTDOWN,对应MINIT)。

Execute *.php

现在我们来深究下php文件的执行,我们都知道,编程语言最终转化成能被计算机理解的汇编语言要经过语法分析、词法分析、生成中间代码、生成目标代码等过程。PHP也不例外,先看一张php编译的流程图:

由图可见,经过词法分析、语法分析之后,最终只生成了中间代码opcode(PHP的一种内部数据结构),并没有继续生成目标代码,故而PHP也被称为解释型语言。

下面我们再来详细看下从源程序生成opcode的过程。
Hello World!为例:

<?php
    echo "Hello World!";
?>

Scanning(Lexing),将PHP代码转换为语言片段(Tokens)

PHP原来使用的是Flex,之后改为re2c,源码目录下的Zend/zend_language_scanner.l是re2c的规则文件,如果安装了re2c的话,我们还可以修改并生成新的规则文件。Zend/zend_language_canner.c会根据规则文件,来对输入的PHP代码进行词法分析,从而得到一个一个的

<?php
    $code = '<?php echo "Hello World!"; ?>';
    $tokens = token_get_all($code);
    foreach($tokens as $key => $token) { 
        $tokens[$key][0] = token_name($token[0]);
    }
    var_dump($tokens);
?>

这里用token_name函数将解析器代号修改成了符号名称说明,更加容易理解,关于解释器代号列表可以参见手册

执行结果如下:

array(7) {
  [0]=>
  array(3) {
    [0]=>string(10) "T_OPEN_TAG"
    [1]=>string(6) "<?php "
    [2]=>int(1)
  }
  [1]=>
  array(3) {
    [0]=>string(6) "T_ECHO"
    [1]=>string(4) "echo"
    [2]=>int(1)
  }
  [2]=>
  string(1) ""
  [3]=>
  array(3) {
    [0]=>string(26) "T_CONSTANT_ENCAPSED_STRING"
    [1]=>string(14) ""Hello World!""
    [2]=>int(1)
  }
  [4]=>
  string(1) ""
  [5]=>
  array(3) {
    [0]=>string(12) "T_WHITESPACE"
    [1]=>string(1) " "
    [2]=>int(1)
  }
  [6]=>
  array(3) {
    [0]=>string(11) "T_CLOSE_TAG"
    [1]=>string(2) "?>"
    [2]=>int(1)
  }
}

我们可以发现,源码中的空格也会原样返回,而且他的,都被转换成一个包含三部分(解释器代号、源码中的原内容、源码中第几行)的数组。

Parsing, 将Tokens转换成简单而有意义的表达式

语法分析阶段首先会丢弃Tokens Array中多余的空格,然后将剩余的Tokens转换成一个个的表达式。在PHP源码中,词法分析器最终调用的是re2c规则定义的lex_scan函数,而提供给Bison的函数则为zendlex。 而yyparse被zendparse代替。

> Bison是一种通用目的的分析器生成器。它将LALR(1)上下文无关文法的描述转化成分析该文法的C程序。 使用它可以生成解释器,编译器,协议实现等多种程序。 Bison向上兼容Yacc,所有书写正确的Yacc语法都应该可以不加修改地在Bison下工作。 它不但与Yacc兼容还具有许多Yacc不具备的特性。

> Bison分析器文件是定义了名为yyparse并且实现了某个语法的函数的C代码。 这个函数并不是一个可以完成所有的语法分析任务的C程序。 除此这外我们还必须提供额外的一些函数: 如词法分析器、分析器报告错误时调用的错误报告函数等等。 我们知道一个完整的C程序必须以名为main的函数开头,如果我们要生成一个可执行文件,并且要运行语法解析器, 那么我们就需要有main函数,并且在某个地方直接或间接调用yyparse,否则语法分析器永远都不会运行。

Compilation, 将表达式编译成Opocdes

在该阶段,Tokens被编译成一个个op_array,之前说了opcode其实是php内部实现的一种数据结构,在源码中,可以看到(version : php5.6):

struct _zend_op {
    opcode_handler_t handler; //执行时调用的处理函数
    znode_op op1; //操作数1
    znode_op op2; //操作数2
    znode_op result; //结果
    ulong extended_value; //额外的信息
    uint lineno; //源码中的行数
    zend_uchar opcode; //opcode代码
    zend_uchar op1_type; //操作数1类型
    zend_uchar op2_type; //操作数1类型
    zend_uchar result_type; //结果类型
};

而编译完成的opcode是存在op_array中的,看下op_array的源码:

struct _zend_op_array {
/* Common elements */
zend_uchar type;
const char *function_name; // 如果是用户定义的函数则,这里将保存函数的名字
zend_class_entry *scope;
zend_uint fn_flags;
union _zend_function *prototype;
zend_uint num_args;
zend_uint required_num_args;
zend_arg_info *arg_info;
/* END of common elements */

zend_uint *refcount;

zend_op *opcodes; // opcode数组
zend_uint last;

zend_compiled_variable *vars;
int last_var;

zend_uint T;

zend_uint nested_calls;
zend_uint used_stack;

zend_brk_cont_element *brk_cont_array;
int last_brk_cont;

zend_try_catch_element *try_catch_array;
int last_try_catch;
zend_bool has_finally_block;

/* static variables support */
HashTable *static_variables;

zend_uint this_var;

const char *filename;
zend_uint line_start;
zend_uint line_end;
const char *doc_comment;
zend_uint doc_comment_len;
zend_uint early_binding; /* the linked list of delayed declarations */

zend_literal *literals;
int last_literal;

void **run_time_cache;
int  last_cache_slot;

void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};

保存好op_array后,由excute方法逐条执行,在此阶段,就会调用之前提到的opcode_handler_t handler里存储的函数指针。

ZEND_API void zend_execute(zend_op_array *op_array TSRMLS_DC)
{
    if (EG(exception)) {
        return;
    } 
    zend_execute_ex(i_create_execute_data_from_op_array(op_array, 0 TSRMLS_CC) TSRMLS_CC);
}

扩展

PHP有三种方式来进行opcode的处理:CALL,SWITCH和GOTO。
PHP默认使用CALL的方式,也就是函数调用的方式,由于opcode执行是每个PHP程序频繁需要进行的操作,可以使用SWITCH或者GOTO的方式来分发, 通常GOTO的效率相对会高一些,不过效率是否提高依赖于不同的CPU。

到这里,我们的编译流程就结束了,我们的代码最终会被Parsing成:

ZEND_ECHO     'Hello World%21'

另外,我们可以借助vld插件来看程序的opcodes,在这里就不详细展开了。

参考资源

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 目录 1、基本概念 什么是CGI? 部分专业术语解释 图解静态请求和动态请求 php组成 2、PHP的生命周期 3...
    fujun_195a阅读 2,784评论 0 1
  • 转自陈明乾的博客,可能有一定更新。 转原文声明:原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 、...
    C86guli阅读 4,675评论 6 72
  • php看着很简单,但是要深入php的运行机制与原理也不是件容易的事,我们除了会使用之外还要知道它底层的工作原理,这...
    文档随手记阅读 384评论 1 11
  • 使用PHP扩展的原因: 准备工作 一:了解PHP源码目录 网上下载下来PHP 5.4版本源代码,目录结构如下: 二...
    Chuck_Hu阅读 3,695评论 1 17
  • 这里说的所有内容都属于json字符串,属于String类型 JSON的基本格式 json从总体上其实就分为两种格式...
    shenlong77阅读 616评论 0 0