程序员和工程师的民间传说(第 1 部分)

程序员和工程师的民间传说(第 1 部分)

这是来自互联网的精选故事,讲述了错误有时如何具有完全令人难以置信的表现。 也许你也有话要说。

汽车对香草冰淇淋过敏

这是一个工程师的故事,他们明白显而易见的并不总是答案,无论事实看起来多么牵强,它们仍然是事实。 通用汽车公司庞蒂亚克分部收到投诉:

这是我第二次给你写信,我不怪你没有回信,因为这听起来很疯狂。 我们家有每天晚上晚饭后吃冰淇淋的传统。 每次冰淇淋的种类都会改变,吃完晚饭,全家人都会选择要买哪种冰淇淋,然后我就去商店。 我最近买了一辆新的庞蒂亚克,从那以后我去买冰淇淋就成了一个问题。 你看,每次我买香草冰淇淋从商店回来,车子就发动不起来。 如果我带任何其他冰淇淋,汽车启动就没有任何问题。 我想问一个严肃的问题,不管听起来多么愚蠢:“庞蒂亚克是什么原因让它在我带香草冰淇淋时它不启动,但在我带另一种口味的冰淇淋时却很容易启动?”

正如你可以想象的那样,部门总裁对这封信持怀疑态度。 不过,为了以防万一,我还是派了工程师去检查。 他很惊讶自己遇到了一位住在美丽地区、富有、受过良好教育的男人。 他们约好晚饭后立即见面,这样两人就可以去商店买冰淇淋。 那天晚上是香草味的,当他们回到车上时,车就发动不起来了。

工程师又来了三个晚上。 第一次吃巧克力冰淇淋。 车子启动了。 第二次有草莓冰淇淋。 车子启动了。 第三天晚上,他要求吃香草。 汽车没有启动。

理性推理,工程师不肯相信这辆车对香草冰淇淋过敏。 因此,我同意车主的意见,让他继续拜访,直到找到问题的解决办法。 一路上,他开始做笔记:他记下了所有信息,一天中的时间,汽油类型,到达商店和从商店返回的时间等等。

工程师很快意识到车主花在购买香草冰淇淋上的时间更少了。 原因就在于店内商品的布局。 香草冰淇淋是最受欢迎的,并且被保存在商店前面的单独冰柜中,以便更容易找到。 所有其他品种都在商店后面,找到合适的品种并付款需要花费更多时间。

现在的问题是工程师:如果从发动机关闭那一刻起经过的时间较短,为什么汽车还没有启动? 由于问题是时间而不是香草冰淇淋,工程师很快找到了答案:这是一个气锁。 这种情况每天晚上都会发生,但当车主花更多时间寻找冰淇淋时,发动机设法冷却到足够的温度并轻松启动。 当该男子购买香草冰淇淋时,发动机仍然太热,气锁没有时间溶解。

寓意:即使是完全疯狂的问题有时也是真实存在的。

古惑狼

经历这些是很痛苦的。 作为一名程序员,你习惯于首先、第二次、第三次责怪你的代码……而在万分之一的地方你会责怪编译器。 再往下看,你已经把责任归咎于设备了。

这是我关于硬件错误的故事。

对于游戏古惑狼,我编写了加载并保存到存储卡的代码。 对于这样一个自鸣得意的游戏开发者来说,这就像在公园散步一样:我以为这项工作需要几天的时间。 然而,我最终调试了六个星期的代码。 一路上,我解决了其他问题,但每隔几天我就会回到这段代码几个小时。 这是痛苦的。

症状如下:当您保存游戏的当前游戏并访问存储卡时,一切几乎总是正常......但有时读取或写入操作会无明显原因超时。 短时间的录音通常会损坏存储卡。 当玩家尝试保存时,他不仅保存失败,还破坏了地图。 废话。

过了一段时间,我们索尼的制片人康妮·巴斯开始恐慌。 我们无法发布带有此错误的游戏,六周后我不明白是什么导致了这个问题。 通过Connie,我们联系了其他PS1开发者:有人遇到过类似的事情吗? 不。 没有人遇到存储卡问题。

当你没有调试的想法时,剩下的唯一方法就是“分而治之”:从有问题的程序中删除越来越多的代码,直到剩下一个相对较小的片段仍然会导致问题。 也就是说,你把程序一块一块地砍掉,直到剩下包含bug的部分。

但问题是,从视频游戏中删减片段非常困难。 如果删除了模拟重力的代码,如何运行它? 还是画人物?

因此,我们必须用存根替换整个模块,这些存根假装做一些有用的事情,但实际上做一些非常简单且不能包含错误的事情。 我们必须编写这样的拐杖才能使游戏至少能够运行。 这是一个缓慢而痛苦的过程。

简而言之,我做到了。 我删除了越来越多的代码,直到只剩下配置系统运行游戏、初始化渲染硬件等的初始代码。 当然,在这个阶段我无法创建保存和加载菜单,因为我必须为所有图形代码创建一个存根。 但我可以假装是使用(不可见的)保存和加载屏幕的用户,并要求保存然后写入存储卡。

这给我留下了一小段代码,仍然存在上述问题 - 但它仍然是随机发生的! 大多数情况下一切都工作正常,但偶尔也会出现故障。 我删除了几乎所有的游戏代码,但错误仍然存​​在。 这令人费解:剩下的代码实际上没有做任何事情。

