如何快速读懂开源代码?_怎么读懂软件代码,超详细

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Golang全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

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

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

如果你需要这些资料,可以添加V获取:vip1024b (备注go)
img

正文

//实例
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 (从业务的角度), 就会慢慢揭开这个黑暗森林的面纱。

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

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

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

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

我不是一个聪明人, 但是笨人自有笨办法:什么事都架不住不断的重复,一遍看不明白,再来第二遍, 两遍搞不明白,再来第三遍…

可能有人要问: 你怎么能这么坚持地刨根问底呢?

答案就是好奇心: 这玩意儿到底是怎么实现的?!




今天一文分享怎么学习开源代码,纯干货纯干货!可以复制到自己的笔记本!

一、看什么样子的开源

  • 开源项目中要有**全套的源码和配套工具,**刚开始学开源的时候可别找那种没有工具或者少源码的给自己增加难度。
  • 需要有完善的文档,比如新手指南、项目整体的架构设计文档、模块详细的设计文档,配置说明文档和注意事项
  • 社区是否活跃,是否还在更新和完善。

这几点非常非常重要,基本上是一个初学者学开源的必备内容。

二、初步了解

2.1 顺一遍文档

很少有小伙伴可以好好的看一遍官方文档,因为一开始的心态就是扎进去学。

这样非常容易造成:

  1. 给自己添加难度,很多细节的内容,根本不懂。
  2. 着急动手实践,发现自己连基本的使用方式都没弄明白。
  3. 或者你能找到和黑马程序员一样给大家按照官方文档讲解的课程
2.2 重点学习思路

首先是项目的背景:项目用了什么技术?提供了什么特性?它实际解决了什么问题

项目的适用场景是:优点是什么?缺点呢?适合与不适合的场景是什么

哪些公司在生产环境中使用?

有欧少公司在生产环境中部署并维护过此开源项目?

一方面经历过生产环境的质量相对较好,及时用bug其他公司或多或少也遇到过,自己解决起来也不至于孤立无援。

2.3 了解代码目录结构

在下载源码之后,先看下代码目录的结构,或者是从示例测试代码入手,比如:

  • base代表基础库
  • net代表网络库
  • demo/example代表示例代码
  • tests:测试代码
  • docs:文档目录(类图、流程图、活动图、业务知识相关资料等)

刚刚入手的时候,建议从标粗的内容开始看起来。

2.4 在安装部署前补充新概念、新技术

在已有的知识体系纸上去学习新的开源项目,需要对概念或者新技术有个大概的了解,才能更好地理解项目的整体实现思路,做起来才可以事倍功半。

千万不要一上来就看代码,核心概念不清楚,原理不懂,核心算法没吃透,看代码非常费劲。

三、安装部署、运行

好的开源项目,文档都是比较完善的,安装部署文档一般会有的,更好的甚至都会有rpm安装包和docker镜像。

先把程序运行起来,这只是第一遍!

运行起来之后,精简自己的环境,减少后面调试过程中会出现的干扰信息。

比如,Nginx使用多进程的方式处理请求,为了调试跟踪Nginx的行为,我经常把worker数量设置为1个,这样调试的时候就知道待跟踪的是哪个进程了。

3.2 成功运行的意义

第一:先来体验项目的功能,对开源项目的功能从上帝视角了解。

第二:下断点>调试>修改代码>观察>再调试,从这个反复的步骤中了解程序的逻辑。

3.3 举例子

安装配置环境>从最简单的例子入门>研究复杂一点的例子>自己写个demo;

常见的安装目录是conf存放配置文件,logs存放日志文件,bin存放日志文件。有一些特殊情况,Nginx有html目录,这种目录能促使我们带着相关疑问继续研究学习,带着问题学习是最高效的。

四、要清楚自己的学习目的

我遇到很多同学上来就是找我,老师给我推荐点什么的开源项目,我说为什么学?很多同学的回答都是感觉该学点源码了。但是完全不知道自己为什么要学。

