Python虚拟机之异常控制流(四)

Python虚拟机中的异常控制流

先前,我们分别介绍了Python虚拟机之if控制流(一)、Python虚拟机之for循环控制流(二)Python虚拟机之while循环控制结构(三)。这一章,我们来了解一下异常机制在Python虚拟机中的实现

首先,我们来看下面的代码:

# python2.5
>>> 1 / 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: integer division or modulo by zero

  

1 / 0在Python中一定会抛出异常,这个异常时ZeroDivisionError。现在,我们来看一下1 / 0对应的字节码指令:

# cat demo5.py 
1 / 0

# python2.5
……
>>> source = open("demo5.py").read()
>>> co = compile(source, "demo5.py", "exec")
>>> import dis
>>> dis.dis(co)
  1           0 LOAD_CONST               0 (1)
              3 LOAD_CONST               1 (0)
              6 BINARY_DIVIDE       
              7 POP_TOP             
              8 LOAD_CONST               2 (None)
             11 RETURN_VALUE 

  

dis模块解析出来的字节码指令,前两行和后两行我们已经很熟悉了。这里,我们主要看一下BINARY_DIVIDE的实现

ceval.c

case BINARY_DIVIDE:
	if (!_Py_QnewFlag)
	{
		w = POP();
		v = TOP();
		x = PyNumber_Divide(v, w);
		Py_DECREF(v);
		Py_DECREF(w);
		SET_TOP(x);
		if (x != NULL)
			continue;
		break;
	}

  

w是PyIntObject对象1,v是PyIntObject对象0,x是做除法操作的结果。我们主要到在将x压入栈之后,会对x的有效性做检验,如果x是一个有效的Python对象,那么Python虚拟机将执行下一条字节码,但如果x为NULL,即不是一个有效的Python对象,那么将通过break跳出Python虚拟机那个对字节码指令进行分派的巨大switch语句。

可以看到,x的值是从PyNumber_Divide这个函数返回的,如果PyNumber_Divide返回的不是NULL,那么就皆大欢喜,程序照常执行下去,可是,如果返回的是NULL,那么是什么原因导致的返回是NULL呢?

从PyNumber_Divide,会经过一系列的动作,在这个过程中,不同的参与除法操作的对象最终将走上不同的路径,而我们的两个PyIntObject的除法路径最终会达到在PyInt_Type中定义的除法操作int_classic_div

intobject.c

static PyObject *
int_classic_div(PyIntObject *x, PyIntObject *y)
{
	long xi, yi;
	long d, m;
	//将x,y中维护的整数值转存到xi,yi中
	CONVERT_TO_LONG(x, xi);
	CONVERT_TO_LONG(y, yi);
	if (Py_DivisionWarningFlag &&
	    PyErr_Warn(PyExc_DeprecationWarning, "classic int division") < 0)
		return NULL;
	switch (i_divmod(xi, yi, &d, &m)) {
	case DIVMOD_OK:
		return PyInt_FromLong(d);
	case DIVMOD_OVERFLOW:
		return PyLong_Type.tp_as_number->nb_divide((PyObject *)x,
							   (PyObject *)y);
	default:
		return NULL;
	}
}

  

现在我们可以确定,问题出在i_divmod中,如果返回的不是DIVMOD_OK或DIVMOD_OVERFLOW,int_classic_div就回返回一个NULL。那么,我们再深入到i_divmod这个函数中

intobject.c

enum divmod_result {
	DIVMOD_OK,		/* Correct result */
	DIVMOD_OVERFLOW,	/* Overflow, try again using longs */
	DIVMOD_ERROR		/* Exception raised */
};