在某个时刻,大概是凌晨三点左右,我突然想到了一个想法。 读取和写入(输入/输出)操作涉及精确的执行时间。 当您使用硬盘驱动器、存储卡或蓝牙模块时,负责读写的低级代码根据时钟脉冲进行读写。

在时钟的帮助下,未直接连接到处理器的设备与处理器上执行的代码同步。 时钟决定波特率——数据传输的速度。 如果时序混乱,那么硬件或软件或两者也会混乱。 这是非常糟糕的,因为数据可能会被损坏。

如果我们的代码中的某些内容混淆了时间怎么办? 我检查了测试程序代码中与此相关的所有内容,并注意到我们将 PS1 中的可编程定时器设置为 1 kHz(每秒 1000 个滴答声)。 这是相当多的;默认情况下,当控制台启动时,它以 100 Hz 运行。 大多数游戏都使用这个频率。

游戏开发者 Andy 将计时器设置为 1 kHz,以便更准确地计算动作。 安迪倾向于做得太过分,如果我们模拟重力,我们会尽可能准确地做到这一点!

但是,如果加速计时器以某种方式影响了程序的整体计时,从而影响了调节存储卡波特率的时钟,该怎么办?

我注释掉了定时器代码。 错误没有再次发生。 但这并不意味着我们修复了它,因为故障是随机发生的。 如果我只是幸运的话怎么办?

几天后我再次尝试了测试程序。 该错误没有再次出现。 我返回到完整的游戏代码库并修改了保存和加载代码,以便可编程计时器在访问存储卡之前重置为其原始值(100Hz),然后重置回1kHz。 没有再发生车祸。

但是为什么会这样呢?

我再次回到测试程序。 我试图找到 1 kHz 定时器发生错误的一些模式。 最终我注意到当有人使用 PS1 控制器玩游戏时会出现错误。 由于我自己很少这样做 - 为什么在测试保存和加载代码时需要控制器? - 我什至没有注意到这种依赖性。 但有一天,我们的一位艺术家正在等我完成测试——当时我可能正在咒骂——并紧张地转动着手中的控制器。 发生了错误。 “等等,什么?!” 好吧,再做一次!”

当我意识到这两个事件是相互关联的时,我能够轻松地重现该错误:我开始记录到存储卡,移动控制器,并损坏了存储卡。 对我来说,这看起来像是一个硬件错误。

我找到康妮,告诉她我的发现。 她将这一信息转达给了设计 PS1 的一位工程师。 “不可能,”他回答道,“这不可能是硬件问题。” 我请康妮为我们安排一次谈话。

工程师打电话给我,我们用他蹩脚的英语和我(极其)蹩脚的日语争论。 最后我说:“让我发送我的 30 行测试程序,其中移动控制器会导致错误。” 他同意。 说这是浪费时间,而且他正忙于一个新项目,但会屈服,因为我们是索尼非常重要的开发商。 我清理了我的测试程序并将其发送给他。

第二天晚上(我们在洛杉矶,他在东京)他打电话给我,不好意思地道歉。 这是一个硬件问题。

我不知道这个错误到底是什么,但从我在索尼总部听到的情况来看,如果你将定时器设置得足够高,它就会干扰主板上定时器晶体附近的组件。 其中之一是存储卡的波特率控制器,它还设置控制器的波特率。 我不是工程师,所以我可能搞砸了一些事情。

但最重要的是主板上的组件之间存在干扰。 当定时器以 1 kHz 运行时,通过控制器端口和存储卡端口同时传输数据时,会出现位丢失、数据丢失和卡损坏的情况。

坏牛

在 1980 世纪 1800 年代,我的导师 Sergei 为 SM-11(PDP-XNUMX 的苏联克隆版)编写了软件。 这台微型计算机刚刚安装在苏联重要交通枢纽斯维尔德洛夫斯克附近的一个火车站。 新系统旨在为货车和货运提供路线。 但它包含一个恼人的错误,导致随机崩溃和崩溃。 跌倒总是发生在有人晚上回家时。 但尽管第二天进行了彻底的调查,计算机在所有手动和自动测试中都工作正常。 这通常表示在某些条件下发生竞争条件或其他一些竞争性错误。 谢尔盖厌倦了深夜的电话,决定彻查到底,首先了解编组站的哪些情况导致了计算机故障。

首先,他收集了所有不明原因跌倒的统计数据,并按日期和时间创建了图表。 模式很明显。 又观察了几天,谢尔盖意识到他可以轻松预测未来系统故障的时间。

他很快了解到,只有当车站对从乌克兰北部和俄罗斯西部运往附近屠宰场的一列牛进行分类时,才会出现干扰。 这本身就很奇怪,因为屠宰场是由距离更近的哈萨克斯坦农场供应的。

1986 年,切尔诺贝利核电站发生爆炸,放射性沉降物使周边地区无法居住。 乌克兰北部、白俄罗斯和俄罗斯西部的大片地区受到污染。 谢尔盖怀疑抵达的车厢辐射水平很高,因此开发了一种方法来测试这一理论。 人们被禁止拥有剂量计,因此谢尔盖在火车站向几名军人登记。 喝了几杯伏特加后,他设法说服一名士兵测量其中一辆可疑车厢的辐射水平。 事实证明,该水平比正常值高出数倍。

牛不仅发出大量辐射,而且辐射水平太高,导致位于空间站旁边建筑物内的 SM-1800 内存中的位随机丢失。

