普通视图

发现新文章,点击刷新页面。
昨天以前政子的博客

2021 这一年

作者 Jason Bian
2022年1月9日 18:30

intro

在抬笔之前,纠结许久,过去的这一年,真的有这么多要记录的东西么,有必要花几个小时的时间去写么?我不知道。但同时,我的鼠标已点开了之前的博客,开始翻看前些年写的文字。看着看着,我找到了许多自己已经不怎么记得,但想起却感到十分温暖的那些瞬间和回忆。虽说都不是什么大事儿,但是如果不记录,它们可能真的就会遗失在记忆的荒野里了。

所以,我还是要写些文字,来记录我这平凡,也小有起伏的一年时间。

目标回顾

首先,回忆一下去年立的那些flag吧。

  • 分享至少 5 篇较为优质的内容
    • 目标完全没有达成,一是由于惰性,二是感觉自己积累不足
  • 对领域内技术的认知上有明显提升,具有一定的专业性和深度
    • 专业知识确有所增加,但离自己的期望还有差距,算是勉强达成
  • 维持学习状态,保持对世界的好奇心,对世界的认知上更进一步
    • 确有长进,但也不多,算是没有原地踏步吧
  • 阅读,至少 5 本较篇幅较长的著作,减少看视频的时间
    • 大篇幅的只有一本花了半年才读完的《红楼梦》;看视频时间有所减少,原因是从看变为了听
  • 锻炼身体,体重维持在 80kg 以下
    • 体重目标超额达成,但原因却不是锻炼身体
  • 多陪陪家人,每周和家人通话
    • 目标也算是基本做到

整体看来,六点Flag,勉强算下来,可以说是完成了一半吧,约等于不及格。

原因肯定是多方面的,不能单独归咎于时间不够。因为我知道就算是时间充裕,也很有可能无法完成上述任务。所以,今年我决定使用一个更加精确的目标系统——OKR对自己的目标进行量化确定/追踪,并且应当可以按照执行情况修改。希望通过新方法,可以让明年的自己及格吧。具体规划放在文末。

生活

这一年的生活给我最大的感悟就是“无常”。疫情无常,生活无常,生死也无常。

下面,就以几个生活中的小片段来简要概括这一年吧。

其一

年初的第一件大事是乐乐的婚礼。在他的婚礼上,我们这三个从小几乎天天在一起的铁哥们,终于能从天南地北百忙之中,在疫情笼罩下,时隔多年之,后再次聚到了一起。我呢,也终于第一次当了伴郎,第一次正经穿上了西装,也第一次从“台上人”视角见证了好朋友的婚礼。疫情之下,虽然没有大操大办,但几天的相处,让我深刻感受到了乐乐和嫂子的深厚感情。所以这里不说别的,祝福乐乐早得贵子,也祝福剑客早日结婚(相见不易,总得有个由头呀)。

刚布置好的婚房

此外,在见面后的聊天中,突然发现童年的好友都已成熟起来:聊天中少了许多幼年时对游戏和故事的痴迷、少年时对宇宙和未来的畅想,却添了对生活的感悟与吐槽,以及对肩上责任的担当。肩膀扛起的东西变重了,脚下也自然就会踏实起来,这或许才是人们仰望天空的底气之所在。

在家后山上看云

其二

虽说现代通讯非常方便,但我们几个也是各自有忙,所以自此一别,再次认真联系,大约是就年底了,原因是他们关心我的心理状况:在奶奶去世后的第三年,从小陪伴到大的另一位亲人——爷爷,也永远离我而去了。

坏事总是十分突然。当时兰州的疫情刚刚有所好转,各种交通刚刚解封,就得到了爷爷病情突然恶化的消息。也顾不得多想,做完核酸第二天的凌晨,就和姑父驱车从北京赶回家中。

一路上

幸运的是,我的决策是对的,在赶回家的那天晚上,见到了爷爷意识完好情况下的最后一面。他在这种情况下,还是像往日那样,一见面就担心我工作忙,穿的少,吃不好。在得知我一切都好后,他也逐渐安心下来。第二天,他的意识就再也没有清醒起来,生命也随之进入了倒计时。最终,在2021年11月23,他还是永远地离开了我们。那天凌晨六点医院门口的瑟瑟寒风,我一定毕生难忘。

爷爷一生很不容易。他生于解放前,小时虽然家里穷,但成绩优异,所以家里一路供他读到了初中毕业。后来,因为自然灾害无力负担,就征兵入伍,去了二炮,做了一个挂着空军名号的步兵。随着国家需要,他们一路辗转,从老家来到北京,又从北京到了茫茫戈壁。此后,他们部队在戈壁上克服了种种困难,为我们国家的国防做出了重要贡献。后来,他转业来到了当时为了“备战”和“备荒”而在西北大山深处筹备建立的884。他们再次发扬那个时代艰苦奋斗的精神,遇山开山,遇水架桥,硬是在只长荒草的群山深处,建起了一座现代的大型铜加工厂,为国家生产了许多战略物资。在他退休后,虽然终于不用为工作日夜操劳,却又开始为了我的成长费心费力,这一晃又是二十多年,直到现在。

而就在我刚力所能及可以养活自己,并且也有一些余力可以让他生活得好一些的时候,他却永远地离开了我。子欲养,却亲不待。

人这个物种大约就是这样,谁也无法脱开的生死的轮回,每个人都应当看开些。但生死事大,岂不痛哉!

爷爷的在党50年纪念章

今年也是建党100周年,在这里我要向爷爷这样为国家建设奉献了一生的老同志们,致以最崇高的敬意!虽然在你们身上没有那些波澜壮阔的功绩,也没有曲折动人的故事,但正是由于千千万万你们的付出,让共和国有底气走到今天,也是千千万万你们所坚持的初心,守住了和传承这个社会的正气。

其三

现在,让我把回忆的时间线再次拉回年初,去回忆另外一条主线。

从年初开始,我进入了频繁去杭州出差的状态。虽说每次的目的地都一样,但随着四季的变化,都有新的风景,好不惬意。比如仲春的龙井茶园、夏日的西子湖畔,还有秋日的大运河滨都让令人着迷。我也终于理解为何有如此多的文人墨客偏爱这里,留下了无数流传青史的千古墨宝。

2021飞行统计

这一年的多次往返让我对这座城市有了许多直观的感受,比如:

  • 自然和人文风光不必多说,不仅好地方多,而且风景在四时都有所不同;
  • 但是基础设施相比于北京也还存在不小的差距,不过可以看出一直都在进步
  • 城市处于扩张阶段,老市民集中在老城区,新市民都在新城区,在新城区能明显感到接纳包容的年轻的城市文化,但老城区却相反
  • 地价房价上涨很快,后上车的人成本变高,且造就了一批房产富豪

另外,更重要的是卤蛋也逐渐在这座城市安顿了下来,所以我之后还有大把时间在这里走走转转,期待能有更多更深入的了解。

烟雨龙井村

西湖落日

繁忙的大运河

其四

除此之外,在去年上半年我也经常进行体育锻炼,在周内最常去的地方是公司楼下的大望京公园,而在周末则非奥森公园莫属了。基本每周的跑步距离在15到20km左右,除此之外还会骑车上下班,一天大约能有十几公里。跑步的配速也逐渐从年初拉夸的七分钟,逐渐恢复到了年中的五分多钟。并且通过一起跑步,也和公司里的一些同事有了更多的交流,收获满满。

某次跑步记录

虽然我的运动量这么大,但是体重却几乎没什么变化。原因很简单——每天都得吃点好的。

跑完步当然要吃顿好的犒劳一下自己

肯定有很多人抱着和我一样的想法:已经这么努力运动了,在饮食上放松一下也没有问题。然而,事情没有这么简单。

在下半年里,我付出了沉重的代价:告别了火锅,告别了啤酒炸鸡,告别了我最爱的麦当劳,甚至告别了多数肉类。同时,我也告别了熬夜看视频,告别了久坐打游戏,也告别了我最爱的长跑。

现在看来,效果还行。体重从巅峰的接近85kg降低到了如今的不到75kg,身体也逐渐恢复正常。不过随着体重的减轻,肌肉们也在一同不断流失,但做什么,都总得付出些代价吧。

下半年的体重变化

钱花光了可以再赚,物品丢掉了可以再买,数据消失了都可以再造,但是健康失去了却很难重新恢复,关心之人去世了就更难弥补。正所谓“知易行难”,这些简单的大道理说起来容易,但若没有经历过,却很难真正理解。如今理解了,却也都付出了很重的代价。

最后,我发现猫咪真的是一个十分神奇的物种,不论什么时候,它们总能让人感到治愈,会让人多一些面对不确定性的勇气,所以就用一张球宝(室友的猫咪)的靓照作为本章的结尾吧。

球宝一瞥

工作 & 技术

坦率地讲,自己在去年工作中的收获是不小的。不仅是技术上做到了真正的入门,还有在心理上具有了一定的主动性。

回过头来审视从刚入职到现在的这一年半的时间,能明显感到自己从一个刚进入公司时几乎什么都不懂,只会低头自己琢磨,不会跟人沟通,甚至因为担心自己水平差,而感到有些自卑的职场新人,成长为了一个较为自信,能直面问题,从更多角度思考,并跟他人配合,共同解决问题的人。

我认为入职即将满一年的时候对自己来说是一个很重要的节点,原因很简单,那会儿刚好在年末评定绩效,所以在这段时间里,我终于有机会也不得不好好梳理和思考去年一年在我身上发生的事情,以及我面对事情所采取的态度和行为,并且也能将这些思考和主管进行直接而充分的沟通。什么是好的,什么是不好的,什么之后应该努力避免,什么事情还能做的更好,都逐渐变得明晰。这种明确的反馈对于一个人的成长有很大的帮助。

部门校招同学培训时的一年香蛋糕

当然,作为一个工程师,除了上面这些比较“务虚”的成长,一定还得聊聊自己技术的进展。

祸福相依,虽说自己干自己的,不懂和别人交流不是什么好事,但是也是由于多数时间都在低头琢磨,自己写代码,所以从刚入职到一周年左右时,我大概零零散散为项目提交了大约一万多行不到两万行代码。因为我所编写模块代码和其它模块有很多耦合,所以编码的时候我也被迫几乎阅读了我们组所负责项目(一个Rust编写的VMM)的大部分代码,这让我很快对这个项目的设计与架构有了大致的了解,也让我更加熟练地掌握了Rust语言。同时,这些工作也让我意外地获得了整个BU的第一届代码贡献奖。

代码贡献奖现场

去年有关技术上的成长大概可以归纳为以下几个方面:

  • 入门虚拟化:从只会使用VmWare,到现在开始认真了解虚拟化的原理,阅读KVM源码,并实现了一些VMM的代码
  • 开始学习Linux内核:阅读了一些和虚拟化以及业务相关子模块的部分源码,同时也编写了几个简单的内核模块。特别是在下半年,由于零零散散地参了一些创新项目,所以也开始对Linux运行所依赖的Arch,以及UML有了更多的理解与思考
  • 了解容器生态:当前主要局限于单台节点中,比如runc/kata的实现原理,containerd的工作流程等等

在公司里,技术和业务一直都是牢不可分的,所以,我也借着开发这些代码的由头,了解到了当前云原生的发展形势、当前遇到的问题,以及众多业务场景(比如函数计算和弹性容器)。随着这些背景知识的输入,让我对整个云计算行业,特别是云原生业务的发展前景,抱有较为乐观的预期。

此外,还有一个好消息是我们大团队将我们平时所研发的操作系统分支及其上下游组件开源为“龙蜥社区”,而我们小团队所做的项目也会通过龙蜥社区KataContainers这两个社区将源代码贡献出去。所以,后续应该就会有更多可聊的技术相关的东西了。

云栖大会中和“小龙人”的合影

经验 & 思考

经过去年一年的经历,也获得了一些的简单的经验,在这里也将其中一些分享出来。还有一些思考不成体系,略显混乱,就不买弄了,等以后有机会仔细整理,再发出来。

人的精力有限

可能因为之前比较年轻,所以总觉得无论有多少事情,我总能想办法做完,大不了熬上几个通宵。

但是到了现在,发现自己的精力已经远不如本科时候了,如果晚上熬夜,第二天可能就完全无法正常工作,这样效率可能比晚上好好休息还要低。这才让我真的意识到自己的精力有限,很多看起来很有兴趣,动动手就能做完的事情,其实自己并没有精力去做。

这对于我这样的完美主义者是一个非常大的打击。无限把事情做到完美的代价要么是很多更重要的事情无法完成,要么是自己的健康受到损害,而且甚至牺牲了这两者还是无法把你最想做的事情做到最好。

在痛苦地挣扎了一段时间后,我还是向规律妥协了:把做事的目的逐渐由把事情做完美变成了把事情做成。我发现影响一件事情成败的关键点只有几个,剩下都是锦上添花,因此做事情不能贪多,需要将它们的优先级进行排序,先做优先级高的,再做优先级低的,虽然有些优先级低的事情做完会让整体更加完美,但是你如果没有精力顾及这么多,那也只能放弃。

另外,虽然个人的精力有限,但是人多力量大呀。如果能把多个人的力量集合起来,就能完成超出个人能力的更大的事情。不过如何利用别人的力量一起完成一件事情,我暂时还没有特别成功的经验可以分享。

小事快速决策

一定有很多人像我一样,是比较纠结的性格,做决策时总是考虑再三,一直无法得到一个明确的结论。这在一般时候不会表现出什么问题,但如果面临的决策非常多,那么做决策这件事儿将会是时间地狱和精力黑洞。

