这是我查看知乎私信时不小心瞅到的问题所触发的。由于 Go 在国内的兴起,我对这个问题也多有思考,就放在这里记录一下好了。知乎的链接我就不贴了,带 nofollow 的都懒得贴了。
首先,我们搞清楚问题是什么。或者说,我反对的究竟是什么?
静态链接,即早期唯一的一种链接出二进制可执行文件的方式,把所有程序需要用到的库全部打包到一个文件里边。后来,由于存储空间越来越不够用,所以发展出了动态共享库,也就是把库编译成 so、dll 或者 dylib 这种由动态库装载器在程序运行前或者运行时进行链接的方案。
静态链接的优点:
-
方便分发,不会因为库的升级而导致程序无法运行。这一点没有严格指定依赖版本的 Arch Linux 用户应该都有所体会。当你更新某个库(比如 boost 或者 icu 什么的)之后,动态链接到旧库的程序会出错。
-
效率稍高。这个反正人类是体会不到的。
基于第一点,用于急救的重要程序最好使用静态链接,特别是 busybox。以前我会安装 busybox 的,后来因为 Arch 改用动态链接 C 库了,对于我不再有意义,所以卸载了。以后 C 库如果出问题就直接重启进救援系统了。
另外我还有静态链接的 32 位 Vim,为的是在 Vim 依赖库更新而 Vim 没更新时依旧有个顺手的编辑器可用。我一直自行编译 Vim 因此这个曾经十分有用。不过由于现在对经常变动导致问题的解释器支持采用运行时动态链接,所以基本不受影响了。
静态链接的缺点:
这个可以列出长长的一串了。
-
占用磁盘空间。我就不怎么喜欢 Haskell 写的程序,太占硬盘了,一个程序就几十M。当然换新电脑之后目前硬盘空间有富余。但是它们还是会渐渐被我的各种源码和虚拟机什么的填满的。
-
占用内存空间。可执行文件在执行时是需要映射到内存中的。如果使用动态链接,那么因为是同一文件,所以在内存时只需要映射一份就可以了。而静态链接,不仅因为来源于不同的文件而需要加载、映射多次,而且因为来自于不同的构建等原因,逻辑上相同的代码往往并不会造成映射之后的内存页相同,使得内存去重机制(如 UKSM)失效。
别说内存是白菜价,除非你来给我手上的笔记本、VPS、服务器、路由器、单板机等都配置个几十G的内存,我付给你等质量的白菜。还记得比尔·盖茨说过的话吗——「640K足够了」。够了吗?
-
占用 I/O 带宽。可执行文件越大,在内存里没有缓存时需要从外存读取的数据也就越多,耗时也就越长。而因为文件体积增大,内存资源越发不够用,I/O 缓存越少,导致缓存命中更低。
-
占用网络带宽。你可执行文件是从网上下的吧?你在国内看个视频还挺流畅,但是到世界各地去下软件你试试看?
-
运行时链接。我写了一个程序,支持 MySQL、SQLite3、PostgreSQL、MongoDB、Oracle 等等等等数据库。但是你显然不会用到所有的数据库支持吧?那你为什么要所有这些数据库的连接库来用一个 SQLite3 数据库的功能呢?使用运行时链接(dlopen 那些函数),程序可以在运行的时候动态判断并加载它此次运行所需要的动态库。
-
升级。openssl 爆出了一个很严重的安全漏洞,已经被修复了,你怎么办?当然是升级呗。那你希望是更新一个几M的包然后重启服务器解决问题,还是下载好几百M的程序、更新每一个你所用到的使用了 openssl 的程序?更何况那些程序本身不一定都更新了,也许为了安全你得自行编译其中的很大一部分(你可以期望有一个安全团队在半夜爬起床去更新一个软件,但是你觉得上千项目的开发者都会这么做吗)。你也不一定能够找到所有静态链接了有漏洞的 openssl 版本的程序,万一漏掉一个,你整个服务器的安全性就没了(所以 openssl heartbleed 漏洞更新之后建议是重启系统而不是重启各服务)。
更别说更底层的库了,比如 C 库或者 C++ 库。不至于人家更新了一行代码,你就要重装整个系统吧?
静态链接有它自身的用处,但是它并不适合所有情况,甚至并不适合大多数情况。动态链接以其微小的运行效率损失为代价,为不论是最终用户还是开发者、打包者提供了更为优秀的库管理方案。之所以很多人看到静态链接相对于动态链接的优势,我认为还是因为他们没什么机会看到静态链接、尤其是大量静态链接会带来的问题。
你不需要把程序都静态链接。你需要的只不过是一个优秀的包管理器和维护团队而已。