static enum divmod_result
i_divmod(register long x, register long y,
         long *p_xdivy, long *p_xmody)
{
	long xdivy, xmody;
	//抛出异常
	if (y == 0) {
		PyErr_SetString(PyExc_ZeroDivisionError,
				"integer division or modulo by zero");
		return DIVMOD_ERROR;
	}

	if (y == -1 && UNARY_NEG_WOULD_OVERFLOW(x))
		return DIVMOD_OVERFLOW;
	xdivy = x / y;
	xmody = x - xdivy * y;

	if (xmody && ((y ^ xmody) < 0) /* i.e. and signs differ */) {
		xmody += y;
		--xdivy;
		assert(xmody && ((y ^ xmody) >= 0));
	}
	*p_xdivy = xdivy;
	*p_xmody = xmody;
	return DIVMOD_OK;
}

  

在i_divmod方法中,我们发现如果除数为0,将调用PyErr_SetString抛出PyExc_ZeroDivisionError异常,并返回指示异常抛出的指示码DIVMOD_ERROR。在Python中,一切东西都是对象,异常也不例外。那么在Python的对象体系中,这个PyExc_ZeroDivisionError到底是属于哪一部分呢?

实际上,这个PyExc_ZeroDivisionError很简单,就是一个PyObject*,仅仅是一个指针

pyerrors.h

PyAPI_DATA(PyObject *) PyExc_ZeroDivisionError;

  

在pyerrors.h中,同时还定义了许多在异常机制中使用的PyObject*

pyerrors.h

……
PyAPI_DATA(PyObject *) PyExc_KeyboardInterrupt;
PyAPI_DATA(PyObject *) PyExc_MemoryError;
PyAPI_DATA(PyObject *) PyExc_NameError;
PyAPI_DATA(PyObject *) PyExc_OverflowError;
PyAPI_DATA(PyObject *) PyExc_RuntimeError;
PyAPI_DATA(PyObject *) PyExc_NotImplementedError;
PyAPI_DATA(PyObject *) PyExc_SyntaxError;
……

  

尽管它们都是再简单不过的PyObject*,但是在Python运行环境初始化时,它们会指向Python创建的异常类型对象,从而指明发生了什么异常

在线程状态对象中记录异常信息

在i_divmod之后,Python的执行路径会沿着PyErr_SetString、PyErr_SetObject,一直到达PyErr_Restore。在PyErr_Restore中,Python将这个异常放置到一个安全的地方:

errors.c

void PyErr_SetString(PyObject *exception, const char *string)
{
	PyObject *value = PyString_FromString(string);
	PyErr_SetObject(exception, value);
	Py_XDECREF(value);
}

void PyErr_SetObject(PyObject *exception, PyObject *value)
{
	Py_XINCREF(exception);
	Py_XINCREF(value);
	PyErr_Restore(exception, value, (PyObject *)NULL);
}

void PyErr_Restore(PyObject *type, PyObject *value, PyObject *traceback)
{
	PyThreadState *tstate = PyThreadState_GET();
	PyObject *oldtype, *oldvalue, *oldtraceback;

	if (traceback != NULL && !PyTraceBack_Check(traceback)) {
		/* XXX Should never happen -- fatal error instead? */
		/* Well, it could be None. */
		Py_DECREF(traceback);
		traceback = NULL;
	}

	//保存以前的异常信息
	oldtype = tstate->curexc_type;
	oldvalue = tstate->curexc_value;
	oldtraceback = tstate->curexc_traceback;
	//设置当前的异常信息
	tstate->curexc_type = type;
	tstate->curexc_value = value;
	tstate->curexc_traceback = traceback;
	//抛弃以前的异常信息
	Py_XDECREF(oldtype);
	Py_XDECREF(oldvalue);
	Py_XDECREF(oldtraceback);
}

    

pystate.h

#define PyThreadState_GET() (_PyThreadState_Current)

  

pystate.c

PyThreadState *_PyThreadState_Current = NULL;

  

用PyThreadState_GET宏获取当前的线程状态tstate,并将PyExc_ZeroDivisionError存放在tstate的curexc_type域,"integer division or modulo by zero"存放在tstate的curexc_value域中

