0%

The Linker Is not a Magical Program

DEPRESSINGLY OFTEN(在我写这篇文章之前又发生了),很多程序员的观念中,将源码编译到静态链接的可执行文件的主要过程是:

  1. 编辑源码
  2. 将源码编译为目标文件(obj文件)
  3. 然后发生了一些神奇的事情
  4. 可以跑了

第3步,即链接这一步。为毛我要将其说得如此荒诞?想当年,我还是个技术支持,就一次又一次地关注这些内容:

  1. 链接器说def表示多次定义;
  2. 链接器说abc表示某个符号未解析;
  3. 为何最终执行文件会那么大?

紧接着会告诉你“我接下来做什么?”,它们通常都融合了“seems to(貌似)”或者“somehow(某种程度)”的短句,一看就自带光环属性。所谓的“貌似”和“某种程度”会让人觉得链接处理是一个非常神奇的过程,只有巫师和术士才能理解。编译过程不会详细说明这些短句类型,这就意味着程序员要自己理解编译器是如何工作的,或者至少明白它们在做什么。

链接器又笨、又乏味、又简单粗暴的程序。它所做的事情就是把目标文件的代码区和数据区串联起来,把引用连接到它们定义的地方,把未解析的符号推到库之外,然后输出一个可执行文件。仅此而已,没有咒语!没有魔法!链接器繁琐的地方是需要解码或生成极其复杂的文件格式,当然这并没有改变链接器的本质。

所以,我们才说链接器提示def就是告诉你多次定义。很多编程语言,如C、C++和D,都包含声明和定义。申明通常放在头文件里,就像这样:

1
extern int iii;

用于生成iii符号给外部调用。此外,定义通常是为了给符号留出空间,它通常在实现文件中,就像这样:

1
int iii = 3;

那么每个符号可以被定义多少次呢?就像电影Highlander一样,它们只允许一次。所以,如果iii定义在多个实现文件中出现?

1
2
3
4
// file a.c
int iii = 3;
// file b.c
double iii(int x) { return 3.7; }

链接器就会警告说iii被多次定义。

不仅是只能定义一个,也必须被定义一个。如果iii仅被声明过,却从来没有定义,那编译器同样会警告说iii是一个未定义的符号。

要搞清楚可执行文件为什么体积大,就要看链接器选了哪些映射文件生成。在可执行文件中映射文件只是一堆符号表,以及它们对应的地址。这能告诉你哪些模块会从库中被链接,以及每个模块的大小。现在你就能看到肥胖的根源了。很多时候,你根本不清楚为什么要链接库中的某个模块,为了搞清楚这点,暂时将模块从库中删除,重新链接,然后“未定义符号”的错误会告诉你到底谁在引用这些模块。

尽管链接器弹出的提示消息并不那么直观,但它真的没什么神奇的。它的机制很简单,你必须要掌握每种情况下的细节。

为你,千千万万遍!

一句简单朴实的话,直戳人性!犹如阿米尔的父亲——被拆成两半的男人,一半是富贵阶级的化生——阿米尔,一半是忠实仆人的化生——哈桑。本是亲兄弟的两个人,却因为阿富汗的人文与种族观念,而不得不定义为主仆关系。当然,对此事浑然不知的二人其实也没什么,有些事情如果从出生就一直是这样,就可以顺理成章地接受,即便它本身是个悲剧。

或许对于哈桑而言,他无所谓“哈扎拉人”这个总是受人鄙夷的标签,从小作一个佣人,不能上学,见谁都要称呼“老爷,少爷”。这一切与生俱来,那就照单全收。而最让他牵肠挂肚的,应该就是自己的主子阿米尔了,或许不是主子,应该是儿时玩伴,亲梅竹马的好朋友。面对好友,以诚相待,为他付出再多也不求回报,唯一的期许也仅仅是他嘴角上扬。也正是这样,哈桑面对小主人下令去追风筝时,才会肆无忌惮地说:“为你,千千万万遍!”

而身为主人的阿米尔呢?同样与身俱来的权贵,和一开始就确定的主仆关系,他会像哈桑撇开所谓的阶层,把对面这家伙当朋友吗?显然,一个正常人,在习惯于自己的崇高地位后,恐怕没那么容易放下身段,何况他还是个孩子。所以在阿米尔眼中,“哈扎拉仆人”才是哈桑理所当然的身份,他也应该这么面对哈桑。所以当没人的时候,他和哈桑玩得很开心;当家中有贵客时,他选择与哈桑保持距离;当他们被欺负时,他选择躲在哈桑身后;当他看到哈桑被强暴时,他选择视而不见。为了让自己良心好过点,他问哈桑是否会听自己的话去吃泥巴,他问父亲说:“有没有想过请新的佣人?”

这位父亲,他永远保持正义,努力做善事,撑起整个家,永远公平地对待自己的亲儿子与哈桑。近乎怒吼自己的儿子永远不要说这种蠢话,哈桑他们是家人,不是佣人。父亲是那样伟岸,那样无可挑剔,直到后来,阿米尔才知晓真相,自己的父亲和妈妈生下了自己,父亲又睡了仆人的老婆,生下了哈桑。其实从一开始,父亲所做的一切,都是在自我救赎。

可惜,战争来了,这场救赎被打断了,父亲带着阿米尔逃到美国避难,曾今的家园留给了佣人阿辛汗。而哈桑在得知阿辛年迈,无力守卫这座家园时,他第一时间回到这里,揽下所有本不属于他的责任,静静地守护着它。夜晚依旧回到破旧的佣人房里,哪怕体面的主人屋子早已人去楼空——这是他的态度。

终于,阿辛汗叫回了阿米尔,近乎恳求他去完成一件非常重要的事。然而对阿米尔来说,哈桑是自己的弟弟,这只不过是个事实,与根深蒂固的“哈扎拉仆人”相比,似乎不重要。
哈桑在常年战乱的阿富汗死去…
哈桑还有个儿子被抓了…
那只是个仆人的孩子…
那是自己的亲侄儿…
哈桑一辈子对这个家不求回报地付出…
自己永远在索取,并选择在关键时刻牺牲掉哈桑…
父亲会怎么做?应该会毫不犹豫冲入战区去营救…
自己已定居美国,有美好的家庭与事业…

阿米尔在挣扎,他已经当了一辈子的懦夫,他的父亲最失望的地方,就是他不懂得担当,他似乎明白了阿辛汗那句话:“那儿有再次成为好人的路”。

我想,当阿米尔得知身世的真相,得知弟弟哈桑的凄惨,得知自己还有未完成的使命时,他内心时拒绝的。他依然想做回那个躲在父亲和哈桑身后的阿米尔,一切都与他无关,只不过是个仆人的孩子,没那么重要。也或许他已经遭受了大半辈子的煎熬,在他内心深处,依然无法回避良心的谴责,他欠哈桑太多,他一直希望父亲以他为荣,可父亲最看重的责任感,在他身上却找不到。

他一开始仅仅是跟着感觉走,深入战区,枪林弹雨,看到了难民的疾苦,看到在球场被虐杀的男女,看到了左眼眶空空如也的乞丐(曾今的大学教授),看到了人间地狱,看到战争可以夺走作为人的基本尊严。就像塔利班的那个魔头阿塞夫的理念,对待这些垃圾种族,杀了他们都算好的——“狗肉”应该拿给狗吃,事实上他们也正是这么做的。

哈桑的儿子索拉博,他那么小,战争夺走了他的家人,他被抓紧难民营,幼小的身躯被强暴了,终有一天会在这里毫无尊严地死去,没人会在乎,和死了条野狗没什么区别。

我想,阿米尔应该是在这短暂的回乡“旅途”中逐渐意识到了这些,才会从一开始被“剧情”推着走,到后来义无反顾地要救出索拉博,哪怕是为之付出生命。或者,他不仅仅是要救索拉博,更重要的是自我救赎。

诚然,当他近乎被阿塞夫打死,用半条命带走了索拉博后,这场救赎才刚刚开始。哈桑的儿子已经被战争摧残到麻木,他的眼里空洞无光,一切的不公他都逆来顺受。他只想回到过去,和爸爸妈妈平静地过着生活,可惜一切都消失了,像梦一样。阿米尔能做的,只有代替哈桑,将这个孩子从战争的阴影中拖出来,帮他找回作为人的尊严,不仅仅是哈扎拉身份,不仅仅是见了谁都叫“老爷,少爷”。

又或者,在那个新年,那片草坪,阿米尔并不打算代替哈桑。对他而言,索拉博就是哈桑的第二次生命,这是真主的指示,他们回到了十二三岁的时光。这次轮到阿米尔了,他努力斗风筝给哈桑看,唯一的期许是哈桑的嘴角微扬,他拼命追向风筝,并回头冲哈桑喊:“为你,千千万万遍!”

一点简单的书评