苏联出现粮食短缺,当局决定将切尔诺贝利肉类与该国其他地区的肉类混合。 这使得在不损失宝贵资源的情况下降低总体放射性水平成为可能。 得知此事后,谢尔盖立即填写了移民文件。 当辐射水平随着时间的推移而降低时,计算机崩溃会自行停止。

通过管道

曾几何时,Movietech Solutions 为电影院创建了软件,专为会计、票务销售和综合管理而设计。 该旗舰应用的DOS版本在北美中小型连锁电影院中颇受欢迎。 因此,当集成了最新触摸屏和自助服务亭并配备各种报告工具的Windows 95版本发布后,它也迅速流行起来,这并不奇怪。 大多数情况下更新都没有问题。 当地IT人员安装了新设备,迁移了数据,业务继续进行。 除非它没有持续下去。 当这种情况发生时,公司就会派出绰号“清洁工”的詹姆斯。

虽然这个昵称暗示着邪恶的类型,但清洁工只是指导员、安装工和多才多艺的组合。 詹姆斯会花几天时间在客户现场将所有组件组装在一起,然后再花几天时间教员工如何使用新系统,解决出现的任何硬件问题,并从根本上帮助软件度过其起步阶段。

因此,在这段忙碌的时间里,詹姆斯早上到达办公室,还没走到办公桌前,就受到了经理的迎接,经理的咖啡因超出了平时的水平,这也就不足为奇了。

“恐怕你需要尽快去新斯科舍省的安纳波利斯。” 他们的整个系统瘫痪了,经过一整夜的工程师工作,我们无法弄清楚发生了什么。 服务器上的网络似乎出现故障。 但只有在系统运行了几分钟之后。

——他们没有回到旧系统? ——詹姆斯十分认真地回答,不过心里却惊讶地睁大了眼睛。

— 确切地说:他们的 IT 专家“改变了优先级”并决定保留旧服务器。 James,他们在六个站点安装了该系统,并且只支付了高级支持费用,他们的业务现在就像 1950 世纪 XNUMX 年代一样运行。

詹姆斯微微直起身子。

- 那是另一回事了好的,让我们开始吧。

当他到达安纳波利斯后,他做的第一件事就是找到客户第一家出现问题的影院。 在机场拍摄的地图上,一切看起来都不错,但所需地址周围的区域看起来很可疑。 不是贫民窟,而是让人想起黑色电影。 当詹姆斯把车停在市中心的路边时,一名妓女走近他。 考虑到安纳波利斯的规模,它很可能是整个城市中唯一的一个。 她的出现立刻让人想起大银幕上那个以性换钱的著名角色。 不,不是关于朱莉娅·罗伯茨,而是关于强·沃特 [暗示电影“午夜牛仔” - 大约。 车道].

送走妓女后,詹姆斯去了电影院。 周围环境已经好了很多,但还是给人一种破败的感觉。 并不是说詹姆斯太担心了。 他以前也去过一些肮脏的地方。 这里是加拿大,即使是抢劫犯在拿走你的钱包后也会有礼貌地说“谢谢”。

电影院的侧门在一条潮湿的小巷里。 詹姆斯走到门口敲了敲门。 很快,它吱吱作响,轻轻地打开了。

-你是清洁工吗? ——里面传来沙哑的声音。

- 是的,是我......我来修复一切。

詹姆斯走进电影院大厅。 显然别无选择,工作人员开始向游客分发纸质门票。 这使得财务报告变得困难,更不用说更有趣的细节了。 但工作人员松了口气迎接了詹姆斯,并立即将他带到了服务器机房。

乍一看,一切都很好。 詹姆斯登录服务器并检查了常见的可疑地方。 没问题。 然而,出于谨慎考虑,詹姆斯关闭了服务器,更换了网卡,并回滚了系统。 她立即​​开始全力工作。 工作人员又开始售票了。

詹姆斯打电话给马克,向他通报了情况。 不难想象詹姆斯可能想留下来看看是否会发生什么意外。 他走下楼梯,开始询问员工发生了什么事。 显然系统已经停止工作了。 他们把它关掉再打开,一切正常。 但10分钟后系统就掉下来了。

就在这时,类似的事情发生了。 突然,票务系统开始抛出错误。 工作人员叹了口气,抓起纸质门票,詹姆斯赶紧奔向服务器机房。 服务器一切看起来都很好。

然后一名员工走了进来。

— 系统再次运行。

詹姆斯很困惑,因为他什么也没做。 更准确地说,没有任何东西能让系统正常工作。 他退出后,拿起手机,拨打了公司的支持热线。 很快,同一名员工进入了服务器机房。

- 系统宕机了。

詹姆斯看了一眼服务员。 屏幕上舞动着一种有趣而熟悉的多彩形状图案——混乱地扭动和缠绕的管道。 我们都曾在某个时候见过这个屏幕保护程序。 它渲染得很漂亮,简直令人着迷。


詹姆斯按下按钮,图案就消失了。 他急忙赶到售票处,半路上遇到了回来找他的工作人员。

— 系统再次运行。

如果你能在心里捂脸,那就是詹姆斯所做的。 屏幕保护程序。 它使用OpenGL。 因此,在运行过程中,它会消耗服务器处理器的所有资源。 结果,每次对服务器的调用都会超时结束。

詹姆斯回到服务器机房,登录,然后将屏幕保护程序替换为带有空白屏幕的漂亮管道。 也就是说,我没有安装一个消耗 100% 处理器资源的屏幕保护程序,而是安装了另一个不消耗资源的屏幕保护程序。 然后我等了10分钟来验证我的猜测。

