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

以下操作需要一些 gdb 调试知识,如果你不熟悉 gdb 调试,建议花几分钟学习一下。

gdb 不难学,掌握常用命令即可,常用命令也不多,推荐《Debugging with GDB》这本书:链接: https://pan.baidu.com/s/1YPAx9CQgQoJmkd38HLZ35Q 提取码: d03u

一、下载 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.gzResolving nginx.org (nginx.org)... 95.211.80.227, 62.210.92.35, 2001:1af8:4060:a004:21::e3Connecting to nginx.org (nginx.org)|95.211.80.227|:80... connected.HTTP request sent, awaiting response... 200 OKLength: 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"选项。

二、调试 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 nginxnginx      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.6Missing 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:164164             sigsuspend(&set);(gdb) l159                 }160             }161162             ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "sigsuspend");163164             sigsuspend(&set);165166             ngx_time_update();167168             ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,(gdb)

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

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

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

 [root@iZbp14iz399acush5e8ok7Z sbin]# gdb attach 5247...部署输出省略...0x00007fd42a1c842b in epoll_wait () from /lib64/libc.so.6Missing 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:800800         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)加一个断点,然后使用 命令让 Nginx 子进程继续运行。

 800         events = epoll_wait(ep, event_list, (int) nevents, timer);(gdb) list795         /* NGX_TIMER_INFINITE == INFTIM */796797         ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,798                        "epoll timer: %M", timer);799800         events = epoll_wait(ep, event_list, (int) nevents, timer);801802         err = (events == -1) ? ngx_errno : 0;803804         if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {(gdb) b 804Breakpoint 1 at 0x44e560: file src/event/modules/ngx_epoll_module.c, line 804.(gdb) cContinuing.

接着我们在浏览器里面访问 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:804804         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 threadsId   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 modeDebugger 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_tngx_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出来的子进程走这个casecase 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) runStarting 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 了父进程:

 ^CProgram 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) runThe program being debugged has been started already.Start it from the beginning? (y or n) yStarting 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".^CThread 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 的各个内部逻辑了。

三、推荐一些 Nginx 学习书单

  • 深入理解Nginx模块开发与架构解析 第2版
  • 实战nginx
  • Nginx高性能Web服务器详解
  • Nginx模块开发指南 使用C++11和Boost程序库

获取链接:

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

如何高效的学习 Nginx 源码,汲取养分?相关推荐

  1. 从9个组件开始,教你如何高效的阅读nginx源码?

    从9个组件开始,教你如何高效的阅读nginx源码?|内存池.线程池.内存共享组件实现. http处理流程.phase原理.红黑树.配置文件.惊群.原子操作 专注于服务器后台开发,包括C/C++,Lin ...

  2. nginx 源码调试

    nginx 源码调试 这段时间正在学习nginx源码,看到一贴子的提问 (帖子:http://www.oschina.net/question/2711991_2165566?p=1#AnchorAn ...

  3. CMake编译Nginx源码

    背景 最近打算学习nginx源码,但使用clion IDE查看不支持跳转.因为源码是使用autotool维护的,而clion需要CMake管理项目.着手编译nginx源码. 环境 os : ubunt ...

  4. 【Nginx源码分析】Nginx配置文件解析(一)

    运营研发团队 李乐 配置文件是nginx的基础,对于学习nginx源码甚至开发nginx模块的同学来说更是必须深究.本文将从源码从此深入分析nginx配置文件的解析,配置存储,与配置查找. 看本文之前 ...

  5. nginx源码学习资源

    nginx源码学习是一个痛苦又快乐的过程,下面列出了一些nginx的学习资源. 首先要做的当然是下载一份nginx源码,可以从nginx官方网站下载一份最新的. 看了nginx源码,发现这是一份完全没 ...

  6. nginx 源码学习笔记(二)——nginx精粹-模块

    看了一点nginx的源码发现,nginx的模块思想确实吸引了我,也不得不佩服俄罗斯人的想问题方式,要分析nginx源码,首先要搞懂的就是nginx的模块思想以及相关的数据结构. 还记得我们上一次写的h ...

  7. 从Nginx源码谈大小写字符转化的最高效代码以及ASCII码表的科学

    说起大小写字母转换,大家很容易想起系统函数是不是,几乎所有的编程语言都提供了这种转换函数,但是你有没有想过这背后是怎么实现的? 让你写怎么实现? 我们都知道Nginx是目前用的最多的Http服务器,那 ...

  8. Nginx源码分析:epoll事件处理模块概述

    nginx源码分析 nginx-1.11.1 参考书籍<深入理解nginx模块开发与架构解析> 事件处理模块概述 Nginx的高效请求的处理依赖于事件管理机制,本次默认的场景是Linux操 ...

  9. Nginx源码研究之nginx限流模块详解

    这篇文章主要介绍了Nginx源码研究之nginx限流模块详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考.一起跟随小编过来看看吧 高并发系统有三把利器:缓存.降级和限流: 限流的目的是通过对并 ...

最新文章

  1. xss劫持 HTML 表单,XSS 之 form表单劫持(通用明文记录)
  2. SRS的2021,盐碱地里种西瓜的王婆
  3. 计算机桌面堆,桌面堆 Desktop heap设置
  4. vi/vim 三种模式及命令 (简单粗暴,轻松搞懂)
  5. 1539. 第 k 个缺失的正整数
  6. JavaScript ES2021 新特性解析
  7. TOMCAT开放远程调试端口
  8. 《穷爸爸,富爸爸》读书笔记
  9. childNodes.length 的临时保存
  10. php swf转image,gif转换swf|助你将gif图片转换成swf(flash)文件
  11. html css画个人印章
  12. php微信公众号回复换行,PHP 微信公众号开发,关键字回复使用switch出错
  13. c语言万年历方案论证,C语言编写方案-万年历分析.doc
  14. 服务器硬盘sas速度多少,R710服务器6块硬盘(SAS 300G 15000转)做完Raid5后读写速度没有一台普通台式机硬盘的读写速度快...
  15. DeepFlow: Deep Learning-Based Malware Detection by Mining Android Application
  16. 苹果Mac字体设计编辑工具:Glyphs
  17. 奋发图强半年多,终于四面阿里如愿拿到心仪offer定级P7
  18. cvc-complex-type.2.4.d: 发现了以元素 ‘base-extension‘ 开头的无效内容。此处不应含有子元素。
  19. Bootstrap标签(label)的使用
  20. EOS智能合约开发(一)

热门文章

  1. webmagic ajax,【WebMagic】抓取前端渲染的页面
  2. ant design pro charts图表渲染之后台数据类型转换
  3. 计算两个时间之间的工作时长
  4. 统计并输出该字符串中26个英文字母
  5. 桌面打不开计算机控制面板,电脑控制面板打不开怎么办?两种解决方法
  6. Javascript DOM 编程艺术读书笔记16/04/01
  7. 英寸和厘米转化python_习题 5: 更多的变量和打印 | 笨办法学 Python
  8. 基于 Pyjwt 的 Flask 用户授权登录
  9. javascript 日期日历控件
  10. QGIS入门实验十四 半透明立体影像图