The Kollected Kode Vicious

Kode Vicious - @kode_vicious

  下载本文的 PDF 版本 PDF

代码越多,Bug 越少?

今天节省的字节可能会在明天反噬你。


George V. Neville-Neil


亲爱的 KV,

我的一位同事一直删除我在代码中对system()的调用,坚持认为最好编写代码来完成我通过 shell 完成的工作。他一直说,使用我们正在使用的语言进行编码比调用 shell 来完成这项工作要安全得多。如果他没有添加 10 到 20 行代码,仅仅为了完成我用一行代码就能完成的工作,我可能会相信他,这行代码是system()。 增加代码行数怎么能减少 Bug 的数量呢?

对一行代码感到满意的人

亲爱的一行代码,

你差点用你对简洁的呼吁说服了我,即使用单行代码system()可以减少 Bug 的可能性。 差点,但还不够。

当你从任何语言调用 shell 时,你使用的不是单行代码,而是数千行。 在此时调用 shell 就像用核武器杀死跳蚤一样。 当你完成时,跳蚤会非常死,但你也浪费了很多能量来杀死它,并且可能会导致附带损害。 每次调用system()都是信任所有底层代码,问题不仅在于底层有很多行代码,还在于 shell 可以做的事情非常强大——它可能是任何系统上最强大的程序。

任何系统(Unix、Windows 或其他)上的命令 shell 的作用都是命令系统,并且随着时间的推移,它已经积累了单行对系统执行操作的能力,而这些操作实际上应该需要更多思考。 最明显的例子是移动和删除文件。 我实际上想认为大多数程序员都知道不要像调用system()并带有rm命令那样做,特别是当他们向system()调用提供未经检查的用户输入时。 虽然我说“我这样认为”,但我脑海深处有一个声音——一个非常响亮的声音——在尖叫,“不是这样的! 不是这样的! 他们会做的! 现在阻止他们!” 我讨厌那个声音,但无论我如何努力淹没它——而且我尝试过——它仍然存在。

比在系统命令中调用rm命令更糟糕的冒犯是通过system()调用 shell 脚本。 为什么? 因为以后阅读你的代码的可怜虫可能不知道脚本是做什么的。 我确信它会有一个描述性的名称,例如 update_sales_2.sh,并且它将被签入到源代码存储库中,而不是存储在 /home/bob/update_test2/ 目录中,但也许不会,而且我在这里描述的脆弱设置将成真:当 Alice 来阅读代码时,她将不得不去阅读 update_sales_2.sh,只要她具有对 Bob 的主目录的读取权限。 她将在凌晨 3 点阅读它,因为某些东西已经坏了,并且这一切都会顺利进行,每个人都会幸福地生活在一起。

也许我最喜欢的滥用代码中的system()例程是在它被用来构建复杂的命令管道时。 在程序中调用system()中使用管道是在你的系统中启动 fork 炸弹的好方法,特别是如果管道是动态构建的。

手动从 shell 运行复杂的管道命令,对机器造成冲击的风险较小,因为人类很慢——并且据推测他们正在注意他们正在做什么。 一旦你将一组管道放入system()中,然后让你的代码无人值守地运行,如果你的管道中存在 Bug,你就会面临反复 fork 子进程并压垮你正在运行的任何机器的风险。

为了 shell 的辩护,它处理管道的能力比大多数编程语言要好,任何尝试在 C 中使用管道和信号的人都会很容易承认这一点,但是自动 fork 进程必须非常小心地完成。 我已经见过太多次程序员在他们的终端窗口中逐步开发出一个漂亮的管道,然后剪切并粘贴到程序中,该程序将不是手动执行,而是由另一个程序执行。 当系统循环运行他们的管道时破坏了他们的系统时,他们脸上的表情有点滑稽,但这几乎无法弥补他们或他们的整个工作组即将因需要系统重置而丢失工作的事实。

最后,这可能是反对使用system()的最重要和最微妙的论点,这样做需要阅读代码的程序员在一种语言和另一种语言之间进行心理上下文切换。 除非你在 shell 脚本中调用system(),否则当程序员在到达system()调用时正在阅读的语言非常不像 shell; 它是 C、C++、Python、Perl、Ruby 或其他语言。 这意味着你在处理代码时建立的所有心理上下文都将丢失,因为你引入了 shell 脚本上下文,或者你只是会忽略它并犯错误,因为当你在进行system().