我很喜欢这部小说,翻译的简直不要太好,要不是小说内容清一色是国外人名,我甚至有点怀疑原著恐怕就是用中文完成的吧。《追风筝的人》其实并不是那种温情脉脉的剧情,或者一味批判战争和揭露人性。要不说作者的文字功底很强嘛,读此书的时候很有代入感,仿佛一幅幅画面从我脑海中闪过。书中每次描述人们的内心活动时,我都会想,其实我在这样的年纪这样的处境,搞不好会和他一样。所以,整部小说总能流露出一种情感上的真实,好像是作者本身的亲身经历一样。

不论如何,本书都非常经典。剧情环环相扣,波澜起伏。我很同情哈桑,也很敬重他们的父亲,他们都很了不起。当然,阿米尔最终为自己,替父亲完成了救赎,这样的结局算不上圆满,却是应得的归宿。

到底什么是软件开发?

程序员,他们总会有另外一种头衔,软件工程师、高级工程师、架构师诸如此类。在软件开发中,程序员自己也喜欢用一些术语:模块、中间件、封装、框架结构等等。听起来是不是很像在搞建筑,没错,连程序员自己也乐于自嘲“搬砖”,久而久之,这些头衔和术语就会给外行一种感觉:“软件开发就是一种建设工程”。于是,当某个单位需要某种大型软件来支撑业务的时候,我们就把这场软件开发称之为:“信息化建设”。

这是罪孽的源头!我们正在把真实的建筑项目管理的思想灌输到软件研发中。

人月——完成一个人物所需的人力和时间,这是建筑工程惯用的思维模式。10个人盖一栋房子需要10个月,那好,把工人增加到100个三班倒,项目周期就可以压缩到1、2个月。不过很可惜,人月在软件项目中,只是一种神话

软件工程是一次协同创造,而非共同组装。它更像是拍一部电影、写一部小说、编一首曲子、画一幅壁画。你什么时候听过后者会用“工程”来描述自己所做的事情,他们更多会谈:灵感、节奏、创意等。而软件开发和它们稍微不同的地方在于,它总会有几个到上千个程序员负责开发——他们都是创造者,就好像一部电影有多个导演,一部小说由多人执笔……

大型软件开发就像陷入焦油坑的巨兽,永远抓不住重点。当需求变化时、当交付期临近时、当技术攻关受阻时,我们会想当然地认为原因是人手不足。于是工程思维登场了,时间、目标、成本、人力资源,通过科学估算后开始增派人手,希望在可控的时间成本下完成预期目标。

然而我们忽略了一个问题——项目复杂度。抱歉,程序员是项目复杂度的原因之一。如果是工地搬砖,那么人数和工作效率是成正比的。如果是编程呢?新加入的程序员需要熟悉业务、项目培训,由于个体差异,还会制造新bug。所以,当软件项目增加人手时,管理成本、沟通成本就会成指数上升,项目复杂度也会疯狂增长。好的情况下,项目进度会得到一点点改善,坏的情况下,不仅项目延期,整个团队都会陷入混乱。

不要用人月去规划软件工程的子任务,我们习惯于把软件研发的绝大部分事情划归给“编程”,然而作者的观点是:1/3计划、1/6编码、1/4构件测试、1/4系统测试。是的,编码只占少得可怜的计划时间,为何呢?

所谓集成,不是用胶水将模块粘到一起

软件开发中的模块化思想,是为将业务拆解,高内聚低耦合,从表面上看还有个好处:可以把不同的模块划分给不同的人或团队去开发。不同的业务模块并行开发,项目效率不就提高了吗?

我记得在读《Just for Fun》的时候,Linus点评过苹果OSX系统的“微内核”架构的,非常值得玩味。Linux的确是宏内核架构,它给人的感觉是大而全很笨重,所以有人会想到“把一个整体切成多个小块,等同于把一个大的困难切分成很多小困难”,一次性解决大的困难很棘手,拆分成很多小困难就容易多了。于是就有了模块化思想的微内核。Linus并没有说这种思想不好,而是提出了新视角。模块化思想就好比人的大脑,不同区域负责不同功能——但有没有考虑过各个功能模块之间存在联系

是的,设想一下,一部小说分为20章节,不同章节由不同的作家同时编写;一部电影被拆分成多个场景,不同场景由不同导演完成;一幅人像油画分为头/手/脚/身,由不同画家绘制。最后的最后,再把拆分的东西在合到一起。原本一个人需要一年完成的事情,现在10个人并行创造,一个月就搞定了。但是!你觉得这样的作品能看么?

没错,大型软件的矛盾就在于,它犹如给万里长城画壁画、拍10000小时的纪录片,写一千万字的小说,最后还要给人一气呵成的感觉。但想想都知道,一个人要完成如此巨著,要多长时间?市场能等吗?我们只能无奈地将其拆成模块,增加人手,东拼西凑,满身bug。

回看软件项目本身,作者是给出了自己的一些建议:

概念的完整性

还是以绘画为例,画家在下笔之前,脑子里是存在一幅模糊但完整的作品的,由于整个作品都是它自己完成,所以它可以按照自己的节奏和把握作画。

软件产品最初同样是存在少数几个人脑中,最有可能是产品经理或项目经理,但是实际开发软件的人未必是他,而是占大多数的程序员。那么问题来了,作为真正打磨作品的匠人,脑子里却没有一幅完整的画面,只是负责领导分配的某个局部,甚至连自己到底要做什么都不知道。这种项目大概率要偏离航道。

尽管很多软件项目负责人亲自编程,但作者还是建议,负责人应该是系统设计和架构师的角色,你自己必须非常清楚软件最终的样子,并使尽浑身解数灌输的团队每个人的脑子里。当遇到技术困难时,你应该提出你的建议,也仅止于建议,否则团队会觉得你瞎指挥,你自己也会累半死。

外科手术式队伍

这是很美妙的比喻!软件项目的团队应该像外科手术团队,而非建筑施工队。每个人在自己的专业领域,针对某个职能,从头负责到底。团队一般二三十人,必须有一个主刀人,还需要一个副手,不仅有程序员,还有项目助理、行政、文档管理等。

这是为了让架构师双手才能从细节中挣脱,严格把控项目的方向和节奏。研发手册、文档、会议记录等资料才能及时跟进,小团队的沟通和执行效率会更高。当整个团队都清楚自己的努力方向时,项目才是前进的。

贯彻执行!沟通!沟通!

进度滞后不是一天造成的,而是不知不觉的积累。软件研发有这样一种现象,领导不懂技术或者无法掌控细节,团队里的人就会找各种理由来掩饰自己的工作情况,表面上看都是一帆风顺,到项目后期却一再延期。

作者的建议是两个,要么随时通过会议、里程碑、沟通的方式,随时掌握最新动态;要么一下子掀开地毯,你会发现底下全是蟑螂(bug)。

即便在项目进展看似不错的情况下,也务必注意沟通。沟通效率低下的团队,项目失败的风险太大。所以,一是注意给团队营造良好的沟通氛围,二是沟通渠道通畅。此外务必记住,每增加一个人,团队的沟通成本也会增加。

交付体验,而不仅仅是产品

软件功能很重要,软件的使用体验也同样重要!多亏了苹果,让我们明白什么是体验,但真没想道本书40年前就洞见了这一点。

关于软件项目的管理,书中还介绍了很多工具和方法,有的现在已经很普及——比如自动化测试、高级语言;有的是杞人忧天——比如软件会把硬盘占满。

好软件是长出来的

传统工程思维难以在软件项目中实践,开发是一种创造,像生命的孕育,必须“十月怀胎”,那就站在生命的角度,思考一下新的软件开发模式。

增量开发

其实这种概念和后来的敏捷开发大同小异。废除瀑布模型,快速做出产品原型,然后交付——反馈——开发——交付——反馈,如此重复,渐进增长,直至达到预期。这就好比从受精卵到成人的过程,总是先有核心,然后在逐渐延伸出来。软件领域这么做,可以提早发现bug和新的需求,并及时跟进市场。

关于“没有银弹”

困扰软件行业的一大问题就是如何提升生产效率,就好像客户需要盖一栋房子,找建设单位,有了设计图纸,房子就能盖起来。可软件工程总是一再延期,总在维护。有没有什么方式,能从根本上提升软件项目的开发效率,作者的答案是没有。

作者一再强调,“根本”和“次要”问题。我的理解:根本问题是如何让软件开发效率翻几十倍,几个程序员可以在几个月就完成超大型软件项目;次要问题是,通过什么技术,能够提高编写代码的速度和质量。

用40年前的眼光来看,人工智能、面向对象、图形界面都是非常有潜力的技术。面向对象主要能实现软件的复用能力,就好像把软件做成各种零件,然后售卖给加工厂;而人工智能是为了尽量减少编写代码,减少bug,专家级程序员或架构师可以更高效看到结果。

毫无疑问,面向对象或集成开发环境等工具的使用,的确提升了现在程序员的效率,但项目管理依然失控,开发进度依然滞后。所以,这些工具其实解决的次要问题。我基本同意作者的观点——没有银弹。