当詹姆斯到达下一家电影院时,他正在考虑如何向经理解释他刚刚飞了 800 公里来关闭屏幕保护程序。

在月相的某个阶段发生坠机

真实的故事。 有一天,出现了一个与月相有关的软件错误。 麻省理工学院的各种项目中常用一个小例程来计算月球真实相位的近似值。 GLS 将此例程构建到 LISP 程序中,该程序在写入文件时会输出带有近 80 个字符长的时间戳的行。 消息的第一行最终会变得太长并导致下一行的情况非常罕见。 当程序后来读取这个文件时,它咒骂了。 第一行的长度取决于确切的日期和时间,以及打印时间戳时阶段规范的长度。 也就是说,这个错误实际上取决于月相!

第一纸质版 术语文件 (Steele-1983) 包含一个导致所描述的错误的行的示例,但排字机“修复”了它。 此后这被描述为“月相错误”。

然而,要小心假设。 几年前,欧洲核子研究中心(CERN)的工程师在大型正负电子对撞机进行的实验中遇到了错误。 由于计算机在向科学家展示结果之前会主动处理该设备生成的大量数据,因此许多人推测该软件在某种程度上对月相敏感。 几位绝望的工程师查出了真相。 该误差的产生是由于月球经过期间地球变形导致27公里长的环的几何形状发生了微小变化! 这个故事已作为“牛顿对粒子物理学的复仇”进入物理学民间传说,也是最简单、最古老的物理定律与最先进的科学概念之间联系的一个例子。

冲厕所让火车停下来

我听说过的最好的硬件错误是在法国的高速列车上。 该漏洞导致火车紧急制动,但前提是车上有乘客。 在每次此类情况下,列车都被停止运行并进行检查,但什么也没发现。 然后他被送回线上,立刻就停了下来。

在一次检查中,一名乘坐火车的工程师去了厕所。 很快他就被冲走了,轰! 紧急停车。

工程师联系了司机询问:

— 刹车前你在做什么?

- 好吧,我在下降时放慢了速度......

这很奇怪,因为在正常运行期间,火车在下坡时会减速数十次。 火车继续前行,在下一个下坡处,司机警告道:

- 我要放慢速度。

什么都没有发生。

— 上次刹车时你做了什么? - 司机问道。

- 嗯...我在厕所...

- 好吧,那就去厕所,做我们再下去时做的事!

工程师去了厕所,当司机警告:“我要减速”时,他冲掉了水。 当然,火车立刻停了下来。

现在他们可以重现问题并需要找到原因。

两分钟后,他们注意到发动机制动遥控电缆(火车两端各有一个发动机)与电气柜壁断开,并躺在控制马桶插头电磁阀的继电器上......当继电器打开时,它会对制动拉索产生干扰,系统针对故障的保护仅包括紧急制动。

讨厌 FORTRAN 的网关

几个月前,我们注意到大陆(在夏威夷)的网络连接变得非常非常慢。 这种情况可能会持续 10-15 分钟,然后突然再次发生。 过了一段时间,我的同事向我抱怨大陆的网络连接 大体 不起作用。 他有一些 FORTRAN 代码需要复制到大陆的一台机器上,但无法复制,因为“网络支持的时间不够长,无法完成 FTP 上传”。

是的,原来是同事试图将FORTRAN源代码的文件FTP到大陆的机器上时出现网络故障。 我们尝试对文件进行归档:然后就顺利复制了(但是目标机没有解包器,所以问题没有解决)。 最后,我们将 FORTRAN 代码“拆分”成非常小的片段,并一次发送一个。 大部分片段复制没有问题,但有少数片段没有通过,或者之后通过 众多 尝试。

当我们检查有问题的段落时,我们发现它们有一些共同点:它们都包含以大写 C 组成的行开始和结束的注释块(正如一位同事更喜欢在 FORTRAN 中进行注释)。 我们给大陆的网络专家发了邮件寻求帮助。 当然,他们想查看我们无法通过 FTP 传输的文件样本……但我们的信件没有到达他们手中。 最后我们想出了一个简单的方法 描述不可传输的文件是什么样的。 它有效:) [我敢在这里添加一个有问题的 FORTRAN 注释之一的示例吗? 可能不值得!]

最终我们设法弄清楚了。 最近在我们校园和大陆网络之间安装了一个新的网关。 它在传输包含重复的大写 C 位的数据包时遇到了巨大的困难! 这些数据包中的一小部分就可能占用所有网关资源并阻止大多数其他数据包通过。 我们向网关制造商投诉……他们回答说:“哦,是的,你面临着重复 C 的错误! 我们已经了解他了。” 我们最终通过从另一家制造商购买新网关解决了这个问题(在前者的辩护中,无法传输 FORTRAN 程序对某些人来说可能是一个优势!)。

艰难时期

几年前,当我致力于用 Perl 创建 ETL 系统以降低 40 期临床试验的成本时,我需要处理大约 000 个日期。 其中两人没有通过测试。 这并没有让我太困扰,因为这些日期取自客户提供的数据,我们可以说,这些数据常常令人惊讶。 但当我查看原始数据时,结果发现这些日期是1年2011月1日和2007年30月XNUMX日。我以为这个bug包含在我刚刚写的程序中,结果发现已经有XNUMX年了老的。 对于那些不熟悉软件生态系统的人来说,这可能听起来很神秘。 由于另一家公司长期以来决定赚钱,我的客户付钱给我修复一个公司无意引入的错误,而另一家公司故意引入的错误。 为了让您理解我在说什么,我需要谈谈添加了最终成为错误的功能的公司,以及导致我修复的神秘错误的其他一些有趣的事件。

