2024年Go最新如何快速读懂开源代码?_怎么读懂软件代码,一起刷完了这份1307页的Golang面试宝典吧

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

	- [4.3 数据结构和算法](#43__886)
	- [linus说:烂程序员关心的是代码,好程序员关心的是数据结构以及他们的关系。](#linushttpswwwzhihucomsearchqlinussearch_sourceEntityhybrid_search_sourceEntityhybrid_search_extrasourceType3Aanswer2CsourceId3A2562876884_894)
+ [五、划重点](#_898)
+ - [安装运行:按照相关文档,安装运行项目:](#_902)
	- [系统测试](#_909)
	- [关键学习](#_918)
+ [六、一些建议](#_931)
+ [选择阅读对象](#_1065)
+ [选择合适工具](#_1073)
+ [从入口开始](#_1084)
+ [结构与动手](#_1092)
+ [修改代码测试](#_1102)
+ [第一:阅读前的准备](#_1134)
+ [第二:从最简单的源码开始](#_1142)
+ [第三:先让项目跑起来](#_1154)
+ [第四:明确阅读方向](#_1160)
+ [第五:带着问题阅读](#_1171)
+ [第六:学习源码风格](#_1184)
+ [小结:](#_1198)

最近刚好在读Python部分源码,这几条建议一定能帮你更好的阅读开源项目

不管是阅读公司项目代码还是开源代码,都不要一开始一头扎进代码里,试图通读源码,这绝对会陷入无止境的细节。

建议先找项目的架构设计和整体流程框架的文档来看,先从顶层设计入手,掌握这个软件整体结构,它有哪些功能特性,它涉及到的关键技术、实现原理等。

接下来才是逐渐厘清模块、函数之间关系,调用图。

这个时候不建议深入看每个函数,只需要做到对整体流程有所把控即可。

RUN起来

真正去阅读的时候,最好的方式,先按着程序员正常的方式DEBUG执行跟踪一遍,也就是先跑起来,去搭建跑起来的环境这个过程也是会让你熟悉这个项目所依赖一些东西。

调试

调试一般跟找Bug差不多,两种手段:

  • 打日志
  • 断点

这两种有些适用场景不同,可以互相结合着使用,比如我最近在看cpython源码中,关于对象的那部分,那我就可以通过修改PyObject_Print函数,然后在其它地方调用,打印出运行时对象的一些我关注的信息。

img

把控关键数据结构和函数

我们说“程序=算法+数据结构”,我实际的体会是数据结构用得更多,其实不少开源项目也大多是if、else堆起来的,算法只会用到最核心的地方,但是数据结构会到处都需要。

我们可以把项目中一些核心的函数、类、数据结构逐一拿下,因为这些关键的代码往往是理解项目细节的关键。

从小的开始

最好是一些「小而美」的代码,小是指代码量少,最好就几千行,这样我们能够充分把控,美则是指代码实现写得很优雅。

我们都知道提高审美能力很重要的一环就是多看美的东西,好的设计。

那写代码也是一样的,自己学完基本语法,写出来的代码大概停留跑起来的水平。

这时候去看看大佬写的代码,你绝对会惊呼 卧槽,还能这样?

这样的代码有哪些呢?

如果你是大一的同学,我推荐你去看看 Linux 内核中关于链表的实现,简直特么太妙了,平常我们定义链表不都是这样嘛

struct Node {
   int data;
   struct Node\* next;
}

但是这个存在的问题就是,每个想用链表存储的结构体,我们都得去写一遍,遍历、插入、删除的逻辑,显然太low了。

那 Linux kernel 中就用宏和指针变换,在对用户自定义数据结构侵入性很小的情况下,实现了其它语言中模板的功能。

相信我,看完,你会学到很多骚操作的,后面找机会写一下这块。

又比如学完 C 语言,你会不会觉得 C 中的字符串有点不灵活,那推荐你去看看 Redis 中关于动态字符串的设计–SDS。

这些代码都不长,也不难理解,但是看完对于提升我们的编码能力是有很大帮助的,这就是小而美。

提倡看源码,不是让你直接上手就啃什么 Nginx、Sqlite、Redis的源码,这些都是几万级别的代码量,一般初学者

即使要啃,我给你说个好办法,这些东西目前来看,代码量都是几万几十万的级别,有点不友好,你可以去 github fork 下来。

然后回退到第一个 commit,从第一个 commit 开始看起,看到第一个完整的版本。

也不一定非要第一个commit,反正大概理念就是尽可能看早期的具备了核心功能的版本,因为很多项目其实都是在不断完善、添加特性,但是最核心的一定是第一个MVP的实现。

这个过程也是能学到不少东西的,看看罗马都是怎么建造成的。

关注一个模块

比如我最近在研究「对象」,就去看了下Python中对象的实现,那对我最重要的就是object.c和object.h这些相关的文件,其它诸如parser、runtime就不是我关注的重点,我也不可能挨着挨着把python源码看完。

我是想通过看源码学习现代语言中对象的实现,那么就只需要关注与这部分相关的代码。

特别是开源项目每个文件上方,都有一个概要性的描述,我推荐把这样的注释看了,有助于帮助我们理解设计思路的大方向。

img

工具

工欲善其事必先利其器,看代码一定少不了到处跳转,这部分可以找个跳转方便的现代IDE就可以了。 比如 IDEA、Pycharm、Vscode、source insight 之类都可以。




为了帮助题主消除阅读开源代码的恐惧感增强信心,我先说点理论的东西,再通过几个案例来分下一下如何快速读懂开源代码,希望对你有帮助。

想在技术上有所造诣或者想成为某一技术领域的专家的同学一定要认认真真的研读几个开源项目的源码。下面我会具体来展开说下这个问题。

一、阅读开源代码存在的一些误区

部分同学阅读源码存在一些不当的习惯或者偏颇的认知方式。

比如,一些同学阅读源码其实是随波逐流的,今天有人推荐阅读 A 项目的源码,他就去阅读 A 项目的源码,明天有人推荐阅读 B 项目的源码,他就去阅读 B 项目的源码。天下源码何其多呀,找到自己感兴趣的或者对自己有用的,不要随波逐流,适合别人的不一定适合你。

有些人阅读源码非要满足了"天时地利人和"才会去阅读。例如,有些人觉得自己不懂网络编程,所以就不方便阅读 Nginx 的源码,有些人听别人说阅读某个项目的源码前必须先做 XX,而自己又不熟悉 XX,所以就放弃了阅读该项目。或者觉得当下时机不适合阅读某个项目的源码。再或者在阅读几个源码文件或者模块的代码时,因为看不懂就放弃了。其实这些做法都不可取,任何源码和你刚进入公司去接触一个新的业务项目的源码一样,只要慢慢熟悉,在这过程中针对性的补缺补差,坚持下来总会有所收获的。尤其是对那些走上工作岗位的读者来说,成年人的世界事情那么多,此生余年应该不会再有什么时间可以同时满足"天时地利人和"了吧。

代码的质量高低是相对的,不要因为一些项目的源码质量低或者不符合你的 style 就放弃。大多数完整的项目代码总有其可取之处,要学会吸取其有用之处。举个例子,很多做 Windows C++ 客户端开发的同学,应该会在网络的各个地方看到很多人抨击 MFC 的,然后一堆建议不要学习 MFC 的。从我个人的经历和感受来看,MFC 的源码还是很值得做 Windows C++ 客户端的同学学习的,尤其是其设计思想。当然,MFC 之所以被很多人抨击,是因为其臃肿笨拙,这有很多历史原因,MFC 不仅封装 Windows 界面逻辑那一套,同时实现了一套常用软件文档、视图模型的程序框架结构,同时自己实现了一套 STL 相关功能,以及其他一些常用功能(如对象的序列化和反序列化)。这些设计思想都被后来的各种软件框架借鉴和继承,例如 QT 和 Java 中的序列化和反序列化。一个开发者如果想成为架构师,其心中一定要对某个场景有一套可行的技术方案,如果你经验不足或者水平不够,拿不出来这样的方案,那就去借鉴和学习这些开源的软件。 而不是只会抨击这些软件源码的缺点,而自己又无更好的解决方案。旧的方案虽然不好,但是我们需要去学习、熟悉,只有熟悉了之后,我们才能基于其去改造和优化。

二、阅读代码的心态

阅读代码的心态,很重要。

个人觉得,一个技术人员如果想通过源码去提高自己,应该以一种"闲登小阁看新晴"的心境去阅读源码,这也许是在某个节假日的清晨,某个下过雨的午后,某个夜黑人静的深夜。看源码尤其是看高质量源码本来就是一种享受,像品茗。闲暇时间去细细品味一些开源软件的源码,和锻炼身体一样,都是人生中重要不紧急的事情,这类事情做的越多,坚持的越久,越能提高你的人生厚度。虽然阅读源码的最终目的是功利性的,但是阅读源码的心态不建议是功利性的,喜欢做一件事本身的过程,比把这件事做好的目标更快乐。

我从学生时代开始,就喜欢看一些开源软件的源码,当然,从现在的标准来看,看的很多源码都不是"高质量"的,择其善者而从之其不善者而改之,不是吗?有些源码可以学习其架构、结构设计,有些源码则可以学习其细节设计(如变量命名、编码风格等)。

看过的这些源码对我的技术视野影响很大。我上大学的时候,迷恋 Flash 编程,当时非常崇拜 Flash 界的两位前辈——鼠标炸弹(https://mousebomb.org/)和寂寞火山(现在已成币圈有名的大佬),另外还有淘沙网的沙子。多年后再看他们的代码可能质量没有那么高,但是我从他们开源出来的代码中学到了很多东西。举个例子,我喜欢在一些成对结束的花括号后面加上明显的成对结束的注释就是从沙子的代码那里学来的。虽然,现在的 IDE 会清楚的标示出来各个花括号的范围,但是这种注释风格在某些时候大大方便了代码阅读和 review。

//实例
class A
{
public:
    void someFunc()
    {
        for (int i = 0; i < 10; ++i)
        {
            for (int j = 0; j < 100; ++j)
            {
                //some codes...
            }// end inner-for-loop  
        }// end outer-for-loop
    }// end method someFunc
}; // end class A

三、阅读源码与辅助材料

大家都知道,时下"知识付费"这个词非常火热,各大平台各个领域都推出了许多基于知识付费的课程,有图文版、语音版和视频版(包括在线实时教育直播)。当然,知识付费是一个好东西。众所周知,如今的互联网信息的特点是信息量大、有用信息少、信息质量良莠不齐,各大平台推出的各种付费课程,精心制作,用心分类和梳理,读者只要花一定的费用,就能省去大量搜索、查找和遴选信息的时间,直接专注于获得相关知识本身。

在各类知识付费课程中,有一类课程是介绍业界或者大家平常工作中用到的一些开源软件的原理的,进一步说,有的是分析这类软件的源码的,如 Nginx、Netty、Spring Boot。

我个人觉得,虽然你可以购买一些这样那样的开源软件的教程或者图书(包括电子书)去学习,但一定不要以这些学习材料为主要的学习这些开源软件的方法和途径,有机会的话,或者说你想要学习的开源软件所使用的开发语言正好是你熟悉或者使用的编程语言,那么你应该尽量多去以阅读这些开源项目的源码本身为主。举个例子,如果你是 C/C++ 后端开发者,那么像 Redis、Nginx(它们都是使用 C 编写的)这样的开源项目的源码你应该认真的去研读一下;如果你是做 Windows C/C++ 客户端或者一名 QT 客户端开发人员,那么像 MFC、DUILIB、金山卫士等源码,你可以拿来读一读;如果你是 Java 程序员,Netty、Spring 等源码是你进阶路上必须迈过去的一关。

为什么建议以阅读相关源码为主,而不是其他相关教程呢?

首先,任何其他相关教程介绍的内容都是基于这个软件的源码实现创作出来的,虽然能帮助你快速理解一些东西,但是不同的教程作者在阅读同样一份代码时的注意点和侧重点不一样,加上如果作者在某些地方有理解偏差的,这种偏差会被引入你所学习的教程或者图书里面,也就是说,你学习的这些东西其实不是第一手的,而是经过别人加工或者理解意译过的,在这个过程中如果别人理解有偏差,那么你或多或少的会受一点影响。所以,为了"不受制于人”,亲自去阅读一些源码是非常有必要的。

其次,如果你按照别人的教程大纲,那么你在学习该软件的开源项目时,可能会受限于别人的视野和侧重点,通俗的说,假设一个开源项目其可以学习和借鉴的内容有 A、B、C、D、E 五个大的点,别人的教程可能只写了 A、B、C、D 四个点,如果你只局限于别人的教程,你就错过 E 这个点了。

这里我举一个具体的例子。我刚开始工作时做的是 C/C++ 客户端开发,我无意中找到了一份完整的电驴源码,但是开始阅读这份代码比较吃力,于是我就在网上找相关的电驴源码分析教程来看。但是呢,网上的这方面的教程都是关于电驴的网络通信模块和通信协议介绍的,很多做客户端的读者是知道的,做客户端开发很大一部分工作是在开发 UI 界面方面的逻辑和布局,其实电驴源码中关于界面设计逻辑写的也是很精彩的,也非常值得当时的我去借鉴和学习。如果我只按照网上的教程去学习,那么就错过这方面的学习了。也就是说,同样一份电驴源码,不同的学习者汲取的其源码中的营养成分是不一样的。

电驴源码链接:

链接:https://pan.baidu.com/s/1DifGKQyAKawvQ80hB3CTgA
提取码:u6p2

img

在 Visual Studio中调试学习电驴源码

四、如何阅读开源代码

说了这么,那如何去阅读源码呢?我这里介绍三种方式。

第一种方式就是所谓的精读和粗读。

很多读者应该听说过这种所谓的阅读源代码的方式,有些观点认为有些源码只需要搞清楚其主要结构和流程就可以了,而另外一些源码需要逐行认真去研读其某个或者某几个模块的源码,或者,只阅读自己感兴趣或者需要的模块。

第二种方式,说的是先熟悉代码的整体结构,再去依次搞清楚各个模块的代码细节,学会记录。

一边学习代码一边记录,是不错的学习方法。

有些同学喜欢给开源代码加上详细的注释,然后分享出来,方便后来自己或者他人阅读。还有的同学会写一些开源项目的源码解析类文章。

数年前,我当时为了学习网络编程和 C++11 新语言特性,利用工作闲暇时间去阅读蘑菇街开源的即时通讯软件 TeamTalk:

https://github.com/balloonwj/TeamTalk

我写了十一篇关于 TeamTalk 源码分析的专栏文章:

TeamTalk源码分析(一)-- TeamTalk介绍

TeamTalk源码分析(二) – 服务器端的程序的编译与部署

TeamTalk源码分析(三) – 服务器端的程序架构介绍

TeamTalk源码分析(四) – 服务器端db_proxy_server源码分析

TeamTalk源码分析(五) – 服务器端msg_server源码分析

TeamTalk源码分析(六) – 服务器端login_server源码分析

TeamTalk源码分析(七) – 服务器端msf源码分析

TeamTalk源码分析(八) – 服务器端file_server源码分析

TeamTalk源码分析(九) – 服务器端route_server源码分析

TeamTalk源码分析(十) – 开放一个TeamTalk测试服务器地址和几个测试账号

TeamTalk源码分析(十一) —— pc客户端源码分析

TeamTalk 服务端网络拓扑图

img

客户端运行截图:

img

img

img

第三种方式是所谓的调试法,通过开源项目的一个或几个典型的流程,去调试跟踪信息流,然后逐步搞清楚整个项目的结构。

我之前在携程旅行网做基础架构时,为了学习 Redis,我一边研究 Redis 的源码,一边编写了利用 GDB 调试 Redis 的教程。

调试是学习开源项目非常好用的一个方法。对于做 Linux C++ 开发一定要会用 GDB 调试 C/C++ 程序。熟练掌握 gdb 调试等于拥有了学习优秀 C 和 C++ 开源项目源码的钥匙,只要可以利用 gdb 调试,再复杂的项目,在不断调试和分析过程中总会有搞明白的一天。

我当时写这套教程有两个初衷:

  • 网上很多关于 gdb 的教程都是零散的,不成体系;
  • GDB 用来教学的调试的都是各种玩具型程序,看完之后很多读者还是不知道如何利用 GDB 调试大型 C/C++ 项目。

因此我结合自己的工作经验,写了一套《gdb 高级调试实战教程》,这个教程有如下特点:

  • 以调试开源项目 Redis-Server 为例,项目不是玩具型的,具有实战意义;
  • 按调试流程,从 gdb 附加调试程序,到启动 gdb 调试再到使用 gdb 中断 Redis 查看各种状态,循序渐进地介绍各种 gdb 调试命令;
  • 介绍了实际工作中 gdb 的各种高级调试技巧,例如如何显示超长字符串、如何使用 gdb 调试多进程程序等等;
  • 也介绍了基于 gdb 的一些高级工具,如 cgdb、VisualGDB,这些章节是为不习惯 gdb 显示源码方式的同学量身定制。

电子书目录如下图所示:

img

img

img

《gdb 高级调试实战教程》电子书下载链接:

链接: https://pan.baidu.com/s/1fS8571m6KYcR4tQ05Lbkgw 提取码: wiw3

相关的配套资源:

链接:https://pan.baidu.com/s/1f4Y275wEhljVK1-ChEdUkw 提取码:snwb

再比如,后来我创业了,我们的项目需要用到 Nginx,另外一点就是很早就听说 Nginx 的性能非常高,使用非常广,我一直也想找个时间去系统地研究一下 Nginx 的源码,例如 Nginx 多进程模式是如何设计的、反向代码是如何实现的等等。我学习 Nginx 源码仍然是调试大法。

注意:Nginx 的功能点比较多,涉及到的新概念和设计思路对于新手也不是特别友好,我建议在了解Nginx 的一些基本用法之后,再通过调试来学习 Nginx 源码。

1 下载 Nginx 源码

从 Nginx 官网下载最新的 Nginx 源码,然后编译安装(回答此问题时,nginx 最新稳定版本是 1.18.0)。

 ## 下载nginx源码
 [root@iZbp14iz399acush5e8ok7Z zhangyl]# wget http://nginx.org/download/nginx-1.18.0.tar.gz
 --2020-07-05 17:22:10--  http://nginx.org/download/nginx-1.18.0.tar.gz
 Resolving nginx.org (nginx.org)... 95.211.80.227, 62.210.92.35, 2001:1af8:4060:a004:21::e3
 Connecting to nginx.org (nginx.org)|95.211.80.227|:80... connected.
 HTTP request sent, awaiting response... 200 OK
 Length: 1039530 (1015K) [application/octet-stream]
 Saving to: ‘nginx-1.18.0.tar.gz’
 
 nginx-1.18.0.tar.gz                            100%[===================================================================================================>]   1015K   666KB/s    in 1.5s    
 
 2020-07-05 17:22:13 (666 KB/s) - ‘nginx-1.18.0.tar.gz’ saved [1039530/1039530]
 
 ## 解压nginx
 [root@iZbp14iz399acush5e8ok7Z zhangyl]# tar zxvf nginx-1.18.0.tar.gz
 
 ## 编译nginx
 [root@iZbp14iz399acush5e8ok7Z zhangyl]# cd nginx-1.18.0
 [root@iZbp14iz399acush5e8ok7Z nginx-1.18.0]# ./configure --prefix=/usr/local/nginx
 [root@iZbp14iz399acush5e8ok7Z nginx-1.18.0]make CFLAGS="-g -O0"
 
 ## 安装,这样nginx就被安装到/usr/local/nginx/目录下
 [root@iZbp14iz399acush5e8ok7Z nginx-1.18.0]make install

注意:使用 make 命令编译时我们为了让生成的 Nginx 带有调试符号信息同时关闭编译器优化,我们设置了"-g -O0"选项。

2 调试 Nginx

可以使用如下两种方式对 Nginx 进行调试:

方法一

启动 Nginx:

 [root@iZbp14iz399acush5e8ok7Z sbin]# cd /usr/local/nginx/sbin
 [root@iZbp14iz399acush5e8ok7Z sbin]# ./nginx -c /usr/local/nginx/conf/nginx.conf
 [root@iZbp14iz399acush5e8ok7Z sbin]# lsof -i -Pn | grep nginx
 nginx      5246            root    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)
 nginx      5247          nobody    9u  IPv4 22252908      0t0  TCP *:80 (LISTEN)

如上所示,Nginx 默认会开启两个进程,在我的机器上以 root 用户运行的 Nginx 进程是父进程,进程号 5246,以 nobody 用户运行的进程是子进程,进程号 5247。我们在当前窗口使用gdb attach 5246命令将 gdb 附加到 Nginx 主进程上去。

 [root@iZbp14iz399acush5e8ok7Z sbin]# gdb attach 5246
 ...省略部分输出信息...
 0x00007fd42a103c5d in sigsuspend () from /lib64/libc.so.6
 Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-72.el8_1.1.x86_64 libxcrypt-4.1.1-4.el8.x86_64 pcre-8.42-4.el8.x86_64 sssd-client-2.2.0-19.el8.x86_64 zlib-1.2.11-10.el8.x86_64
 (gdb)

此时我们就可以调试 Nginx 父进程了,例如使用 bt 命令查看当前调用堆栈:

 (gdb) bt
 #0  0x00007fd42a103c5d in sigsuspend () from /lib64/libc.so.6
 #1  0x000000000044ae32 in ngx_master_process_cycle (cycle=0x1703720) at src/os/unix/ngx_process_cycle.c:164
 #2  0x000000000040bc05 in main (argc=3, argv=0x7ffe49109d68) at src/core/nginx.c:382
 (gdb) f 1
 #1  0x000000000044ae32 in ngx_master_process_cycle (cycle=0x1703720) at src/os/unix/ngx_process_cycle.c:164
 164             sigsuspend(&set);
 (gdb) l
 159                 }
 160             }
 161
 162             ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "sigsuspend");
 163
 164             sigsuspend(&set);
 165
 166             ngx_time_update();
 167
 168             ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
 (gdb)

使用 f 1 命令切换到当前调用堆栈#1,我们可以发现 Nginx 父进程的主线程挂起在src/core/nginx.c:382处。

此时你可以使用 c 命令让程序继续运行起来,也可以添加断点或者做一些其他的调试操作。

再开一个 shell 窗口,使用gdb attach 5247将 gdb 附加到 Nginx 子进程:

 [root@iZbp14iz399acush5e8ok7Z sbin]# gdb attach 5247
 ...部署输出省略...
 0x00007fd42a1c842b in epoll_wait () from /lib64/libc.so.6
 Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-72.el8_1.1.x86_64 libblkid-2.32.1-17.el8.x86_64 libcap-2.26-1.el8.x86_64 libgcc-8.3.1-4.5.el8.x86_64 libmount-2.32.1-17.el8.x86_64 libselinux-2.9-2.1.el8.x86_64 libuuid-2.32.1-17.el8.x86_64 libxcrypt-4.1.1-4.el8.x86_64 pcre-8.42-4.el8.x86_64 pcre2-10.32-1.el8.x86_64 sssd-client-2.2.0-19.el8.x86_64 systemd-libs-239-18.el8_1.2.x86_64 zlib-1.2.11-10.el8.x86_64
 (gdb)

我们使用 bt 命令查看一下子进程的主线程的当前调用堆栈:

 (gdb) bt
 #0  0x00007fd42a1c842b in epoll_wait () from /lib64/libc.so.6
 #1  0x000000000044e546 in ngx_epoll_process_events (cycle=0x1703720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:800
 #2  0x000000000043f317 in ngx_process_events_and_timers (cycle=0x1703720) at src/event/ngx_event.c:247
 #3  0x000000000044c38f in ngx_worker_process_cycle (cycle=0x1703720, data=0x0) at src/os/unix/ngx_process_cycle.c:750
 #4  0x000000000044926f in ngx_spawn_process (cycle=0x1703720, proc=0x44c2e1 <ngx_worker_process_cycle>, data=0x0, name=0x4cfd70 "worker process", respawn=-3)
     at src/os/unix/ngx_process.c:199
 #5  0x000000000044b5a4 in ngx_start_worker_processes (cycle=0x1703720, n=1, type=-3) at src/os/unix/ngx_process_cycle.c:359
 #6  0x000000000044acf4 in ngx_master_process_cycle (cycle=0x1703720) at src/os/unix/ngx_process_cycle.c:131
 #7  0x000000000040bc05 in main (argc=3, argv=0x7ffe49109d68) at src/core/nginx.c:382
 (gdb) f 1
 #1  0x000000000044e546 in ngx_epoll_process_events (cycle=0x1703720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:800
 800         events = epoll_wait(ep, event_list, (int) nevents, timer);
 (gdb)

可以发现子进程挂起在src/event/modules/ngx_epoll_module.c:800的 epoll_wait 函数处。我们在 epoll_wait 函数返回后(src/event/modules/ngx_epoll_module.c:804)加一个断点,然后使用 c 命令让 Nginx 子进程继续运行。

 800         events = epoll_wait(ep, event_list, (int) nevents, timer);
 (gdb) list
 795         /* NGX_TIMER_INFINITE == INFTIM */
 796
 797         ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
 798                        "epoll timer: %M", timer);
 799
 800         events = epoll_wait(ep, event_list, (int) nevents, timer);
 801
 802         err = (events == -1) ? ngx_errno : 0;
 803
 804         if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
 (gdb) b 804
 Breakpoint 1 at 0x44e560: file src/event/modules/ngx_epoll_module.c, line 804.
 (gdb) c
 Continuing.

接着我们在浏览器里面访问 Nginx 的站点,我这里的 IP 地址是我的云主机地址,读者实际调试时改成自己的 Nginx 服务器所在的地址,如果是本机就是 127.0.0.1,由于默认端口是 80,所以不用指定端口号。

 http://你的IP地址:80
 等价于
 http://你的IP地址

此时我们回到 Nginx 子进程的调试界面发现断点被触发:

 Breakpoint 1, ngx_epoll_process_events (cycle=0x1703720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:804
 804         if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
 (gdb) 

使用 bt 命令可以获得此时的调用堆栈:

 (gdb) bt
 #0  ngx_epoll_process_events (cycle=0x1703720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:804
 #1  0x000000000043f317 in ngx_process_events_and_timers (cycle=0x1703720) at src/event/ngx_event.c:247
 #2  0x000000000044c38f in ngx_worker_process_cycle (cycle=0x1703720, data=0x0) at src/os/unix/ngx_process_cycle.c:750
 #3  0x000000000044926f in ngx_spawn_process (cycle=0x1703720, proc=0x44c2e1 <ngx_worker_process_cycle>, data=0x0, name=0x4cfd70 "worker process", respawn=-3)
     at src/os/unix/ngx_process.c:199
 #4  0x000000000044b5a4 in ngx_start_worker_processes (cycle=0x1703720, n=1, type=-3) at src/os/unix/ngx_process_cycle.c:359
 #5  0x000000000044acf4 in ngx_master_process_cycle (cycle=0x1703720) at src/os/unix/ngx_process_cycle.c:131
 #6  0x000000000040bc05 in main (argc=3, argv=0x7ffe49109d68) at src/core/nginx.c:382
 (gdb) 

使用 info threads 命令可以查看子进程所有线程信息,我们发现 Nginx 子进程只有一个主线程:

 (gdb) info threads
   Id   Target Id                                Frame 
 * 1    Thread 0x7fd42b17c740 (LWP 5247) "nginx" ngx_epoll_process_events (cycle=0x1703720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:804
 (gdb) 

Nginx 父进程不处理客户端请求,处理客户端请求的逻辑在子进程中,当单个子进程客户端请求数达到一定数量时,父进程会重新 fork 一个新的子进程来处理新的客户端请求,也就是说子进程数量可以有多个,你可以开多个 shell 窗口,使用 gdb attach 到各个子进程上去调试。

然而,方法一存在一个缺点,即程序已经启动了,我们只能使用 gdb 观察程序在这之后的行为,如果我们想调试程序从启动到运行起来之间的执行流程,方法一可能不太适用。有些读者可能会说:用 gdb 附加到进程后,加好断点,然后使用 run 命令重启进程,这样就可以调试程序从启动到运行起来之间的执行流程了。问题是这种方法不是通用的,因为对于多进程服务模型,有些父子进程有一定的依赖关系,是不方便在运行过程中重启的。这个时候方法二就比较合适了。

方法二

gdb 调试器提供一个选项叫 follow-fork,通过 set follow-fork mode 来设置:当一个进程 fork 出新的子进程时,gdb 是继续调试父进程(取值是 parent)还是子进程(取值是 child),默认是父进程(取值是 parent)。

 # fork之后gdb attach到子进程
 set follow-fork child
 # fork之后gdb attach到父进程,这是默认值
 set follow-fork parent

我们可以使用 show follow-fork mode 查看当前值:

 (gdb) show follow-fork mode
 Debugger response to a program call of fork or vfork is "child".

我们还是以调试 Nginx 为例,先进入 Nginx 可执行文件所在的目录,将方法一中的 Nginx 服务停下来:

 [root@iZbp14iz399acush5e8ok7Z sbin]# cd /usr/local/nginx/sbin/
 [root@iZbp14iz399acush5e8ok7Z sbin]# ./nginx -s stop

Nginx 源码中存在这样的逻辑,这个逻辑会在程序 main 函数处被调用:

 //src/os/unix/ngx_daemon.c:13行
 ngx_int_t
 ngx_daemon(ngx_log_t *log)
 {
     int  fd;
 
     switch (fork()) {
     case -1:
         ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "fork() failed");
         return NGX_ERROR;
     
     //fork出来的子进程走这个case
     case 0:
         break;
     
     //父进程中fork返回值是子进程的PID,大于0,因此走这个case
     //因此主进程会退出
     default:
         exit(0);
     }
 
     //...省略部分代码...
 }

如上述代码中注释所示,为了不让主进程退出,我们在 Nginx 的配置文件中增加一行:

 daemon off;

这样 Nginx 就不会调用 ngx_daemon 函数了。

接下来,我们执行gdb nginx,然后通过设置参数将配置文件 nginx.conf 传给待调试的 Nginx 进程:

 Quit anyway? (y or n) y
 [root@iZbp14iz399acush5e8ok7Z sbin]# gdb nginx 
 ...省略部分输出...
 Reading symbols from nginx...done.
 (gdb) set args -c /usr/local/nginx/conf/nginx.conf
 (gdb) 

接着输入 run 命令尝试运行 Nginx:

 (gdb) run
 Starting program: /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
 [Thread debugging using libthread_db enabled]
 ...省略部分输出信息...
 [Detaching after fork from child process 7509]

如前文所述,gdb 遇到 fork 指令时默认会 attach 到父进程去,因此上述输出中有一行提示”Detaching after fork from child process 7509“,我们按 Ctrl + c 将程序中断下来,然后输入 bt 命令查看当前调用堆栈,输出的堆栈信息和我们在方法一中看到的父进程的调用堆栈一样,说明 gdb在程序 fork 之后确实 attach 了父进程:

 ^C
 Program received signal SIGINT, Interrupt.
 0x00007ffff6f73c5d in sigsuspend () from /lib64/libc.so.6
 (gdb) bt
 #0  0x00007ffff6f73c5d in sigsuspend () from /lib64/libc.so.6
 #1  0x000000000044ae32 in ngx_master_process_cycle (cycle=0x71f720) at src/os/unix/ngx_process_cycle.c:164
 #2  0x000000000040bc05 in main (argc=3, argv=0x7fffffffe4e8) at src/core/nginx.c:382
 (gdb) 

如果想让 gdb 在 fork 之后去 attach 子进程,我们可以在程序运行之前设置 set follow-fork child,然后使用 run 命令重新运行程序。

 (gdb) set follow-fork child 
 (gdb) run
 The program being debugged has been started already.
 Start it from the beginning? (y or n) y
 Starting program: /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
 [Thread debugging using libthread_db enabled]
 Using host libthread_db library "/lib64/libthread_db.so.1".
 [Attaching after Thread 0x7ffff7fe7740 (LWP 7664) fork to child process 7667]
 [New inferior 2 (process 7667)]
 [Detaching after fork from parent process 7664]
 [Inferior 1 (process 7664) detached]
 [Thread debugging using libthread_db enabled]
 Using host libthread_db library "/lib64/libthread_db.so.1".
 ^C
 Thread 2.1 "nginx" received signal SIGINT, Interrupt.
 [Switching to Thread 0x7ffff7fe7740 (LWP 7667)]
 0x00007ffff703842b in epoll_wait () from /lib64/libc.so.6
 (gdb) bt
 #0  0x00007ffff703842b in epoll_wait () from /lib64/libc.so.6
 #1  0x000000000044e546 in ngx_epoll_process_events (cycle=0x71f720, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:800
 #2  0x000000000043f317 in ngx_process_events_and_timers (cycle=0x71f720) at src/event/ngx_event.c:247
 #3  0x000000000044c38f in ngx_worker_process_cycle (cycle=0x71f720, data=0x0) at src/os/unix/ngx_process_cycle.c:750
 #4  0x000000000044926f in ngx_spawn_process (cycle=0x71f720, proc=0x44c2e1 <ngx_worker_process_cycle>, data=0x0, name=0x4cfd70 "worker process", respawn=-3)
     at src/os/unix/ngx_process.c:199
 #5  0x000000000044b5a4 in ngx_start_worker_processes (cycle=0x71f720, n=1, type=-3) at src/os/unix/ngx_process_cycle.c:359
 #6  0x000000000044acf4 in ngx_master_process_cycle (cycle=0x71f720) at src/os/unix/ngx_process_cycle.c:131
 #7  0x000000000040bc05 in main (argc=3, argv=0x7fffffffe4e8) at src/core/nginx.c:382
 (gdb) 

我们接着按 Ctrl + C 将程序中断下来,然后使用 bt 命令查看当前线程调用堆栈,结果显示确实是我们在方法一中子进程的主线程所在的调用堆栈,这说明 gdb 确实 attach 到子进程了。

我们可以利用方法二调试程序 fork 之前和之后的任何逻辑,是一种较为通用的多进程调试方法,建议读者掌握。

总结起来,我们可以综合使用方法一和方法二添加各种断点调试 Nginx 的功能,慢慢就能熟悉 Nginx 的各个内部逻辑了。

3 推荐一些 Nginx 学习书单

必看 Nginx 经典书籍(含下载方式)mp.weixin.qq.com/s/uP6_2UaTFZkwvGhNopWzSg

以上三种方式都是不错的阅读源码的方式,读者可以根据自己的水平、目的和所处阶段去使用。

最后,阅读源码不是做给别人看的,如果你之前从未意识到阅读各种大大小小的开源项目的源码的重要性,从现在开始,循序渐进,少买点在线课程,少囤点书,多读些开源代码吧。




开源代码一行一行读肯定是不行,因为人脑的缓存和内存都非常低,短期之内记不住几行代码,阅读量上来,如果没有纲领,基本就是浪费时间。下面的几个方法可以快速的建立纲领:

  • 阅读文档
  • 运行实例
  • Debug
  • 阅读 commit 历史
前置准备

首先,需要准备该语言的 IDE 或者具有 IDE 功能的编辑器,比如代码调转、补全、类型推断等等功能。

然后,把代码拉到本地,用 IDE 打开。

最后,确保可以编译 或者 解释 某个实例。

阅读文档

如果对一个开源项目感兴趣,特别是想学到一些新知识,阅读文档是最佳的途径。一般的开源项目文档组织如下:

  • 用户说明
    • 项目说明:解释这个项目的目的、设计原则等等
      • 快速开始:一般包括几个运行实例
  • API 说明

一般如果某个项目属于你比较熟悉的业务领域,那么稍微阅读一下用户说明,就可以看看 API 的组成,主要关注:

上面这些内容有助于理解项目整体的一些特性,非常有助于后面阅读源代码。

运行实例

这部分就是把代码 Pull 回本地,然后运行之前看到的一些实例。注意最好不要安装二进制或者包后运行。而是pull代码回本地后编译运行,这样可以为下一步 Debug 关键功能做好准备。

通过运行实例,可以熟悉该项目一些基本的使用方法和规则,找到自己感兴趣的部分,就可以开始进行 Debug 操作了。

Debug

阅读文档和源代码属于静态分析,软件的运行时需要在大脑中就行模拟(大脑就变成了一个解释器)。可是有些项目抽象比较复杂,大脑往往很难完成这个解释运行时的工作。Debug 就成了快速熟悉代码运行时的最佳方式。

把一个自己感兴趣的实例运行在 Debug 模式,然后一步一步的 Step in,观察数据是如何变化、函数调用如何发展等等。

很多语言都有函数调用分析的静态分析工具,但是个人感觉对于大项目,函数的调用栈非常复杂,今天分析工具生成的图表不一定非常直观。大部分时候,还是 Debug 某个 code path 比较直接。

阅读 commit 历史

再熟悉了一些 Code path 以后就可以聚焦在自己最为感兴趣的功能或者模块上。这时候我们应该利用好版本控制工具。

比如,对于一个项目,我们可以回滚的某个早期的版本,观察代码的演变;也可以观察某个函数的变化历史。这些 Commit 往往体现了贡献者对该函数或者模块理解的变化。会帮助我们进一步理解项目。




为什么人们都愿意“重新造轮子”?一个重要的原因就是:读别人的代码比自己写还难!

开源代码不适合新手学习,除非是那种具有良好文档的,而且深入浅出的优秀代码。

但这几乎是不可能的。因为:

1、文档的写作是很枯燥的;

2、开源代码是免费的,其作者没有义务为你解释服务;

3、开源代码更倾向于使用一些“复杂”的“新”技术,而不是朴实的保守的旧技术。

如果题主英语好,情况可能会好一点,因为有限的开源代码文档资料绝大部分都是英文的。

如果题主有兴趣的话,我开源了两个项目,正在进行中:

英雄帖:开源项目招募英才

。但不得不说,写文档真是件苦逼的事!




"没有经验的技术差底子薄的初级程序员,如何阅读项目源码? "

“有人阅读过 mybatis 的源码吗 ?就看一个初始化过程就看的已经头晕眼花了,小伙伴们支支招吧!”

"源码应该怎么阅读,我曾经尝试阅读一些源码,例如alibaba的druid中sqlparser部分,spring-mvc,但是发现很吃力,都说debug是最好的阅读方式,我在debug时经常有跟丢的现象……就是走着走着感觉好像进入了一些我当前不太关注细枝末节。 "

。。。。。。

估计很多人都有这样的疑惑。

我非常能理解小伙伴们的痛苦,因为我也是这么痛苦着走过来的。

阅读优秀源码的好处想必大家都知道,学习别人优秀的设计,合理的抽象,简洁的代码… 总之是好处多多。

但是真的把庞大的代码放到你的面前,就如同一个巨大的迷宫,要在其中东转西转寻出一条路来,把迷宫的整个结构搞清楚,理解核心思想,真心不容易。

在阅读由面向对象的语言如Java写的代码时,会发现接口和具体的实现经常对应不起来,不太清楚一个功能到底是怎么在哪个实现类中才能找到。 不像C语言,就是函数调用函数,相对还好点。

如果是动态语言如Ruby,Python, 一个变量的类型甚至都不容易知道,阅读的难度大大增加。

还有一个重要的原因,现在我们看到的源码基本上都经过若干年发展、经过很多人不断地完善的,枝枝蔓蔓非常多,魔鬼都在细节中。 阅读的时候很容易陷进去, 看了几十层函数调用以后,就彻底懵了,就放弃了: 甭管你把源码吹得天花乱坠, 老子再也不看了。

经过很多痛苦的挣扎以后,我也算有一些成功的经历,今天用治学的三个境界来类比, 给大家分享一下:

昨夜西风凋碧树,独上高楼,望尽天涯路

想把源码搞懂,吃透,首先得登高望远,瞰察路径,明确目标与方向,了解源码的概貌。

所以有些准备工作必须得做。

1. 阅读源码之前,需要有一定的技术储备。

比如设计模式,在很多Java源码中几乎就是标配,尤其是这几个:模板方法,单例,观察者,工厂方法,代理,策略,装饰者。

再比如阅读Spring源码,肯定得先了解IoC是怎么回事,AOP的实现方式,CGLib,Java动态代理等,自己动手,写点相关的代码,把这些知识点掌握了。

2. 必须得会使用这个框架/类库, 最好是精通各种各样的用法。

上面刚提过,魔鬼都在细节中,如果有些用法根本不知道,可能你能看明白代码是什么意思,但是不知道它为什么这些写。

3. 先去找书,找资料,了解这个软件的整体设计。

都有哪些模块? 模块之间是怎么关联的?怎么关联的?

可能一下子理解不了,但是要建立一个整体的概念,就像一个地图,防止你迷航。

在读源码的时候可以时不时看看自己在什么地方。

4. 搭建系统,把源代码跑起来!

相信我,Debug是非常非常重要的手段, 你想通过只看而不运行就把系统搞清楚,那是根本不可能的!

衣带渐宽终不悔,为伊消得人憔悴

5. 根据你对系统的理解,设计几个主要的测试案例,定义好输入,输出。

运行系统,慢慢地debug ,一步步地走,这是个死功夫,没有办法绕过。

Debug一遍肯定是不行的,需要Debug很多遍。

第一遍尽可能抛弃细节,抓住主要流程, 比如有些看起来不重要的方法就不进去看了。

第二遍、第三遍…再去看那些细节。

一个非常重要的工作就是记笔记(又是写作!),画出系统的类图(不要依靠IDE给你生成的), 记录下主要的函数调用, 方便后续查看。

文档工作极为重要,因为代码太复杂,人的大脑容量也有限,记不住所有的细节。 文档可以帮助你记住关键点, 到时候可以回想起来,迅速地接着往下看。

要不然,你今天看的,可能到明天就忘个差不多了。

给大家看看我做的一些笔记, 格式不重要,很随意,方便自己看懂就行。

img

img

img

img

6. 主要的测试案例搞明白了,丰富测试案例,考虑一些分支流程。

继续Debug…

总之,静态地看代码 + 动态地debug (从业务的角度), 就会慢慢揭开这个黑暗森林的面纱。

这一步会非常非常地花费时间,但是你做完了,对系统的理解绝对有质的飞跃。

众里寻他千百度,蓦然回首,那人却在灯火阑珊处。

没有千百度的上下求索,不会有瞬间的顿悟和理解,衷心祝愿阅读源码的朋友们都能达到这一境界。

最后一点,也是最关键的一点: 要能坚持下去。

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

的细节。 文档可以帮助你记住关键点, 到时候可以回想起来,迅速地接着往下看。

要不然,你今天看的,可能到明天就忘个差不多了。

给大家看看我做的一些笔记, 格式不重要,很随意,方便自己看懂就行。

img

img

img

img

6. 主要的测试案例搞明白了,丰富测试案例,考虑一些分支流程。

继续Debug…

总之,静态地看代码 + 动态地debug (从业务的角度), 就会慢慢揭开这个黑暗森林的面纱。

这一步会非常非常地花费时间,但是你做完了,对系统的理解绝对有质的飞跃。

众里寻他千百度,蓦然回首,那人却在灯火阑珊处。

没有千百度的上下求索,不会有瞬间的顿悟和理解,衷心祝愿阅读源码的朋友们都能达到这一境界。

最后一点,也是最关键的一点: 要能坚持下去。

[外链图片转存中…(img-dh5r0Rwf-1715647251086)]
[外链图片转存中…(img-i0pVNS2I-1715647251086)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 23
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值