《人月神话》书评

《人月神话》应该是我还在上大学的时候就听说的,工作多年后终于有幸拜读。坦白说,还好是现在的我读了此书——如果是学生时代的我读了的话,要么认为这是一本垃圾,要么跟风说它是传世之作。

本书总体有些晦涩,一是偏管理学术,二是年代久远、三是翻译确实不太好。比如我第一次看到“结构师”的时候一脸懵逼——以前的程序员还要负责制图么?然后我看到了architect,好吧。书中在讨论很多项目或产品研发时,把重点放在了该用高级语言还是汇编,该为用户保留多少内存空间,该不该搭建测试服务器等等。毫无疑问,这些问题在今天根本不值一提,然而40年前呢?那个内存按K来计算,CPU慢成狗,还没有图形界面,计算机还死贵死贵的年代,这些确实是问题。

作者用40年前的眼光思考了未来的软件开发模式,看看现在的大型软件开发管理,DDD/TDD、持续集成、持续部署、持续交付。不得不说,《人月神话》里外科手术队伍、系统测试、增量开发、交付体验等观点,确实在今天被一一验证了,也许这也是它越来越经典的原因吧。

这几年的程序员经历,让我对研发项目有切身的体会,在读此书的时候才有很多共鸣。

Let Your Rroject Speak for Itself

你的项目可能有一个版本控制系统在其中。也许它还链接到了一台持续集成服务器中,通过自动化测试正确性。那可太优秀了,老铁。

你可以在持续集成服务器中加入静态代码分析工具用来收集代码指标。这些指标能提供代码特定方面的反馈,以及它们随着时间推移是如何演变的。当你安装了代码指标后,总会有一条你不想逾越的红线。假设你从20%的测试覆盖率开始,并且不希望低于15%。持续集成服务器能让帮你一直跟踪这些数字,但你不得不周期性地检查。想象一幅画面,有可以将这个人物交给项目自身,当有错误的时候可以依赖它的报告。

你需要给项目一个声音。它能够通过email或及时消息动作,通知开发者有关最近一次下降或改进的数字。但是,在办公室采用极限反馈设备(XFD),它能更有效地体现这个项目。

所谓XFD就是触发一个物理设备,比如一盏灯、一个便携喷泉、一个玩具机器人、甚至一个USB火箭发射器,基于自动分析的结果来触发。当你的限定范围被打破,设备就会改变状态。比如灯泡,它会亮起,闪瞎你的眼。哪怕在你着急出门回家的情况下,也不可能错过这个消息。

依托于各种类型的极限返回设备,你可以听到构建碎了一地,看到代码的红色警报,闻到它身上的香水味。如果你在分布式团队里上班,这些设备也可以复制到不同地方。去你们经理的办公室里给他装一台红绿灯吧,随时展示产品的健康度,这哥们儿肯定会感激你祖宗八辈。

发挥你的想象去选择一个合适的设备。如果你们团队文化相当让人不爽,你可以为团队找个遥控玩具来做吉祥物。如果你想要逼格高点,那就选一款高大上的灯泡。可以去网上寻找灵感,任何具备电源插头或远程控制功能东西都有可能拿来做极限反馈设备。

极限反馈设备充当了项目的音箱后,项目就和开发者物理上联系在一起,根据团队的表现鞭挞或称赞他们。你可以应用语音合成软件+一对高音喇叭来进一步将其人格化。现在,你们的项目真的会自己说话了!

本文源码:https://github.com/Philon/rpi-drivers/tree/master/03-device_io

本文是上一篇《GPIO驱动之按键中断》的扩展,之前的文章侧重于中断原理及Linux/IRQ基础,用驱动实现了一个按键切换LED灯色的功能。但涉及中断的场景,就会拔出萝卜带出泥要实现IO的同步/异步访问方式。归根结底,驱动要站在(用户层)接口调用的角度,除了实现自身的功能,还要为用户层提供一套良好的访问机制。本文主要涉及以下知识点:

  • 机制与策略原则
  • IO阻塞/非阻塞——read/write
  • IO多路复用——select/epoll
  • 信号异步通知——signal

机制与策略

Linux的核心思想之一就是“一切皆文件”,而这其中最重要的原则便是“提供机制,而非策略”。文件——内核层与用户层分水岭,通过文件,用户可以用简单而统一的方式去访问复杂而多样的设备,且不必操心设备内部的具体细节。然而哪些部分该驱动实现,哪些部分又该留给用户实现,这是需要拿捏的,我个人理解:

  • 机制,相当于怎么做,提供某个功能范围的实现形式、框架、标准
  • 策略,相当于做什么,提供某种功能的具体实现方法和细节

以上一篇按键中断的驱动为例——“按一下切换一种灯色”,显然,这根本不符合驱动设计原则。首先,驱动把两种设备打包进一个程序;其次,驱动实现了“切换灯色”这个具体业务功能(策略);最后,驱动根本没有提供用户层的访问机制。

还是回到需求——“按一下切换一种灯色”,理想情况下应该是这样的:

  1. LED驱动——向用户层提供灯色切换机制
  2. 按键驱动——向用户层提供“按下事件”通知/获取机制
  3. 由用户层自行决定收到按键事件后,如何切换灯色

从以上分析看,原本一个驱动代码分拆分成led驱动、按键驱动、切灯app三个部分:led驱动已经在第1章《GPIO驱动之LED》里实现了,因此现在还差两件事:

  • 按键驱动,砍掉原有的led功能实现,增加中断标志获取的访问机制
  • 切灯app,这个就无所谓了,只要有了机制,策略想怎么写就怎么写

IO的阻塞/非阻塞访问

阻塞/非阻塞,是访问IO的两种不同机制,即同步和异步获取。

所谓阻塞访问,就是当用户层read/write设备文件时,如果预期的结果还没准备好,进程就会被挂起睡眠,让出CPU资源,直到要读写的资源准备好后,重新唤醒进程执行操作。以本文实际的按键中断来说,当用户层读按键设备节点时,只要按键没有被按下,进程就应该一直阻塞在read函数,直到触发中断后才返回。

所谓非阻塞访问,就是调用open(file, O_NONBLOCK)或者ioctl(fd, F_SETFL, O_NONBLOCK),将文件句柄设置为非阻塞模式,此时,如果要读写的资源还没准备好,read/write会立刻返回-EAGAIN错误码,进程不会被挂起。

简而言之:

  • 阻塞模式:read/write时会被挂起,让出CPU,无法及时响应后续业务
  • 非阻塞模式:read/write时不会挂起,占有CPU,不影响后续程序执行

显然,通过以上介绍,驱动需要在自己的xxx_read() xxx_write()等文件接口里实现阻塞功能,在Linux内核中主要通过“等待队列”来实现进程的阻塞与唤醒的。

等待队列

等待队列是基于链表实现的数据结构,用来表示某个事件上所有等待的进程。内核接口如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <linux/wait.h>

// 等待队列链表数据结构
struct wait_queue_head {
spinlock_t lock;
struct list_head head;
};
typedef struct wait_queue_head wait_queue_head_t;

// 初始化一个等待队列
#define init_waitqueue_head(wq_head)

// 声明一个等到节点(进程)
#define DECLARE_WAITQUEUE(name, tsk)

// 添加/删除节点到等待队列中
void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry);
void remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry);

///////////////////////////////////////////
// 往下看之前先记住两个函数
// set_current_state(value) 👈设置当前进程的状态,比如阻塞
// schedule() 👈调度其他进程
///////////////////////////////////////////

// 【接收】开始等待事件到来
// 以下宏,均是由上边的两个函数封装而言,不过是设置进程状态不同罢了
#define wait_event(wq_head, condition) // 不可被中断
#define wait_event_interruptible(wq_head, condition) // 可被信号中断
#define wait_event_timeout(wq_head, condition, timeout) // 会超时
#define wait_event_interruptible_timeout(wq_head, condition, timeout) // 可被信号中断和超时

// 【发送】唤醒队列中的所有等待队列
#define wake_up(x)
#define wake_up_interruptible(x)

上边的API比较难理解的就是wait_event_xxxwake_up_xxx,其实很简单,一般在驱动的读写接口里调用wait_xxx让进程切换到阻塞状态等待唤醒,然后在中断或其他地方调用wake_up_xxx即可唤醒队列。再有,通常情况下建议使用interruptible模式,否则进程将无法被系统信号中断。

最简单的按键阻塞实现

下面来实现按键的阻塞访问,当用cat /dev/key命令去读按键是否按下时,应该会被阻塞,直到按键真的被按下。为此,需要实现:

  1. init函数中初始化gpio按键相关,以及一个“等待队列”
  2. read函数中创建一个“等待”,然后进入阻塞,直到被唤醒
  3. 在中断响应函数中,唤醒整个“等待队列”

PS:以下驱动也顺便实现了“非阻塞”访问模式,这个其实很简单,无非就是判断以下文件标识是否为O_NONBLOCK即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/uaccess.h>

