注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

python异常处理  

2017-09-04 21:24:30|  分类: python |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、异常处理
最早接触异常处理是在C++中遇到的(原因是个人本身就只会这个语言),C++标准对于编译器具体如何实现异常处理并没有规定,所以不同的编译器对于这个特性的实现也各不相同。大致来说VC对于这个的实现相对比较直观,就是在函数的开始安装异常处理相关的数据结构,这个安装随着程序的运行而进行,因而是有运行时消耗的;GCC的实现在运行时没有负担,只有在异常真正抛出的时候通过查找异常栈帧来完成,这种实现需要在编译时生成更多的信息来供运行时查询。
在python中,同样内置了异常处理机制,在没有查看python的具体实现之前,可以猜测它的实现应该更类似于VC的实现方法,就是为每个异常模块在运行时安装对应的异常栈帧,在异常发生的时候根据异常发生的位置查找对应的异常处理接口。毕竟这种实现相对比较直观简单,而且脚本语言本身对于运行时效率的要求比C++要低一些。这里顺便说一下:linux内核中访问用户态数据的异常处理同样是通过编译时生成异常然后运行时查表完成的。所以具体怎么实现并没必要觉得羞愧难当,大家归根到底都是查表啦。
和C++的异常处理想比较,pyhon有特殊的地方,它可以提供更为友好的运行时异常处理机制,这种运行时异常在C++中典型场景就是空指针访问、除零异常,因为C++语言本身不内置处理这两个异常,但是python可以。由于python并没有提供指针机制,所以空指针访问这个就不太可能发生;剩下的就是除零异常了。这种异常并不是程序主动抛出的业务异常,而是最早发生在CPU中,直接派发给CPU的硬件异常处理机制,经过操作系统,最后才是应用程序。它和程序内的异常处理有本质区别,也就是说,python如果要接手这种异常,就需要采用一种和程序内异常不同的处理机制。

二、python的内置异常实现
1、python对于异常的语法定义
可能是由于python语法使用了类似于正则表达式的描述规则,这个定义看起来比较简洁,不过这不是重点,关键是要看怎么实现。
Python-3.6.1\Grammar\Grammar.txt
try_stmt: ('try' ':' suite
           ((except_clause ':' suite)+
            ['else' ':' suite]
            ['finally' ':' suite] |
           'finally' ':' suite))
with_stmt: 'with' with_item (',' with_item)*  ':' suite
with_item: test ['as' expr]
# NB compile.c makes sure that the default except clause is last
except_clause: 'except' [test ['as' NAME]]
2、语义分析
好在代码实现中有完整的注释说明,不用再从代码揣测具体的实现意图,照例把注释贴在这里:
Python-3.6.1\Python\compile.c
/*
   Code generated for "try: S except E1 as V1: S1 except E2 as V2: S2 ...":
   (The contents of the value stack is shown in [], with the top
   at the right; 'tb' is trace-back info, 'val' the exception's
   associated value, and 'exc' the exception.)

   Value stack          Label   Instruction     Argument
   []                           SETUP_EXCEPT    L1
   []                           <code for S>
   []                           POP_BLOCK
   []                           JUMP_FORWARD    L0

   [tb, val, exc]       L1:     DUP                             )
   [tb, val, exc, exc]          <evaluate E1>                   )
   [tb, val, exc, exc, E1]      COMPARE_OP      EXC_MATCH       ) only if E1
   [tb, val, exc, 1-or-0]       POP_JUMP_IF_FALSE       L2      )  如果异常不匹配,继续下一个异常处理匹配。
   [tb, val, exc]               POP
   [tb, val]                    <assign to V1>  (or POP if no V1)
   [tb]                         POP
   []                           <code for S1>
                                JUMP_FORWARD    L0 本异常处理完成之后,跳过所有异常handler匹配

   [tb, val, exc]       L2:     DUP
   .............................etc.......................

   [tb, val, exc]       Ln+1:   END_FINALLY     # re-raise exception

   []                   L0:     <next statement>

   Of course, parts are not generated if Vi or Ei is not present.
*/
static int
compiler_try_except(struct compiler *c, stmt_ty s)
这里可以看到实现的确是在try的地方生成一个
SETUP_EXCEPT    L1
指令,指令的参数为第一个异常捕捉点(也就是第一个“except”位置)。
3、SETUP_EXCEPT指令对应的解释代码
每次添加一个新的block对应的异常处理handler时,PyFrame_BlockSetup会自动增加当前函数内部f->f_iblock++的值,相当于一个异常块在函数内唯一的运行时编号。
Python-3.6.1\Python\ceval.c
        TARGET(SETUP_LOOP)
        TARGET(SETUP_EXCEPT)
        TARGET(SETUP_FINALLY) {
            /* NOTE: If you add any new block-setup opcodes that
               are not try/except/finally handlers, you may need
               to update the PyGen_NeedsFinalizing() function.
               */

            PyFrame_BlockSetup(f, opcode, INSTR_OFFSET() + oparg,
                               STACK_LEVEL());
            DISPATCH();
        }

