C 的学习一直断断续续的,不过半年前因为 syslog-ng 项目,最近一周因为 lighttpd 的模块改造,不得不狠狠地啃了两次 C 语言,虽然工作以来基本都是使用 shell 和 Python,但还是可以总结一些经验如下:
- 找入口:首先,找函数入口: main()。有些软件如 lighttpd 有多个二进制程序,可以扫一眼 Makefile.am,大致可以找到
- 读文档、找资料、理解基本框架:不要上手就开始读 .c 源文件,可以先找官方文档,以及放狗(google)找相关文档了解程序基本框架,如果有架构图最好。例如,syslog-ng 的文档说明就写得非常好,对于帮助我们理解其中的基本概念非常有帮助。特别是到后面理解其中的大量结构体和回调函数的时候,都可以在说明文档中找到实例映射。
此外,可以先找现有资料,放狗,使用 “xxx 源码解析” 这样的关键字搜一下,通常会有别人已经写过的一些分析资料。… 如果没有?… 那么恭喜你,你走在了时代的前面… “)
- 基础知识准备:如果要真正读好别人写的 C 代码,还是需要一些基础知识,除了基本语法之外,还需要了解一些基本的标准库(至少有个概念),另外,对系统调用应该有一定认识。这一点上,一定要拜读一下 Steve Richard 的《UNIX 环境高级编程》。此外,对不同的应用类型,都会有一些基本的套路。例如,对于 bash 这样的命令行解释器(其他应用也可能会有命令解释,包括使用其他语言编写的程序,例如 Python 中提供 cmd 模块),以前有本书说过如何编写一个简单的 bash,当时实践过的,虽然里面连 cd 命令都没有,但是很好的实践材料。下次把书名找出来。
而对于 syslog-ng, lighttpd 这样的服务程序,通常分为两种模式,即多线程模式或 poll() 模式,我一般倾向使用 poll——基本上,我现在也觉得 “thread is a bad idea”,若非万不得已,不推荐使用对线程。很多性能方面的问题,可以利用 Linux 操作系统来补足。
服务进程模式,通常都是先 init,然后解析命令行和配置文件,再进入到主循环,使用 poll/epoll 这样的系统调用(不同的系统具体实现不同),反复处理各种请求。有了这个概念,找到主循环中的 poll 所在,就可以找到读代码一个起点。
- 了解数据结构: 了解数据结构是重中之重,一开始不可能全部搞清楚,只能通过通读所有的 *.h 文件了解初步的数据结构,并努力“想象”这些结构体和现实实体之间的联系,特别是和文档中提到的实体之间的联系。这个过程也可以让我们大概了解相关的各个 .c 文件的大概作用,标记出来、写下来。
高级语言都有一套很方便的基本数据结构,如 Python 中的 Int, String, List, Dict, Tuple, File 等,而 C 语言中,目前似乎没有什么标准,这也是灵活性所必须付出的代价。例如 syslog-ng 使用了 glib 库中的数据结构,而 lighttpd 则自己写了一套 array, keyvalue, string, buffer 以及 splay_tree 等。对于理解程序主体,这些数据结构帮助不大,开始的时候,可以先放在一边,只有大概知道是什么意思,在代码中遇到的时候,可以根据其函数名用高级语言的方法想象一下。
现在的很多 C 程序,都在实现时候模拟了面向对象的方法,具体方法就是在结构体中大量使用函数指针形成回调函数。所以在研究这些数据结构的时候,可以用对象化的思想来帮助理解,并问自己一些问题:
Q: 这些数据结构之间的关系是什么?如何用对象化的组装关系(composite)来理解
Q: 代码中为何要使用这样的数据结构?
Q: 在什么地方、以何种方式使用这些数据结构?
Q: 关于 typedef — 何时创建的实例? - 梳理回调: 对代码回调 (callbacks) 有基本的认识,知道一些重要的结构体中的回调函数大体上都会指向哪些 .c 文件
- 跟踪调用关系: 分析、跟踪函数/文件(.c) 的调用关系,通常,有这样几种方法:
a. gdb 法: 了解一些 gdb 的有用的指令。《C in a Nutshell》中最后一章比较经典和实用,可以参考一下 (Nutshell 系列都不错好像,《Linux in a Nutshell》《Python in a Nutshell》都有很有价值的章节和信息)。
gdb 法的问题是会比较慢,特别是在对指令使用不够纯熟的情况下。我觉得比较适合小范围跟踪,即对整个调用流程有一定认识之后,对一些关注的函数或热点,进行比较细致的跟踪
b. 从整体上把握,则可以采用日志法,即打开程序的调试(debug)模式或详细模式,查看其日志输出。另外,可以参考其 log 方法,使用和代码一致的 log 方式,将自己关心的一些值和运行点输出日志
c. 我自己使用的一种土办法:trace。就是在源代码中每个函数进入的时候插入一个宏,打印它所在的文件和自己的函数名,以及栈的深度(
SDP++
),然后在输出的时候有一个对应的宏,做(SDP--
)。这个修改是在源代码中直接修改并需要重新编译的。其思路倒是和 gprof, gdb 这些抓取符号表并在函数入口处插入一个
_mcount()
函数很类似。只是目前我还不知道如何能够实现,所以还是只能通过这种土办法——说它土,是因为我只是写了一个 Python 脚本(确切地说,针对 syslog-ng 和 lighttpd 分别都做了修改),而它只是基于行来进行分析,找到函数的出口和入口,并不是 gcc 那样的语法分析器,所以很难准确,还要花一些时间在跟踪的时候进行校对。(不知道 ctags 又如何,但我目前发现,有些以宏的方式定义函数,ctags 还是找不到)
(利用 gprof 当然也是一个办法。配合 gprof2dot.py 和 graphivz 可以生成函数调用关系图,但这个图并不包含顺序,而且只是一个统计值,所以一般只用在性能调试的时候)
(有时间可以研究一下 gprof 和 gdb 的实现,以及 ltrace/strace,也许可以结合这个 trace 的思路做点东西出来)
稍后有时间我会写写这个 trace 方法,以及相应的脚本。trace 出来的效果大致如下:
[Intranet root@oplive-test2 /root/lighttpd-1.4.26.roczhou/src] #python ctrace.lighttpd.py call -l /root/ctrace/ctrace.16 | grep -E '^ [0-9]|^debug' | less debug 2 plugins_call_handle_trigger(srv) debug slot in plugin: mod_proxy 1 stat_cache.c# stat_cache_trigger_cleanup, 741 1 fdevent.c# fdevent_poll, 175 1 fdevent_linux_sysepoll.c# fdevent_linux_sysepoll_poll, 86 debug 2 plugins_call_handle_trigger(srv) debug slot in plugin: mod_proxy 1 stat_cache.c# stat_cache_trigger_cleanup, 741 1 fdevent.c# fdevent_poll, 175 1 fdevent_linux_sysepoll.c# fdevent_linux_sysepoll_poll, 86 1 fdevent.c# fdevent_event_next_fdndx, 233 1 fdevent_linux_sysepoll.c# fdevent_linux_sysepoll_event_next_fdndx, 116 1 fdevent.c# fdevent_event_get_revent, 182 1 fdevent_linux_sysepoll.c# fdevent_linux_sysepoll_event_get_revent, 91 1 fdevent.c# fdevent_event_get_fd, 190 1 fdevent_linux_sysepoll.c# fdevent_linux_sysepoll_event_get_fd, 106 1 fdevent.c# fdevent_get_handler, 198 1 fdevent.c# fdevent_get_context, 211 1 network.c# network_server_handle_fdevent, 29 2 connections.c# connection_accept, 1375 3 connections.c# connections_get_new_connection, 43 4 connections.c# connection_init, 738 4 connections.c# connection_init, 738 4 connections.c# connection_init, 738 4 connections.c# connection_init, 738 4 connections.c# connection_init, 738 4 connections.c# connection_init, 738 4 connections.c# connection_init, 738 ............ 4 log.c# log_error_write, 281 4 log.c# log_error_write, 281 debug 5 plugins_call_handle_uri_raw(srv, con) 4 log.c# log_error_write, 281 4 log.c# log_error_write, 281 debug 5 plugins_call_handle_uri_clean(srv, con) debug slot in plugin: mod_access 5 mod_access.c# mod_access_patch_connection, 89 5 log.c# log_error_write, 281 debug slot in plugin: mod_cache 4 log.c# log_error_write, 281 4 stat_cache.c# hashme, 239 4 log.c# log_error_write, 281 4 stat_cache.c# stat_cache_get_entry, 380 5 stat_cache.c# hashme, 239 5 splaytree.c# splaytree_splay, 64 4 status_counter.c# status_counter_set, 59 5 status_counter.c# status_counter_get_counter, 18 debug slot in plugin: mod_proxy 4 log.c# log_error_write, 281 4 log.c# log_error_write, 281 4 log.c# log_error_write, 281 4 log.c# log_error_write, 281 4 log.c# log_error_write, 281 4 log.c# log_error_write, 281 debug 5 plugins_call_handle_docroot(srv, con) debug slot in plugin: mod_cache
第一列是该函数的栈深度,后面是所在文件和函数名,最后是行号(目前还不十分准确),通过缩进之间的关系,就可以了解函数调用的顺序和相互之间的关系了。这种跟踪方式也有利于我们梳理回调,特别是在关系还没搞清楚和不习惯这种思维方式的时候(我刚开始读 syslog-ng 的时候就是)。
- 数据流: 最后,弄清数据的流向:请求输入 –> 处理 –> 结果输出这样整个的流程,以及对应的数据结构和这个/这些数据结构会经过的其他数据结构实体。例如,在 syslog-ng 中有 struct *LogMessage,在 lighttpd 中,每个 connection *con 都有 request *request 和 response *request
- 配置: 配置的结构,以及如何发散到各个不同的数据结构实体的实现。