#define KEY_GPIO 17

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Philon | https://ixx.life");

static wait_queue_head_t r_wait;

// 读取阻塞
static ssize_t gpiokey_read(struct file *filp, char __user *buf, size_t len, loff_t * off)
{
int data = 1;
// 新建一个”等待“并加入队列
DECLARE_WAITQUEUE(wait, current);
add_wait_queue(&r_wait, &wait);

if ((filp->f_flags & O_NONBLOCK) && !gpio_get_value(KEY_GPIO)) {
// 如果是非阻塞访问,且按键没有按下时,直接返回错误
return -EAGAIN;
}

// 进程进入阻塞状态,等待事件唤醒(可被信号中断)
wait_event_interruptible(r_wait, gpio_get_value(KEY_GPIO));
remove_wait_queue(&r_wait, &wait);

// 被唤醒后,进行业务处理,返回一个“按下”标志给用户进程
len = sizeof(data);
if ((data = copy_to_user(buf, &data, len)) < 0) {
return -EFAULT;
}
*off += len;

return 0;
}

static int press_irq = 0;
static struct timer_list delay;

// 按键中断顶半部响应及防抖延时判断
static irqreturn_t on_key_pressed(int irq, void* dev)
{
mod_timer(&delay, jiffies + (HZ/20));
return IRQ_HANDLED;
}
static void on_delay50(struct timer_list* timer)
{
if (gpio_get_value(KEY_GPIO)) {
// 按下按键后,唤醒阻塞队列
wake_up_interruptible(&r_wait);
}
}

struct file_operations fops = {
.owner = THIS_MODULE,
.read = gpiokey_read,
};

struct miscdevice gpiokey = {
.minor = 1,
.name = "gpiokey",
.fops = &fops,
.nodename = "mykey",
.mode = 0700,
};

static int __init gpiokey_init(void)
{
// 初始化定时器,用于防抖延时
timer_setup(&delay, on_delay50, 0);
add_timer(&delay);

// 初始化“读阻塞”等待队列
init_waitqueue_head(&r_wait);

// 向内核申请GPIO和IRQ并绑定中断处理函数
gpio_request_one(KEY_GPIO, GPIOF_IN, "key");
press_irq = gpio_to_irq(KEY_GPIO);
if (request_irq(press_irq, on_key_pressed, IRQF_TRIGGER_RISING, "onKeyPress", NULL) < 0) {
printk(KERN_ERR "Failed to request irq for gpio%d\n", KEY_GPIO);
}

// 注册驱动模块并创建设备节点
misc_register(&gpiokey);
return 0;
}
module_init(gpiokey_init);

static void __exit gpiokey_exit(void)
{
misc_deregister(&gpiokey);
free_irq(0, NULL);
gpio_free(KEY_GPIO);
del_timer(&delay);
}
module_exit(gpiokey_exit);

按键中断阻塞访问效果

多路复用IO模型-poll

如果程序只监听一个设备,那用阻塞或非阻塞足够了,但如果设备数量繁多呢?比如我们的键盘,有一百多个键,难道每次都要全部扫描一遍有没有被按下?(当然,键盘事件有另外的机制,这里只是举个例子)

这个时候轮询操作就非常有用了,据我所知有很多小型的网络服务正是用此机制实现的高性能并发访问,简单来说,就是把成千上万个socket句柄放到一种名叫fd_set的集合里,然后通过select()/epoll()同时监听集合里的句柄状态,其中任何一个socket可读写时就唤醒进程并及时响应。

综上,设备驱动要做的,便是实现select/epoll的底层接口。而有关select的应用层开发这里就不介绍了,网上一大堆。

驱动模块的多路复用实现其实非常简单,和读写接口一样,你只需实现file_operations里的poll接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <linux/poll.h>

// file_operations->poll
// 由驱动自行实现多路复用功能
__poll_t (*poll) (struct file *, struct poll_table_struct *);

// 在具体实现poll接口是需要调用
// 该函数本身不会引发阻塞,仅仅是把select的等待指向驱动模块
// 睡眠是由用户层等select()自身完成的
// 当它遍历完全部的设备文件后,相当于把自己的等待节点指向了每一个设备驱动
// 任何一个设备唤醒时都会触发select唤醒
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p);

// 既然poll接口不会阻塞,那就直接告诉用户层,设备当前的可操作状态
// 便于select判断文件的读写状态
#define POLLIN 0x0001 // 可读
#define POLLPRI 0x0002 // 紧急数据可读
#define POLLOUT 0x0004 // 可写
#define POLLERR 0x0008 // 错误
#define POLLHUP 0x0010 // 被挂起
#define POLLNVAL 0x0020 // 非法

#define POLLRDNORM 0x0040 // 普通数据可读
#define POLLRDBAND 0x0080 // 优先数据可读
#define POLLWRNORM 0x0100 // 普通数据可写
#define POLLWRBAND 0x0200 // 优先数据可写
#define POLLMSG 0x0400 // 有消息
#define POLLREMOVE 0x1000 // 被移除
#define POLLRDHUP 0x2000 // 读被挂起

结合第二小结的等待队列,实现gpio按键的多路复用IO就非常简单了,只需要在原有的代码里加入下面这段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 实现内核的poll接口
static __poll_t gpiokey_poll(struct file *filp, struct poll_table_struct *wait)
{
__poll_t mask = 0; // 设备的可操作状态

// 加入等待队列
poll_wait(filp, &r_wait, wait);
if (gpio_get_value(KEY_GPIO)) {
// 按键设备不存在写,所以总是返回可读,如果可以时
mask = POLLIN | POLLRDNORM;
}

return mask;
}

// 注意接口要加入到文件操作描述里
struct file_operations fops = {
.owner = THIS_MODULE,
.read = gpiokey_read,
.poll = gpiokey_poll,
};

异步通知-信号

不论IO阻塞、非阻塞还是多路复用,都是由应用程序主动向设备发起访问,有没有一种机制,就像邮箱一样,当有信息来临时再通知用户,用户仅仅是被动接收——答:信号!

信号本质上来说,就是软件层对中断的一种模拟。正如常见的Ctrl+Ckill等,都是向进程发送信号的手段。所以信号也可以理解为是一种特殊的中断号或事件ID。其实在Linux应用开发中,会涉及很多的“终止/定时/异常/掉电”等信号捕获,我们写的程序之所以能被Ctrl+C终止,就是因为在应用接口里已经实现了相关信号的捕获处理。

为了更好地理解设备驱动有关信号机制的实现,必须先站在用户层的角度看看信号是如何被调用的。有关Linux常用的标准信号这里也不展开讨论,请用好互联网。这里仅仅是看一个应用程序如何接收指定设备的SIGIO信号的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/fcntl.h>

// 按键信号的处理函数
void on_keypress(int sig)
{
printf("key pressed!!\n");
}

int main(int argc, char* argv[])
{
// 第一步:将驱动的拥有者指向本进程,否则设备信号不知道发给谁
int oflags = 0;
int fd = open("/dev/mykey", O_RDONLY);
fcntl(fd, F_SETOWN, getpid());
oflags = fcntl(fd, F_GETFL) | O_ASYNC;
fcntl(fd, F_SETFL, oflags);

// 第二步:捕获想要的信号,并绑定到相关处理函数
signal(SIGIO, on_keypress);

// 以下无关紧要,就是等到程序退出
printf("I'm doing something ...\n");
getchar();
close(fd);
return 0;
}

从以上代码来看,应用程序要实现信号捕获需要操作:

  1. F_SETOWN让设备文件指向自己,确保信号的传输目的地
  2. O_ASYNC或者FASYNC标志告诉驱动(即调用驱动的xxx_fasync接口),我要去做其他事了,有情况请主动通知我
  3. 设置信号捕获及相关处理handler

所以对应的,内核模块也需要实现信号的发送也需要三个步骤:

  1. filp->f_onwer指向进程ID,这点已经又内核完成,不用再实现
  2. 实现xxx_fasync()接口,在里面初始化一个fasync_struct用于信号处理
  3. 当有情况时,使用kill_fasync()发送信号给进程

内核具体接口如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 设备文件的异步接口,当用户层标记了O_ASYNC或FASYNC时触发
struct file_operations {
int (*fasync) (int fd, struct file *filp, int mode);
...
};

// 异步“小助手”,初始化用,一般在xxx_fasync()接口里调用
// 前面三个参数由用户层传进来,最后一个是“异步队列”,该函数会为其分配内存初始化
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);

// 通过fa,发送信号到进程
// 后两个参数为信号ID、可读/可写状态
void kill_fasync(struct fasync_struct **fa, int sig, int band);

// 最后务必注意,在xxx_close或xxx_release中让文件描述从异步队列中剥离
// 否则用户进程挂了,驱动还一直向其发送信号,岂不有病
static int xxx_close(struct inode *node, struct file *filp)
{
...
xxx_fasync(-1, filp, 0);
}