在过去的美好时光,苹果电脑有时会自发地将日期重置为 1 年 1904 月 1 日。 原因很简单:它使用电池供电的“系统时钟”来跟踪日期和时间。 电池没电后发生了什么? 计算机开始按照纪元开始以来的秒数来跟踪日期。 我们所说的纪元指的是参考原始日期,对于 Macintosh 来说,它是 1904 年 XNUMX 月 XNUMX 日。电池耗尽后,当前日期被重置为指定的日期。 但为什么会发生这种情况呢?

此前,Apple 使用 32 位来存储自原始日期以来的秒数。 一位可以存储两个值之一 - 1 或 0。两位可以存储四个值之一:00、01、10、11。三位 - 八个值之一:000、001、010、011、100 、101、110、111 等32可以存储232个值之一,即4秒。 对于 Apple 日期,这相当于大约 294 年,因此较旧的 Mac 无法处理 967 年之后的日期。 如果系统电池耗尽,日期将重置为自纪元开始以来的 296 秒,并且您必须在每次打开计算机时手动设置日期(或直到您购买新电池)。

然而,Apple 决定将日期存储为自纪元以来的秒数,这意味着我们无法处理纪元之前的日期,这会产生深远的影响,正如我们将看到的。 苹果引入了一项功能,而不是一个错误。 除其他外,这意味着 Macintosh 操作系统不受“千年虫”的影响(对于许多拥有自己的日期系统来规避限制的 Mac 应用程序来说,这是不能说的)。

前进。 我们使用了 Lotus 1-2-3,IBM 的“杀手级应用程序”,它帮助发起了 PC 革命,尽管 Apple 计算机拥有 VisiCalc,它使个人计算机取得了成功。 平心而论,如果1-2-3没有出现,个人电脑就很难腾飞,个人电脑的历史也可能有截然不同的发展。 Lotus 1-2-3 错误地将 1900 年视为闰年。 当微软发布第一个电子表格 Multiplan 时,它占据了一小部分市场份额。 当他们启动 Excel 项目时,他们决定不仅复制 Lotus 1-2-3 的行和列命名方案,而且还故意将 1900 年视为闰年,以确保错误兼容性。 这个问题今天仍然存在。 也就是说,在 1-2-3 中这是一个错误,但在 Excel 中这是一个有意识的决定,确保所有 1-2-3 用户都可以将其表导入 Excel 而不更改数据,即使数据不正确。

但还有另一个问题。 首先,微软发布了适用于 Macintosh 的 Excel,它不识别 1 年 1904 月 1 日之前的日期。而在 Excel 中,1900 年 XNUMX 月 XNUMX 日被认为是时代的开始。 因此,开发人员进行了更改,使他们的程序能够识别时代类型并根据所需的时代在其内部存储数据。 微软甚至为此写了一篇解释文章。 这个决定导致了我的错误。

我的 ETL 系统从客户那里收到了在 Windows 上创建的 Excel 电子表格,但也可以在 Mac 上创建。 因此,表中纪元的开始可能是 1 年 1900 月 1 日,也可能是 1904 年 XNUMX 月 XNUMX 日。 如何找出? Excel 文件格式显示了必要的信息,但我使用的解析器没有显示它(现在显示了),并假设您知道特定表的纪元。 我可能可以花更多的时间来理解 Excel 二进制格式并向解析器作者发送补丁,但我还有很多工作要做,所以我很快编写了一个启发式方法来确定纪元。 她很简单。

在 Excel 中,日期 5 年 1998 月 07 日可以用以下格式表示:“05-98-5”(无用的美国系统)、“Jul 98, 5”、“July 1998, 5”、“98-Jul-8601”或一些其他格式,另一种无用的格式(讽刺的是,我的 Excel 版本不提供的格式之一是 ISO 35981)。 但是,在表中,未格式化的日期存储为 epoch-1900 的“34519”或 epoch-1904 的“4”(数字表示自该纪元以来的天数)。 我只是使用一个简单的解析器从格式化日期中提取年份,然后使用 Excel 解析器从未格式化日期中提取年份。 如果两个值相差 1904 年,那么我就知道我使用的是 epoch-XNUMX 的系统。

为什么我不直接使用格式化日期? 因为 5 年 1998 月 98 日可以被格式化为“July, XNUMX”,并丢失该月的日期。 我们收到了来自许多公司的表格,这些公司以多种不同的方式创建它们,因此由我们(在本例中为我)来计算日期。 此外,如果 Excel 做得对,那么我们也应该如此!

同时我遇到了 39082。让我提醒您,Lotus 1-2-3 认为 1900 是闰年,并且在 Excel 中忠实地重复了这一点。 由于这会在 1900 年基础上增加一天,因此许多日期计算函数在这一天可能会出现错误。 也就是说,39082 可能是 1 年 2011 月 31 日(在 Mac 上)或 2006 年 2011 月 1900 日(在 Windows 上)。 如果我的“年份解析器”从格式化值中提取出 2006 年,那么一切都很好。 但由于 Excel 解析器不知道正在使用哪个纪元,因此它默认为 epoch-5,返回 XNUMX 年。 我的应用程序发现差异为 XNUMX 年,认为这是一个错误,将其记录下来,并返回一个未格式化的值。

