当前位置: 首页 > 科技观察

在Python虚拟机里转悠,回来就被杀了!

时间:2023-03-23 11:06:35 科技观察

我出生在C盘的一个很深的目录里,不知道是谁把我放在这里的。闲来无事,整天睡觉,醒来就和邻居Account.class聊天,他曾经跑到内存里的一个Java虚拟机,不停的跟我重复他的JVM奇遇,什么奇葩警察,什么样的虚拟机搭建,什么样的cleaner,看得我心痒痒的,想来一场这样的冒险。他告诉我:冒险的开始是两个警察,你就等着他们来吧。1奇怪的警察那天我正在睡觉,突然有人敲我的门。我打开门,看到一高一矮两个陌生的警察!我的冒险即将开始。“你是ClassLoader吗?”记得Account.class告诉我会有一个叫ClassLoader的警察来加载。“什么ClassLoader?我们Python不玩Java!”恶狠狠的矮个警察递上工作证:“我是Python编译器,奉命查你住处,你是不是藏了什么pyc文件?”“pyc?什么pyc?”感觉剧情的发展明显和Account.class说的不一致。“别冒充你!”他环顾四周,过了一会儿,一个叫user.pyc的家伙从一个叫_pycache_的角落里拉了出来,“你敢说你没有私人文件吗?”我真的惊呆了,我做的是user.py,这个pyc什么时候藏在这里了。“让我查查查查,”Python编译器拿了放大镜开始查看pyc这家伙的二进制数据,“嗯,MagicNumber是3394,是我们Python3.7编译出来的,但是从修改时间戳,太旧了。”Python编译器刚说完,拔出手枪,砰的一声,该杀了这个pyc了,转头对我说:“现在,我给你重新编译。”可怜的pyc,来不及多说一句,消失在空气中。“一个叫order.py的文件导入了你,现在奉命带你去内存编译。”Python编译器冷冷的说道。我一惊:“我们的Python不是解释执行的吗?为什么还要编译呢?”“真是无知,我们的Python有一个执行字节码的虚拟机,先编译,再解释执行!去,去记忆,去编译。”两个警察不让我带东西,就把我推上了车,我们一起向回忆跑去。2、打听消息,感觉前途未卜。编译完你不会杀了我吧?不能坐以待毙,一定要多了解资料。“警察哥,你怎么找到我的?”我小心翼翼地问高个子警察。高个子警察还算友善,挥了挥手里的笔记本:“我是Python解释器,我们按照笔记本上记录的Python模块搜索规则进行搜索,你看,先从程序所在目录开始搜索运行,然后从中找到PYTHONPATH,然后进行python安装相关的默认路径设置。”“你看,”他指着笔记本说,“你在C:\users\andy\temp\python\目录下。”我认为这类似于Java的ClassPath。“原来如此,那你为什么要杀那个pyc?”我心里一紧张,下意识的看了一眼驱动中的Python编译器。》编译一次很费时间,所以我把字节码缓存在pyc文件里,如果你的源代码没有变化,下次就不用编译了,直接执行,不然pyc文件也没用。“我长长的松了一口气,看来我的源代码被改了!“为什么不用ClassLoader呢?听说Java就是这么干的。”有一个很高级的概念,代码可以从网上下载下来,在本地JVM中执行,但是怎么知道网上的代码是否有害呢?于是创建了沙盒机制,ClassLoader也分层,Java的核心类(如java.lang.String)只能由最顶层的ClassLoader来加载,防止别有用心的人写一个核心类重名造成损害。”我点点头:“哦,我们Python没有这样的要求,拿到源文件,编译解释执行后,就不需要复杂的ClassLoader了。”3编译和编译时说着,车子驶向了记忆。Python编译器下车把我的代码都搬进了内存,接着就是一系列眼花缭乱的词法分析,语言分析,形成抽象语法树,再由抽象语法树形成字节码,这里省略了3000字,没有表格.最后,他把我变成了内存中的二进制字节码。“这到底是什么?”Python编译器说:“这是pyc,即PyCodeObject,编译一次很累,我把这个PyCodeObject对象保存在pyc文件中,下次不用再编译了。”比如,”高大的Python解释器接口说道,“在你的user.py中,有这样一段代码defadd(a,b):c=a+bprint(c)编译成PyCodeObject后会是这个样子:(注:这里展示的只是一个片段,实际的PyCodeObject往往是一个复杂的嵌套连接结构)局部常量表记录了局部变量a,b,c。符号表记录了程序引用的符号,比如print等。字节码才是真正的指令,这些指令会引用常量表和符号表。”仅仅展示一个片段就这么复杂,我太懒得看这么多细节,心里想着跟着Account.class的脚本走,下一步就是去方法区了。可是高大的Python解释器说:“我们这里没有方法区,Python对象和数据结构存放在一个Heap中,user.py,这是你的地址,你可以带着PyCodeObject去那里,一会儿会有线程联系你。”4Execution在去Heap区的路上,只见一群全副武装的士兵马不停蹄地巡逻,时不时拉出一些东西塞进车里,不用说,这些都是Terriblecleaner。我仔细观察了一下,上面有引用计数每个对象的头部。如果使用,计数会增加,如果不使用使用,它会减少。如果它变成零,对不起,这很危险。我们根据地址找到了隔间。我们两个人刚坐下,桌上的可视电话就响了。在图片中,我看到一个编号为0x7954的线程坐在一个明亮的CPU车间里。他面前是一个工作台,上面有一个很深的水桶(后来才知道这叫栈)和一排小格子。有一把醒目的大锁,上面写着“GIL”。这个线程对我说:“我是线程0x7954,我们的老板Python解释器让我调用你的add函数,请告诉我第一条指令。”我说:“c=a+b”“听着我不明白,你必须告诉我字节码。”恍然大悟,赶紧从PyCodeObject中的字节码区查找:“LOAD_FAST0(a)”0x7594从0号格子里找到了数字10,也就是ad??d函数的参数a的值入栈,然后0x7594说:“下一条指令。”“LOAD_FAST1(b)”和数字20入栈:然后:BINARY_ADD,应该是加法运算。0x7954迅速取出10和20,相加,放入result30onthestack.Finally:STORE_FAST2(c)所以0x7954取出30放到编号为2的格子里,看到这里明白了Account.class曾经说过JVM是基于栈的虚拟机,看来那个PythonVM也是一样。。不过既然都是虚拟机,为什么两个整数相加(BINARY_ADD)在这里执行的这么慢?电话那头的0x7954好像看穿了我的心思:“我最烦这个BINARY_ADD指令,Python是动态类型语言,具体类型只知道n在运行时,比如编译完这段代码s1="hello"s2="world"s=s1+s2,底层指令也是BINARY_ADD,所以在执行这条指令的时候,需要进行类型判断。如果操作数是整数,就把它们相加;如果操作数是字符串,则进行连接;一个是整型,一个是字符串,你要转换,我容易吗!”看来静态类型也不错,可以直接编译成对应的字节码,整数的加法是iadd,和strings的连接都是其他字节码,所以运行时不需要判断参数类型。5GIL执行了好久了,这些字节码我都背得这么熟,这里真无聊。0x7954执行完一个STORE_FASTinstruction,居然停了,我喜出望外,account.class告诉我,一旦停了,就意味着程序员要debug了,他们的秒比我们的多十天,还有一个长假。但是没有任何调试,0x7954从工作台上拿起了GIL的大锁,离开了CPU车间。他对我说:“对不起,刚才Python解释器说我已经运行了100个ticks,我必须放弃这个GIL锁,让其他线程使用CPU车间。”我说:“不,你这里有4个刻度。”一个CPU车间(CPU核),怎么不去别的车间执行呢?”“不行,这是老大的规矩,不管有多少个CPU车间,只有抢到GIL锁的线程可以运行。”“那么多Threads在等GIL,那么多CPU车间是空的,一个核难,多核围观,浪费,浪费!”不禁心酸,不知如何等了很久,但是0x7954又拿到了GIL锁,进入了CPU车间执行,我注意到一个特点,就是字节码里面有很多调用print函数的地方,程序员为什么不debug,为什么不开心holidayscome?0x7954说:“码农分两种:1.调试者,出错了喜欢调试;输出信息3.思维派,有问题先在脑子里分析定位,和然后debug,我觉得我们的Python程序员属于第二种。这位程序员“去年”也调试过Java,他是怎么跑到Python的?变成出口大饼了?我很不解。6Epilogue代码终于执行了,整个世界都消失了,我回到了硬盘,如Account.class所说,如梦似幻。user.pyc热情的跟我打招呼:“哥回来了,你别再改了,你一改我就完了。”我说,“我不想改,改了我就活不下去了,但我也管不了程序员……”话还没说完,我就觉得有种挑剔的感觉吹在我的头上。我知道程序员动了我的源码,说不定是个bug修改。我知道我必须被新版本覆盖。user.pyc喃喃自语:“完了,这么快就变了……”这时,又是敲门声……【本文为专栏作家“刘鑫”原创稿件,转载,请通过作者微信获取授权公众号coderising】点此查看作者更多好文