搞清楚了内核关于异步信号的机制,下面用让gpiokey支持SIGIO信号吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
static struct {
int irq; // 按键GPIO中断号
struct timer_list delay; // 防抖延时
wait_queue_head_t r_wait; // IO阻塞等待队列
struct fasync_struct* fa; // 异步描述
} dev;

// 实现fops->gpiokey_fasync接口,支持用户层的FASYNC标记
// 将用户进程文件描述添加到异步队列中
static int gpiokey_fasync(int fd, struct file *filp, int mode)
{
return fasync_helper(fd, filp, mode, &dev.fa);
}

// 用户进程关闭设备时,务必将其从异步队列中剥离
static int gpiokey_close(struct inode *node, struct file *filp)
{
gpiokey_fasync(-1, filp, 0);
return 0;
}

// 当按键中断触发后,将信号发送至用户进程
static irqreturn_t on_key_pressed(int irq, void* dev_id)
{
mod_timer(&dev.delay, jiffies + (HZ/20));
return IRQ_HANDLED;
}
static void on_delay50(struct timer_list* timer)
{
if (gpio_get_value(KEY_GPIO)) {
wake_up_interruptible(&dev.r_wait); // 唤醒阻塞队列
kill_fasync(&dev.fa, SIGIO, POLL_IN); // 发送SIGIO异步信号
}
}

struct file_operations fops = {
...
.fasync = gpiokey_fasync,
.release = gpiokey_close,
};

从输出结果中可以看到,程序启动并执行后续,完全没有监听设备,当按键被按下时,信号传回进程并触发了on_keypress()函数。

1
2
3
4
5
philon@rpi:~/modules $ sudo insmod gpiokey.ko
philon@rpi:~/modules $ ./signal_test
I'm doing something ...
key pressed!!
key pressed!!

异步IO

自Linux2.6以后,IO的异步访问又多了一种新方式——aio,此方式在实际开发中并不多见,尤其是嵌入式领域!因此本文不打算深入讨论,这里作为知识扩展仅做个简单介绍。

异步IO的核心思想就是——回调,例如aio_read(struct aiocb *cb)aio_write(truct aiocb *cb),程序调用该函数后不会阻塞,当文件读写就绪后,会自动根据cb描述进行回调。

此外,AIO有应用层基于线程的glibc实现,以及内核层的fops接口实现,甚至还有类型libuv、libevent这样的事件驱动的第三方框架可供使用。

就我个人而言,技术是把双刃剑,回调是一种看似美妙的骚操作,但如果你编写的业务具有强逻辑性,那回调在时序上的失控,以及返回状态的多样化,会随着代码的壮大而进入回调陷阱,深深地无法自拔。给我这种感受的并非C语言,而是JavaScript。

总之,没有最优秀的技术,只有最适用的场景!我的原则是:用回调,远离嵌套回调。

小结

  • Linux内核模块应当“提供机制,而非策略”
  • 阻塞IO是在用户层读写访问时,是进程睡眠,由驱动来唤醒
  • 非阻塞IO是有IO_NONBLOCK标记时,当资源不可访问时,直接返回-EAGAIN
  • 多路复用IO是通过select/epoll进行多个设备监听,驱动须实现对应的fops->poll接口
  • 异步IO即信号,由设备驱动作为信号源,主动向进程发送通知
  • 不同的IO同步/异步访问机制无优劣之分,而是取决于具体的应用场景
  • 务必搞懂等待队列,它贯穿以上几种IO访问机制

Learn to Say, “Hello, World”

PAUL LEE,用户名leep,更多时候被称为Hoppy,享有当地编程问题专家的声誉。我需要帮助了,就走到Hoppy的桌子前,问他是否有空帮我看一眼代码。

“当然”,Hoppy说,“搬个凳子过来”。
我小心翼翼地(搬),不至于弄到他身后那堆成金字塔的空可乐瓶。
“什么代码”?
“文件里的一个函数”,我说。
“好,让我们看一下这个函数”。Hoppy把手中的K&R副本放到一边,然后在我前面噼里啪啦滑响键盘。
“IDE在哪?”显然,Hoppy不用IDE,只是某种我不会操作的编辑器。他抓起键盘,一番快捷键后,我们打开了一个文件——很大的文件——然后我们找到了那个函数——很大的函数。他翻页到我想要问的条件判断代码块那里。
“如果x为假,这个分支实际上会做什么?”我问,“它肯定出错了”。

我整个早上都在尝试找到一种方法让x强制为假,但它在一个大工程下的大文件下的大函数里,重新编译运行的循环试验让我失望。难道像Hoppy这样的专家也不能告诉我答案吗?

Hoppy承认他自己也不确定原因。但令我震惊的是,他没有立刻拿回那本K&R。而是把那个代码块复制到新的编辑器里,重新缩进它,封装到一个函数里。很快,他写好一个死循环的main函数,提示用户输入值,传到那个函数里,打印结果。他把这段代码保存为tryit.c。所有这些步骤我自己都会做,只是或许没那么快罢了。但他的下一步操作很棒,对我而言也很陌生:

$ cc tryit.c && ./a.out

看!他实际的编程操作,几分钟前还在构思,现在已经能跑了。我们尝试了集中值并验证了我的想法(有些问题上我是对的!)然后他又交叉检查了K&R里相关的章节。我感谢了Hoppy并离开,再次小心翼翼不要碰到他的可乐金字塔。

回到我的座位,我关掉了IDE。我已经习惯了在用一个大型工程去做一个大型产品,我一开始便认为这是我应该做的。然而通用计算机也可以完成很小的任务呀。我打开了一个文本编辑器,并敲下:

1
2
3
4
5
6
#include <stdio.h>
int main()
{
printf(“Hello, World\n”);
return 0;
}

回想一下,你每次去看病或做体检时要抽多少血?我映像中至少3管吧。这么多血液会送到具备职业资格的实验人员手中,他们立刻舞动起各式各样的大型仪器,捣鼓着五颜六色的瓶瓶罐罐,仿佛准备用你的DNA制造一个科学怪人。然而事实上,我们的血液样本被送到实验室后,会展开几十上百种的血液分析,最终生成不同的指标数据,用于评判你的身体是否存在某些疾病风险,又或者为医生的诊断提供准确依据。我们小老百姓都知道,这个过程叫“血常规检查”。

去医院做血常规其实是很烦人的,排队、挂号、就诊、抽血、等待、再排队、医生诊断……除了浪费时间外,还要被护士姐姐抽取整整三大管血。如果突然有一个人跳出来和你说,她有一款革命性的产品,能彻底改变当今医学界,妈妈再也不用担心我生病了,你会不会觉得这家伙是个骗子?没错,她就是个骗子,甚至骗倒了很多硅谷精英,斩获了10多亿美元的投资。

先来看看这款革命性产品的初衷吧:指尖轻轻一刺,只取一滴血,就能完成数百种血液分析,数据可通过云端提供给专业医生,随时随地为你打造最精准的医疗策略。而它的外表犹如你家里微波炉,是的,这个神奇小盒子装下了一整个实验室和数百名实验人员。以至于它的创始人都管它叫——迷你实验室。想想吧,当癌症、心脏病、糖尿病以及其他各种血液相关的疾病刚刚出现苗头,便可及时发现,并把它们扼杀在摇篮里。我们再也不会为亲人的提前离世而悲痛不已。还在等什么,立刻订购,99包邮哦,亲~

等一下,如果你还是不愿意购买这款“迷你实验室”,还可以选择到最先进的“健康中心”体检,那里配备了数十台类似的共享血检仪。还在等什么,立刻下载APP,扫码验血吧!

《坏血》完整地叙述了这个伟大创意从诞生到没落的过程。

全明星阵容,闪亮登场

一位年轻女性伊丽莎白·霍姆斯,她创立了一家名为“希拉洛斯”的医疗技术公司,带着前边所说的伟大愿景,以乔布斯为偶像,向苹果公司致敬,她从一开始便准备在医学界掀起一场腥风血雨。

这样的产品,这样的商业模式,毋庸置疑,它是颠覆性的,也正是如此,希拉洛斯公司一开始便吸引了许多超一流的人才和明星投资人。其中不乏像甲骨文创始人拉里·埃里森,美国前国务卿乔治·舒尔茨,亨利·基辛格,苹果高管,以及各种名牌大学教授争相加入。

市场呢?连锁药店沃尔格林、连锁超市西夫韦,甚至还有五角大楼准备军用。缺钱是吧,没关系,甲方爸爸给!一句话,赶紧做,产品出来不愁卖!

知名度?希拉洛斯后期一度估值达到90亿美元,被封为独角兽。各种《财富》、《福布斯》、《时代》的主流媒体争相报道,伊丽莎白更是被提名世界最有影响力的100人之一。而“年轻创业女性”的人物背景,更是给这家公司镀上一层励志的光环,更容易受人青睐。

这样的资源配置,有几家初创公司敢想。但,一把好牌为何到最后打烂了?显然,“管理”的锅是甩不掉的。

