Linux安全体系的ClamAV病毒扫描程序[转]
来源:互联网 发布:qq头像psd源码 编辑:程序博客网 时间:2024/05/16 19:48
摘自:http://www.shangshuwu.cn/index.php/Linux安全体系的ClamAV病毒扫描程序
ClamAV是使用广泛且基于GPL License的开放源代码的典型杀毒软件,它支持广泛的平台,如:Windows、Linux、Unix等操作系统,并被广泛用于其他应用程序,如:邮件客户端及服务器、HTTP病毒扫描代理等。ClamAV源代码可从http://www.clamav.net下载。
本章分析了ClamAV的客户端、服务器及病毒库更新升级应用程序,着重阐述了Linux下C语言编程中的许多经典用法。
目录
- 1 1 ClamAV概述
- 2 2 ClamAV编译安装及使用
- 2.1 2.1 clamd后台与clamdscan客户端
- 2.2 2.2 clamav-milter邮件扫描器
- 2.3 2.3 建立病毒库自动更新
- 2.4 2.4 libclamav库API
- 3 3 clamd服务器
- 3.1 3.1 应用程序命令参数分析
- 3.2 3.2 clamd服务器入口函数clamd
- 3.3 3.3 设置系统限制及确定资源使用量
- 3.4 3.4 配置文件解析
- 3.5 3.5 log文件操作
- 3.6 3.6使用syslog机制输出调试信息
- 3.7 3.7 用户组及文件权限设置
- 3.8 3.8 进程后台化
- 3.9 3.9 利用socket在进程间通信
- 3.9.1 (1)clamd服务器socket连接
- 3.9.2 (2) clamd从socket收发数据
- 3.9.3 (3) socket描述符多路复用
- 3.9.4 (4)使用临时socket传输数据
- 3.10 3.10 子进程执行系统命令及环境变量设置
- 3.11 3.11线程
- 3.11.1 (1) 线程创建及线程属性
- 3.11.2 (2) 线程结束
- 3.11.3 (3) 取消线程
- 3.11.4 (4) 线程的等待
- 3.11.5 (5) 互斥
- 3.11.6 (6) 线程数据
- 3.12 3.12 线程池
- 3.13 3.13 信号处理
- 3.14 3.14 OnAccess扫描病毒线程clamukoth
- 3.15 3.15 服务器进程自动重启动保护
- 3.15.1 (1) cron定时机制
- 3.15.2 (2) clamd后台进程的定期检查
- 3.15.3 (3) 运行脚本clamd
- 4 4 libclamav库API
- 4.1 4.1 病毒库的装载
- 4.2 4.2 病毒扫描
- 5 5客户端端应用程序
- 5.1 5.1 clamdscan客户端
- 5.2 5.2 qtclamavclient客户端应用程序
- 6 6 病毒库升级程序freshclam
- 6.1 6.1 病毒库定时更新
- 6.2 6.2 域名信息查询
- 6.2.1 (1) DNS消息格式及域名查询函数
- 6.2.2 (2) 下载管理函数downloadmanager
- 6.2.3 (3) 域名查询函数txtquery
- 6.3 6.3 HTTP协议下载病毒库文件
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 dazuko254 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’)dnldefine(‘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.comHTTPProxyPort 1234HTTPProxyUsername myusernameHTTPProxyPassword mypass
配置文件中的DatabaseMirror指定了数据库服务器,freshclam将尝试从这个服务器下载直到最大次数。缺省的数据库镜像是database.clamav.net,为了从最近的镜像下载数据库,你应使用db.xx.clamav.net配置freshclam,xx代表你的国家代码。例如,如果你的服务器在"Ascension Island",你应该加下面的行到freshclam.conf中:
DNSDatabaseInfo current.cvd.clamav.netDatabaseMirror db.ac.clamav.netDatabaseMirror 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:functionalitylevel required:MD5 checksum:digital signature:builder name:build time (sec)
使用命令sigtool –info可显示CVD文件的详细信息,方法与显示结果列出如下:
#sigtool -i daily.cvdBuild time: 11 Sep 2004 21-07 +0200Version: 487# of signatures: 1189Functionality level: 2Builder: ccordesMD5: a3f4f98694229e461f17d2aa254e9a43Digital signature: uwJS6d+y/9g5SXGE0Hh1rXyjZW/PGK/zqVtWWVL3/tfHEnA17z6VB2IBR2I/OitKRYzmVo3ibU7bPCJNgi6fPcW1PQwvCunwAswvR0ehrvY/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(第一个固定参数地址) 低位地址 函数返回地址 图5 函数调用时参数传递时参数在内存中压栈的次序图宏定义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_SAFEpthread_mutex_lock(&logg_mutex); //多线程互斥锁#endifif(!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_SAFEpthread_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"); //将错误信息发向socketreturn -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_POLLstruct 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; } #elsefd_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(¤t_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); //用来保护全局变量progexitprogexit = 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/shPATH=/etc:/bin:/sbin:/usr/bin:/usr/sbinHOME=/var/log##minutehourmdaymonthwday who command#*/5 * * * * root/usr/libexec/atrun## rotate log files every hour, if necessary0****root/usr/sbin/newsyslog## do daily/weekly/monthly maintenance0 2 * * * root /etc/daily 2>&1 | sendmail root30 3 * * 6 root /etc/weekly 2>&1 | sendmail root30 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,310-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)=060b01e9b440ba0001b91703cd217303e91800b8004233c933d2cd21b440ba0b01b90300cd2173Gen.1000Years.791.B (Clam)=0301e9430200000090cd2000000000000000000d0a4d656d6f727920616c6c6f6361746573696f6e206572726fGen.100-Years (Clam)=4d6f6e786c612d42205b5648505d202020202020202020202020436c65616e2d557020202020202e202e202e20782078202e202e202e202e202e20202020203550535152bbfe128b0f81f1ff00890f4b81fb160175f1Gen.100-Years=fe3a558bec50817e0400c0730c2ea147Gen.1024-PrScr.1=8cc0488ec026a103002d800026a30300Gen.1024-PrScr.2=041f3df0f07505a10301cd0526a1Gen.1024-PrScr.3=012ea30300b4400e1fba0004b90004e8e8007230Gen.1024-PrScr.4=bf00b82125cd2133c08ec0b8f0f026Gen.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 21770332e5c92be38ce0f195019258c8376dc:1640013:Submission 2247571d934fdf522c4227485716b0413c7be:55296:Submission 23647810439699f2dc802ff8c530f59f23b7c:876032:Submission 24405848b157359e022907645c222b3daf72d: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-cli192bd7afb1479aad2b64a6c176773a01:761344:Trojan.Beastdoor.207.C-cliaaab755d9baf21baf05de8f32af2c996:57856:Trojan.BO.A-Clid3edf9b7d99205afda64b3a7c1a63264:307200:Trojan.Boid.20-cli-1de59dc8df6021d19246f9b74dd1d68bc:32768:Trojan.Boid.20-cli-2d9c8d35d577b7bc2cdbe26282383400a:36864:Trojan.Boid.20-edit565ce39278f60226fbbe920f79e77eb2:17408:Trojan.Ceptio.10-cli01868bc1780b71996e90dafd180dae1b: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:*:3c6f626a65637420747970653d222f2f2f2f2f2f2f2f2f2f2f2fHTML.Phishing.Bank-1:3:*:3c6d6170206e616d653d22{-36}223e3c6172656120636f6f7264733d22302c20302c20{4-12}222073686170653d22726563742220687265663d22{-160}3c2f6d61703e3c696d67207372633d226369643aExploit.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 depthWorm.Padowor.A-zippwd:1:*:72767:69779:5f6f7a3f:*:1:1Trojan.Dumador-31-zippwd:1:*:21008:20598:ba9f27fb:8:1:1Worm.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_Tsocklen_t peer_size;#elseint peer_size;#endifchar 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.comnameserver 129.188.1.1nameserver 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_CLAMDif(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.1Host:download.microtool.deAccept:*/*Pragma:no-cacheCache-Control:no-cacheReferer: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.1200OKDate:Mon,31Dec200104:25:57GMTServer:Apache/1.3.14(Unix)Content-type:text/htmlLast-modified:Tue,17Apr200106:46:28GMTEtag:"a030f020ac7c01:1e9f"Content-length:39725426Content-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;}
- Linux安全体系的ClamAV病毒扫描程序[转]
- Linux安全体系的ClamAV病毒扫描程序[转]
- 安装clamav,linux下的病毒扫描
- Clamav 搭建实现 Linux 病毒防治
- clamav完整查杀linux病毒实战
- Linux 下使用杀毒软件clamav扫描木马病毒
- Linux 下使用杀毒软件clamav扫描木马病毒
- iptables+p3scan+ClamAV 实现病毒网关(转)
- 安全威胁无孔不入:基于Linux系统的病毒(转)
- linux clamav杀毒软件的安装
- Linux安全体系的文件权限管理
- linux安全体系的文件权限管理
- Linux安全体系的文件权限管理
- LSAT(Linux Security Auditing Tool) 本地安全扫描程序的安装及使用
- Linux安全扫描概述
- Linux安全扫描概述
- linux下开源查杀病毒软件clamav的安装
- 基于数据库的CS程序的简单安全体系
- HDMI传输原理解析
- 横空出世,席卷Csdn:记微软等100题系列数次被荐[100题维护地址]
- VM NAT 设置遇到的问题~~~
- Server 编程会用到的工具
- IFrame的显示和隐藏
- Linux安全体系的ClamAV病毒扫描程序[转]
- 预览ExtJS 4.0的新功能(六):读写器/Opeartion
- 英特尔称MeeGo将专注智能手机等四大方面应用
- 消息队列(Message Queue)简介及其使用
- Unpacking Argument Lists
- Xilinx设计元素缩写查询
- 7.c++-内存对齐的一点个人理解(#pragma pack(k))
- MeeGo系统平板电脑和智能手机明年上市
- . Net环境下消息队列(MSMQ)对象的应用