Python无论多么强悍,总会在一个操作系统提供的线程中运行。真实的线程及其状态由操作系统来管理和维护,但是Python虚拟机在运行时总需要另外一些与线程相关的状态和信息,比如是否发生异常这样的信息,这些信息显然是不能由操作系统提供,而PyThreadState对象正是Python为线程准备的Python虚拟机一级保存线程状态信息的对象。在这里,当前活动线程对象对应的PyThreadState对象可以通过PyThreadState_GET来获得,在得到线程状态对象之后,就将异常信息存放在线程对象状态中

在Python启动,进行初始化的时候,会调用PyThreadState_New创建一个新的PyThreadState对象,并将其赋给_PyThreadState_Current,这个对象就是和当前活动线程关联的线程状态对象

在Python的sys标准库中,提供了一个接口,使得我们能够在异常发生时,访问Python虚拟机存放在线程对象中的异常信息 

# python2.5
……
>>> try:
...     1 / 0
... except Exception:
...     import sys
...     print(sys.exc_info()[0])
...     print(sys.exc_info()[1])
... 
<type 'exceptions.ZeroDivisionError'>
integer division or modulo by zero

  

sys.exc_info()[0]为tstate->curexc_type,sys.exc_info()[1]为tstate->curexc_value

展开栈帧

我们看到异常已经被记录在线程状态中了,那么现在可以回头看,在跳出分派字节码指令的switch块之后,发生了什么动作。这里还存在一个问题,导致跳出那个巨大的switch块的原因可能是执行完字节码之后正常跳出,也可能是发生异常后跳出,那么Python是如何区分的呢?

ceval.c

PyObject * PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
	……

	for (;;)
	{
		……
		//巨大的switch语句
		switch (opcode)
		{
		……
		} 
		……
		if (why == WHY_NOT)
		{
			if (err == 0 && x != NULL)
			{

				if (PyErr_Occurred())
					fprintf(stderr,
							"XXX undetected error\n");
				else
				{
					READ_TIMESTAMP(loop1);
					continue; //没有异常情况发生,执行下一条字节码指令

				}

			}
			//设置了why,通知虚拟机,异常发生了
			why = WHY_EXCEPTION;
			x = Py_None;
			err = 0;
		}
		……
		//尝试捕捉异常
		if (why != WHY_NOT)
			break;

	} 
}

  

在跳出switch之后,首先会检查x的值,如果x为NULL,表示有异常发生,那么Python虚拟机将why设置为WHY_EXCEPTION。这里的x是PyNumber_Divide的结果,我们刚才看到,在抛出异常之后,这个x就为NULL了。变量why实际上维护的是Python虚拟机中执行字节码指令的那个for循环内的状态。当为WHY_NOT时,表示一切正常,没有错误发生,而设置为WHY_EXCEPTION之后,表示字节码在执行过程中,有异常发生

在Python虚拟机意识到有异常发生之后,就要开始进入异常处理的流程,这个流程会涉及到PyFrameObject对象所提及的那个PyFrameObject对象链表。在Python之PyFrameObject动态执行环境这一章中,我们介绍了PyFrameObject对象是Python虚拟机对栈帧的模拟,当发生函数调用时,Python虚拟机会创建一个与被调用函数对应的PyFrameObject对象(栈帧),并通过该对象中的f_back连接到调用者对应的PyFrameObject对象,以此形成一条PyFrameObject对象的链表

现在,我们考虑一下如果函数调用时发生了异常:

# python2.5
……
>>> def h():
...     1 / 0
... 
>>> def g():
...     h()
... 
>>> def f():
...     g()
... 
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
  File "<stdin>", line 2, in g
  File "<stdin>", line 2, in h
ZeroDivisionError: integer division or modulo by zero

  

图1-1虚拟机执行函数h时的栈帧链表

可以看到,在输出信息中,出现了函数调用的信息,比如在源代码的哪一行调用了函数,调用了什么函数,这些输出信息呈现出一个链表一样的结构。在Python虚拟机处理异常的流程中,涉及了一个traceback对象,在这个对象中记录栈帧链表的信息,Python虚拟机利用这个对象来讲栈帧链表中每一个栈帧当前的状态可视化,即为上面的输出信息

