一则表驱动法的应用实例
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 }
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 }
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 }
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 }
统计结果如下所示:
对象 |
条件 |
耗时(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个最老对象),而不关心对象内部信息。