如何阅读源代码(转)

 写在前面的话:
自从我在linuxaid.com.cn上发表一些文章开始,就不断的有网友发来电子邮件,或是就其中某些问题进行探讨,或是查询其他文章的地址(往
往这些网友看的是其他网站转载的我的文章),我很高兴自己写出的文章有这么多人回应,因为这是对我最好的赞赏,也很高兴有这么多人对我的文章感兴趣。但是
常常因为工作关系。有很多邮件是询问我的其他文章在哪里能够找到,我不一定能够及时回复,也觉得回复同样的问题比较麻烦,所以在这里重复一下,我为
linuxaid.com.cn写的文章都能在www.linuxaid.com.cn的应用研发栏目中找到,我的一部分文章收集在
bambi.may10.ca/~ariesram/articles下面(这是个很简陋的网页,只有文本格式的文章,也欢迎有兴趣的网友帮我设计一下
网页),我的邮件地址:ariesram@linuxaid.com.cn,
或ariesram@may10.ca。请转载文章的网站保留这一说明,欢迎网友写email给我探讨问题,虽然我不能确保能及时回复。
正文:
由于工作的关系,我常常需要读一些源代码,并在上面做一些修改并且拿来使用,或是借鉴其中的某些部分。能够说,open
source对于程式员来说,是很有意义的事情。根据我的经验,读源代码,至少有3个好处。第一个好处是能够学习到很多编程的方法,看好的源代码,对于提
高自己的编程水平,比自己写源代码的帮助更大。当然不是说不用自己写,而是说,自己写代码的同时,能够从别人写的好的源代码中间学习到更多的编程方法和技
巧。第二个好处是,能够提高自己把握大规模源代码的能力。一个比较大型的程式,往往都是经过了很多个版本很长的时间,有很多人参和研发,修正错误,添加功
能而发展起来的。所以往往源代码的规模都比较大,少则10-100多k, 多的有好几十个MB.
在阅读大量源代码的时候,能够提高自己对大的软件的把握能力,快速了解脉络,熟悉细节,不但仅是编程技巧,还能在程式的架构,设计方面提高自己的能力。
(这里说一句题外话,>这本书相信很多人都看过,而且很多人对他推崇备至,奉为经典。现在也出了不少书,都是冠以
"设计模式"这一名称。在书中就提到,设计模式并不是一本教材,不是教您如何去编程式,而是把平时编程中一些固定的模式记录下来,加以不断的测试和改进,
分发给广大程式员的一些经验之谈。我在看这本书的时候,有一些地方一些设计方法往往让我有似曾相识的感觉,另外一些则是我以前就常常用到的。而这些经验的
获得,一部分得益于自己的编码过程,另外一个很重要的来源就是阅读别人写的源代码。)阅读源代码第三个好处,就是获得一些好的思想。比如,有很多人在开始
一个软件项目之前都喜欢到sourceforge.net上去找一下,是否有人以前做过相同或相似的软件,假如有,则拿下来读一读,能够使自己对这个软
件项目有更多更深的认识。我以前曾想找一本关于如何阅读源代码的书来看看,却没有找到。相反,倒是找到了不少分析源代码的书,比如Linux
kernel, Apache source,
等等。所以我想,为什么不自己来写下一些经验和大家交流呢?(当然不是写书,没有那个能力也没有那个时间。)所以在这里我准备用一个例子来写一下如何阅读
源代码,分享一些经验,算是抛砖引玉吧!
我找的例子是个统计日志的工具,webalizer.
(这个工具我以前用过,似乎记得以前的版本是用perl写的,不知道现在为什么作者把他完全修改成了C,可能是为了效率,也可能根本就是我记错了。)之所
以选择这个软件来作为例子,一方面是因为他是用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) 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>教材或>等书。
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;i1) 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 ,
&hist_year,
&hist_hit,
&hist_files,
&hist_site,
&hist_xfer,
&hist_fday,
&hist_lday,
&hist_page,
&hist_visit);
if (numfields==8) /* kludge for reading 1.20.xx history files */
{
hist_page = 0;
hist_visit = 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;i0) 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) eos=eob-1;
while ( (*cp1 != '[') && (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) 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)) 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) ) return 0;
/* response code */
log_rec.resp_code = atoi(cp1);
/* xfer size */
while ( (*cp1 != '') && (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) 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) 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=12)||(rec_min>59)||(rec_sec>59)||(rec_year='A') && (*cp1string))!=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
最后,对于在这篇文章中提到的分析源代码程式的一些方法做一下小结,以作为本文的结束。
分析一个源代码,一个有效的方法是:
1、阅读源代码的说明文档,比如本例中的README, 作者写的很的周详,仔细读过之后,在阅读程式的时候往往能够从README文档中找到相应的说明,从而简化了源程式的阅读工作。
2、假如源代码有文档目录,一般为doc或docs, 最好也在阅读源程式之前仔细阅读,因为这些文档同样起了很好的说明注释作用。
3、从makefile文档入手,分析源代码的层次结构,找出哪个是主程式,哪些是函数包。这对于快速把握程式结构有很大帮助。
4、从main函数入手,一步一步往下阅读,碰到能够猜测出意思来的简单的函数,能够跳过。但是一定要注意程式中使用的全局变量(假如是C程式),能够把关键的数据结构说明拷贝到一个文本编辑器中以便随时查找。
5、分析函数包(针对C程式),要注意哪些是全局函数,哪些是内部使用的函数,注意extern关键字。对于变量,也需要同样注意。先分析清楚内部函数,再来分析外部函数,因为内部函数肯定是在外部函数中被调用的。
6、需要说明的是数据结构的重要性:对于一个C程式来说,任何的函数都是在操作同一些数据,而由于没有较好的封装性,这些数据可能出现在程式的任何地方,被任何函数修改,所以一定要注意这些数据的定义和意义,也要注意是哪些函数在对他们进行操作,做了哪些改变。
7、在阅读程式的同时,最好能够把程式存入到cvs之类的版本控制器中去,在需要的时候能够对源代码做一些修改试验,因为动手修改是比仅仅是阅读要好得多
的读程式的方法。在您修改运行程式的时候,能够从cvs中把原来的代码调出来和您改变的部分进行比较(diff命令),
能够看出一些源代码的优缺点并且能够实际的练习自己的编程技术。
8、阅读程式的同时,要注意一些小工具的使用,能够提高速度,比如vi中的查找功能,模式匹配查找,做标记,更有grep,find这两个最强大最常用的文本搜索工具的使用。
对于一个Unix/Linux下面以命令行方式运行的程式,有这么一些套路,大家能够在阅读程式的时候作为参考。
1、在程式开头,往往都是分析命令行,根据命令行参数对一些变量或数组,或结构赋值,后面的程式就是根据这些变量来进行不同的操作。
2、分析命令行之后,进行数据准备,往往是计数器清空,结构清零等等。
3、在程式中间有一些预编译选项,能够在makefile中找到相应部分。
4、注意程式中对于日志的处理,和调试选项打开的时候做的动作,这些对于调试程式有很大的帮助。
5、注意多线程对数据的操作。(这在本例中没有涉及)
结束语:
当然,在这篇文章中,并没有阐述任何的阅读源代码的方法和技巧,也没有涉及任何辅助工具(除了简单的文本编辑器),也没有涉及面向对象程式的阅读方法。我想把这些留到以后再做讨论。也请大家能够就这些话题展开讨论。
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值