当异常在h函数对应的栈帧中发生时,Python虚拟机会创建一个traceback对象,用于记录异常发生时活动栈帧的状态

ceval.c

PyObject * PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
	……

	for (;;)
	{
		……
		//巨大的switch语句
		switch (opcode)
		{
		……
		} 
		……
		if (why == WHY_EXCEPTION)
		{
			//创建traceback对象
			PyTraceBack_Here(f);

			if (tstate->c_tracefunc != NULL)
				call_exc_trace(tstate->c_tracefunc,
							   tstate->c_traceobj, f);
		}
		……
	} 
}

  

这里的tstate还是我们之前所提到的那个与当前活动线程对应的线程对象,其中的c_tracefunc是用户自定义的追踪函数,主要用于编写Python的debugger。通常情况下这个值为NULL,所以我们不考虑它,这里我们重点观察Python虚拟机究竟创建了一个怎样的traceback对象

traceback.c

int PyTraceBack_Here(PyFrameObject *frame)
{
	//获得线程状态对象
	PyThreadState *tstate = PyThreadState_GET();
	//保存线程状态对象中现在维护的traceback对象
	PyTracebackObject *oldtb = (PyTracebackObject *) tstate->curexc_traceback;
	//创建新的traceback对象
	PyTracebackObject *tb = newtracebackobject(oldtb, frame);
	if (tb == NULL)
		return -1;
	//将新的traceback对象交给线程状态对象
	tstate->curexc_traceback = (PyObject *)tb;
	Py_XDECREF(oldtb);
	return 0;
}

  

原先旧的traceback对象是保存在线程状态对象中,我们来看看这个traceback对象究竟什么样

traceback.h

typedef struct _traceback {
	PyObject_HEAD
	struct _traceback *tb_next;
	struct _frame *tb_frame;
	int tb_lasti;
	int tb_lineno;
} PyTracebackObject;

  

traceback对象和PyFrameObject对象一样,是一个链表结构,由tb_next来链接前一个traceback对象,所以我们来看看这个链表是怎么产生的呢?

traceback.c

static PyTracebackObject *
newtracebackobject(PyTracebackObject *next, PyFrameObject *frame)
{
	PyTracebackObject *tb;
	if ((next != NULL && !PyTraceBack_Check(next)) ||
			frame == NULL || !PyFrame_Check(frame)) {
		PyErr_BadInternalCall();
		return NULL;
	}
	//申请内存,创建对象
	tb = PyObject_GC_New(PyTracebackObject, &PyTraceBack_Type);
	if (tb != NULL) {
		Py_XINCREF(next);
		//建立链表
		tb->tb_next = next;
		Py_XINCREF(frame);
		tb->tb_frame = frame;
		tb->tb_lasti = frame->f_lasti;
		tb->tb_lineno = PyCode_Addr2Line(frame->f_code, 
						 frame->f_lasti);
		PyObject_GC_Track(tb);
	}
	return tb;
}

  

这里我们可以看到,next正是从线程状态对象中得到traceback对象,在newtracebackobject中,两个traceback对象被链接起来。同时,在新创建的traceback对象中,还利用tb_frame与其对应的PyFrameObject对象建立了联系。另外,还存储了当前最后执行的一条字节码指令及其在源代码中对应的行号。在PyCodeObject中,co_lnotab代表的字节码指令与源代码行号的对应关系,PyCode_Addr2Line正是利用这个co_lnotab获得了frame->f_lasti所指示的字节码指令在源代码对应的行号

Python虚拟机意识到有异常抛出,并创建traceback对象之后,它会在当前的栈帧中包裹着异常代码的except语句,如果没有找到,那么Python虚拟机会退出当前的活动栈帧,并沿着栈帧链表向上回退到上一个栈帧,在图1-2中,即从函数h对应的PyFrameObject对象沿着f_back回退到函数g对应的PyFrameObject对象。这个回退动作正在PyEval_EvalFrameEx的最后完成

