您的位置:首页 > 编程语言 > Python开发

我到Python虚拟机里逛了一圈,回来就被干掉了!

2019-12-02 08:50 866 查看

我出生在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喃喃自语:“完了,这么快就改了.....”  


这时候门外又响起了敲门声......


往期精彩回顾
我是一个线程

我是一个Java Class

面向对象圣经

函数式编程圣经

TCP/IP之大明邮差

CPU阿甘

我是一个网卡

我是一个路由器

一个故事讲完HTTPs

编程语言的巅峰

Java:一个帝国的诞生

JavaScript:一个屌丝的逆袭

负载均衡的原理

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: