一则表驱动法的应用实例

 

1 需求场景

     考虑如下需求场景:

     终端按固定时间间隔(单位为分钟)生成诊断日志(格式为UserName-Status-yyyy-mm-dd-hh-mm.log),并上传至服务器。若终端与服务器的传输通道中断,则终端本地暂存最新的N个日志文件,即第(N+1)个周期生成的新日志将覆盖第1个周期的旧日志,以此类推。待传输恢复后,终端一次性上传该N个日志。

     本文主要讨论该需求中“最新日志覆盖最旧日志”的功能点。为突出层次,文中“覆盖”用先删除旧日志后创建新日志实现。实际中先删后建和先建后删各有优点,前者适于内存受限(如仅允许存在N个固定大小的日志),后者更为安全(避免创建新日志失败但已删除旧日志)。

     创建文件时调用现成库函数即可,而删除时需确定当前最旧的那个文件。下文将介绍几种删除判决的实现方式(时间复杂度依次降低)。

 

2 实现思路

2.1 比较文件的创建时间

     Linux系统中文件没有创建时间的概念,只有访问时间(atime)、修改时间(mtime)和状态改动时间(ctime)。因该需求中日志创建后立即写入内容,其后内容和状态(权限与属性等)均不再改变,故可用mtime或ctime表征创建时间。

     调用stat库函数即可获取日志的三种时间,比较各日志的mtime或ctime(整数)信息即可。

 1 #define FILE_NUM       (unsigned short)30 //允许共存的文件数目
 2 #define FILE_NAME_LEN  sizeof("%Log-Clover-65535.log")
 3 
 4 #include <sys/types.h>
 5 #include <sys/stat.h>
 6 #include <dirent.h>
 7 #include <errno.h>
 8 #define DIR_FILE_LEN   96 //目录和文件组成的绝对路径最大长度
 9 const char *gDirectoryName = "/sdb1/Clover/linux_test/log";
10 
11 
12 int CreateLogFile1(const char *pszCreateFileName){//比较文件时间戳
13     DIR *pDir = opendir(gDirectoryName);
14     if(NULL == pDir){
15        fprintf(stderr,"Cannot open directory: %s!\n", gDirectoryName);
16        return -1;
17     }
18 
19     unsigned char ucFileNum = 0;
20     time_t tFileTempCtime = 0;
21     char szDelFileName[NAME_MAX+1] = {0};   //待删除的文件名
22     struct dirent *pFileEntry = NULL;
23     while((pFileEntry = readdir(pDir)) != NULL){
24         //剔除当前目录.,上一级目录..及隐藏文件,避免死循环遍历目录
25         if(0 == strncmp(pFileEntry->d_name, ".", 1))
26             continue;
27 
28         struct stat tFileStatus;          //文件状态信息
29         char szAbsFile[DIR_FILE_LEN] = {0};  //文件绝对路径
30         sprintf(szAbsFile, "%s/%s", gDirectoryName, pFileEntry->d_name);
31         if(stat(szAbsFile, &tFileStatus) != 0){
32             fprintf(stderr,"Call stat error(errno:%d)!\n", errno);
33             return -1;
34         }
35 
36         if(S_ISDIR(tFileStatus.st_mode)) //跳过子目录
37             continue;
38 
39         if((ucFileNum < 1) ||
40            (tFileStatus.st_ctime < tFileTempCtime)){ //首个文件或当前文件更老
41             tFileTempCtime = tFileStatus.st_ctime;
42             strcpy(szDelFileName, pFileEntry->d_name);
43         }
44         ucFileNum++;
45         if(ucFileNum >= FILE_NUM){
46             char szDeleteAbsFile[DIR_FILE_LEN] = {0};  //待删除的文件绝对路径
47             sprintf(szDeleteAbsFile, "%s/%s", gDirectoryName, szDelFileName);
48             if(0 != remove(szDeleteAbsFile)){
49                 fprintf(stderr,"Fail to delete file: %s!\n", szDeleteAbsFile);
50                 return -1;
51             }
52         }
53     }
54 
55     char szCreateAbsFile[DIR_FILE_LEN] = {0};  //待创建的文件绝对路径
56     sprintf(szCreateAbsFile, "%s/%s", gDirectoryName, pszCreateFileName);
57     FILE *pFile = fopen(szCreateAbsFile, "w+");
58     if(NULL == pFile){
59         fprintf(stderr,"Cannot open file: %s\n", szCreateAbsFile);
60         return -1;
61     }
62 
63     fputs(szCreateAbsFile, pFile); //暂时写入绝对路径
64     fclose(pFile);
65     printf("Create file(%s) success!\n", pszCreateFileName);
66 
67     closedir(pDir);
68 
69     return 0;
70 }
View Code

2.2 比较文件名字符串

     因日志文件名包含时间信息,故可直接调用strcmp库函数比较日志文件名字符串。

 1 int CreateLogFile2(const char *pszCreateFileName){//比较文件名
 2     DIR *pDir = opendir(gDirectoryName);
 3     if(NULL == pDir){
 4        fprintf(stderr,"Cannot open directory: %s!\n", gDirectoryName);
 5        return -1;
 6     }
 7 
 8     unsigned char ucFileNum = 0;
 9     char szDelFileName[NAME_MAX+1] = {0};   //待删除的文件名
10     struct dirent *pFileEntry = NULL;
11     while((pFileEntry = readdir(pDir)) != NULL){//readdir返回的文件顺序与文件名无关
12         //剔除当前目录.,上一级目录..及隐藏文件,避免死循环遍历目录
13         if(0 == strncmp(pFileEntry->d_name, ".", 1))
14             continue;
15 
16         if(ucFileNum < 1)  //首个文件
17             strcpy(szDelFileName, pFileEntry->d_name);
18 
19         if(strcmp(szDelFileName, pFileEntry->d_name) > 0)  //当前文件更老
20             strcpy(szDelFileName, pFileEntry->d_name);
21 
22         ucFileNum++;
23         if(ucFileNum >= FILE_NUM){
24             char szDeleteAbsFile[DIR_FILE_LEN] = {0};  //待删除的文件绝对路径
25             sprintf(szDeleteAbsFile, "%s/%s", gDirectoryName, szDelFileName);
26             if(0 != remove(szDeleteAbsFile)){
27                 fprintf(stderr,"Fail to delete file: %s!\n", szDeleteAbsFile);
28                 return -1;
29             }
30         }
31     }
32 
33     char szCreateAbsFile[DIR_FILE_LEN] = {0};  //待创建的文件绝对路径
34     sprintf(szCreateAbsFile, "%s/%s", gDirectoryName, pszCreateFileName);
35     FILE *pFile = fopen(szCreateAbsFile, "w+");
36     if(NULL == pFile){
37         fprintf(stderr,"Cannot open file: %s\n", szCreateAbsFile);
38         return -1;
39     }
40 
41     fputs(szCreateAbsFile, pFile); //暂时写入绝对路径
42     fclose(pFile);
43     printf("Create file(%s) success!\n", pszCreateFileName);
44 
45     closedir(pDir);
46 
47     return 0;
48 }
View Code

2.3 比较文件句柄

     创建日志时为其创建一个句柄(整数编号),比较句柄即可。为便于说明,定义如下信息:

typedef struct{

    char szFileName[NAME_MAX+1]; //文件名称

    unsigned int dwFileHandler;      //文件句柄

}FILE_INFO;

static FILE_INFO gFileInfoMap[FILE_NUM] = {{{0}}};  //FILE_NUM为允许共存的文件数目

     全局信息表gFileInfoMap存储当前各日志文件的文件名及其句柄。每当创建一个新日志时,删除句柄最小的文件,并更新其对应的数组记录:文件名称改为新日志名,文件句柄值增加FILE_NUM。

     例如:

     {“log20130517”, 1};

     {“log20130518”, 2};

     {“log20130519”, 3};

     创建“log20130520”文件后,上面的结构体数组内容变为

     {“log20130520”, 4};

     {“log20130518”, 2};

     {“log20130519”, 3};

     下次再创建新文件时覆盖“log20130518”——哪怕新文件名为“log20130513”(比前两种方法更安全)!

     该方法无需关心文件的时间信息。这类似于新员工报道时分配的工号,工号数字的大小就代表入职的时间先后(无论工号是否包含入职精确时间)。

     因2.4节方法可视为本节的“升级版“,此处省略本节实现。