ceval.c

PyObject * PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
	……
	for (;;)
	{
		……
		//巨大的switch语句
		switch (opcode)
		{
		……
		} 
		……
		//尝试捕捉异常
		if (why != WHY_NOT)//[1]
			break;
		……
	} 
	……
	if (why != WHY_RETURN)
		retval = NULL;//[2]:利用retval通知前一个栈帧有异常出现
	……
	//[3]:将线程状态对象的活动栈帧设置为当前栈帧的上一个栈帧,完成栈帧回退的动作
	tstate->frame = f->f_back;
	return retval;
}

  

可以看到,如果开发人员没有提供任何捕捉异常的动作,那么程序将执行到[1]处,这里也是for循环的结尾处,由于异常没有被捕捉到,why这个值仍然是WHY_EXCEPTION,那么会通过break动作跳出Python执行字节码的那个for循环。最后,由于异常没有被捕捉到,PyEval_EvalFrameEx的返回值将再代码的[2]处设置为NULL。同时,通过重新设置当前线程状态对象的活动栈帧,完整栈帧回退动作

这里还有一个问题PyEval_EvalFrameEx的返回值会去到哪里?当然是返回到PyEval_EvalFrameEx本身中了,PyEval_EvalFrameEx作为Python虚拟机的主要实现代码,当Python虚拟机运行时,这个函数会被递归调用,从函数名我们也可以看出一些端倪,这是一个与某个PyFrameObject对象的执行相关的函数,既然PyFrameObject对象有一个链表,那么PyEval_EvalFrameEx也就只能通过递归与链表结构对应了

也就是当Python虚拟机执行函数g时,它在PyEval_EvalFrameEx中执行与g对应的PyFrameObject对象中的字节码指令序列。当在g中调用h时,Python虚拟机为h创建新的PyFrameObject对象,同时递归调用PyEval_EvalFrameEx,而这次执行的是与h相对应的PyFrameObject对象中的字节码指令序列了。当在函数h中发生异常,导致PyEval_EvalFrameEx结束,自然要返回与函数g对应的PyEval_EvalFrameEx中。由于在返回时,设置的retval为NULL,所以Python虚拟机回到与g对应的PyEval_EvalFrameEx这中再次意识到有异常发生。接下来的动作就顺理成章了,创建traceback对象,寻找程序员指定的except,如果没有异常捕捉动作,那么g也要退出PyEval_EvalFrameEx,而回到与f对应的PyEval_EvalFrameEx

这个沿着栈帧不断回退的过程我们称之为栈帧展开,在栈帧展开的过程中,Python虚拟机不断创建各个栈帧对应的traceback对象,并将其形成链表

图1-2trackback对象链表与PyFrameObject对象链表

由于我们没有设置任何的异常捕捉代码,Python虚拟机的执行流程会一直返回到PyRun_SimpleFileExFlags,这个函数与Python运行时初始化有关

 pythonrun.c  

int PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit,
			PyCompilerFlags *flags)
{
	……
	if (maybe_pyc_file(fp, filename, ext, closeit)) {
		……
	} else {
		//PyRun_FileExFlags将最终调用PyEval_EvalFrameEx
		v = PyRun_FileExFlags(fp, filename, Py_file_input, d, d,
				      closeit, flags);
	}
	if (v == NULL) {
		PyErr_Print();
		return -1;
	}
	……
	return 0;
}

  

PyRun_FileExFlags返回值就是PyEval_EvalFrameEx返回的那个NULL,所以接下来,会调用PyErr_Print。正是在这个PyErr_Print中,Python虚拟机从线程状态信息中取出其维护的traceback对象,并遍历traceback对象链表,逐个输出其中的信息

 

posted @ 2018-08-19 16:53  北洛  阅读(691)  评论(0编辑  收藏  举报