混蛋领导,任性管理

伊丽莎白是个非常强势的女性,对于领导者来说甚至不算是件坏事,但极强的权力欲、不允许有反对声、不懂细节的盲目自信、乐于勾心斗角,这些性格问题却在她的日常管理中随处可见。

这么说吧,研发人员认为某个技术还无法商用,但她的态度就是:“这个很简单嘛,给你两周时间做出来”、“什么,还敢讨价还价,开了!”、“哟,敢在背后议论我,立刻滚”、“我们正在改变世界,如果你有任何质疑,现在就离开”。

更可悲的是,伊丽莎白还有个比她大20多岁的男朋友——桑尼,他是这家公司的二老板。如果说在伊丽莎白的领导下,员工尚且有一丝喘息空间,那这家伙简直是要把你勒死。他脾气极度火爆,会用各种办法来威胁员工,甚至侮辱他们。确切地说,他要的根本不是人才,而是绝对忠诚和绝对服从的奴才。

从宏观上看,希拉洛斯对待员工就是准军事化管理,任何人不得交头接耳,切断一切可能的私下沟通方式,随时随地监听员工的邮箱以及各种电子痕迹。他们开除人像流水线作业一样犀利,并且喜欢模仿电影情节,让保安把这些倒霉蛋“请”出去。员工离职后,公司会威逼利诱让他们关系要好的同事抖出一些猛料,以此为由逼迫离职人员放弃自己的股权及其他合法权益。

此外,在公司运作方面也比较奇葩,裙带关系就不消多说,二老板都可以是男朋友,再招一些亲朋好友又算什么。给病人化验的试剂过期了——没关系,反正又没人知道;实验操作不规范——什么叫规范?你有专业实验的资格证吗——哦,我刚毕业两周。医疗资质需要国家机构监管——不怕,我们想办法绕开!

是的,这就是管理层的工作重心,产品反而是次要的。

只要落实好PPT,其他都能糊弄

如此管理混乱不堪的公司居然能经营10来年,自然是有绝招的。然而这种绝招在国内其实并不少见——吹!

伊丽莎白这个人其实并非那么不堪,抛开领导力上的各种性格缺陷,她本身是一个很勤奋也有大抱负的人。但她最具杀伤力的地方在于“现实扭曲力”,这个词之前是用来形容乔布斯的。简单来说,只要你听这种人演讲,就能短暂地忘掉现实情况,和他的愿景产生同频共振,他的言语会有一种极强的感染力,让人信服,并愿意为此付诸行动。是不是很想宗教?没错,伊丽莎白会用她装出来的低沉嗓音和成熟稳重的气质,让投资人认为她和其他人不一样,再加上听起来非常靠谱的产品和公司愿景,听她的演讲,就是会让人相信——她做得到!

此外,希拉洛斯公司在门面上下足了功夫,总能给人一种高大上的第一映像,再加上铺满墙面的各种励志标语、媒体报道,聚集于此不论老板、员工、投资人、客户,他们都会逐渐转变为同一种身份——教徒。

诚然,我这么形容有些夸张了,但事实上是,在产品和服务方面,希拉洛斯却是没做好,公司真正能拿出来的就两款“产品”:其一名为“爱迪生”,其实就是里面装个机械臂,自动完成几项简单的血检过程;其二名为“健康中心”,其实就是采购了很多商业血检仪器,并装修得很漂亮的实验室,为人们提供检测服务。

然而不论在产品的功能和性能上,两款产品表现都非常差劲。自家的“爱迪生”总是故障频发,要么程序员不熟悉实验流程,无法开发专业程序,要么血液样本压根不够完成实验。“健康中心”买的是西门子的产品,然后狸猫换太子队外宣称是自家的,但所谓的指尖一滴血,完成240多项血常规检查,终归是梦想。一滴血液样本被用于前几项分析后就被污染了,根本无法继续化验,健康中心的部分检查也确实用到了“爱迪生”,不过也就10多项,而且实验结构基本是错的。同一指标,两次化验得到的数据可以悬殊30%以上。

当然,公司的高管会想办法逼迫他们的员工对数据造假,让它看起来合理,然后在确认表上签字。总之,只要产品还能“工作”,不影响他们的商业运作,其他都不重要。

公司其实一直在研发一款重磅产品——迷你实验室。不过在两位老板任性地管理之下,公司人员流动速度之快,当然是胎死腹中啦。

底线?不存在的

“爱迪生”和“健康中心”最终是服务病人的,带产品前期并没有大面积铺开。而且正如前面所说,希拉洛斯的首要目的是ABC轮融资上市,为了PPT,他们需要好看的数据。可产品和服务实际上都很弱鸡,为此,公司开始逼迫实验人员开始对病人的数据造假,对整个实验室内部严格保密,任何想进入实验室参观的人,都以商业机密为由予以拒绝。

很多员工因为无法回避良知而选择离职,但公司会严格把控员工的通信,以防止泄密,在撂下狠话,只要发现谁泄密,就用法律武器,告到它倾家荡产。其中一位叫伊恩的员工,曾在希拉洛斯与其他公司的专利诉讼斗争中,因为受不了公司给他施加的压力而自杀,对此,希拉洛斯却选择尽可能隐瞒在职人员,装作什么都没发生。

总之,在希拉洛斯,对员工采取各种卑鄙的手段似乎没什么不妥,一切看领导心情。后来还是有人因受不了良心的谴责,想华尔街日报记者揭发他们,这位记者就是本书作者。

在希拉洛斯如此严苛的通讯监管制度下,泄密的事情马上被发现,公司并没有正面处理,而是雇佣私家侦探24小时监视员工,甚至以类似黑社会的手段去恐吓员工,这个时候的希拉洛斯,已经谈不上什么道德了。

总之,一面是商业上各种忽悠,一面是业务上各种造假,一面是内部各种霸凌。好在本书作者和公司员工都顶住了压力,才让这次爆料得意公之于众。

然而,希拉洛斯已经服务了很多病人,他们因荒谬的数据早已吃上了错误的药品,谁能为此负责?没有,甚至连一句道歉都没有。公司依然想着压住事实、压住舆论,公司形象最重要…

这样的“独角兽”最终玩火自焚,宣布破产,两位老板自然也被绳之以法。

一些感想

其实,在读本书的过程中,尤其是前半部分(主要叙述创业经历),我觉得和国内的情况很类似。有很多初创公司的套路都差不多,一流的PPT,二流的管理,三流的产品。但这不重要,产品、颠覆、平台、革命、重新定义…算了吧,什么是商业模式?忽悠+融资+上市+套现+跑路这个过程本身才是商业模式!

但这么说就对不起那些认真做产品的初创公司了,脚踏实地者大有人在。我想说的是,大跃进式的创新很精彩,但不是每个人都是乔布斯;摩尔定律固然热血沸腾,但不是每个行业都奏效。苹果2018年发布iPad Pro的时候宣称它比第一代快了1000倍,那么电池的进步呢?这么多年了,除了增加容量,优化省电技术外,电池本身的突破几乎为零吧。因为电池属于化学材料领域,而非IT。

如果医学界也和IT一样的发展速度,我估计今天的人类早就超神了。因此,像血检这种看似伟大的梦想,实则被计算机领域给带偏了。我们今天的医疗技术是提高了很多,但这其中的“技术”更多是仪器设备的功劳——它们属于计算机领域。而那些无法避免的医学项目和理论支撑,其实没我们想象那么发达。正如今天我发感冒烧用被注射的针水,效果会比20年前好100倍?我估计其中有很多连名字都没变过吧。

希拉洛斯的失败原因之一,很显然是他们把技术问题和医学问题混淆了。但这也给我们创业一个警醒——不要老想着颠覆一切。你的想法再伟大,人还是要吃饭的,白里透红的黑线是画不出来的。我们总能看到各种有关黑科技的新闻,令人神往,但就目前来看,什么把人的寿命延长到200岁,彻底击败癌症,超级基因改造人之类的想法,短期内真的不现实——这个短期是指我活到老死那天。

最后,关于希拉洛斯的失败,更多是从道德上的谴责,这无可厚非。我在读到本书后半部分的时候,隐隐觉得这更像是这位记者对自己战果的一次骄傲的回顾,就像一位战士在对围坐身旁的孩子诉说着他的英勇事迹,或是一个酒鬼嘬了一口美酒后还忍不住吧唧两下嘴。但必须肯定,他做了一件好事,为人类避免了一场灾难。有关希拉洛斯公司的“品行”,我觉得没什么好说的,无节操的公司以前就有,以后也不会消失,都是老生常谈了,于我而言只有两位前辈的训诫:

做事,要有谱气!
作为人,何为正确?

本文源码:https://github.com/Philon/rpi-drivers/tree/master/02-gpio_key

在上一篇中主要学习了GPIO原理及Linux字符设备,其过程大致是这样子的:

