本节书摘来自异步社区《深入剖析Nginx》一书中的第2章,第2.4节,作者: 高群凯 更多章节内容可以访问云栖社区“异步社区”公众号查看。

2.4 获得Nginx程序完整执行流程

利用strace命令能帮助我们获取到Nginx在运行过程中所发起的所有系统调用,但是不能看到Nginx内部各个函数的调用情况。利用gdb调试Nginx能让我们很清晰地获得Nginx每一步的执行流程,但是单步调试毕竟是非常麻烦的,有没有更为方便的方法一次性获得Nginx程序执行的整个流程呢?答案是肯定的,而且方法还非常多1。虽然相比直接使用某些强大工具(如System Tap2)而言,下面要介绍的方法比较笨,但它的确可行,而且从这个过程中也许能学到一些额外的知识。我们只需利用gcc的一个名为-finstrument- functions3的编译选项,再加上一些我们自己的处理,就可以达到既定目的。关于gcc提供的这个-finstrument-functions选项,这里不做过多介绍,我们只需明白它提供的是一种函数调用记录追踪功能。关于这些,感兴趣的读者请直接参考gcc官网手册,下面来看获得Nginx程序完整执行流程的具体操作。

首先,我们准备两个文件,文件名和文件内容分别如下。

00: 代码片段2.4-1,文件名: my_debug.h
01: #ifndef MY_DEBUG_LENKY_H
02: #define MY_DEBUG_LENKY_H
03: #include <stdio.h>
04: 
05: void enable_my_debug( void ) __attribute__((no_instrument_function));
06: void disable_my_debug( void ) __attribute__((no_instrument_function));
07: int get_my_debug_flag( void ) __attribute__((no_instrument_function));
08: void set_my_debug_flag( int ) __attribute__((no_instrument_function));
09: void main_constructor( void ) __attribute__((no_instrument_function, constructor));
10: void main_destructor( void ) __attribute__((no_instrument_function, destructor));
11: void __cyg_profile_func_enter( void *,void *) __attribute__((no_instrument_ function));
12: void __cyg_profile_func_exit( void *, void *) __attribute__((no_instrument_ function));
13: 
14: #ifndef MY_DEBUG_MAIN
15: extern FILE *my_debug_fd;
16: #else
17: FILE *my_debug_fd;
18: #endif
19: #endif
00: 代码片段2.4-2,文件名: my_debug.c
01: #include "my_debug.h"
02: #define MY_DEBUG_FILE_PATH "/usr/local/nginx/sbin/mydebug.log"
03: int _flag = 0;
04: 
05: #define open_my_debug_file()  \
06:      (my_debug_fd = fopen(MY_DEBUG_FILE_PATH, "a"))
07: 
08: #define close_my_debug_file()  \
09:      do {  \
10:           if (NULL != my_debug_fd) {  \
11:                 fclose(my_debug_fd);  \
12:           }  \
13:      }while(0)
14: 
15: #define my_debug_print(args, fmt...) \
16:       do{  \
17:           if (0 == _flag) {  \
18:                  break;  \
19:           }  \
20:           if (NULL == my_debug_fd && NULL == open_my_debug_file()) {  \
21:                 printf("Err: Can not open output file.\n");  \
22:                 break;  \
23:           }  \
24:           fprintf(my_debug_fd, args, ##fmt);  \
25:           fflush(my_debug_fd);  \
26:      }while(0)
27: 
28: void enable_my_debug( void )
29: {
30:      _flag = 1;
31: }
32: void disable_my_debug( void )
33: {
34:      _flag = 0;
35: }
36: int get_my_debug_flag( void )
37: {
38:      return _flag;
39: }
40: void set_my_debug_flag( int flag )
41: {
42:      _flag = flag;
43: }
44: void main_constructor( void )
45: {
46:      //Do Nothing
47: }
48: void main_destructor( void )
49: {
50:      close_my_debug_file();
51: }
52: void __cyg_profile_func_enter( void *this, void *call )
53: {
54:      my_debug_print("Enter\n%p\n%p\n", call, this);
55: }
56: void __cyg_profile_func_exit( void *this, void *call )
57: {
58:      my_debug_print("Exit\n%p\n%p\n", call, this);
59: }

这两个文件是我2009年写的,比较乱,不过够用且测试无误,所以我这里也就直接先用它。将这两个文件放到/nginx-1.2.0/src/core/目录下,然后编辑/nginx-1.2.0/objs/Makefile文件,给CFLAGS选项增加-finstrument-functions选项。

02: 代码片段2.4-3,文件名: Makefile
03: CFLAGS =  -pipe  -O0 -W -Wall -Wpointer-arith -Wno-unused-parameter -Wunused- function -Wunused-va        riable -Wunused-value -Werror -g -finstrument-functions

接着,需要将my_debug.h和my_debug.c引入到Nginx源码里一起编译,所以继续修改/nginx- 1.2.0/objs/Makefile文件,根据Nginx的Makefile文件特点,修改的地方主要有如下几处。

00: 代码片段2.4-4,文件名: Makefile
01: …
18: CORE_DEPS = src/core/nginx.h \
19:         src/core/my_debug.h \
20: …
84: HTTP_DEPS = src/http/ngx_http.h \
85:         src/core/my_debug.h \
86: …
102: objs/nginx:     objs/src/core/nginx.o \
103:             objs/src/core/my_debug.o \
104: …
211:             $(LINK) -o objs/nginx \
212:             objs/src/core/my_debug.o \
213: …
322: objs/src/core/my_debug.o: $(CORE_DEPS) \
323:             src/core/my_debug.c
324:             $(CC) -c $(CFLAGS) $(CORE_INCS) \
325:                        -o objs/src/core/my_debug.o \
326:                        src/core/my_debug.c
327: …

为了在 Nginx 源码里引入 my_debug,这需要在 Nginx 所有源文件都包含有头文件 my_ debug.h,当然没必要每个源文件都去添加对这个头文件的引入,我们只需要在头文件ngx_core.h内加入对my_debug.h文件的引入即可,这样其他Nginx的源文件就间接地引入了这个文件。

37: 代码片段2.4-5,文件名: ngx_core.h
38: #include "my_debug.h"
在```  
源文件nginx.c的最前面加上对宏MY_DEBUG_MAIN的定义,以使得Nginx程序有且仅有一个my_debug_fd变量的定义。

06: 代码片段2.4-6,文件名: nginx.c
07: #define MY_DEBUG_MAIN 1
08:
09: #include
10: #include
11: #include

最后就是根据我们想要截取的执行流程,在适当的位置调用函数enable_my_debug();和函数disable_my_debug();,这里仅作测试,直接在main函数入口处调用enable_my_debug();,而disable_my_debug();函数就不调用了。

200: 代码片段2.4-7,文件名: nginx.c
201: main(int argc, char const argv)
202: {
203: …
208: enable_my_debug();

至此,代码增补工作已经完成,重新编译Nginx,如果之前已编译过Nginx,那么需记得先把Nginx源文件的时间戳进行刷新。

以单进程模式运行Nginx,并且在配置文件里将日志功能的记录级别设置低一点,否则将有大量的日志函数调用堆栈信息,经过这样的设置后,我们才能获得更清晰的Nginx执行流程,即配置文件里做如下设置。

00: 代码片段2.4-8,文件名: nginx.c
01: master_process off;
02: error_log logs/error.log emerg;

正常运行后的Nginx将产生一个记录程序执行流程的文件,这个文件会随着Nginx的持续运行迅速增大,所以在恰当的地方调用disable_my_debug();函数是非常有必要的,不过我这里在获取到一定量的信息后就直接kill掉Nginx进程了。mydebug.log的内容如下所示。
[root@localhost sbin]# head -n 20 mydebug.log 
Enter
0x804a5fc
0x806e2b3
Exit
0x804a5fc
0x806e2b3
…
这记录的是
N`


ginx执行函数调用关系,不过这里的函数还只是以对应的地址显示而已,利用另外一个工具 addr2line 可以将这些地址转换回可读的函数名。addr2line 工具在大多数Linux发行版上默认有安装,如果没有那么在官网4下载即可,其具体用法也可以参考官网手册5。这里我们直接使用,写个addr2line.sh脚本。

00: 代码片段2.4-9,文件名: addr2line.sh
01: #!/bin/sh
02: 
03: if [ $# != 3 ]; then
04:     echo 'Usage: addr2line.sh executefile addressfile functionfile'
05:     exit
06: fi;
07: 
08: cat $2 | while read line
09: do
10:      if [ "$line" = 'Enter' ]; then
11:              read line1
12:              read line2
13: #           echo $line >> $3
14:              addr2line -e $1 -f $line1 -s >> $3
15:              echo "--->" >> $3
16:              addr2line -e $1 -f $line2 -s | sed 's/^/    /' >> $3
17:              echo >> $3
18:      elif [ "$line" = 'Exit' ]; then
19:              read line1
20:              read line2
21:              addr2line -e $1 -f $line2 -s | sed 's/^/    /' >> $3
22:              echo "<---" >> $3
23:              addr2line -e $1 -f $line1 -s >> $3
24: #           echo $line >> $3
25:              echo >> $3
26:      fi;
27: done
执行addr2line.sh进行地址与函数名的转换,这个过程挺慢的,因为从上面的Shell脚本可以看到对于每一个函数地址都调用addr2line进行转换,执行效率完全没有考虑,不过够用就好,如果非要追求高效率,直接写个C程序来做这个转换工作当然也是可以的。

[root@localhost sbin]# vi addr2line.sh
[root@localhost sbin]# chmod a+x addr2line.sh 
[root@localhost sbin]# ./addr2line.sh nginx mydebug.log myfun.log
[root@localhost sbin]# head -n 12 myfun.log 
main
nginx.c:212
--->
     ngx_strerror_init
     ngx_errno.c:47
     ngx_strerror_init
     ngx_errno.c:47
<---
main
nginx.c:212
…

关于如何获得Nginx程序执行流程的方法大体就是上面描述的这样,不过这里介绍得很粗略,写的代码也仅只是作为示范使用,关于 gcc 以及相关工具的更深入研究已不在本书的讨论范围之内,如感兴趣可查看上文中提供的相关链接。