为了解决这个问题,我写了这个(伪代码):

diff = formatted_year - parsed_year
if 0 == diff
    assume 1900 date system
if 4 == diff
    assume 1904 date system
if 5 == diff and month is December and day is 31
    assume 1904 date system

然后所有 40 个日期都被正确解析。

在大型打印作业中

1980 世纪 XNUMX 年代初,我父亲在存储技术公司工作,该部门现已解散,该部门生产磁带驱动器和用于高速磁带输送的气动系统。

他们重新设计了驱动器,以便可以将一个中央“A”驱动器连接到七个“B”驱动器,而控制“A”驱动器的 RAM 中的小型操作系统可以将读写操作委托给所有“B”驱动器。

每次启动驱动器“A”时,都需要将软盘插入到连接到“A”的外围驱动器中,以便将操作系统加载到其内存中。 它非常原始:计算能力由 8 位微控制器提供。

此类设备的目标受众是拥有大型数据仓库的公司(银行、零售连锁店等),这些公司需要打印大量地址标签或银行对账单。

一位客户遇到了问题。 在打印作业进行过程中,一个特定的驱动器“A”可能会停止工作,从而导致整个作业停止。 为了恢复驱动器的运行,工作人员必须重新启动一切。 如果这种情况发生在一个六小时任务的中间,那么就会浪费大量昂贵的计算机时间,并且整个操作的时间表会被打乱。

技术人员是从存储技术公司派来的。 但尽管他们尽了最大努力,他们仍无法在测试条件下重现该错误:它似乎发生在大型打印作业的中间。 问题不在于硬件,他们更换了所有可以更换的东西:RAM、微控制器、软盘驱动器、磁带驱动器的每个可能的部分 - 问题仍然存在。

随后技术人员给总部打电话,给专家打电话。

这位专家抓起一把椅子和一杯咖啡,坐在计算机房里(当时有专门的计算机房间),看着工作人员排队等待一份大型打印作业。 专家一直在等待故障发生——结果确实发生了。 所有人都看向高手,但他不明白为什么会出现这种情况。 于是他下令工作重新排队,所有工作人员和技术人员都返回工作岗位。

专家再次坐在椅子上,开始等待失败。 大约六个小时过去了,故障发生了。 专家再次毫无头绪,只是一切都发生在一个挤满了人的房间里。 他命令任务重新开始,然后坐下来等待。

第三次失败时,专家注意到了一些事情。 当工作人员更换外部驱动器中的磁带时,发生了故障。 而且,当一名员工走过地板上的某一块瓷砖时,故障就发生了。

高架地板由铝砖制成,铺设高度为 6 至 8 英寸。 许多计算机电线都铺设在高架地板下,以防止任何人意外踩到重要电缆。 瓷砖铺得非常紧,以防止碎片进入高架地板下面。

专家意识到其中一块瓷砖变形了。 当员工踩到瓷砖的角落时,瓷砖的边缘会与相邻的瓷砖发生摩擦。 连接瓷砖的塑料部件也会与瓷砖发生摩擦,从而引起静电微放电,从而产生射频干扰。

如今,RAM 得到了更好的保护,免受射频干扰。 但在那些年,情况并非如此。 专家意识到这种干扰会扰乱内存,进而扰乱操作系统的运行。 他致电支持服务,订购了新瓷砖,并亲自安装,问题就消失了。

涨潮了!

故事发生在朴茨茅斯(我认为)码头区一间办公室四楼或五楼的服务器机房里。

有一天,带有主数据库的 Unix 服务器崩溃了。 他们重新启动了他,但他高兴地继续一次又一次跌倒。 我们决定给支持服务人员打电话。

支持人员...我认为他的名字是马克,但这并不重要...我认为我不认识他。 真的没关系。 我们还是和马克一起吧,好吗? 伟大的。

因此,几个小时后,马克到达(从利兹到朴茨茅斯并不远,你知道),打开服务器,一切正常。 典型的该死的支持,客户对此感到非常沮丧。 马克查看了日志文件,没有发现任何异常情况。 于是马克回到火车上(或者他乘坐的任何交通工具,据我所知,这可能是一头跛脚牛……无论如何,这并不重要,好吗?)然后返回利兹,浪费了时间那天。

当天晚上服务器再次崩溃。 故事是一样的...服务器不升。 马克尝试远程提供帮助,但客户端无法启动服务器。

另一趟火车,公共汽车,柠檬酥皮或其他一些垃圾,马克回到了朴茨茅斯。 看,服务器启动没有任何问题! 奇迹。 马克花了几个小时检查操作系统或软件是否一切正常,然后出发前往利兹。

中午左右,服务器崩溃了(别着急!)。 这次引入硬件支持人员来更换服务器似乎是合理的。 但不,大约10小时后它也会下降。

这种情况连续几天重复出现。 服务器正常工作,大约 10 小时后崩溃,并且在接下来的 2 小时内无法启动。 他们检查了冷却、内存泄漏,他们检查了一切,但什么也没发现。 然后崩溃就停止了。

这一周无忧无虑地过去了……每个人都很开心。 快乐直到一切重新开始。 图片是一样的。 工作10小时,停机2-3小时...

然后有人(我想他们告诉我这个人与 IT 无关)说:

“是潮水了!”

这声惊呼引起了茫然的目光,有人的手可能在安全呼叫按钮上犹豫了。

“它不再随着潮汐而起作用。”

