正文:
因爲工做的關係,我經常須要讀一些源代碼,並在上面作一些修改而且拿來使用,或者是借鑑其中的某些部分。能夠說,open source對於程序員來講,是頗有意義的事情。根據個人經驗,讀源代碼,至少有3個好處。第一個好處是能夠學習到不少編程的方法,看好的源代碼,對於提升本身的編程水平,比本身寫源代碼的幫助更大。固然不是說不用本身寫,而是說,本身寫代碼的同時,能夠從別人寫的好的源代碼中間學習到更多的編程方法和技巧。第二個好處是,能夠提升本身把握大規模源代碼的能力。一個比較大型的程序,每每都是通過了不少個版本很長的時間,有不少人蔘與開發,修正錯誤,添加功能而發展起來的。因此每每源代碼的規模都比較大,少則10-100多k, 多的有好幾十個MB. 在閱讀大量源代碼的時候,可以提升本身對大的軟件的把握能力,快速瞭解脈絡,熟悉細節,不只僅是編程技巧,還能在程序的架構,設計方面提升本身的能力。(這裏說一句題外話,<<設計模式>>這本書相信不少人都看過,並且不少人對它推崇備至,奉爲經典。如今也出了很多書,都是冠以"設計模式"這一名稱。在書中就提到,設計模式並非一本教材,不是教你如何去編程序,而是把平時編程中一些固定的模式記錄下來,加以不斷的測試和改進,分發給廣大程序員的一些經驗之談。我在看這本書的時候,有一些地方一些設計方法每每讓我有似曾相識的感受,另一些則是我之前就經常用到的。而這些經驗的得到,一部分得益於本身的編碼過程,另一個很重要的來源就是閱讀別人寫的源代碼。)閱讀源代碼第三個好處,就是得到一些好的思想。好比,有不少人在開始一個軟件項目以前都喜歡到sourceforge.net上去找一下,是否有人之前作過相同或者類似的軟件,若是有,則拿下來讀一讀,可使本身對這個軟件項目有更多更深的認識。我之前曾經想找一本關於如何閱讀源代碼的書來看看,卻沒有找到。相反,卻是找到了很多分析源代碼的書,好比Linux kernel, Apache source, 等等。因此我想,爲何不本身來寫下一些經驗和你們交流呢?(固然不是寫書,沒有那個能力也沒有那個時間。)因此在這裏我準備用一個例子來寫一下如何閱讀源代碼,分享一些經驗,算是拋磚引玉吧!html
我找的例子是一個統計日誌的工具,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 644node
# where are the GD header files?
GDLIB=/usr/include
這些定義了安裝的路徑,執行程序的安裝路徑,編譯器,配置文件的安裝路徑,編譯的選項,安裝程序,安裝程序的選項等等。要注意的是,這些並非軟件的做者寫的,而是./configure的輸出結果。呵呵. :-)下面纔是主題內容,也是咱們關心的。
# Shouldn't have to touch below here!linux
all: webalizerc++
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.cweb
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.cwindows
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<TOTAL_RC;i++) response[i].count = 0;
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<MAXHASH;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<cur_tstamp/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(" ");
}
這一段,作了一些後期的處理。接下來的部分,我想在本文中略過,留給感興趣的讀者本身去作分析。緣由有兩點:
一、這個程序在前面結構化比較強,而到告終構上後面有些亂,雖然代碼效率仍是比較高,可是可重用性不夠強, 限於篇幅,我就再也不一一解釋了。
二、前面分析程序過程當中,也對後面的代碼作了一些預測和估計,也略微涉及到了後面的代碼,並且讀者能夠根據上面提到的原則來本身分析代碼,也做爲一個實踐吧。
最後,對於在這篇文章中提到的分析源代碼程序的一些方法作一下小結,以做爲本文的結束。
分析一個源代碼,一個有效的方法是:
一、閱讀源代碼的說明文檔,好比本例中的README, 做者寫的很是的詳細,仔細讀過以後,在閱讀程序的時候每每可以從README文件中找到相應的說明,從而簡化了源程序的閱讀工做。
二、若是源代碼有文檔目錄,通常爲doc或者docs, 最好也在閱讀源程序以前仔細閱讀,由於這些文檔一樣起了很好的說明註釋做用。
三、從makefile文件入手,分析源代碼的層次結構,找出哪一個是主程序,哪些是函數包。這對於快速把握程序結構有很大幫助。
四、從main函數入手,一步一步往下閱讀,遇到能夠猜想出意思來的簡單的函數,能夠跳過。可是必定要注意程序中使用的全局變量(若是是C程序),能夠把關鍵的數據結構說明拷貝到一個文本編輯器中以便隨時查找。
五、分析函數包(針對C程序),要注意哪些是全局函數,哪些是內部使用的函數,注意extern關鍵字。對於變量,也須要一樣注意。先分析清楚內部函數,再來分析外部函數,由於內部函數確定是在外部函數中被調用的。
六、須要說明的是數據結構的重要性:對於一個C程序來講,全部的函數都是在操做同一些數據,而因爲沒有較好的封裝性,這些數據可能出如今程序的任何地方,被任何函數修改,因此必定要注意這些數據的定義和意義,也要注意是哪些函數在對它們進行操做,作了哪些改變。
七、在閱讀程序的同時,最好可以把程序存入到cvs之類的版本控制器中去,在須要的時候能夠對源代碼作一些修改試驗,由於動手修改是比僅僅是閱讀要好得多的讀程序的方法。在你修改運行程序的時候,能夠從cvs中把原來的代碼調出來與你改動的部分進行比較(diff命令), 能夠看出一些源代碼的優缺點而且可以實際的練習本身的編程技術。
八、閱讀程序的同時,要注意一些小工具的使用,可以提升速度,好比vi中的查找功能,模式匹配查找,作標記,還有grep,find這兩個最強大最經常使用的文本搜索工具的使用。
對於一個Unix/Linux下面以命令行方式運行的程序,有這麼一些套路,你們能夠在閱讀程序的時候做爲參考。
一、在程序開頭,每每都是分析命令行,根據命令行參數對一些變量或者數組,或者結構賦值,後面的程序就是根據這些變量來進行不一樣的操做。
二、分析命令行以後,進行數據準備,每每是計數器清空,結構清零等等。
三、在程序中間有一些預編譯選項,能夠在makefile中找到相應部分。
四、注意程序中對於日誌的處理,和調試選項打開的時候作的動做,這些對於調試程序有很大的幫助。
五、注意多線程對數據的操做。(這在本例中沒有涉及)
結束語: 固然,在這篇文章中,並無闡述全部的閱讀源代碼的方法和技巧,也沒有涉及任何輔助工具(除了簡單的文本編輯器),也沒有涉及面向對象程序的閱讀方法。我想把這些留到之後再作討論。也請你們能夠就這些話題展開討論。