阅读源代码系列 转 http://blog.csdn.net/goodfriends2007/article/details/6881883
摘要:本文从阅读源代码的目的和意义开始,主要介绍了怎样阅读别人的源代码,列举了阅读开源代码的例子,以及阅读开源代码工具和阅读源代码的技巧。
引言
大家在软件开发过程中,在加入一个团队后或多或少都会接触到原来的源代码,要不是在原有基础上继续开发或者维护,或者在原有代码上改进优化进行新产品开发,或者在开发一个新模块的时后需要借鉴下类似的开源软件源代码,接下来我讲谈谈对源代码的处理问题。
1阅读源代码的目的和意义
1.1阅读源代码的目的
阅读别人的代码作为软件开发人员来说是一件经常要做的事情。
第一个是在学习新的编程语言的时候通过阅读别人的代码是一个最好的学习方法,也是积累编程经验和技巧的过程。如果你有机会阅读一些操作系统的代码会帮助你理解一些基本的原理。
第二个就是在你作为一个质量保证人员或一个小领导的时候;如果你要做白盒测试的时候没有阅读代码的能力是不能完成相应的任务。
第三个就是如果你中途接手一个项目的时候或给一个项目做代码维护和优化的时候是要有阅读代码的能力的。
1.2.阅读源代码意义
因为源代码的处理在程序员的软件开发中不可避免,那么在阅读源代码中有哪些意义呢?
第一个意义是可以学习到很多编程的方法和技巧。看好的源代码,对于提高自己的编程水平,比自己写源代码的帮助更大。当然不是说不用自己写,而是说,自己写代码的同时,可以从别人写的好的源代码中间学习到更多的编程方法和技巧。
第二个意义是,可以提高自己把握大规模源代码的能力。一个比较大型的程序,往往都是经过了很多个版本很长的时间,有很多人参与开发,修正错误,添加功能而发展起来的。所以往往源代码的规模都比较大,少则10-100多k, 多的有好几十个MB. 在阅读大量源代码的时候,能够提高自己对大的软件的把握能力,快速了解脉络,熟悉细节,不仅仅是编程技巧,还能在程序的架构,设计方面提高自己的能力。在设计模式的书中就提到,设计模式并不是一本教材,不是教你如何去编程序,而是把平时编程中一些固定的模式记录下来,加以不断的测试和改进,分发给广大程序员的一些经验之谈。
第三个意义,就是获得一些好的思想。比如有很多人在开始一个软件项目之前都喜欢到sourceforge.net上去找一下,是否有人以前做过相同或者相似的软件,如果有,则拿下来读一读,可以使自己对这个软件项目有更多更深的认识。
2怎样阅读别人的源代码
2.1收集所有可能收集的材料
阅读代码要做的第一件事情是收集所有和项目相关的资料。比如你要做一个项目的维护和优化,那么你首先要搞明白项目做什么用的,那么调研、需求分析文档、概要设计文档、详细设计文档、测试文档、使用手册都是你要最先搞到手的。如果你是为了学习那么尽量收集和你的学习有关的资料,比如你想学习linux的文件系统的代码,那最好要找到linux的使用手册、以及文件系统设计的方法、数据结构的说明。这些资料在网上或书店里都可以找到。
材料的种类分为几种类型:
a、基础资料。
比如你阅读turbo c2的源代码你要有turbo c2的函数手册,使用手册等专业书籍;java 的话不但要有函数手册,还要有类库函数手册。这些资料都是你的基础资料。另外你要有一些关于uml的资料可以作为查询手册也是一个不错的选择。
b、和程序相关的专业资料。
每一个程序都是和相关行业相关的。比如我阅读过一个关于气象分析方面的代码,因为里边用到了一个复杂的数据转换公式,所以不得不把自己的大学时候课本找出来来复习一下高等数学的内容。如果你想阅读linux的文件管理的代码,那么找一本讲解linux文件系统的书对你的帮助会很大。
c、相关行业的文档资料
这一部分的资料分为两种,一个相关行业的资料,比如你要阅读一个税务系统的代码那么有一些财务/税务系统的专业资料和国家的相关的法律、法规的资料是必不可少的。
2.2留备份,构造可运行的环境
了解基础知识,不要上来就阅读代码,打好基础可以做到事半功倍的效果,代码拿到手之后的第一件事情是先做备份,最好是刻在一个光盘上,同时上传在版本控制CVS、SVN或GIT库,在代码阅读的时候一点不动代码是很困难的一件事情,特别是你要做一些修改性或增强性维护的时候。而一旦做修改就可能发生问题,到时候要恢复是经常发生的事情,如果你不能很好的使用版本控制软件那么先留一个备份是一个最起码的要求了。
在做完备份之后最好给自己构造一个可运行的环境,当然可能会很麻烦,但可运行代码和不可运行的代码阅读起来难度会差很多的(运行的代码可以debug,单步跟,数据流是怎么走的,可以比较容易理清动态流程)。所以多用一点时间搭建一个环境是很值得的,而且我们阅读代码主要是为了修改其中的问题或做移植操作。不能运行的代码除了可以学到一些技术以外,用处有限。
2.3 找阅读开始的地方
做什么事情都要知道从那里开始,读程序也不例外。在c语言里,首先要找到main()函数,然后逐层去阅读,其他的程序无论是VC、delphi、Java都要首先找到程序头,否则你是很难分析清楚程序的层次关系。
2.4分层次阅读
在阅读代码的时候不要一头就扎下去,这样往往容易只见树木不见森林,阅读代码比较好的方法有一点像二叉树的广度优先的遍历。在程序主体一般会比较简单,调用的函数会比较少,根据函数的名字以及层次关系一般可以确定每一个函数的大致用途,将你的理解作为注解写在这些函数的边上。当然很难一次就将全部注解都写正确,有时候甚至可能是你猜测的结果,不过没有关系这些注解在阅读过程是不断修正的,直到你全部理解了代码为止。一般来说采用逐层阅读的方法可以是你系统的理解保持在一个正确的方向上。避免一下子扎入到细节的问题上。在分层次阅读的时候要注意一个问题,就是将系统的函数和开发人员编写代码区分开。在c, c++,java ,delphi中都有自己的系统函数,不要去阅读这些系统函数,除非你要学习他们的编程方法,否则只会浪费你的时间。将系统函数表示出来,注明它们的作用即可,区分系统函数和自编函数有几个方法,一个是系统函数的编程风格一般会比较好,而自编的函数的编程风格一般比较会比较差。从变量名、行之间的缩进、注解等方面一般可以分辨出来,另外一个是像DELPHI会在你编程的时候给你生成一大堆文件出来,其中有很多文件是你用不到了,可以根据文件名来区分一下时候是系统函数,最后如果你实在确定不了,那就用开发系统的帮助系统去查一下函数名,对一下参数等来确定即可。
2.5写注解
写注解是在阅读代码中最重要的一个步骤,在我们阅读的源代码一般来说是我们不熟悉的系统,阅读别人的代码一般会有几个问题:
a、搞明白别人的编程思想不是一件很容易的事情,即使你知道这段程序的思路的时候也是一样。
b、阅读代码的时候代码量一般会比较大,如果不及时写注解往往会造成读明白了后边忘了前边的现象。
c、阅读代码的时候难免会出现理解错误,如果没有及时的写注解很难及时的发现这些错误。
d、不写注解有时候你发生你很难确定一个函数你什么时候阅读过,它的功能是什么,经常会发生重复阅读、理解的现象。
一些写注解的基本方法:
a、猜测的去写,刚开始阅读一个代码的时候,你很难一下子就确定所有的函数的功能,不妨采用采用猜测的方法去写注解,根据函数的名字、位置写一个大致的注解,当然一般会有错误,但你的注解实际是不但调整的,直到最后你理解了全部代码。
b、按功能去写,别把注解写成语法说明书,千万别看到fopen就写打开文件,看到fread就写读数据,这样的注解一点用处都没有,而应该写在此处打开参数配置文件(****.dat)读出系统初始化参数……,这样才是有用的注解。
c、在写注解的时候另外要注意的一个问题是分清楚系统自动生成的代码和用户自己开发的代码,一般来说没有必要给系统自动生成的代码写注解。像delphi的代码,我们往往要自己编写一些自己的代码段,还要对一些系统自动生成的代码段进行修改,这些代码在阅读过程是要写注解的,但有一些没有修改过的自动生成的代码就没有必要写注解了。
d、在主要代码段要写较为详细的注解。有一些函数或类在程序中起关键的作用,那么要写比较详细的注解。这样对你理解代码有很大的帮助。
e、对你理解起来比较困难的地方要写详细的注解,在这些地方往往会有一些编程的技巧。不理解这些编程技巧对你以后的理解或移植会有问题。
f、写中文注解。如果你的英文足够的好,不用看这条了,但很多的人英文实在不怎么样,那就写中文注解吧,我们写注解是为了加快自己的理解速度。中文在大多数的时候比英文更适应中国人。与其写一些谁也看不懂的英文注解还不如不写。
2.6重复阅读
一次就可以将所有的代码都阅读明白的人是没有的。至少我还没有遇到过。反复的去阅读同一段代码有助于代码的理解。一般来说,在第一次阅读代码的时候你可以跳过很多一时不明白的代码段,只写一些简单的注解,在以后的重复阅读过程用,你对代码的理解会比上一次理解的更深刻,这样你可以修改那些注解错误的地方和上一次没有理解的对方。一般来说,对代码阅读3,4次基本可以理解代码的含义和作用。(书读百遍,其意自现)
2.7运行并修改代码
如果你的代码是可运行的,那么先让它运行起来,用单步跟踪的方法来阅读代码,会提高你的代码理解速度。通过看中间变量了解代码的含义,而且对以后的修改会提供很大的帮助。
用自己的代码代替原有代码,看效果,但在之前要保留源代码。600行的一个函数,阅读起来很困难,编程的人不是一个好的习惯。在阅读这个代码的时候将代码进行修改,变成了14个函数。每一个大约是40-50 行左右。
3怎样阅读开源代码的例子
开源代码在linux下多一些,下面借鉴别的一个例子对阅读源代码的过程了解下。
我找的例子是一个统计日志的工具,webalizer。之所以选择这个软件来作为例子,一方面是因为它是用C写的,流程比较简单,没有C++的程序那么多的枝节,而且软件功能不算复杂,代码规模不大,能够在一篇文章的篇幅里面讲完; 另外一个方面是因为恰巧前段时间我因为工作的关系把它拿来修改了一下,刚看过,还没有忘记。我采用的例子是webalizer2.01-09,也可以到它的网站http://www.mrunix.net/webalizer/ 下载最新的版本。这是一个用C写的,处理文本文件(简单的说是这样,实际上它支持三种日志文本格式:CLF, FTP, SQUID), 并且用html的方式输出结果。读者可以自己去下载它的源代码包,并一边读文章,一边看程序。解压缩它的tar包(我download的是它的源代码tar包),在文件目录中看到这样的结果:
$ ls
aclocal.m4 dns_resolv.c lang output.h webalizer.1 CHANGES dns_resolv.h lang.h parser.c webalizer.c configure graphs.c linklist.c parser.h webalizer.h configure.in graphs.h linklist.h preserve.c webalizer_lang.h COPYING hashtab.c Makefile.in preserve.h webalizer.LSM Copyright hashtab.h Makefile.std README webalizer.png country-codes.txt INSTALL msfree.png README.FIRST DNS.README install-sh output.c sample.conf
首先,我阅读了它的README(这是很重要的一个环节), 大体了解了软件的功能,历史状况,修改日志,安装方法等等。然后是安装并且按照说明中的缺省方式来运行它,看看它的输出结果。(安装比较简单,因为它带了一个configure, 在没有特殊情况出现的时候,简单的./configure, make, make install就可以安装好。)然后就是阅读源代码了。我从makefile开始入手(我觉得这是了解一个软件的最好的方法)在makefile开头,有这些内容:
prefix = /usr/local
exec_prefix = ${prefix}
BINDIR = ${exec_prefix}/bin
MANDIR = ${prefix}/man/man1
ETCDIR = /etc
CC = gcc
CFLAGS = -Wall -O2
LIBS = -lgd -lpng -lz -lm
DEFS = -DETCDIR="/etc" -DHAVE_GETOPT_H=1 -DHAVE_MATH_H=1
LDFLAGS=
INSTALL= /usr/bin/install -c
INSTALL_PROGRAM=${INSTALL}
INSTALL_DATA=${INSTALL} -m 644
# where are the GD header files?
GDLIB=/usr/include
这些定义了安装的路径,执行程序的安装路径,编译器,配置文件的安装路径,编译的选项,安装程序,安装程序的选项等等。要注意的是,这些并不是软件的作者写的,而是./configure的输出结果。下面才是主题内容,也是我们关心的。
# Shouldn't have to touch below here!
all: webalizer
webalizer: webalizer.o webalizer.h hashtab.o hashtab.h linklist.o linklist.h preserve.o preserve.h dns_resolv.o dns_resolv.h parser.o parser.h output.o output.h graphs.o graphs.h lang.h webalizer_lang.h
$(CC) ${LDFLAGS} -o webalizer webalizer.o hashtab.o linklist.o preserv e.o parser.o output.o dns_resolv.o graphs.o ${LIBS}
rm -f webazolver
ln -s webalizer webazolver
webalizer.o: webalizer.c webalizer.h parser.h output.h preserve.h graphs.h dns_resolv.h webalizer_lang.h
$(CC) ${CFLAGS} ${DEFS} -c webalizer.c
parser.o: parser.c parser.h webalizer.h lang.h
$(CC) ${CFLAGS} ${DEFS} -c parser.c
hashtab.o: hashtab.c hashtab.h dns_resolv.h webalizer.h lang.h
$(CC) ${CFLAGS} ${DEFS} -c hashtab.c
linklist.o: linklist.c linklist.h webalizer.h lang.h
$(CC) ${CFLAGS} ${DEFS} -c linklist.c
output.o: output.c output.h webalizer.h preserve.h
hashtab.h graphs.h lang.h
$(CC) ${CFLAGS} ${DEFS} -c output.c
preserve.o: preserve.c preserve.h webalizer.h parser.h
hashtab.h graphs.h lang.h
$(CC) ${CFLAGS} ${DEFS} -c preserve.c
dns_resolv.o: dns_resolv.c dns_resolv.h lang.h webalizer.h
$(CC) ${CFLAGS} ${DEFS} -c dns_resolv.c
graphs.o: graphs.c graphs.h webalizer.h lang.h
$(CC) ${CFLAGS} ${DEFS} -I${GDLIB} -c graphs.c
好了,不用再往下看了,这些就已经足够了。从这里我们可以看到这个软件的几个源代码文件和他们的结构。webalizer.c是主程序所在的文件,其他的是一些辅助程序模块。对比一下目录里面的文件,
$ ls *.c *.h
dns_resolv.c graphs.h lang.h output.c parser.h webalizer.c
dns_resolv.h hashtab.c linklist.c output.h preserve.c webalizer.h
graphs.c hashtab.h linklist.h parser.c preserve.h webalizer_lang.h
于是,让我们从webalizer.c开始吧。
作为一个C程序,在头文件里面,和C文件里面定义的extern变量,结构等等肯定不会少,但是,单独看这些东西我们不可能对这个程序有什么认识。所以,从main函数入手,逐步分析,在需要的时候再回头来看这些数据结构定义才是好的方法。(顺便说一句,Visual C++, 等windows下的IDE工具提供了很方便的方法来获取函数列表,C++的类列表以及资源文件,对于阅读源代码很有帮助。Unix/Linux也有这些工具,但是,我们在这里暂时不说,而只是通过最简单的文本编辑器vi来讲)。跳过webalizer.c开头的版权说明部分(GPL的),和数据结构定义,全局变量声明部分,直接进入main()函数。在函数开头,我们看到:
/* initalize epoch */
epoch=jdate(1,1,1970); /* used for timestamp adj. */
/* add default index. alias */
add_nlist("index.",&index_alias);
这两个函数暂时不用仔细看,后面会提到,略过。
sprintf(tmp_buf,"%s/webalizer.conf",ETCDIR);
/* check for default config file */
if (!access("webalizer.conf",F_OK))
get_config("webalizer.conf");
else if (!access(tmp_buf,F_OK))
get_config(tmp_buf);
从注释和程序本身可以看出,这是查找是否存在一个叫做webalizer.conf的配置文件,如果当前目录下有,则用get_config来读入其中内容,如果没有,则查找ETCDIR/webalizer.conf是否存在。如果都没有,则进入下一部分。(注意:ETCDIR = @ETCDIR@在makefile中有定义)
/* get command line options */
opterr = 0; /* disable parser errors */
while ((i=getopt(argc,argv,"a:A:c:C:dD:e:E:fF:g:GhHiI:l:Lm:M:n:N:o:pP:qQr:R:s:S:t:Tu:U:vVx:XY"))!=EOF)
{
switch (i)
{
case 'a': add_nlist(optarg,&hidden_agents); break; /* Hide agents */
case 'A': ntop_agents=atoi(optarg); break; /* Top agents */
case 'c': get_config(optarg); break; /* Config file */
case 'C': ntop_ctrys=atoi(optarg); break; /* Top countries */
case 'd': debug_mode=1; break; /* Debug */
case 'D': dns_cache=optarg; break; /* DNS Cache filename */
case 'e': ntop_entry=atoi(optarg); break; /* Top entry pages */
case 'E': ntop_exit=atoi(optarg); break; /* Top exit pages */
case 'f': fold_seq_err=1; break; /* Fold sequence errs */
case 'F': log_type=(optarg[0]=='f')?
LOG_FTP:(optarg[0]=='s')?
LOG_SQUID:LOG_CLF; break; /* define log type */
case 'g': group_domains=atoi(optarg); break; /* GroupDomains (0=no) */
case 'G': hourly_graph=0; break; /* no hourly graph */
case 'h': print_opts(argv[0]); break; /* help */
case 'H': hourly_stats=0; break; /* no hourly stats */
case 'i': ignore_hist=1; break; /* Ignore history */
case 'I': add_nlist(optarg,&index_alias); break; /* Index alias */
case 'l': graph_lines=atoi(optarg); break; /* Graph Lines */
case 'L': graph_legend=0; break; /* Graph Legends */
case 'm': visit_timeout=atoi(optarg); break; /* Visit Timeout */
case 'M': mangle_agent=atoi(optarg); break; /* mangle user agents */
case 'n': hname=optarg; break; /* Hostname */
case 'N': dns_children=atoi(optarg); break; /* # of DNS children */
case 'o': out_dir=optarg; break; /* Output directory */
case 'p': incremental=1; break; /* Incremental run */
case 'P': add_nlist(optarg,&page_type); break; /* page view types */
case 'q': verbose=1; break; /* Quiet (verbose=1) */
case 'Q': verbose=0; break; /* Really Quiet */
case 'r': add_nlist(optarg,&hidden_refs); break; /* Hide referrer */
case 'R': ntop_refs=atoi(optarg); break; /* Top referrers */
case 's': add_nlist(optarg,&hidden_sites); break; /* Hide site */
case 'S': ntop_sites=atoi(optarg); break; /* Top sites */
case 't': msg_title=optarg; break; /* Report title */
case 'T': time_me=1; break; /* TimeMe */
case 'u': add_nlist(optarg,&hidden_urls); break; /* hide URL */
case 'U': ntop_urls=atoi(optarg); break; /* Top urls */
case 'v':
case 'V': print_version(); break; /* Version */
case 'x': html_ext=optarg; break; /* HTML file extension */
case 'X': hide_sites=1; break; /* Hide ind. sites */
case 'Y': ctry_graph=0; break; /* Supress ctry graph */
}
}
if (argc - optind != 0) log_fname = argv[optind];
if ( log_fname && (log_fname[0]=='-')) log_fname=NULL; /* force STDIN? */
/* check for gzipped file - .gz */
if (log_fname) if (!strcmp((log_fname+strlen(log_fname)-3),".gz")) gz_log=1;
这一段是分析命令行参数及开关。(getopt()的用法我在另外一篇文章中讲过,这里就不再重复了。)可以看到,这个软件虽然功能不太复杂,但是开关选项还是不少。大多数的unix/linux程序的开头部分都是这个套路,初始化配置文件,并且读入分析命令行。在这段程序中,我们需要注意一个函数:add_nlist(). print_opts(), get_config()等等一看就明白,就不用多讲了。这里我们已经是第二次遇到add_nlist这个函数了,就仔细看看吧。
$ grep add_nlist *.h
linklist.h:extern int add_nlist(char *, NLISTPTR *); /* add list item */
可以发现它定义在linklist.h中。
在这个h文件中,当然会有一些数据结构的定义,比如:
struct nlist { char string[80]; /* list struct for HIDE items */
struct nlist *next; };
typedef struct nlist *NLISTPTR;
struct glist { char string[80]; /* list struct for GROUP items */
char name[80];
struct glist *next; };
typedef struct glist *GLISTPTR;
这是两个链表结构。还有
extern GLISTPTR group_sites ; /* "group" lists */
extern GLISTPTR group_urls ;
extern GLISTPTR group_refs ;
这些都是链表, 太多了,不用一一看得很仔细,因为目前也看不出来什么东西。当然要注意它们是extern的, 也就是说,可以在其他地方(文件)看到它们的数值(类似于C++中的public变量)。这里还定义了4个函数:
extern char *isinlist(NLISTPTR, char *); /* scan list for str */
extern char *isinglist(GLISTPTR, char *); /* scan glist for str */
extern int add_nlist(char *, NLISTPTR *); /* add list item */
extern int add_glist(char *, GLISTPTR *); /* add group list item */
注意,这些都是extern的,也就是说,可以在其他地方见到它们的调用(有点相当于C++中的public函数)。再来看看linklist.c,
NLISTPTR new_nlist(char *); /* new list node */
void del_nlist(NLISTPTR *); /* del list */
GLISTPTR new_glist(char *, char *); /* new group list node */
void del_glist(GLISTPTR *); /* del group list */
int isinstr(char *, char *);
这5个函数是内部使用的(相当于C++中的private), 也就是说,这些函数只被isinlist(NLISTPTR, char *), isinglist(GLISTPTR, char *), add_nlist(char *, NLISTPTR *), add_glist(char *, GLISTPTR *)调用,而不会出现在其他地方。所以,我们先来看这几个内部函数。举例来说,
add_nlist(char *)
NLISTPTR new_nlist(char *str)
{
NLISTPTR newptr;
if (sizeof(newptr->string) < strlen(str))
{
if (verbose)
fprintf(stderr,"[new_nlist] %s ",msg_big_one);
}
if (( newptr = malloc(sizeof(struct nlist))) != NULL)
{strncpy(newptr->string, str, sizeof(newptr->string));newptr->next=NULL;}
return newptr;
}
这个函数分配了一个struct nlist, 并且把其中的string赋值为str, next赋值为NULL.这实际上是创建了链表中的一个节点。verbose是一个全局变量,定义了输出信息的类型,如果verbose为1,则输出很详细的信息,否则输出简略信息。这是为了调试或者使用者详细了解程序情况来用的。不是重要内容,虽然我们常常可以在这个源程序的其他地方看到它。另外一个函数:
void del_nlist(NLISTPTR *list)
{
NLISTPTR cptr,nptr;
cptr=*list;
while (cptr!=NULL)
{
nptr=cptr->next;
free(cptr);
cptr=nptr;
}
}
这个函数删除了一个nlist(也可能是list所指向的那一个部分开始知道链表结尾),比较简单。看完了这两个内部函数,可以来看
/*********************************************/
/* ADD_NLIST - add item to FIFO linked list */
/*********************************************/
int add_nlist(char *str, NLISTPTR *list)
{
NLISTPTR newptr,cptr,pptr;
if ( (newptr = new_nlist(str)) != NULL)
{
if (*list==NULL) *list=newptr;
else
{
cptr=pptr=*list;
while(cptr!=NULL) { pptr=cptr; cptr=cptr->next; };
pptr->next = newptr;
}
}
return newptr==NULL;
}
这个函数是建立了一个新的节点,把参数str赋值给新节点的string, 并把它连接到list所指向链表的结尾。另外的三个函数:new_glist(), del_glist(), add_glist()完成的功能和上述三个差不多,所不同的只是它们所处理的数据结构不同。看完了这几个函数,我们回到main程序。接下来是,
/* setup our internal variables */
init_counters(); /* initalize main counters */
我们所阅读的这个软件是用来分析日志并且做出统计的,那么这个函数的名字已经告诉了我们,这是一个初始化计数器的函数。简略的看看吧!
$ grep init_counters *.h
webalizer.h:extern void init_counters();
在webalizer.c中找到:
void init_counters()
{
int i;
for (i=0;i for (i=0;i<31;i++) /* monthly totals */
{
tm_xfer[i]=0.0;
tm_hit[i]=tm_file[i]=tm_site[i]=tm_page[i]=tm_visit[i]=0;
}
for (i=0;i<24;i++) /* hourly totals */
{
th_hit[i]=th_file[i]=th_page[i]=0;
th_xfer[i]=0.0;
}
......
}略过去一大串代码,不用看了,肯定是计数器清0。在主程序中,接下来是:
if (page_type==NULL) /* check if page types present */
{
if ((log_type == LOG_CLF) || (log_type == LOG_SQUID))
{
add_nlist("htm*" ,&page_type); /* if no page types specified, we */
add_nlist("cgi" ,&page_type); /* use the default ones here... */
if (!isinlist(page_type,html_ext)) add_nlist(html_ext,&page_type);
}
else add_nlist("txt" ,&page_type); /* FTP logs default to .txt */
}
page_type这个变量在前面见过,
case 'P': add_nlist(optarg,&page_type); break; /* page view types
根据在最开始读过的README文件,这个page_type是用来定义处理的页面的类型的。在README文件中,
-P name Page type. This is the extension of files you consider to be pages for Pages calculations (sometimes called 'pageviews'). The default is 'htm*' and 'cgi' (plus whatever HTMLExtension you specified if it is different). Don't use a period!
我们在程序中也可以看到,如果没有在命令行中或者config文件中指定,则根据处理的日志文件的类型来添加缺省的文件类型。比如对于CLF文件(WWW日志),处理html, htm, cgi文件
if (log_type == LOG_FTP)
{
/* disable stuff for ftp logs */
ntop_entry=ntop_exit=0;
ntop_search=0;
}
else
.....
这一段是对于FTP的日志格式,设置搜索列表。
for (i=0;i {
sm_htab[i]=sd_htab[i]=NULL; /* initalize hash tables */
um_htab[i]=NULL;
rm_htab[i]=NULL;
am_htab[i]=NULL;
sr_htab[i]=NULL;
}
清空哈西表,为下面即将进行的排序工作做好准备。关于哈西表,这是数据结构中常用的一种用来快速排序的结构,如果不清楚,可以参考相关书籍,比如清华的<<数据结构>>教材或者<<数据结构的C++实现>>等书。
if (verbose>1)
{
uname(&system_info);
printf("Webalizer V%s-%s (%s %s) %s ",
version,editlvl,system_info.sysname,
system_info.release,language);
}
这一段,是打印有关系统的信息和webalizer程序的信息(可以参考uname的函数说明)。
#ifndef USE_DNS
if (strstr(argv[0],"webazolver")!=0)
{
printf("DNS support not present, aborting... ");
exit(1);
}
#endif /* USE_DNS */
这一段,回忆我们在看README文件的时候,曾经提到过可以在编译的时候设置选项开关来设定DNS支持,在源代码中可以看到多次这样的代码段出现,如果不指定DNS支持,这些代码段则会出现(ifdef)或者不出现(ifndef).下面略过这些代码段,不再重复。
/* open log file */
if (gz_log)
{
gzlog_fp = gzopen(log_fname,"rb");
if (gzlog_fp==Z_NULL)
{
/* Error: Can't open log file ... */
fprintf(stderr, "%s %s ",msg_log_err,log_fname);
exit(1);
}
}
else
{
if (log_fname)
{
log_fp = fopen(log_fname,"r");
if (log_fp==NULL)
{
/* Error: Can't open log file ... */
fprintf(stderr, "%s %s ",msg_log_err,log_fname);
exit(1);
}
}
}
这一段,回忆在README文件中曾经读到过,如果log文件是gzip压缩格式,则用gzopen函数打开(可以猜想gz***是一套针对gzip压缩格式的实时解压缩函数),如果不是,则用fopen打开。
/* switch directories if needed */
if (out_dir)
{
if (chdir(out_dir) != 0)
{
/* Error: Can't change directory to ... */
fprintf(stderr, "%s %s ",msg_dir_err,out_dir);
exit(1);
}
}
同样,回忆在README文件中读到过,如果参数行有-o out_dir, 则将输出结果到该目录,否则,则输出到当前目录。在这一段中,如果输出目录不存在(chdir(out_dir) != 0)则出错。
#ifdef USE_DNS
if (strstr(argv[0],"webazolver")!=0)
{
if (!dns_children) dns_children=5; /* default dns children if needed */
if (!dns_cache)
{
/* No cache file specified, aborting... */
fprintf(stderr,"%s ",msg_dns_nocf); /* Must have a cache file */
exit(1);
}
}
......
在上面曾经提到过,这是DNS解析的代码部分,可以略过不看,不会影响对整个程序的理解。
/* prep hostname */
if (!hname)
{
if (uname(&system_info)) hname="localhost";
else hname=system_info.nodename;
}
这一段继续处理参数做准备工作。如果在命令行中指定了hostname(机器名)则采用指定的名称,否则调用uname查找机器名,如果没有,则用localhost来作为机器名。(同样在README中说得很详细)
/* get past history */
if (ignore_hist) {if (verbose>1) printf("%s ",msg_ign_hist); }
else get_history();
如果在命令行中指定了忽略历史文件,则不读取历史文件,否则调用get_history()来读取历史数据。在这里,我们可以回想在README文件中同样说过这一细节,在命令行或者配置文件中都能指定这一开关。需要说明的是,我们在这里并不一定需要去看get_history这一函数,因为从函数的名称,README文件和程序注释都能很清楚的得知这一函数的功能,不一定要去看代码。而如果要猜想的话,也可以想到,history是webalizer在上次运行的时候记录下来的一个文件,而这个文件则是去读取它,并将它的数据包括到这次的分析中去。不信,我们可以来看看。
void get_history()
{
int i,numfields;
FILE *hist_fp;
char buffer[BUFSIZE];
/* first initalize internal array */
for (i=0;i<12;i++)
{
hist_month[i]=hist_year[i]=hist_fday[i]=hist_lday[i]=0;
hist_hit[i]=hist_files[i]=hist_site[i]=hist_page[i]=hist_visit[i]=0;
hist_xfer[i]=0.0;
}
hist_fp=fopen(hist_fname,"r");
if (hist_fp)
{
if (verbose>1) printf("%s %s ",msg_get_hist,hist_fname);
while ((fgets(buffer,BUFSIZE,hist_fp)) != NULL)
{
i = atoi(buffer) -1;
if (i>11)
{
if (verbose)
fprintf(stderr,"%s (mth=%d) ",msg_bad_hist,i+1);
continue;
}
/* month# year# requests files sites xfer firstday lastday */
numfields = sscanf(buffer,"%d %d %lu %lu %lu %lf %d %d %lu %lu",
&hist_month[i],
&hist_year[i],
&hist_hit[i],
&hist_files[i],
&hist_site[i],
&hist_xfer[i],
&hist_fday[i],
&hist_lday[i],
&hist_page[i],
&hist_visit[i]);
if (numfields==8) /* kludge for reading 1.20.xx history files */
{
hist_page[i] = 0;
hist_visit[i] = 0;
}
}
fclose(hist_fp);
}
else if (verbose>1) printf("%s ",msg_no_hist);
}
/*********************************************/
/* PUT_HISTORY - write out history file */
/*********************************************/
void put_history()
{
int i;
FILE *hist_fp;
hist_fp = fopen(hist_fname,"w");
if (hist_fp)
{
if (verbose>1) printf("%s ",msg_put_hist);
for (i=0;i<12;i++)
{
if ((hist_month[i] != 0) && (hist_hit[i] != 0))
{
fprintf(hist_fp,"%d %d %lu %lu %lu %.0f %d %d %lu %lu ",
hist_month[i],
hist_year[i],
hist_hit[i],
hist_files[i],
hist_site[i],
hist_xfer[i],
hist_fday[i],
hist_lday[i],
hist_page[i],
hist_visit[i]);
}
}
fclose(hist_fp);
}
else
if (verbose)
fprintf(stderr,"%s %s ",msg_hist_err,hist_fname);
}
在preserve.c中,这两个函数是成对出现的。get_history()读取文件中的数据,并将其记录到hist_开头的一些数组中去。而put_history()则是将一些数据记录到同样的数组中去。我们可以推测得知,hist_数组是全局变量(在函数中没有定义),也可以查找源代码验证。同样,我们可以找一找put_history()出现的地方,来验证刚才的推测是否正确。在webalizer.c的1311行,出现:
month_update_exit(rec_tstamp); /* calculate exit pages */
write_month_html(); /* write monthly HTML file */
write_main_index(); /* write main HTML file */
put_history(); /* write history */
可以知道,推测是正确的。再往下读代码,
if (incremental) /* incremental processing? */
{
if ((i=restore_state())) /* restore internal data structs */
{
/* Error: Unable to restore run data (error num) */
/* if (verbose) fprintf(stderr,"%s (%d) ",msg_bad_data,i); */
fprintf(stderr,"%s (%d) ",msg_bad_data,i);
exit(1);
}
......
}
同样,这也是处理命令行和做数据准备,而且和get_history(), put_history()有些类似,读者可以自己练习一下。下面,终于进入了程序的主体部分, 在做完了命令行分析,数据准备之后,开始从日志文件中读取数据并做分析了。
/*********************************************/
/* MAIN PROCESS LOOP - read through log file */
/*********************************************/
while ( (gz_log)?(our_gzgets(gzlog_fp,buffer,BUFSIZE) != Z_NULL):
(fgets(buffer,BUFSIZE,log_fname?log_fp:stdin) != NULL))
我看到这里的时候,颇有一些不同意作者的这种写法。这一段while中的部分写的比较复杂而且效率不高。因为从程序推断和从他的代码看来,作者是想根据日志文件的类型不同来采用不同的方法读取文件,如果是gzip格式,则用our_gzgets来读取其中一行,如果是普通的文本文件格式,则用fgets()来读取。但是,这段代码是写在while循环中的,每次读取一行就要重复判断一次,明显是多余的而且降低了程序的性能。可以在while循环之前做一次这样的判断,然后就不用重复了。
total_rec++;
if (strlen(buffer) == (BUFSIZE-1))
{
if (verbose)
{
fprintf(stderr,"%s",msg_big_rec);
if (debug_mode) fprintf(stderr,": %s",buffer);
else fprintf(stderr," ");
}
total_bad++; /* bump bad record counter */
/* get the rest of the record */
while ( (gz_log)?(our_gzgets(gzlog_fp,buffer,BUFSIZE)!=Z_NULL):
(fgets(buffer,BUFSIZE,log_fname?log_fp:stdin)!=NULL))
{
if (strlen(buffer) < BUFSIZE-1)
{
if (debug_mode && verbose) fprintf(stderr,"%s ",buffer);
break;
}
if (debug_mode && verbose) fprintf(stderr,"%s",buffer);
}
continue; /* go get next record if any */
}
这一段代码,读入一行,如果这一行超过了程序允许的最大字符数(则是错误的日志数据纪录),则跳过本行剩下的数据,忽略掉(continue进行下一次循环)。同时把total_bad增加一个。如果没有超过程序允许的最大字符数(则是正确的日志数据纪录),则
/* got a record... */
strcpy(tmp_buf, buffer); /* save buffer in case of error */
if (parse_record(buffer)) /* parse the record */
将该数据拷贝到一个缓冲区中,然后调用parse_record()进行处理。我们可以同样的推测一下,get_record()是这个程序的一个主要处理部分,分析了日志数据。在parse_record.c中,有此函数,
/*********************************************/
/* PARSE_RECORD - uhhh, you know... */
/*********************************************/
int parse_record(char *buffer)
{
/* clear out structure */
memset(&log_rec,0,sizeof(struct log_struct));
/*
log_rec.hostname[0]=0;
log_rec.datetime[0]=0;
log_rec.url[0]=0;
log_rec.resp_code=0;
log_rec.xfer_size=0;
log_rec.refer[0]=0;
log_rec.agent[0]=0;
log_rec.srchstr[0]=0;
log_rec.ident[0]=0;
*/
#ifdef USE_DNS
memset(&log_rec.addr,0,sizeof(struct in_addr));
#endif
/* call appropriate handler */
switch (log_type)
{
default:
case LOG_CLF: return parse_record_web(buffer); break; /* clf */
case LOG_FTP: return parse_record_ftp(buffer); break; /* ftp */
case LOG_SQUID: return parse_record_squid(buffer); break; /* squid */
}
}
可以看到,log_rec是一个全局变量,该函数根据日志文件的类型,分别调用三种不同的分析函数。在webalizer.h中,找到该变量的定义,从结构定义中可以看到,结构定义了一个日志文件所可能包含的所有信息(参考CLF,FTP, SQUID日志文件的格式说明)。
/* log record structure */
struct log_struct { char hostname[MAXHOST]; /* hostname */
char datetime[29]; /* raw timestamp */
char url[MAXURL]; /* raw request field */
int resp_code; /* response code */
u_long xfer_size; /* xfer size in bytes */
#ifdef USE_DNS
struct in_addr addr; /* IP address structure */
#endif /* USE_DNS */
char refer[MAXREF]; /* referrer */
char agent[MAXAGENT]; /* user agent (browser) */
char srchstr[MAXSRCH]; /* search string */
char ident[MAXIDENT]; }; /* ident string (user) */
extern struct log_struct log_rec;
先看一下一个parser.c用的内部函数,然后再来以parse_record_web()为例子看看这个函数是怎么工作的,parse_record_ftp, parse_record_squid留给读者自己分析作为练习。
/*********************************************/
/* FMT_LOGREC - terminate log fields w/zeros */
/*********************************************/
void fmt_logrec(char *buffer)
{
char *cp=buffer;
int q=0,b=0,p=0;
while (*cp != '')
{
/* break record up, terminate fields with '' */
switch (*cp)
{
case ' ': if (b || q || p) break; *cp=''; break;
case '"': q^=1; break;
case '[': if (q) break; b++; break;
case ']': if (q) break; if (b>0) b--; break;
case '(': if (q) break; p++; break;
case ')': if (q) break; if (p>0) p--; break;
}
cp++;
}
}
从parser.h头文件中就可以看到,这个函数是一个内部函数,这个函数把一行字符串中间的空格字符用''字符(结束字符)来代替,同时考虑了不替换在双引号,方括号,圆括号中间的空格字符以免得将一行数据错误的分隔开了。(请参考WEB日志的文件格式,可以更清楚的理解这一函数)
int parse_record_web(char *buffer)
{
int size;
char *cp1, *cp2, *cpx, *eob, *eos;
size = strlen(buffer); /* get length of buffer */
eob = buffer+size; /* calculate end of buffer */
fmt_logrec(buffer); /* seperate fields with 's */
/* HOSTNAME */
cp1 = cpx = buffer; cp2=log_rec.hostname;
eos = (cp1+MAXHOST)-1;
if (eos >= eob) eos=eob-1;
while ( (*cp1 != '') && (cp1 != eos) ) *cp2++ = *cp1++;
*cp2 = '';
if (*cp1 != '')
{
if (verbose)
{
fprintf(stderr,"%s",msg_big_host);
if (debug_mode) fprintf(stderr,": %s ",cpx);
else fprintf(stderr," ");
}
while (*cp1 != '') cp1++;
}
if (cp1 < eob) cp1++;
/* skip next field (ident) */
while ( (*cp1 != '') && (cp1 < eob) ) cp1++;
if (cp1 < eob) cp1++;
/* IDENT (authuser) field */
cpx = cp1;
cp2 = log_rec.ident;
eos = (cp1+MAXIDENT-1);
if (eos >= eob) eos=eob-1;
while ( (*cp1 != '[') && (cp1 < eos) ) /* remove embeded spaces */
{
if (*cp1=='') *cp1=' ';
*cp2++=*cp1++;
}
*cp2--='';
if (cp1 >= eob) return 0;
/* check if oversized username */
if (*cp1 != '[')
{
if (verbose)
{
fprintf(stderr,"%s",msg_big_user);
if (debug_mode) fprintf(stderr,": %s ",cpx);
else fprintf(stderr," ");
}
while ( (*cp1 != '[') && (cp1 < eob) ) cp1++;
}
/* strip trailing space(s) */
while (*cp2==' ') *cp2--='';
/* date/time string */
cpx = cp1;
cp2 = log_rec.datetime;
eos = (cp1+28);
if (eos >= eob) eos=eob-1;
while ( (*cp1 != '') && (cp1 != eos) ) *cp2++ = *cp1++;
*cp2 = '';
if (*cp1 != '')
{
if (verbose)
{
fprintf(stderr,"%s",msg_big_date);
if (debug_mode) fprintf(stderr,": %s ",cpx);
else fprintf(stderr," ");
}
while (*cp1 != '') cp1++;
}
if (cp1 < eob) cp1++;
/* minimal sanity check on timestamp */
if ( (log_rec.datetime[0] != '[') ||
(log_rec.datetime[3] != '/') ||
(cp1 >= eob)) return 0;
/* HTTP request */
cpx = cp1;
cp2 = log_rec.url;
eos = (cp1+MAXURL-1);
if (eos >= eob) eos = eob-1;
while ( (*cp1 != '') && (cp1 != eos) ) *cp2++ = *cp1++;
*cp2 = '';
if (*cp1 != '')
{
if (verbose)
{
fprintf(stderr,"%s",msg_big_req);
if (debug_mode) fprintf(stderr,": %s ",cpx);
else fprintf(stderr," ");
}
while (*cp1 != '') cp1++;
}
if (cp1 < eob) cp1++;
if ( (log_rec.url[0] != '"') ||
(cp1 >= eob) ) return 0;
/* response code */
log_rec.resp_code = atoi(cp1);
/* xfer size */
while ( (*cp1 != '') && (cp1 < eob) ) cp1++;
if (cp1 < eob) cp1++;
if (*cp1<'0'||*cp1>'9') log_rec.xfer_size=0;
else log_rec.xfer_size = strtoul(cp1,NULL,10);
/* done with CLF record */
if (cp1>=eob) return 1;
while ( (*cp1 != '') && (*cp1 != ' ') && (cp1 < eob) ) cp1++;
if (cp1 < eob) cp1++;
/* get referrer if present */
cpx = cp1;
cp2 = log_rec.refer;
eos = (cp1+MAXREF-1);
if (eos >= eob) eos = eob-1;
while ( (*cp1 != '') && (*cp1 != ' ') && (cp1 != eos) ) *cp2++ = *cp1++;
*cp2 = '';
if (*cp1 != '')
{
if (verbose)
{
fprintf(stderr,"%s",msg_big_ref);
if (debug_mode) fprintf(stderr,": %s ",cpx);
else fprintf(stderr," ");
}
while (*cp1 != '') cp1++;
}
if (cp1 < eob) cp1++;
cpx = cp1;
cp2 = log_rec.agent;
eos = cp1+(MAXAGENT-1);
if (eos >= eob) eos = eob-1;
while ( (*cp1 != '') && (cp1 != eos) ) *cp2++ = *cp1++;
*cp2 = '';
return 1; /* maybe a valid record, return with TRUE */
}
该函数,一次读入一行(其实是一段日志数据中间的一个域,因为该行数据已经被fmt_logrec分开成多行数据了。根据CLF中的定义,检查该数据并将其拷贝到log_rec结构中去,如果检查该数据有效,则返回1。回到主程序,
/* convert month name to lowercase */
for (i=4;i<7;i++)
log_rec.datetime[i]=tolower(log_rec.datetime[i]);
/* get year/month/day/hour/min/sec values */
for (i=0;i<12;i++)
{
if (strncmp(log_month[i],&log_rec.datetime[4],3)==0)
{ rec_month = i+1; break; }
}
rec_year=atoi(&log_rec.datetime[8]); /* get year number (int) */
rec_day =atoi(&log_rec.datetime[1]); /* get day number */
rec_hour=atoi(&log_rec.datetime[13]); /* get hour number */
rec_min =atoi(&log_rec.datetime[16]); /* get minute number */
rec_sec =atoi(&log_rec.datetime[19]); /* get second number */
....
在parse_record分析完数据之后,做日期的分析,把日志中的月份等数据转换成机器可读(可理解)的数据,并存入到log_rec中去。
if ((i>=12)||(rec_min>59)||(rec_sec>59)||(rec_year<1990))
{
total_bad++; /* if a bad date, bump counter */
if (verbose)
{
fprintf(stderr,"%s: %s [%lu]",
msg_bad_date,log_rec.datetime,total_rec);
......
如果日期,时间错误,则把total_bad计数器增加1,并且打印错误信息到标准错误输出。
good_rec = 1;
/* get current records timestamp (seconds since epoch) */
req_tstamp=cur_tstamp;
rec_tstamp=((jdate(rec_day,rec_month,rec_year)-epoch)*86400)+
(rec_hour*3600)+(rec_min*60)+rec_sec;
/* Do we need to check for duplicate records? (incremental mode) */
if (check_dup)
{
/* check if less than/equal to last record processed */
if ( rec_tstamp <= cur_tstamp )
{
/* if it is, assume we have already processed and ignore it */
total_ignore++;
continue;
}
else
{
/* if it isn't.. disable any more checks this run */
check_dup=0;
/* now check if it's a new month */
if (cur_month != rec_month)
{
clear_month();
cur_sec = rec_sec; /* set current counters */
cur_min = rec_min;
cur_hour = rec_hour;
cur_day = rec_day;
cur_month = rec_month;
cur_year = rec_year;
cur_tstamp= rec_tstamp;
f_day=l_day=rec_day; /* reset first and last day */
}
}
}
/* check for out of sequence records */
if (rec_tstamp/3600 < cur_tstamp/3600)
{
if (!fold_seq_err && ((rec_tstamp+SLOP_VAL)/3600 { total_ignore++; continue; }
else
{
rec_sec = cur_sec; /* if folding sequence */
rec_min = cur_min; /* errors, just make it */
rec_hour = cur_hour; /* look like the last */
rec_day = cur_day; /* good records timestamp */
rec_month = cur_month;
rec_year = cur_year;
rec_tstamp= cur_tstamp;
}
}
cur_tstamp=rec_tstamp; /* update current timestamp */
如果该日期、时间没有错误,则该数据是一个好的数据,将good_record计数器加1,并且检查时间戳,和数据是否重复数据。这里有一个函数,jdate()在主程序一开头我们就遇到了,当时跳了过去没有深究,这里留给读者做一个练习。(提示:该函数根据一个日期产生一个字符串,这个字符串是惟一的,可以检查时间的重复性,是一个通用函数,可以在别的程序中拿来使用)
/*********************************************/
/* DO SOME PRE-PROCESS FORMATTING */
/*********************************************/
/* fix URL field */
cp1 = cp2 = log_rec.url;
/* handle null '-' case here... */
if (*++cp1 == '-') { *cp2++ = '-'; *cp2 = ''; }
else
{
/* strip actual URL out of request */
while ( (*cp1 != ' ') && (*cp1 != '') ) cp1++;
if (*cp1 != '')
{
/* scan to begin of actual URL field */
while ((*cp1 == ' ') && (*cp1 != '')) cp1++;
/* remove duplicate / if needed */
if (( *cp1=='/') && (*(cp1+1)=='/')) cp1++;
while ((*cp1 != ' ')&&(*cp1 != '"')&&(*cp1 != ''))
*cp2++ = *cp1++;
*cp2 = '';
}
}
/* un-escape URL */
unescape(log_rec.url);
/* check for service (ie: http://) and lowercase if found */
if ( (cp2=strstr(log_rec.url,"://")) != NULL)
{
cp1=log_rec.url;
while (cp1!=cp2)
{
if ( (*cp1>='A') && (*cp1<='Z')) *cp1 += 'a'-'A';
cp1++;
}
}
/* strip query portion of cgi scripts */
cp1 = log_rec.url;
while (*cp1 != '')
if (!isurlchar(*cp1)) { *cp1 = ''; break; }
else cp1++;
if (log_rec.url[0]=='')
{ log_rec.url[0]='/'; log_rec.url[1]=''; }
/* strip off index.html (or any aliases) */
lptr=index_alias;
while (lptr!=NULL)
{
if ((cp1=strstr(log_rec.url,lptr->string))!=NULL)
{
if ((cp1==log_rec.url)||(*(cp1-1)=='/'))
{
*cp1='';
if (log_rec.url[0]=='')
{ log_rec.url[0]='/'; log_rec.url[1]=''; }
break;
}
}
lptr=lptr->next;
}
/* unescape referrer */
unescape(log_rec.refer);
......
这一段,做了一些URL字符串中的字符转换工作,很长,我个人认为为了程序的模块化,结构化和可复用性,应该将这一段代码改为函数,避免主程序体太长,造成可读性不强和没有移植性,和不够结构化。跳过这一段乏味的代码,进入到下面一个部分---后处理。
if (gz_log) gzclose(gzlog_fp);
else if (log_fname) fclose(log_fp);
if (good_rec) /* were any good records? */
{
tm_site[cur_day-1]=dt_site; /* If yes, clean up a bit */
tm_visit[cur_day-1]=tot_visit(sd_htab);
t_visit=tot_visit(sm_htab);
if (ht_hit > mh_hit) mh_hit = ht_hit;
if (total_rec > (total_ignore+total_bad)) /* did we process any? */
{
if (incremental)
{
if (save_state()) /* incremental stuff */
{
/* Error: Unable to save current run data */
if (verbose) fprintf(stderr,"%s ",msg_data_err);
unlink(state_fname);
}
}
month_update_exit(rec_tstamp); /* calculate exit pages */
write_month_html(); /* write monthly HTML file */
write_main_index(); /* write main HTML file */
put_history(); /* write history */
}
end_time = times(&mytms); /* display timing totals? */
if (time_me' '(verbose>1))
{
printf("%lu %s ",total_rec, msg_records);
if (total_ignore)
{
printf("(%lu %s",total_ignore,msg_ignored);
if (total_bad) printf(", %lu %s) ",total_bad,msg_bad);
else printf(") ");
}
else if (total_bad) printf("(%lu %s) ",total_bad,msg_bad);
/* get processing time (end-start) */
temp_time = (float)(end_time-start_time)/CLK_TCK;
printf("%s %.2f %s", msg_in, temp_time, msg_seconds);
/* calculate records per second */
if (temp_time)
i=( (int)( (float)total_rec/temp_time ) );
else i=0;
if ( (i>0) && (i<=total_rec) ) printf(", %d/sec ", i);
else printf(" ");
}
这一段,做了一些后期的处理。接下来的部分,我想在本文中略过,留给感兴趣的读者自己去做分析。原因有两点:
a、这个程序在前面结构化比较强,而到了后面结构上有些乱,虽然代码效率还是比较高,但是可重用性不够强, 限于篇幅,我就不再一一解释了。
b、前面分析程序过程中,也对后面的代码做了一些预测和估计,也略微涉及到了后面的代码,而且读者可以根据上面提到的原则来自己分析代码,也作为一个实践吧。4阅读开源代码工具
工欲善其事,必先利其器。阅读源代码最好的工具是understand,用Source Insight也可以,至于Understand工具的使用技巧和方法参见代码阅读分析工具Understand使用总结这篇文章。
5阅读源代码的技巧
最后,对于在这篇文章中提到的分析源代码程序的一些方法做一下小结,以作为本文的结束。
5.1分析一个源代码,一个有效的方法是:
a、阅读源代码的说明文档,比如源代码中的README, 作者写的非常的详细,仔细读过之后,在阅读程序的时候往往能够从README文件中找到相应的说明,从而简化了源程序的阅读工作。
b、如果源代码有文档目录,一般为doc或者docs, 最好也在阅读源程序之前仔细阅读,因为这些文档同样起了很好的说明注释作用。
c、从makefile文件入手,分析源代码的层次结构,找出哪个是主程序,哪些是函数包。这对于快速把握程序结构有很大帮助。
d、从main函数入手,一步一步往下阅读,遇到可以猜测出意思来的简单的函数,可以跳过。但是一定要注意程序中使用的全局变量(如果是C程序),可以把关键的数据结构说明拷贝到一个文本编辑器中以便随时查找。
e、分析函数包(针对C程序),要注意哪些是全局函数,哪些是内部使用的函数,注意extern关键字。对于变量,也需要同样注意。先分析清楚内部函数,再来分析外部函数,因为内部函数肯定是在外部函数中被调用的。
f、需要说明的是数据结构的重要性:对于一个C程序来说,所有的函数都是在操作同一些数据,而由于没有较好的封装性,这些数据可能出现在程序的任何地方,被任何函数修改,所以一定要注意这些数据的定义和意义,也要注意是哪些函数在对它们进行操作,做了哪些改变。
g、在阅读程序的同时,最好能够把程序存入到SVN之类的版本控制器中去,在需要的时候可以对源代码做一些修改试验,因为动手修改是比仅仅是阅读要好得多的读程序的方法。在你修改运行程序的时候,可以从SVN中把原来的代码调出来与你改动的部分进行比较(diff命令), 可以看出一些源代码的优缺点并且能够实际的练习自己的编程技术。
h、阅读程序的同时,要注意一些小工具的使用,能够提高速度,比如vi中的查找功能,模式匹配查找,做标记,还有grep,find这两个最强大最常用的文本搜索工具的使用。
i、对于一个大的项目,首先要弄清项目的框架结构和各个项目模块的功能(输入什么,处理以后输出什么). 在这一点上Ant工具做的相当到位,通过编写build.xml和xml的良好的语法结构可以清楚的看到框架。Make工具也做比较出色。具体细节可参考GNU Make /Apache Ant Manual和程序的build.xml或makefile文件。
j、参照源代码和对应文档及业务知识 掌握各个项目模块的主流程也就是先从每个模块的main函数开始,按照顺序列出所用的函数,试着画流程图。注意:对于列出的函数我们现在只关心输入什么,处理后输出什么即函数的功能,不关心函数的实现,用UltraEdit32最新版阅读时十分方便。(用sourceinsight或者understand阅读源代码工具更好)
k、以上两步熟悉以后,在进一步熟悉各个项目模块的主流程,要弄清各个自定义函数的具体实现(标准库函数除外 原因:由厂商提供,厂商只提供函数的功能)。
l、在每一步都要做好源代码阅读笔记,总结方法和技巧。每个项目的源代码阅读要多读几遍,书读百遍,其义之见,定期与同仁切磋交流。
m、提出更好的解决方案,(按照软件工程的设计步骤)评估方案的性能(界面,易用性,内存等方面).
n、每日构建 具体参考构建工具和相关文挡,接着,看一看大师是如何做的。一般,要初步了解人家的框架模型,(这可以通过追踪一些核心函数/类得到一些印象,或者开发文档等);进一步,找到核心数据结构,核心数据结构会直接影响代码的质量。(曾经有人说:我不要看你的程序,让我看一看你的数据结构!)事实上,当你完全读懂它的数据结构时,在来阅读源代码,就是水到渠成的事了。
5.2 Unix/Linux下面以命令行方式运行的程序
对于一个Unix/Linux下面以命令行方式运行的程序,有这么一些套路,大家可以在阅读程序的时候作为参考。
a、在程序开头,往往都是分析命令行,根据命令行参数对一些变量或者数组,或者结构赋值,后面的程序就是根据这些变量来进行不同的操作。
b、分析命令行之后,进行数据准备,往往是计数器清空,结构清零等等。
c、在程序中间有一些预编译选项,可以在makefile中找到相应部分。
d、注意程序中对于日志的处理,和调试选项打开的时候做的动作,这些对于调试程序有很大的帮助。
e、注意多线程对数据的操作。
对于开源软件,下列方法也不错:
先用看看实现的功能,如果自己会咋样设计实现,查看相关文档,多次阅读理解其中设计模式、架构、算法实现细节
5.3 开源软件
开源项目已阅读了不少,总结下来按照下面的steps来操作比较恰当:
a阅读features。以此来搞清楚该项目有哪些特性
b思考。想想如果自己来做有这些features的项目该如何构架
c下载并安装demo或sample。通过demo或sample直观地感受这个项目
d搜集能得到的doc,尽快地掌握如何使用这个项目
e如果有介绍项目架构的文档,通过它了解项目的总体架构,如果没有,通过api-doc了解源码包的结构
f分两遍来阅读源码。第一遍以应用为线索,以总体结构为基础,阅读在应用中使用到的类和方法,但不用过深挖掘细节,对于嵌套调用,只用通过函数名了解最上层函数的意义,这一遍的目的在于把大致结构了然于心。第二遍就是阅读类和方法的实现细节,以第一遍的阅读为基础,带着疑问去阅读那些自己难以实现的模块。
g总结。回味这个项目设计上的精妙,用到了哪些设计模式,能在哪些领域可以借鉴等等。
结束语:
当然,在这篇文章中,并没有阐述所有的阅读源代码的方法和技巧,也没有涉及面向对象程序的阅读方法。我想把这些留到以后再做讨论。也请大家可以就这些话题展开讨论。
6、参考文献
1)、怎样阅读源代码
2)、如何阅读源代码
3)、如何阅读别人的代码
注:因一些问题有疑惑或需要经验的指导,在网上搜索了一些文章,经过自己的体会整理成这篇博客,有些文章拷贝时没记录原作者而只能在参考文献中列出文章名称,在此对原作者的开源共享精神十分感谢,也希望和大家多交流,不当之处请指教,谢谢!