对于 IT 支持人员来说,这似乎是一个完全陌生的概念,他们不太可能坐下来喝咖啡时阅读《潮汐年鉴》。 他们解释说,这与潮汐没有任何关系,因为服务器已经工作一周,没有出现任何故障。

“上周潮水很低,但这周潮水很高。”

为那些没有游艇执照的人提供的一些术语。 潮汐取决于月球周期。 随着地球自转,太阳和月球的引力每 12,5 小时就会产生一次潮汐波。 在12,5小时的周期开始时有涨潮,在周期中间有退潮,在周期结束时再次涨潮。 但随着月球轨道的变化,低潮和高潮之间的差异也会发生变化。 当月球位于太阳和地球之间或在地球的另一侧(满月或无月)时,我们会得到 Syzygyn 潮汐 - 最高的高潮和最低的低潮。 在半月时,我们会得到正交潮汐——最低潮汐。 两个极端之间的差异大大减小。 月球周期持续 28 天: 日月 - 正交 - 日月 - 正交。

当技术人员了解到潮汐力的本质时,他们立即想到需要报警。 而且很符合逻辑。 但事实证明,这家伙是对的。 两周前,一艘驱逐舰停泊在离办​​公室不远的地方。 每当潮水将其提升到一定高度时,船上的雷达站就会与服务器室地板齐平。 雷达(或电子战设备,或其他一些军事玩具)在计算机中造成了混乱。

火箭的飞行任务

我的任务是将一个大型(约 400 万行)火箭发射控制和监视系统移植到新版本的操作系统、编译器和语言。 更准确地说,从 Solaris 2.5.1 到 Solaris 7,从使用 Ada 83 编写的 Verdix Ada 开发系统 (VADS) 到使用 Ada 95 编写的 Rational Apex Ada 系统。VADS 被 Rational 购买,其产品是尽管 Rational 尝试实现 VADS 特定包的兼容版本以简化向 Apex 编译器的过渡,但已过时。

三个人帮助我干净地编译了代码。 花了两周时间。 然后我自己努力让这个系统运转起来。 总之,这是我遇到过的最糟糕的软件系统架构和实现,所以又花了两个月的时间才完成移植。 然后该系统被提交进行测试,这又花了几个月的时间。 我立即纠正了测试中发现的错误,但数量很快就减少了(源代码是生产系统,所以它的功能运行得相当可靠,我只需要删除适应新编译器时出现的错误)。 最终,当一切都按预期进行时,我被转移到另一个项目。

感恩节前的星期五,电话响了。

火箭发射原本应该在大约三周内进行测试,但在倒计时的实验室测试期间,命令序列被阻止。 在现实生活中,这将中止测试,如果在启动发动机后几秒钟内发生堵塞,辅助系统将发生一些不可逆转的动作,这将需要火箭进行长期且昂贵的准备。 它不会开始,但很多人会对时间和大量金钱的损失感到非常沮丧。 不要让任何人告诉你国防部花钱鲁莽——我从来没有遇到过一个合同经理不把预算放在第一位或第二位,然后才是进度表。

在过去的几个月里,这个倒计时挑战已经以多种形式运行了数百次,只出现了一些小问题。 因此,这种情况发生的可能性非常低,但其后果却非常严重。 将这两个因素相乘,您就会明白,该新闻预测我和数十名工程师和经理的假期周将被毁。

作为移植系统的人,我也受到了关注。

与大多数安全关键系统一样,会记录大量参数,因此很容易识别系统崩溃之前执行的几行代码。 当然,它们绝对没有任何异常;在同一次运行中,相同的表达式实际上已经成功执行了数千次。

我们将 Apex 的人员召集到 Rational,因为他们是开发编译器的人,并且他们开发的一些例程在可疑代码中被调用。 他们(以及其他所有人)对有必要找到具有国家重要性的问题的根源感到印象深刻。

由于期刊中没有任何有趣的内容,我们决定尝试在当地实验室重现该问题。 这不是一件容易的事,因为该事件大约每 1000 次运行发生一次。 一个可疑的原因是调用了供应商开发的互斥函数(VADS 迁移包的一部分) Unlock 并没有导致解锁。 调用该函数的处理线程处理心跳消息,该消息名义上每秒到达。 我们将频率提高到10赫兹,即每秒10次,然后开始跑步。 大约一个小时后,系统自行锁定。 在日志中,我们看到记录消息的顺序与失败测试期间的顺序相同。 我们又进行了几次运行,系统在启动后 45-90 分钟内一直被阻塞,并且每次日志都包含相同的路线。 尽管我们在技术上运行不同的代码 - 消息频率不同 - 系统的行为是相同的,所以我们确信这种​​负载场景会导致相同的问题。

现在我们需要弄清楚表达式序列中阻塞发生的具体位置。

该系统的实现使用了 Ada 任务系统,但使用得非常糟糕。 任务是 Ada 中的高级并发可执行构造,类似于执行线程,仅内置于语言本身中。 当两个任务需要通信时,它们“设置集合点”,交换必要的数据,然后停止集合点并返回各自的独立执行。 然而,该系统的实施方式有所不同。 目标任务会合后,该目标任务会与另一个任务会合,然后该任务会与第三个任务会合,依此类推,直到完成某些处理。 在此之后,所有的交会都完成了,每个任务都必须返回执行。 也就是说,我们正在处理世界上最昂贵的函数调用系统,它在处理部分输入数据时停止了整个“多任务”过程。 而在此之前并没有导致问题只是因为吞吐量非常低。