1
电路图“看引脚” --> 手册“看物理地址” --> 寄存器手册“看GPIO逻辑控制” --> 复用功能 --> 地址操作

可以看到,整个过程是非常繁琐的,驱动程序必须精确到处理器的每一根引脚的状态,如果所有驱动都这么写估计当场就跪了。因此,本文除了学习GPIO中断原理之外,更重要是掌握以下知识:

  • 混杂设备机制
  • Linux内核GPIO接口
  • ARM中断基础
  • Linux内核中断接口

ARM中断基础

中断,就是由外部电路产生的一个电信号,强制CPU从当前执行代码区转移到中断处理函数。ARM架构的CPU中断硬件原理和这个差不多,注意这里说的是ARM的CPU,仅仅代表处理器当中的一个核,不要把CPU和SoC划等号。

CPU能提供的中断资源是非常有限的,一般也就一两个“引脚”,但我们在看芯片手册的时候就会发现,几乎每个GPIO都具备中断功能,那可是几十上百个中断啊!这归功于内部继承的PIC——可编程中断控制器,它负责监听所有GPIO的中断信号,并在外设给出中断信号时真正去触发CPU中断,并告诉CPU是谁触发的。这种中断信号源被抽象为——中断号。

在现代多核处理器架构下,ARM用的是GIC(通用中断控制器),它能支持SGI(软件生成中断)、PPI(单核私有外设中断)、SPI(多核共享外设中断)。默认情况下,ARM处理器的外设中断总是先给到CPU0,如果其忙不过来才往后传递。

那么CPU收到中断信号后又如何处理呢?ARM共有7种工作模式,常规情况下会运行于用户模式(用户代码区),一旦中断触发,会立刻切换至中断模式(响应函数),中断模式分为IRQ(中断)和FIQ(快速中断),它们二者的区别是,FIQ可以进一步中断IRQ。

由于是实战操作,过于理论的东西就不往上放了,如果要进一步了解ARM中断,可以参考这篇文章👉:https://my.oschina.net/u/914989/blog/121585

这里只需要掌握两个重要概念:

  1. 中断号本身可以看作一种独立的CPU资源,通过中断控制器监听真实的物理资源(引脚)状态
  2. CPU的外设中断会直接触发PC跳转到指定代码区

Linux/IRQ基础

正如前文所说,中断是让CPU切换执行上下文,尽管Linux操作系统通过时间切片的方式实现多任务,但IRQ切换是硬件层级的,进入中断函数就意味着什么进程、调度、并发等软件概念将全部失效。举例来说,一个进程调用sleep只会让自身运行停止并让出CPU资源,但在内核中断函数当中sleep,那就真睡过去了——整个操作系统的调度机制都会崩溃掉。

所以,Linux将中断处理分为“顶半部”和“底半部”,可以简单粗暴地理解:

  • 顶半部,硬件级响应,处理内容必须快准狠,尽快将CPU资源交还操作系统
  • 底半部,交由系统任务队列调度,处理耗时的响应业务

打个比方,顶半部好比医院挂号,底半部好比排队就诊的过程。但不要死脑筋,如果响应业务本身并不耗时,就没必要再拆分为两个处理部分了,比如出院缴费,直接在顶半部搞定。

带着这个原则,看一下Linux中断编程接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <linux/interrupt.h> 

// 根据GPIO引脚号获取对应中断号
int gpio_to_irq(unsigned gpio);

// 申请占用中断号,并绑定处理函数
// - irq 中断号
// - handler 顶半部中断处理函数
// - flags 中断触发方式
// - name 中断名称
// - dev 中断参数传递
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);

// 释放中断号
void *free_irq(unsigned int irq, void *dev);

下面来实际操作一把——Linux中断的顶半部处理实现。

最简单的GPIO中断

先来看个接线图,为了更好地展示中断,“继承”了上一篇文章的三色LED接线,预期要实现是“每按一次键改变一种颜色”。按键Key的两个脚接到了树莓派的GPIO173V3上,换句话说,就是用GPIO17接收上升沿中断信号。而LED的控制电路保持之前的不变。

接线图
电路图

实现GPIO上升沿中断大体分为4步:

  1. 设置GPIO复用功能为输入模式 gpio_request()
  2. 获取GPIO对应中断号 gpio_to_irq()
  3. 申请中断号、中断类型、绑定处理函数 request_irq()
  4. 释放中断(卸载驱动时) free_irq()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <linux/module.h>
#include <linux/gpio.h> // 各种gpio的数据结构及函数
#include <linux/interrupt.h> // 内核中断相关接口

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Philon | https://ixx.life");

// 稍后由内核分配的按键中断号
static unsigned int key_irq = 0;

// 定义按键的GPIO引脚功能
static const struct gpio key = {
.gpio = 17, // 引脚号为BCM - 17
.flags = GPIOF_IN, // 功能复用为输入
.label = "Key0" // 标示为Key0
};

// 按键中断“顶半部”处理函数
static irqreturn_t on_key_press(int irq, void* dev)
{
printk(KERN_INFO "key pressed\n");
return IRQ_HANDLED;
}

static int __init gpiokey_init(void)
{
int rc = 0;

// 向内核申请GPIO
if ((rc = gpio_request_one(key.gpio, key.flags, key.label)) < 0) {
printk(KERN_ERR "ERROR%d: cannot request gpio\n", rc);
return rc;
}

// 获取中断号
key_irq = gpio_to_irq(key.gpio);
if (key_irq < 0) {
printk(KERN_ERR "ERROR%d:cannot get irq num\n", key_irq);
return key_irq;
}

// 申请上升沿触发中断
if (request_irq(key_irq, on_key_press, IRQF_TRIGGER_RISING, "onKeyPress", NULL) < 0) {
printk(KERN_ERR "cannot request irq\n");
return -EFAULT;
}

return 0;
}
module_init(gpiokey_init);

static void __exit gpiokey_exit(void)
{
// 释放中断号及GPIO
free_irq(key_irq, NULL);
gpio_free(key.gpio);
}
module_exit(gpiokey_exit);

上述代码非常简单,就是在按下按键的时候,打印一条消息。可以通过dmesg命令查看内核打印消息:

1
2
3
4
5
6
7
8
philon@rpi:~/modules $ sudo insmod gpiokey.ko 
philon@rpi:~/modules $ dmesg
...
[ 77.238326] gpiokey: no symbol version for module_layout
[ 77.238345] gpiokey: loading out-of-tree module taints kernel.
[ 79.310635] key pressed
[ 79.463206] key pressed
[ 79.463262] key pressed # 我摸着右边的鼻孔对天发誓,我只按了一下!

正如文章最开始所说,Linux对各种资源的调用是有相关API的,要尽量使用内核接口编写驱动程序,一能保证底层代码的质量,二能提高代码的移植性。关于GPIO资源的调用,要熟悉以下接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <linux/gpio.h>

struct gpio {
unsigned gpio; // GPIO编号
unsigned long flags; // GPIO复用功能配置
const char *label; // GPIO标签名
};

// 单个GPIO资源申请/释放
int gpio_request_one(unsigned gpio, unsigned long flags, const char *label);
void gpio_free(unsigned gpio);

// 多个GPIO资源申请/释放
int gpio_request_array(const struct gpio *array, size_t num);
void gpio_free_array(const struct gpio *array, size_t num);

// GPIO状态读写
int gpio_get_value(unsigned gpio);
void gpio_set_value(unsigned gpio, int value);

按键防抖,中断的底半部与定时器接口

由于前边的代码没有做防抖,明明只按了一下按键,中断函数却被连续触发了3次。话说在单片机里实现按键防抖是非常简单的,无非就是睡个50毫秒,再确认是否真的按下即可。但是前文也明确说了,Linux是多任务系统,永远不要试图在中断函数里睡眠。因此,防抖只能放在Linux中断的底半部。

此外,慎用睡眠函数!除非你很清楚它不是忙等待。在多任务系统下,按键防抖的逻辑应该是——触发中断后,让出CPU资源50毫秒,然后再确认是否真的按下。

先来认识一下底半部机制,Linux内核提供的底半部机制主要有软中断tasklet工作队列线程IRQ

  • 软中断,是有内核软件模拟的一种中断机制,注意不要和ARM指令触发的中断混淆,后者本质上是硬中断
  • tasklet,基于软中断实现的中断调度机制,本质上还是中断,不允许在处理函数中sleep
  • 工作队列,类似于tasklet,区别在于工作队列底层基于线程,可以在处理函数中sleep
  • 线程IRQ,不用解释了,就是个线程

有关Linux底半部的知识不适合放在这里,建议参考此文:http://chinaunix.net/uid-20768928-id-5077401.html

这里了解底半部机制的目的,仅仅是为了挑选一种何时的响应方式,首先可以明确,软中断和tasklet不能睡,pass。线程维护麻烦,pass。就只剩工作队列了。尽管工作队列可以睡,但内核提供的usleep/msleep等接口本质上是忙等待,依旧占用CPU资源,pass。怎么办呢——工作队列+定时器。当中断来临后:

  1. 顶半部迅速定义个工作队列,交由内核调度
  2. 当工作队列被调度时,迅速定义个定时器——延时50ms
  3. 当定时器到时中断,才真的去做防抖判断