Python-3.6.1\Objects\frameobject.c
void
PyFrame_BlockSetup(PyFrameObject *f, int type, int handler, int level)
{
    PyTryBlock *b;
    if (f->f_iblock >= CO_MAXBLOCKS)
        Py_FatalError("XXX block stack overflow");
    b = &f->f_blockstack[f->f_iblock++];
    b->b_type = type;
    b->b_level = level;
    b->b_handler = handler;
}
4、异常抛出时的处理逻辑
异常发生之后,通过查找当前的函数全局f_iblock值(也就是当前最顶层的异常索引)来执行最接近额异常处理handler:
#ifdef CASE_TOO_BIG
        default: switch (opcode) {
#endif
        TARGET(RAISE_VARARGS) {
            PyObject *cause = NULL, *exc = NULL;
            switch (oparg) {
            case 2:
                cause = POP(); /* cause */
            case 1:
                exc = POP(); /* exc */
            case 0: /* Fallthrough */
                if (do_raise(exc, cause)) {
                    why = WHY_EXCEPTION;
                    goto fast_block_end;
                }
                break;
            default:
                PyErr_SetString(PyExc_SystemError,
                           "bad RAISE_VARARGS oparg");
                break;
            }
            goto error;
        }
……
fast_block_end:
        assert(why != WHY_NOT);

        /* Unwind stacks if a (pseudo) exception occurred */
        while (why != WHY_NOT && f->f_iblock > 0) {
            /* Peek at the current block. */
            PyTryBlock *b = &f->f_blockstack[f->f_iblock - 1];

            assert(why != WHY_YIELD);
            if (b->b_type == SETUP_LOOP && why == WHY_CONTINUE) {
                why = WHY_NOT;
                JUMPTO(PyLong_AS_LONG(retval));
                Py_DECREF(retval);
                break;
            }
……
            /* Now we have to pop the block. */
            f->f_iblock--;

            if (b->b_type == EXCEPT_HANDLER) {
                UNWIND_EXCEPT_HANDLER(b);
                continue;
            }

三、运行时除零异常
1、运行时除零
按照常规的理解,除零异常应该发生在运行时,找了下代码,发现没有地方安装运行时除零的异常信号SIGFPE的处理函数。gdb运行python,除零时调试器也没有收到信号。情况看起来有点诡异。
tsecer@harry: cat /home/tsecer/CodeTest/python.div.zero/python.div.zero.py
var = 10 / 0 
tsecer@harry: ./python -m dis /home/tsecer/CodeTest/python.div.zero/python.div.zero.py 
  1           0 LOAD_CONST               0 (10)
              2 LOAD_CONST               1 (0)
              4 BINARY_TRUE_DIVIDE
              6 STORE_NAME               0 (var)
              8 LOAD_CONST               2 (None)
             10 RETURN_VALUE
tsecer@harry: ./python  /home/tsecer/CodeTest/python.div.zero/python.div.zero.py 
Traceback (most recent call last):
  File "/home/tsecer/CodeTest/python.div.zero/python.div.zero.py", line 1, in <module>
    var = 10 / 0 
ZeroDivisionError: division by zero
tsecer@harry: 
2、除法的运行
从指令代码来看,除法使用的是BINARY_TRUE_DIVIDE指令,所以原因可能是隐藏在这个指令的内部。对于整数的除法,指令对应的解释函数为long_true_divide,同样这里只看注释就行
static PyObject *
long_true_divide(PyObject *v, PyObject *w)
{
……
       In more detail:

       0. For any a, a/0 raises ZeroDivisionError; for nonzero b, 0/b
       returns either 0.0 or -0.0, depending on the sign of b.  For a and
       b both nonzero, ignore signs of a and b, and add the sign back in
       at the end.  Now write a_bits and b_bits for the bit lengths of a
       and b respectively (that is, a_bits = 1 + floor(log_2(a)); likewise
       for b).  Then
……
}
也就是说,除法的执行会预先进行判断,对于被除数为零的情况根本不会调用CPU的除法运算,所以也就不会触发CPU的运行时除零异常。
浮点数的除法同样有该判断
static PyObject *
float_div(PyObject *v, PyObject *w)
{
    double a,b;
    CONVERT_TO_DOUBLE(v, a);
    CONVERT_TO_DOUBLE(w, b);
    if (b == 0.0) {
        PyErr_SetString(PyExc_ZeroDivisionError,
                        "float division by zero");
        return NULL;
    }
    PyFPE_START_PROTECT("divide", return 0)
    a = a / b;
    PyFPE_END_PROTECT(a)
    return PyFloat_FromDouble(a);
}

四、运行时的SIGINT处理
在系统启动时,专门注册了SIGINT的处理函数,所以在python交互式界面中不能通过 ctrl+C 来退出进程
Python-3.6.1\Modules\signalmodule.c
PyMODINIT_FUNC
PyInit__signal(void)
{
……
    if (Handlers[SIGINT].func == DefaultHandler) {
        /* Install default int handler */
        Py_INCREF(IntHandler);
        Py_SETREF(Handlers[SIGINT].func, IntHandler);
        old_siginthandler = PyOS_setsig(SIGINT, signal_handler);
    }
……
}
五、操作系统如何处理ctrl + d 组合键
1、组合键的识别
tsecer@harry: stty -a
speed 38400 baud; rows 24; columns 80; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = M-^?; eol2 = M-^?;
swtch = M-^?; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W;
lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 hupcl -cstopb cread -clocal -crtscts -cdtrdsr
-ignbrk brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff
-iuclc ixany imaxbel iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt
echoctl echoke
tsecer@harry: 
linux-2.6.32.60\drivers\char\n_tty.c
static inline void n_tty_receive_char(struct tty_struct *tty, unsigned char c)
{
……
if (c == EOF_CHAR(tty)) {
if (tty->read_cnt >= N_TTY_BUF_SIZE)
return;
if (tty->canon_head != tty->read_head)
set_bit(TTY_PUSH, &tty->flags);
c = __DISABLED_CHAR;
goto handle_newline;
}
2、组合键的读取
这里只注意一点,就是即使是ctrl+d组合生成的__DISABLED_CHAR也只有一次被访问的机会。
static ssize_t n_tty_read(struct tty_struct *tty, struct file *file,
unsigned char __user *buf, size_t nr)
{
……
if (tty->icanon) {
/* N.B. avoid overrun if nr == 0 */
while (nr && tty->read_cnt) {
……
c = tty->read_buf[tty->read_tail];
spin_lock_irqsave(&tty->read_lock, flags);
tty->read_tail = ((tty->read_tail+1) &
 (N_TTY_BUF_SIZE-1));
tty->read_cnt--;
……
if (!eol || (c != __DISABLED_CHAR)) {
if (tty_put_user(tty, c, b++)) {
retval = -EFAULT;
b--;
break;
}
nr--;
}

3、用户态读取的效果
也就是说,所谓的 EOF也是一次性的,并且可以和之前缓存的内容合并,合并时它只相当于一个换行。或者说,它导致读取缓冲区满,而读取时不将这个字符传递给用户态,所以如果只输入这一个字符,read返回值就为0,而这个通常用户态程序会认为是EOF的标志。
tsecer@harry: cat read.eof.cpp 
#include <unistd.h>
#include <stdio.h>

int main()
{
while (true)
{
 char buff[1024];
 int iread = read(0, buff, sizeof(buff));
 printf("iread %d\n", iread);
}
return 0;

}
tsecer@harry: g++ read.eof.cpp
tsecer@harry: ./a.out 
diread 1
iread 0

  评论这张
 
阅读(18)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017