IPython Notebook架构之Kernel

14109阅读 0评论2012-11-25 HyryStudio
分类:Python/Ruby

IPython Notebook中输入的代码经由浏览器发送给Web服务器,再由Web服务器发送消息到IPython的Kernel执行代码,在Kernel中执行代码所产生的输出会再发送给Web服务器从而发送给浏览器,完成整个运行过程。Web服务器和Kernel之间采用进行通信。下面为其通信的示意图:

图中,Kernel经由绿色的DEAL-ROUTER通道接收来自Web服务器的命令消息,并返回应答消息。通过红色的PUB-SUB通道传输被执行代码所产生的输出信息。

在Kernel中,用户代码在一个用户环境(字典)中执行,通常无法获得关于Kernel的信息。但是由于用户代码和Kernel在同一进程中执行,因此我们可以通过一些特殊的代码研究Kernel是如何接收、运行并返回消息的。

Kernel中的Socket对象

我们可以通过gc模块的get_objects()遍历进程中所有的对象,找到我们需要的对象:

import gc
def get_objects(class_name):
    return [o for o in gc.get_objects() if type(o).__name__ == class_name]
kapp = get_objects("IPKernelApp")[0]

Kernel的最上层是一个IPKernelApp对象,上面我们通过get_objects()找到它。它的shell_socketiopub_socket分别用于接收命令和广播代码执行输出,对应于图中的绿色和红色端口。

kapp.shell_socket, kapp.iopub_socket
(, )

在Notebook中执行print时,会经由iopub_socket将输出的内容传送给Web服务器,最终在Notebook界面中显示。print语句实际上会调用sys.stdout完成输出工作。让我们看看Kernel中的sys.stdout是什么对象:

import sys
print sys.stdout
print sys.stdout.pub_socket


可以看出sys.stdout是一个对kapp.iopub_socket进行包装的OutStream对象。下面是输出错误信息的sys.stderr的内容,可以看出它和sys.stdout使用同一个Socket对象。

print sys.stdout
print sys.stderr.pub_socket


Kernel中的线程

下面让我们看看Kernel中的各个线程。通过threading.enumerate()可以获得当前进程中的所有线程:

import threading
threading.enumerate()
[<_MainThread(MainThread, started -1219512640)>,
 ,
 ,
 ]

下面是各个线程所完成的工作:

只需要在IPython代码中搜索Heartbeat、ParentPollerUnix和HistorySaving等,就可以找到这些线程的代码,这里就不再多做分析了。下面让我们着重看看主线程是如何执行用户代码的。

用户代码的执行

我们可以通过在用户代码中执行traceback.print_stack()输出整个执行堆栈:

import traceback
traceback.print_stack()
  File "", line 1, in 
  File "/usr/local/lib/python2.7/dist-packages/IPython/zmq/ipkernel.py", line 928, in main
    app.start()
  File "/usr/local/lib/python2.7/dist-packages/IPython/zmq/kernelapp.py", line 348, in start
    ioloop.IOLoop.instance().start()
  File "/usr/local/lib/python2.7/dist-packages/zmq/eventloop/ioloop.py", line 340, in start
    self._handlers[fd](fd, events)
  File "/usr/local/lib/python2.7/dist-packages/zmq/eventloop/zmqstream.py", line 420, in _handle_events
    self._handle_recv()
  File "/usr/local/lib/python2.7/dist-packages/zmq/eventloop/zmqstream.py", line 452, in _handle_recv
    self._run_callback(callback, msg)
  File "/usr/local/lib/python2.7/dist-packages/zmq/eventloop/zmqstream.py", line 394, in _run_callback
    callback(*args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/IPython/zmq/ipkernel.py", line 268, in dispatcher
    return self.dispatch_shell(stream, msg)
  File "/usr/local/lib/python2.7/dist-packages/IPython/zmq/ipkernel.py", line 236, in dispatch_shell
    handler(stream, idents, msg)
  File "/usr/local/lib/python2.7/dist-packages/IPython/zmq/ipkernel.py", line 371, in execute_request
    shell.run_cell(code, store_history=store_history, silent=silent)
  File "/usr/local/lib/python2.7/dist-packages/IPython/core/interactiveshell.py", line 2612, in run_cell
    interactivity=interactivity)
  File "/usr/local/lib/python2.7/dist-packages/IPython/core/interactiveshell.py", line 2691, in run_ast_nodes
    if self.run_code(code):
  File "/usr/local/lib/python2.7/dist-packages/IPython/core/interactiveshell.py", line 2741, in run_code
    exec code_obj in self.user_global_ns, self.user_ns
  File "", line 2, in 
    traceback.print_stack()