举一个在工作中常见的例子:在开发一个模块的时候,虽然架构设计已经确定了,但在真正编码的时候依旧经常会面临怎么写比较好这个问题,比如大到一个消息通知机制应该怎么设计,小到这个变量应该叫什么名字。不知道大家会怎么样,我经常会为了这些事情纠结很久,甚至把每种方案都写一遍,看看哪个更好。但最终往往只有两种情况:要么是它不重要,怎么写都可以,要么是只有其中一个设计比较好,但是需要写完后面的代码你才会知道,现在纠结并没有什么作用,甚至可能纠结很久,还是会选择一条错误的道路。

因此,在当前决策对未来事情发展影响比较小的情况下,快速决策快速做选择快速试错才是比较优的策略,如果一味纠结,不仅会浪费很多时间,而且往往也不会增大把事情做对的概率。所以,纠结症患者不如放下心中的纠结,闷着头随便选一个,省时还省力。

不过上面的方法并不适用于重要事情的决策。因为重要事情决策可能会对后面产生重大影响,哪怕成功率只提高一点,也会有很大的价值。而决策的关键的是收集和整理信息,如果只顾着快,而忽略了很多有用的信息或没挖掘到信息中比较重要的点,导致决策出现失误,这就得不偿失了。

合理预估时间

在多人协作时,每个人的工作可能多少都会依赖别人的工作,而为了便于将每个人的工作进行组合,管理者一般会采用排期的形式把控项目的进度。所以,在工作中,就总会有人问你:你觉得这个事情多久能够完成?

但从个人的角度来看,面对一个稍微复杂一些的事情:

  • 总是需要较长的时间完成
  • 往往中间会因为吃饭/睡觉/开会/有其他更紧急事情等多种原因被分割成多段,保存/恢复工作状态需要消耗意志力和时间
  • 事情完成的过程中几乎必然会出现许多意料之外的情况
  • 因为事情紧急,为了督促自己尽快完成,可能会预估一个比较早的时间
  • 人们的思考模式也决定了我们往往对一个事情的预估是偏乐观的

所以,这最终会导致个人对一件事情完成时间的预估是比较乐观的,在deadline之前一段时间往往会拼命去赶,就这样还不一定可以做完,导致延期,而且还可能会由于个人的延期导致项目节奏被打乱,从而导致项目的延期。

虽说项目规划者有责任考虑到这些原因,但作为参与人,也有必要对项目完成的时间进行较为合理的评估。目前,就个人经验看来,一件事情完成的时间往往是自己脑中认为可以完成的时间再增加50%以上。因此,在需要精确项目时间的时候,我们可以通过将预估完成时间直接*2的方法来为自己保留合理的裕度。

新年OKR

去年,直接用几条flag表达了对新的一年的期望,由于没有合理的checkpoint与拆分,导致很多都没有完成,今年我认为需要改变策略,用用新的OKR工具做做试验,看看是否能让自己真正动起来,将目标的完成率提高。

下面,就将今年的一些OKR列出来,不过使用博客跟踪OKR的效率肯定不高,所以后续会尝试配合一些跟踪目标的软件一起使用。

Object1:生活健康自律

健康的身体是一切的前提,所以我希望将身体健康,生活自律放在OKR的第一位

KR1: 早睡觉,不熬夜

睡眠质量对身体健康和工作效率都十分关键,因此早睡早起是必须要做到的目标。

由于之前都睡得比较晚,睡眠时间需要慢慢调整,所以将晚睡定义为在凌晨12:40之后上床睡觉

非特殊情况,每周最多有一天晚睡/熬夜

KR2:坚持锻炼

由于每天的工作都是对着电脑久坐,因此必须要让自己有时间动起来

锻炼以一定强度的有氧运动为主,且不能过于剧烈,可选的方案:慢跑、球类运动、骑行、爬山、跳绳等,时长以30分钟以上为宜

在三月份气温回暖后,非特殊情况(如生病/出门在外等)每周至少进行三次锻炼活动,三月份之前,每周至少两次

KR3:健康饮食、控制体重

饮食对身体健康的影响也十分显著,体重亦是如此,因此需要进行合理的安排

饮食目前没找到太好的定量约束方案,就限定一下吃饭速度吧,如果有一同吃饭的人,不能吃的比所有人都快

体重比较方便定量,在3月份气温回暖后,逐渐将体重控制在70KG(±3KG)

Object2:知识的输入和输出

KR1:阅读原版书籍

只有阅读一手知识才能让自己真正理解知识,通过看视频/读别人的笔记获得知识的速度虽然快,但是可能会忽略掉其中的许多细节

能增长知识的书籍分为两种:技术书籍和人文社科书籍,它们的阅读方式和收获也是不同的,因此单独制定计划:

技术书籍两本:

  • 计算机体系结构——量化研究方法
  • 暂定

人文社科书籍五本:

  • 八次危机
  • 暂定

另外,暂定每周有五天时间每天阅读半小时

KR2:学习优秀源码

精读优秀的源代码和原版书籍一样重要,需要仔细阅读,暂定两个虚拟化的项目,后面再做补充

  • KVM
  • QEMU

KR3:输出内容

在阅读完别人的内容或者自己进行一些实践活动之后,如果不及时总结,可能会导致没有彻底理解,而总结的最好方式就是输出一篇较长的文章,既锻炼逻辑思维,也提升写作能力,因此暂定一年输出五篇文章

Object3:其他目标

除了主要方向外,还有一些其他的目标,暂时只列陪伴家人,后续再做补充

KR1:陪伴家人

陪伴家人的目标去年执行得不错,今年要继续保持

  • 每周和家人电话
  • 逢年过节没有疫情的情况下回家

The End

最后,就以前几天自己做的一个梦作为博客的结尾吧:不知何年何月何日,乘坐一架很大的宽体客机回家,但是飞到大约三分之二的地方,飞机突然失控坠向地面,好在紧急迫降成功,降落在了一个不知名的地方。这里到处都是荒漠,还有一些居民,只有天边模模糊糊可以看见草原。手机导航没有坏,它告诉我没有别的交通方式,只有步行导航,而且需要走很久很久才能到家。我也找了一圈,也的确没有看见其他的交通工具。许多乘客都因要很久才能到目的地而选择在这个荒漠小城留下,看情况再决定是否出发,但是我在找附近的居民买了一些馕作为干粮后,马上就出发了。

能不能走出荒漠,我不知道,至于能不能走回家,就更不知道了。但是梦中的我做出了坚决的选择:不要在意这些,走下去,马上出发!

2020年终总结

作者 Jason Bian
2021年1月1日 17:09

时间很快,如今已经是2021年伊始了。虽然已经很久没有写过文章,但是,面对2020年,我总还纠结着要说些什么。

2020最大的主题就是变化。不仅有意料之中的变化,更多是意料之外的变化。

疫情

说到变化,总也绕不过的就是疫情。

我的2020年是以一次滑雪作为开始,在那场滑雪之后,我还在畅想在结束毕业论文写作之后,毕业前的时间我应该怎样度过,是要趁着有闲逛上大半个中国,还是省一些钱躲在实验室里啃啃买来一直没读的大黑书。

年初滑雪

放假回家后,就开始盘算:工作后肯定没法在家待太长时间,不妨这次放假在家里多待几天,多陪陪家人(多睡睡懒觉),不到交论文的那天,坚决不回学校。

然而现实远超我的想象,不仅交论文的那天没能回学校,甚至到了毕业那天,都不能回去。

事情的起因还是一月份开始听说有一种类似SARS的病毒已经开始悄然传播,那时候感觉病毒还很远,和我没有关系;直到除夕前一天,武汉封城,全国人民在家过年,我开始知道它的名字叫新冠病毒,而且十分严重,和我们每个人每天的生活息息相关;后面,我又通过躲在手机屏幕后的暗暗观察,发现这件事情不仅改变了我的计划,改变了中国人的生活,甚至整个人类都与此息息相关;而直到一年后的今天,疫情依旧不知疲倦地在全球蔓延。

是呀,仿佛离我们很遥远的事情,在短短一年内变成了每个人所面临的现实,这对于一直沿着一条可被经验预测的道路前行的我来说,的确是一种震撼。

另外还有一个比较深刻的感受,是“正常化偏误”。之前听的比较多的一个例子是一个小镇旁边有一座火山,那里的居民居然在火山爆发以后觉得问题不会那么严重,没有逃跑,导致伤亡惨重。我想我一定不会这么蠢,一定要最开始就逃走。但是,当疫情来临的时候,我在最开始就觉得应该是要买口罩了吧?但是又觉得会不会没这么糟,现在就去买口罩会不会显得自己很异类?就这样拖延了两天,发现城里已经几乎买不到正常价格的口罩了。过后,我才醒悟过来,原来自己也随时可能变成眼看着火山灰把自己掩埋的那个人,这也是我2020年学到的第一个教训:当你觉得应该行动的时候,就一定要开始行动,不要在乎别人眼里你是怎样的。

病情

在疫情不太紧急的四月份,在家赋闲,于是给自己送给一台手术作为生日礼物。人生第一次手术还挺成功,我的胆囊和矿泉水瓶盖大的结石永远地离我而去。不多说,希望看到这里的每一个人都能按时吃早饭,健康饮食吧。

毕业

在开始读研的时候,我预想了无数种毕业时候我会做些什么,至少应该和卤蛋同学补上之前本科毕业没拍成的毕业照。但万万没想,这次毕业没有聚餐,没有毕业照,甚至都没有亲手拿到打印出的沉甸甸的论文,只有腾讯会议上大家还算灿烂的笑容(毕竟所有同学都顺利毕业)。

毕业“合影”

毕业以后我的一个愿望是等疫情结束,能回到陪伴我三年时间的小灰楼302去逛逛,找潘老师以及师弟师妹们聊聊天,可惜变化永远都是猝不及防——当时老纪说的多年前位于澡堂三层的上古实验室居然变成了我们如今的新去处。

工作

人呐就是不知道,自己就不可预料。我绝对不知道,我作为一个搞网络搞SDN的,怎么就来做云原生了。

说实话,福厂的工作节奏还是有点快,让摸了七年鱼的我花了很久去适应了。不过好在我遇到了一群非常nice的同事,特别是几位师兄,从他们那里学习到了非常密集的知识,让我从一个无法无天的野生程序猿变成了一个(自认为)还算及格的底层工程师。

这半年来,我的工作主要集中在和容器沙箱相关的技术之中,从一个虚拟化的门外汉,到看见了门槛之所在。另外,通过熟读Firecracker源码,也让我对Rust和microVM有了一个全新的认识。

在公司遥看望京SOHO

读书or视频

今年唯一的遗憾就是没有阅读太多的书,特别是很多书读到一半就放下,并没有读完。分析是两个方面的原因:一是由于生活模式的切换,自由时间变少,时间碎片化严重,没有相对较长的时间和精力来进行阅读;另一方面是B站使用时间显著增长,妄图通过对知识区视频的学习跳过读书快速获取知识。

这里可以借这个机会稍微聊聊我对B站的体会:在深度使用B站接近1年后,就知识区和科技区来看,UP主的数目持续增多,优质稿件也不断变多,这就吸引我每天花费大把的精力在观看所关注UP主的视频上,甚至都不敢去刷推荐流,很担心又看上哪个新的up主,想去追视频但是已经没有时间了。我不知道这是好事还是坏事,但是对B站来说绝对是一件好事,每天都会冒出许多新的创作者,分享许多奇奇怪怪的新内容,而且这也的确能够吸引观看者花费更多注意力在B站上面。这一切的一切,让我想起了刚上大学时候的微信公众号,嗯,熟悉的感觉又回来了。

还是说回读书,我认为能够进行较长时间专注的阅读,特别是大部头的书,对我来说还是十分重要的。因为视频中的知识点往往都比较零散,而且很难讲深,构成体系,只能作为谈资泛泛去听。所以,我认为系统性地读书还是十分必要的,接下来我一定得压缩每天在B站上花费的时间,重新回来读书中来。

希望接下来的一段时间,先把剩下的半本《红楼梦》读完。

一点思考

论2020年对我影响最大的人,莫过于温铁军和他的《八次危机》了。在B站看见他的视频后,发现他的思想理论和主流观点差别很大,但是又有一种吸引我不断去看他的演讲去思考他说过的每一句话的魅力。原因很简单,我逐渐意识到,他说的可能才是对的。

回想起来,印象最深的莫过于“代价”和“矛盾”,大到国家发展现代化需要代价,维持现有制度也需要代价,小到个人,过上好日子需要代价,不被别人支配更需要代价。大概就是只要你想要改变现状,就总要付出点你当下拥有的资源,以便让你在未来的时空里拥有你想要的资源。之前中学老师的一句口头禅“出来混,总是要还的”大概就是这个现象的通俗版本吧。和代价孪生的是“矛盾”,因为代价总是需要牺牲一些来成全另一些,那么牺牲者与被牺牲者之间就会产生矛盾,那么我想这也可能也是导致矛盾在不同的时期动态变化的一个原因。正因为代价和矛盾的不断转换,推动了事物的不断发展,个人是这样,国家和世界也都是这样。

结语

