Press "Enter" to skip to content

深入理解PHP原理之异常机制

PHP的异常机制的原理是什么?
在PHP每一个可独立执行的op array最后的ZEND_HANDLE_EXCEPTION是用来干什么呢?
让我们从一个问题说起, 上周的时候, blue5tar提了一个问题:"对于下面的代码, onError明明执行了, 但是onException却没有执行, 为什么?".

<?php
function onError($errCode, $errMesg, $errFile, $errLine) {
    echo "Error Occurred\n";
    throw new Exception($errMesg);
}
function onException($e) {
    echo $e->getMessage();
}
set_error_handler("onError");
set_exception_handler("onException");
/* 我从不会以我的名字命名文件, 所以这个文件不存在 */
require("laruence.php");

运行结果:

Error Occurred
PHP Fatal error:  main(): Failed opening required 'laruence.php'

首先, 我们要知道, Require在包含一个找不到的问题的时候, 会前后抛出俩个错误,

1. WARNING : 在PHP试图打开这个文件的时候抛出.
2. E_COMPILE_ERROR : 从PHP打开文件的函数返回失败以后抛出.

而我们知道, set_error_handler是不能捕获E_COMPILE_ERROR错误的:

The following error types cannot be handled with a user defined function: E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING, and most of E_STRICT raised in the file where set_error_handler() is called.

