你好,我是海纳。
随着第12节课的完结,《编程高手必学的内存知识》的软件篇也就落下帷幕了。这段时间里,我和同学们进行了很多交流,看到有些人的留言和困惑,比如代码太多,不知道从哪里入手;再比如,有些代码需要的前置理论知识太多,不知道从哪里获取资料;还有感觉自己什么都想学,但又学得不深入等等。
其实这些问题,在我学习基础软件的道路上也曾经遇到过。所以做为过来人,我想和你分享一下这些年我学习基础软件的经验。我总结出了五条经验,和你一起探讨。
相信很多人都会有这样的困惑:在计算机知识上,我应该每一种都掌握一点,还是应该钻研一种呢?
我们知道,计算机相关的知识是十分庞大的,从芯片设计到图形渲染,从网络协议到编译优化,这些分门别类的知识,没有人能够完全掌握。正如庄子所说,知识是没有边界的,而且个人的精力是有限的,所以我们一定要选择最适合自己的道路去学习,切不可贪多务得。
对于年轻人来说,多尝试不同的方向,多了解不同的领域是必要的。但是在找到自己喜欢的方向后,还是要注重知识的深度,在一个领域重点深挖下去。
我们经常说既要注重知识的广度,又要注重知识的深度,要做一专多能的人才。但广度和深度是有区分的,人必须要先吃透一个领域,才能建立起自己的比较优势。
深度的建立,我建议你不必在所有的领域都精通,只要精通一个方向,就可以在团队中找到自己的位置,发挥独特的价值。
在建立起深度以后,就可以考虑一下拓宽广度了。这样你不仅可以更轻松地与其他领域的专家交流,提高工作效率,还能从全局把握软件的整体架构,为将来晋升到架构师做准备。
关于广度的建立,我建议你从自己的工作出发,先学习上下游的知识是最容易的。比如说,我最先开始学习操作系统内核,实现到文件系统的时候,我想实现自己的可执行文件,就需要了解编译器的知识,那么从操作系统过渡到编译器就是自然而然的。
总之,请找到你自己热爱的方向,然后把有限的精力投入到最有价值的地方。
工作两三年的程序员最爱问的问题是:我感觉现在每天都在写基本的增删查改,没有前途,我该怎么办?或者是:我觉得只有做操作系统和编译器才是真正的“软件开发”,我现在做的事情没有价值,我不想做了……像这样的问题,我经常会在微信上收到。
或许你也有类似的困惑,所以趁着这个机会,我来做一个统一的回答:在这世上,只有小程序员,没有小软件,不要好高骛远,你要永远记得路在脚下的道理。我见过太多喜欢拽名词的程序员,他们总觉得各种概念、专用名词和缩写说起来很有格调,而对于勤勤恳恳地实现业务需求、改进日常工作效率却兴趣缺缺。这种想法是不对的。
我有一个朋友,在一家互联网公司用Go语言写逻辑,他来问我说:“每天都在写增删查改,想学底层知识,又不知道如何下手”。我就问他:“既然你每天都用Go语言,应该对Go语言很熟吧,你能不能和我讲一下Goroutine怎么实现的?defer特性是怎么实现的?Go的GC算法与其他语言中的GC算法对比的优劣是什么样呢?”他支支吾吾很难回答上来。
其实,人对于熟悉的东西往往会熟视无睹,就像你每天都在使用的工具,如果别人随口一问,你未必能回答得上来。实际上,这些东西学习的门槛并不高,我们的工具链里有太多开源的软件,它们的源代码很容易获取,一些流行度比较高的工具,对它们的注释和解析更是唾手可得。所以只要你愿意留心,往深处挖掘,路就在你的脚下。
再来接着说我这个朋友,当时我给他的建议就是把Go语言吃透。比如说,可以先把所有的语法都熟练掌握,再去学习语言库的实现,然后可以进一步学习Docker和k8s的底层原理,再到Go的编译器实现和Go的内存管理等等高阶的知识。
通过这个故事,我想告诉你的是,虽然每个人的工作职责、学历背景和成长历程都不一样,但希望你一定不要好高骛远,只去贪图口嗨的快乐,而是应该去探索一条适合自己的成长之路,要脚踏实地。
我在评论区看到有同学说“内存知识学起来很难”,其实越是往计算机底层原理深挖,门槛就越高,学习的曲线也就更陡峭。当遇到瓶颈期的时候,锲而不舍就非常重要了。你不要怕慢,其实一天只需要搞懂一个小问题,取得一点点小进步就是可以的。
我曾经在学习Linux内核的时候,就一直有一个疑问:一个进程的空间是4G,那么简单计算一下,光是为了编码这4G的空间,就需要4M的页表,更不要说还有页目录表。这么看的话,启动一个进程至少就得4M的物理空间。那为什么不管是Windows还是Linux,启动一个hello world进程 ,我看到进程所占的内存资源都不大呢?
为了搞清楚这个问题,我就去翻看Linux内核代码。由于当时最新的内核版本是2.6,我就尝试从代码层面去理解,但这部分的代码太复杂了,我只能一点点地看,今天知道了mm_struct是干嘛的,明天搞懂了vm_area_struct……就这样一步步往前走。直到有一天,不知怎么,突然就把所有的关键点串了起来,页表设置申请分配的过程就全看明白了。
原来,页表是按需要分配的。看到结果我哑然失笑,花费了那么多时间去查看那么复杂的代码,最终得到的结果就这么简单?
但实际上,在这个过程中,我收获的决不是只有这个“愚蠢”问题的答案,而是我每天都坚持看一点代码,即使看不懂也在一点点坚持,一个结构一个结构地、一个函数一个函数地去分析,最终把内存管理的其他知识也融会贯通了。
金庸在《侠客行》里讲石破天练太玄经的过程,和我经历的这个过程真的很像,我建议你去看一看这本小说,相信你会对如何坚持学习有很大感悟。
总之,希望你记住,坚持不懈、锲而不舍才是学习底层知识的必备心理素质。
在这个课程里,我看到了很多留言,很多同学的思考是非常有启发性的,比如说,Keepgoing同学在第8节课的留言就总结得非常好。这个留言总结了三种重定位时机,真正把符号重定位搞明白了。明显看出他经过了很深入的思考,这时候他再去提问就真正问到本质了。
为什么我要专门讲这一点呢?主要是因为我们这个专栏聚焦于内存管理,它不可能面面俱到,所以在各个环节难免会有一些你觉得没有太搞清楚的地方。这时候,思考就显得非常重要了。
我在每节课留下的思考题,你一定要去认真思考,这样才能有所收获。即使你一时半会儿想不到正确答案,那也要先进入“愤”和“悱”的状态,然后再来提问,这样再有人对你进行一点“启”和“发”,你就能立即获得非常深刻的理解了。
如果我只是简单地告诉你问题的答案,而你缺少了思考的过程,那么你不光是难以深刻理解这个问题,甚至还有可能会误解我的意思,从而建立了错误的概念。这样的话,你学得越多,就错得越多了,这可能还不如不学。那么在碰到问题的时候,我们应该怎么去思考呢?
其实,思考的过程就是要多建立抽象思维、多举例和多类比。比如说,我们在学习内存分配算法的时候,一开始学习的就是实际具体的内存分配算法。我们了解到,按需分配会产生外部碎片,把内存分割成相同大小的页,页与页之间紧密排列,可以解决外部碎片,但不能避免内部碎片。
后来,我们又了解到,进程调度的过程也是一个对时间区间进行分配的过程,如果你清楚寄存器分配算法的话,你应该也能理解,它的本质是对变量生命周期,进行时间区间的分配。
这样我们就通过多个领域的学习,建立起了一种更高的抽象思维:这些问题的本质都是区间的分配和管理。
在这种更高级的思维下,我们再去学习空闲链表、伙伴系统和线段树等用于区间管理的数据结构和算法,你就能触类旁通、事半功倍了。比如Slab分配器,虽然我们这个课程里没有提到过,但你从区间管理的视角去看,它不过就是类似于页管理那样,把空间分割成更小的同等大小的多块空间而已,一种大小适应一种内核数据结构。
建立了这种抽象思维,当你下次再遇到类似的问题,就能举一反三了。
学与思的有机结合,我相信你从中学时代就已经非常熟悉了,这里我还是想结合具体的例子再讲一下如何去运用“学”和“思”。
当你了解了一个现象或者一个API的用法,想要知道它的原理的时候,不要直接就去翻代码。这时候,我建议你停下来思考一下,如果这个功能让你来设计,你会怎么做?
当你把每个环节都设计得差不多了,再去源代码里验证,看看别人的设计和你的设计是不是相同的。如果差别比较大,你就可以对比两种设计,找出它们各自的优缺点,同时思考自己设计的差距在哪里。如果你的设计比较好,那就正好是一个可以贡献开源,提升个人影响力的好机会了。
我之前在研究Linux信号机制的时候,就是先停下来想:PCB中肯定要有信号相关的数据结构,内核提供signal系统调用肯定还要有能力,根据ID号找到相应的PCB,然后把它里面与信号相关的标志置位,那目标进程什么时候能处理这个信号呢?
经过思考,我猜想:因为信号的处理都是在内核中的,所以放在中断处理的前面或者后面是比较合理的,毕竟中断才是用户态和内核态切换的时机。
想清楚了这些以后,我再去翻看代码,在我猜想的地方,果然都找到了相关的数据结构和函数。其实,如果你稍加留意就会发现,我们专栏的第10节课就是按照这种思路去写的:学思结合,思在前,学在后。这也是我学习基础软件的基本思路。我希望你在接下来的课程学习中,可以多去注意体会。
好了,以上就是我想和你分享的内容,我向你大概介绍了学习计算机底层原理和基础软件的几条注意事项,希望能对你有所帮助。我们下节课再见!
评论