昨天午睡的时候做了一个梦,梦见自己在搬家,搬家的对象中有一个大箱子,很难搬走。箱子里装的是一个游戏,游戏的在一些城堡中发生,城堡的主人既希望继续战斗,赢得更大的城堡,又担心自己进攻的时候自己的城堡被别人抢走。我纠结了很久,决定让主人公继续战斗,并终于搬走了这个箱子。但是做完这个决定后,感受到了无尽的空虚与劳累,就仿佛做了一个多大的选择一样。紧接着,突然惊醒,意识到这个城堡小游戏的经历原来可能是自己前一天晚上没睡好而在纠结是否要起床的投影。可真的就只是这样么?我想也不尽然。总之,既然已经在梦中做出了选择,就不如在2021继续践行吧。

Flags

最后是每年的保留节目,立Flag,希望明年都能完成:

  • 分享至少5篇较为优质的内容
  • 对领域内技术的认知上有明显提升,具有一定的专业性和深度
  • 维持学习状态,保持对世界的好奇心,对世界的认知上更进一步
  • 阅读,至少5本较篇幅较长的著作,减少看视频的时间
  • 锻炼身体,体重维持在80kg以下
  • 多陪陪家人,每周和家人通话

使用Qemu和GDB对Linux内核进行调试

作者 Jason Bian
2020年5月2日 17:11

使用Qemu对Linux内核进行调试是一种较为便捷的方式,近日进行了一番实践,并将大致步骤与其中一些小坑记录了下来。

环境

由于放长假赋闲在家,所以手头只有一台装有MacOS的MBP可用,而Linux内核的开发与调试使用Linux环境下会比较方便,所以就使用VMware Fusion创建了一台安装有Ubuntu 18.04系统的虚拟机。由于编译Linux内核及相关软件需要的资源较多,所以为虚拟机配置了双核CPU、2GB内存和20GB磁盘空间(笔记本本身资源有限),但实际使用(特别是物理内存和硬盘)捉襟见肘,于是又在系统中添加了3GB的SWAP内存并扩容了20GB的磁盘空间(其实还是不太够)才解决问题。

编译Linux内核

首先,尝试对内核进行编译,在编译前需要使用通过KConfig启动内核的调试配置。

下载内核源码

由于Linux内核代码量非常大,且由于国内网络大家都懂的原因,所以的下载内核源码是一项较为复杂的体力活动。