工作队列API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <linux/workqueue.h>

// 工作队列原型
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};

// 工作队列回调函数原型
typedef void (*work_func_t)(struct work_struct *work);

// 初始化一个工作队列,绑定回调
INIT_WORK(work, func);
// 启动队列,之后会由内核完成调度
schedule_work(&my_wq);

定时器API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <linux/timer.h>

// 全局变量
// 记录上电后定时器中断次数,也就是开机时长,但不是微秒或纳秒的概念
extern unsigned long volatile jiffies;
// 表示CPU一秒钟有多少个定时器中断
#define HZ 100

// 简单来说,如果要定义一个100ms的延时,相当于以下公式:
// jiffies + (HZ/10)
// 相当于以现在的jiffies做偏移,而1s的十分之一就是100ms

// 定时器原型
struct timer_list {
struct hlist_node entry;
unsigned long expires;
void (*function)(struct timer_list *);
u32 flags;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};

// 向内核注册一个定时器
#define timer_setup(timer, callback, flags)
void add_timer(struct timer_list *timer);

// 向内核删除一个定时器
int del_timer(struct timer_list *timer);

// 修改定时器的下次的jiffies
int mod_timer(struct timer_list *timer, unsigned long expires)

下面是本文的完整代码,按一次按键,切换一次彩色led的颜色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h> // 混杂设备相关结构
#include <linux/gpio.h> // 各种gpio的数据结构及函数
#include <linux/interrupt.h> // 内核中断相关接口
#include <linux/workqueue.h>
#include <linux/timer.h>

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Philon | https://ixx.life");

// 定义按键的GPIO引脚
static const struct gpio key = {
.gpio = 17, // 引脚号为BCM - 17
.flags = GPIOF_IN, // 功能复用为输入
.label = "Key0" // 标示为Key0
};

// 定义三色LED的GPIO引脚
static const struct gpio leds[] = {
{ 2, GPIOF_OUT_INIT_HIGH, "LED_RED" },
{ 3, GPIOF_OUT_INIT_HIGH, "LED_GREEN" },
{ 4, GPIOF_OUT_INIT_HIGH, "LED_BLUE" },
};

static unsigned int keyirq = 0; // GPIO按键中断号
static struct work_struct keywork; // 按键工作队列
static struct timer_list timer; // 定时器作为中断延时

// 按键中断“顶半部”处理函数,启用工作队列
static irqreturn_t on_key_press(int irq, void* dev)
{
schedule_work(&keywork);
return IRQ_HANDLED;
}

// 按键中断“底半部”工作队列,启动一个50ms的延时定时器
void start_timer(struct work_struct *work)
{
mod_timer(&timer, jiffies + (HZ/20));
}

// 按键防抖定时器,及处理函数
void on_delay_50ms(struct timer_list *timer)
{
static int i = 0;
if (gpio_get_value(key.gpio)) {
gpio_set_value(leds[i].gpio, 0);
i = ++i == 3 ? 0 : i;
gpio_set_value(leds[i].gpio, 1);
}
}

static int __init gpiokey_init(void)
{
int rc = 0;

// 向内核申请GPIO
if ((rc = gpio_request_one(key.gpio, key.flags, key.label)) < 0
|| (rc = gpio_request_array(leds, 3)) < 0) {
printk(KERN_ERR "ERROR%d: cannot request gpio\n", rc);
return rc;
}

// 获取中断号
keyirq = gpio_to_irq(key.gpio);
if (keyirq < 0) {
printk(KERN_ERR "can not get irq num.\n");
return -EFAULT;
}

// 申请上升沿触发
if (request_irq(keyirq, on_key_press, IRQF_TRIGGER_RISING, "onKeyPress", NULL) < 0) {
printk(KERN_ERR "can not request irq\n");
return -EFAULT;
}

// 初始化按键中断底半部(工作队列)
INIT_WORK(&keywork, start_timer);

// 初始化定时器
timer_setup(&timer, on_delay_50ms, 0);
add_timer(&timer);

return 0;
}
module_init(gpiokey_init);

static void __exit gpiokey_exit(void)
{
free_irq(keyirq, NULL);
gpio_free_array(leds, 3);
gpio_free(key.gpio);
del_timer(&timer);
}
module_exit(gpiokey_exit);

小结

  • ARM有7种工作模式,其中IRQ和FIQ为中断模式,会导致CPU跳转到指定代码区
  • Linux/IRQ分为顶半部和底半部机制
  • 顶半部处理要快且不是睡眠
  • 底半部又分为4种机制,软中断、tasklet、工作队列、线程IRQ
  • 我们可以通过gpio_xxx函数访问CPU资源,而无需地操作底层寄存器
  • 如果有延时需求,最好采用内核提供的定时器接口

Learn to Estimate

作为一个程序员,你需要向你的主管、同事、用户提供你完成这些任务所需要的估算,因为他们需要清楚地知道实现他们的目标所需的时间、成本、技术及其他资源。

为了更好地估算,学习一些估算方法显然很重要。然而首先呢,应该学习什么是估算以及它该如何使用,这些最为基础——虽然看似奇怪,但很多程序员和主管对此都搞求不懂。

下面一段程序员和项目经理间的交流可谓典型:
项目经理:完成xyz功能开发,你估计要多长时间?
程序员:一个月。
项目经理:太久了!我们只有一个礼拜。
程序员经:那至少也得三个星期。
项目经理:我最多给你两周。
程序员:成交!

最终,程序员给出了一个领导可以接受的“估算”。但因为是程序员的预估,领导就会让程序员为此负责。为了理解这次谈话的问题,我们需要定义三件事——估算、目标、承诺:

  • 估算是对值、数字、数量以及其他事物的大概计算或判断。这就意味着估算是基于硬指标结合先前的经验对事实的度量——在计算它时,希望和愿望肯定会被忽略掉。也就是说,只是大概,估算不可能做到严谨,例如一次开发任务周期不可能估算到234.14天。
  • 目标是对理想业务的客观描述,比如:“这个系统须至少支撑400个用户同时访问。”
  • 承诺是对特定的功能在确定的日期或事件下,许诺达到特定的质量水准。例如:“搜索功能将在下次产品发布时可用。”

估算、目标、承诺彼此独立,但目标和承诺应当基于合理的估算。正如Steve McConnell所说:“软件估算的首要目标不是预测项目的结果;而是明确项目目标是否切实可行,足够受控于整个项目,直到看见它们的那天。”因此,估算的主旨是为了做一份尽量靠谱的项目管理计划,让项目团队能基于切实可行的目标作出承诺。

所以,上边的谈话,经理其实是让程序员为他脑子里并不清晰的目标作出承诺,而非估算。下次你在管别人要估算时,要确保每个人都理解谈论的内容,你的项目才可能有很好的成功机会。现在到你你学技术的时间了…

程序员需要多沟通

程序员的生活中,几乎大部分的沟通与计算机相关——更确切地说,与运行在上面的程序相关。这种沟通是一种能让机器读懂的表达方式。这仍是一次令人振奋的展望:程序,从想法到实现,几乎不涉及任何物理实质。

程序员需要熟练运用机器语言,不论现实的还是虚拟的,并通过开发工具完成该语言相关的抽象。因此,学习很多不同的抽象是非常重要的,此外,有些想法也极难表达。优秀的程序员能够站在生活之外的角度,用其他语言表达他的目的。

除了与机器沟通,程序员还需要与同行沟通。今天大型项目更多是团队协作,而非简单的编程艺术。理解和表达变得比机器的抽象可读性要重要得多。我认识的很多大牛除了说一口流利的母语,还会其他语言。这可不仅仅是为了和其他人沟通:说好一种语言会让思路清晰,这是在抽象某个问题时不可或缺的。对于编程领域而言也一样。

除了与机器、自己和同行沟通外,一个项目有非常多的利益共同体,大量的非技术背景人员参与其中。他们在测试、质保、部署。对于市场和销售,他们是办公室(或商场、学校)的终端用户。你需要理解他们及其痛点。但如果你不会他们的语言——他们世界/领域内的语言,这就几乎不可能了。尽管你自认为与他们洽谈愉悦,但他们恐怕不会这么想。

如果你与会计交谈,你需要基本的成本中心、捆绑成本、资本等方面的知识。如果你和市场、法务交流,你应该熟悉他们的行话和语言(本质上,是他们的思维模式)。项目中所有这些领域属于都需要由某个人掌握——理论上,当然是程序员啦。通过计算机,程序员要最终把想法变为现实。

当然,生活不仅仅是软件工程。正如Charlemagne所说:“拥有另一种语言就是拥有另一个灵魂”。为了你在软件行业的迭代,你会感激自己懂外语。知道何时听胜于说,懂得很多语言的词穷。