我描述这个任务机制是因为当请求或期望完成集合时,可能会发生“任务切换”。 也就是说,处理器可以开始处理另一个准备执行的任务。 事实证明,当一个任务准备好与另一个任务交汇时,可以开始执行完全不同的任务,并最终控制权返回到第一个交汇点。 并且可能会发生其他事件导致任务切换; 其中一个事件是对系统函数的调用,例如打印或执行互斥体。

为了了解哪一行代码导致了问题,我需要找到一种方法来记录一系列语句的进度,而不触发任务切换,这将防止发生崩溃。 所以我无法利用 Put_Line()以避免执行 I/O 操作。 我可以设置一个计数器变量或类似的东西,但是如果我无法在屏幕上显示它,我如何才能看到它的值呢?

另外,在检查日志时发现,尽管心跳消息的处理被冻结,从而阻塞了进程的所有 I/O 操作并阻止了其他处理的执行,但其他独立任务仍在继续执行。 也就是说,工作并未完全被阻止,只是(关键)任务链被阻止。

这是评估阻塞表达式所需的线索。

我制作了一个 Ada 包,其中包含一个任务、一个枚举类型和该类型的全局变量。 可枚举文字被绑定到有问题的序列的特定表达式(例如 Incrementing_Buffer_Index, Locking_Mutex, Mutex_Unlocked),然后在其中插入赋值表达式,将相应的枚举分配给全局变量。 由于所有这些的目标代码只是在内存中存储一​​个常量,因此其执行导致的任务切换是极不可能的。 我们主要怀疑可以切换任务的表达式,因为阻塞发生在执行时,而不是在切换回任务时返回(出于多种原因)。

跟踪任务只是循环运行并定期检查全局变量的值是否已更改。 每次更改后,该值都会保存到文件中。 然后短暂的等待和一张新的支票。 我将变量写入文件是因为只有在问题区域切换任务时系统选择执行该任务时才会执行该任务。 该任务中发生的任何事情都不会影响其他不相关的被阻止任务。

预计当系统到达执行有问题的代码时,全局变量将在移动到下一个表达式时重置。 然后会发生一些事情导致任务切换,由于其执行频率(10 Hz)低于监控任务,监控器可以捕获全局变量的值并将其写入。 在正常情况下,我可以获得枚举子集的重复序列:任务切换时变量的最后值。 挂起时,全局变量不应再更改,最后写入的值将指示哪个表达式未完成。

我通过跟踪运行了代码。 他愣住了。 监控工作就像发条一样运转。

日志包含预期的序列,该序列被一个指示互斥体已被调用的值中断 Unlock,并且任务尚未完成 - 就像之前数千个调用的情况一样。

Apex 工程师此时正在狂热地分析他们的代码,并在互斥锁中找到了理论上可能发生锁定的位置。 但其概率非常低,因为只有在特定时间发生的特定事件序列才会导致阻塞。 墨菲定律,伙计们,这就是墨菲定律。

为了保护我需要的代码片段,我用一个小的本机 Ada 互斥包替换了互斥函数调用(构建在操作系统互斥功能之上),以控制对该片段的互斥访问。

我将其插入代码并运行测试。 七个小时后,代码仍然有效。

我的代码被提交给 Rational,他们在那里对其进行编译、反汇编,并检查它是否使用了有问题的互斥函数中所使用的相同方法。

这是我职业生涯中最拥挤的代码审查 🙂 房间里大约有 20 名工程师和经理和我在一起,另外 XNUMX 个人正在参加电话会议 - 他们都检查了大约 XNUMX 行代码。

代码被审查,新的可执行文件被组装并提交以进行正式的回归测试。 几周后,倒计时测试成功,火箭起飞。

好吧,这一切都很好,但是这个故事的重点是什么?

这绝对是一个令人恶心的问题。 数十万行代码、并行执行、十多个交互进程、糟糕的架构和糟糕的实现、嵌入式系统的接口以及数百万美元的花费。 没有压力,对吧。

尽管我在进行移植时处于聚光灯下,但我并不是唯一一个致力于解决此问题的人。 但即使我做到了,这并不意味着我理解了所有数十万行代码,甚至浏览了它们。 全国各地的工程师都对代码和日志进行了分析,但当他们告诉我他们对失败原因的假设时,我只花了半分钟就反驳了他们。 当我被要求分析理论时,我会把它传递给其他人,因为对我来说很明显这些工程师走错了路。 听起来很自以为是吗? 是的,这是事实,但我出于另一个原因拒绝了这些假设和要求。

我了解问题的本质。 我不知道具体发生在哪里或为什么发生,但我知道发生了什么。

多年来,我积累了很多知识和经验。 我是使用 Ada 的先驱之一,了解它的优点和缺点。 我知道 Ada 运行时库如何处理任务并处理并行执行。 我了解内存、寄存器和汇编器级别的低级编程。 换句话说,我在自己的领域有很深的知识。 我用它们来查找问题的原因。 我不仅仅解决了这个错误,我还了解如何在非常敏感的运行时环境中找到它。

对于那些不熟悉这种斗争的特征和条件的人来说,这种与代码斗争的故事并不是很有趣。 但这些故事可以帮助我们了解如何解决真正困难的问题。

要解决真正困难的问题,您需要的不仅仅是一名程序员。 您需要了解代码的“命运”、它如何与其环境交互以及环境本身如何工作。

然后你的假期周就会被毁掉。

要继续进行下去。

来源: habr.com

添加评论