第一种方法是直接Clone Linux源码的Git仓库,当前,其仓库大约为3.7GB。在通过内核官网(https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/)或者GitHub(https://github.com/torvalds/linux)进行Clone的过程中,经常会遇到连接断开的情况,非常捉急。而如果通过国内镜像源,如清华Kernel Git镜像进行Clone的时候,最开始速度飞快,但是后面速度会越来越慢。因此,如果不像我这样头铁的话,不建议使用这样的方式下载Kernel的源码。

另一种较为简单的方式是下载特定版本的源码,这些源码的tarball包可以从内核官网或者镜像站获得。我在实验中使用的内核版本为4.19,gz压缩包的大小约为150MB。

配置内核

如果是使用Git Clone的方式获取的内核源码,需要通过git checkout v4.19将内核源码置位4.19版本。

在编译之前,首先需要安装相关的依赖(如果提示缺少其它依赖按需安装即可)

1
sudo apt install libncurses5-dev libssl-dev bison flex libelf-dev gcc make openssl libc6-dev

在编译之前,需要使用KConfig对内核编译选项进行配置,在内核文件夹下,使用make menuconfig(命令行界面)或make gconfig(基于gtk的图形化界面)对内核进行配置。在配置时,需要打开如下选项:

1
2
3
Kernel hacking -> Kernel debugging
Kernel hacking -> KGDB:kernel debugger
Kernel hacking -> Compile time checks and compiler options -> Provide GDB scripts for kernel debugging

并保证如下选项没有开启:

1
Kernel hacking -> Compile time checks and compiler options -> Reduce debugging information

在退出配置后,可以发现内核目录中生成了一个名为.config的配置文件。

编译内核

配置完成后,就可以使用make编译内核,在多核CPU中可以使用make -jx启动多线程编译(x为启动的线程数)。

如果一切正常,在漫长的等待后,内核将编译完成。编译会在内核根目录下生成vmlinux 文件,它是编译出的原始内核文件(含有调试信息),而会在arch/x86/boot/bzImage目录下生成压缩后的内核文件(当然是在编译的体系结构为x86的情况下)。

编译安装GDB和Qemu

由于内核调试所需的GDB和Qemu版本可能会比apt源中的版本高,所以,最好自行编译安装这些软件。

编译安装GDB

首先,从官网(http://www.gnu.org/software/gdb/download/)下载GDB的源码并解压(这里使用的是官网中最新的GDB 9.1),需要注意的是,网上有些博客中提到需要修改GDB的源码,其实是不必要的,报错的原因是没有自动检测到目标体系结构的类型,所以只需设置该类型即可。

解压后进入GDB文件夹,执行下列指令,即可完成编译安装:

1
2
3
4
5
mkdir build 
cd build
../configure
make -j4
sudo make install

最后,通过使用gdb -v确定gdb的版本是否为9.1,如果是,则说明安装成功。

编译安装Qemu

首先,从官网下载(https://www.qemu.org/download/#source)Qemu的源码并解压(这里使用的是Qemu 5.0.0)。

由于在Ubuntu GUI中使用Qemu还需要多媒体图形库SDL,所以需要首先使用apt安装sdl:

1
sudo apt install libsdl2-2.0-0 libsdl2-dev libsdl2-gfx-1.0-0 libsdl2-gfx-dev libsdl2-image-2.0-0 libsdl2-image-dev 

进入Qemu目录后,执行./configure检查系统配置并生成Makefile,需要注意检查的时候是否检测到了SDL的支持,其输出的部分内容如下所示:

1
2
3
4
5
6
7
8
profiler          no
static build no
SDL support yes (2.0.8)
SDL image support yes
GTK support no
GTK GL support no
VTE support no
TLS priority NORMAL

然后执行make && make install即可完成Qemu的编译与安装。

在安装完Qemu后,会生成如qemu-xxxqemu-system-xxx的一系列命令,用于仿真不同体系结构的用户态应用和操作系统,可以通过如qemu-system-x86_64 --version命令确认Qemu是否安装成功。

制作ROOTFS

在内核启动后需要一个带有init程序的rootfs,所以在调试内核前需要制作一个rootfs。

构建基于initrd的rootfs

initrd是一种位于内存的根文件系统,它可以在硬盘被驱动之前载入系统。这里为了方便,只将一个简单的程序写入initrd,并将其作为init程序(即系统启动后的第一个用户态进程)。除此之外,也可以使用busybox作为initrd中的init程序。

创建一下简单的c程序,命名为fakeinit.c

1
2
3
4
5
6
7
8
9
10
11
#include <stdio>
int main()
{
printf("hello world!");
printf("hello linux!");
printf("hello world!");
printf("hello linux!");
fflush(stdout);
while(1);
return 0;
}

然后使用gcc编译这段代码,在编译的时候需要使用静态链接,并且如果如果在配置内核的时候没有启用64位支持(64-bit kernel),则需要将代码编译为32位程序,方法是在gcc命令行中添加-m32选项。

编译命令如下:

1
2
gcc --static -o fakeinit fakeinit.c
gcc --static -o fakeinit fakeinit.c -m32 (编译为32位可执行程序)

在编译后,使用cpio程序进行打包:

1
echo fakeinit | cpio -o --format=newc > initrd_rootfs.img

这样,一个基于initrd的rootfs即制作完成。

构建基于硬盘镜像的rootfs

这里使用busybox构建基于硬盘镜像的rootfs。其中,busybox是一个集成了数百个Linux常用命令和工具的单个软件,在对内核进行测试的时候非常方便,号称“The Swiss Army Knife of Embedded Linux”。

下载编译busybox

首先,从官网(https://busybox.net/downloads/)下载busybox的源码并解压(这里使用的是最新的busybox-1.31.1)。

在解压并进入busybox文件夹后,首先使用make gconfigmake menuconfig对其进行配置,需要启用如下选项:

1
Settings -> Build Options -> Build static binary (no shared libs)

如果需要将其编译为32位版本,则需要将-m32命令填入如下选项:

1
2
Settings -> Build Options -> Additional CFLAGS
Settings -> Build Options -> Additional LDFLAGS

与内核相同,在退出后,会在目录中生成一个名为.config的配置文件。

然后,使用make命令编译busybox。

使用busybox创建rootfs

首先,创建一个空的磁盘镜像文件,然后将其格式化:

1
2
dd if=/dev/zero of=./busybox_rootfs.img bs=1M count=10
mkfs.ext3 ./busybox_rootfs.img

然后,挂载刚刚创建的磁盘镜像(需要使用loop设备):

1
2
mkdir rootfs_mount
sudo mount -t ext3 -o loop ./busybox_rootfs.img ./rootfs_mount

接着,在busybox源码目录中,将编译好的busybox目标文件安装到rootfs文件夹:

1
make install CONFIG_PREFIX=/path/to/rootfs_mount/

最后,配置busybox的init,并卸载rootfs:

1
2
3
4
5
mkdir /path/to/rootfs_mount/proc
mkdir /path/to/rootfs_mount/dev
mkdir /path/to/rootfs_mount/etc
cp busybox-source-code/examples/bootfloppy/* /path/to/rootfs_mount/etc/
sudo umount /path/to/rootfs_mount

现在,一个基于busybox的rootfs磁盘镜像就制作成功了。

使用Qemu和GDB调试内核

使用Qemu启动内核

由于编译的内核体系结构为x86,所以使用qemu-system-x86_64程序来载入并启动内核。

如果使用intird作为rootfs,则具体命令为:

1
2
3
4
5
6
qemu-system-x86_64 \
-kernel ./linux/arch/x86/boot/bzImage \ # 指定编译好的内核镜像
-initrd ./rootfs/initrd_rootfs.img \ # 指定rootfs
-serial stdio \ #指定使用stdio作为输入输出
-append "root=/dev/ram rdinit=/fakeinit console=ttyS0 nokaslr" \ # 内核参数,指定使用initrd作为rootfs,禁止地址空间布局随机化
-s -S # 指定Qemu在启动时暂停并启动gdb server,等待gdb的连入(端口默认为1234)

如果使用磁盘镜像作为rootfs,则具体命令为:

1
2
3
4
5
6
qemu-system-x86_64 \
-kernel ./linux/arch/x86/boot/bzImage \
-hda ./rootfs/busybox_rootfs.img \ # 指定磁盘镜像
-serial stdio \
-append "root=/dev/sda console=ttyS0 nokaslr" \ # 内核参数,指定root磁盘,禁止地址空间布局随机化
-s -S

使用GDB调试内核

最后一步,由于刚刚Qemu开启了远程调试,所以只需要将gdb通过连入即可:

1
gdb ./linux/vmlinux # 指定调试文件为包含调试信息的内核文件

如果此时直接在gdb调试器中使用target remote:1234连入Qemu的gdb server,则会出现报错Remote ‘g’ packet reply is too long,这是由于gdb没有正确识别调试目标的体系结构造成的(有些博客认为需要修改源代码屏蔽这个错误,实际上是不必要的),所以只需要在远程attach之前使用set arch i386:x86-64:intel设置目标体系结构即可。

例如,你希望在start_kernel函数设置断点进行调试,则在启动Qemu后,gdb的命令如下:

1
2
3
4
5
6
gdb ~/linux/vmlinux
(gdb) set arch i386:x86-64:intel
(gdb) add-auto-load-safe-path ~/linux
(gdb) target remote:1234
(gdb) b start_kernel
(gdb) c

可以发现,内核在启动后被中断在start_kernel函数上。

后记

内核文档

在内核的文档中,有一篇详细讲解了如何使用GDB调试内核。

该文档的最新版本可见于内核的官网:https://www.kernel.org/doc/html/latest/dev-tools/gdb-kernel-debugging.html

而具体的版本就需要在内核源码中编译文档了,例如html版本的文档可以使用make htmldocs进行编译,在启动HTTP服务器后,可以在浏览器中进行访问,例如,http://127.0.0.1:8000/dev-tools/gdb-kernel-debugging.html

参考来源

本文参考了两篇较为优质的博客:

在NodeJS中与C++代码通信

作者 Jason Bian
2019年10月22日 23:24

最近在项目中遇到需要在 NodeJS 中调用 C++代码的问题,在此略作总结。

主要方案

在 NodeJS 中,和其他语言编写的代码通信主要有两种方案:

  • 使用 AddOn 技术,使用 C++为 NodeJS 编写一个拓展,然后在代码中调用其他语言所编写的源码 or 动态库
  • 使用 FFI(Foreign Function Interface)技术,直接在 Node 中引入其他语言所编写的动态链接库

在对这两种方式进行比较后,发现这两种方式各有优劣。

首先,AddOn 技术比较通用,它可以使用 C++代码来拓展 Node 的行为,很多库都是使用这种方式来完成一些比较底层操作(比如和操作系统的一些通信)的。但是它写起来比较麻烦,要编写一个 C++项目,还要按照 NodeJS 的规范 export 相应的函数,而且每次安装的时候都需要进行编译(以适应本地 Node 的版本)。如果只是调用一个 DLL,那就还需要在项目里重新包装一遍 DLL 的接口。

如果使用 FFI 技术,限制就会比较多,首先,它只能调用其他动态库,如果你想使用 C/C++完成更多功能的话,还需要再封装一层 DLL,另外,它只支持_cdecl调用约定(也就是 DLL 在导出的时候一定要标记用_cdecl编译命令),不支持_stdcall或者_fastcall调用。但是调用起来就会很方便,可以直接在 JS 代码中声明 DLL 的接口就可以了。

综上比较,如果只调用第三方 DLL(而且恰好是_cdecl导出),使用 FFI 就再合适不过了(虽然性能可能会有一定的损失,而且调试起来会有困难)。

其实,从理论上来讲,FFI 也是基于 AddOn 技术的,只是它可以帮你把在 JS 中定义的接口直接转换成 C 语言的接口,并利用 NodeJS 的 Buffer 内存,将其同载入的 DLL 共享。当然由于 FFI 的这种通用性,也导致了一定的性能损失。

下面就以在 Windows 平台上使用 FFI 为例,简单聊一下如何使用 NodeJS 和 C++编译而成的 DLL 通信吧。

FFI 使用准备

安装 NodeJS

可能你的环境中已经有 NodeJS 了,但是,如果是最新版本,在安装 FFI 的时候会出现各种兼容性的问题(比如编译无法通过,虽然已经有人提供了 patch,但是还没有被 merge 进主分支,为了避免出现 bug,还是暂时不用为妙)。所以可以安装 LTS 版本代替。

另外,还需要注意要调用的 DLL 是 32 位还是 64 位的,Node 的版本需要和 DLL 的版本匹配。因为如果 64 位 Node 调用了 32 位的 DLL,是无法成功装载的,反之亦然。

安装 Windows 的 C++工具链

这里有两种方案:

  • 安装 Visual Studio,并安装相应的工具链。如果使用 VS 2019 版本的话,需要安装 C++桌面开发和 Windows SDK 相关的工具(Node v10 现在只支持 v141 版本的 MSVC),这种方式便于后续的调试工作(虽然也很艰难)
  • 在安装 Node 之后,使用管理员权限运行 Powershell,并全局安装 windows-build-tools,参考命令 npm install --global --production windows-build-tools

安装 node-gyp

node-gyp 是一个 Node 中基于 gyp 的跨平台的编译工具,用于编译其他库。

在安装的时候,需要使用 VC 的工具链,所以如果没有把工具链放在全局变量中,需要打开 VS 的Developer Powershell安装,该命令行一般在开始菜单的 Visual Studio 文件夹中。

参考命令:npm install -g node-gyp

安装 FFI 及 REF

下面的步骤依旧需要 VC 工具链,所以可能依旧需要在Developer Powershell中执行(建议常备该窗口,后面只要涉及到编译安装的命令都需要用到)。

安装 FFI 及相关工具的时候如果没有 VC 工具链,则会直接安装二进制代码,这样可能会出现包的 ABI 版本和 NodeJS 的 ABI 版本不符合的情况(在下面的 Tips 中会提到)。

现在,切换到项目的文件夹中,安装下面的包。其中,ffi 包是用以支持 FFI 功能的,ref 包是用以支持指针功能(原理是通过 Node 的 Buffer 内存,将 JS 的结构和 C 结构相互转换的)的,ref-*是用以支持高级结构的(比如数组和结构体)

1
2
3
4
npm install ffi -s
npm install ref -s
npm install ref-array -s
npm install ref-struct -s

除此之外,如果想支持 VC 中常见的 wchar 类型,还可以安装 ref-wchar 包。

安装 electron-rebuild 包

如果是 electron 项目,还推荐安装 electron-rebuild 包,该包可以遍历 node_modules 目录下的所有包,并将其重新编译。

然后,推荐在 package.json 中配置 electron-rebuild 的命令:

1
2
3
"scripts": {
"rebuild": "./node_modules/.bin/electron-rebuild"
}

之后执行在需要重新编译的时候只需要执行npm run rebuild即可。

使用方式

简单概览

可以查看如下官方示例:

1
2
3
4
5
6
var ffi = require('ffi')

var libm = ffi.Library('libm', {
ceil: ['double', ['double']]
})
libm.ceil(1.5) // 2

在引入 FFI 后,使用 FFI 调用了 libm 库(可能这个示例只能在类 Unix 系统中使用),一般拓展名为 libm.so,系统会在系统目录下搜索这个动态库,并将它使用动态链接器载入到 node 进程中。

接着,程序声明了 libm 库中的一个方法 ceil(向上取整),其中,该函数的返回值是 double 类型(第一重数组中的 doule),而该函数的入参也是一个 double 类型的值(第二重数组中的 double)。

最后,直接使用libm.ceil方法即可调用动态库中的函数,并返回正确的值。

这只是一个 FFI 的简单用例,更复杂的用法(主要是异步调用和回调函数)可以参考 FFI 的实例页https://github.com/node-ffi/node-ffi/wiki/Node-FFI-Tutorial

类型

FFI 的类型系统其实记住了 ref 库的类型,ref 库的类型系统基于 NodeJS 的 Buffer 内存,可以根据 Buffe 中数据的类型对 Buffer 内存中的数据进行访问和修改。

ref 自带的数据类型都是基本类型,比如 int 类型、bool 类型或者 string 类型。所有类型可以参考 ref 的wiki

ref 中的多数类型都有简写,比如ref.types.int可以简写为int

需要注意的是,char*可以写为string,对应的 ref 类型为ref.types.CString。值得注意的是,string 在 JS 中是基本类型,在 C 中却是引用类型。

对于指针类型,ref 提供了一个方法ref.refType()来得到,比如int*类型就可以使用ref.refType('int')得到。当然,为了省事,也可以直接用int*表示。

而对指针解引用,ref 库也提供了一个deref()方法。只要在对应类型的变量上使用该方法,就可以得到指针指向内容的变量。比如一个指向int*类型的 JS 变量 a_pointer,那么我们如果想得到具体的整数值,就可以使用a_pointer.deref()方法。

相反的,如果想获取某个变量的地址,就需要对某个变量使用ref()方法。

需要注意掌握类型与变量值的区别,使用ref.typesref.refType或者是下面会提到的ref_struct({...})获得的类型,而如果想获得某个类型的变量,有两个方法,一个是从 FFI 函数的返回值中获取,另一个是在 Buffer 中开辟一个空间,来存放类型为所获得类型的变量,下面会具体讲到。

如果需要在 NodJS 的 Buffer 中开辟长度为某个类型的空间,可以使用ref.alloc()函数,只要将类型名传入即可。比如,想开辟一个类型为 int 的内存,就可以使用ref.alloc('int')得到。

此外,还有以下几点需要注意:

  • 如果开辟类型为字符串的内存,推荐使用方法 ref.allocCString,其参数为一个 JS 的字符串。因为 C 语言的字符串在末尾有一个\0标识符,所以用这个方法可以更安全地得到 C 字符串。
  • 如果在 C 语言中值为 NULL,则在 JS 中对应的值为 ref.NULL。
  • 如果遇到指针类型,可以统一用'void'或者ref.types.void表示。
  • 如果要表示一个函数的指针,可以使用'pointer'表示。

对于复合类型,比如数组或者结构体,ref 库本身没有提供相应的支持,需要使用 ref-array 和 ref-struct 库来实现,具体可以参考这两个库的文档。

另外,对于 Windows API 中较为常见的宽字符 wchar 类型,也有一个基于 ref 的库 ref-wchar 进行支持。

最后,附上 ref 的文档http://tootallnate.github.io/ref/,具体的 API 都可以在这里进行查阅。

调用外部符号

假设我们有如下 C 代码(并把它写的复杂一些):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* main.c */
typedef struct t_s_t{
int a;
char b;
} t_s;

__declspec(dllexport) int add_one(int a) {
return a + 1;
}

__declspec(dllexport) void struct_test(t_s** t_s_p) {
*t_s_p = (t_s *)malloc(sizeof(t_s));
(*t_s_p)->a = 1;
(*t_s_p)->b = 'd';
}

上面的代码中,声明了一个结构体t_s,以及两个函数add_onestruct_test。其中,函数前面的__declspec标记表示声明该函数为导出函数。VC 默认导出 C 函数时是用_cdecl调用约定。

其中,add_one方法的作用显而易见,是将传入参数加一再返回。而struct_test函数的作用是先在堆上开辟一个大小为生面声明结构体的内存空间,然后将该内存空间的指针赋给传入的参数,并将该结构体赋值。(这里的代码其实不够严谨,没有进行内存回收,但这不是本文的重点,所以先不做讨论)

需要注意的是,如果是 C++代码,需要使用extern "C"标记导出,否则会因为符号修饰和调用约定的问题导致无法通过源代码中的符号找到该函数。

我们可以使用 VS 的Developer Powershell对上述源码进行编译:

1
2
cl /c main.c
Link /dll main.obj

编译后将生成 main.dll,我们在后面会用到这个动态库。

针对上述 C 函数,我们有如下 JS 代码,并假设和 C 代码在同一文件夹下:

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
/* index.js */
const ffi = require('ffi')
const ref = require('ref')
const ref_struct = require('ref-struct')

const t_s = ref_struct({
a: ref.types.int,
b: ref.types.char
})

const t_s_ref = ref.refType(t_s)

const test_ffi = ffi.Library(__dirname + '\\main', {
add_one: ['int', ['int']],
// aka 'add_one': [ref.types.int, ['int']],
struct_test: ['void', ['pointer']]
// aka 'struct_test': ['void', [t_s_ref]],
})
const result = test_ffi.add_one(20)
console.log(result) //21

t_s_p = ref.alloc(t_s_ref)
test_ffi.struct_test(t_s_p)
console.log(t_s_p)
console.log(t_s_p.deref())
console.log(t_s_p.deref().deref()) //a->1, b->'d'

首先,在代码中声明了一个结构体t_s,和 C 语言中的t_s*类型相对应。然后,我们还得到了结构体t_s的一个引用t_s_ref,和 C 语言中的t_s**类型对应。为什么在 C 语言中都多了一层指针呢?其原因和前面的字符串一样。

然后,声明了 test_ffi 变量,它调用了 ffi.Library 方法,该方法返回了 JS 中 DLL 的句柄和函数的声明,通过该变量可以进行 DLL 的调用。该方法有两个参数,一个是动态库的名称(可以略去拓展名 dll),另一个就是描述 C 语言函数的符号及其参数的对象。

该列表在上面已经简单介绍过了,对象的 key 是函数名,value 是一个数组,数组中第一个元素为函数的返回值的类型,第二个元素为另一个数组,它里面包含了函数的入参的类型。这些类型就用到了上一节中介绍的基于 ref 包的类型系统,类型可以用字符串表示,也可以用代码表示,可以参考代码中 aka 的注释。

接下来,代码通过test_ffi.add_one调用了 C 语言动态库中的add_one函数,可以看出,调用的方式和 JS 中的函数并无二致。但是需要注意参数类型千万不能传错,特别要注意 C 语言的 string 类型和 JS 中的 string 类型不同,要按照前文提到的方法进行转换。

然后,代码使用 ref.alloc 方法为 t_s_ref 变量开辟了一个内存空间(注意这只是一个指针大小的空间,并非结构体大小),并将该地址赋给 t_s_p 变量,然后将该变量传递给 struct_test 函数。因为 t_s_p 是一个二重指针,所以需要解两次引用,才能得到结构体真实的值。

回调函数

回调函数可以使用ffi.Callback()函数声明,该函数的第一个参数为返回值,第二个参数为入参列表,第三个参数为真实回调函数的闭包。

比如一个回调函数的定义如下,该函数会得到用户名和 id,并返回动作是否执行成功:

1
typedef int(*callback)(int, const char*);

那么,在 ffi 中,就可以使用如下方式声明该回调函数:

1
2
3
4
const callback_function = ffi.Callback('int', ['int', 'string'], (id, username) => {
// do something
return 1
})

在声明之后,需要将该回调函数做为参数传入某个函数:

1
2
3
4
5
6
test_ffi.set_a_callback(callback_function)

// Make an extra reference to the callback pointer to avoid GC
process.on('exit', function() {
callback_function
})

特别需要注意的是,设置完回调函数以后一定要保证该函数在 JS 中还存在一个引用(比如上面讲该函数的一个引用放在了 NodeJS 的 exit 事件中,这也是比较经典的做法)。否则,该函数将会被 NodeJS 的 GC 析构。其表现是:在程序刚开始执行的时候一切正常,但是执行了一会儿之后在调用这个回调函数,程序就会异常退出。如果用 VS 去对程序 Debug,就会发现该程序可能访问了非法指针,这是因为 DLL 代码中也存放了该回调函数的指针,但是在 JS 中该指针指向的地址因为没有被 JS 中的代码引用,所以被 CG 被释放,这样 DLL 中代码调用该地址的函数的时候,就会访问到非法的内存。

一些 Tips

DLL 的调试方法

在使用 ffi 的过程中,可能发现最大的问题就是程序难以调试。特别在面对 DLL 的时候,就像针对一个黑盒操作一样,虽然已经对着头文件将他的 API 使用 FFI 翻译为 JS 的代码,但还是难以确定传参或返回值是否正确,在 C++的代码中该参数是否正确传入,传入后是否正确执行等等。这就需要一个能够调试的方法。

一个比较好用的方式是使用宇宙第一 IDE Visual Studio 的 Attach(附加)到进程的方式进行调试。但这种调试方法的前提是手中有 DLL 的源码或 PDB(符号)文件(如果没有的话就只能看到出现异常的代码附近的反汇编的代码了,而通常这些异常都是内存错误引起的,其实它附近的数据可能没有多大的意义)。

如果手上有源文件,那么首先打开工程,然后在 NodeJS 载入 DLL 之后,就可以在启动工程的时候选择“附加到进程”,在对话框中选择 NodeJS 进程即可进入调试界面。在调试界面里,可以插入断点,也可以看到断点附近的内存。

如果手上没有源文件,但是有 PDB 文件(或者少量的源码),可以使用 VS 打开一个空工程,然后在调试的设置中添加符号文件的位置,这样也可以进行断点调试,在调试的过程中可以查看代码有没有命中断点,在命中断点时,会引导你载入项目文件,如果有的话可以选择,否则可以查看断点附近的反汇编代码。

具体的调试方法可以参考 MSDN 文档:https://docs.microsoft.com/en-us/visualstudio/debugger/attach-to-running-processes-with-the-visual-studio-debugger?view=vs-2019,在这里就不过多叙述了。

如何载入在其他文件夹中的 DLL

如果 JS 文件和 DLL 文件不在同一文件夹中,可能会出现载入失败,会出现类似于“Dynamic Linking Error: Win32 error 126”的错误提示。

这时,就需要将 DLL 文件夹的路径放在系统寻找动态链接库的 PATH 中,但是 FFI 并没有提供此类接口。不过,好在 Windows API 提供了 SetDllDirectoryA 这个接口用以切换该进程中寻找 DLL 的 PATH,可以使用如下代码完成这个操作:

1
2
3
4
const kernel32_ffi = ffi.Library('kernel32', {
SetDllDirectoryA: ['bool', ['string']]
})
kernel32_ffi.SetDllDirectoryA(your_custom_dll_directory)

一些链接时候的错误提示

上面已经提到,如果动态库不在 PATH 中的话,会出现无法找到动态库的情况,这时候会报“Dynamic Linking Error: Win32 error 126”的错误,在另外一些时候,只要没有找到动态库,都会报该错误。如果出现该错误,就需要检查动态库的名称是否正确,检查动态库的版本是否正确(比如 32 位 Node 使用了 64 位的 DLL)等。

另外,还有一个“Dynamic Linking Error: Win32 error 127”错误比较常见,该错误指没有在 DLL 中找到对应的符号,这可能就需要检查在 ffi 中声明的函数名是否正确以及是否 DLL 版本有偏差了。

在 Electron 中使用 FFI

由于每一个 Electron 的版本是基于相应的 Node 和 Chrome 版本构建的,所以在使用 FFI 之前需要根据所使用的 Electron 版本安装本地的 NodeJS 版本,否则 FFI 可能会和 Node 版本不匹配,导致提示 ABI 版本不一致:xx was compiled against a different Node.js version using NODE_MODULE_VERSION x. This version of Node.js requires NODE_MODULE_VERSION xx(NodeJS 使用 NODE_MODULE_VERSION 来辨别 ABI 版本)。

这种情况下,可以使用前文提到的electron-rebuild对项目中的所有插件进行重新编译(需要注意本地 NodeJS 的 ABI 版本一定要和 Electron 中 NodeJS 的 ABI 版本一致)。

另外,需要注意的是,Electron 5 以上的版本使用了 NodeJS 12 的 ABI,但是当前的 Ref 库并不支持该 ABI,会导致编译失败。不过已经有人提交了 pull request 进行修复,相信之后会有一个可用的版本出来。

另外,也有了 NAPI 版本的 FFI 和 Ref,分别名为ffi-napiref-napi,和 ref 相关的包,比如 array 和 struct 拓展,也有了相应的 NAPI 版本,命名规则同上。使用 NAPI 的 Node C++ 拓展接口相对稳定,是今后的趋势。

最后,Electron 版本可以在https://electronjs.org/releases/stable中查看,而 NodeJS 及其 ABI 的版本可以在https://nodejs.org/en/download/releases/中查看。

一些资源

最后,在这里放上最近踩坑时候经常使用到的一些资源吧:

简谈C++中指针与引用的底层实现

作者 Jason Bian
2019年9月11日 20:52

引用是C++相对于C而引入的一个重要特性,它使得许多地方的语法变得更加简洁,但是它们的底层究竟是怎么实现的呢?

在Wikipedia中,对指针有如下介绍:

In computer science, a pointer is a programming language object that stores the memory address of another value located in computer memory. A pointer references a location in memory, and obtaining the value stored at that location is known as dereferencing the pointer.

从定义可以看出,指针从本质上来讲就是一个变量,其中所存储的就是其他变量的地址。 而C语言中的指针非常灵活,它可以任意指向某一个地址,不论这个地址究竟是否存在,或它究竟存储的是否为指针所代表类型的数据。

那么也不难想到,指针在实现的时候也是内存里的一个变量,它存有其他变量的地址。

在Wikipedia中,对引用有如下介绍:

In computer science, a reference is a value that enables a program to indirectly access a particular datum, such as a variable’s value or a record, in the computer’s memory or in some other storage device. The reference is said to refer to the datum, and accessing the datum is called dereferencing the reference.

In the C++ programming language, a reference is a simple reference datatype that is less powerful but safer than the pointer type inherited from C. The name C++ reference may cause confusion, as in computer science a reference is a general concept datatype, with pointers and C++ references being specific reference datatype implementations. The definition of a reference in C++ is such that it does not need to exist. It can be implemented as a new name for an existing object (similar to rename keyword in Ada).

从上面的定义可以看出,在C++中,引用可以狭义地认为是某一个变量的别名,它本身是并不存在的。

基于以上说法,我就一度认为引用只是C++编译器在编译时的一些黑魔法,它在运行的时候将两个解析到的符号链接成了一个,从而完成了引用,而在编译之后,引用与本体就是一个变量(一个寄存器或栈上的值)。

但是,事实却打了我的脸。

我们通过以下程序进行检验:

1
2
3
4
5
6
7
8
int main()
{
int a = 0;
int *pa = &a;
int &ra = a;
++(*pa);
++ra;
}

该程序声明了一个变量a,然后分别声明指针pa指向a的地址,生命引用ra指向a,最后分别使用指针和地址对a进行了一次自加操作。

接下来,使用gcc -S test.cpp -o test.s -O0将上面的C++程序编译为汇编,看一下这些操作具体都是怎么实现的。(环境为MacOS,LLVM 10.0.1)

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
.section__TEXT,__text,regular,pure_instructions
.build_version macos, 10, 14sdk_version 10, 14
.globl_main ## -- Begin function main
.p2align4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
; 保护现场
pushq%rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
; 保存栈指针
movq%rsp, %rbp
.cfi_def_cfa_register %rbp
; 设置返回值为0
xorl%eax, %eax
; int a = 0
movl$0, -4(%rbp)
; t0 = &a
leaq-4(%rbp), %rcx
; int *pa = t0
movq%rcx, -16(%rbp)
; int &ra = t0
movq%rcx, -24(%rbp)
; t0 = pa
movq-16(%rbp), %rcx
; t1 = *t0 = *pa
movl(%rcx), %edx
; ++t1
addl$1, %edx
; *t0 = *pa = t1
movl%edx, (%rcx)
; t0 = &ra = &a
movq-24(%rbp), %rcx
; t1 = *t0 = *(&ra) = ra = a
movl(%rcx), %edx
; ++t1
addl$1, %edx
; *t0 = *(&ra) = ra = a = t1
movl%edx, (%rcx)
; 恢复现场
popq%rbp
; 返回
retq
.cfi_endproc
## -- End function

.subsections_via_symbols

我已经将汇编中的关键代码加上了注释,可以看出,变量a,指针pa,以及引用ra都位于栈上,index分别在-4、-16、-24。需要注意的是,引用并不是直接复用了变量a的-4(%rbp)地址,而是像指针一样,使用了一个新地址,并且将leaq计算得到的a的地址写入了其中。

而在进行自加的时候,除了最开始将指针中的内容拷贝到寄存器中所用的地址不同以外,指针和引用所使用的方式是完全相同的。

这个结果令我非常意外,编译器其实是将开发者对引用的操作翻译成了对指针的操作

最后,发现现代编译器还是很聪明的,如果将优化级别调到更高,就会发现它直接将中间的计算过程全部简化,直接返回,这是因为计算结果并没有任何输出,它是不必要的。如果将上面的代码从main函数转移到其他函数中,编译器这时虽然不能放弃计算其中的数值,但还是做了尽力的优化,直接返回结果(movl $2, %eax)。

进一步的实验

在文章发出后,有同学提出了质疑,认为可能只是MacOS上gcc编译器的特定操作,并不具有普适性,所以我在Linux和Windows上重复了上述实验。

在Linux环境中(发行版为Ubuntu 18.04,gcc版本为7.5.0)使用相同指令编译后得到的汇编码如下所示:

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
.file"test_ref.cpp"
.text
.globlmain
.typemain, @function
main:
.LFB0:
.cfi_startproc
; 保护现场
pushq%rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
; 保存栈指针
movq%rsp, %rbp
.cfi_def_cfa_register 6
subq$32, %rsp
movq%fs:40, %rax
movq%rax, -8(%rbp)
; 设置返回值为0
xorl%eax, %eax
; int a = 0
movl$0, -28(%rbp)
; t0 = &a
leaq-28(%rbp), %rax
; int *pa = t0
movq%rax, -24(%rbp)
; t0 = &a
leaq-28(%rbp), %rax
; int &ra = t0
movq%rax, -16(%rbp)
; t0 = pa
movq-24(%rbp), %rax
; t1 = *t0 = *pa
movl(%rax), %eax
; t2 = *t0 + 1
leal1(%rax), %edx
; t0 = pa
movq-24(%rbp), %rax
; *t0 = *pa = t2 = *t0 + 1
movl%edx, (%rax)
; t0 = ra
movq-16(%rbp), %rax
; t1 = *t0 = *pa
movl(%rax), %eax
; t2 = *t0 + 1
leal1(%rax), %edx
; t0 = ra
movq-16(%rbp), %rax
; *t0 = *ra = t2 = *t0 + 1
movl%edx, (%rax)
; 返回值设置为0
movl$0, %eax
movq-8(%rbp), %rcx
xorq%fs:40, %rcx
je.L3
call__stack_chk_fail@PLT
.L3:
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.sizemain, .-main
.ident"GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section.note.GNU-stack,"",@progbits

可以看出与MacOS中gcc的编译结果基本相同。

在Windows环境中(Windows 10,vs2019,cl版本为19.23.28106.4),可以使用命令cl /Od /FA .\test_ref.cpp对源码进行编译,可以得到汇编代码如下:

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
; Listing generated by Microsoft (R) Optimizing Compiler Version 19.23.28106.4 

TITLEC:\Users\jason\test\test_ref.cpp
.686P
.XMM
include listing.inc
.modelflat

INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES

PUBLIC_main
; Function compile flags: /Odtp
_TEXTSEGMENT
_ra$ = -12; size = 4
_pa$ = -8; size = 4
_a$ = -4; size = 4
_mainPROC
; File C:\Users\jason\test\test_ref.cpp
; Line 2
pushebp
movebp, esp
subesp, 12; 0000000cH
; Line 3
movDWORD PTR _a$[ebp], 0
; Line 4
leaeax, DWORD PTR _a$[ebp]
movDWORD PTR _pa$[ebp], eax
; Line 5
leaecx, DWORD PTR _a$[ebp]
movDWORD PTR _ra$[ebp], ecx
; Line 6
movedx, DWORD PTR _pa$[ebp]
moveax, DWORD PTR [edx]
addeax, 1
movecx, DWORD PTR _pa$[ebp]
movDWORD PTR [ecx], eax
; Line 7
movedx, DWORD PTR _ra$[ebp]
moveax, DWORD PTR [edx]
addeax, 1
movecx, DWORD PTR _ra$[ebp]
movDWORD PTR [ecx], eax
; Line 8
xoreax, eax
movesp, ebp
popebp
ret0
_mainENDP
_TEXTENDS
END

cl编译器的汇编代码格式和gcc略有不同,但含义相近,并且可以比较轻易地通过上面标出的代码行数确定汇编代码的含义。可以看出,它使用的方法也和前两种相同。

这里再进一步,引入知乎上“XZiar”同学的评论,她的评论更加深入地理解其中的机制:

其实不是说“把引用解释成指针”吧。

在机器码层面,也不存在指针,只存在地址(指针其实还隐含了类型信息)。变量这个概念也是不存在的,只有“无格式数据”,被带格式的指令操作而已。

所以你看到引用和指针的效果一样,是因为在机器码层面,没有多余的信息去表明他们的区别了。

而在语言层面,引用的确可以理解为const指针

另外,她针对为什么汇编代码中引用把地址复制了一遍也进行了更深入的解释:

另外引用把地址复制一遍也是很正常的,编译器也的确没法在编译期完全分析出引用的具体指向。考虑如下代码:

int a=0,b=1; int& c = flag ? a : b;

引用只不过因为const所以不能被重置,但具体指向什么,是可以运行期决定的。

到这里,对于指针和引用底层实现的探索也基本结束了,可以看出,在不启用编译器优化的情况下,主流编译器都会选择将C++中的引用解释为“const指针”。

但是,如果在启动编译器优化的情况下会是如何呢?在MacOS中,将源代码中的返回值改为a后(为了防止编译器优化后认为没有输出于是什么都不做),同时将编译器优化选项调整为O1和O2,其结果是相同的,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.section__TEXT,__text,regular,pure_instructions
.build_version macos, 10, 15sdk_version 10, 15, 4
.globl_main ## -- Begin function main
.p2align4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq%rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq%rsp, %rbp
.cfi_def_cfa_register %rbp
movl$2, %eax
popq%rbp
retq
.cfi_endproc
## -- End function

可以看出汇编版本的代码中省略了所有和指针、引用及内存操作相关的代码,直接将返回值设置为2。

从这里可以看出,编译器的作用是将语言编写的代码翻译为合理的汇编代码,只要汇编代码可以源代码的真实意图执行即可。由于机器码可以表达的概念有限(基本上就是对于寄存器和内存的运算),而高级语言可以表达的概念十分多样,所以编译器就需要将高级语言中的各种复杂概念映射(也可以看做是翻译)为机器码中的简单概念,映射的过程可能会有多种方案,其最终选择是由编译器来决定的。在C++指针和引用的翻译中,主流的C++编译器都选择将它们映射为机器码中的地址,而舍弃了其中的类型信息。

2018 年终总结

作者 Jason Bian
2018年12月30日 15:06

Intro

倏忽之间,又过去了一年。是时候向 2018 说一声再见了。

今年,我只在写 年关随笔 时立了一个 Flag,要练习深度写作,至少每个月输出一篇文章。可惜因为暑假时候偷懒,这个 Flag 也并没有达成。不过,我还是要在 2019 重新立起这样一个 Flag —— 2019 年每个月至少输出一篇文章,可以是技术,可以是随笔,也可以是观点。希望明年这个时候再回来看,不要像今年一样打脸。

再说回 2018。这一年相比前些年,在我的努力下,节奏逐渐慢了下来,让我有了许多时间静下心来思考、做事,也有时间和爱人相处、与朋友侃大山(至少在十一月之前是这样的)。同时,这一年又并不像以往那么顺遂,让我明白了人力有限,世间有太多力所不能及之事。

时光·记忆

在大自然面前,没有什么是能够永恒的。人不能够,事不能够,城也不能够,甚至文明也不能够。

在奶奶走后,我每每做梦都会回到小时候,梦见亲人们都在,他们都能和谐相处;梦见家乡没有衰败,还有很多人居住;梦见我很强,能够办到一切事情。

可惜梦中和现实却每每都是相反的。我无力改变太多,但至少可以做些什么。眼下能做的事情,就是把家乡的景,家乡的事儿多记录一些,等多年后,这里变成一片荒山野岭,无人问津的时候,人们还是能从我只字片语的记录之中,知道有这样一个地方,曾经有这样一批人,为了祖国的建设,把最好的青春和无尽的才华都奉献在这西北的茫茫荒山之中。

我在这里贴上几张年初所拍的照片吧。

这两张是白银市老城区白天和晚上的景观,拍摄于白银市人民医院综合楼楼顶。白银市从今年开始增加了许多城市亮化的工程,在白天看来就是一个暮气沉沉的工业老城,但是在晚上看来尤其漂亮。

下面的照片均是在西北铜加工厂(884)社区拍摄。

这是 884 附近的一座山,山上有一块形状酷似像狮子的石头,所以小时候我们都管这个山叫做“狮子山”。不过可能由于石头连年风化,已经随时会滚落下来,才有人在山脚下立了块标识危险牌子。

这是一条通往旁边村子的路,货车多半可能是准备进入大山深处,去拉从山中开采的石灰石。水泥路已经被一辆辆超载的大货车压得几乎没有了。

这是西北铜加工厂的正门(有两个,这是其一),在当年国企兴旺之时,上下班的时候人们进进出出摩肩接踵,但是现在物是人非,已经看不到什么人进出厂区的大门了。

这是通向厂门的桥,当年为了让职工可以在发洪水的时候安全上下班,就修了两座连通厂区和家属区的桥。桥下还有一个足球场,在发洪水的时候用作泄洪(我们称为“沙沟”),这两座桥似乎一个名为“兴旺桥”,一个名为“振兴桥”,但是年久失修,现在已经是两座危桥了。

这是奶奶之前工作过的食堂,由于年久失修,们都已经被水泥封了起来。透过门缝,还能看见火红的标语,一刹那间仿佛又回到了当年。

这是西铜的职工浴池,我们都叫大澡堂。前几年浴池终于也关闭了,我只能透过蒙着厚厚灰尘的玻璃撇入浴室的一角,找到当年的回忆。

这是当年“达利居”饭庄(之后改名为西铜宾馆)的舞厅。据说西铜鼎盛的时期,会有许多年轻的男男女女穿梭在其中。不过我记事的时候已经没有什么人了。

这是一座无名小山上凉亭的一角。当时厂里希望把这座山修成一座公园,于是修整了山道,修建了几座凉亭。不过还没修好,就已经开始亏损,这座公园也就只剩下这两个凉亭了。

以上都是我在西铜或者说是白银记忆的一个小角落,还有大量我的、同学的、长辈们的记忆有待挖掘。现在的西铜,已经破败不堪。一眼望去,只有一座座废弃的建筑和一望无际的荒山。一路走去,已经很少能碰见朝气蓬勃的年轻人,只有沟壑纵横的长辈们。这座城因他们而生,也注定会随着他们离去而消失殆尽。所以,我能做的,就是尽量用图片和文字来保留着座终将消失的城,留住他们珍贵的记忆。

出游

相比前几年,今年出行的地方没有那么多,但是每个地方都很值得好好留念。正所谓——世界很大,不能只看去了哪里,还要看是和谁一起去的。

苏州

2018 年出行的第一站是苏州,这也是我走过得祖国河山中最喜欢的地方之一。苏州不仅有数不尽的美丽景致,更有独特且很有内涵的地方文化,更有令人适宜的生活环境。更重要的呐,这还是和卤蛋同学第一次一起出远门旅行。刚好还欠卤蛋同学一个游记,就先在这里简要地写一下好了。

苏州城区不大,只靠地铁和公交在一个小时之内都可以到达。但是在城内逛游的时候真的可以感受到就像它的园林一般的一步一景。走着走着,就会出现一座很漂亮的古桥,又向前走两步,就到了一个小有名气的私家园林,再向前一看,发现这不是一家百年老字号嘛(快快快去吃好吃的别看了)。

另外,苏州话也非常有特色。虽然我听不懂,但是可以感到说话的人都还挺温柔的(就算骂人都骂的很柔),在苏州评弹里就更能体现了,每一个曲调都很柔美,让它所表达的故事格外的柔美。当地人也都十分热心,问路时候可以感受到他们十分有耐心,说话比较慢,令人舒适感倍增。当然,这也从侧面体现了他们生活比较安逸,节奏比较慢。

下面,我贴几张比较有意思的照片好了。

这是刚到苏州时候,沿着一条街向虎丘走,卤蛋同学在街边特别激动,左拍拍右拍拍。我刚好留意到他们街头布置得很细心,很能为街道环境和有宠物的人着想,在墙边设立了宠物便纸箱。

街角一转,居然到了到了河边。河边灰白色的天空衬着灰白色的墙,倒映在水里,十分宁静。这时,船夫划着一艘游船从远处徐徐开来,扰乱了湖水,也令人在心中荡起了浅浅的涟漪。

虎丘山中的一个沟壑。虽然小,但也有了一种悬崖峭壁的感觉,沟底的水中映出来天上的雨滴,植物颜色十分撞眼,犹如通往世外桃源的一条幽径。

市内的一座不知名公园。远望时感觉画面十分饱满,而近观又感到场景十分开阔,眼睛都舍不得多眨一下,生怕错过哪一处的美景。

苏州评弹剧场内部。老师底蕴深厚,唱腔优美。很多字发的都是古音,所以需要有一块屏幕向观众展示唱词。每唱一曲都会一下子把我带入古代天堂般繁华的苏州,置身闹市之中,但又让我进入心流。

黄天源的苏州小吃。如果苏州糕点说自己第二好吃,怕是没有什么地方敢说自己是第一了。不过最让我惊喜的是苏面,这是我除了牛肉面以外第二个感到能百吃不厌的面了。

除了这些,在苏州还有许多有趣的见闻,而且都还有许多没有去到的地方。以后可以有缘再见咯。

天津

在暑假的时候去了一趟天津。我在这次旅行中得到了一个惊人结论——天津和北京一样热。所以以后夏天还是老老实实待在实验室吹空调好了。

即使这么热,天津之眼还是要排很久的队,所以就随手拍了张照。

天津之行的时候还是我和卤蛋同学在一起一周年的纪念日,于是——再回来以后我受宠若惊呆若木鸡地收到了卤蛋同学送的花(hmmm 这应该也是人生中的第一束花),拿到花以后我甚至不知道该把它放在哪里,是不是应该把它插到水桶里养起来。

灵山

高中地理曾经学过——海拔每升高 100 米,气温下降 0.6 摄氏度。这大概是我在灵山之行中最直观的感受了。灵山是北京第一高峰,在门头沟区,几乎已经到了北京同河北的交界处,需要驱车三个小时才能赶到。

灵山是同潘老师、林博以及卤蛋同学我们一行四人去的。去爬山的时候天还是挺热的,所以我带了一件坛服去御寒。刚开始爬的时候我还很跳腾,但是到了一半左右,山势变陡,狂风大作,气温骤降。已经冷到了骨子里,无奈只好作罢。

灵山上的风景与山下迥异,像极了西北高原,居然出现了驴子和牦牛。下面是在灵山拍摄的一些照片。

活动

今年我也刻意减少了很多不必要的活动。在这说说几个比较有意思的吧。

Hack for Good

我最开心的不是又参与了一次 Hachathon,而是 Hackathon 成为了俱乐部每年的例行活动(而且这一场还是卤蛋同学参与办的)。

这次 Hachaton 弹幕派作为合作方参与到了其中,并且我也带团队来和大家一起 Hack 了两天,成果显著,做好了小程序和控制面板上线前的最后准备。

SDN 大赛

还有一件值得一提的事情是搞了两年的 SDN 大赛终于以二等奖为结果圆满收官。比赛中强哥、天骄、高钱、格格这些队友都十分给力,另外加上小雨姐和潘老师的助攻,让我们成功杀入了决赛圈。

现在看来,我做了一年的 P4 实验系统也随着这个比赛到了的尾声。一年以来,我总是在不断地在这个系统的 Bug 和 Debug 中度过,每天总是担心训练效果不是很理想(事实证明效果的确也不理想)。前些日子,基于这个系统的 INT 遍历算法中了一篇 Infocom,也算是这套系统的最大成就吧。

技术

今年最令我担心的就是我的技术了。因为离工作的日子越发的近,我却还是没有一门能拿得出手的技术。这些年来,我做了挺多多,了解了挺多,都是项目要用到什么就去学习什么,一直没有给自己找到一个能够系统深入的方向,说起来也很是惭愧。

不过下半年也算是有一个好的开始吧。我开始去刷 Leetcode(刷过的题目都可以在博客中的leetcode 板块看到),去看一些框架的源码,开始做 Go 语言的项目,去了解容器和一些 Linux 协议栈相关的知识。现在看来,能做的就只能是再接再厉,不要再掉链子了。

弹幕派

在这里我也给弹幕派做一个年终总结吧。

去年一年,弹幕派的增长效果还不错。在推广营销投入几乎为 0 ,且前有夹击后有追兵的情况下,靠着之前的老本,在用户量和营收上都得到了明显的增长。

在今年一年,一共有数万个用户注册了弹幕派账号,成功举办了几千场大大小小的活动。一共有数十万人参与到了活动现场的弹幕互动中,产生了数百万条弹幕,并产生了数万个付费订单。另外,弹幕派网站创造了数百万的 PV 和几十万的 UV。这在互联网公司看来可能都不算什么,但是对于我们团队来说,是一个不小的进步。

我们也投入了大量的精力在研发上面。我们在去年年初就将系统迁移到了 Swarm 集群中,并配置了半自动的编码、发布、测试、预览、上线的环境,经过一年的实践,系统运行良好。而在前端层面,我们将原有的多种技术栈进行了整合,都迁移到了以 Vue 为基础的技术栈中,这样团队成员在不同项目之间切换就变得游刃有余。而后端我们继续沿用了 Laravel+Workerman 的架构,并感到目前还远远没有到达天花板。

今年的状况就是这样,期待明年可以有更强劲的增长吧。

一些小事儿

今年有两次去同学家里做客的经历。一次是同卤蛋同学一起去胃寒家,一起做了一大桌菜(感谢心灵手巧的胃寒和蓉蓉);另一次是自己去牧野家,一起吃了一顿史上最小的小火锅(感谢牧野和小团子的精心准备)。

吃腻了学校的食堂,有时候觉得能和爱人在自己家里做饭吃(特别是再和朋友一起煮一锅热气腾腾的火锅,一起聊聊天),的确是很开心的。所以我有时候也开始对未来在外面打拼的时光充满期待。谁知道呢,或许也是一座围城吧。

年中的时候我也拿到了驾照,不过今年唯一一次路驾还是和牧野,从中关村开到了学校。以后有机会还得多开车,向着老司机的行列迈进。

年末的时候玩了今年的唯一一款游戏《古剑奇谭 3》,虽然游戏只花了 99,但是我体验到了几倍于价格的诚意。游戏画面精美,优化十分到位,我在大一时候买的 Y400 在现在还能基本流畅地进行游戏,即时战斗也颇为精彩。而在游戏中,文明的光芒、历史的厚重与人类的传承在我的眼前徐徐展开,而主角之间那种的真情也让我倍感舒适。比起之前玩的仙侠类的 RPG,古剑三的进步不止一点点。最近有机会我想专门写一篇相关的文章。

沉重的话题

2018 年这一年,有很多人离开了我。包括我的亲人,以及许多敬重的人。

奶奶是 2 月份走的,现在每每想起心中依旧难以平静。而在这之后,这一年难过的事情就没有中断。

回忆了一下,逝去的科学巨匠有霍金和高锟。霍金的黑洞理论其价值非我所能评论,但是他的《时间简史》以及《果壳中的宇宙》陪伴了我整个少年时期。而高锟所发明的光纤正构成了我们现在有线通信的根基。他们的逝世是全人类的一大损失,随着他们的离开,科学世界的光芒也变得暗淡了许多。搜索后我才知道,仅 2018 年,就有 31 名两院院士离开了我们。这怕是绝无仅有的。他们共和国的栋梁,他们的贡献深刻地影响着我们的生活,我深切缅怀他们。

除他们外,还有许多社会知名人士也相继逝世,他们许多人对我们的社会有着深刻的影响。联合国秘书长安南是一个小时候经常听到的名字,他所在的时候,也是联合国知名度最高的时候,他纵横捭阖,致力于解决国际争端,给全人类一个美好的世界。著名主持人李咏的去世也勾起了我对童年的回忆,在印象中,他是一个每天都很欢乐,手持锤子打电话砸金蛋的主持人,但从没想过他会这么快就离开这个世界。而他和哈林的爱情故事,更令我十分动容。除此之外,对文化影响很深的还有金庸、李敖和曾仕强。金庸无需我多言,小时候看的很多武侠小说和电视剧都出自他手,他告诉了我什么是江湖,也给我带来了无数乐趣。李敖和曾仕强是台湾的两位思想家。其中,曾仕强《易经》可能是百家讲坛最受欢迎的节目之一了,他讲的内容大多记不得了,但是他那种睿智而又儒雅的风格令我印象深刻。

最后,还有田家炳先生。田家炳先生在国内可能没有邵逸夫那么有名,但是他也捐助了无数学校,让这些学校可以改建校舍,购置教具,相互交流,提升教学质量。我所读的高中就是一所田家炳中学在上高中的时候,我们体验到了新的教学楼,用上了很高级的电子白板。而在我们毕业后,学校也将土操场进行了翻新,还新建了装备齐全的科学楼。这其中有很大程度上都是由于他老人家。作为一名田家炳中学的学生,听到他逝去的消息,我的心情相当沉痛。

而我们国家也正在经历“百年未有之大变局”。这一年随着贸易战开打,经济局势开始变得紧张起来。随着数字货币、互联网借贷和共享单车的迅速衰落,互联网产业也迎来了变数很大的一个时期。独角兽们即使股票纷纷破发,也要流血上市,而下半年来更是处处风传裁员浪潮。这一年注定是一场场艰难的战役。

The End

不论如何,时间是不会停下来等人的。现在,2018 年已经过去,2019 年已经到来。该面对的还是要面对,该承受的也要去承受。

上面所回忆的事情,不论好事坏事,不论是否开心,也只是生活中事情的一少部分,生活中更多的事情,是稀松平常,甚至结束以后就会忘记的。但是,这些事情其实才是生活的主旋律。倘若可以在这些事情之中发现美,生活也就会有更多的乐趣了。

2019 注定依旧是很有悬念的一年,不能守成,还需不断奋斗。加油吧!

解决IOS中微信浏览器软键盘弹出导致的若干Bug

作者 Jason Bian
2018年12月5日 11:24

问题 1 键盘弹起后会遮挡键盘上方的内容

在微信浏览器中,如果需要模拟一个类似微信聊天的窗口,那么一般情况下需要将输入框使用 fixed 定位放置在页面最下方。就像这样:

但是,在 IOS 中的虚拟键盘和 Android 里是不同的。在 IOS 中,虚拟键盘弹出以后,键盘上面的输入提示会比键盘弹出慢半拍,所以就会导致输入法的提示框将正常页面挡住的情况。

这时候,就需要在键盘弹出后,等待一段时间(几百毫秒),然后再将页面的滚动条进行调整,就可以让页面弹到键盘之上。

假设页面布局如下(使用了 Vue 框架),其中 Dialogues 组件是可以滚动的聊天内容,PageFooter 是使用 fixed 定位在页面底部的输入框:

1
2
3
4
5
6
7
8
9
10
<div id="index">
<div id="mainPanel">
<div id="dialogueContent">
<Dialogues></Dialogues>
</div>
</div>
<footer>
<PageFooter></PageFooter>
</footer>
</div>

然后,就可以在监听到软键盘打开事件(比如 dialogueContent 中 input 元素的 onclick 或者 onchange 事件)后,执行下面的语句,让 dialogContent 的滚动条向下滚动,这样里面的内容就不会被覆盖了。

1
2
3
4
5
6
7
let dialogueContent = document.querySelector('#dialogueContent')
setTimeout(function() {
dialogueContent.scrollTop = dialogueContent.scrollHeight
setTimeout(function() {
dialogueContent.scrollTop = dialogueContent.scrollHeight
}, 250)
}, 250)

至于为什么要触发两次呢,是因为 IOS 手机种类比较多,从五六年前 iPhone5s 到最新的 iPhoneXR 都有人在用,每种手机的响应速度也各不相同,有些手机可能在第一个 250ms 内还没有完成键盘弹出的工作,所以可以再加一个定时器来兼容旧的手机。

但问题也依旧存在,就是 fixed 定位的输入框在软键盘弹出以后就不会像 Android 一样固定在页面底部,而是可以上下滑动,类似于 absolute 定位。这时,可以将 body 设置为 absolute 定位,然后再将 MainPage 使用 absolute 定位于 body 底部。

问题 2 在虚拟键盘收起以后 body 定位的问题

紧接着,第二个问题就出现了。在点击虚拟键盘右上角的“完成按钮以后”,页面下方并没有回弹,而是定在了原来软键盘上方的位置,必须在页面上滑动两下才可以触发回弹。(特别是在大屏的 IOS 手机上)

大概是这样:

而在将 body 设置为 absolute 以后,情况更加离奇。页面 UI 是回弹了,但是触控事件响应的位置是没有回弹的,依旧是软键盘打开区域的上方。解决方法同样是必须在页面上方华东两下才能回弹。

大概是这样:

最开始的想法是去监听 resize 事件,如果软键盘收回,就强行调整 body 的高度,但是发现软键盘的弹出和收回并不会触发该事件,只得作罢。

最终,找到了一篇解决 类似问题的文章,才找到了解决方案。

大致思路就是在键盘收回以后,主动触发浏览器对页面的重绘操作。如何进行呢?只需要在监听到 onblur 事件以后,让页面滚动到原来的位置即可。

比如,组件模板中的 HTML 为

1
<input type="text" placeholder="发送内容" v-model="content" @blur="resizeWindow">

然后可以在 JS 的 methods 中添加一个函数:

1
2
3
resizeWindow() {
document.body.scrollTop = document.body.scrollTop
}

这样,输入框在失去焦点以后会触发页面的重绘,刚刚的问题就随之而解了。

《科技创新与创业》课程总结

作者 Jason Bian
2018年8月6日 17:02

intro

上学期同Thesharing以及 Stone 去北大旁听了将近一学期的《科技创新与创业》(课程网站:http://net.pku.edu.cn/dlib/pkuxstart/)。 这个课程由百度七剑客之一的雷鸣主持,邀请了很多行业内有名的企业家来讲课,几乎都是北大校友(感慨一下北大校友文化真的很棒)。 我想在这篇文章中总结一下他们所讲的一些能引起我思考的观点和内容,以及经过我提炼加工所得到的结论。

三角关系

这里的三角关系并不是指恋爱中的那种复杂关系,而是指一个行业中相互制约的几个要素之间的复杂关系。其中一种要素发生大的变化(一般是非连续性的),这个产业整体以及几个要素之间的相互关系也会随之发生变化,这往往预示着新机会的到来。

微博 CEO 来去之间举了一个例子——移动互联网中存在的三角关系:运营商、手机制造商和互联网公司。 这个三角关系中某个要素发生变革就会导致移动互联网行业的巨变,会有一波新的公司起来。比如运营商 4G 网络的普及,使得网速变得越来越快,流量变得越来越便宜,这就催生了短视频行业的兴起,这也催生了一系列的公司和产品,比如快手和抖音。

明略数据的吴明辉提出在大数据这个产业中也存在一个三角关系——人、数据源和场景。 数据源在未来物联网时代会发生很大的变化,数据世界中的主导位置可能会从原来的因特网中的数据变为物联网传感器中的数据,这对于很多公司来说是一个全新的机会。

我认为,这些三角关系代表了一个场景之下的几个利益方,某一个方面出现技术或者认知方面的突破,都会造成行业的重新洗牌。比如在虚拟现实产业链中,不仅仅有设备制造商和内容生产商,还会有运营商的机会,因为 VR 数据的传输需要大量的带宽。所以,5G 时代到来以后,VR 会不会又重新火起来呢?

产品方法论

很多嘉宾都不约而同地说到了俞军老师有关用户收益的产品方法论:

产品的价值(用户的收益)= 新体验 - 旧体验 - 迁移成本。

这条方法论十分重要,是衡量你做出的一个新产品是否可以干掉旧产品的一个关键因素(这条方法论也经常被SGanker提到)。

我认为这条方法论的基础是一个共识——真正的需求不是被创造出来的,而是来自人类的本性。你所创造的产品是使用了一个新的形式或者新的技术来包装这个古老的需求。所以,一定要想清楚,你在做这个产品的时候到底替代了谁,新的产品解决了什么旧的问题。

另外一个问题是如何区分真的需求和伪需求。这里 OFO 的戴威提供了一个方法——将这种需求通过英语 Need/Want 归类,如果是 Need,那么这个需求就是真的需求,如果是 Want,那么这个需求就是伪需求。

不仅仅要检查这个需求是真需求还是伪需求,更要思考这个需求是不是只有自己需要,还是你的朋友也需要,更或许是人人都需要。

这里也有一个技巧,就是把自己的产品和别人去讲解,看他们是否感兴趣。不过身边的人可能会担心得罪你,不愿意说你产品的坏话,所以可以要找陌生人后者敢于说真话的人来介绍你的产品,来了解这个需求到底是不是真需求。

顺势而为

说到顺势而为,我第一个想到的是雷军和他的顺为资本,以及他“风口上的猪”的理论。我想,他也是因为带着金山硬挺了这么多年才悟出的这个道理吧。我想,现在的小米就是他顺势而为的结果。 关于顺势而为,还有一句话我很喜欢,也想放在这里——“一个人的命运,不仅要看个人的奋斗,还要看历史的进程。”这句话从辩证唯物的立场讨论了为什么要顺势而为。

下面就来讨论一下如何顺势而为。

从更高的高度来看问题

微博 CEO 王高飞从很高的层次来分析了微博发展的时候遇到的许多问题以及解决方法。他和他的团队的思考更多的不是现在用户的需求,而是未来五年左右中国经济社会的发展可能会让某些用户的需求变得旺盛,微博就会在这里提前布局。而对于许多目前无法超越的竞争对手,微博选择了从大局考虑,不去正面竞争而更换别的赛道(关于赛道,下面还会提到)。

比如微博在 2012 年就判断了中国经济未来五年的趋势:比如移动互联网的增量更多来自于二三线城市。而微博当时发力的主要方向就是去做二三线城市的消费升级,上线了一系列针对这部分用户的产品。

但是这样的分析可能也会有所遗漏,比如微博没有想到五六线城市的下沉也会是一个机会,而这个机会造就了快手的极速增长。

从这些嘉宾的口中,我也大致总结了大家都认为未来可能会蓬勃发展的行业:

  • 首先还是互联网和 AI。互联网会和传统行业更深地结合(也就是互联网+),而 AI 也会改造更多的行业。
  • 另外一个风口是生物医药(我之前的确从未关注过)。由于生物医疗行业中基因组学和蛋白质学这些基础技术的突破,导致未来这个行业很可能会非常蓬勃地发展。现在,投资机构对于生物医药行业的投资已经是仅次于互联网行业的存在了。
  • 随着中国人口红利的消失,很多产品野蛮生长的机会已经不是很大了。而大家对于消费升级的追求导致会有更多的创新品牌诞生。
  • 人口红利消失也会导致 2B 的服务会越来越多。

所以如果要创业,那么一定要选择具有先发优势的行业,即判断未来几年的主力消费人群和用户增量会在什么地方。

最后需要提到的是,现在全面创业的热潮已经过去,很多创业公司纷纷死去,只有很少具有竞争优势的公司存活了下来。

赛道的选择十分重要

赛道理论是投资界的一个理论。他们将某一个细分的行业或领域称之为一个赛道。而这个赛道上会有许多类似的公司在竞争(很像在一起赛跑)。既然是比赛,那么肯定会有前几名,而投资机构就会投资头部的那些具有竞争优势的企。等这个赛道成熟以后,可怕的幂律就会发生作用,前几家公司会吃掉这个赛道中九成以上的市场,后面的公司几乎没有任何机会。

所以,选择一个自己可以具有竞争优势的赛道就显得十分重要了。关于不同赛道上的公司的信息,可以在一些咨询公司的网站上面看见(比如艾瑞咨询:http://www.iresearch.com.cn/)。

另外,你所选择的赛道要具有空间和时间上很强的成长性。有空间的成长性,行业的天花板高,大家有充足的空间赚到很多,才会有人愿意投入资本和时间将这个市场做大。有时间的成长性,说明你做的方向是正确的,等五年到十年以后,时间还是你的朋友。

市场、产品和技术之间的关系

因为身边同学多是技术出身,所以我接触到的很多人可能都认为技术对于一个公司而言最重要的。但事实可能并非如此——许多产品是由市场拉动而非技术推动的,一般情况下开发一个市场所花费的成本要远远大于技术。所以开发产品时应当从市场的痛点来着手,而非技术高低。

不仅市场比技术更重要,产品也比技术更重要。在设计产品的时候需要有用户视角,使用同理心去思考用户的感受,并培养用户使用产品时的参与感。这里好像又说到了产品方法论,既然说到了,不如再举一个例子:腾讯的 10/100/1000 法则。这个法则要求腾讯的产品经理每个月必须做 10 个用户调查,关注 100 个用户博客,收集并反馈 1000 个用户体验。

商业模式

一个好的受资本市场欢迎的商业模式需要四个要素:能赚钱(有人买单)、规模性(可复制)、有壁垒(不会被腾讯抄袭)和可持续(未来成长空间很大)。

更好的商业模式不仅考虑了自身的发展,还要考虑到产业链中整个链条的利益分配问题。

非连续性机会

驱动社会经济发展的核心要素是非连续性机会,只有抓住非连续性机会的公司才可能获得爆发式的发展,并且有机会获得垄断地位。

好公司

护城河理论

“护城河”理论是巴菲特提出的。他认为好的公司需要有一条护城河来避免来自外部的竞争。有以下几种创造护城河的方法:

  • 一种护城河是单一产品规模,公司拥有一个使用规模非常大的产品,以形成规模效应。互联网公司,如腾讯,ebay,沃尔玛等公司都属于这一类。
  • 另一种是知识产权,比如商标或者一些关键的技术专利。迪士尼、耐克等公司就属于这一类。
  • 最后一种是客户转用其他产品需要很高的成本。比如 Oracle 和微软。

几乎所有的好公司都在致力于建立护城河以获取垄断地位,最终占领用户的心智。

如何与大公司竞争

我一度认为类似“如果腾讯也开始做你们的产品怎么办?”这种问题是无解的。不过听完课以后,我的思路产生了变化。原因有三:

  • 首先,你做的业务有可能是巨头们看不上的不怎么赚钱的小业务,除非这个业务以后会成长成为一个巨无霸,那么巨头一般是没有精力或者成本来同你竞争的。但就是这种巨头看不上的业务可能能让你赚的盆满钵满,在未来的某个时间,有娱非连续性机会,这个业务也或许就会成为商业的主战场。
  • 其次,巨头往往都是上市公司,上市公司往往背负着到很多方面的利益。如果他们需要从赚钱的业务上将资源倾斜到其他需要和创业公司竞争的地方,那么他们的股东和员工都可能会不乐意,甚至股价也会下跌很多。所以,他们很多时候可能会选择投资或者收购而非直接竞争。
  • 最后,市场中一直都有许多资本,为了不贬值,它们必须被投资到有很大增值潜力的地方,大公司往往增长不会太大,而增长潜力很高的小公司却可以做到。所以资本市场会愿意把钱交给具有很大增值空间的小公司,和大公司去打的。所以,为了可以和大公司竞争,创业公司需要业务+资本的双轮驱动。不仅要做好自己的业务,也要积极向外寻求资本,这样才能有一息存活的机会。

垄断才能创造利润

能够创造价值的公司并不一定可以创造很好的价格。因为在尚未形成垄断的时候,市场上存在着很多竞争对手,博弈论一定会在这里发挥作用,导致你无法让产品或公司有一个很好的价格。

这里说的垄断不一定是产品的垄断,还可以是应用场景的垄断。场景垄断垄断的是消费者的心智。比如苹果公司的产品,虽然从来没有在市场上形成单一品类的垄断,但是它们加起来形成了一个生态系统,它们垄断了消费者的心智。苹果公司靠它获得了非常高的利润率。同样垄断也表现在股价上——苹果公司在前几天终于成为了地球上第一家市值突破万亿美元大关的公司。

这里我还可以用小米公司来举一个例子。小米公司的互联网手机模式在刚出来的时候,受到了众多用户的欢迎,增长非常迅速,表现在资本层面就是估值越来越高。但是随后随着荣耀、OV 等公司加入这个赛道,同小米形成了强力的竞争,小米模式出现了很多问题。小米公司的价值无疑是很高的,但是没有垄断。这种问题表现在股价上就是上市以后几乎两次破发,小米公司并没有得到和价值匹配的好的价格。但是它的生态链以及和用户建立信任关系的商业手段都是在为建立(不同于苹果模式的)新的垄断去做尝试。小米公司的模式究竟能走到何时何地,我们可以拭目以待。

创始人

先说一个结论,创始人的高度很大程度上决定了企业的高度。因为企业的文化、商业模式以及关键决策几乎都来自于创始人。不过创始人的高度也不是一成不变的,他们会随着公司的成长不断的学习和成长。

领导力

一家公司的创始人往往就是领导者。领导者和管理者有很大的区别。管理者只需要管理员工,按时按量完成任务即可。但是领导人需要通过他的很强的人格魅力来带领大家一起向前。所以领导人不仅需要做事,还需要做人,不仅要做事做人,更需要有很长远的目光。

做为领导者往往都是很孤独的。麦肯锡健康的樊琴还说,创业不仅孤独,而且几乎没有成就感。因为一切都是从零开始,而且一旦开始就永远没有尽头。

刚刚说过,创始人的高度很大程度上决定了企业的高度,所以创始人需要很强的学习能力,需要在企业成长的同时也不断学习,要和企业一同成长,甚至要比企业的成长还要迅速。但是,创始人不仅仅需要学习,还需要将他的决策力、执行力、组织力和感召力都输出给其他人,营造气氛,让大家一起干起来,也就是“使众人行”。

创始人的选择

OFO 的戴威给出了他选择创始人的思路。首先,公司只能有一个创始人。其次,创始人和联合创始人之间必须要比较知根知底(比如哥们),同时也最好可以兼具能力上的互补。

另外,价值观的统一也非常重要,创始人之间需要有强烈的相互认可,不然现在的兄弟可能会变成之后的友商。

最不易稳固的结构是只根据需要能力来选择一些不太熟悉的人,雷鸣认为这样的创始团队算是“草台班子”,而草台班子是迟早要散的。

投资人的选择

将好几位老师的话总结一下,就是:投资人不仅要选有钱的,还要选择除了钱以外可以给企业提供更多帮助的人。另外在敲定投资的时候,一些法律问题一定要了解清楚,不然可能会因为协议中的一些条款就让自己倾家荡产。

商业和社会成熟度

商业不是过家家,创始人需要很强的商业成熟度和社会成熟度。

商业成熟度主要反映在对商业本质的认识:比如有客户来源、商业模式、对竞争对手的认识和投资策略等等。创始人没有很好的商业成熟度,公司一定是无法存活的。

社会成熟度来源于步入社会以后对社会的认识,比如经验、能力、人脉和价值观。董小玲认为,这些成熟度在高校中是很难锻炼出来的,而在大学生走上社会的半年左右的时间里,会逐渐积聚。这个时候创业者既对社会有了清醒的认识,还没有忘记自己的理想,是创业的最佳时机。

家庭关系

令我诧异的是有很多创业者创业中断的原因不是融资失败、竞争对手或者政策问题等等来自外部的因素,而是因为家庭成员之间意见不一致的问题。这里的家庭成员多是另一半、自己的父母或者是另一半的父母。

因为创业面临了很大的不确定性,所以如果其他家庭成员(特别是另一半的家长)接受不了这种不稳定性而不同意你去创业,这无异于后院起火,创业很可能就会失败了。

这里董小玲提到了一个规律,就是如果父母对创业特别懂或者一点都不懂,这都好办,但是如果父母半懂不懂那可能麻烦就会大一些了。

所以如何平衡创业与家庭之间的关系,是往往被创业者忽略的一个很大的问题。

一些其他的话题

以下是嘉宾们对于一些热门话题的比较有趣的观点。

区块链

邓锋认为,对于区块链应该区分链圈和币圈。区块链解决了信任问题,也解决了价值重新分配的问题,而币圈都是骗子。

一个技术(比如区块链)的出现并不能颠覆一个行业,只能作为增量而存在。

数据

吴明辉认为数据是对世界的观察,帮助没有观察的人解决信息不对等的问题。它可以创造信任,降低决策成本,帮助决策者进行快速的决策。但另一个角度来看,数据不一定是真实的,因为它是主观的,它本身也没有任何价值。但是只要它存在,就可以创造信任,而通过信任就会产生很多的价值。

另外,历史的发展多是不连续的,而数据代表过去,所以过去的数据很难预测长期的未来,但是它可以预测短期的未来。

商业的本质也是在利用信息不对等来解决问题创造价值,如果利用数据来做生意,使得信息对等了,那么商业就不存在了。所以用数据做生意是商业中的一个悖论。

商业计划书

弘道资本的李晓光认为商业计划书的目的是为了获得投资,核心内容是你投资我可以赚大钱。商业计划书的质量决定了 VC 是否会找你进行面对面沟通。

商业计划书需要准备几种:五分钟版本、演示 PPT、完整计划书、未来财务预测(需要专业的财务模型)。

推荐书

课堂上有许多老师给出了推荐阅读的书目,我也在这里略作总结:

最后,使用戴威的一句话作为文章的结尾吧:

不要迷信于别人的经验和方法论,创业者应该在创业中不断尝试,找到属于自己的方法论。

使用Webpack-Dev-Server处理跨域请求

作者 Jason Bian
2018年4月17日 22:56

在前端调试的时候,跨域一直都是一个比较麻烦的问题,这个在之前的文章关于跨域问题的一个解决方法中其实已经讨论了一些可以使用的方法。

如果要使用 JSONP,第一是需要修改的地方比较多,而且也不太符合前端发展的大趋势,如果使用 CORS 的话并没有 application/json 类型。而且更重要的是这只是在前端调试时候的需求,并不是在上线以后的需求,所以对后端有太多的入侵也不好。

所以就有一个念想突然在大脑中闪过——加入有一个代理不就可以解决这个问题了?但是又想了一下写起来还挺麻烦,于是就被搁置了。 直到前几天 Stone 提到其实 webpack-dev-server 早就想到并且已经帮我们实现了。

于是,我就在一个 Vue 项目中进行测试,发现真的很赞,既可以本地 Server 热加载,还可以直接跨域调用远程 API,完美解决了之前遇到的所有问题。

接下来我简要介绍一下步骤(以一个 Vue 脚手架建立的 webpack 项目为例):首先检查build/webpack.dev.conf.js中是否有

1
proxy: config.dev.proxyTable,

这个配置项,如果被注释掉,请打开注释,如果没有,请加入到 devServer 对象中 然后在 config/index.js 中的 dev 对象中加入 proxyTable 配置项:

1
2
3
4
5
6
7
proxyTable: {
'/**': {
target: 'http://api.xxx.com',
changeOrigin: true,
secure: false
}
},

前面的键 /** 意思是代理所有请求,如果代理某些请求,可以将其改为诸如 /api 之类的字符串。

后面的 target 就是要代理到的网站,changeOrigin 的意思就是把 http 请求中的 Origin 字段进行变换,在浏览器接收到后端回复的时候,浏览器会以为这是本地请求,而在后端那边会以为是在站内的调用。

这样,通过这个简单的配置,就完美地解决了跨域的问题。

之后,在直接运行

1
npm run dev

的时候,就可以将测试前端中的 ajax 请求代理到后端服务器进行测试啦!

最后,贴上官方文档,具体的配置大家可以参考这里:
https://webpack.js.org/configuration/dev-server/#devserver-proxy

谈及自由与美的理想

作者 Jason Bian
2017年5月1日 23:19

这个周就要立夏了,在这个夏天我要战胜它,也战胜自己。

周末和老弟遛弯聊天,他跟我谈及一个严肃而单纯的问题:既然毒品能给人带来快感,那为什么不人人都去吸毒呢?

我假装成一副胸有成竹的模样,“好问题!不过先别讲这个,我要和你谈谈音乐。我还记得上次和你去国家大剧院听新年音乐会时候你的表现。”

为什么谈音乐呢?我无非是希望引导他去认识到音乐之美,而审美判断是“无利害而生愉悦”,从而认识到有比毒品更自由的获得快瑟之感的方式——艺术或者说是道德。

我几乎成功了。

“那么这种愉悦和那种愉悦有何区别?”这个话题绕不开道德。

“而且你刚刚在讲,美的理想和道德的理念是同一的。那么假设一个人没有(像我们世人一样的)道德,那么他是不是也无法感受到艺术之美,那么他只能从毒品之类的东西中获得物质的快感?而凭什么人人都有这样的道德呢?”

我再令他去想为什么国家和法律如此“暴力”:道德次序的维持需要暴力,暴力保证了次序,也保证了减少暴力。所以毒品和问题需要暴力解决。

“但是,道德又有什么意义呢?你用暴力去维系道德又有什么意义呢?”

“因为人需要自由。人向往自由。音乐可以让人获得自由,道德可以,而毒品不行!”

但自由又有什么意义呢?什么才是自由呢?

我有些歇斯底里,不过在五月份,话题也总是绕不开它。

“你说向往自由,但是佛陀获得了自由,所以他领会到了空。但是空,不会太无聊点了么?”

“所以你想要的意义不就是来自于欲望,这能让你不那么无聊。”

“但是欲望和自由是不是就矛盾了?”

所以人生大概就是痛苦的吧。

❌
❌