通过这个执行堆栈,我们可以看到用户代码是如何被调用的。

首先,在KernelApp对象的start()中,调用ZeroMQ中的ioloop.start()处理来自shell_socket的消息。当从Web服务器接收到execute_request消息时,将调用kernel.execute_request()方法。

kapp.kernel.execute_request
>

execute_request()中调用shell对象的如下方法最终执行用户代码:

print kapp.kernel.shell.run_cell
print kapp.kernel.shell.run_ast_nodes
print kapp.kernel.shell.run_code
>
>
>

shell对象在其user_global_nsuser_ns属性在执行代码,这两个字典就是用户代码的执行环境,实际上它们是同一个字典:

print globals() is kapp.kernel.shell.user_global_ns
print globals() is kapp.kernel.shell.user_ns
True
True

查看shell_socket的消息

我们还可以利用inspect.stack()获得前面的执行堆栈中的各个frame对象,从而查看堆栈中的局域变量的内容,这样可以观察到Kernel经由shell_socket接收的回送的消息:

import inspect
frames = {}
for info in inspect.stack():
    if info[3] == "dispatch_shell":
        frames["request"] = info[0]
    if info[3] == "execute_request":
        frames["reply"] = info[0]
print "hello world"
hello world

下面是Kernel接收到的消息:

frames["request"].f_locals["msg"]
{'buffers': [],
 'content': {u'allow_stdin': False,
  u'code': u'import inspect\nframes = {}\nfor info in inspect.stack():\n    if info[3] == "dispatch_shell":\n        frames["request"] = info[0]\n    if info[3] == "execute_request":\n        frames["reply"] = info[0]',
  u'silent': False,
  u'user_expressions': {},
  u'user_variables': []},
 'header': {u'msg_id': u'EAB8E58D574B406C9B8C4C7954A677E3',
  u'msg_type': u'execute_request',
  u'session': u'4C4B18395EF44483B912275A5F036290',
  u'username': u'username'},
 'metadata': {},
 'msg_id': u'EAB8E58D574B406C9B8C4C7954A677E3',
 'msg_type': u'execute_request',
 'parent_header': {}}

下面是Kernel对上述消息的应答:

frames["reply"].f_locals["reply_msg"]
{'content': {'execution_count': 31,
  'payload': [],
  'status': u'ok',
  'user_expressions': {},
  'user_variables': {}},
 'header': {'date': datetime.datetime(2012, 11, 25, 17, 13, 18, 21057),
  'msg_id': '2963ebb0-cbed-48d7-b833-9b6a829b03bd',
  'msg_type': u'execute_reply',
  'session': u'eac59565-34ff-4618-b92c-8fbbdad75482',
  'username': u'kernel',
  'version': [0, 14, 0, 'dev']},
 'metadata': {'dependencies_met': True,
  'engine': u'10aa4832-7ff8-4420-b4c7-8f33b0914a2b',
  'started': datetime.datetime(2012, 11, 25, 17, 13, 18, 1414),
  'status': u'ok'},
 'msg_id': '2963ebb0-cbed-48d7-b833-9b6a829b03bd',
 'msg_type': u'execute_reply',
 'parent_header': {u'msg_id': u'EAB8E58D574B406C9B8C4C7954A677E3',
  u'msg_type': u'execute_request',
  u'session': u'4C4B18395EF44483B912275A5F036290',
  u'username': u'username'},
 'tracker': }

注意上面的应答消息并非代码的执行结果,代码的输出在执行代码时已经经由sys.stdout->iopub_socket发送给Web服务器了。

小结

我们通过Python的各种标准库:gc, threading, traceback, inspect查看了Kernel是如何接收和发送消息,以及如何运行用户代码的。更详细的信息请读者参考IPython的开发文档

通过研究Kernel的工作原理和源代码,我们可以学习到如何使用ZeroMQ制作多进程通信的分布式应用程序。

上一篇:IPython Notebook简介1
下一篇:IPython架构之Display