2.4 表驱动法(无需比较)

     为便于说明,定义如下信息:

//gFileNameMap按新旧次序环形记录FILE_NUM个文件的文件名信息

static char gFileNameMap[FILE_NUM][FILE_NAME_LEN] = {{0}};

//gLatestFileIndex记录gFileNameMap第一维最新文件的下标,

//该下标+1并对FILE_NUM取余后对应的元素为最旧的文件,即

//Oldest = (Latest + 1) mod FILE_NUM.

static unsigned short gLatestFileIndex = FILE_NUM-1;

     该方法利用“环形存储时最新文件后必为最旧文件“这一特性。最新文件为数组最后一个元素时,取余操作将定位到数组最前端的最旧文件。

 1 int CreateLogFile3(const char *pszCreateFileName){//表驱动法(无需比较)
 2     DIR *pDir = opendir(gDirectoryName); //目录操作并非必要
 3     if(NULL == pDir){
 4        fprintf(stderr,"Cannot open directory: %s!\n", gDirectoryName);
 5        return -1;
 6     }
 7 
 8     char szDeleteAbsFile[DIR_FILE_LEN] = {0};  //待删除的文件绝对路径
 9     unsigned short wOldestFileIndex = 0; //(gLatestFileIndex+1)%FILE_NUM;
10     if(FILE_NUM != (gLatestFileIndex+1))
11         wOldestFileIndex = gLatestFileIndex + 1;
12     if('\0' != gFileNameMap[wOldestFileIndex][0]){//记录为空时remove会删除目录
13         sprintf(szDeleteAbsFile, "%s/%s", gDirectoryName, gFileNameMap[wOldestFileIndex]);
14         if(0 != remove(szDeleteAbsFile)){
15             fprintf(stderr,"Fail to delete file: %s!\n", szDeleteAbsFile);
16             return -1;
17         }
18     }
19 
20     char szCreateAbsFile[DIR_FILE_LEN] = {0};  //待创建的文件绝对路径
21     sprintf(szCreateAbsFile, "%s/%s", gDirectoryName, pszCreateFileName);
22     FILE *pFile = fopen(szCreateAbsFile, "w+");
23     if(NULL == pFile){
24         fprintf(stderr,"Cannot open file: %s\n", szCreateAbsFile);
25         return -1;
26     }
27 
28     fputs(szCreateAbsFile, pFile); //暂时写入绝对路径
29     fclose(pFile);
30     printf("Create file(%s) success!\n", pszCreateFileName);
31 
32     gLatestFileIndex = wOldestFileIndex;
33     strcpy(gFileNameMap[gLatestFileIndex], pszCreateFileName);
34 
35     closedir(pDir);
36 
37     return 0;
38 }
View Code

 

3 耗时比较

    采用如下方法粗略统计除2.3节外的三种实现耗时(未考虑缓存、预热等因素):

 1 #include <sys/time.h>
 2 #define TIME_ELAPSED(codeToTime) do{ \
 3     struct timeval beginTime, endTime; \
 4     gettimeofday(&beginTime, NULL); \
 5     {codeToTime;} \
 6     gettimeofday(&endTime, NULL); \
 7     long secTime  = endTime.tv_sec - beginTime.tv_sec; \
 8     long usecTime = endTime.tv_usec - beginTime.tv_usec; \
 9     printf("[%s(%d)]Elapsed Time: SecTime = %lds, UsecTime = %ldus!\n", __FUNCTION__, __LINE__, secTime, usecTime); \
