摘自:http://www.shangshuwu.cn/index.php/Linux安全體系的ClamAV病毒掃描程序
ClamAV是使用廣泛且基於GPL License的開放源代碼的典型殺毒軟件,它支持廣泛的平臺,如:Windows、Linux、Unix等操作系統,並被廣泛用於其他應用程序,如:郵件客戶端及服務器、HTTP病毒掃描代理等。ClamAV源代碼可從http://www.clamav.net 下載。
本章分析了ClamAV的客戶端、服務器及病毒庫更新升級應用程序,着重闡述了Linux下C語言編程中的許多經典用法。
1 ClamAV概述
計算機防病毒的方法一般有比較法、文件校驗和法、病毒掃描法和病毒行爲監測法。
病毒比較法有長度比較法、內容比較法、內存比較法、中斷比較法等,長度比較法是比較文件的長度是否發生變化,內容比較法是比較文件的內容是否發生變化及文件的更新日期是否改變,內存比較法是正常系統的內存空間是否改變,中斷比較法是比較系統的中斷向量是否被改變。
病毒比較法常常只能說明系統被改變,至於改變系統的程序是否是病毒以及病毒名都很難確定。
文件校驗和法是將正常文件的內容計算其校驗和,並將校驗和寫入寫入別的文件保存。以後使用文件時可檢查檢驗和,或定期檢查文件校驗和,看文件是否發生改變。這種方法只能說明文件的改變,但無法準確地說明是否是病毒。這種方法常被用來保護系統的註冊表或系統配置文件。
病毒掃描法(Virus Scanner)是用病毒體中的特定字符串對被檢測的文件或內存進行掃描。如果在掃描的文件中病毒的特定字符串,就認爲文件感染了字符串所代表的病毒。從病毒中提取的特徵字符串被用一定的格式組織在一起並加上簽名保護就形成了病毒庫。病毒特徵字符串或特徵字必須能鑑別病毒且必須能將病毒與正常的非病毒程序區分開,因此,對於病毒掃描法來說,病毒特徵碼的提取很關鍵,同時,病毒庫需要不斷的更新,加入新病毒的特徵碼。
病毒掃描法是反病毒軟件最常用的方法,它對已知病毒的掃描非常有效,還能準確的報告病毒的名稱,並可以按病毒的特徵將病毒從感染的文件中清除。但對未知的病毒卻無法檢測。
病毒程序還常用被加密或壓縮,或放在壓縮的軟件包中,因此,病毒掃描時還應具備相應的解密和解壓縮方法。
病毒行爲監測法是根據病毒異常運行行爲來判斷程序是否感染病毒。這種方法無法準確確認是否是病毒,但可以預報一些未知病毒。
ClamAV是UNIX下的反病毒工具,用於郵件網關的e-mail掃描。它提供了多線程後臺,命令行掃描器和通過Internet的自動庫升級工具。它還包括一個病毒掃描器共享庫。
ClamAV是基於GPL License的開放源代碼軟件,它支持快速掃描、on-access(文件訪問)掃描,可以檢測超過35000病毒,包括worms(蠕蟲)、trojans(特洛伊木馬)、, Microsoft Office和MacOffice宏病毒等。它還可掃描包括Zip、RAR (2.0)、Tar等多種格式的壓縮文件,具有強大的郵件掃描器,它還有支持數字簽名的先進數據庫更新器和基於數據庫版本查詢的DNS。
ClamAV工具已在GNU/Linux、Solaris、FreeBSD、OpenBSD 2、AIX 4.1/4.2/4.3/5.1HPUX 11.0、SCO UNIX、IRIX 6.5.20f、Mac OS X、BeOS、Cobalt MIPS boxes、Cygwin、Windows Services for Unix 3.5 (Interix)等操作系統平臺上經過測試,但有的操作系統中部分特徵不支持。
ClamAV包括clamscan查病毒應用程序、clamd後臺、clamdscan客戶端、libclamav庫、clamav-milter郵件掃描器應用程序幾個部分。ClamAV工具的組成圖如圖1。
clamscan查病毒應用程序可直接在命令行下查殺文件或目錄的病毒;clamd後臺使用libclamav庫查找病毒,它的一個線程負責on-access查殺病毒;clamdscan客戶端通過clamd後臺來查殺病毒,它可以替代clamscan應用程序;libclamav庫提供ClamAV接口函數,被其他應用程序調用來查殺病毒;clamav-milter郵件掃描器與sendmail工具連接,使用clamd來查殺email病毒。
ClamAV使用Dazuko軟件來進行on-access查殺病毒,Dazuko軟件的dazuko內核模塊可以使用LSM、系統調用hook和RedirFS模塊進行文件訪問攔截,Dazuko軟件的dazuko庫將攔截的文件上報給clamd後臺,由clamd來掃描病毒。
圖1 ClamAV工具的組成圖
2 ClamAV編譯安裝及使用
編譯ClamAV時應包括zlib庫,很多程序中的壓縮或者解壓縮函數都會用到這個庫。另外還需要包括bzip2和bzip2-devel庫、GNU MP 3庫。GMP包允許freshclam驗證病毒庫的數據簽名,你可在http://www.swox.com/gmp/下載GNU MP。
在Linux下編譯安裝ClamAV的步驟如下:
(1) 下載clamav-0.88.tar.gz
(2) 解壓縮文件包
# tar xvzf clamav-0.88.tar.gz
(3)進入解壓縮後的clamav目錄
# cd clamav-0.88
(4) 添加用戶組clamav和組成員clamav
# groupadd clamav # useradd -g clamav -s /bin/false -c "Clam AntiVirus" clamav
(5) 假定你的home目錄是/home/gary,如下配置軟件:
$ ./ configure --prefix =/ home/ gary/ clamav --disable-clamav
(6) 編譯,安裝
# make # make install
(7) 在/var/log/目錄下添加兩個log文件:clam.log和clam-update.log,將所有者改爲新加的clamav用戶並設置相應的文件讀寫權限。
( 7 ) 在/ var/ log/ 目錄下添加兩個log文件:clam.log和clam-update.log,將所有者改爲新加的clamav用戶並設置相應的文件讀寫權限。 # touch /var/log/clam-update.log # chmod 600 /var/log/clam-update.log # chown clamav /var/log/clam-update.log # touch /var/log/clam.log # chmod 600 /var/log/clam.log # chown clamav /var/log/clam.log
(8) 修改/etc/clam.conf將開始的有"Example"的那行用#註釋掉。
#Example
然後在命令行裏輸入:clamd開始病毒守護程序。
#clamd
(9) 修改/etc/freshclam.conf將開始的有"Example"的那行用#註釋掉。
#Example
修改UpdateLogFile /var/log/freshclam.log
爲UpdateLogFile /var/log/clam-update.log
(10) 用freshclam升級病毒庫:
#freshclam
(11) 查殺當前目錄下的文件
clamscan
(12) 查殺當前目錄所有文件及目錄!
clamscan -r
(13) 查殺dir目錄,
clamscan dir
(14) 查殺目錄dir下所有文件及目錄!
clamscan -r dir
(15) 看幫助信息
clamscan --help
2.1 clamd後臺與clamdscan客戶端
clamd是使用libclamav庫掃描文件病毒的多線程後臺,它可工作在兩種網絡模式下:偵聽在Unix (local) socket和TCP socket。後臺由clamd.conf文件配置。通過設置cron的工作,在每隔一段時間檢查clamd是否啓動運行,並在clamd死亡後自動啓動它。在contrib/clamdwatch/目錄下有腳本樣例。
Clamdscan是一個簡單的clamd客戶端,許多情況下,你可用它替代clamscan,它僅依賴於clamd,雖然它接受與clamscan同樣的命令行選項,但大多數選項被忽略,因爲這些選項已在clamd.conf中配置。
clamd的一個重要特徵是基於Dazuko模塊進行on-access病毒掃描,即攔截文件系統的訪問,觸發clamd對訪問文件進行病毒掃描。Dazuko模塊在http://dazuko.org上可用。
clamd中一個名爲Clamuko的線程負責與Dazuko進行通信。
Dazuko模塊的編譯方法如下:
$ tar zxpvf dazuko- a.b .c .tar .gz $ cd dazuko- a.b .c $ make dazuko
或者
$ make dazuko-smp ( 對於smp內核) $ su # insmod dazuko.o # cp dazuko.o /lib/modules/‘uname -r‘/misc # depmod -a
爲了Linux啓動時會自動加入這個模塊,你可以加"dazuko"條目到/etc/modules中,或者在一些啓動文件中加入命令modprobe dazuko。
你還必須如下創建一個新設備:
$ cat / proc/ devices | grep dazuko 254 dazuko $ su -c "mknod -m 600 /dev/dazuko c 254 0"
2.2 clamav-milter郵件掃描器
Nigel Horne公司的clamav-milter是Sendmail工具的非常快速的email掃描器。它用C語言編寫僅依賴於libclamav或clamd。
通過加入下面的行到/etc/mail/sendmail.mc中,就可將clamav-milter與Sendmail連接起來:
INPUT_MAIL_FILTER( ‘clmilter’,‘S =local :/ var/ run/ clamav/ clmilter.sock, F =, T =S:4m;R:4m’) dnl define( ‘confINPUT_MAIL_FILTERS’, ‘clmilter’)
如果你正以—external運行clamd,檢查clamd.conf中的條目是否有如下:
LocalSocket /var/run/clamav/clamd.sock
接着,按下面方法啓動clamav-milter:
/usr/local/sbin/clamav-milter -lo /var/run/clamav/clmilter.sock
然後重啓動sendmail。
2.3 建立病毒庫自動更新
freshclam是ClamAV的缺省數據庫更新器,它可以下面兩種方法工作:
(1) 交互方式:使用命令行的方式進行交互。
(2) 後臺進程的方式:它獨立運行不需要干預。
freshclam由超級用戶啓動,並下降權限,切換到clamav用戶。freshclam使用database.clamav.net 輪詢調度(round-robin)DNS,自動選擇一個數據庫鏡像。freshclam通過DNS支持數據庫版本驗證,它還支持代理服務器(與認證一起)、數字簽名和出錯說明。
ClamAV使用freshclam工具,週期地檢查新數據庫的發佈,並保持數據庫的更新。
還可以創建freshclam.log文件,將freshclam.log修改成clamav擁有的log文件,修改方法如下:
# touch /var/log/freshclam.log # chmod 600 /var/log/freshclam.log # chown clamav /var/log/freshclam.log
編輯freshclam.conf文件或clamd.conf文件(如果它們融合在一起),配置UpdateLogFile指向創建的log文件。
以後臺運行freshclam的方法如下:
# freshclam –d
還可以使用cron後臺自動定時運行freshclam,方法是加入下面行到crontab中:
N * * * * /usr/local/bin/freshclam --quiet
其中,N應是3~57之間的數據,表示每隔N小時檢查新病毒數據庫。
代理服務器通過配置文件配置,當HTTPProxyPassword被激活時,freshclam需要嚴格的許可,方法列出如下:
HTTPProxyServer myproxyserver.com
HTTPProxyPort 1234
HTTPProxyUsername myusername
HTTPProxyPassword mypass
配置文件中的DatabaseMirror指定了數據庫服務器,freshclam將嘗試從這個服務器下載直到最大次數。缺省的數據庫鏡像是database.clamav.net,爲了從最近的鏡像下載數據庫,你應使用db.xx.clamav.net配置freshclam,xx代表你的國家代碼。例如,如果你的服務器在"Ascension Island",你應該加下面的行到freshclam.conf中:
DNSDatabaseInfo current.cvd.clamav.net DatabaseMirror db.ac.clamav.net DatabaseMirror database.clamav.net
兩字符國家代碼在http://www.iana.org/cctld/cctld-whois.htm上可查找到。
2.4 libclamav庫API
每個使用libclamav庫的應用程序必須包括clamav.h頭文件,方法如下:
#include <clamav.h>
libclamav庫API的使用樣例見clamscan/manager.c,下面說明API函數。
(1) 裝載庫
初始化庫的函數列出如下:
int cl_loaddb( const char * filename, struct cl_node ** root, unsigned int * signo) ; int cl_loaddbdir( const char * dirname, struct cl_node ** root, unsigned int * signo) ; const char * cl_retdbdir( void ) ;
其中,函數cl_loaddb裝載選擇的數據庫,函數cl_loaddbdir從目錄dirname裝載所有的數據庫,函數返回缺省(硬編碼hardcoded)數據庫的目錄路徑。在初始化後,一個內部數據庫代表由參數root傳出,root必須被初始化到NULL,裝載的簽名序號由參數signo傳出,如果不關心簽名計數,參數signo設置爲NULL。函數cl_loaddb和cl_loaddbdir裝載成功時,返回0,失敗時,返回一個負數。
函數cl_loaddb用法如下:
... struct cl_node * root = NULL; int ret, signo = 0 ; ret = cl_loaddbdir( cl_retdbdir( ) , & root, & signo) ;
(2) 錯誤處理
使用函數cl_strerror將錯誤代碼轉換成可讀的消息,函數cl_strerror返回一個字符串,使用方法如下:
if ( ret) { //ret是錯誤碼,爲負數 printf ( "cl_loaddbdir() error: %s/n " , cl_strerror( ret) ) ; exit( 1 ) ; }
(3) 初始化數據庫內部傳輸
函數cl_build被用來初始化數據庫的內部傳輸路徑,函數列出如下:
int cl_build( struct cl_node * root) ;
函數cl_build使用方法如下:
if ( ( ret = cl_build( root) ) ) printf ( "cl_build() error: %s/n " , cl_strerror( ret) ) ;
(4) 數據庫重裝載
保持內部數據庫實例的更新是很重要的,你可以使用函數簇cl_stat來檢查數據庫的變化,函數簇cl_stat列出如下:
int cl_statinidir( const char * dirname, struct cl_stat * dbstat) ; int cl_statchkdir( const struct cl_stat * dbstat) ; int cl_statfree( struct cl_stat * dbstat) ;
調用函數cl_statinidir初始化結構cl_stat變量,方法如下:
... struct cl_stat dbstat; memset( & dbstat, 0 , sizeof ( struct cl_stat) ) ; cl_statinidir( dbdir, & dbstat) ;
僅需要調用函數cl_statchkdir 來檢查數據庫的變化,方法如下:
if ( cl_statchkdir( & dbstat) == 1 ) { //數據庫發生變化 reload_database...; //重裝載數據庫 cl_statfree( & dbstat) ; cl_statinidir( cl_retdbdir( ) , & dbstat) ; }
在重裝載數據庫後,需要重初始化這個結構。
(5) 數據掃描函數
使用下面的函數可以掃描一個buffer、描述符或文件:
int cl_scanbuff( const char * buffer, unsigned int length, const char ** virname, const struct cl_node * root) ; int cl_scandesc( int desc, const char ** virname, unsigned long int * scanned, const struct cl_node * root, const struct cl_limits * limits, unsigned int options) ; int cl_scanfile( const char * filename, const char ** virname, unsigned long int * scanned, const struct cl_node * root, const struct cl_limits * limits, unsigned int options) ;
所有這些函數存儲病毒名在指針virname中,它指向內部數據庫結構的一個成員,不能直接釋放。
後兩個函數還支持文件限制結構cl_limits,結構cl_limits用來限制了掃描文件數量、大小等,以防止服務超載攻擊,列出如下:
struct cl_limits { int maxreclevel; /* 最大遞歸級 */ int maxfiles; /*掃描的最大文件數*/ int maxratio; /* 最大壓縮率*/ short archivememlim; /* 使用bzip2 (0/1)的最大內存限制*/ long int maxfilesize; /* 最大的文件尺寸,大於這個尺寸的文件不被掃描*/ } ;
參數options配置掃描引擎,並支持下面的標識(可以使用標識組合):
- CL_SCAN_STDOPT 推薦的掃描選項集的別名,它用來給將來libclamav的版本新特徵使用。
- CL_SCAN_RAW 不做任何事情,如果不想掃描任何特殊文件,就單獨使用它。
- CL_SCAN_ARCHIVE 激活各種文件格式的透明掃描。
- CL_SCAN_BLOCKENCRYPTED 庫使用它標識加密文件作爲病毒(Encrypted.Zip,Encrypted.RAR)。
- CL_SCAN_BLOCKMAX 如果達到maxfiles、maxfilesize或maxreclevel限制,標識文件作爲病毒。
- CL_SCAN_MAIL 激活對郵件文件的支持。
- CL_SCAN_MAILURL 郵件掃描器將下載並掃描列在郵件中的URL,這個標識不應該在裝載的服務器上使用,由於潛在的問題,不要在缺省情況下設置這個標識。
- CL_SCAN_OLE2 激活對Microsoft Office文檔文件的支持。
- CL_SCAN_PE 激活對便攜執行文件(Portable Executable file)的掃描,並允許libclamav解開UPX、Petite和FSG格式壓縮的可執行文件。
- CL_SCAN_BLOCKBROKEN libclamav將嘗試檢測破碎的可執行文件並標識它們爲Broken.Executable。
- CL_SCAN_HTML 激活HTML格式(包括Jscript解密)文件掃描。
上面所有函數,如果文件掃描無病毒時,返回0(CL_CLEAN),當檢測到立於病毒時,返回CL_VIRUS,函數操作失敗返回其它值。
掃描一個文件的方法如下:
... struct cl_limits limits; const char * virname; memset( & limits, 0 , sizeof ( struct cl_limits) ) ; /* 掃描目錄中的最大文件數*/ ; limits.maxfiles = 1000 /* 掃描目錄中文件最大尺寸*/ limits.maxfilesize = 10 * 1048576 ; /* 10 MB */ /* 最大遞歸級數*/ limits.maxreclevel = 5 ; /* 最大壓縮率*/ limits.maxratio = 200 ; /*取消對bzip2掃描器的內存限制*/ limits.archivememlim = 0 ; if ( ( ret = cl_scanfile( "/home/zolw/test" , & virname, NULL, root, & limits, CL_STDOPT) ) == CL_VIRUS) { printf ( "Detected %s virus./n " , virname) ; } else { printf ( "No virus detected./n " ) ; if ( ret != CL_CLEAN) printf ( "Error: %s/n " , cl_strerror( ret) ) ; }
(6) 釋放內存
因爲內部數據庫的root使用了應用程序分配的內存,因此,如果不再掃描文件時,用下面的函數釋放root。
void cl_free( struct cl_node * root) ;
(7) 使用clamav-config命令檢查libclamav編譯信息
使用clamav-config命令檢查的方法及顯示的結果列出如下:
# clamav-config --libs -L/ usr/ local/ lib -lz -lbz2 -lgmp -lpthread # clamav-config --cflags -I/ usr/ local/ include -g -O2
(8) ClamAV病毒庫格式
ClamAV病毒庫(ClamAV Virus Database 簡稱CVD)是一個數據簽名的裝有一個或多個數據庫的.tar文件。文件頭是512字節長字符串,用冒號分開,格式如下:
ClamAV-VDB:build time :version:number of signatures:functionality level required:MD5 checksum:digital signature:builder name:build time ( sec)
使用命令sigtool –info可顯示CVD文件的詳細信息,方法與顯示結果列出如下:
#sigtool -i daily.cvd Build time : 11 Sep 2004 21 -07 +0200 Version: 487 # of signatures: 1189 Functionality level: 2 Builder: ccordes MD5: a3f4f98694229e461f17d2aa254e9a43 Digital signature: uwJS6d+y/ 9g5SXGE0Hh1rXyjZW/ PGK/ zqVtWWVL3/ tfHEnA17z6VB2IBR2I/ OitKRYzm Vo3ibU7bPCJNgi6fPcW1PQwvCunwAswvR0ehrvY/ 4ksUjUOXo1VwQlW7l86HZmiMUSyAjnF/ gciOSsOQa9Hli8D5uET1RDzVpoWu/ idVerification OK
3 clamd服務器
clamd服務器是實現病毒掃描功能的後臺進程,它使用socket通信、信號同步、線程池、後臺進程等典型技術。
3.1 應用程序命令參數分析
應用程序常使用一些命令行參數選項,應用程序主函數main中常需要對這些參數選項進行分析。參數選項常用"--"和"-"表示,"--"後跟單詞表示選項名詳解,選項名的值用"="前綴進行標識,"-" 後跟字母表示選項名縮寫,選項名的值用空格前綴進行標識。
例如,clamd應用程序的參數選項列出如下:
$clamd –help Clam AntiVirus Daemon 0.88.4 ( C) 2002 - 2005 ClamAV Team - http: //www.clamav.net/team.html -- help - h Show this help. -- version - V Show version number. -- debug Enable debug mode. -- config- file= FILE - c FILE Read configuration from FILE.
標準C庫提供了下述函數進行命令行參數分析:
#include <unistd.h> int getopt( int argc, char * const argv[ ] , const char * optstring) ; extern char * optarg; //用於存放選項值 extern int optind, opterr, optopt; #define _GNU_SOURCE #include <getopt.h> int getopt_long( int argc, char * const argv[ ] , const char * optstring, const struct option * longopts, int * longindex) ; int getopt_long_only( int argc, char * const argv[ ] , const char * optstring, const struct option * longopts, int * longindex) ;
其中,函數參數argc和argv是main函數的參數,參數optstring表示分析選項的方法,參數longopts爲用戶定義的選項數組,longindex爲命令行選項的序號。
函數getopt可以分析"-"標識的命令行參數,函數getopt_long是對getopt的擴展,它可以分析"-"和"--"標識的命令行參數。命令行參數解析時,函數每解析完一個argv,optind加1,返回該選項字符。當解析完成時,返回-1。
參數optstring一般由用戶設置,表示分析命令行參數的方法,參數optstring說明如下:
optstring爲類似"a:b"字符串表示命令行參數帶有選項值。
optstring爲類似"a::b"字符串表示命令行參數可能帶有選項值,也可能沒有。
optstring的開頭字符爲":",表示如果選項值失去命令行參數時,返回":",而不是" '",默認時返回" '"。
optstring的開頭字符爲'+',表示遇到無選項參數,馬上停止掃描,隨後的部分當作參數來解釋。
optstring的開頭字符爲'-',表示遇到無選項參數,把它當作選項1的參數。
結構option描述了命令行一個參數選項的構成,結構option說明如下:
struct option { const char * name; //長參數選項名 int has_arg; //選項值個數:0,1,2,爲2時表示值可有可無 int * flag; // flag爲NULL,則getopt_long返回val。否則返回0 int val; //指明返回的值,短參數名 } ;
clamd服務器是一個後臺進程,它實現了病毒掃描的具體功能。在clamd/options.c中的函數main解析了命令行的各種選項,函數main調用C庫函數getopt_long依次分析出每個命令行選項,並將每個命令行選項及值存儲在鏈表中。鏈表定義如下(在clamd/options.h中):
struct optnode { //鏈表結點結構 char optchar; //短選項名 char * optarg; //選項值,來自於C庫函數getopt_long解析並存在全局變量optarg中的選項值 char * optname; //長選項名 struct optnode * next; //下一個結點,當爲最後一個結點時,指向NULL } ; struct optstruct { struct optnode * optlist; //命令行選項的鏈表 char * filename; } ;
clamd服務器在clamd/options.c中提供了對這個鏈表的操作函數,如:創建鏈表、釋放鏈表、讀取鏈表成員、加入鏈表成員等。
函數main選定了應用程序所支持的命令行參數選項數組long_options[],函數getopt_long解析命令行選項時,如果長選項對應匹配有短選項,將輸出短選項,如:"--configfile"對應"-c"。解析出的選項存在鏈表opt->optlist中,非參數選項字符存在opt->filename中。
例如,當輸入clamd --config-file=test test1 -V --debug test2時,opt->optlist鏈表中將存有{0,0,debug},{‘V’,0,0},{‘c’,"test",0},opt->filename存有"test1 test2"。
函數main 列出如下(在clamd/options.c中):
int main( int argc, char ** argv) { int ret, opt_index, i, len; struct optstruct * opt; const char * getopt_parameters = "hc:V" ; //支持的短選項名集合 //應用程序clamd支持的參數選項定義 static struct option long_options[ ] = { { "help" , 0 , 0 , 'h' } , { "config-file" , 1 , 0 , 'c' } , { "version" , 0 , 0 , 'V' } , { "debug" , 0 , 0 , 0 } , { 0 , 0 , 0 , 0 } } ; ...... opt = ( struct optstruct* ) mcalloc( 1 , sizeof ( struct optstruct) ) ; //創建參數鏈表 opt-> optlist = NULL; opt-> filename = NULL; while ( 1 ) { //循環解析每個命令行選項 opt_index= 0 ; //解析一個命令行選項,解析出的值存在於C庫全局變量optarg中,匹配的短選項名存於ret中 // getopt_parameters爲格式"hc:V",argv將重排序,非選項參數依次排在選項參數之後 ret= getopt_long( argc, argv, getopt_parameters, long_options, & opt_index) ; if ( ret == - 1 ) //選項解析完畢,跳出循環 break ; switch ( ret) { case 0 : //ret爲0,表示沒有匹配的短選項名,如:"debug" register_long_option( opt, long_options[ opt_index] .name ) ; //將無配置短選項名的長選項存入鏈表 break ; default : if ( strchr( getopt_parameters, ret) ) //ret是否屬於支持的短選項名 register_char_option( opt, ret) ; //短選項存入鏈表 else { fprintf( stderr, "ERROR: Unknown option passed./n " ) ; free_opt( opt) ; exit( 40 ) ; } } } if ( optind < argc) { len= 0 ; /* 計數非選項參數長度 */ for ( i= optind; i< argc; i++ ) //選項參數optind之後爲非選項參數 len+= strlen( argv[ i] ) ; //計算非選項參數的長度,如:test1,test2 len= len+ argc- optind- 1 ; /* add spaces between arguments */ opt-> filename= ( char * ) mcalloc( len + 256 , sizeof ( char ) ) ; //存入非選項參數,opt->filename =“test1 test2” for ( i= optind; i< argc; i++ ) { strncat( opt-> filename, argv[ i] , strlen( argv[ i] ) ) ; //連接字符串 if ( i != argc- 1 ) strncat( opt-> filename, " " , 1 ) ; //連接一個空格 } } clamd( opt) ; free_opt( opt) ; //釋放鏈表 return ( 0 ) ; }
函數register_long_option將命令行的長選項添加到鏈表中,函數register_long_option列出如下(在clamd/options.c中):
void register_long_option( struct optstruct * opt, const char * optname) { struct optnode * newnode; newnode = ( struct optnode * ) mmalloc( sizeof ( struct optnode) ) ; //分配新結點 newnode-> optchar = 0 ; if ( optarg != NULL) { // optarg是C庫的全局變量,存儲有函數getopt_long分析出的選項值 newnode-> optarg = ( char * ) mcalloc( strlen( optarg) + 1 , sizeof ( char ) ) ; strcpy( newnode-> optarg, optarg) ; //拷貝選項參數值 } else newnode-> optarg = NULL; //分配字符串空間,加1是爲了字符串結尾保護 newnode-> optname = ( char * ) mcalloc( strlen( optname) + 1 , sizeof ( char ) ) ; strcpy( newnode-> optname, optname) ; //拷貝選項名 newnode-> next = opt-> optlist; //新結點加入到鏈表 opt-> optlist = newnode; }
函數clamd根據命令行選項值調用相應處理函數,函數clamd中與命令行選項處理相關的代碼列出如下:
void clamd( struct optstruct * opt) { ...... if ( optc( opt, 'V' ) ) { //檢查命令行選項鍊表中是否含有短選項名爲'V'的成員 print_version( ) ; //打印版本信息 exit( 0 ) ; } if ( optc( opt, 'h' ) ) { //檢查命令行選項鍊表中是否含有短選項名爲'h'的成員 help( ) ; //打印幫助信息 } if ( optl( opt, "debug" ) ) { //檢查命令行選項鍊表中是否含有長選項名爲"debug"的成員 ...... debug_mode = 1 ; //設置爲調試模式 } ...... }
3.2 clamd服務器入口函數clamd
函數clamd是clamd服務器的入口函數。clamd服務器在函數main解析了命令行的各種選項後,調用函數clamd。clamd服務器由函數clamd建立,函數clamd分析配置文件後,調用umask(0)使進程具有讀寫執行權限,然後初始化logger系統(包括使用syslog),並通過設置組ID和用戶ID來降低權限,還設置臨時目錄的環境變量,裝載病毒庫,使進程後臺化。然後,使用socket套接口,接收客戶端的服務請求並啓動相應的服務。
函數clamd列出如下(在clamav/clamd.c中):
void clamd( struct optstruct * opt) { ...... /* 省略與根據命令行選項打印病毒庫版本、幫助信息*/ ...... if ( optl( opt, "debug" ) ) { //如果有debug選項,設置debug標識 #if defined(C_LINUX) struct rlimit rlim; rlim.rlim_cur = rlim.rlim_max = RLIM_INFINITY; //表示core文件大小不受限制,core文件是應用崩潰時記錄的內存映像 if ( setrlimit( RLIMIT_CORE, & rlim) < 0 ) perror( "setrlimit" ) ; #endif debug_mode = 1 ; //debug標識,debug_mode是全局變量 } /* 分析配置文件 */ if ( optc( opt, 'c' ) ) cfgfile = getargc( opt, 'c' ) ; else cfgfile = CL_DEFAULT_CFG; if ( ( copt = parsecfg( cfgfile, 1 ) ) == NULL) { fprintf( stderr, "ERROR: Can't open/parse the config file %s/n " , cfgfile) ; exit( 1 ) ; } //加權限掩碼,即掩碼爲1的權限位不起作用,掩碼爲0,表示爲777權限,即root權限 umask( 0 ) ; /*初始化logger */ ...... if ( ( cpt = cfgopt( copt, "LogFile" ) ) ) { logg_file = cpt-> strarg; ...... time ( & currtime) ; //得到當前時間 if ( logg( "+++ Started at %s" , ctime( & currtime) ) ) { //將當前時間寫入log文件 fprintf( stderr, "ERROR: Problem with internal logger. Please check the permissions on the %s file./n " , logg_file) ; //將錯誤寫出到標準錯誤輸出句柄上,一般爲標準輸出 exit( 1 ) ; } } else logg_file = NULL; #在系統log中加入clamd已啓動信息 #if defined(USE_SYSLOG) && !defined(C_AIX) if ( cfgopt( copt, "LogSyslog" ) ) { int fac = LOG_LOCAL6; //表示是本地消息 if ( ( cpt = cfgopt( copt, "LogFacility" ) ) ) { if ( ( fac = logg_facility( cpt-> strarg) ) == - 1 ) { fprintf( stderr, "ERROR: LogFacility: %s: No such facility./n " , cpt-> strarg) ; exit( 1 ) ; } } openlog( "clamd" , LOG_PID, fac) ; // LOG_PID表示每條消息中加入pid logg_syslog = 1 ; syslog( LOG_INFO, "Daemon started./n " ) ; //將字符串加入到系統log,表示clamd已啓動 } #endif ...... #ifdef C_LINUX procdev = 0 ; if ( stat( "/proc" , & sb) != - 1 && ! sb.st_size ) procdev = sb.st_dev ; #endif ...... /* 通過設置組ID和用戶ID來降低權限*/ ...... /* 設置臨時目錄的環境變量*/ if ( ( cpt = cfgopt( copt, "TemporaryDirectory" ) ) ) cl_settempdir( cpt-> strarg, 0 ) ; if ( cfgopt( copt, "LeaveTemporaryFiles" ) ) cl_settempdir( NULL, 1 ) ; /* 裝載病毒庫*/ if ( ( cpt = cfgopt( copt, "DatabaseDirectory" ) ) || ( cpt = cfgopt( copt, "DataDirectory" ) ) ) dbdir = cpt-> strarg; else dbdir = cl_retdbdir( ) ; //從DATADIR得到缺省病毒庫目錄 if ( ( ret = cl_loaddbdir( dbdir, & root, & virnum) ) ) { //裝載病毒庫 ...... } ...... if ( ( ret = cl_build( root) ) != 0 ) { ...... } /* fork進程後臺*/ if ( ! cfgopt( copt, "Foreground" ) ) daemonize( ) ; if ( tcpsock) ret = tcpserver( opt, copt, root) ; else ret = localserver( opt, copt, root) ; logg_close( ) ; freecfg( copt) ; }
3.3 設置系統限制及確定資源使用量
C庫中與資源限制相關的函數有函數getrlimit和setrlimit,列出如下:
#include <sys/types.h> #include <sys/time.h> #include <sys/resource.h> //得到資源種類resource的限制值(包括當前限制值和最大限制值) int getrlimit( int resource, struct rlimit * rlp) ; // resource表示資源種類,rlp設置資源限制值 int setrlimit( int resource, const struct rlimit * rlp) ; //設置限制值 //參數who爲或RUSAGE_CHILDREN,RUSAGE_SELF表示得到當前進程的資源使用信息,RUSAGE_CHILDREN表示得到子進程的資源使用信息 //參數rusage用於存儲查詢到的資源使用信息 int getrusage( int who, struct rusage * rusage) ;
函數getrlimit查詢本進程所受的系統限制,系統的限制通過結構rlimit來描述,結構rlimit列出如下:
struct rlimit { rlim_t rlim_cur; //當前的限制值 rlim_t rlim_max; //最大限制值 } ;
在結構rlimit中,rlim_cur表示進程的當前限制,它是軟限制,進程超過軟限制時,還繼續運行,但會收到與當前限制相關的信號。rlim_max是進程的最大限制,僅由root用戶設置,進程不能超過它的最大限制,當前限制可以最大限制的範圍內設置。
資源類型的宏定義在/usr/include/bits/resource.h文件中,列出如下:
/* 指示沒有限制的值*/ #ifndef __USE_FILE_OFFSET64 # define RLIM_INFINITY #else # define RLIM_INFINITY 0xffffffffffffffffuLL #endif enum __rlimit_resource { /* 本進程可以使用CPU的時間秒數,達到當前限制,收到SIGXCPU信號*/ RLIMIT_CPU = 0 , #define RLIMIT_CPU RLIMIT_CPU /*本進程可創建的最大文件尺寸,超出限制時,發出SIGFSZ信號*/ RLIMIT_FSIZE = 1 , #define RLIMIT_FSIZE RLIMIT_FSIZE /*進程數據段的最大尺寸,數據段是C/C++中用malloc()分配的內存,超出限制,將不能分配內存*/ RLIMIT_DATA = 2 , #define RLIMIT_DATA RLIMIT_DATA /*進程棧的最大尺寸,超出限制,會收到SIGSEV信號*/ RLIMIT_STACK = 3 , #define RLIMIT_STACK RLIMIT_STACK /* 進程能創建的最大core文件,core文件用於進程崩潰時倒出進程現場,達到限制時,將中斷寫core文件的進程*/ RLIMIT_CORE = 4 , #define RLIMIT_CORE RLIMIT_CORE /* 最大常駐集(resident set)尺寸,它影響到內存交換,超出常駐集尺寸的進程將可能被釋放物理內存*/ RLIMIT_RSS = 5 , #define RLIMIT_RSS RLIMIT_RSS /*進程打開文件的最大數量,超出限制,將不能打開文件*/ RLIMIT_NOFILE = 7 , RLIMIT_OFILE = RLIMIT_NOFILE, /* 用於BSD操作系統*/ #define RLIMIT_NOFILE RLIMIT_NOFILE #define RLIMIT_OFILE RLIMIT_OFILE /*地址空間限制*/ RLIMIT_AS = 9 , #define RLIMIT_AS RLIMIT_AS /*應用程序可以同時開啓的最大進程數*/ RLIMIT_NPROC = 6 , #define RLIMIT_NPROC RLIMIT_NPROC /* 鎖住在內存的地址空間*/ RLIMIT_MEMLOCK = 8 , #define RLIMIT_MEMLOCK RLIMIT_MEMLOCK /* 文件鎖的最大數量*/ RLIMIT_LOCKS = 10 , #define RLIMIT_LOCKS RLIMIT_LOCKS RLIMIT_NLIMITS = 11 , RLIM_NLIMITS = RLIMIT_NLIMITS #define RLIMIT_NLIMITS RLIMIT_NLIMITS #define RLIM_NLIMITS RLIM_NLIMITS } ;
當將rlim_cur設置RLIM_INFINITY時,進程將不收到任何限制警告。如果進程不願處理當前限制引起的信號,可將rlim_cur設置RLIM_INFINITY。
函數clamd 中調用到setrlimit(RLIMIT_CORE, &rlim),它將不限制core文件的大小,core文件用於在進程崩潰時,倒出(dump)進程的現場,core文件存儲這個現場信息用於對進程崩潰的調試分析。
3.4 配置文件解析
clamd服務器的配置文件在clamav/etc/clamd.conf文件中,應用程序的配置文件是由用戶直接寫入的文本文件。應用程序分析配置文件,得到用戶對應用程序運行設置的選項。應用程序分析配置文件就會用到配置文件分析器。配置文件分析器是分析配置文件的函數,包括函數parsecfg、freecfg、regcfg和cfgopt。函數parsecfg分析配置文件,調用函數regcfg將分析出的配置項及值存入配置鏈表中;函數cfgopt從配置鏈表中查詢配置項名,返回配置項名對應的配置。
配置選項由配置選項名和選項值組成,配置文件中的配置選項類型使用結構cfgoption描述。配置文件分析器分析配置文件每行後,得到選項名與選項值,選項值可能爲數字或字符。選項名及選項值存在結構cfgstruct。
函數parsecfg分析配置文件後,將配置選項組成一個結構cfgstruct實例的鏈表返回。
結構cfgoption和cfgstruct列出如下(在clamav/shared/cfgparser.h中):
struct cfgoption { const char * name; //選項名 int argtype; //選項的類型 } ; struct cfgstruct { char * optname; //選項名 char * strarg; //字符串選項值 int numarg; //數字選項值 struct cfgstruct * nextarg; struct cfgstruct * next; } ;
函數*parsecfg從配置文件clamd.conf文件中讀取每行,每行由選項名和選項參數值組成。將配置文件每行的選項名與服務器應用程序中選項數組cfg_options中選項進行比較,若相匹配,根據選項類型分析選項參數值,並將選項名與選項值加入到選項鍊表的節點上。函數*parsecfg返回由配置文件生成的選項鍊表。
函數*parsecfg列出如下(在clamav/shared/cfgparser.c中):
struct cfgstruct * parsecfg( const char * cfgfile, int messages) { ...... //定義配置文件中選項的類型 struct cfgoption cfg_options[ ] = { { "LogFile" , OPT_FULLSTR} , //佔一行的字符串參數 { "LogFileUnlock" , OPT_NOARG} , //沒有參數 { "LogFileMaxSize" , OPT_COMPSIZE} , //轉換KByte和MByte到Byte ...... { "OnUpdateExecute" , OPT_FULLSTR} , /* freshclam */ { "OnErrorExecute" , OPT_FULLSTR} , /* freshclam */ { "OnOutdatedExecute" , OPT_FULLSTR} , /* freshclam */ { "LocalIPAddress" , OPT_STR} , /*用於freshclam的字符串參數*/ { 0 , 0 } , //表示結尾 } ; if ( ( fs = fopen( cfgfile, "r" ) ) == NULL) //打開配置文件 return NULL; /*函數fgets從fs中讀取尺寸最大LINE_LENGTH(爲1024)長度的字符,存入buff中,當讀到EOF時,讀操作停止。下次再調用函數fgets時,將從新的一行開始。讀完一行後,在buff末尾加上‘/0’字符*/ while ( fgets( buff, LINE_LENGTH, fs) ) { //每次讀取配置文件的一行 line++; if ( buff[ 0 ] == '#' ) //跳過註釋行 continue ; //如果存在Example行,說明配置文件還沒修改過,需要用戶配置好後,去掉這一行,這樣配置文件纔可用 if ( ! strncmp( "Example" , buff, 7 ) ) { //如果buff的前7個字符爲“Example” if ( messages) fprintf( stderr, "ERROR: Please edit the example config file %s./n " , cfgfile) ; fclose( fs) ; return NULL; } //每行的第一個域爲選項名,第二個域爲參數 if ( ( name = cli_strtok( buff, 0 , " /r /n " ) ) ) { //得到一行的第0域的值,每個域以“/r/n”中的字符分隔 arg = cli_strtok( buff, 1 , " /r /n " ) ; found = 0 ; //遍歷選項數組cfg_options,查找與配置文件相匹配的選項名 for ( i = 0 ; ; i++ ) { pt = & cfg_options[ i] ; if ( pt-> name) { if ( ! strcmp( name, pt-> name) ) { //在數組中找到與配置文件中相匹配的選項 found = 1 ; switch ( pt-> argtype) { //根據選項類型分析字符串 case OPT_STR: //字符串參數 ...... copt = regcfg( copt, name, arg, 0 ) ; break ; case OPT_FULLSTR: //佔一行的字符串參數 ...... free ( arg) ; //返回buff匹配子字符串" "(空格)的位置的字符指針 arg = strstr( buff, " " ) ; arg = strdup( ++ arg) ; //strdup表示字符複製,這裏用於刪除空格 //如果“/n/r”任何一個字符在arg中,返回匹配位置開始字符指針 if ( ( c = strpbrk( arg, "/n /r " ) ) ) //將“/n/r”轉換成“/0” * c = '/0 ' ; copt = regcfg( copt, name, arg, 0 ) ; break ; case OPT_NUM: //數字參數 ...... copt = regcfg( copt, name, NULL, atoi( arg) ) ; //將字符轉換成int類型 free( arg) ; break ; case OPT_COMPSIZE: //轉換KByte和MByte到Byte ...... //將arg的最後一個字符轉換成小寫 ctype = tolower( arg[ strlen( arg) - 1 ] ) ; if ( ctype == 'm' || ctype == 'k' ) { char * cpy = ( char * ) mcalloc( strlen( arg) , sizeof ( char ) ) ; //拷貝參數arg除表示單位的字符外的字符串到cpy strncpy( cpy, arg, strlen( arg) - 1 ) ; ...... if ( ctype == 'm' ) //轉換單位MByte到1024*1024 calc = atoi( cpy) * 1024 * 1024 ; else calc = atoi( cpy) * 1024 ; free( cpy) ; } else { ...... copt = regcfg( copt, name, NULL, calc) ; } free( arg) ; break ; case OPT_NOARG: //沒有參數 ...... copt = regcfg( copt, name, NULL, 0 ) ; break ; case OPT_OPTARG: //選項參數字符串 copt = regcfg( copt, name, arg, 0 ) ; break ; default : ...... free ( name) ; free( arg) ; break ; } } } else break ; } ...... } } fclose( fs) ; return copt; }
函數*regcfg將選項值加到選項名爲optname的節點上,參數copt爲節點鏈表,每個節點是一個cfgstruct結構實例,參數optname是選項名,參數strarg是字符串參數,參數numarg是int類型參數。
函數*regcfg列出如下:
struct cfgstruct * parsecfg( const char * cfgfile, int messages) { ...... //定義配置文件中選項的類型 struct cfgoption cfg_options[ ] = { { "LogFile" , OPT_FULLSTR} , //佔一行的字符串參數 { "LogFileUnlock" , OPT_NOARG} , //沒有參數 { "LogFileMaxSize" , OPT_COMPSIZE} , //轉換KByte和MByte到Byte ...... { "OnUpdateExecute" , OPT_FULLSTR} , /* freshclam */ { "OnErrorExecute" , OPT_FULLSTR} , /* freshclam */ { "OnOutdatedExecute" , OPT_FULLSTR} , /* freshclam */ { "LocalIPAddress" , OPT_STR} , /*用於freshclam的字符串參數*/ { 0 , 0 } , //表示結尾 } ; if ( ( fs = fopen( cfgfile, "r" ) ) == NULL) //打開配置文件 return NULL; /*函數fgets從fs中讀取尺寸最大LINE_LENGTH(爲1024)長度的字符,存入buff中,當讀到EOF時,讀操作停止。下次再調用函數fgets時,將從新的一行開始。讀完一行後,在buff末尾加上‘/0’字符*/ while ( fgets( buff, LINE_LENGTH, fs) ) { //每次讀取配置文件的一行 line++; if ( buff[ 0 ] == '#' ) //跳過註釋行 continue ; //如果存在Example行,說明配置文件還沒修改過,需要用戶配置好後,去掉這一行,這樣配置文件纔可用 if ( ! strncmp( "Example" , buff, 7 ) ) { //如果buff的前7個字符爲“Example” if ( messages) fprintf( stderr, "ERROR: Please edit the example config file %s./n " , cfgfile) ; fclose( fs) ; return NULL; } //每行的第一個域爲選項名,第二個域爲參數 if ( ( name = cli_strtok( buff, 0 , " /r /n " ) ) ) { //得到一行的第0域的值,每個域以“/r/n”中的字符分隔 arg = cli_strtok( buff, 1 , " /r /n " ) ; found = 0 ; //遍歷選項數組cfg_options,查找與配置文件相匹配的選項名 for ( i = 0 ; ; i++ ) { pt = & cfg_options[ i] ; if ( pt-> name) { if ( ! strcmp( name, pt-> name) ) { //在數組中找到與配置文件中相匹配的選項 found = 1 ; switch ( pt-> argtype) { //根據選項類型分析字符串 case OPT_STR: //字符串參數 ...... copt = regcfg( copt, name, arg, 0 ) ; break ; case OPT_FULLSTR: //佔一行的字符串參數 ...... free ( arg) ; //返回buff匹配子字符串" "(空格)的位置的字符指針 arg = strstr( buff, " " ) ; arg = strdup( ++ arg) ; //strdup表示字符複製,這裏用於刪除空格 //如果“/n/r”任何一個字符在arg中,返回匹配位置開始字符指針 if ( ( c = strpbrk( arg, "/n /r " ) ) ) //將“/n/r”轉換成“/0” * c = '/0 ' ; copt = regcfg( copt, name, arg, 0 ) ; break ; case OPT_NUM: //數字參數 ...... copt = regcfg( copt, name, NULL, atoi( arg) ) ; //將字符轉換成int類型 free( arg) ; break ; case OPT_COMPSIZE: //轉換KByte和MByte到Byte ...... //將arg的最後一個字符轉換成小寫 ctype = tolower( arg[ strlen( arg) - 1 ] ) ; if ( ctype == 'm' || ctype == 'k' ) { char * cpy = ( char * ) mcalloc( strlen( arg) , sizeof ( char ) ) ; //拷貝參數arg除表示單位的字符外的字符串到cpy strncpy( cpy, arg, strlen( arg) - 1 ) ; ...... if ( ctype == 'm' ) //轉換單位MByte到1024*1024 calc = atoi( cpy) * 1024 * 1024 ; else calc = atoi( cpy) * 1024 ; free( cpy) ; } else { ...... copt = regcfg( copt, name, NULL, calc) ; } free( arg) ; break ; case OPT_NOARG: //沒有參數 ...... copt = regcfg( copt, name, NULL, 0 ) ; break ; case OPT_OPTARG: //選項參數字符串 copt = regcfg( copt, name, arg, 0 ) ; break ; default : ...... free ( name) ; free( arg) ; break ; } } } else break ; } ...... } } fclose( fs) ; return copt; } 函數* regcfg將選項值加到選項名爲optname的節點上,參數copt爲節點鏈表,每個節點是一個cfgstruct結構實例,參數optname是選項名,參數strarg是字符串參數,參數numarg是int 類型參數。 函數* regcfg列出如下: struct cfgstruct * regcfg( struct cfgstruct * copt, char * optname, char * strarg, int numarg) { struct cfgstruct * newnode, * pt; //給鏈表copt分配新的節點 newnode = ( struct cfgstruct * ) mmalloc( sizeof ( struct cfgstruct) ) ; newnode-> optname = optname; newnode-> nextarg = NULL; newnode-> next = NULL; //將選項參數值存入新節點 if ( strarg) newnode-> strarg = strarg; else { newnode-> strarg = NULL; newnode-> numarg = numarg; } if ( ( pt = cfgopt( copt, optname) ) ) { //如果在鏈表中找到這個選項名的節點,則將參數加到這個節點上 while ( pt-> nextarg) pt = pt-> nextarg; pt-> nextarg = newnode; return copt; //返回找到的節點 } else { //若鏈表中沒找到這個選項名的節點,則加上新節點,並返回新節點 newnode-> next = copt; return newnode; } }
3.5 log文件操作
文件操作有基於文件描述符和基於流的操作,它們各有特點,基於文件描述符對整個文件操作較簡單,如:文件加鎖、修改及獲得文件屬性等。基於流的操作對文件內容的操作相對簡單,常用來對文件進行讀寫操作。
基於文件描述符的文件操作函數有open、close、read、write、lseek、fstat、dup2等,特點是函數使用文件描述符進行操作。
基於流的文件的操作函數有fopen、fclose、fread、fwrite、fflush、fseek等。特點是函數使用FILE類型的數據流進行讀寫操作,文件被打開時,C庫給文件指定了緩衝區而不需要用戶管理緩衝區。
由於基於文件描述符和基於流的操作各有特點,因此,經常需要轉換操作,當從文件描述符轉換到基於流操作時,可調用函數fdopen從描述符獲得FILE類型的數據流。當從基於流操作轉換成文件描述符操作時,可調用函數fileno將FILE類型的數據流轉換成文件描述符。
格式化輸入函數有scanf(從標準輸入流輸入)、fscanf(從指定的流輸入)和sscanf(從字符串輸入)。
格式化輸出函數有printf(向標準輸出流輸出)、fprintf(向指定的流輸出)、sprintf(向一個字符串輸出)、snprintf(向字符串輸出,可設定緩衝區)、vfprintf(可變參數的字符串輸出)、vsnprintf(將可變參數的字符串寫入到緩衝區)。
基於字符的輸入函數有fgetc、getc、getchar等,基於字符的輸出函數有fputc、putc、putchar、ungetc等。
基於行的輸入函數有fgets、gets,基於行的輸出函數有fputs、puts。
格式化輸出函數使用了可變參數,如:int printf( const char* format, ...),參數format是固定的,其他參數的個數和類型都不是固定的。這些函數是通過函數調用時參數壓棧的機制來獲取各個參數的。由於不同的硬件平臺,內存對齊的格式不一樣,因此,提取可變參數的方法還與平臺有關,在stdarg.h頭文件中,針對不同平臺有不同的宏定義,下面以x86平臺下的宏定義來說明可變參數提取方法。
stdarg.h頭文件定義了獲取可變參數的宏,它們的定義如下:
typedef char * va_list; //& ~(sizeof(int) - 1) )表示&~(4-1),即與11...1100進行邏輯與運算,32位對齊 #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) //通過第一個固定參數v獲得第一個可變參數ap #define va_arg(ap,t) ( *(t *) - _INTSIZEOF(t� ) //得到t類型的變量 #define va_end(ap) ( ap = (va_list)0 ) //將指針置爲無效
在_INTSIZEOF(n)定義中,sizeof(int)=4,32位CPU是32位對齊,即4個字節。_INTSIZEOF(n)表示得出變量的所佔字節大小並對n的變量類型進行32位對齊。
函數調用時參數的壓棧方式如圖5,從圖中可以看出,函數的返回地址及參數時依次排列的,因此,從函數的返回地址可以依次得到各個參數的值。
高位地址 | |
第二個可變參數地址 | |
第一個可變參數地址 | |
*str(第一個固定參數地址) | |
低位地址 | 函數返回地址 |
宏定義va_start(ap,v)表示在第一個固定變量v的地址加上固定變量v所佔內存的大小,這將得到第一個可變參數的地址放入ap。
宏定義va_arg(ap,t)再將ap轉換成t類型的變量。它先ap指向下一個可變參數,然後減去當前可變參數的大小,即得到當前可變參數的內存地址,再進行類型轉換。參數類型t的獲取,對於printf這樣的函數來說,是通過分析format字符串來確定每個可變參數的類型。
clamd使用了類似於printf的可變參數的函數logg來將clamd程序的信息寫入log文件或通過syslog寫入系統log文件中。函數logg調用了C庫函數vfprintf、fprintf、vsnprintf和syslog,函數vfprintf將可變參數的字符串寫入文件,函數fprintf將字符串信息寫入文件,函數vsnprintf將可變參數的字符串寫入到緩衝區。
函數logg還使用了函數umask處理打開文件所需要的權限,使用寫文件保護鎖來保護log文件的寫操作。同時,因支持多個線程,還使用了線程互斥鎖。
函數logg與寫log文件相關代碼列出如下(在clamav/shared/output.c中):
int logg( const char * str, ...) { ...... va_start ( args, str) ; //通過固定參數str得到第一個可變參數的地址,存入args /* va_copy is less portable so we just use va_start once more */ va_start( argscpy, str) ; if ( logg_file) { #ifdef CL_THREAD_SAFE pthread_mutex_lock( & logg_mutex) ; //多線程互斥鎖 #endif if ( ! logg_fs) { old_umask = umask( 0037 ) ; //打開文件的操作需要0037掩碼的權限,操作完成後恢復權限 //配置文件中logg_file缺省配置爲/tmp/clamd.log if ( ( logg_fs = fopen( logg_file, "a" ) ) == NULL) { //以追加方式打開log文件 /*如果打開失敗*/ umask( old_umask) ; #ifdef CL_THREAD_SAFE pthread_mutex_unlock( & logg_mutex) ; #endif printf ( "ERROR: Can't open %s in append mode (check permissions!)./n " , logg_file) ; return - 1 ; } else umask( old_umask) ; if ( logg_lock) { //需要文件鎖 memset( & fl, 0 , sizeof ( fl) ) ; fl.l_type = F_WRLCK; //文件鎖類型爲寫鎖定 // fileno將FILE類型的logg_fs轉換成文件描述符,F_SETLK表示操作爲文件加鎖 if ( fcntl( fileno( logg_fs) , F_SETLK, & fl) == - 1 ) { //如果設置文件鎖出錯 #ifdef CL_THREAD_SAFE pthread_mutex_unlock( & logg_mutex) ; #endif return - 1 ; } } } /*寫入當前時間信息到log文件*/ if ( logg_time && ( ( * str != '*' ) || logg_verbose) ) { time( & currtime) ; //獲取當前時間 pt = ctime( & currtime) ; //轉換時間值到本地時間,並轉換成字符串,如:Wed Jun 15 11:38:07 198800 timestr = mcalloc( strlen( pt) , sizeof ( char ) ) ; strncpy( timestr, pt, strlen( pt) - 1 ) ; //拷貝字符串到timestr fprintf( logg_fs, "%s -> " , timestr) ; //將timestr字符串寫入log文件 free( timestr) ; //釋放內存 } if ( logg_size) { //需要限制log文件大小 if ( stat( logg_file, & sb) != - 1 ) { //得到文件統計信息到sb if ( sb.st_size > logg_size) { //比較文件大小是否超出限制 logg_file = NULL; fprintf( logg_fs, "Log size = %d, maximal = %d/n " , ( int ) sb.st_size , logg_size) ; fprintf( logg_fs, "LOGGING DISABLED (Maximal log file size exceeded)./n " ) ; fclose( logg_fs) ; logg_fs = NULL; #ifdef CL_THREAD_SAFE pthread_mutex_unlock( & logg_mutex) ; #endif return 0 ; } } } if ( * str == '!' ) { //將錯誤字符串寫入log文件 fprintf( logg_fs, "ERROR: " ) ; //向文件寫入字符串 vfprintf( logg_fs, str + 1 , args) ; //向文件寫入可變參數字符串,str爲格式字符串,args爲變量參數 } else if ( * str == '^' ) { fprintf( logg_fs, "WARNING: " ) ; vfprintf( logg_fs, str + 1 , args) ; } else if ( * str == '*' ) { if ( logg_verbose) vfprintf( logg_fs, str + 1 , args) ; } else vfprintf( logg_fs, str, args) ; //寫出信息 fflush( logg_fs) ; //將內存中內容刷新進文件 #ifdef CL_THREAD_SAFE pthread_mutex_unlock( & logg_mutex) ; #endif } ...... va_end ( args) ; va_end( argscpy) ; return 0 ; }
3.6使用syslog機制輸出調試信息
Linux操作系統內核及應用程序的信息可以通過syslogd後臺進程寫入到/var/log目錄下的信息文件(messages.*中)。syslogd後臺進程通過打開和讀/dev/klog設備可以讀取內核的調試信息,如:printk的打印信息。用戶進程(或後臺進程)可以調用syslog函數將產生調試信息寫入系統log。syslogd後臺進程根據/etc/syslog.conf中的設定把log信息寫入相應文件中、郵寄給特定用戶或者直接以消息的方式發往控制檯。
在C庫中,函數closelog, openlog和syslog發送消息到系統logger,這幾個函數的定義列出如下:
#include <syslog.h> void openlog( const char * ident, int option, int facility) ; void syslog( int priority, const char * format, ...) ; void closelog( void ) ; #include <stdarg.h> void vsyslog( int priority, const char * format, va_list ap) ;
函數closelog()關閉用來寫信息到系統logger的描述符。
函數openlog()給程序打開一個到系統logger的連接,參數ident所指的字符串常被設置到程序名,參數option定義了控制operlog()操作,參數facility表示寫入log信息的程序類型,如:LOG_KERN(內核信息)、LOG_LPR(打印機子系統信息)、LOG_ERR(錯誤信息)等。
函數syslog()產生log消息,並將被syslogd後臺進程分配到合適的文件或設備。函數closelog()和openlog()是可選的,函數syslog()可以自動調用它們,這時,參數ident爲缺省值NULL。
函數logg中與syslog相關的代碼列出如下(在clamav/shared/output.c中):
int logg( const char * str, ...) { char * pt, * timestr, vbuff[ 1025 ] ; ...... va_start ( args, str) ; //通過固定參數str得到第一個可變參數的地址,存入args /* va_copy is less portable so we just use va_start once more */ va_start( argscpy, str) ; ...... #if defined(USE_SYSLOG) && !defined(C_AIX) if ( logg_syslog) { //如果使用系統log vsnprintf( vbuff, 1024 , str, argscpy) ; //將格式字符串str和可變參數argscpy寫到vbuff中 vbuff[ 1024 ] = 0 ; //末尾設置爲0 if ( vbuff[ 0 ] == '!' ) { //錯誤信息 syslog( LOG_ERR, "%s" , vbuff + 1 ) ; //將vbuff信息寫入到錯誤信息類型中 } else if ( vbuff[ 0 ] == '^' ) { //警告信息 syslog( LOG_WARNING, "%s" , vbuff + 1 ) ; //加1是爲了不輸出'^'字符 } else if ( vbuff[ 0 ] == '*' ) { //調試信息 if ( logg_verbose) { syslog( LOG_DEBUG, "%s" , vbuff + 1 ) ; //將vbuff信息寫入到調試信息類型中 } } else syslog( LOG_INFO, "%s" , vbuff) ; //將vbuff信息寫入到log信息類型中 } #endif va_end( args) ; va_end( argscpy) ; return 0 ; }
3.7 用戶組及文件權限設置
用戶登錄時,會有一個用戶標識符uid和用戶組標識符gid,root用戶的uid爲0。當進程運行時,它可以當使用函數getuid()和getgid()得到這個uid和gid,返回進程的真實用戶標識符ruid和用戶真實組標識符rgid。函數setuid和setgid執行後會把進程的euid或egid設爲文件的uid或gid。進程執行打開文件、收發信號等操作時,系統檢查uid和gid,檢查有否操作權限。
文件系統中的文件或目錄等對於uid、gid和其它組有一個寫、讀和執行的控制權限位,創建文件的uid爲文件的所有者,只有文件的所有者或root用戶可以改變文件的控制權限。
用戶標識符和組標識符說明如下:
(1) ruid和rguid
它們是運行進程的真實用戶ID和組ID。
(2) euid和egid
有效ID是進程運行時設置的ID,它們是用於權限檢查(文件系統除外)的有效用戶ID和組ID,一般等於ruid和rguid。
(3) suid和sgid
它們是保存的用戶ID和組ID,suid屬性允許可信任的程序臨時切換自己的uid。按以下規則使用suid:如果ruid 被改變,或者euid爲不等於ruid的值,suid就被設爲新的euid。非特權用戶可以用自己的suid來設置euid,把ruid設爲euid,以及把euid設爲ruid。從而讓普通用戶可以執行某些特權操作,操作完後。回覆到普通用戶權限。
(4) fsuid和fsgid
fsuid和fsgid 是文件系統用戶ID和文件系統羣組ID,它是Linux特有的屬性,用來允許NFS 服務器一類的程序把自己的文件系統權限限制在某些給定的UID 上,而不給這些UID 向進程發送信號的許可。一旦euid被改變,fsuid就被改爲新的euid值;非root 調用者只能把fsuid設置爲當前的ruid、euid或當前的fsuid。
應用程序可以設置進程的用戶ID和組ID,常用來降低權限。它先使用函數getpwnam從密碼文件/etc/passwd中取出指定用戶名或uid的條目,返回由結構passwd描述帳號數據,如:用戶口令、home目錄等。還可以直接使用函數getpw、fgetpwent、getpwent和getpwuid得到具體的數據。
函數initgroups (const char *user, gid_t group) 初始化進程的組訪問鏈表,它讀取組數據庫/etc/group,提取用戶爲user的所有組,同時還加入被充組group到組訪問鏈表。
或者調用函數getgroups(int size, gid_t list[])將參數list 數組中的組加入到當前進程的組設置中。參數size爲list()的gid_t數目,最大值爲NGROUP(32)。
然後調用函數setgid(gid_t gid)設置當前進程有有效組ID,調用函數setuid(uid_t uid)設置當前進程的用戶ID。
函數getpwnam及結構passwd列出如下:
#include <pwd.h> #include <sys/types.h> struct passwd * getpwnam( const char * name) ; struct passwd * getpwuid( uid_t uid) ; struct passwd { char * pw_name; /* 用戶名*/ char * pw_passwd; /*用戶口令*/ uid_t pw_uid; /* 用戶id */ gid_t pw_gid; /* 組id */ char * pw_gecos; /* 真實名字*/ char * pw_dir; /* home目錄 */ char * pw_shell; /* shell程序*/ } ;
應用程序可使用函數chmod、chown、chgroup等更改文件和目錄的權限。當應用程序:創建文件和目錄時,它會給文件或目錄指定缺省權限,函數umask可以更改缺省權限設置。函數umask(mode_t mask)設置訪問文件的權限掩碼,將訪問文件的權限變爲mode&~umask權限值,通常umask值默認爲022。例如:函數open()中參數指定了一個權限值,如:0666,由打開文件的真正權限爲0666&~022=0644,也就是rw-r--r--。當參數mask值爲0時,則真正權限爲0777。
創建新文件時,函數umask設置掩碼中的每個權限位都會導致文件中的相應權限位被清除(處於禁用狀態),因此權限受到了屏蔽。相反,在掩碼中清除的位允許在新創建的文件中啓用相應的文件模式位。
函數clamd中與用戶權限設置相關的代碼列出如下:
void clamd( struct optstruct * opt) { ...... //加權限掩碼,即掩碼爲1的權限位不起作用,掩碼爲0,表示爲777權限,即root權限 umask( 0 ) ; ...... /* 通過設置組ID和用戶ID來降低權限*/ #ifndef C_OS2 //函數geteuid返回當前進程的有效用戶ID,有效ID設置被執行文件的ID位 if ( geteuid( ) == 0 && ( cpt = cfgopt( copt, "User" ) ) ) { //配置文件中User爲clamav //從密碼文件/etc/passwd中取出名爲clamav的用戶,如果爲空,表示沒用該用戶,退出 if ( ( user = getpwnam( cpt-> strarg) ) == NULL) { ...... } if ( cfgopt( copt, "AllowSupplementaryGroups" ) ) { //允許補充用戶組 #ifdef HAVE_INITGROUPS //將用戶爲cpt->strarg的組及被充組user->pw_gid初始化爲進程的組訪問鏈表 if ( initgroups( cpt-> strarg, user-> pw_gid) ) { ...... } #else logg( "AllowSupplementaryGroups: initgroups() not supported./n " ) ; #endif } else { #ifdef HAVE_SETGROUPS if ( setgroups( 1 , & user-> pw_gid) ) { //給進程設置補充組ID,1表示1個補充組ID ...... } #endif } if ( setgid( user-> pw_gid) ) { //設置進程的組ID ...... } if ( setuid( user-> pw_uid) ) { //設置進程的用戶ID ...... } logg( "Running as user %s (UID %d, GID %d)/n " , cpt-> strarg, user-> pw_uid, user-> pw_gid) ; } #endif ...... }
3.8 進程後臺化
clamd是一個長駐內存的後臺進程。它需要不斷地查詢內核攔截文件訪問後的掃描事件,另一方面還要與各個掃描客戶端通信並提供病毒掃描服務。
後臺進程(Daemon)又稱守護進程,它不屬於任何控制終端且週期性地執行某些操作,常被用作服務器進程,如:inetd、httpd等。
後臺進程由於長駐內存,因此,它必須與運行前的環境無關,除此之外,它與一般應用程序編程沒有什麼區別。因此,後臺進程應用程序的編程中常加入一個函數daemonize(void)實現後臺化,解除與運行前的環境的關係,其它與一般應用程序編程一樣。
新的應用程序啓動變成進程時,它會從父進程中繼承運行環境,如:用戶權限、控制終端,會話和進程組等。後臺進程必須解決這些相關性,爲了解決與運行環境的相關性,後臺進程應成爲新會話、新進程組的首進程,應該沒有控制終端。方法如下:
(1) 常忽略除SIGKILL和SIGSTOP之外的信號。
(2)調用fork終止父進程,讓應用程序在子進程中運行,這樣也脫離控制檯的控制。
if ( pid= fork( ) ) //如果是父進程 exit( 0 ) ; //結束父進程
(3) 調用函數setsid()以新的會話運行應用程序
每個進程屬於進程組,進程組號(gid)是進程組長的進程號(pid),進程組長一般是第一個創建進程組的進程。每個進程還有一個會話號(session id,簡稱sid),每個會話由多個進程組組成,會話組長進程的進程組gid爲會話的sid。
通常,一個會話對應一個控制終端,即多個進程組共享一個控制終端,進程創建時,控制終端,登錄會話和進程組通常是從父進程繼承下來的。後臺進程必須擺脫與控制終端的關係,一般通過調用函數setsid新建一個會話來擺脫與控制終端的關係。
後臺進程還常修改從父進程繼承來的用戶和用戶組。
(4) 關閉從創建它的父進程繼承的打開的文件描述符,包括標準描述符。後臺進程有自己信息輸出、輸入方法,不能接收終端信息或將信息發送給終端。
(5) 改變工作目錄,正在運行的進程所在目錄的文件系統不能被拆卸,後臺進程需要將工作目錄改變爲根目錄。
(6) 改變權限掩碼,使用函數umask(0)設置權限掩碼,這樣,後臺進程對文件操作有讀寫執行權限。
後臺進程不屬於任何終端,它的調試信息等不能象普通程序一樣將信息輸出到標準輸出和標準錯誤輸出中。因此,一般將後臺進程的信息寫一個log文件或者使用系統的syslog來記錄信息。
clmad應用程序在運行之初設置了用戶及用戶組,使用函數umask(0)設置權限掩碼,然後調用函數daemonize處理進程後臺化的操作。
函數daemonize列出如下(在clamav/clamd/clamd.c中):
void daemonize( void ) { int i; #ifdef C_OS2 return ; #else /*刪除標準輸入、標準輸出和錯誤輸出設備的數據*/ // /dev/null是個空設備,所有寫入這個設備的數據都會被丟棄 if ( ( i = open( "/dev/null" , O_WRONLY) ) == - 1 ) { //不能打開空設備 logg( "!Cannot open /dev/null. Only use Debug if Foreground is enabled./n " ) ; for ( i = 0 ; i <= 2 ; i++ ) close( i) ; //關閉標準描述符0,1,2,分別對應標準輸入、標準輸出、錯誤輸出 } else { //如果打開了空設備 close( 0 ) ; //關閉0描述符 dup2( i, 1 ) ; //重定向到i,即將寫向描述符1的數據輸向空設備 dup2( i, 2 ) ; //2重定向到i } if ( ! debug_mode) chdir( "/" ) ; //將工作目錄改變到根目錄下 if ( fork( ) ) exit( 0 ) ; //父進程退出 setsid( ) ; //創建新會話的sid #endif }
3.9 利用socket在進程間通信
套接字socket常用於客戶/服務器模型的應用程序之間的通信或數據傳遞,客戶端和服務器端即可以是在同一臺計算機上的兩個應用程序,也可以同網絡連接的兩臺計算機,一臺運行客戶端應用程序,另一臺運行服務器應用程序。
對TCP/IP協議中,客戶與服務器之間的傳輸分爲面向連接的傳輸和非連接的傳輸,分別對應TCP和UDP協議。
面向連接客戶程序的算法如下:
(1) 找到服務器的IP地址和協議端口號。
(2) 創建一個套接接口描述符。
(3) 指明一個本地機器沒有使用的協議端口。
(4) 與服務器建立連接。
(5) 與服務器通信(請求和應答)。
(6) 關閉連接。
面向連接客戶程序在使用socket中調用函數的次序一般爲socket()、connect()、send()、recv()、close()。
無連接客戶程序的算法如下:
(1) 找到服務器的IP地址和協議端口號。
(2) 創建一個套接接口描述符。
(3) 指明一個本地機器沒有使用的協議端口。
(4) 將數據包發向服務器。
(5) 關閉連接。
服務器一般設計爲一次處理多個請求的併發模式,也可以設計爲一次處理一個請求單次服務方式。服務器可以處理面向連接和無連接的通信模式。對於面向連接的通信模式,每個連接需要一個惟一端口號,無連接的通信模式,可以多個通信使用一個端口號。
面向連接的併發服務器一般使用多進程來達到併發性,這個多進程分爲主進程和從進程。主進程最先執行,它在從所周知的端口上打開一個套接口,等待下一個請求,爲每個請求創建一個從服務器進程,一個從進程與一個客戶通信,從進程響應完後就退出。
無連接的服務器只用單進程處理就足夠了。
面向連接的併發服務器的算法如下:
(1) 主進程創建一個套接品,並綁定到衆所周知的端口上,將套接字設置爲被動模式。
(2) 重複調用accept接收來自客戶的請求,並創建新的從進程來處理請求。
(3) 從進程通過連接與客戶進行交互,讀取請求併發迴響應。
(4) 從進程處理完用戶的請求後退出。
面向連接的服務器在使用socket中調用函數的次序一般爲socket()、bind()、listen()、accept()、recv()、send()。
socket支持的協議簇有: AF_INET(IPv4協議)、AF_INET6(IPv6協議)、AF_LOCAL(Unix域協議)等,支持數據流類型有SOCK_STREAM(數據流)、SOCK_DGRAM(數據報)和SOCK_RAW(原始數據流)和SOCK_PACKET(數據包)。
數據流傳遞是最常見的情況,下面就以數據流傳遞的方式說明利用socket套接字在客戶/服務器上面向連接的數據傳輸過程。其中,客戶端是clamdclan應用程序,服務器是clamd應用程序。qtclamavclient客戶端應用程序中使用了QSocket類,對數據通信傳遞過程已封裝,在應用程序中已很難看出socket的使用過程,故圖中客戶端以clamdclan應用程序爲例。客戶端的socket通信過程後面的小節說明。
圖6說明了數據流向服務器傳送過程,首先,客戶通過服務器通過公開的端口號與服務器建立連接,客戶向服務器發送服務請求,服務器在每個公開的端口號上建立服務線程,以反覆循環方式處理來自這個端口號的請求,如:clamd的病毒掃描線程,服務線程創建臨時端口號(在非公開端口號時動態分配),並將臨時端口號送給客戶端。
接着,客戶端收到臨時端口號後,再創建臨時套接字來傳遞數據,直到數據傳遞完,關閉臨時套接字。客戶端還可在剛使用的公開端口上傳遞或接收請求相關的數據,服務請求相關的數據傳遞完後,關閉這個公開端口上的套接字。
服務器也在臨時端口號上與客戶端傳遞數據,服務線程通過循環接收從臨時端口號上來自客戶的數據,並進行相應服務,如:病毒掃描。並將服務中的信息反饋給客戶端,服務完後,關閉臨時端口上的套接字。公開端口上的請求處理完後,關閉建立在公開端口號上的套接字。
圖6 面向連接並行服務器與客戶端的數據流傳遞過程
圖7中顯示了clamd服務器應用程序與socket操作相關的函數調用過程,clamd服務器在公開端口上建立嵌套字,並使用掃描線程分析客戶端的服務請求命令,函數command分析客戶端的服務請求,決定調用不同的掃描方式函數,如:函數scan、scanstream等。數據流傳遞方式調用函數scanstream。函數scanstream從臨時socket讀取數據,然後調用函數cl_scandesc來對數據進行病毒掃描。掃描的結果調用函數mdprintf返回客戶端。
簡單地說,clamd服務器使用一個線程,將客戶端的服務請求轉變成對clamlib庫中的API函數的調用,由clamlib庫完成病毒掃描。從函數command以後起就進入了clamlib庫函數了。
圖7 clamd服務器中與socket相關的函數調用過程clamd服務器中提供的服務定義列出如下(在clamav/clamd/session.h中):
#define CMD1 "SCAN" //病毒文件掃描 #define CMD2 "RAWSCAN" //原始數據的掃描 #define CMD3 "QUIT" //退出掃描 #define CMD4 "RELOAD" //重裝載病毒庫 #define CMD5 "PING" //查看到clamd的連接是否正常,返回“PONG”信息 #define CMD6 "CONTSCAN" //連續掃描 #define CMD7 "VERSION" //需要返回病毒庫版本號 #define CMD8 "STREAM" //以數據流形式掃描病毒 #define CMD9 "SESSION" //會話是否失敗 #define CMD10 "END" //掃描結束 #define CMD11 "SHUTDOWN" //掃描關閉 #define CMD12 "FD" //傳入文件描述符對文件進行掃描
(1)clamd服務器socket連接
clamd服務器提供了基於AF_INET(即IPv4協議)的網絡服務器socket接口和基於AF_LOCAL(即UNIX協議簇)的本地服務器socket接口,它們的編程方法類似,只是使用網絡服務器socket接口時,還需要輸入IP地址,並進行網絡字節序與主機字節序的轉換。從網絡字節序轉換到主機字節序使用函數ntohs,從主機字節序轉換到網絡字節序使用函數htons。
下面只分析clamd服務器使用本地套接字的函數localserver。
本地套接字綁定struct sockaddr_un。struct sockaddr_un有兩個參數:sun_family、sun_path。其中,sun_family只能是AF_LOCAL或AF_UNIX,sun_path是本地文件的路徑。通常將文件放在/tmp目錄下。
函數localserver創建套接字後,綁定socket文件到套接字上,接着,使用函數listen偵聽在套接字上,並使用一個無限循環調用函數accept從已連接的隊列中取出一個已完成的連接,然後創建線程在這個連接上收發數據。針對一個公開端口,創建一個線程,在一個連接上的工作完成時,銷燬線程和連接。
函數localserver中與socket相關的部分列出如下(在clamav/clamd/server-th.c中):
int localserver( const struct optstruct * opt, const struct cfgstruct * copt, struct cl_node * root) { ...... memset ( ( char * ) & server, 0 , sizeof ( server) ) ; server.sun_family = AF_UNIX; strncpy( server.sun_path , cfgopt( copt, "LocalSocket" ) -> strarg, sizeof ( server.sun_path ) ) ; //創建套接口描述符,使用AF_UNIX協議簇和SOCK_STREAM數據流類型 if ( ( sockfd = socket( AF_UNIX, SOCK_STREAM, 0 ) ) == - 1 ) { ...... } //綁定套接字到特定的地址和端口,如果服務器使用公開的端口號,就不需要調用這個函數,在調用函數connect或listen時,內核模塊會自動綁定到公開的端口,對於AF_UNIX,指socket文件 if ( bind( sockfd, ( struct sockaddr * ) & server, sizeof ( struct sockaddr_un) ) == - 1 ) { //如果綁定失敗 if ( errno == EADDRINUSE) { //如果指定的端口號(或socket文件)被其它進程使用 if ( connect( sockfd, ( struct sockaddr * ) & server, sizeof ( struct sockaddr_un) ) >= 0 ) { //如果連接成功,說明socket文件被使用,關閉套接字描述符,退出 close( sockfd) ; exit( 1 ) ; } if ( cfgopt( copt, "FixStaleSocket" ) ) { //配置文件中有FixStaleSocket項 if ( unlink( server.sun_path ) == - 1 ) { //如果刪除socket文件失敗,退出 ...... } //再次綁定socket,若失敗,則退出 if ( bind( sockfd, ( struct sockaddr * ) & server, sizeof ( struct sockaddr_un) ) == - 1 ) { ...... } } else if ( stat( server.sun_path , & foo) != - 1 ) { //把文件的統計信息存入foo中 ...... } } ...... } ...... //監聽客戶端的請求,服務器一般先使用函數listen監聽客戶端的請求,再使用函數accept處理連接 // backlog規定了內核爲此套接口排隊的最大排隊個數,缺省爲15 if ( listen( sockfd, backlog) == - 1 ) { ...... } acceptloop_th( sockfd, root, copt) ; return 0 ; }
函數acceptloop_th在socket建立後,創建一個線程來處理客戶端的請求。它從連接隊列中得到一個連接,並將連接的套接字描述符交給掃描線程處理。函數acceptloop_th中與socket相關的代碼列出如下:
int acceptloop_th( int socketd, struct cl_node * root, const struct cfgstruct * copt) { ...... //創建線程池,線程池處理函數爲scanner_thread if ( ( thr_pool= thrmgr_new( max_threads, idletimeout, scanner_thread) ) == NULL) { logg( "!thrmgr_new failed/n " ) ; exit( - 1 ) ; } time( & start_time) ; for ( ;; ) { //利用循環得到一個socket上的多個連接 //從已完成連接的隊列中返回一個連接,如果已連接隊列爲空,則進程睡眠。操作成功,返回已建立的連接,出錯返回-1 new_sd = accept( socketd, NULL, NULL) ; //已連接 if ( ( new_sd == - 1 ) && ( errno != EINTR) ) { //中斷可以引起返回-1,應剔除這種情況 continue ; } ...... if ( ! progexit && new_sd >= 0 ) { //填寫客戶端連接信息結構client_conn client_conn = ( client_conn_t * ) mmalloc( sizeof ( struct client_conn_tag) ) ; client_conn-> sd = new_sd; //連接的socket描述符 client_conn-> options = options; client_conn-> copt = copt; client_conn-> root = cl_dup( root) ; client_conn-> root_timestamp = reloaded_time; client_conn-> limits = & limits; client_conn-> mainpid = mainpid; if ( ! thrmgr_dispatch( thr_pool, client_conn) ) { //線程分發任務 close( client_conn-> sd) ; free( client_conn) ; } } pthread_mutex_lock( & exit_mutex) ; //線程退出互斥鎖 if ( progexit) { if ( new_sd >= 0 ) { close( new_sd) ; } pthread_mutex_unlock( & exit_mutex) ; break ; } pthread_mutex_unlock( & exit_mutex) ; ...... } //endfor ...... shutdown ( socketd, 2 ) ; //關閉所有的在socket上的全雙工連接,2表示不允許再有傳輸 close( socketd) ; //關閉描述符 return 0 ; }
在上述函數中,由函數socket返回的sockfd是監聽套接口描述符,函數accept返回的new_sd是已連接套接口描述符。它們的區別是:一個socket,只有一個監聽套接口描述符,而且一直存在,但在一個socket上可能有多個連接,每一個連接有一個已連接套接口描述符,當連接斷開時關閉這個描述符new_sd。只有不使用這個socket時,才能關閉監聽套接口描述符sockfd。
(2) clamd從socket收發數據
每個掃描客戶端應用程序與服務器建立連接時,服務器創建一個線程循環處理在已連接的socket描述符上的會話。這個線程調用函數select或poll睡眠等待在socket描述符上直到超時或描述符上有數據改變後,才解析socket上傳來的數據,根據用戶的請求調用相應的處理函數。
函數command從socket中讀取服務請求,調用功能函數執行相應的服務,對應於網絡協議標準分層中的會話層。函數command中的部分代碼列出如下(在clamav/clamd/session.c中):
int command( int desc, const struct cl_node * root, const struct cl_limits * limits, int options, const struct cfgstruct * copt, int timeout) { char buff[ 1025 ] ; int bread, opt, retval; struct cfgstruct * cpt; retval = poll_fd( desc, timeout) ; //調用函數select進行多路複用 switch ( retval) { case 0 : //超時 return - 2 ; case - 1 : //錯誤 mdprintf( desc, "ERROR/n " ) ; //將錯誤信息發向socket return - 1 ; } //利用循環直到讀出數據 while ( ( bread = readsock( desc, buff, 1024 ) ) == - 1 && errno == EINTR) ; //從socket中讀取服務請求 if ( bread == 0 ) { /* 連接關閉 */ return - 1 ; } if ( bread < 0 ) { return - 1 ; } buff[ bread] = 0 ; //緩衝區末尾加0 cli_chomp( buff) ; //刪除末尾的‘/n’或‘/r’字符 //根據服務請求請用相關的服務 if ( ! strncmp( buff, CMD1, strlen( CMD1) ) ) { /* 比較前面SCAN字符 */ //按文件路徑掃描病毒文件 // 由buff + strlen(CMD1) + 1的地址得到文件路徑名 if ( scan( buff + strlen( CMD1) + 1 , NULL, root, limits, options, copt, desc, 0 ) == - 2 ) //掃描文件 if ( cfgopt( copt, "ExitOnOOM" ) ) return COMMAND_SHUTDOWN; } …… } else if ( ! strncmp( buff, CMD8, strlen( CMD8) ) ) { /* STREAM */ if ( scanstream( desc, NULL, root, limits, options, copt) == CL_EMEM) //掃描數據流 if ( cfgopt( copt, "ExitOnOOM" ) ) return COMMAND_SHUTDOWN; } .... else { mdprintf( desc, "UNKNOWN COMMAND/n " ) ; } return 0 ; /*沒有錯誤和服務請求*/ }
從socket接收或發送數據一般使用函數recv和send或recvmsg和sendmsg,這兩個函數及參數用到的結構定義如下:
int recvmsg( int sockfd, struct msghdr * msg, int flags) int sendmsg( int sockfd, struct msghdr * msg, int flags) struct msghdr { void * msg_name; int msg_namelen; struct iovec * msg_iov; //接受或發送數據緩衝區的數組 int msg_iovlen; //結構數組的大小 void * msg_control; //輔助數據 int msg_controllen; //輔助數據緩衝區大小 int msg_flags; //已接收消息的標識 } struct iovec { void * iov_base; /* 緩衝區開始的地址 */ size_t iov_len; /* 緩衝區的長度 */ }
函數readsock調用函數recvmsg從socket中讀取數據放在buf中返回,列出如下(在clamav/clamd/others.c中):
int readsock( int sockfd, char * buf, size_t size) { int fd; ssize_t n; struct msghdr msg; struct iovec iov[ 1 ] ; ...... iov [ 0 ] .iov_base = buf; //存儲消息的緩衝區 iov[ 0 ] .iov_len = size; memset( & msg, 0 , sizeof ( msg) ) ; msg.msg_iov = iov; msg.msg_iovlen = 1 ; ...... if ( ( n = recvmsg( sockfd, & msg, 0 ) ) <= 0 ) //接收數據 return n; errno = EBADF; if ( n != 1 || buf[ 0 ] != 0 ) //buf中有數據,如果字符串頭有“FD”,返回-1 return ! strncmp( buf, CMD12, strlen( CMD12) ) ? - 1 : n; //CMD12爲"FD" ...... }
函數mdprintf將可變參數的字符串str通過函數send發送到socket上。列出如下(在clamav/shared/output.c中):
int mdprintf( int desc, const char * str, ...) { va_list args; char buff[ 512 ] ; int bytes; va_start( args, str) ; bytes = vsnprintf( buff, sizeof ( buff) , str, args) ; //將字符串及可變參數寫入buff va_end( args) ; if ( bytes == - 1 ) return bytes; if ( bytes >= sizeof ( buff) ) bytes = sizeof ( buff) - 1 ; return send( desc, buff, bytes, 0 ) ; //發送到socket }
(3) socket描述符多路複用
在TCP連接中,recv等函數默認爲阻塞模式,即一旦調用了這些函數,就需要等待數據到來之後函數纔會返回。當然也可以使用函數setsockopt()設置超時時限,當超時時間到時,函數recv返回。
進程可以使用系統調用select()或函數poll實現多路複用,即同時監控一個以上的文件描述符(fd)。當沒有設備準備好時,select()發生阻塞,直到超時返回,如果其中一個設備準備好就返回。函數poll與select功能類似,這裏只說明函數select。
使用函數select可以指定等待多個描述字,任何一個準備好都可以喚醒進程進行處理。
函數select定義如下:
#include<sys/time.h> #include<sys/types.h> #include<unistd.h> int select( int n, fd_set * readfds, fd_set * writefds, fd_set * exceptfds, struct timeval * timeout) ;
函數select()等待文件描述符狀態的改變後返回。其中參數n爲最大文件描述符加1,參數readfds、writefds 和exceptfds分別爲讀、寫和例外描述符數組。參數timeout爲超時值。
timeval的結構定義如下:
struct timeval{ long tv_sec; //表示幾秒 long tv_usec; //表示幾微妙 }
select調用返回時,除了已經就緒的描述符外,select將清除readfds、writefds和exceptfds中的所有沒有就緒的描述符。正常情況下,函數select返回值就緒的文件描述符個數;超時後,返回0;出錯時返回-1並設置相應錯誤號。
系統提供了4個宏對描述符集進行操作,這些宏列出如下:
#include <sys/select.h> #include <sys/time.h> void FD_SET( int fd, fd_set * fdset) ; //設置fdset中對應於文件描述符fd的位(設置爲1) void FD_CLR( int fd, fd_set * fdset) ; //清除fdset中對應於文件描述符fd的位 void FD_ISSET( int fd, fd_set * fdset) ; //檢測fdset中對應於文件描述符fd的位是否被設置 void FD_ZERO( fd_set * fdset) ; //清除fdset中的所有位(既把所有位都設置爲0)
clmad在掃描線程處理函數scanner_thread中使用函數poll_fd對sockfd描述符進行多路複用,函數poll_fd列出如下:
int poll_fd( int fd, int timeout_sec) { int retval; #ifdef HAVE_POLL struct pollfd poll_data[ 1 ] ; poll_data[ 0 ] .fd = fd; poll_data[ 0 ] .events = POLLIN; poll_data[ 0 ] .revents = 0 ; if ( timeout_sec > 0 ) { timeout_sec *= 1000 ; } while ( 1 ) { retval = poll( poll_data, 1 , timeout_sec) ; if ( retval == - 1 ) { if ( errno == EINTR) { //信號中斷 continue ; } return - 1 ; } return retval; } #else fd_set rfds; struct timeval tv; if ( fd >= DEFAULT_FD_SETSIZE) { return - 1 ; } while ( 1 ) { FD_ZERO( & rfds) ; //描述符集清0 FD_SET( fd, & rfds) ; //加入描述符fd對應的位到rfds tv.tv_sec = timeout_sec; //設置超時 tv.tv_usec = 0 ; retval = select( fd+ 1 , & rfds, NULL, NULL, ( timeout_sec> 0 ? & tv : NULL) ) ; if ( retval == - 1 ) { if ( errno == EINTR) { continue ; } return - 1 ; } return retval; } #endif return - 1 ; }
(4)使用臨時socket傳輸數據
函數scanstream提供數據流病毒掃描服務。對於數據流傳輸模式,服務器找到一個空閒的端口號作爲臨時端口,建立socket套接口用來傳輸數據,傳輸來的數據放在一個臨時文件中,由clamlib庫函數對這個臨時文件進行病毒掃描,若發現病毒根據配置文件中的選項,對病毒文件使用系統命令進行處理,並返回信息到客戶端。函數scanstream通過臨時端口號接收傳輸數據的流程圖如圖14。
函數scanstream接收來自客戶端數據流時,先將接收到的數據流存入臨時文件,然後調用掃描庫函數對這個臨時文件進行病毒掃描。
圖14 函數scanstream通過臨時端口號接收傳輸數據的流程圖
函數scanstram列出如下(在clamav/clamd/scanner.c中):
int scanstream( int odesc, unsigned long int * scanned, const struct cl_node * root, const struct cl_limits * limits, int options, const struct cfgstruct * copt) { …… char buff[ FILEBUFF] ; ...... /*綁定到空閒的端口號直到綁定成功 */ while ( ! bound && -- portscan) { //缺省portscan爲1000,即掃描1000個端口 if ( rnd_port_first) { //表示第一次隨機分配端口 /*首先隨機嘗試一個端口號 */ port = min_port + cli_rndnum( max_port - min_port + 1 ) ; rnd_port_first = 0 ; } else { /* 嘗試其它相鄰的端口號*/ if ( -- port < min_port) port= max_port; } memset( ( char * ) & server, 0 , sizeof ( server) ) ; server.sin_family = AF_INET; server.sin_port = htons( port) ; //主機字節序轉換到網絡字節序 if ( ( cpt = cfgopt( copt, "TCPAddr" ) ) ) { //從配置文件中查詢"TCPAddr"的值 pthread_mutex_lock( & gh_mutex) ; 發 //加線程鎖 //通過名字得到主機信息,如果失敗,返回信息到客戶端 if ( ( he = gethostbyname( cpt-> strarg) ) == 0 ) { mdprintf( odesc, "gethostbyname(%s) ERROR/n " , cpt-> strarg) ; pthread_mutex_unlock( & gh_mutex) ; return - 1 ; } server.sin_addr = * ( struct in_addr * ) he-> h_addr_list[ 0 ] ; //得到地址 pthread_mutex_unlock( & gh_mutex) ; } else server.sin_addr .s_addr = INADDR_ANY; if ( ( sockfd = socket( AF_INET, SOCK_STREAM, 0 ) ) == - 1 ) //如果創建socket不成功 continue ; if ( bind( sockfd, ( struct sockaddr * ) & server, sizeof ( struct sockaddr_in) ) == - 1 ) //如果綁定不成功 close( sockfd) ; else bound = 1 ; //已成功綁定 } …… if ( ! bound && ! portscan) { //如果沒發現空閒端口號 logg( "!ScanStream: Can't find any free port./n " ) ; mdprintf( odesc, "Can't find any free port. ERROR/n " ) ; //發送錯誤信息回客戶端 close( sockfd) ; //關閉套接口描述符 return - 1 ; } else { //已綁定在空閒的端口號上,這個空閒端口號成爲數據傳輸的臨時端口號 listen( sockfd, 1 ) ; //在套接口上偵聽,1表示隊列長度爲1 mdprintf( odesc, "PORT %d/n " , port) ; //發送臨時端口號回客戶端 } switch ( retval = poll_fd( sockfd, timeout) ) { //調用函數select進行多路複用 case 0 : /* timeout */ mdprintf( odesc, "Accept timeout. ERROR/n " ) ; logg( "!ScanStream: accept timeout./n " ) ; close( sockfd) ; return - 1 ; case - 1 : mdprintf( odesc, "Accept poll. ERROR/n " ) ; logg( "!ScanStream: accept poll failed./n " ) ; close( sockfd) ; return - 1 ; } //返回已連接的套接口描述符到acceptd if ( ( acceptd = accept( sockfd, NULL, NULL) ) == - 1 ) { //如果沒連接上 close( sockfd) ; mdprintf( odesc, "accept() ERROR/n " ) ; //發送錯誤到客戶端 return - 1 ; } if ( ( tmp = tmpfile( ) ) == NULL) { //用C庫函數tmpfile創建一個惟一的臨時文件 shutdown( sockfd, 2 ) ; //關閉sockfd上的所有連接 close( sockfd) ; //關閉描述符 close( acceptd) ; //關閉已連接的描述符 mdprintf( odesc, "tempfile() failed. ERROR/n " ) ; //發送錯誤到客戶端 return - 1 ; } tmpd = fileno( tmp) ; //由FILE類型數據流tmp轉換到文件描述符 ...... btread = sizeof ( buff) ; while ( ( retval = poll_fd( acceptd, timeout) ) == 1 ) { bread = read( acceptd, buff, btread) ; //從socket中讀取數據 if ( bread <= 0 ) break ; size += bread; if ( writen( tmpd, buff, bread) != bread) { //將數據寫入臨時文件中 …… } …… } …… lseek( tmpd, 0 , SEEK_SET) ; //文件定位指針回到0 //調用clamlib庫函數cl_scandesc掃描病毒 ret = cl_scandesc( tmpd, & virname, scanned, root, limits, options) ; if ( tmp) fclose( tmp) ; close( acceptd) ; //關閉連接的套接口描述符 close( sockfd) ; if ( ret == CL_VIRUS) { //發現病毒 mdprintf( odesc, "stream: %s FOUND/n " , virname) ; //將病毒信息送回客戶端 virusaction( "stream" , virname, copt) ; //根據配置文件對病毒文件進行清除操作 } else if ( ret != CL_CLEAN) { //返回信息錯誤 mdprintf( odesc, "stream: %s ERROR/n " , cl_strerror( ret) ) ; } else { //沒發現病毒 mdprintf( odesc, "stream: OK/n " ) ; //返回信息到客戶端 } return ret; }
3.10 子進程執行系統命令及環境變量設置
在程序中,子進程常用於執行一個單獨的應用程序,程序可以使用exec簇函數執行應用程序或者使用函數system調用系統提供了各種命令,就象在終端上執行命令行一樣方便。
用戶使用系統調用函數fork創建新進程,調用fork的進程稱爲父進程,新創建的進程稱爲子進程。父子進程除了返回值pid不同外,具有相同的用戶級上下文。子進程pid爲0。父子進程可通過管道、套接字、消息隊列、共享內存進行通信,函數fork列出如下:
#include <sys/types.h> #include <unistd.h> pid_t fork( void ) ; pid_t vfork( void ) ;
函數vfork和fork的作用基本相同,但vfork不完全拷貝父進程的數據段,而是和父進程共享數據段。
函數waitpid等待子進程中斷或結束,函數wait和waitpid列出如下:
#include<sys/types.h> #include<sys/wait.h> pid_t wait ( int * status) ; pid_t waitpid( pid_t pid, int * status, int options) ;
函數waitpid()和wait功能類似,wait()是waitpid()特定模式,它只用於等待子進程。這兩個函數暫時停止當前進程的執行,直到有信號來到或子進程結束。函數執行成功則返回子進程pid,如果有錯誤發生則返回-1,失敗原因存於errno中。
如果在調用wait()時子進程已經結束,則wait()會立即返回子進程結束狀態值。子進程的結束狀態值由參數status返回,函數返回子進程的進程ID。參數pid其他數值意義如下:
- pid<-1 等待進程組ID爲pid絕對值的子進程。
- pid=-1 等待任何子進程,相當於wait()。
- pid=0 等待進程組ID與該進程相同的子進程。
- pid>0 等待pid進程的子進程。
參數option指定進程所做的操作,可以爲0 或下面的操作常數的或邏輯(OR)組合:
- WNOHANG 如果不使進程掛起而立即返回。
- WUNTRACED 如果子進程結束就返回。
子進程的結束狀態返回後存於status,底下有幾個宏可判別結束情況
- WIFEXITED(status) 如果子進程正常結束,則返回真。
- WEXITSTATUS(status) 返回子進程exit()返回的結束代碼,一般會先用WIFEXITED 來判斷是否正常結束才能使用此宏。
- WIFSIGNALED(status) 如果子進程是因爲信號而結束則此宏值爲真
- WTERMSIG(status) 取得子進程因信號而中止的信號代碼,一般會先用WIFSIGNALED 來判斷後才使用此宏。
- WIFSTOPPED(status) 如果子進程處於暫停執行情況則此宏值爲真。一般只有使用WUNTRACED 時纔會有此情況。
- WSTOPSIG(status) 取得引發子進程暫停的信號代碼,一般會先用WIFSTOPPED 來判斷後才使用此宏。
在clamd服務器,函數virusaction根據配置文件的選項,在子進程中執行系統命令,對病毒感染的文件進行操作(如:刪除),父進程等待直到子進程操作的完成。
函數virusaction列出如下(在clamav/clamd/other.c中):
void virusaction( const char * filename, const char * virname, const struct cfgstruct * copt) { pid_t pid; struct cfgstruct * cpt; if ( ! ( cpt = cfgopt( copt, "VirusEvent" ) ) ) //分析配置選項 return ; pid = fork( ) ; if ( pid == 0 ) { /* 子進程... */ char * buffer, * pt, * cmd; //得到操作系統命令,如:/usr/local/bin/send_sms 123456789 "VIRUS ALERT: %v"命令,它發送郵件信息,其中,%v用病毒名替代 cmd = strdup( cpt-> strarg) ; //先分配空間,再拷貝字符串 //將cmd中%v用病毒名替換 if ( ( pt = strstr( cmd, "%v" ) ) ) { //返回cmd中“%v”字符開始的指針,即%v開始的字符串 buffer = ( char * ) mcalloc( strlen( cmd) + strlen( virname) + 10 , sizeof ( char ) ) ; * pt = 0 ; //將cmd從%V開始的位置置爲0,即刪除了%v以後的字符串 pt += 2 ; //pt跳過2個字符,即跳過%v strcpy( buffer, cmd) ; //cmd拷貝到buffer中 strcat( buffer, virname) ; //將virname拷貝到buffer末尾 strcat( buffer, pt) ; //將pt拷貝到buffer末尾 free( cmd) ; cmd = strdup( buffer) ; //先分配空間,再拷貝字符串 free( buffer) ; } /* 設置環境變量,CLAM_VIRUSEVENT_FILENAME =文件名*/ buffer = ( char * ) mcalloc( strlen( ENV_FILE) + strlen( filename) + 2 , sizeof ( char ) ) ; sprintf( buffer, "%s=%s" , ENV_FILE, filename) ; putenv( buffer) ; /* 設置環境變量,CLAM_VIRUSEVENT_VIRUSNAME buffer = (char *) mcalloc(strlen(ENV_VIRUS) + strlen(virname) + 2, sizeof(char�; sprintf(buffer, "%s=%s", ENV_VIRUS, virname); putenv(buffer); /* 執行系統命令*/ exit( system( cmd) ) ; …… } else if ( pid > 0 ) { //父進程 /* 父進程等待子進程的退出 */ waitpid( pid, NULL, 0 ) ; } else { /* 錯誤*/ logg( "!VirusAction: fork failed./n " ) ; } }
3.11線程
Linux系統下的多線程遵循POSIX線程接口,稱爲pthread。線程由系統內核調度程序來實現。由於運行於一個進程中的多個線程,它們彼此之間使用相同的地址空間,共享大部分數據,所以一個線程的數據可以直接爲其它線程所用,因此,多個線程的使用效率比多個進程高得多。
線程的生命週期一般包括就緒、運行、阻塞和終止幾個狀態,一般用條件等待或互斥量進行阻塞。
線程的編程基本模型有流水線、工作組和客戶端/服務器模型。流水線模式是指一個線程的操作結果交給下一步驟的其他線程,幾個線程並行運行組成流水線。工作組模式是指工作組中的線程執行的工作獨立,由多個線程獨立完成一個操作集。客戶端/服務器模式是指客戶端線程提出服務請求,由服務器線程完成服務請求的功能。
(1) 線程創建及線程屬性
在Linux上,線程在用戶空間由函數pthread_create創建,但從Linux內核來看,線程和進程最終都由內核函數do_fork()創建,一個進程的多個線程也是特殊的進程,他們有各自的進程描述結構,但與一般進程不同的時,線程共享了同一個進程上下文。因此,線程又稱爲輕量級進程。內核函數do_fork()創建進程和線程時,它們使用的參數不同,當內核函數do_fork()創建線程時,它使用了參數CLONE_VM(共享內存空間)、CLONE_FS(共享文件系統信息)、CLONE_FILES(共享文件描述符表)等。當用戶空間使用fork系統調用時創建進程時,內核調用函數do_fork()不使用任何共享屬性,進程擁有獨立的運行環境。
創建一個新線程使用的函數定義如下:
#include <pthread.h> int pthread_create ( pthread_t * thread, pthread_attr_t * attr, void * ( * start_routine) ( void * ) , void * arg) ;
其中,參數thread返回創建的線程ID,參數attr用來設置線程屬性,參數start_routine是線程運行函數,參數arg是運行函數的參數。當創建線程成功時,返回0,若失敗則返回不爲0。常見的錯誤碼有EAGAIN和EINVAL。EAGAIN表示由於系統資源限制引起的錯誤,如:線程數目太多。EINVAL表示參數attr代表的線程屬性值非法。
線程的屬性結構pthread_attr_t定義如下(在/usr/include/bits/pthreadtypes.h中)
typedef struct __pthread_attr_s { /*缺省爲PTHREAD_CREATE_JOINABLE,表示原有的線程等待創建的線程結束,在原有的線程中用pthread_join()來等待創建的線程結束。還可設置爲PTHREAD_CREATE_DETACH狀態,表示分離獨立的線程,此時,不能再恢復到PTHREAD_CREATE_JOINABLE狀態*/ int __detachstate; /*表示調度策略,有SCHED_OTHER(正常、非實時)、SCHED_RR(實時、輪轉法)和SCHED_FIFO(實時、先入先出)三種,缺省爲SCHED_OTHER,後兩種調度策略僅對超級用戶有效。運行時可以用過pthread_setschedparam()來改變*/ int __schedpolicy; /*在調度策略爲實時(即SCHED_RR或SCHED_FIFO)時,可以改變成員sched_priority值,它表示線程的運行優先級。還通過pthread_setschedparam()函數來改變,缺省爲0*/ struct __sched_param __schedparam; /*值爲PTHREAD_EXPLICIT_SCHED(缺省值)和PTHREAD_INHERIT_SCHED,前者表示使用attr中指定調度策略和調度參數,後者表示繼承調用者線程的值*/ int __inheritsched; /*值爲PTHREAD_SCOPE_SYSTEM(缺省值)和PTHREAD_SCOPE_PROCESS,前者表示與系統中所有線程一起競爭CPU時間,後者表示僅與同進程中的線程競爭CPU*/ int __scope; size_t __guardsize; int __stackaddr_set; void * __stackaddr; size_t __stacksize; } pthread_attr_t;
pthread_attr_t結構中屬性可以通過屬性函數進行設置。
(2) 線程結束
函數pthread_exit結束一個線程,返回會下放在__retval中,在結束線程之前,它還調用用戶爲線程註冊的清除處理函數。函數pthread_exit定義如下:
extern void pthread_exit ( void * __retval) __attribute__ ( ( __noreturn__) ) ;
(3) 取消線程
處理線程取消操作的函數定義如下:
/*設置當前線程的取消狀態,返回舊狀態在__oldstate中。state值爲:PTHREAD_CANCEL_ENABLE(缺省)和PTHREAD_CANCEL_DISABLE,分別表示收到信號後設爲CANCLED狀態和忽略CANCEL信號繼續運行*/ extern int pthread_setcancelstate ( int __state, int * __oldstate) ; /*設置線程取消的類型,type值爲:PTHREAD_CANCEL_DEFFERED(表示收到信號後繼續運行至下一個取消點再退出)和PTHREAD_CANCEL_ASYCHRONOUS(表示立即執行取消動作),僅當Cancel狀態爲Enable時有效,oldtype存入舊類型值。*/ extern int pthread_setcanceltype ( int __type, int * __oldtype) ; /* 立即取消線程或下一次可能時機取消線程*/ extern int pthread_cancel ( pthread_t __cancelthread) ; /*檢查本線程正掛起的取消 ,並它已被取消,則就象使用pthread_exit(PTHREAD_CANCELED)一樣終止線程*/ extern void pthread_testcancel ( void ) ;
(4) 線程的等待
多個線程之間常需要同步,如:一個線程等待另一個線程的數據。此時,需要使用線程的等待。當等待的條件滿足時喚醒等待的線程。
線程的等待相關函數列出如下:
/* 調用的線程等待線程__th的結束,線程的退出狀態存在_thread_return 中,這樣兩個線程融合爲一個*/ extern int pthread_join ( pthread_t __th, void ** __thread_return) ; /* 指示線程__th從不與屬性爲PTHREAD_JOIN 的線程融合,當__the結束時,它的資源立即被釋放,而不是等待另一個線程執行pthread_join 操作。*/ extern int pthread_detach ( pthread_t __th) __THROW; /* 等待條件__cond 時喚醒線程*/ extern int pthread_cond_signal ( pthread_cond_t * __cond) __THROW; /*等待條件__cond 時喚醒所有線程*/ extern int pthread_cond_broadcast ( pthread_cond_t * __cond) __THROW;
(5) 互斥
線程間常需要共享數據,如果兩個線程同時訪問共享數據時,必須保持訪問的互斥性,如:一次只能允許一個線程寫數據,其他線程必須等待。這種互斥機制是通過互斥鎖實現的。
線程與互斥鎖相關的函數定義如下:
/*使用值__mutex_attr初始化互斥對象,__mutex 初始化爲互斥對象*/ extern int pthread_mutex_init ( pthread_mutex_t * __restrict __mutex, __const pthread_mutexattr_t * __restrict __mutex_attr) __THROW; /* 銷燬互斥對象__mutex */ extern int pthread_mutex_destroy ( pthread_mutex_t * __mutex) __THROW; /*嘗試鎖定互斥對象。如果互斥對象處於解鎖狀態,那麼將獲得該鎖並且函數將返回零。如果互斥對象已鎖定,它不會阻塞,而是返回非零的EBUSY 錯誤值。以後再嘗試鎖定。*/ extern int pthread_mutex_trylock ( pthread_mutex_t * __mutex) __THROW; /* 等待直到__mutex 鎖可用,並加鎖*/ extern int pthread_mutex_lock ( pthread_mutex_t * __mutex) __THROW; /* 解鎖*/ extern int pthread_mutex_unlock ( pthread_mutex_t * __mutex) __THROW;
(6) 線程數據
在多線程程序中有三種數據類型全局變量、局部變量和線程數據。全局變更和局部變量的含義與單線程程序中一樣,線程數據類似於全局變量,但它的作用範圍是一個線程,即在一個線程內部的值是一致的,不同的線程中將線程數據附加鍵值進行區別,線程數據通過鍵值得到。
線程數據必須通過相關的函數用使用,有4個函數處理線程數據,它們的作用有:創建一個鍵(函數pthread_key_create);給一個鍵指定線程數據(函數pthread_setpecific);從一個鍵讀取線程數據(函數pthread_getspecific);刪除鍵。
下面以一個樣例說明如何創建線程數據,在樣例中的函數createWindow會被各個線程使用,樣例列出如下:
/*爲線程數據聲明一個鍵*/ pthread_key_t myWinKey; /* 函數 createWindow */ void createWindow ( void ) { Fl_Window * win; //將被用作線程數據的變量 //表示線程初始化一次,進程中只有一個這個變量實例,因此,使用了靜態變量 static pthread_once_t once= PTHREAD_ONCE_INIT; /*爲了讓鍵只被創建一次,調用函數pthread_once,它只在第一次調用時執行初始化函數createMyKey創建鍵*/ pthread_once ( & once, createMyKey) ; /*win指向一個新建立的窗口*/ win= new Fl_Window( 0 , 0 , 100 , 100 , "MyWindow" ) ; /* 對此窗口作一些可能的設置工作,如大小、位置、名稱等*/ setWindow( win) ; /* 將窗口指針值win綁定在鍵myWinKey上*/ pthread_setpecific ( myWinKey, win) ; } /* 函數 createMyKey,創建一個鍵,並指定了destructor */ void createMyKey ( void ) { pthread_keycreate( & myWinKey, freeWinKey) ; } /* 函數 freeWinKey,釋放空間*/ void freeWinKey ( Fl_Window * win) { delete win; }
在線程的函數中使用線程數據時,可以用下面的方法得線程數據:
Fl_Window * win 2 = pthread_getspecific( myWinKey) ;
clamd服務器在函數acceptloop_th中創建線程,它使用一個線程管理OnAccess病毒掃描,使用線程池管理用戶的病毒掃描請求。服務器使用一個公開的端口接收所有的客戶端的服務請求,多個客戶端的服務請求使用多個線程調用函數scanner_thread來完成服務請求的功能。
OnAccess病毒掃描線程的分離狀態屬性爲PTHREAD_CREATE_JOINABLE,因此,重裝載病毒庫後,使用函數pthread_kill發信號殺死OnAccess病毒掃描線程,使用函數pthread_join等待它完全退出後,才能重創建這個線程。
掃描線程的分離狀態屬性爲PTHREAD_CREATE_DETACHED,它是個分離獨立的線程,不影響其它線程。
函數acceptloop_th與線程相關的代碼列出如下:
int acceptloop_th( int socketd, struct cl_node * root, const struct cfgstruct * copt) { int new_sd, max_threads, stdopt; threadpool_t * thr_pool; ...... pthread_attr_t thattr; if ( ( cpt = cfgopt( copt, "MaxThreads" ) ) ) { //從配置文件中讀出最大線程數 max_threads = cpt-> numarg; } else { max_threads = CL_DEFAULT_MAXTHREADS; } ...... pthread_attr_init ( & thattr) ; //初始化線程屬性 pthread_attr_setdetachstate( & thattr, PTHREAD_CREATE_DETACHED) ; //是分離的、獨立的線程 if ( cfgopt( copt, "ClamukoScanOnLine" ) || cfgopt( copt, "ClamukoScanOnAccess" ) ) #ifdef CLAMUKO { pthread_attr_init( & clamuko_attr) ; //初始化線程屬性 //設置線程屬性,這種屬性表示原有的線程等待創建的線程結束。只有當pthread_join()函數返回時,創建的線程纔算終止,才能釋放自己佔用的系統資源。 pthread_attr_setdetachstate( & clamuko_attr, PTHREAD_CREATE_JOINABLE) ; tharg = ( struct thrarg * ) mmalloc( sizeof ( struct thrarg) ) ; tharg-> copt = copt; tharg-> root = root; tharg-> limits = & limits; tharg-> options = options; //創建線程clamukoth,它的參數是tharg,創建的線程id放在clamuko_pid返回 pthread_create( & clamuko_pid, & clamuko_attr, clamukoth, tharg) ; } #endif ...... #if defined(C_BIGSTACK) || defined(C_BSD) /*由於病毒掃描使用了很大的buffer,因此,設置線程的堆棧大小,方法是先得到屬性,再設置屬性*/ pthread_attr_getstacksize( & thattr, & stacksize) ; pthread_attr_setstacksize( & thattr, stacksize + SCANBUFF + 64 * 1024 ) ; #endif pthread_mutex_init( & exit_mutex, NULL) ; //初始化互斥變量exit_mutex,NULL表示初始爲缺省值 pthread_mutex_init( & reload_mutex, NULL) ; //創建線程,並創建線程池,處理函數爲scanner_thread if ( ( thr_pool= thrmgr_new( max_threads, idletimeout, scanner_thread) ) == NULL) { exit( - 1 ) ; } ....... for ( ;; ) { new_sd = accept( socketd, NULL, NULL) ; //得到連接的套接口描述符 ...... if ( ! progexit && new_sd >= 0 ) { client_conn = ( client_conn_t * ) mmalloc( sizeof ( struct client_conn_tag) ) ; client_conn-> sd = new_sd; client_conn-> options = options; client_conn-> copt = copt; client_conn-> root = cl_dup( root) ; client_conn-> root_timestamp = reloaded_time; client_conn-> limits = & limits; client_conn-> mainpid = mainpid; //本進程的pid if ( ! thrmgr_dispatch( thr_pool, client_conn) ) { close( client_conn-> sd) ; free( client_conn) ; } } if ( selfchk) { //需要檢查庫 time( & current_time) ; if ( ( current_time - start_time) > ( time_t) selfchk) { //如果病毒庫的時間超過有效期,重裝載庫 if ( reload_db( root, copt, TRUE) ) { //檢查庫 //全局變量reload被多個線程使用,必須使用互斥鎖 pthread_mutex_lock( & reload_mutex) ; reload = 1 ; //需要重裝載庫 pthread_mutex_unlock( & reload_mutex) ; } time( & start_time) ; } } pthread_mutex_lock( & reload_mutex) ; if ( reload) { //重裝載庫 pthread_mutex_unlock( & reload_mutex) ; root = reload_db( root, copt, FALSE) ; //不檢查庫,而是重裝載庫 pthread_mutex_lock( & reload_mutex) ; reload = 0 ; time( & reloaded_time) ; //將當前時間放入reloaded_time,作爲重裝載庫的時間 pthread_mutex_unlock( & reload_mutex) ; #ifdef CLAMUKO if ( cfgopt( copt, "ClamukoScanOnLine" ) || cfgopt( copt, "ClamukoScanOnAccess" ) ) { //向線程clamuko_pid發信號SIGUSR1 pthread_kill( clamuko_pid, SIGUSR1) ; //線程屬性爲PTHREAD_CREATE_JOINABLE,用pthread_join()等待線程結束 pthread_join( clamuko_pid, NULL) ; tharg-> root = root; pthread_create( & clamuko_pid, & clamuko_attr, clamukoth, tharg) ; //創建線程 } #endif } else { pthread_mutex_unlock( & reload_mutex) ; } } //endfor /* 等待所有當前的任務完成,銷燬線程管理器*/ thrmgr_destroy( thr_pool) ; #ifdef CLAMUKO if ( cfgopt( copt, "ClamukoScanOnLine" ) || cfgopt( copt, "ClamukoScanOnAccess" ) ) { //向線程clamuko_pid發信號SIGUSR1 pthread_kill( clamuko_pid, SIGUSR1) ; //線程屬性爲PTHREAD_CREATE_JOINABLE,用pthread_join()等待線程結束 pthread_join( clamuko_pid, NULL) ; } #endif ...... return 0 ; }
3.12 線程池
線程池用隊列或鏈表將工作(如:服務請求)保存在工作隊列,工作用需要處理的任務的特徵數據代表。線程池使用預創建技術,它創建一定數量的空閒線程,這些線程處於阻塞(Suspended)狀態。當有工作任務時,緩衝池選擇一個空閒線程,把任務附在這個線程運行,即線程執行工作處理函數,任務完成後成爲空閒線程,線程不退出,而是回到線程池。線程池維持着一定數量的線程。
線程池技術減少了線程創建和銷燬的次數,但也佔用了少量資源。因此,線程池技術適合於單位時間內處理任務多且線程創建銷燬頻繁的工作。
clamd服務器使用了一個簡單的線程池,這個線程池只使用了處理函數scanner_thread,即每個線程都調用處理函數scanner_thread。線程池以多個同樣的線程分別獨立進行病毒的掃描。
線程池一般使用了線程池結構、工作隊列結構和工作隊列成員結構來描述,每個工作以某一特徵數據代表,將工作放入工作隊列中,clamd服務器的線程池結構、工作隊列結構和工作隊列成員結構列出如下(在clamav/clamd/thrmgr.h中):
typedef struct threadpool_tag { pthread_mutex_t pool_mutex; pthread_cond_t pool_cond; pthread_attr_t pool_attr; pool_state_t state; //線程池狀態:無效、有效、退出 int thr_max; //最大線程數 int thr_alive; //激活的線程數 int thr_idle; //空閒的線程數 int idle_timeout; //空閒定時 void ( * handler) ( void * ) ; //線程處理函數 work_queue_t * queue; //工作隊列 } threadpool_t; typedef struct work_queue_tag { work_item_t * head; work_item_t * tail; int item_count; //工作隊列中的成員數 } work_queue_t; typedef struct work_item_tag { struct work_item_tag * next; void * data; struct timeval time_queued; } work_item_t;
工作隊列鏈表的管理函數有work_queue_new、work_queue_add和work_queue_pop,分別用來創建工作隊列鏈表、向鏈表增加成員、從鏈表中取出成員。
函數thrmgr_new創建線程池,初始化線程池結構threadpool_t中的成員,賦上線程池處理函數,線程條件初始化爲缺省值PTHREAD_COND_INITIALIZER。
函數thrmgr_new列出如下(在clamav/clamd/thrmgr.c中):
threadpool_t * thrmgr_new( int max_threads, int idle_timeout, void ( * handler) ( void * ) ) { threadpool_t * threadpool; if ( max_threads <= 0 ) { return NULL; } //初始化線程池結構 threadpool = ( threadpool_t * ) mmalloc( sizeof ( threadpool_t) ) ; threadpool-> queue = work_queue_new( ) ; //初始化工作隊列鏈表 if ( ! threadpool-> queue) { free( threadpool) ; return NULL; } threadpool-> thr_max = max_threads; threadpool-> thr_alive = 0 ; threadpool-> thr_idle = 0 ; threadpool-> idle_timeout = idle_timeout; threadpool-> handler = handler; pthread_mutex_init( & ( threadpool-> pool_mutex) , NULL) ; //初始化線程互斥變量 if ( pthread_cond_init( & ( threadpool-> pool_cond) , NULL) != 0 ) { //初始化線程條件 free( threadpool) ; return NULL; } if ( pthread_attr_init( & ( threadpool-> pool_attr) ) != 0 ) { //初始化線程屬性 free( threadpool) ; return NULL; } //設置爲分離獨立的線程 if ( pthread_attr_setdetachstate( & ( threadpool-> pool_attr) , PTHREAD_CREATE_DETACHED) != 0 ) { free( threadpool) ; return NULL; } threadpool-> state = POOL_VALID; return threadpool; }
當clamd服務器接收到來自客戶端的服務請求時,它將服務請求作爲工作使用函數thrmgr_dispatch將工作加在工作隊列上,並查找線程池,如果空閒的線程沒達到指定的數量,就創建線程,然後喚醒排在最前面的空閒線程。
函數thrmgr_dispatch列出如下(在clamav/clamd/thrmgr.c中):
int thrmgr_dispatch( threadpool_t * threadpool, void * user_data) { pthread_t thr_id; if ( ! threadpool) { //線程池結構爲空,返回錯誤 return FALSE; } /* 鎖住threadpool */ if ( pthread_mutex_lock( & ( threadpool-> pool_mutex) ) != 0 ) { //加鎖 return FALSE; } if ( threadpool-> state != POOL_VALID) { //線程池無效,返回錯誤 if ( pthread_mutex_unlock( & ( threadpool-> pool_mutex) ) != 0 ) { //開鎖 return FALSE; } return FALSE; } work_queue_add( threadpool-> queue, user_data) ; //將工作加入工作隊列 //沒有空閒線程,且激活的線程數沒超過最大值 if ( ( threadpool-> thr_idle == 0 ) && ( threadpool-> thr_alive < threadpool-> thr_max) ) { /* 創建新的線程thrmgr_worker */ if ( pthread_create( & thr_id, & ( threadpool-> pool_attr) , thrmgr_worker, threadpool) != 0 ) { logg( "!pthread_create failed/n " ) ; } else { threadpool-> thr_alive++; //激活的線程計數加1 } } //發射條件信號,喚醒排在第一個的睡眠線程 pthread_cond_signal( & ( threadpool-> pool_cond) ) ; if ( pthread_mutex_unlock( & ( threadpool-> pool_mutex) ) != 0 ) { //開鎖 return FALSE; } return TRUE; }
線程池中的線程thrmgr_worker不斷地從工作隊列中查找是否有工作要做,如果沒有就等待,當等待超時,線程退出。如果工作隊列中有工作,就調用處理函數對工作進行處理。如果沒有激活的線程,就廣播線程條件信號,讓所有線程從等待狀態中喚醒。
線程函數thrmgr_worker列出如下(clamav/clamd/thrmgr.c中):
void * thrmgr_worker( void * arg) { threadpool_t * threadpool = ( threadpool_t * ) arg; void * job_data; int retval, must_exit = FALSE; struct timespec timeout; /* 循環查找工作 */ for ( ;; ) { //操作threadpool時加線程鎖 if ( pthread_mutex_lock( & ( threadpool-> pool_mutex) ) != 0 ) { //加鎖 /*加鎖出錯*/ exit( - 2 ) ; } timeout.tv_sec = time( NULL) + threadpool-> idle_timeout; //定時 timeout.tv_nsec = 0 ; threadpool-> thr_idle++; //如果從工作隊列中沒找到工作且線程池處於非退出狀態,就等待 while ( ( ( job_data= work_queue_pop( threadpool-> queue) ) == NULL) && ( threadpool-> state != POOL_EXIT) ) { /* 睡眠等待在條件pool_cond 上,條件滿足時喚醒,如果等待超時,返回錯誤 */ retval = pthread_cond_timedwait( & ( threadpool-> pool_cond) , & ( threadpool-> pool_mutex) , & timeout) ; if ( retval == ETIMEDOUT) { //等待超時,中斷循環 must_exit = TRUE; break ; } } //從工作隊列中沒找到工作 threadpool-> thr_idle--; if ( threadpool-> state == POOL_EXIT) { //如果線程池是退出狀態 must_exit = TRUE; } if ( pthread_mutex_unlock( & ( threadpool-> pool_mutex) ) != 0 ) { //開鎖 exit( - 2 ) ; } if ( job_data) { //如果數據存在,調用處理函數 threadpool-> handler( job_data) ; } else if ( must_exit) { break ; } } if ( pthread_mutex_lock( & ( threadpool-> pool_mutex) ) != 0 ) { //加鎖 exit( - 2 ) ; } threadpool-> thr_alive--; if ( threadpool-> thr_alive == 0 ) { /* 喚醒所有的線程,發出所有線程完成的條件信號 */ pthread_cond_broadcast( & threadpool-> pool_cond) ; } if ( pthread_mutex_unlock( & ( threadpool-> pool_mutex) ) != 0 ) { //開鎖 exit( - 2 ) ; } return NULL; }
3.13 信號處理
linux下進程間通信的方法主要有管道(Pipe)及有名管道(named pipe)、信號(Signal)、消息隊列、共享內存、信號量(semaphore)和套接口(Socket)幾種。管道常用於父子進程間的數據傳遞;共享內存常用於進程間大量共享數據的傳遞;消息隊列用於進程間的消息傳遞;信號用於進程對系統定義的各種信號的異步處理;信號量常用於多個進程訪問互斥共享資源時對互斥共享資源的保護,套接口常用於客戶端/服務器模型的應用程序之間的通信(本地或網絡上的應用程序之間)。
對於進程來說,不同的進程具有獨立的數據空間,進程間傳遞數據必須使用都進程間的通信方法,標準C庫提供了進程間通信的基本方法,爲了使用方便,許多具體的應用封裝了自己的進程間通信方法,如:Qt使用Qcopchannel,文件讀寫使用的互斥鎖函數flock等。
對線程來說,通信則方便得多,因爲同一進程下的線程之間共享數據空間,因此,全局變量數據可以直接被其它線程使用。正是由於數據的共享,全局變量數據可以同時被兩個線程所修改,因此,函數中static的數據使用不當,可能讓多線程程序崩潰。
信號應用於異步事件的處理,它可用於進程或線程。線程提供了專門的函數用於線程間的同步和互斥。信號的一個最常見的用途是在錯誤發生時通知進程結束。
對於普通權限的進程來說,具有相同uid和gid的進程或在同一進程組中的進程才能傳遞信號。信號最終通過設置內核進程結構task_struct的signal域裏的某一位來取作用的,進程的每次調度時會檢查signal域,然後執行信號對應的處理函數。
在/usr/include/sys/signal.h有各種信號的定義。絕大部分是系統定義且有系統定義的處理函數。除了SIGSTOP和SIGKILL外,所有的信號都能被阻塞。
系統提供了函數signal和sigaction註冊信號的響應函數,使進程能改變自己對信號的處理函數。一個用戶進程常需要對多個信號進行處理,可以使用信號集,系統提供了一套函數對信號集進行處理。
信號掩碼是發送給當前進程、被阻塞的信號集,信號阻塞指當信號被放入信號掩碼集中,這個信號將被進程忽略,除非它被指定了信號響應函數。對於信號掩碼集中指定了信號響應函數的信號,它將執行指定的信號響應。不在信號掩碼中的信號繼續執行系統缺省的響應處理函數。
信號的發射函數有系統調用函數kill, raise, alarm和setitimer函數,kill向指定進程發送信號,raise向自己發送信號,alarm函數在指定時間後向自己發送SIGALRM信號。
信號的阻塞函數有sigprocmask和sigsuspend,函數sigprocmask設置信號掩碼和去掉信號掩碼,函數sigsuspend使進程掛起,直到信號掩碼的處理函數執行完成才恢復執行。
函數acceptloop_th與信號處理相關的代碼通過信號掩碼屏蔽不需要的信號,對於進程關心的信號設置了信號響應函數,這部分代碼列出如下:
int acceptloop_th( int socketd, struct cl_node * root, const struct cfgstruct * copt) { ...... struct sigaction sigact; ...... sigset_t sigset; /* 設置信號掩碼集,忽略信號掩碼集中的信號,進程對它們不作響應*/ //將sigset所指向的信號集設定爲滿,即包含所有信號 sigfillset( & sigset) ; //從信號集sigset中刪除信號SIGINT sigdelset( & sigset, SIGINT) ; //進程中斷信號,通常是從終端輸入的中斷指令,如:Ctrl+C鍵 sigdelset( & sigset, SIGTERM) ; //調用kill()命令時缺省產生的信號,表示中止進程 sigdelset( & sigset, SIGSEGV) ; //使用非法內存地址所產生的信號 sigdelset( & sigset, SIGHUP) ; //當終端發現斷線情況時發送給控制終端的進程的信號,常用來通知守護進程重新讀取系統配置文件 sigdelset( & sigset, SIGPIPE) ; //當對一個讀進程已經運行結束的管道執行寫操作時產生的信號 sigdelset( & sigset, SIGUSR2) ; //用戶定義信號 //將sigset設置爲信號掩碼集,SIG_SETMASK表示信號集sigset對信號掩碼進行賦值操作 sigprocmask( SIG_SETMASK, & sigset, NULL) ; /* 對信號SIGINT, SIGTERM, SIGSEGV 等指定信號響應函數*/ sigact.sa_handler = sighandler_th; //加入信號處理函數 sigemptyset( & sigact.sa_mask ) ; //清空信號掩碼集 sigaddset( & sigact.sa_mask , SIGINT) ; //將信號SIGINT加入信號掩碼集 sigaddset( & sigact.sa_mask , SIGTERM) ; sigaddset( & sigact.sa_mask , SIGHUP) ; sigaddset( & sigact.sa_mask , SIGPIPE) ; sigaddset( & sigact.sa_mask , SIGUSR2) ; sigaction( SIGINT, & sigact, NULL) ; //註冊信號SIGINT的處理函數 sigaction( SIGTERM, & sigact, NULL) ; sigaction( SIGHUP, & sigact, NULL) ; sigaction( SIGPIPE, & sigact, NULL) ; sigaction( SIGUSR2, & sigact, NULL) ; if ( ! debug_mode) { sigaddset( & sigact.sa_mask , SIGHUP) ; //將信號SIGHUP加入信號掩碼集 // SIGSEGV是使用非法內存地址所產生的信號 sigaction( SIGSEGV, & sigact, NULL) ; //註冊信號SIGSEGV處理函數 } ...... return 0 ; }
信號發射常使用函數kill,函數scanner_thread收到客戶端的關閉請求時,發射SIGTERM信號關閉指定進程conn->mainpid。函數scanner_thread中發射信號部分的代碼列出如下:
void scanner_thread( void * arg) { sigset_t sigset; int ret, timeout, session= FALSE; ...... /*忽略所有的信號*/ sigfillset( & sigset) ; //將所有的信號填充信號集 pthread_sigmask( SIG_SETMASK, & sigset, NULL) ; //設置信號掩碼 ...... switch ( ret) { case COMMAND_SHUTDOWN: pthread_mutex_lock( & exit_mutex) ; //用來保護全局變量progexit progexit = 1 ; kill( conn-> mainpid, SIGTERM) ; //發送信號SIGTERM給主線程,表示結束進程 pthread_mutex_unlock( & exit_mutex) ; break ; ...... } ...... }
信號掩碼的信號處理函數sighandler_th列出如下:
void sighandler_th( int sig) { switch ( sig) { case SIGINT: case SIGTERM: progexit = 1 ; //設置程序退出標識,去通知函數scanner_thread的執行退出,即退出會話 break ; case SIGSEGV: //使用非法內存地址所產生的信號 logg( "Segmentation fault :-( Bye../n " ) ; _exit( 11 ) ; /* probably not reached at all */ break ; /* not reached */ case SIGHUP: //終端斷線時發送組終端控制進程的信號 sighup = 1 ; //設置標識後,在函數logg中根據標識重新打開log文件 break ; case SIGUSR2: //用戶定義信號,這裏用於重裝載病毒庫 reload = 1 ; break ; default : break ; /* Take no action on other signals - e.g. SIGPIPE */ } }
3.14 OnAccess掃描病毒線程clamukoth
OnAccess掃描是指當用戶打開、讀、寫等操作文件時,Linux內核從文件系統攔截用戶進程的訪問,通過hook函數調用用戶空間的病毒掃描庫函數掃描用戶訪問的文件。線程clamukoth負責OnAccess操作中的病毒掃描。OnAccess操作中的用戶空間接口函數由名爲Dazuko的庫完成。
線程clamukoth設置Dazuko模塊,並從Dazuko模塊得到攔截的文件訪問信息,然後,調用clamlib庫函數對文件進行掃描,發現病毒後,對文件進行處理,並將文件訪問許可返回Dazuko模塊去內核控制文件的訪問。Dazuko模塊見本章後面小節的分析。
線程clamukoth還屏蔽除了SIGUSR1、SIGSEGV 外所有的信號,信號SIGUSR1用於線程的退出設置。
函數clamukoth列出如下(在clamav/clamd/clamuko.c中):
void * clamukoth( void * arg) { struct thrarg * tharg = ( struct thrarg * ) arg; ...... /* 屏蔽除了SIGUSR1、SIGSEGV 外所有的信號*/ sigfillset( & sigset) ; //將所有的信號放入信號集sigset sigdelset( & sigset, SIGUSR1) ; //從信號集sigset刪除信號SIGUSR1 sigdelset( & sigset, SIGSEGV) ; //將sigset設置爲線程的信號掩碼集,線程屏蔽這些信號 pthread_sigmask( SIG_SETMASK, & sigset, NULL) ; act.sa_handler = clamuko_exit; //設置信號的響應函數 sigfillset( & ( act.sa_mask ) ) ; sigaction( SIGUSR1, & act, NULL) ; sigaction( SIGSEGV, & act, NULL) ; /*將ClamAV註冊到Dazuko模塊上*/ if ( dazukoRegister( "ClamAV" , "r+" ) ) { return NULL; } /*設置訪問掩碼,這裏攔截文件的open、close和exec操作*/ if ( cfgopt( tharg-> copt, "ClamukoScanOnOpen" ) ) { mask |= DAZUKO_ON_OPEN; } if ( cfgopt( tharg-> copt, "ClamukoScanOnClose" ) ) { mask |= DAZUKO_ON_CLOSE; } if ( cfgopt( tharg-> copt, "ClamukoScanOnExec" ) ) { mask |= DAZUKO_ON_EXEC; } if ( ! mask) { dazukoUnregister( ) ; return NULL; } //設置訪問掩碼 if ( dazukoSetAccessMask( mask) ) { dazukoUnregister( ) ; return NULL; } if ( ( pt = cfgopt( tharg-> copt, "ClamukoIncludePath" ) ) ) { while ( pt) { if ( ( dazukoAddIncludePath( pt-> strarg) ) ) { //設置include路徑 dazukoUnregister( ) ; return NULL; } pt = ( struct cfgstruct * ) pt-> nextarg; } } else { dazukoUnregister( ) ; return NULL; } if ( ( pt = cfgopt( tharg-> copt, "ClamukoExcludePath" ) ) ) { //循環設置exclude路徑 while ( pt) { if ( ( dazukoAddExcludePath( pt-> strarg) ) ) { dazukoUnregister( ) ; return NULL; } pt = ( struct cfgstruct * ) pt-> nextarg; } } if ( ( pt = cfgopt( tharg-> copt, "ClamukoMaxFileSize" ) ) ) { sizelimit = pt-> numarg; } else sizelimit = CL_DEFAULT_CLAMUKOMAXFILESIZE; ...... while ( 1 ) { if ( dazukoGetAccess( & acc) == 0 ) { //得到內核攔截的文件訪問信息 clamuko_scanning = 1 ; scan = 1 ; if ( sizelimit) { stat( acc-> filename, & sb) ; //得到文件的大小統計信息存入sb if ( sb.st_size > sizelimit) { //檢查文件大小是否超限 scan = 0 ; } } if ( scan && cl_scanfile( acc-> filename, & virname, NULL, tharg-> root, tharg-> limits, tharg-> options) == CL_VIRUS) { //如果掃描文件發現病毒 virusaction( acc-> filename, virname, tharg-> copt) ; //病毒文件處理 acc-> deny = 1 ; //文件訪問拒絕 } else acc-> deny = 0 ; //文件訪問許可 if ( dazukoReturnAccess( & acc) ) { //將設置許可後的acc送回到內核模塊控制文件訪問 dazukoUnregister( ) ; clamuko_scanning = 0 ; return NULL; } clamuko_scanning = 0 ; } } return NULL; }
當需要線程clamukoth退出時,函數acceptloop_th通過函數pthread_kill(clamuko_pid, SIGUSR1)給線程發送信號SIGUSR1,由它的響應函數clamuko_exit處理線程的退出。函數clamuko_exit設置了文件訪問許可,並將設置的文件訪問許可返回給內核模塊,然後註銷Dazuko模塊、退出線程。
函數clamuko_exit列出如下:
void clamuko_exit( int sig) { if ( clamuko_scanning) { acc-> deny = 0 ; dazukoReturnAccess( & acc) ; /* is it needed ? */ } if ( dazukoUnregister( ) ) pthread_exit( NULL) ; //線程退出 }
3.15 服務器進程自動重啓動保護
clamd服務器進程是後臺進程,它常駐內存並且一直運行。但如果某些異常因素導致clamd進程死亡退出時,必須提供機制讓clamd自動重新啓動。
clamd服務器進程的自動重啓動保護是通過cron機制進行的,腳本clamav/contrib/init/RedHat/clamd提供了clamd應用程序的運行方法,Perl語言腳本clamdwatch提供了clamd後臺進程運行的檢查方法,cron機制每隔一段時間調用clamdwatch檢查clamd後臺進程是否正常運行,如果不是正常運行,則殺死clamd後臺進程並重新啓動。
腳本clamav/contrib/init/RedHat/clamd調用了/etc/init.d/functions腳本中定義的函數完成了管理clamd後臺進程的功能,它還被拷貝爲/etc/init.d/clamav-daemon,這樣,Linux操作系統啓動時就啓動了clamd後臺進程。
(1) cron定時機制
linux系統提供了命令crontab和at來設置定時運行命令或應用程序,crontab設置每隔一段時間週期執行應用程序,at設置定時執行應用程序。
Linux提供了crond後臺進程,它每分鐘檢查一次/etc/crontab, /etc/cron.d/ 和/var/spool/cron下文 件的變更,如果發現變化,它會下載這些文件的配置到內存。因此,配置文件改變了,crond不需要重新啓動也能使用改變了的配置文件。配置文件/etc/crontab是針對系統的任務,/var/spool/cron下文件是針對每個用戶的任務。
命令crontab用於設置crond後臺進程的配置文件,在/var/spool/cron下與用戶同名的配置文件就是由命令crontab -e 編輯生成,它們不可以直接編輯。
crond後臺進程的配置文件的格式類似如下:
# /etc/crontab - root's crontab for FreeBSD # # $Id: crontab,v 1.13 1996/01/06 22:21:37 ache Exp $ # From: Id: crontab,v 1.6 1993/05/31 02:03:57 cgd Exp # SHELL =/ bin/ sh PATH =/ etc:/ bin:/ sbin:/ usr/ bin:/ usr/ sbin HOME =/ var/ log # #minute hour mday month wday who command # */ 5 * * * * root / usr/ libexec/ atrun # # rotate log files every hour, if necessary 0 * * * * root / usr/ sbin/ newsyslog # # do daily/weekly/monthly maintenance 0 2 * * * root / etc/ daily 2 >& 1 | sendmail root 30 3 * * 6 root / etc/ weekly 2 >& 1 | sendmail root 30 5 1 * * root / etc/ monthly 2 >& 1 | sendmail root # # time zone change adjustment for wall cmos clock, # does nothing, if you have UTC cmos clock. # See adjkerntz(8) for details. 1 ,31 0 -4 * * * root / sbin/ adjkerntz -a
其中,第一列爲分鐘,規定每小時的第幾分執行相應的程序,第二列爲每天第幾小時執行程序,第三列爲每月的第幾天,第四列爲第幾周,第五列爲每週的第幾天,第六列爲執行該文件的用戶身份,第七列爲要執行的命令。特殊符號"/5"表示每隔5分鐘運行一次。
(2) clamd後臺進程的定期檢查
clamdwatch從/etc/crontab運行來檢查clamd後臺進程的狀態。/etc/crontab中應加上下面一行:
*/ 1 * * * * root / usr/ local/ bin/ clamdwatch.pl -q && ( / usr/ bin/ killall -9 clamd; rm -fr / var/ amavis/ clamd; / etc/ init.d/ clamav-daemon start 2 >& 1 )
這一行表示每隔一分鐘使用clamdwatch.pl檢查clamd後臺進程是否正常運行,如果運行異常返回0時,則殺死所有的clamd後臺進程,刪除/var/amavis/clamd,並重新啓動clamav-daemon,"2>&1"表示把錯誤輸出重定向到標準輸出,即將運行中的錯誤定向到終端顯示。
腳本clamdwatch.pl含有EICAR測試病毒簽名的拷貝,它將EICAR測試病毒內容寫入一臨時文件,通過套接字連接clamd,並請求clamd掃描這個臨時文件,如果15秒(缺省的超時限制)內,clamd返回應答表示找到病毒,則腳本返回1,如果clamd超時、沒有應答或沒有發現病毒,則返回0,如果socket沒有連接上也返回0。
(3) 運行腳本clamd
腳本clamav/contrib/init/RedHat/clamd提供了啓動、停止、重啓動、重裝載病毒庫、查詢clamd後臺進程運行狀態的功能,它使用了/etc/init.d/functions腳本中定義的函數完成這些功能,重裝載病毒庫是通過發送信號SIGHUP給clamd後臺進程,由clamd後臺進程完成具體的重裝載病毒庫操作。
4 libclamav庫API
libclamav庫API提供了病毒掃描的各種函數接口。libclamav庫使用的是病毒掃描法(Virus Scanner)。從病毒中提取的特徵字符串被用一定的格式組織在一起並加上簽名保護就形成了病毒庫,clamav使用的病毒庫一般後綴爲.cvd文件。
libclamav庫的病毒掃描法使用Aho-Corasick精確字符串匹配算法,將病毒庫中的特徵碼與文件中的字符串進行比較,以確定文件中是否有字符串精確匹配上病毒庫中的特徵碼,從而確定是否感染病毒。
Aho-Corasick在Boyer-Moore算法基礎上進行了的多種改進,Boyer Moore算法對要搜索的字符串進行逆序字符比較,當被搜索的字符串中的字符在特徵字符串中不存在時,將跳過搜索字符串中一個子段。Boyer Moore算法還利用上一次比較的結果來確定下一次的比較位置,Boyer Moore算法與線性搜索比起來每次移動的步長比較多,線性搜索每次移動一個字符,因此,Boyer Moore算法比線性搜索快得多。Aho-Corasick通過創建一種狀態圖並採用由軟件實現的有限狀態機來確定字符串在文本中的位置,消除了搜索性能與字符串數量的相關性。
4.1 病毒庫的裝載
調用函數cl_loaddb裝載病毒庫時,函數cl_loaddb將病毒庫文件clamav/database/main.cvd經數字簽名驗證和解壓縮後,存放在在/tmp目錄下生成臨時目錄中。病毒庫解碼成main.db、main.ndb、main.zmd和main.fp幾個庫,然後依次將這些庫解析出病毒數據存放在病毒數據鏈表中,解析完後,會刪除這些臨時病毒庫文件。
解碼的病毒庫經裝載後形成病毒數據鏈表,所有的數據存放在結構cl_node實例root中,當掃描病毒時,使用root中的數據與文件中數據進行匹配,以確定是否染病毒。
理解了臨時病毒庫的結構,就很容易理解函數cl_loaddb解析病毒庫的過程,因此,這裏不分析函數cl_loaddb了。臨時病毒庫的結構分別說明如下:
(1) main.db庫
main.db庫描述了一般文件病毒的特徵碼,使用Boyer Moore算法進行掃描。main.db庫以‘=’符號爲分隔符,前面爲病毒名,後面爲病毒特徵碼,對於"(Clam)"字符串必須從病毒名中剔除。main.db庫的內容經函數cli_loaddb解析後,存於結構cli_bm_patt的virname和pattern成員中。每一個病毒對應一個cli_bm_patt結構實例,這些結構實例組成鏈表存入在結構cl_node中。
main.db庫第1行解析後存入結構cli_bm_patt的結果如下:
struct cli_bm_patt { char * pattern= “21b8004233c999cd218bd6b90300b440c……541bb80157cd21b43ecd2132ed8a4c18”; char * virname = “_0017_0001_000”; // ‘=’符號左邊字符串 char * offset = 0 ; const char * viralias; unsigned int length; unsigned short target= 0 ; struct cli_bm_patt * next; } ;
main.db庫部分內容列出如下:
_0017_0001_000 =21b8004233c999cd218bd6b90300b440cd218b4c198b541bb80157cd21b43ecd2132ed8a4c18 _0017_0001_001 =b3005a8b4e27b440cd21e8c2045a59b440cd21e81902b440cd21b8004233c999cd218bd6b90300 _0017_0001_002 =8bfd83c72d515757e8b3005a8b4e27b440cd21e8c2045a59b440cd21e81902b440cd21b8004233 _0017_0001_003 =ee50f7d8250f008bc85803c150b440cd21582d0300c604e98944018bd6b985092bd1050301 _0017_0001_004 =40cd21e8c2045a59b440cd21e81902b440cd21b8004233c999cd218bd6b90300b440cd218b4c19 _0023_0004_003 =8becc7460200405d58b90400ba4a08cd21e90001803e6208407411803e5c086b740aa14a08 _0023_0004_004 =0550558becc7460200405d5833d2b98700cd2150558becc7460200405d58ba9a10b91310cd21bf _0023_0005_000 =558becc7460200405d5833d2b98700cd2150558becc7460200405d58ba9a10b91310cd21b8 _0023_0005_001 =50558becc7460200405d58ba9a10b91310cd21bf4a08b0e9aa58abb06baab80042505833c933d2 _0023_0005_002 =8becc7460200405d58ba9a10b91310cd21b80042505833c933d2cd2150558becc746020040 _0024_25199_001 ( Clam) =cd2133c9b8004299cd21b440ba890459cd21b801575a59cd21b43ecd21585a1f59cd215a1fb8 _0025_0006_000 =894515505657551e0653e84c005bb440cd213bc8071f5d5f5e587519c7044de92d0400894402 _0025_0006_001 =89440233c026894515b904008bd6b440cd21061f8f4515804d0640b43e9c0ee8d8fe804d0540 _0026_0006_000 =b440cd21e80d00b91800baa702b440cd21e952ff32c0eb02b002b44233c933d2cd21c3bf02 _0030_0001_006 =c98bd1b802422e8b1e390f9cfa2eff1ee80dc38becb80057e8ebffbb630f890f895702e8c802 _0034_0001_003 =45118be8b904002bc1a3160ab440ba140acd2126896d1506570e07be0001bf040db98b04f3a5b8 _0034_0001_004 =0a5253568bddfec7e8d4f7b4405a5bcd21b440b916095acd215f0726804d0640b43ecd21c3 ...... Gen.1000Years.791.A ( Clam) =060b01e9b440ba0001b91703cd217303e91800b8004233c933d2cd21b440ba0b01b90300cd2173 Gen.1000Years.791.B ( Clam) =0301e9430200000090cd2000000000000000000d0a4d656d6f727920616c6c6f6361746573696f6e206572726f Gen.100-Years ( Clam) =4d6f6e786c612d42205b5648505d202020202020202020202020436c65616e2d557020202020202e202e202e20782078202e202e202e202e202e20202020203550535152bbfe128b0f81f1ff00890f4b81fb160175f1 Gen.100-Years=fe3a558bec50817e0400c0730c2ea147 Gen.1024-PrScr.1=8cc0488ec026a103002d800026a30300 Gen.1024-PrScr.2=041f3df0f07505a10301cd0526a1 Gen.1024-PrScr.3=012ea30300b4400e1fba0004b90004e8e8007230 Gen.1024-PrScr.4=bf00b82125cd2133c08ec0b8f0f026 Gen.1024-SBC ( mem) =f8039148cd210ac474521e33ff8e
(2) main.fp
庫main.fp數據用於填充結構cli_md5_node,函數cli_loadhdb解析庫中的每一行,每行以":"分隔字段,每個字段填入結構cli_md5_node中各成員,每行填充一個結構實例,多個實例組成鏈表。下面以庫main.fp第一行爲例,數據填充如下:
struct cli_md5_node { char * virname= “Submission”, * viralias; //第2字段 第3字段可選 unsigned char * md5 = “d1ef8a0e477570ad39f4667129400b05”; //第0字段 unsigned int size= 1598056 ; //第1字段 unsigned short fp= 1 ; //表示.fp庫 struct cli_md5_node * next; } ;
庫main.fp數據部分數據如下:
d1ef8a0e477570ad39f4667129400b05:1598056 :Submission 21770 332e5c92be38ce0f195019258c8376dc:1640013 :Submission 22475 71d934fdf522c4227485716b0413c7be:55296 :Submission 23647 810439699f2dc802ff8c530f59f23b7c:876032 :Submission 24405 848b157359e022907645c222b3daf72d:1942528 :Submission 32849
(3) main.hdb
庫main.hdb的解析與庫main.fp類似,都填充結構cli_md5_node,解析函數都是函數cli_loadhdb,區別僅在於結構cli_md5_node填充0。下面以庫main.hdb第一行爲例,數據填充如下:
struct cli_md5_node { char * virname= “Trojan.Beastdoor .207.B - cli”, * viralias= “21770 ”; //第2字段 第3字段可選 unsigned char * md5 = “0060c80aedf81b1e4b58358afe1bf299: 761344 ”; //第0字段 unsigned int size= 761344 ; //第1字段 unsigned short fp= 0 ; //表示.hdb庫 struct cli_md5_node * next; } ;
庫main.fp數據部分數據如下:
0060c80aedf81b1e4b58358afe1bf299:761344 :Trojan.Beastdoor.207.B-cli 192bd7afb1479aad2b64a6c176773a01:761344 :Trojan.Beastdoor.207.C-cli aaab755d9baf21baf05de8f32af2c996:57856 :Trojan.BO.A-Cli d3edf9b7d99205afda64b3a7c1a63264:307200 :Trojan.Boid.20-cli-1 de59dc8df6021d19246f9b74dd1d68bc:32768 :Trojan.Boid.20-cli-2 d9c8d35d577b7bc2cdbe26282383400a:36864 :Trojan.Boid.20-edit 565ce39278f60226fbbe920f79e77eb2:17408 :Trojan.Ceptio.10-cli 01868bc1780b71996e90dafd180dae1b:13312 :Trojan.Ceptio.10-edit
(4) main.ndb
庫main.ndb說明了HTML類病毒的情況,HTML類病毒使用Aho-Corasick法掃描。庫main.ndb數據用於填充結構cli_ac_patt ,函數cli_loadndb解析庫中的每一行,每行以":"分隔字段,每個字段填入結構cli_ac_patt中各成員,每行填充一個結構實例,多個實例組成鏈表。下面以庫main.ndb第一行爲例,數據填充如下:
struct cli_ac_patt { short int * pattern = “3c6f626a65637420747970653d222f2f2f2f2f2f2f2f2f2f2f2f”; //第4字段 unsigned int length, mindist= 4 , maxdist= 6 ; //數據格式爲:{mindist-maxdist} char * virname = “Exploit.HTML .ObjectType ”, * offset=0 ; //第0字段、第2字段(‘*’表示NULL) const char * viralias; unsigned short int sigid, parts, partno, alt, * altn; unsigned short type, target= 3 ; //第1字段 char ** altc; struct cli_ac_patt * next; } ;
庫main.ndb部分數據列出如下:
Exploit.HTML.ObjectType:3 :* :3c6f626a65637420747970653d222f2f2f2f2f2f2f2f2f2f2f2f HTML.Phishing.Bank-1 :3 :* :3c6d6170206e616d653d22{ -36 } 223e3c6172656120636f6f7264733d22302c20302c20{ 4 -12 } 222073686170653d22726563742220687265663d22{ -160 } 3c2f6d61703e3c696d67207372633d226369643a Exploit.HTML.MHTRedir.1n:3 :* :6d732d6974733a6d68746d6c3a66696c653a2f2f633a5c* 21687474703a2f2f
(5) main.zmd
庫main.zmd說明了病毒壓縮的情況。庫main.zmd數據用於填充結構cli_ac_patt ,函數cli_loadmd解析庫中的每一行,若行開頭爲‘#’符號,表示此行爲註釋行不需要解析,第1行註釋行說明了各個字段名。每行以":"分隔字段,每個字段填入結構cli_meta_node 中各成員,字段爲‘*’符號表示爲NULL。每行填充一個結構實例,多個實例組成鏈表。下面以庫main.zmd第一行爲例,數據填充如下:
struct cli_meta_node { int csize= 69779 , size= 72767 , method= 0 ; unsigned int crc32= 5f6f7a3f, fileno= 1 , encrypted= 1 , maxdepth= 1 ; char * filename= 0 , * virname= “Worm.Padowor .A - zippwd”; //第2字段、第0字段 struct cli_meta_node * next; } ;
庫main.ndb部分數據列出如下:
# virname:encrypted:filename:normal size:csize:crc32:cmethod:fileno:max depth Worm.Padowor.A-zippwd:1 :* :72767 :69779 :5f6f7a3f:* :1 :1 Trojan.Dumador-31 -zippwd:1 :* :21008 :20598 :ba9f27fb:8 :1 :1 Worm.Kimazo.A-zippwd:1 :* :75776 :43733 :7b3fcf13:* :1 :1
4.2 病毒掃描
病毒掃描時,對於壓縮和加密過的文件需要經過解壓縮和解密後再進行掃描。由於文件壓縮及加密的方法很多,clamav包括或調用了常用的解壓縮和解密算法函數,這些解壓縮和解密算法與具體的協議相關,這裏不分析了。壓縮和加密過的文件解壓縮或解密後放在/tmp目錄下,再象普通文件一樣掃描,掃描完後,刪除臨時文件。
對於某些特殊文件,如:.jpg文件,clamavlib庫針對特殊病毒進行專門檢查,如:.jpg文件中的Exploit.W32.MS04-028病毒。這些病毒的檢查,跟病毒感染文件的機制相關。
對於普通文件的病毒掃描,clamavlib庫先後使用了Boyer-Moore和Aho-Corasick算法進行病毒匹配查找,這兩種算法分別對應病毒特徵庫main.db和main.ndb。這兩種算法詳細說明請參考相應的文獻。
5客戶端端應用程序
根據不同的應用需求,有不同的病毒掃描器客戶端應用程序,如:郵件服務器病毒掃描器、郵件客戶端病毒掃描器、HTTP反病毒代理、samba(文件共享)反病毒掃描器等。它們通過socket與clamd服務器通信來進行病毒掃描。
掃描客戶端應用程序可能有多個,但它們使用的服務器都是clamd。下面分析比較有代表意義的客戶端應用程序clamdav和qtclamavclient。clamdscan是C語言通用病毒掃描應用程序,對文件或目錄進行病毒掃描;qtclamavclient是用Qt編寫的帶簡單圖形界面的通用病毒掃描應用程序。
客戶端應用程序利用socket通信時,應用程序在創建socket和使用函數connect進行連接後,就可以向socket發送和接收數據了,而不需要服務器端的綁定(bind)、監聽(listen)和接受連接(accept)的過程。
向socket發送和接收數據是一個阻塞的過程,即向socket發送數據必須等待數據發送完,然後從socket中接收數據直到socket中有數據並接收了數據爲止。在不能阻塞的程序中必須使用線程機制解決阻塞,對於Qt,可以使用QSocket,因爲Qt的事件機制已使用線程解決了阻塞問題。
5.1 clamdscan客戶端
在clamdscan應用程序中,主函數main分析用戶在命令行設置的選項後調用函數clamscan。clamdscan應用程序的函數調用層次圖如圖3。
clamdscan應用程序與clamscan應用程序都是使用同樣的函數main,它們可支持同樣的命令行選項,但clamdscan應用程序忽略了許多命令行選項,而使用配置文件配置好的選項。
圖3 clamdscan應用程序的函數調用層次圖
函數clamscan打開或創建log文件,寫入開始時間。然後調用函數client將掃描的選項信息通過socket發給clamd服務器,並等待clamd服務器通過socket返回的信息,如果發現病毒,將病毒清除結果信息寫入log文件。在函數client掃描完文件或目錄後,函數clamscan再將掃描的總結信息寫入log文件。
函數client掃描由命令行參數傳入的目錄或者掃描來自標準輸入的數據流。對於目錄掃描,掃描命令及從服務器傳回的信息都通過服務請求的套接字傳輸。客戶端將有病毒的文件進行刪除或者移去到另一個文件夾保存。
對於來自標準輸入的數據流,服務請求套接字連接建立後,服務器創建臨時端口號,客戶端使用這個臨時端口號創建套接字並進行連接,在傳輸完數據後關閉這個臨時端口對應的套接字。服務器的掃描病毒信息由服務請求套接字返回。客戶端記錄感染的病毒數。
客戶端從socket中讀取數據時,一般在while循環語句中使用函數read讀取數據,直到數據讀完爲止。
函數client分析如下(在clamav/clamdscan/client.c中):
int client( const struct optstruct * opt, int * infected) { ...... //命令行參數選項中文件名不存在,則掃描當前目錄 if ( opt-> filename == NULL || strlen( opt-> filename) == 0 ) { /*將當前目錄的絕對路徑返回到cwd中,限制長度爲200,如果路徑超長,返回NULL且errno爲ERANGE */ if ( ! getcwd( cwd, 200 ) ) { ...... } if ( ( sockd = dconnect( opt) ) < 0 ) //創建socket並連接 return 2 ; if ( ( ret = dsfile( sockd, cwd, opt) ) >= 0 ) * infected += ret; else errors++; close( sockd) ; ...... } else if ( ! strcmp( opt-> filename, "-" ) ) { /*從標準輸入stdin掃描數據 */ if ( ( sockd = dconnect( opt) ) < 0 ) //創建連接 return 2 ; if ( ( ret = dsstream( sockd, opt) ) >= 0 ) //數據流傳輸 * infected += ret; else errors++; close( sockd) ; } else { //文件目錄掃描 /*循環進行單個文件目錄掃描*/ int x; char * thefilename; /*函數cli_strtok從opt->filename選項字符串中解析出多個文件路徑名,參數x爲域序號,"/t"爲域分界字符串*/ for ( x = 0 ; ( thefilename = cli_strtok( opt-> filename, x, "/t " ) ) != NULL; x++ ) { fullpath = thefilename; if ( stat( fullpath, & sb) == - 1 ) { //文件信息不存在,說明文件不存在 ...... } else { if ( strlen( fullpath) < 2 || ( fullpath[ 0 ] != '/' && fullpath[ 0 ] != '// ' && fullpath[ 1 ] != ':' ) ) { fullpath = abpath( thefilename) ; //得到絕對路徑,即當前目錄路徑+文件名 free( thefilename) ; ...... } switch ( sb.st_mode & S_IFMT) { // S_IFMT表示文件類型的位掩碼 case S_IFREG: //常規文件 case S_IFDIR: //目錄 if ( ( sockd = dconnect( opt) ) < 0 ) return 2 ; if ( ( ret = dsfile( sockd, fullpath, opt) ) >= 0 ) //單個目錄掃描 * infected += ret; else errors++; close( sockd) ; break ; default : mprintf( "@Not supported file type (%s)/n " , fullpath) ; errors++; } } free( fullpath) ; } } return * infected ? 1 : ( errors ? 2 : 0 ) ; }
函數dconnect根據命令行參數選項創建AF_UNIX或SOCKET_INET類型的套接字,並建立連接。其列出如下(在clamav/clamdscan/client.c中)::
int dconnect( const struct optstruct * opt) { ...... if ( ( copt = parsecfg( clamav_conf, 1 ) ) == NULL) { //將配置文件clamav_conf選項解析到鏈表copt中 ...... //省略了錯誤處理 } memset( ( char * ) & server, 0 , sizeof ( server) ) ; memset( ( char * ) & server2, 0 , sizeof ( server2) ) ; /*設置連接的缺省地址,缺省爲本地地址*/ server2.sin_addr .s_addr = inet_addr( "127.0.0.1" ) ; if ( cfgopt( copt, "TCPSocket" ) && cfgopt( copt, "LocalSocket" ) ) { //配置文件不能同時配置這兩個選項值 ......//打印配置文件出錯 } else if ( ( cpt = cfgopt( copt, "LocalSocket" ) ) ) { //從配置選項鍊表中讀出"LocalSocket"選項 /*使用AF_UNIX類型的socket,即使用臨時文件作爲socket server.sun_family = AF_UNIX; strncpy(server.sun_path, cpt->strarg, sizeof(server.sun_path�; //拷貝文件路徑名 if < 0) {//創建socket ...... } if(connect(sockd, (struct sockaddr *) &server, sizeof(struct sockaddr_un� < 0) {//連接 close(sockd); ...... } } else if) { //從配置選項鍊表中讀出"TCPSocket"選項 /*使用SOCKET_INET類型的socket,即使用TCP通信 if < 0) { //創建socket ...... } server2.sin_family = AF_INET; server2.sin_port = htons(cpt->numarg); if) { //從配置選項鍊表中讀出"TCPAddr"選項 if == 0) {//由ip地址查詢得到主機的地址及名字信息 close(sockd); ...... } //第一個IP地址,因爲主機可能有多個地址 server2.sin_addr = *(struct in_addr *) he->h_addr_list[0]; } if(connect(sockd, (struct sockaddr *) &server2, sizeof(struct sockaddr_in� < 0) {//連接 close(sockd); ...... } } ...... return sockd; }
函數dsstream向服務器clamd發出數據流掃描的服務請求"STREAM",接着讀取服務器分配的臨時端口號,在臨時端口號上創建臨時套接字,並建立連接。它將來自標準輸入的數據流通過臨時套接字發送給服務器,發送完後,關閉臨時套接字,並通過服務請求套接字從服務器接收病毒掃描信息。
函數dsstream列出如下:
int dsstream( int sockd, const struct optstruct * opt) { int wsockd, loopw = 60 , bread, port, infected = 0 ; struct sockaddr_in server; struct sockaddr_in peer; #ifdef HAVE_SOCKLEN_T socklen_t peer_size; #else int peer_size; #endif char buff[ 4096 ] , * pt; if ( write( sockd, "STREAM" , 6 ) <= 0 ) { //向socket寫入進行數據流掃描的命令 ...... } memset( buff, 0 , sizeof ( buff) ) ; while ( loopw) { //循環最多60次,直到讀到端口號 read( sockd, buff, sizeof ( buff) ) ; //從socket中讀取數據,buff爲PORT+端口號 if ( ( pt = strstr( buff, "PORT" ) ) ) { //返回buff中含有"PORT"的首地址 pt += 5 ; //跳過"PORT"字符,得到指向臨時端號的字符串指針地址 sscanf( pt, "%d" , & port) ; //讀取端口號,從字符串到整數轉換 break ; } loopw--; } ...... /* 在臨時端口上創建套接字,然後進行連接*/ if ( ( wsockd = socket( SOCKET_INET, SOCK_STREAM, 0 ) ) < 0 ) { //創建套接字 ...... } server.sin_family = AF_INET; server.sin_port = htons( port) ; //主機字節序轉換到網絡字節序 peer_size = sizeof ( peer) ; //得到sockd上另一連接方的地址信息,存入peer if ( getpeername( sockd, ( struct sockaddr * ) & peer, & peer_size) < 0 ) { ...... } switch ( peer.sin_family ) { case AF_UNIX: server.sin_addr .s_addr = inet_addr( "127.0.0.1" ) ; //轉換IP地址格式到unsigned long類型 break ; case AF_INET: server.sin_addr .s_addr = peer.sin_addr .s_addr ; break ; default : mprintf( "@Unexpected socket type: %d./n " , peer.sin_family ) ; return - 1 ; } if ( connect( wsockd, ( struct sockaddr * ) & server, sizeof ( struct sockaddr_in) ) < 0 ) { //連接 ...... } while ( ( bread = read( 0 , buff, sizeof ( buff) ) ) > 0 ) { //從標準輸入中讀取數據流到buff,0爲標準輸入描述符 if ( write( wsockd, buff, bread) <= 0 ) { //將數據寫入臨時端口號對應的socket中 ...... } } close( wsockd) ; //寫完數據後,關閉臨時端口號對應的socket memset( buff, 0 , sizeof ( buff) ) ; //buff置0,這樣可重新使用這塊緩存 while ( ( bread = read( sockd, buff, sizeof ( buff) ) ) > 0 ) { //從socket中讀取數據 mprintf( "%s" , buff) ; //打印調試信息 if ( strstr( buff, "FOUND/n " ) ) { //發現病毒 infected++; logg( "%s" , buff) ; } else if ( strstr( buff, "ERROR/n " ) ) { //掃描出錯 logg( "%s" , buff) ; return - 1 ; } memset( buff, 0 , sizeof ( buff) ) ; } return infected; }
函數dsfile將服務請求和掃描目錄一起通過服務請求套接字發送給服務器,然後接收病毒掃描信息,並刪除或移走受病毒感染的文件。其列出如下:
int dsfile( int sockd, const char * filename, const struct optstruct * opt) { ...... scancmd = mcalloc( strlen( filename) + 20 , sizeof ( char ) ) ; sprintf( scancmd, "CONTSCAN %s" , filename) ; //命令字符+文件路徑 if ( write( sockd, scancmd, strlen( scancmd) ) <= 0 ) { //寫入socket ...... } free( scancmd) ; ret = dsresult( sockd, opt) ; //從socket中讀取掃描結果,並對病毒文件進行刪除操作 ...... return ret; }
函數dsresult處理病毒掃描結果。它重定向服務請求套接字描述符sockd到FILE類型的數據流fd,然後用while循環語句從fd中循環讀取每行字符串,解析字符串信息,移走或刪除病毒文件。
函數dsresult列出如下:
int dsresult( int sockd, const struct optstruct * opt) { int infected = 0 , waserror = 0 ; char buff[ 4096 ] , * pt; FILE * fd; //dup用來複制描述符,即將sockd定向到fd。fdopen用來將文件描述符轉換到FILE類型數據流 if ( ( fd = fdopen( dup( sockd) , "r" ) ) == NULL) { ...... } //fgets從fd中讀取最大4095個字符,如果遇到換行符則停止,並在讀取的字符串後加上'/0' while ( fgets( buff, sizeof ( buff) , fd) ) { //字符串buff格式類似爲"/home/root/test.txt:FOUND/n" if ( strstr( buff, "FOUND/n " ) ) { //strstr返回從buff中第一次找到"FOUND/n"字符串的位置 infected++; //感染病毒的文件數計數 logg( "%s" , buff) ; mprintf( "%s" , buff) ; if ( optl( opt, "move" ) ) { //配置選項中配置爲移走病毒文件 /* filename: Virus FOUND */ if ( ( pt = strrchr( buff, ':' ) ) ) { //strrchr返回':'在字符串buff中最後一次發生的地址 * pt = 0 ; //將':'替換爲0,buff爲"/home/root/test.txt/0FOUND/n",成爲僅有路徑字符串 move_infected( buff, opt) ; } ...... } else if ( optl( opt, "remove" ) ) { //配置選項中配置爲刪除病毒 if ( ! ( pt = strrchr( buff, ':' ) ) ) { mprintf( "@Broken data format. File not removed./n " ) ; } else { * pt = 0 ; //將':'替換爲0 if ( unlink( buff) ) { //刪除文件或文件鏈接 notremoved++; } ...... } } } if ( strstr( buff, "ERROR/n " ) ) { waserror = 1 ; } } fclose( fd) ; //關閉複製的文件描述符fd,但沒關閉sockd return infected ? infected : ( waserror ? - 1 : 0 ) ; }
函數move_infected移走病毒感染文件,它從命令行參數選項中解析出病毒文件存儲目錄路徑名,並判斷這個目錄的讀寫權限,然後查看目錄中是否有需移走的病毒文件名,如果有,就將目錄中的文件名加上計數後綴,以免被覆蓋。再接着使用函數rename將病毒文件重命名到病毒文件存儲目錄。如果重命名失敗,就使用拷貝刪除的辦法將病毒文件移到病毒文件存儲目錄,並保持原病毒文件的文件信息。
函數move_infected列出如下:
void move_infected( const char * filename, const struct optstruct * opt) { char * movedir, * movefilename, * tmp, numext[ 4 + 1 ] ; struct stat fstat, mfstat; int n, len, movefilename_size; struct utimbuf ubuf; if ( ! ( movedir = getargl( opt, "move" ) ) ) { //從輸入命令行參數選項中讀取移除文件存放目錄 ...... } if ( access( movedir, W_OK| X_OK) == - 1 ) { //檢查寫和執行訪問權限 ...... } if ( stat( filename, & fstat) == - 1 ) { //得到文件filename的信息,存放在fstat中 ...... } if ( ! ( tmp = strrchr( filename, '/' ) ) ) //得到最後一個'/'字符之後的地址 tmp = ( char * ) filename; //指向最後一個'/'字符地址處,即截取文件名 //分配空間,大小爲:目錄路徑+移除文件名+重複文件名計數 movefilename_size = sizeof ( char ) * ( strlen( movedir) + strlen( tmp) + sizeof ( numext) + 2 ) ; if ( ! ( movefilename = mmalloc( movefilename_size) ) ) { exit( 2 ) ; } if ( ! ( strrcpy( movefilename, movedir) ) ) { //拷貝目錄路徑movedir,從後向前拷貝,即先使用最後1位 ...... } strcat( movefilename, "/" ) ; //兩字符串相連接並在末尾加上'/0' //連接上文件名,連接成功時,返回與movefilename一致的連接字符串指針 if ( ! ( strcat( movefilename, tmp) ) ) { ...... } if ( ! stat( movefilename, & mfstat) ) { //得到文件信息,說明文件存在 if ( fstat.st_ino == mfstat.st_ino ) { //文件系統節點號相同,說明是同一個文件 ...... } else { /* 文件存在,給文件名加上計數後綴,以例覆蓋了已存在的文件*/ len = strlen( movefilename) ; //得到路徑字符串的長度 n = 0 ; do { //如果文件名存在,就循環給文件名加入計數後綴 movefilename[ len] = 0 ; //末尾加0,表示爲字符串結束 sprintf( numext, ".%03d" , n++ ) ; //寫入計數到字符串,用來作爲文件名後綴 strcat( movefilename, numext) ; //連接計數字符串 //如果文件路徑的信息存在,說明文件存在,繼續向後計數 } while ( ! stat( movefilename, & mfstat) && ( n < 1000 ) ) ; } } /*重命名操作將原文件改名而文件信息不變*/ if ( rename( filename, movefilename) == - 1 ) { //將病毒文件重命名,相當於移除文件 /*如果重命名失敗,就使用拷貝、刪除操作,如:不同類型文件系統之間重命名操作常會失敗。此時,需要將原文件信息拷貝過去*/ if ( filecopy( filename, movefilename) == - 1 ) { ...... } chmod( movefilename, fstat.st_mode ) ; //改變到原文件的訪問權限 chown( movefilename, fstat.st_uid , fstat.st_gid ) ; //改變到原文件的用戶和用戶組 ubuf.actime = fstat.st_atime ; ubuf.modtime = fstat.st_mtime ; utime( movefilename, & ubuf) ; //改變到原文件的訪問和修改時間 if ( unlink( filename) ) { //刪除文件 ...... } } free( movefilename) ; //翻譯分配的內存 }
5.2 qtclamavclient客戶端應用程序
qtclamavclient客戶端應用程序使用了類QSocket與服務器通信,QSocket類提供了一個有緩衝的TCP連接,它將socket套接接口函數封裝在QSocket類。
qtclamavclient客戶端通信過程與圖6中流程類似,只是socket的連接過程都封裝在QSocket類中。使用QSocket類通過通信的好處是QSocket類遵循了Qt的事件機制,因此,在從套接字收發數據時,Qt的事件循環不會被阻塞,即圖形界面依舊能響應鼠標鍵盤事件。缺點是可能接連發送多個請求,這需要服務器進行特殊處理,或者客戶端限制爲一個請求迴應後再發一個請求,qtclamavclient客戶端就是這樣實現的。
QSocket類的成員函數connectToHost()打開一個被命名的主機的連接。絕大多數網絡協議是基於包或行的。canReadLine()可以識別socket是否包含一個來自服務器可讀的的行,並且由bytesAvailable()返回可被讀取的字節數。
信號error()、connected()、readyRead()和connectionClosed()通知你連接的進展。當connectToHost()已經完成它的DNS查找並且正在開始它的TCP連接時,hostFound()被髮射。當close()成功時,delayedCloseFinished()被髮射。
state()返回socket的狀態,如:空閒、DNS查找、正在連接或已經連接狀態。address()和port()返回連接所使用的IP地址和端口。peerAddress()和peerPort()函數返回自身所用到的IP地址和端口並且peerName()返回自身所用的名稱(通常是被傳送給connectToHost()的名字)。
QSocket類還提供了對socketf進行讀寫操作的函數,如:open()、close()、flush()、size()、at()、atEnd()、readBlock()、writeBlock()、getch()、putch()、ungetch()和readLine()等。
qtclamavclient應用程序是一個簡單的病毒掃描客戶端,將讀取文件數據發送到clamd服務器去掃描,客戶端上顯示掃描結果及掃描進度條。它通過Client類實現了病毒客戶端的UI界面及與clamd的連接。
在Client類實例創建時,Client類構造函數創建Socket類實例stream_socket,並連接它的各種信號,接着Client類構造函數調用函數connectToHost將socket連接到服務器上。
當服務器連接成功時,Socket類connected()信號觸發SLOT函數Stream_socketConnected,Stream_socketConnected調用sendFileToServer函數將數據寫到socket上。
當服務器從socket返回數據時,會發射readyRead()信號,觸發SLOT函數socketReadyRead()從socket中讀取每行數據進行分析。
qtclamavclient客戶端應用程序先通過公開的端口號與服務器連接,服務器再組客戶端分配臨時端口號,客戶端通過臨時端口號向服務器傳送掃描的文件數據。
qtclamavclient應用程序中與socket連接相關的代碼列出如下:
類Client構造函數構造圖形界面的各個實例,創建服務請求套接字及臨時數據流套接字,用函數connect連接信號與槽,連接到服務器clamd上。類Client構造函數列出如下:
Client( const QString & host, const QString & port) { ...... // 創建服務請求套接字,用於發送客戶端的服務請求信號 socket = new QSocket( this ) ; connect( socket, SIGNAL( connected( ) ) , SLOT( socketConnected( ) ) ) ; connect( socket, SIGNAL( connectionClosed( ) ) , SLOT( socketConnectionClosed( ) ) ) ; connect( socket, SIGNAL( readyRead( ) ) , SLOT( socketReadyRead( ) ) ) ; connect( socket, SIGNAL( error( int ) ) , SLOT( socketError( int ) ) ) ; //臨時數據流套接字,用於發送掃描的文件內容數據流,數據發送完後,關閉這個套接字 stream_socket = new QSocket( this ) ; //在connectToHost()已被調用且連接成功後,發射connected()信號。 connect( stream_socket , SIGNAL( connected( ) ) , SLOT( Stream_socketConnected( ) ) ) ; //當另一端已關閉連接時,發射這個信號 connect( stream_socket , SIGNAL( connectionClosed( ) ) , SLOT( Stream_socketConnectionClosed( ) ) ) ; //當有數據來到且可讀時,發射這個信號。新數據來到時,這個信號只發射一次。如果沒有讀取全部數據,這個信號不會再次發射,除非新的數據到達這個套接字 connect( stream_socket , SIGNAL( readyRead( ) ) , SLOT( socketReadyRead( ) ) ) ; //在錯誤發生之後,發射這個信號,參數爲錯誤值 connect( stream_socket , SIGNAL( error( int ) ) , SLOT( socketError( int ) ) ) ; ...... //試圖連接主機指定端口並且立即返回, 任何連接或者正在進行的連接被立即關閉,並且QSocket進入HostLookup 狀態。當查找成功,它發射hostFound(),開始一個TCP連接並且進入Connecting狀態。最後,當連接成功時,它發射connected()並且進入Connected狀態。如果在任何一個地方出現錯誤,它發射error()。 socket-> connectToHost( ClamAV_host, clamav_main_port ) ; ...... }
函數ClamAVScan是掃描的入口函數,在圖形界面上按"掃描"按鈕時調用這個函數,其列出如下:
void ClamAVScan ( ) { ...... socket -> connectToHost( ClamAV_host, clamav_main_port ) ; //連接到服務器 sendStringToServer( "STREAM" ) ; //發送服務請求,"STREAM"表示數據流掃描 ...... }
函數sendStringToServer 發送服務請求字符串給服務器,其列出如下:
void sendStringToServer( const QString & stringtosend) { //寫服務請求給服務器 QTextStream os( socket) ; os << stringtosend << "/n " ; }
函數sendFileToServer在臨時套接字上發送單個文件內容數據給服務器進行病毒掃描,其列出如下:
void sendFileToServer( const QString & file_to_scan) { ...... QFile file( file_to_scan) ; if ( file.open ( IO_ReadOnly) ) //打開需要掃描的文件 { ...... //從文件中讀出數據到buffer,再將buffer中數據寫入socket,直到文件內容讀完 while ( ( ( nbytes = file.readBlock ( buffer, sizeof ( buffer) ) ) > 0 ) && ( file_size <= max_stream_size) ) { stream_percent += nbytes; if ( stream_socket-> isOpen( ) ) //如果socket是打開狀態 { //從buffer中向套接字中寫入sizeof(buffer)字節,返回所寫的字節數。如果發生錯誤,返回-1 if ( stream_socket-> writeBlock( buffer, sizeof ( buffer) ) == - 1 ) ...... } ...... } } //數據發送完畢,關閉臨時套接字 if ( stream_socket-> state( ) == QSocket:: Connected ) stream_socket-> close( ) ; //關閉socket,表示一個文件內容已傳送完,以便服務器開始掃描 ...... }
當套接字中有數據準備好時,Qt事件機制觸發事件處理函數socketReadyRead。函數socketReadyRead從套接字中循環所有數據,每次讀取一行字符串並進行解析。這裏從套接字讀出的字符串是服務器clamd執行病毒掃描結果的信息,函數socketReadyRead根據解析的信息決定是否掃描下一個文件。
函數socketReadyRead列出如下:
void socketReadyRead( ) { // 從服務器讀取數據 while ( socket-> canReadLine( ) ) { //如果可以從套接字中讀取一行,返回真,否則返回假 //返回包含換行符(/n)的一行文本。如果canReadLine()返回假,則它返回“” Socket_Read_Line = socket-> readLine( ) ; if ( strstr( Socket_Read_Line, "PORT" ) != NULL) //如果含有字符串“PORT” { //按格式從Socket_Read_Line中讀取端口號 if ( sscanf( Socket_Read_Line, "PORT %hu/n " , & clamav_stream_port) != 1 ) { //不能讀取端口號 ...... } else { //讀取端口號 // 連接到服務器 ...... stream_socket -> connectToHost( ClamAV_host, clamav_stream_port ) ; } } else if ( strstr( Socket_Read_Line, "FOUND" ) != NULL) { //Clamd --> A VIRUS FOUND ...... scan_next_file ( ) ; //掃描下一個文件 } else if ( strstr( Socket_Read_Line, "OK" ) != NULL) { //Clamd --> NO VIRUS FOUND ...... scan_next_file ( ) ; //掃描下一個文件 } ...... } }
函數Stream_socketConnected()是臨時數據流套接字的連接事件處理函數,當臨時套接字已連接時,如果文件沒掃描完,則繼續發送文件內容給服務器。其列出如下:
函數socketConnectionClosed是服務請求套接字關閉的事件處理函數,如果服務請求套接字關閉時還需要掃描文件,則重新建立連接,發送服務請求給服務器。其列出如下:
void socketConnectionClosed( ) { ...... if ( scanninginprogress == 1 ) //還需要病毒掃描 { socket-> connectToHost( ClamAV_host, clamav_main_port ) ; //建立連接 sendStringToServer( "STREAM" ) ; //發送服務請求 } }
函數scan_next_file掃描下一個文件,如果文件沒掃描完,則繼續發送文件內容給服務器,如果所有文件都掃描完,則顯示掃描統計信息,停止掃描。其列出如下:
void scan_next_file( ) { ...... if ( scanninginprogress == 1 ) //還需要掃描文件 { ++ it; if ( it == files_to_scan.end ( ) ) //掃描最後一個文件 { ......//省略顯示病毒掃描信息 totalwarnings = 0 ; //警告信息計數 totalfilesscanned = 0 ; //已掃描的文件計數 totalvirusfound = 0 ; //發現病毒的文件計數 scanninginprogress = 0 ; //表示不需要再掃描 } else { FileToScan = * it; // FileToScan爲當前需要掃描文件的絕對路徑字符串 if ( ! FileToScan.isEmpty ( ) ) { sendFileToServer( FileToScan) ; //發送文件內容給服務器 } } } }
6 病毒庫升級程序freshclam
病毒庫升級程序freshclam可完成病毒庫的定時升級工作,它通過網絡地址找到合適的服務器,連接到服務器檢查病毒庫版本,並從網站服務器上下載最新的病毒庫,下載完後再完成病毒庫的更新工作。
病毒庫升級程序freshclam的函數調用層次圖如圖15。
圖15 病毒庫升級程序freshclam的函數調用層次圖
6.1 病毒庫定時更新
函數freshclam在應用程序啓動時更新病毒庫,還可以定時進行更新,應用程序可以作爲後臺進程運行,此時,常將應用程序設置爲定時更新病毒庫。
函數freshclam列出如下(在clamav/freshclam/freshclam.v中):
int freshclam( struct optstruct * opt) { ...... /*geteuid得到當前進程的有效用戶ID,真實用戶ID與調用進程時的用戶ID對應,有效用戶ID與文件執行時被設置的ID位對應*/ if ( ! geteuid( ) ) { if ( ( user = getpwnam( unpuser) ) == NULL) { //得到用戶信息存入user中,unpuser爲用戶名 mprintf( "@Can't get information about user %s./n " , unpuser) ; exit( 60 ) ; /* 60爲用戶定義的錯誤號,便於查詢程序從哪裏退出*/ } ......//省略用戶和用戶組的設置 ...... memset ( & sigact, 0 , sizeof ( struct sigaction) ) ; sigact.sa_handler = daemon_sighandler; //設置信號處理函數句柄 ...... bigsleep = 24 * 3600 / checks; //睡眠時間爲:24小時*3600秒/每天更新檢查次序 if ( ! cfgopt( copt, "Foreground" ) ) daemonize( ) ; //後臺執行 ...... /*設置信號處理函數*/ sigaction( SIGTERM, & sigact, NULL) ; sigaction( SIGHUP, & sigact, NULL) ; sigaction( SIGINT, & sigact, NULL) ; sigaction( SIGCHLD, & sigact, NULL) ; /*睡眠等待直到接收到指定信號時纔開始更新下載操作*/ while ( ! terminate) { //在接收到信號時設置terminate爲0,如:鬧鐘定時運行 ret = download( copt, opt) ; //更新下載病毒庫操作 if ( ret > 1 ) { const char * arg = NULL; if ( optl( opt, "on-error-execute" ) ) //從命令行參數得到錯誤處理命令 arg = getargl( opt, "on-error-execute" ) ; else if ( ( cpt = cfgopt( copt, "OnErrorExecute" ) ) ) //從配置文件得到錯誤處理命令 arg = cpt-> strarg; if ( arg) execute( "OnErrorExecute" , arg) ; //利用shell命令執行錯誤處理命令或應用程序 } sigaction( SIGALRM, & sigact, & oldact) ; //設置信號處理函數,並將舊信號處理函數存入oldact sigaction( SIGUSR1, & sigact, & oldact) ; time( & wakeup) ; //得到以秒計數的當前時間,Epoch方式計時(即從1970/1/1/00:00:00開始) wakeup += bigsleep; //加上睡眠時間 alarm( bigsleep) ; //鬧鐘定時 do { pause( ) ; //進程睡眠直到收到信號 time( & now) ; //得到以秒計數的當前時間 } while ( ! terminate && now < wakeup) ; if ( terminate == - 1 ) { //接收到SIGALRM或SIGUSR1信號 logg( "Received signal: wake up/n " ) ; terminate = 0 ; } else if ( terminate == - 2 ) { //接收到SIGHUP信號,重新打開log文件 logg( "Received signal: re-opening log file/n " ) ; terminate = 0 ; logg_close( ) ; } sigaction( SIGALRM, & oldact, NULL) ; //恢復舊信號處理函數 sigaction( SIGUSR1, & oldact, NULL) ; } } else ret = download( copt, opt) ; //更新下載病毒庫操作 ...... return ( ret) ; }
函數daemon_sighandler是信號的處理函數,它對幾個指定信號分別進行了處理,其列出如下:
static short terminate = 0 ; extern int active_children; static void daemon_sighandler( int sig) { switch ( sig) { case SIGCHLD: //子進程結束或中斷時通知其父進程的信號 //等待任意一子進程,WNOHANG表示如果沒有子進程退出立即返回 waitpid( - 1 , NULL, WNOHANG) ; active_children--; //激活的子進程計數減1 break ; case SIGALRM: //鬧鐘函數alarm發出的信號 case SIGUSR1: //用戶定義 terminate = - 1 ; //表示需要進行更新病毒庫操作 break ; case SIGHUP: //通常用來通知守護進程重新讀取系統配置文件 terminate = - 2 ; //表示需要進行更新病毒庫操作,還需要關閉log文件 break ; default : terminate = 1 ; //不需要更新病毒庫 break ; } return ; }
函數execute利用父子進程機制執行shell命令行,它先創建子進程,接着在子進程中執行shell命令行,父進程中對子進程計數進行累加。其列出如下:
void execute( const char * type, const char * text ) { pid_t pid; if ( active_children< CL_MAX_CHILDREN ) switch ( pid= fork( ) ) { //創建子進程 case 0 : //子進程 if ( - 1 == system( text) ) //執行shell命令 { mprintf( "@%s: couldn't execute /" %s/" ./n " , type, text) ; } exit( 0 ) ; //運行完後退出子進程 case - 1 : //子進程創建失敗 mprintf( "@%s::fork() failed, %s./n " , type, strerror( errno) ) ; break ; default : //父進程,pid大於0時爲子進程號 //子進程的等待在SIGCHLD信號處理函數中進行 active_children++; //激活的子進程計數加1 } else //子進程數超出最大允許數,打印調試信息 { mprintf( "@%s: already %d processes active./n " , type, active_children) ; } }
6.2 域名信息查詢
(1) DNS消息格式及域名查詢函數
域名查詢一般用來通過主機名查詢主機的IP地址等,如:查詢www.isi.edu的IP地址。當客戶機發出域名查詢請求時,本地域名服務器接受查詢請求,本地域名服務器先在本地查詢,如果查詢不到,則它將在域名樹中的各分支上下遞歸搜索來尋找答案。
DNS(Domain Name System)消息由消息頭、消息段組成,消息頭說明有哪幾段信息、查詢的類型(標準、反向、服務器狀態等等)、回答是否授權、是查詢還是回答消息等。
消息段有question、answrer、authority和additional段,question段用於客戶機向服務器提出請求,answrer段爲服務器的應答信息,authority段爲授權信息,additional段爲附加信息。其中answrer、authority和additional段消息格式一樣。
question段包含被查詢的域名(name)、查詢的類型(type)以及查詢的類別(class)三個信息。如:名字爲www.isi.edu,類別爲C_IN(Internet類別),類型T_TXT(文本類型)。
answrer段各自包括名字(name)、類型(type)、類別(class)、TTL(傳輸時間長度)、長度(源數據長度)以及源數據(RData)幾部分。answer段的名字、類型、類別部分應該和question段相同。TTL部分表示收到的記錄數據的有效時間,而Rdata是服務器回答的數據。
客戶機域名信息查詢一般使用DNS 解析庫函數,這些函數包括res_init、res_query和dn_expand等,這三個函數的聲明列出如下:
#include <netinet/in.h> #include <arpa/nameser.h> #include <resolv.h> extern struct state _res; int res_init( void ) ; int res_query( const char * dname, int class, int type, unsigned char * answer, int anslen) ; int dn_expand( unsigned char * msg, unsigned char * eomorig, unsigned char * comp_dn, unsigned char * exp_dn, int length) ;
函數res_init()讀取配置文件/etc/resolv.conf得到缺省的域名、搜索次序和域名服務器地址。如果沒給出服務器名,則嘗試使用本地主機作爲服務器名。它還可使用環境變量LOCALDOMAIN替換域名服務器名。函數res_init()在調用其他函數之前被調用。例如,配置文件/etc/resolv.conf如下:
domain pc.tc.com nameserver 129.188.1.1 nameserver 129.188.2.2
函數res_query()查詢域名服務器,並返回域名服務器迴應的信息。
函數dn_expand()將壓縮的域名comp_dn擴展到全部的域名,並放在exp_dn中。壓縮的域名含有域名服務器的查詢或應答信息,msg指向這個消息的開始。
(2) 下載管理函數downloadmanager
下載管理函數downloadmanage通過域名服務器查詢域名是否存在,並從域名服務器應答信息中判斷freshclam軟件是否過期。然後下載病毒庫,並通過socket通知clamd更新病毒庫。
函數downloadmanage列出如下(在clamav/freshclam/manager.c中):
int downloadmanager( const struct cfgstruct * copt, const struct optstruct * opt, const char * hostname) { ...... #ifdef HAVE_RESOLV_H ...... dnsdbinfo = "current.cvd.clamav.net" ; //得到域名 if ( optl( opt, "no-dns" ) ) { dnsreply = NULL; } else { if ( ( dnsreply = txtquery( dnsdbinfo, & ttl) ) ) { //通過DNS服務器查詢域名,查詢所用時間存在ttl中 if ( ( pt = cli_strtok( dnsreply, 3 , ":" ) ) ) { //返回第3個條目(即記錄時間)的值,":"爲條目的分界符 int rt; time_t ct; rt = atoi( pt) ; //將字符串轉換爲整數 free( pt) ; time( & ct) ; //當前時間 if ( ( int ) ct - rt > 10800 ) { //3*3600=10800,即DNS記錄的年齡爲3個小時 ......//打印警告信息 } } else { //解析第3個條目出錯 free( dnsreply) ; dnsreply = NULL; } if ( dnsreply) { ...... if ( ( newver = cli_strtok( dnsreply, 0 , ":" ) ) ) { //第0個條目爲軟件版本 if ( vwarning && ! strstr( cl_retver( ) , "devel" ) && ! strstr( cl_retver( ) , "rc" ) ) { if ( strcmp( cl_retver( ) , newver) ) { // 比較版本號,freshclam軟件版本過期 outdated = 1 ; //表示軟件過期 } } } } ...... } } #endif /* HAVE_RESOLV_H */ ...... memset ( ipaddr, 0 , sizeof ( ipaddr) ) ; if ( ( ret = downloaddb( DB1NAME, "main.cvd" , hostname, ipaddr, & signo, copt, dnsreply, localip, outdated) ) > 50 ) { ...... } else if ( ret == 0 ) updated = 1 ; //病毒庫更新 /* if ipaddr[0] != 0 it will use it to connect to the web host */ if ( ( ret = downloaddb( DB2NAME, "daily.cvd" , hostname, ipaddr, & signo, copt, dnsreply, localip, outdated) ) > 50 ) { ...... } else if ( ret == 0 ) updated = 1 ; ...... if ( updated) { ...... #ifdef BUILD_CLAMD if ( optl( opt, "daemon-notify" ) ) { //命令行參數選項中有"daemon-notify" ...... notify ( clamav_conf) ; //通過socket通知服務器重新裝載病毒庫 } #endif ...... } }
(3) 域名查詢函數txtquery
應用程序freshclam調用函數txtquery在域名服務器查詢域名信息並且返回域名信息給調用函數。函數txtquery被調用時,參數domain賦值爲"current.cvd.clamav.net"。
函數txtquery先使用函數res_init讀取配置文件得到域名服務器的配置信息,然後調用函數res_query查詢域名服務器並返回查詢信息,再由函數dn_expand解析查詢信息。然後再把解析出來的信息進行處理後返回。
函數txtquery列出如下(在clamav/freshclam/dns.c中):
char * txtquery( const char * domain, unsigned int * ttl) { unsigned char answer[ PACKETSZ] , * pt; char host[ 128 ] , * txt; int len, exp, cttl, size, txtlen, type; if ( res_init( ) < 0 ) { //讀取配置文件,得到缺省的域名,搜索次序和地址。 return NULL; } memset( answer, 0 , PACKETSZ) ; // PACKETSZ定義爲512 //查詢C_IN 類和T_TXT 類型的doname是否是有效的域名,域名服務器的應答消息放在answer中 // domain爲"current.cvd.clamav.net" if ( ( len = res_query( domain, C_IN, T_TXT, answer, PACKETSZ) ) < 0 ) { //返回消息的長度放在len中 return NULL; } pt = answer + sizeof ( HEADER) ; //得到壓縮的域名 //壓縮的域名pt擴展到全稱的域名host,壓縮的域名存在answer中 if ( ( exp = dn_expand( answer, answer + len, pt, host, sizeof ( host) ) ) < 0 ) { //exp爲返回的壓縮的域名長度 return NULL; } pt += exp; GETSHORT( type, pt) ; //得到類型 if ( type != T_TXT) { return NULL; } pt += INT16SZ; /* class */ //pt指向壓縮的域名,host爲擴展的域名 if ( ( exp = dn_expand( answer, answer + len, pt, host, sizeof ( host) ) ) < 0 ) { return NULL; } pt += exp; GETSHORT( type, pt) ; if ( type != T_TXT) { return NULL; } pt += INT16SZ; /* class */ GETLONG( cttl, pt) ; * ttl = cttl; //迴應時間 GETSHORT( size, pt) ; txtlen = * pt; if ( txtlen >= size || ! txtlen) { return NULL; } if ( ! ( txt = mmalloc( txtlen + 1 ) ) ) //數據長度 return NULL; pt++; strncpy( txt, ( char * ) pt, txtlen) ; //拷貝迴應的數據 txt[ txtlen] = 0 ; return txt; }
6.3 HTTP協議下載病毒庫文件
WWW的組成技術包括HTTP、HTML、URL以及CGI等。CGI(CommonGatewayInterface,通用網關接口)是應用程序與WWW服務器交互的一個標準接口,允許用戶編寫擴展應用程序來擴展服務器的功能。
URL的格式爲:
HTTP://<IP地址>/[端口號]/[路徑][ '<查詢信息>]
WWW服務器的主要協議是HTTP協議,即超文體傳輸協議。HTTP是基於客戶機/服務器模式的WWW瀏覽的網絡協議。客戶機瀏覽器向服務器發送請求,服務器迴應相應的網頁。在Internet上,HTTP通信通常建立在TCP/IP連接上,缺省端口是TCP 80。基於HTTP協議信息交換過程一般由建立連接、發送請求信息、發送響應信息、關閉連接幾步組成。
在HTTP1.1定義了三種最基本的請求方法有GET、HEAD、POST,而PUT、DELETE、LINK、UNLINK方法許多HTTP服務器都不使用。三種最基本的請求方法說明如下:
GET: 請求指定的頁面信息,並返回實體主體。
HEAD: 只請求頁面的首部,用於客戶程序和服務器之間交流一些內部數據。
POST: 請求服務器接受所指定的文檔作爲對所標識的URL的新的從屬實體。POST在後面持續發送數據,讓服務器處理。POST方法通常需要服務器啓動CGI程序處理POST發送來的數據。
客戶機/服務器的請求/應答消息格式分別說明如下:
(1) 客戶機程序請求消息格式
下面以一個樣例說明請求消息的格式:
一個典型的請求消息列出如下:GET http:// class/ download.microtool.de:80 / somedata.exe HTTP/ 1.1 Host:download.microtool.de Accept:*/* Pragma:no-cache Cache-Control:no-cache Referer:http:// class/ download.microtool.de/ User-Agent:Mozilla/ 4.04 [ en] ( Win95;I;Nav) Range:bytes =554554 -
第一行表示HTTP客戶端通過GET方法獲得指定URL下的文件。HTTP/1.1表示協議版本號。
Host頭域指定發出請求的Intenet主機和端口號。
Referer頭域允許客戶端指定發出請求URL地址,給服務器生成回退鏈表等使用。
Range頭域表示請求實體的字節大小範圍。
User-Agent頭域內容包含發出請求的用戶信息。
(2) 服務器響應消息格式
Web服務器首先傳送HTTP頭信息,然後傳送具體內容,HTTP頭信息和HTTP具體信息之間用一個空行分開。
一個典型的響應消息列出如下:HTTP/ 1.1200OK Date:Mon,31Dec200104:25 :57GMT Server:Apache/ 1.3.14( Unix) Content-type:text/ html Last-modified:Tue,17Apr200106:46 :28GMT Etag:"a030f020ac7c01:1e9f" Content-length:39725426 Content-range:bytes554554-40279979 / 40279980
Web服務器應答的第一行,列出服務器正在運行的HTTP版本號和應答代碼。代碼"200 OK"表示請求完成。
content_type指示HTTP具體信息的MIME類型。如:content_type:text/html指示傳送的數據是HTML文檔。
content_length指示HTTP具體信息的長度(字節)。
Last-modified指示內容的最後修訂時間。
Server指示服務器的軟件信息。。
Content-Range指示在整個內容中的插入位置和整個內容的長度。
Web服務器程序實現GET請求的方法是:創建socket套接描述符,監聽端口8080,等待、接受客戶機連接到端口8080,得到與客戶機連接的socket描述符。從socket中讀取客戶機提交的請求信息,分析請求信息,如果請求類型是GET,則從請求信息中解析出所要訪問的文件名。如果文件存在,讀取文件,把HTTP頭信息和文件內容通過socket傳回給客戶機應用程序。然後關閉文件,關閉與客戶機應用程序連接的socket描述符。
客戶機應用程序freshclam在函數downloaddb中創建對Web服務器的連接,發送GET類型的請求命令,並從Web服務器接收到病毒庫數據,存儲到病毒庫文件中。
函數downloaddb將Web服務器上的病毒庫版本與本地病毒庫進行比較,如果本地病毒庫是舊的,則從Web服務器上下載病毒庫到一個隨機的臨時文件中。再接着,對臨時文件中的病毒庫進行數字簽名驗證,檢查病毒庫版本號。然後,刪除本地病毒庫,將臨時文件命名爲本地病毒庫。
函數downloaddb的參數localname爲本地病毒庫名,參數remotename爲Web服務器上病毒庫名,參數hostname爲主機地址。
函數downloaddb列出如下(在clamav/freshclam/manager.c中):int downloaddb( const char * localname, const char * remotename, const char * hostname, char * ip, int * signo, const struct cfgstruct * copt, const char * dnsreply, char * localip, int outdated) { ...... memset ( ipaddr, 0 , sizeof ( ipaddr) ) ; if ( ! nodb && dbver == - 1 ) { if ( ip[ 0 ] ) //使用IP地址連接,得到連接的套接字描述符放在hostfd中 hostfd = wwwconnect( ip, proxy, port, ipaddr, localip) ; else //使用主機名連接 hostfd = wwwconnect( hostname, proxy, port, ipaddr, localip) ; ...... if ( ! ip[ 0 ] ) strcpy( ip, ipaddr) ; //將函數wwwconnect得到的IP地址拷貝到ip中 //通過socket從Web服務器上讀取病毒庫頭512字節,進行數字簽名驗證後,讀取病毒庫信息 remote = remote_cvdhead( remotename, hostfd, hostname, proxy, user, pass, & ims) ; ...... dbver = remote-> version; //得到Web服務器上病毒庫版本 cl_cvdfree( remote) ; close( hostfd) ; } ...... //省略:比較本地與Web服務器上的病毒庫版本,如果本地庫版本與Web服務器上一致,則返回 if ( current) cl_cvdfree( current) ; if ( ipaddr[ 0 ] ) { //如果ip地址存在 hostfd = wwwconnect( ipaddr, proxy, port, NULL, localip) ; //使用ip地址連接Web服務器 } else { hostfd = wwwconnect( hostname, proxy, port, ipaddr, localip) ; //使用主機名連接Web服務器,並得到地址 if ( ! ip[ 0 ] ) strcpy( ip, ipaddr) ; //拷貝ip地址 } ...... /*創建clamav產生的由隨機數組成的臨時文件名,因此不存在讀寫競爭*/ tempname = cli_gentemp( "." ) ; //下載病毒庫到臨時文件 if ( ( ret = get_database( remotename, hostfd, tempname, hostname, proxy, user, pass) ) ) { ...... } close( hostfd) ; if ( ( ret = cl_cvdverify( tempname) ) ) { //對病毒庫進行數字簽名驗證 ...... } ...... //省略:讀取病毒庫文件頭,驗證版本號是否是新的,如不是,則刪除臨時文件 //下載的病毒庫版本中新的,則刪除舊庫,將臨時文件命名爲本地病毒庫 if ( ! nodb && unlink( localname) ) { //刪除本地病毒庫文件 ...... } else { if ( rename( tempname, localname) == - 1 ) { //將臨時文件命名爲本地庫文件 ...... } ...... * signo += current-> sigs; cl_cvdfree( current) ; free( tempname) ; return 0 ; }
函數wwwconnect通過服務器名得到服務器的ip地址等,創建socket並進行socket綁定、連接操作,連接成功,返回socket描述符。 函數wwwconnect列出如下(在clamav/freshclam/manager.c中):
int wwwconnect( const char * server, const char * proxy, int pport, char * ip, char * localip) { ...... name .sin_family = AF_INET; #ifdef PF_INET socketfd = socket( PF_INET, SOCK_STREAM, 0 ) ; //創建socket #else socketfd = socket( AF_INET, SOCK_STREAM, 0 ) ; #endif ...... if ( localip) { //如果本地ip存在 if ( ( he = gethostbyname( localip) ) == NULL) { //由主機名查詢到主機的ip地址等 ...... } else { struct sockaddr_in client; memset ( ( char * ) & client, 0 , sizeof ( struct sockaddr_in) ) ; client.sin_family = AF_INET; client.sin_addr = * ( struct in_addr * ) he-> h_addr_list[ 0 ] ; if ( bind( socketfd, ( struct sockaddr * ) & client, sizeof ( struct sockaddr_in) ) != 0 ) { //綁定socket ...... } else { ia = ( unsigned char * ) he-> h_addr_list[ 0 ] ; sprintf( ipaddr, "%u.%u.%u.%u" , ia[ 0 ] , ia[ 1 ] , ia[ 2 ] , ia[ 3 ] ) ; } } } //end localip if ( proxy) { //代理服務器 hostpt = proxy; if ( ! ( port = pport) ) { #ifndef C_CYGWIN //如果是Cygwin系統 //從/etc/services讀取匹配“webcache”和“TCP”的行,放入wegcache中,包括服務器名、端口號等 const struct servent * webcache = getservbyname( "webcache" , "TCP" ) ; if ( webcache) port = ntohs( webcache-> s_port) ; //將端口號由網絡字節序轉換爲主機字節序 else port = 8080 ; endservent( ) ; //關閉文件/etc/services #else port = 8080 ; #endif } } else { //服務器的名字 hostpt = server; port = 80 ; } if ( ( host = gethostbyname( hostpt) ) == NULL) { //由服務器名得到ip地址等信息 ...... } //連接服務器名對應的ip地址,如果連接上服務器,就返回連接的套接字描述符socketfd for ( i = 0 ; host-> h_addr_list[ i] != 0 ; i++ ) { ia = ( unsigned char * ) host-> h_addr_list[ i] ; sprintf( ipaddr, "%u.%u.%u.%u" , ia[ 0 ] , ia[ 1 ] , ia[ 2 ] , ia[ 3 ] ) ; if ( ip) strcpy( ip, ipaddr) ; name.sin_addr = * ( ( struct in_addr * ) host-> h_addr_list[ i] ) ; name.sin_port = htons( port) ; //端口號從主機字節序轉換網絡字節序 if ( connect( socketfd, ( struct sockaddr * ) & name, sizeof ( struct sockaddr_in) ) == - 1 ) { //連接端口 continue ; } else { return socketfd; } } close( socketfd) ; return - 2 ; }
函數get_database連接服務器,並從服務器下載數據到本地文件file中。它將用戶名和密碼轉換成Base64格式,編寫併發送下載命令,從服務器接收、分析並保存數據。
函數get_database列出如下(在clamav/freshclam/manager.c中):
int get_database( const char * dbfile, int socketfd, const char * file, const char * hostname, const char * proxy, const char * user, const char * pass) { char cmd[ 512 ] , buffer[ FILEBUFF] , * ch; int bread, fd, i, rot = 0 ; char * remotename = NULL, * authorization = NULL; const char * rotation = "|/-// " ; if ( proxy) { //代理服務器 remotename = mmalloc( strlen( hostname) + 8 ) ; sprintf( remotename, "http://%s" , hostname) ; //得到主機名,如:http://db.ac.clamav.net if ( user) { int len; char * buf = mmalloc( ( strlen( pass) + strlen( user) ) * 2 + 4 ) ; char * userpass = mmalloc( strlen( user) + strlen( pass) + 2 ) ; sprintf( userpass, "%s:%s" , user, pass) ; //代理服務器的用戶名和口令 /*轉換成base64格式,Base64是MIME郵件中常用的編碼方式之一。它將輸入的字符串或數據編碼成只含有{‘A‘-‘Z‘,...‘/‘}這64個可打印字符*/ len= fmt_base64( buf, userpass, strlen( userpass) ) ; free( userpass) ; buf[ len] = '/0 ' ; authorization = mmalloc( strlen( buf) + 30 ) ; sprintf( authorization, "Proxy-Authorization: Basic %s/r /n " , buf) ; free( buf) ; } } //創建將存入病毒庫的臨時文件file,file爲隨機產生的文件名 #if defined(C_CYGWIN) || defined(C_OS2) if ( ( fd = open( file, O_WRONLY| O_CREAT| O_EXCL| O_BINARY, 0644 ) ) == - 1 ) { #else if ( ( fd = open( file, O_WRONLY| O_CREAT| O_EXCL, 0644 ) ) == - 1 ) { #endif ...... } //編寫下載病毒庫的命令,如: // GET http://db.ac.clamav.net/main.cvd HTTP/1.1/r/n // Host:主機名/r/n授權 // User-Agent: "PACKAGE"/"VERSION"/r/n // Cache-Control: no-cache/r/n // Connection: close/r/n // /r/n snprintf( cmd, sizeof ( cmd) , "GET %s/%s HTTP/1.1/r /n " "Host: %s/r /n %s" "User-Agent: " PACKAGE"/" VERSION"/r /n " "Cache-Control: no-cache/r /n " "Connection: close/r /n " "/r /n " , ( remotename != NULL) ? remotename: "" , dbfile, hostname, ( authorization != NULL) ? authorization: "" ) ; //將命令寫入socket write( socketfd, cmd, strlen( cmd) ) ; free( remotename) ; free( authorization) ; /*讀取所有的http頭 */ ch = buffer; i = 0 ; while ( 1 ) { /*一次接收一個字節直到到達/r/n/r/n */ if ( recv( socketfd, buffer + i, 1 , 0 ) == - 1 ) { ...... } //如果到達/r/n/r/n,中斷循環 if ( i> 2 && * ch == '/n ' && * ( ch - 1 ) == '/r ' && * ( ch - 2 ) == '/n ' && * ( ch - 3 ) == '/r ' ) { i++; break ; } ch++; i++; } //while buffer[ i] = 0 ; //末尾加0 /*檢查資源是否存在*/ if ( ( strstr( buffer, "HTTP/1.1 404" ) ) != NULL) { //如果有錯誤字符串“HTTP/1.1 404”,表示沒發現服務器 ...... } /* 接收主體內容並寫到文件中*/ //從socket中讀取數據到buffer,bread爲實際讀出的字節數 while ( ( bread = read( socketfd, buffer, FILEBUFF) ) ) { write( fd, buffer, bread) ; //將讀取的數據寫入到文件描述符fd中 fflush( stdout) ; //將標準輸出緩存中數據寫到標準輸出stdout rot++; rot %= 4 ; } close( fd) ; return 0 ; }