是要了解其中一个模块,比如是基础模块还是业务模块

4.2 学习顺序
  • 第一步:业务逻辑的实现流程,中间调用了第三方库函数、utils函数、定制的数据结构和算法等;还要了解对外的接口,这些接口的入口,出口参数以及作用。
  • 第二步:看看内存池的实现代码、调度器代码、dpdk中对于海量数据包是如何处理的。
  • 第三步:制作成excel表格,记录走读进度
  • 第四步:批判性的思维,为什么要这样做?

比如:从main函数进入,使用gdb单步跟踪清理一次完整流程(如程序初始化)的代码调用路径,这可以通过debug来观察运行时的变量和行为

如果实在是读不下去的话,先找到自己的兴趣所在,如果你对网络通讯感兴趣,就阅读网络层代码,深入到实现细节。比如:它用了什么库、采用了什么设计模式、为什么这么做?如果可以,debug的细节是什么?

或者是,看1.0版本的源码,1.0版本的内容,从复杂度上来说,都小很多,比较容易

4.3 数据结构和算法

如果上面的步骤都完成了,在来这里。如果真的不明白可以跳过去!比如判断参数的就可以跳过不看的,其次就是代码中有没有一些非顺序的代码,如果有你能理解么?比如通过中间件MQ继续后续的流程等等,所以大家要学会分析。

优秀的开源项目当中都有很多经典的算法,可以全部跳过之后再来学习

因为结构定义了一个程序的架构,结构定下来了才有具体的实现,好比盖房子。数据结构就是框架的结构,至于算法,暂时属于不需要深究的细节。先了解入口出口参数以及作用就可以了。

linus说:烂程序员关心的是代码,好程序员关心的是数据结构以及他们的关系。

所以在,读一份代码的时候,理清楚核心的数据结构之间的关系很重要。

五、划重点

将上面说的学习事项再次总结梳理一遍!

安装运行:按照相关文档,安装运行项目:

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

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
img

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

  • 第一步:业务逻辑的实现流程,中间调用了第三方库函数、utils函数、定制的数据结构和算法等;还要了解对外的接口,这些接口的入口,出口参数以及作用。
  • 第二步:看看内存池的实现代码、调度器代码、dpdk中对于海量数据包是如何处理的。
  • 第三步:制作成excel表格,记录走读进度
  • 第四步:批判性的思维,为什么要这样做?

比如:从main函数进入,使用gdb单步跟踪清理一次完整流程(如程序初始化)的代码调用路径,这可以通过debug来观察运行时的变量和行为

如果实在是读不下去的话,先找到自己的兴趣所在,如果你对网络通讯感兴趣,就阅读网络层代码,深入到实现细节。比如:它用了什么库、采用了什么设计模式、为什么这么做?如果可以,debug的细节是什么?

或者是,看1.0版本的源码,1.0版本的内容,从复杂度上来说,都小很多,比较容易

4.3 数据结构和算法

如果上面的步骤都完成了,在来这里。如果真的不明白可以跳过去!比如判断参数的就可以跳过不看的,其次就是代码中有没有一些非顺序的代码,如果有你能理解么?比如通过中间件MQ继续后续的流程等等,所以大家要学会分析。

优秀的开源项目当中都有很多经典的算法,可以全部跳过之后再来学习

因为结构定义了一个程序的架构,结构定下来了才有具体的实现,好比盖房子。数据结构就是框架的结构,至于算法,暂时属于不需要深究的细节。先了解入口出口参数以及作用就可以了。

linus说:烂程序员关心的是代码,好程序员关心的是数据结构以及他们的关系。

所以在,读一份代码的时候,理清楚核心的数据结构之间的关系很重要。

五、划重点

将上面说的学习事项再次总结梳理一遍!

安装运行:按照相关文档,安装运行项目:

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

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-cSemblrd-1713209442770)]

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值