所以, 在onError中, 只能捕获到第一个WARNING错误, 而在onError中抛出的异常, 为什么没有被默认exception_handler捕获呢?
这就要说说PHP的异常机制了.
了解opcode(深入理解PHP原理之Opcodes的同学都知道, 在PHP5.3以前, 每一个可独立运行的op array(文件, 函数, 方法)的最后一条opcode都是ZEND_HANDLE_EXCEPTION, 而这个opcode是做什么用的呢?
原来在PHP中, 当有异常被throw的时候, 会跳到每一个op array的最后一行, 来执行这条ZEND_HANDLE_EXCEPTION, 伪码如下:

void on_throw_exception(zval *exception TSRMLS_DC) {
1. 判断是否已经有异常抛出
2. 记录exception
3. 记录下一条要执行的op line的序号
4. 下一条要执行的op line序号 = 当前op array的最后一条
}

恩, 就和改写ip寄存器一样, 改写下一条要执行的op line的序号, 就改变了程序的流向, 这样, 就会进入到了ZEND_HANDLE_EXCEPTION的处理逻辑中.
而在ZEND_HANDLE_EXCEPTION中, 会判断这个异常是否在try catch中,

如果是    则把下一条要执行的op line, 置为第一个catch的op line, 并继续执行.
如果不是  则销毁一些不需要的变量, 和opline,  然后直接结束执行过程.

有的同学要问了:"那set_exception_handler设置的异常默认处理函数(user_exception_handler)什么时候起作用呢?"
恩, 是在执行完成退出执行LOOP以后才判断是否有默认异常处理函数, 如果有才调用:

//执行
zend_execute(EG(active_op_array) TSRMLS_CC);
if (EG(exception)) {
	if (EG(user_exception_handler)) {
		调用用户定义的默认异常处理函数
	} else {
		未捕获的异常
	}
} else {
	没有异常
}
destroy_op_array(EG(active_op_array) TSRMLS_CC);
efree(EG(active_op_array));

PHP异常流程
PHP异常流程

注: 图中有一处不严谨, 即在确定是否最后一个catch块的时候, 会同时判断(is_a), 如果是才进入最后一个catch块执行.
而PHP在遇到Fatal Error的时候, 会直接zend_bailout, 而zend_bailout会导致程序流程直接跳过上面代码段, 也可以理解为直接exit了(longjmp), 这就导致了user_exception_handler没有机会发生作用.
了解到这些, 我想文章开头的问题的为什么? 也就很清晰了吧?
最后, 关于ZEND_HANDLE_EXCEPTION, 也许有同学会有疑问: 如果是这样, 那为什么每一个可独立执行的op array最后都有这个ZEND_HANDLE_EXCEPTION呢? 最简单的, 如果一个函数中不会throw, 那么这个opcode 是明显不需要的啊? 嘿嘿, 你很聪明, PHP 5.3开始, 已经按照你的想法调整了.. 只有在throw时刻, 才会动态的生成ZEND_HANDLE_EXCEPTION opline.
PHP5 changelog:

Changed exception handling. Now each op_array doesn't contain ZEND_HANDLE_EXCEPTION opcode in the end. (Dmitry)

38 Comments

  1. 蔡志超
    蔡志超 August 14, 2022

    有个问题,当发生内存超标时,有没有办法最后再执行一段自定义函数来dump需要的变量或者静态变量数据来排查内存使用问题或内存泄漏

  2. 83Hung
    83Hung August 11, 2017

    Hello blogger, i must say you have hi quality articles here.
    Your page can go viral. You need initial traffic boost only.
    How to get it? Search for; Mertiso’s tips go viral

  3. php错误处理 – 西米糍
    php错误处理 – 西米糍 September 18, 2014

    […] 上一段代ç ä¸­ï&frac14;Œæˆ‘们希望æ•èŽ·phpå‘生的所有错误ï&frac14;Œç„¶åŽè®°å&frac12;•logå‘é€å‘Šè­¦é‚®ä»¶ã€‚ä&frac12;†çŽ°å®žæ€»ä¸å°&frac12;如人æ„ï&frac14;šä¸æ˜¯æ‰€æœ‰é”™è¯¯éƒ&frac12;å¯ä»¥è¢«set_error_handlerè®&frac34;ç&frac12;®çš„错误å¥æŸ„æ•èŽ·ã€‚å&frac14;•ç”¨å®˜æ–&sup1;手册原文ï&frac14;šâ€œä»¥ä¸‹çº§åˆ«çš„错误ä¸èƒ&frac12;由用户定ä&sup1;‰çš„å‡&frac12;æ•°æ¥å¤„ç†ï&frac14;š E_ERROR〠E_PARSEã€E_CORE_ERROR〠E_CORE_WARNING〠E_COMPILE_ERROR〠E_COMPILE_WARNINGï&frac14;Œå’Œåœ¨è°ƒç”¨ set_error_handler()å‡&frac12;数所在文件中产生的大多数 E_STRICTâ€ã€‚官æ–&sup1;æ&sup2;¡æœ‰ç»™å‡ºåŽŸå› ï&frac14;Œä&frac12;†ä¸éš&frac34;看出这些错误è¦ä&sup1;ˆæ˜¯è¿è¡Œæ—¶çš„致å‘&frac12;错误ï&frac14;Œè¦ä&sup1;ˆæ˜¯php核心或ç&frac14;–译时的错误ï&frac14;Œå› æ­¤ä&sup1;Ÿä¸éš&frac34;猜æƒ&sup3;ï&frac14;šå¯&sup1;于è¿è¡Œæ—¶çš„致å‘&frac12;错误ï&frac14;Œphp直接中断ï&frac14;Œå¯&frac14;致了错误处ç†å‡&frac12;æ•°æ&sup2;¡æœ‰æœºä&frac14;šæ‰§è¡Œï&frac14;ˆè¯¦è§Laruence的这篇文章ï&frac14;‰ï&frac14;›å¯&sup1;于php核心或ç&frac14;–译时候å‘生的错误ï&frac14;Œæˆ‘们的php脚本æ &sup1;本ä¸èƒ&frac12;执行ï&frac14;Œæ‰€ä»¥set_error_handleræ&sup2;¡æœ‰èµ·ä&frac12;œç”¨ã€‚ […]

  4. […] 上一段代ç ä¸­ï&frac14;Œæˆ‘们希望æ•èŽ·phpå‘生的所有错误ï&frac14;Œç„¶åŽè®°å&frac12;•logå‘é€å‘Šè­¦é‚®ä»¶ã€‚ä&frac12;†çŽ°å®žæ€»ä¸å°&frac12;如人æ„ï&frac14;šä¸æ˜¯æ‰€æœ‰é”™è¯¯éƒ&frac12;å¯ä»¥è¢«set_error_handlerè®&frac34;ç&frac12;®çš„错误å¥æŸ„æ•èŽ·ã€‚å&frac14;•ç”¨å®˜æ–&sup1;手册原文ï&frac14;šâ€œä»¥ä¸‹çº§åˆ«çš„错误ä¸èƒ&frac12;由用户定ä&sup1;‰çš„å‡&frac12;æ•°æ¥å¤„ç†ï&frac14;š E_ERROR〠E_PARSEã€E_CORE_ERROR〠E_CORE_WARNING〠E_COMPILE_ERROR〠E_COMPILE_WARNINGï&frac14;Œå’Œåœ¨è°ƒç”¨ set_error_handler()å‡&frac12;数所在文件中产生的大多数 E_STRICTâ€ã€‚官æ–&sup1;æ&sup2;¡æœ‰ç»™å‡ºåŽŸå› ï&frac14;Œä&frac12;†ä¸éš&frac34;看出这些错误è¦ä&sup1;ˆæ˜¯è¿è¡Œæ—¶çš„致å‘&frac12;错误ï&frac14;Œè¦ä&sup1;ˆæ˜¯php核心或ç&frac14;–译时的错误ï&frac14;Œå› æ­¤ä&sup1;Ÿä¸éš&frac34;猜æƒ&sup3;ï&frac14;šå¯&sup1;于è¿è¡Œæ—¶çš„致å‘&frac12;错误ï&frac14;Œphp直接中断ï&frac14;Œå¯&frac14;致了错误处ç†å‡&frac12;æ•°æ&sup2;¡æœ‰æœºä&frac14;šæ‰§è¡Œï&frac14;ˆè¯¦è§Laruence的这篇文章ï&frac14;‰ï&frac14;›å¯&sup1;于php核心或ç&frac14;–译时候å‘生的错误ï&frac14;Œæˆ‘们的php脚本æ &sup1;本ä¸èƒ&frac12;执行ï&frac14;Œæ‰€ä»¥set_error_handleræ&sup2;¡æœ‰èµ·ä&frac12;œç”¨ã€‚ […]

  5. php错误处理 | Avery
    php错误处理 | Avery November 20, 2013

    […] 上一段代码中,我们希望捕获php发生的所有错误,然后记录log发送告警邮件。但现实总不尽如人意:不是所有错误都可以被set_error_handler设置的错误句柄捕获。引用官方手册原文:“以下级别的错误不能由用户定义的函数来处理: E_ERROR、 E_PARSE、E_CORE_ERROR、 E_CORE_WARNING、 E_COMPILE_ERROR、 E_COMPILE_WARNING,和在调用 set_error_handler()函数所在文件中产生的大多数 E_STRICT”。官方没有给出原因,但不难看出这些错误要么是运行时的致命错误,要么是php核心或编译时的错误,因此也不难猜想:对于运行时的致命错误,php直接中断,导致了错误处理函数没有机会执行(详见Laruence的这篇文章);对于php核心或编译时候发生的错误,我们的php脚本根本不能执行,所以set_error_handler没有起作用。 […]

  6. 简果
    简果 July 24, 2013

    对于一个请求只执行一次php 函数,有没有必要定义异常捕获?自己定义异常对比内置的错误处理有什么优势啊?

  7. […] 正如文章开头提问中的: “它的性能如何?”, 异常机制确实要比返回状态码的方式昂贵一些, 对于C++来说, 在异常发生的时候, 还要发生堆栈解退(对于PHP来说, 没有这个逻辑, 具体的大家可以参看我之间写的一篇文章: 深入理解PHP原理之异常机制). […]

  8. […] 正如文章开头提问中的: “它的性能如何?”, 异常机制确实要比返回状态码的方式昂贵一些, 对于C++来说, 在异常发生的时候, 还要发生堆栈解退(对于PHP来说, 没有这个逻辑, 具体的大家可以参看我之间写的一篇文章: 深入理解PHP原理之异常机制). […]

  9. zhkzyth
    zhkzyth January 13, 2012

    @laruence “默认函数的执行是在执行器完成后, 文章中有叙及.” 是不是可以这样理解:
    “恩, 是在执行完成退出执行LOOP以后才判断是否有默认异常处理函数, 如果有才调用:”文中说的loop,指的是function onError(),而这个时候抛出的错误,按照流程图来理解的话,应该走到判断有没有exception_handler这一步的。因为是执行完loop才去判断自定义的exception_handler,所以判断为false,抛出php fatal error的错误,然后退出程序….

  10. xxx
    xxx November 19, 2011

    博主,能不能改进下php的报错啊,像这个 Fatal error: Exception thrown without a stack frame in Unknown on line 0 ,能不能准确的定位呢?现在出现这个错只能猜啊,还猜不对

  11. Lun
    Lun May 31, 2011

    楼主你的E-R图是 那个工具画得啊 ,好漂亮o ·

  12. seven
    seven May 15, 2011

    另外可否详细介绍一下try cache的机制呢?我也google了很多文章看过,结果还是神魂颠倒.
    总感觉try cache是个鸡肋,习惯了js的try cache然后用php的感觉总是达不到预期的效果.

  13. seven
    seven May 15, 2011

    那既然如此,当程序中遇到E_COMPILE_ERROR错误时我们该如何捕捉呢?
    如果设置一下Error_reporting让他不报错那是哑巴吃黄连,可是如果直接把错误输出到屏幕这也太那个啥了….
    所以可否做到不管出现任何错误的时候都写入自己定义的log文件内呢?同时屏幕上不要报错出来.

  14. Elwood Susana
    Elwood Susana May 7, 2011

    Excellent job. The work is very much of particular focus to our team.

  15. gogo
    gogo February 9, 2011

    请问如何防止局部内存泄露问题,例如异常之前emalloc(n)的空间,在异常情况下如何释放.

  16. 雪候鸟
    雪候鸟 February 8, 2011

    @小兴 这个不行, fatal是致命的, 影响到了PHP本身执行的错误, 这种错误无法捕获。

  17. 小兴
    小兴 February 7, 2011

    您好,请教一个问题,我发现PHP是无法捕捉到Fatal Error致命错误的,不知道是否有其他方法能捕捉到呢?多谢!顺祝年快乐:)

  18. laruence
    laruence August 31, 2010

    @xiaobao 哦? 你的PHP是什么版本?

  19. xiaobao
    xiaobao August 31, 2010

    getMessage();
    }
    set_error_handler(“onError”);
    set_exception_handler(“onException”);
    /* 我从不会以我的名字命名文件, 所以这个文件不存在 */
    require(“laruence.php”);
    ?>
    上面的代码会输出什么呢?
    我试了下,结果抛出的异常也被捕捉到了!!
    什么原因,能解释下吗?万分感谢。。

  20. laruence
    laruence August 26, 2010

    @xiaobao 默认函数的执行是在执行器完成后, 文章中有叙及.

  21. xiaobao
    xiaobao August 26, 2010

    上面的流程图对吗?
    如果没有一个catch捕捉到异常,那么还会判断有没有exception_handler,如果没有,才最终终止执行,弹出uncautch…

  22. 烂页
    烂页 August 18, 2010

    鸟哥威武。我基本看后不留言。今天还是留言吧!哈哈!

  23. simplechen
    simplechen August 8, 2010

    流程图是用什么工具做的?google docs 中的绘图吗?

  24. MoontoC
    MoontoC August 4, 2010

    对于解析错误致命错误之类,只有shutdown function可以接收或者拦截
    但是理想条件下是该函数是绝对的第一个shutdown function, 不然会变得无法预估发生什么

  25. trylife
    trylife August 3, 2010

    沙发后再仔细看

Comments are closed.