调用时你没有在 shell 中思考。 这并非不可能做到,但它肯定会增加读者的认知负荷,因此你最好有一个非常好的理由切换到 shell——比不想弄清楚 unlink 系统调用是如何工作的要好。

KV

亲爱的 KV,

为什么一些现代网络协议没有序列号? 我认为到现在为止,所有协议设计者都应该意识到,在每个数据包中加入一个简单的序列号有助于人们调试他们的网络设置。

乱序

亲爱的 OoS,

你还不如问问为什么人们在安全带技术已被证明可以挽救生命这么多年后,仍然坚持不系安全带。

似乎人们会坚持乐观地认为,只要他们足够小心,一切都会好起来的。 他们认为坏事只会发生在别人的协议或数据包上,而不会发生在他们自己的协议或数据包上。 希望永存,并在寒冷的经验寒冬中死去。

我想就你关于网络协议设计中理智的呼吁提出两点。 首先,重要的不仅仅是拥有序列号,序列号的使用方式也很重要。 考虑 TCP 中的序列号,它计算两个端点之间已通信的字节数。 在 TCP 设计时,最常用的最快网络是 10-Mbps 以太网 LAN。 请注意,那是 M,而不是 G——10 位/秒。 在 10 Mbps 的速度下,传输 2^32 字节的数据大约需要 3400 秒,或略少于一个小时,这对计算机来说是永恒的。 在当今可用的商品 10-Gbps 硬件上,传输相同的数据需要 3.4 秒,这意味着序列空间大约每四秒翻转一次。 如果数据包丢失超过四秒钟,则连接上的数据很有可能被篡改。 随着很快可用的硬件,序列空间翻转的时间将降至 0.3 秒。

所有这些并不是说 TCP 设计得很差(至少它有序列号),但对于现代协议的设计者来说,重要的是要了解在选择序列号时,未来的验证与空间之间的权衡。 如果在某个时候 TCP 得到扩展,那么序列号可以增加到 64 位,即使在 100 Gbps 的速度下,也需要 46 年才能翻转该数字。 任何在网络中丢失这么长时间的数据包都将完全丢失。 当你选择序列号时,请考虑你要保护什么。 对于 TCP,它保护所有传输的字节,以便在交付时不会丢失或重新排序。 对于其他协议,可能只需要计算整个消息,以便接收者可以说数据包 A 在数据包 B 之前到达,而不是担心消息中的每个字节。

我想提出的第二点是,时间戳不是好的序列号。 虽然通常认为时间总是向前移动,但这在计算中通常并非如此。 许多 Bug 出现在计算机上处理时间时,其中最不重要的原因是不同计算机上的不同时钟通常以不同的速度前进。 这就是为什么我们有诸如 NTP(网络时间协议)和 PTP(精确时间协议)之类的协议来规范我们的计算机时钟。 可惜的是,计算机不喜欢被规范,即使在运行时间协议时,两台计算机上的时钟始终会彼此偏移一些,因此运行时间协议并不能解决此问题。 撇开计算机计时的令人费解的相对论问题不谈——并相信我,你真的想把这些问题放在一边——事实仍然是,使用计算机上的时间作为数据包序列号是有问题的。 递增计数器比确保你收到的时间戳单调递增更容易、更快且不易出错。 对于数据包排序的情况,越简单越好——而简单就是计数器。

对于那些设计或希望设计网络协议的人,请,我恳求你们,不要吝啬序列号。 今天节省的字节将在明天咬你的屁股。

KV

KODE VICIOUS,凡人称之为 George V. Neville-Neil,为了乐趣和利润而从事网络和操作系统代码工作。 他还教授各种与编程相关的课程。 他的兴趣领域是代码探险、操作系统和重写你的糟糕代码(好吧,也许不是最后一个)。 他在马萨诸塞州波士顿的东北大学获得了计算机科学学士学位,并且是 、Usenix 协会和 IEEE 的成员。 他是一位狂热的自行车爱好者和旅行家,目前居住在纽约市。

© 2012 1542-7730/11/0800 $10.00

acmqueue

最初发表于 Queue vol. 10, no. 8
数字图书馆 中评论本文








© 保留所有权利。

© . All rights reserved.