我到Python虚拟机里逛了一圈,回来就被干掉了!
我出生在C盘一个很深的目录下,也不知道是谁把我放到这里的。我无事可干,整天就是睡觉,睡醒了就和我的邻居Account.class聊天,他曾经去过一次内存的Java虚拟机,不停地给我重复...
我出生在C盘一个很深的目录下,也不知道是谁把我放到这里的。
我无事可干,整天就是睡觉,睡醒了就和我的邻居Account.class聊天,他曾经去过一次内存的Java虚拟机,不停地给我重复他的JVM奇遇记,什么陌生警察,什么虚拟机大楼,什么清理者,让我听得心痒痒的,也想来一次这样的冒险。
(码农翻身注:详情请移步《我是一个Java class》)
他告诉我:冒险经历的开端是两个警察,你就等着他们来吧。
1
陌生警察
这一天我正在睡觉,突然咣咣有人砸我房门。
我打开门一看,一高一矮两个陌生警察!我的冒险之旅要开场了。
“你们是ClassLoader吧?” 我想起了Account.class告诉我,会有个叫ClassLoader的警察来装载。
“什么ClassLoader? 我们Python不玩Java那一套!” 凶神恶煞的矮个子警察递上了工作证:“我是Python编译器,现在奉命对你的住处进行检查,有没有私藏pyc文件?”
“pyc? 什么pyc?” 我感觉情节发展和Account.class说得明显不符。
“别装了你!” 他四处查看,没一会儿,在一个叫做_pycache_的角落里拉出来一个叫做user.pyc的家伙,“敢说你没有私藏文件?”
我真是惊呆了,我确实是user.py,这个pyc是什么时候藏在这里的。
“让我检查检查,” Python编译器拿着放大镜开始查看pyc这个家伙的二进制数据,“嗯,Magic Number是3394,是我们Python3.7编译出来的,不过从修改时间戳看,实在是太老了。”
Python编译器刚说完,抽出手枪,砰的一声,就把这个pyc该干掉了, 他把头转向我:“现在,我对你重新编译。”
可怜的pyc,连个台词都来不及说,就消失在空气中了。
“有个叫order.py 的文件 import了你,现在我们奉命带你去内存编译。” Python编译器冷冰冰地说到。
我很惊奇:“我们Python不是解释执行吗,怎么还要编译?”
“真是无知,我们Python有虚拟机,执行的是字节码,是先编译,再解释执行!走,去内存编译。”
两个警察不允许我带任何东西,便把我推上车,我们一起奔向内存。
2
打探消息
我觉得前途未卜,不会编译完以后把我也干掉吧?不能坐以待毙,一定得多了解信息。
“警察大哥,你们是怎么找到我的?” 我小心地问那个高个警察。
高个儿警察还算和蔼,挥了挥手中的一个本子:“我是Python解释器,我们会根据本子上记录的Python模块搜索规则来查找,你看,先从程序运行的当前目录找,然后从PYTHONPATH找,然后是python的安装设置相关的默认路径。”
“瞧瞧,” 他指着本子说,“你就在C:\users\andy\temp\python\这个目录下。”
我心说这和Java的ClassPath差不多。
“原来如此,那为什么把那个pyc给枪毙了?” 我心里紧张,下意识地看了一眼开车的Python编译器。
“编译一次挺花费时间的,所以就把字节码缓存到了pyc文件中,如果你的源码没有变化,下次就不用编译,直接执行了。否则,那个pyc文件就没用了。”
我长出一口气,看来我的源码有改动!
“咱们怎么不用ClassLoader呢,我听说Java都是这么干的。”
“说来话长,” 高个儿警察很有耐心,“他们Java最早的时候有个非常先进的理念,代码可以从网络下载,在本地的JVM的执行, 但是你怎么知道网上的那些代码有没有危害?所以就搞了一个沙箱机制,ClassLoader也分了层,Java的核心类(如java.lang.String)只能由最上层的ClassLoader来装载,防止别有用心的人写个同名的核心类搞破坏。”
我点头:“奥,我们Python没有这样的需求,拿到源文件,编译后解释执行,也就不需要复杂的Class Loader了。”
3
编译
说话间,车子就开到了内存。
Python编译器下车,把我的代码通通搬到内存,然后是一系列让人眼花缭乱的词法分析,语言分析, 形成抽象语法树,从抽象语法树中形成字节码,此处略去3000字不表。
终于,他在内存中把我变成了二进制的字节码。
“这是什么鬼? ”
Python编译器说:“这就是pyc啊,就是PyCodeObject,编译一次累死人,我把这个PyCodeObject的对象保存到pyc文件中,下一次就不用编译了。”
“我给你举个例子,”高个的Python解释器接口道,“在你的user.py中有这么一段代码
def add(a,b):
c = a + b
print(c)
编译成PyCodeObject以后大概是这个样子:
(注:这里展示的只是一个片段,实际的PyCodeObject经常是一个复杂的嵌套接结构)
局部常量表中记录的是局部变量a,b,c 。
符号表中记录了程序引用的符号,如print等。
字节码就是真正的指令了,这些指令会引用常量表和符号表。”
只是展示一个片段就这么复杂了,我懒得去看这么多的细节,心里想着按照Account.class的剧本,接下来就要去方法区了。
可是高个子的Python解释器说:“我们这儿没有方法区,Python的对象和数据结构都是保存在一个Heap中的,user.py,这是你的地址,你带着PyCodeObject到那里去吧,一会儿就有线程联系你了。”
4
执行
去Heap区的路上,我看到一队全副武装的士兵不停地在巡逻,时不时把一些对象拉出来,塞到车里,不用说,这些都是可怕的清理者。
我仔细观察了一下,每个对象的头上都有一个引用计数,如果被使用,计数就会增加,不用就会减少,如果变成零,对不起,那就危险了。
按照地址找到了格子间,我俩刚坐下来,桌子上的视频电话就响了。
画面中,我看到一个编号为0x7954的线程坐在一个明亮的CPU车间里,他的面前是一个工作台,工作台上有一个深桶(后来知道这叫做栈)和一排小格子,还有一个引人注目的大锁,上面写着“GIL”。
这个线程对我说:“我是线程0x7954,我们的老板Python解释器让我调用你的add函数,请把第一条指令给我说一下。”
我说:“c = a +b ”
“听不懂,你得给我说字节码。”
我恍然大悟,赶紧从PyCodeObject中的字节码区域寻找:“LOAD_FAST 0 (a)”
0x7594从编号为0的格子中找到了数字10, 也就是add函数的参数a 的值,放入栈中
然后0x7594说:“下一条指令。”
“LOAD_FAST 1 (b)”
于是数字20被放入了栈中:
然后是:BINARY_ADD, 这应该是个加法操作。
0x7954迅速地把10,20都取出来,做了加法,把结果30放入栈中。
最后是 :STORE_FAST 2 (c)
于是0x7954取出30,放到了编号为2的格子中
看到这里, 我就明白了Account.class曾经说过JVM是个基于栈的虚拟机, 看来Python VM也是如此啊。
不过既然都是虚拟机,为什么这里执行两个整数的加法操作(BINARY_ADD)会这么慢呢?
电话那头的0x7954似乎看透了我的心思:“我最烦这个BINARY_ADD指令了,Python是动态类型语言,运行期才知道具体类型,比如这段代码
s1 = "hello"
s2 = "world"
s = s1 + s2
编译后,底层的指令也是BINARY_ADD, 所以在执行这个指令的时候,还需要做类型判断,如果操作数是整数,就相加;如果操作数是字符串,就做连接;如果一个是整数,一个是字符串,还得做转型,我容易吗我!”
看来静态类型也有好处,可以直接编译成对应的字节码,整数相加就是iadd,字符串连接是其他字节码,在运行时就不用判断参数类型了。
5
GIL
执行的时间长了,我对这些字节码熟得都能背下来了,这里实在是无聊。
0x7954执行完一条STORE_FAST指令以后,居然停了下来,我心中大喜,Account.class告诉过我,一旦停下来,那就是程序员要调试了,他们的一秒是我们的十多天,将会有个漫长的假期。
但是没有什么调试, 0x7954从工作台上抱起GIL这个大锁离开了CPU车间。
他对我说:“对不起,刚才Python解释器说我已经运行了100个ticks,必须得放弃这个GIL的锁,让别的线程使用CPU车间了。”
我说:“不对啊,你这里有4个CPU车间(CPU core),你为什么不去别的车间执行?”
“没办法,这是老大规定的,不管有多少个CPU车间,只有抢到GIL锁的哪个线程才能运行。”
“这么多线程在等待GIL,这么多CPU车间空着,一核有难,多核围观,浪费啊,浪费!” 我不由得痛心疾首。
不知道等了多久,0x7954又获得了GIL锁,进入CPU车间执行。
我注意到一个特点,字节码中对print函数的调用特别特别多。
程序员们怎么不调试呢?快乐假期怎么还不来呢?
0x7954说:“码农有两类
1. 调试派,出了问题喜欢调试
2. 输出派,不喜欢单步调试,喜欢通过print来输出信息
3. 思考派,出了问题先在脑子中分析定位,然后再调试。
我看咱们这位Python程序员属于第二种。”
这个程序员“去年”还调试Java呢,怎么到了Python这里就变成输出派了?我很疑惑。
6
尾声
代码终于执行完了,整个世界都消失了,我又回到了硬盘,正如Account.class所说,像做了一场梦一样。
user.pyc热情地给我打招呼:“大哥回来了,你可千万别再改动了,你一改动我就完蛋。”
我说:“我也不想改,一改我也活不成, 但是我也控制不了程序员啊......”
话还没说完,就感觉头上遭遇了一记暴击,我知道程序员动了我的源码,也许是修改了一个Bug,我知道自己要被新版本覆盖了。
user.pyc喃喃自语:“完了,这么快就改了.....”
这时候门外又响起了敲门声......
往期精彩回顾
更多推荐
所有评论(0)