10 }while(0)
11 
12 #include <unistd.h>
13 void CalcTime1(void){
14     unsigned short wFileIdx = 0;
15     for(; wFileIdx < 1; wFileIdx++){
16         //time_t精度为秒,批量测试文件创建时需作秒级延迟;
17         //但这样会影响计时统计,因为sleep会"淹没"CreateLogFile1(单次执行微妙级)
18         //sleep(1);
19         char szFileName[FILE_NAME_LEN] = {0};
20         sprintf(szFileName, "Log-Clover-%05d.log", wFileIdx);
21         CreateLogFile1(szFileName);
22     }
23 }
24 void CalcTime2(void){
25     unsigned short wFileIdx = 0;
26     for(; wFileIdx < 1000; wFileIdx++){
27         char szFileName[FILE_NAME_LEN] = {0};
28         sprintf(szFileName, "Log-Clover-%05d.log", wFileIdx);
29         CreateLogFile2(szFileName);
30     }
31 }
32 void CalcTime3(void){
33     unsigned short wFileIdx = 0;
34     for(; wFileIdx < 1000; wFileIdx++){
35         char szFileName[FILE_NAME_LEN] = {0};
36         sprintf(szFileName, "Log-Clover-%05d.log", wFileIdx);
37         CreateLogFile3(szFileName);
38     }
39 }
40 
41 int main(void){
42     TIME_ELAPSED(CalcTime1());
43     TIME_ELAPSED(CalcTime2());
44     TIME_ELAPSED(CalcTime3());
45     DIR *pDir = NULL;
46     TIME_ELAPSED(pDir = opendir(gDirectoryName));
47     TIME_ELAPSED(readdir(pDir));
48     struct stat tFileStatus;          //文件状态信息
49     char szAbsFile[DIR_FILE_LEN] = {0};  //文件绝对路径
50     sprintf(szAbsFile, "%s/%s", gDirectoryName, "Log-Clover-00002.log");
51     TIME_ELAPSED(stat(szAbsFile, &tFileStatus));
52     TIME_ELAPSED(remove(szAbsFile));
53     FILE *pFile = NULL;
54     TIME_ELAPSED(pFile = fopen(szAbsFile, "a+"));
55     TIME_ELAPSED(fputs(szAbsFile, pFile));
56     TIME_ELAPSED(fclose(pFile));
57     TIME_ELAPSED(closedir(pDir));
58     TIME_ELAPSED(strcmp("Log-Clover-00000.log","Log-Clover-00001.log"));
59 
60     TIME_ELAPSED({int i=0;for(;i<10000;i++){strcmp("Log-00000","Log-00001");}});
61     TIME_ELAPSED({int i=0;for(;i<10000;i++){int new=20;if(FILE_NUM!=new+1){int old=new+1;}}});
62     TIME_ELAPSED(int new=20;if(FILE_NUM!=new+1){int old=new+1;});
63 
64     return 0;
65 }
View Code

    统计结果如下所示:

对象

条件

耗时(us)

2.1: CalcTime1()

单次

792

2.2: CalcTime2()

单次

214

2.2: CalcTime2()

FILE_NUM=30,循环1000次

274357

2.4: CalcTime3()

FILE_NUM=30,循环1000次

77789

opendir()

单次

81

readdir()

单次

13

stat()

单次

9

remove()

单次

78

fopen()

单次

33

fputs()

单次

8

fclose()

单次

39

closedir()

单次

8

strcmp()

单次

2

strcmp()

循环10000次

27

Oldest = (Latest + 1) mod FILE_NUM

循环10000次

36

Oldest = (Latest + 1) mod FILE_NUM

单次

1

注意:计时结果仅作粗略参考,且每次统计时结果可能略有不同。

    可见,文件操作函数(opendir、fopen等)的计时噪声会“淹没“比较代码,导致2.4节表驱动法相比其他方法效果不甚显著。同时考虑到表驱动法其实无需调用opendir/closedir函数,故执行效率可进一步提高。

 

5 总结

     本文给出的表驱动法适用于如下场景:

     在按照时空顺序依次创建的若干对象中,查找符合指定时空规则的某个对象(如第N个最老对象),而不关心对象内部信息。

 

posted @ 2014-05-20 15:02  clover_toeic  阅读(1008)  评论(0编辑  收藏