巧妙利用访问时间提取和重组代码的实践

 

说明

     本文主要介绍巧妙利用访问时间提取和重组某嵌入式产品SDK代码的实践。

 

一  问题提出

     目前产品平台为便于编译管理,要求各模块组织为include-source目录结构,分别存放头文件和源文件。但芯片厂家提供的SDK按功能划分为众多子目录,如subdir1(.c,.h)…subdirN(.c,.h)…Makefile,并将头文件和源文件一并存放在各子目录内。

     此外,厂家SDK支持多种管理场景,通过选项开关编译不同的目录和文件。而产品硬件定板后管理场景固定,其他场景所涉及的代码将不再需要。

     因此,需要遍历厂家SDK目录,提取在用的文件并重组为include-source目录结构。

 

二  解决方案

     根据重组要求,有四种解决方案。

2.1 方案一

     某些产品已将厂家SDK重组为include-source目录结构。因此,可人工比对现有产品已重组的SDK,从新的SDK中摘取所需文件,重组后再次编译,根据编译结果进行必要的修改和调整。

     显然,该方案效率很低(尤其是甄别不同场景所用的同名文件时),而且不安全(可能遗漏SDK新增内容)。

2.2 方案二

     编译厂家SDK后,解析编译输出中出现的文件(表明在用),手工或脚本提取后重组。

     该方案解析难度稍大,且需考虑重复出现的文件(尤其是嵌套包含的头文件出现编译警告时)。

2.3 方案三

     可参考《将模块代码量精简为2%的实践》一文中剔除未使用条件编译分支的思路,稍加修改即可用于解决本文所提问题。

     其原理如下:

     1) 创建include和source目录。

     2) 在SDK待处理代码(所有源文件和头文件)的首行插入gcc扩展预编译头#warning。

     3) 编译待处理代码获取gcc编译输出并进行分析。

     4) 编译结果中”#warning”警告所涉及的文件表明在用,可删除编译头后按源文件和头文件类型分别拷贝至include和source目录。

     其中,步骤1和3可手工进行,步骤2和4对《实践》一文中提供的Python脚本稍加修改即可实现。

2.3 方案四

     编译SDK后,遍历目录读取各文件的访问时间*,提取符合指定时间(表明本次编译读到)的文件后重组。“指定时间”格式形如"Mon Aug 18 09:59:46 2014",通常设置为精确到日或时的编译时间。

     该方案实现简单,应用起来也比方案三方便,故作为本文的优选实现。

 

三  代码实现

     本节将实现上节所述的方案四(假设源文件名为DirMover.c)。所需的头文件如下:

1 #include <string.h>
2 #include <time.h> //ctime
3 #include <errno.h>
4 #include <unistd.h> //getcwd
5 #include <sys/stat.h>
6 #include <dirent.h>

     首先定义一组全局数据:

1 #define DIR_LEN        512  //目录绝对路径最大长度
2 #define DIR_FILE_LEN   1024 //目录和文件组成的绝对路径最大长度
3 
4 //将目标代码的原始目录结构改为Include-Source目录结构
5 typedef struct{
6     char szIncDir[DIR_LEN];  //Include目录
7     char szSrcDir[DIR_LEN];  //Source目录
8 }T_DIR_TREE;
9 char gszCmpTime[sizeof("Mon Aug 18 09:59:46 2014")] = {0};

     重组前需要创建include和source目录。MakeCleanDir()函数用于创建空的目录,若目录已存在且内含文件,则删除已存在的文件。

 1 int MakeCleanDir(const char *pszDir, mode_t dwMode)
 2 {
 3     if(0 == mkdir(pszDir, dwMode))
 4         return 0;
 5 
 6     if(errno != EEXIST)
 7     {
 8         fprintf(stderr, "Cannot make directory: %s(%s)!\n", pszDir, strerror(errno));
 9         return -1;
10     }
11 
12     char szCopyCmd[DIR_FILE_LEN] = {0};
13     snprintf(szCopyCmd, sizeof(szCopyCmd), "rm -rf %s/*", pszDir);
14     system(szCopyCmd); //删除该目录下的文件    
15     return 0;
16 }

     为简化实现,删除操作直接调用rm命令。基于同样的考虑,后文拷贝文件时也直接调用cp命令。

     创建重组目录后,调用TraverseDirectory()函数遍历原SDK目录:

 1 typedef int (*TravFileFunc)(char *pszAbsFile, struct stat *ptFileStatus, void* pvTravInfo);
 2 int TraverseDirectory(const char *pszCurDir, void* pvTravInfo, TravFileFunc fpTravFile)
 3 {
 4     DIR *pDir = opendir(pszCurDir);
 5     if(NULL == pDir){
 6        fprintf(stderr, "Cannot open directory: %s!\n", pszCurDir);
 7        return -1;
 8     }
 9 
10     struct dirent *pFileEntry = NULL;
11     while((pFileEntry = readdir(pDir)) != NULL){
12         //剔除当前目录.,上级目录..及隐藏文件,避免死循环遍历目录
13         if(0 == strncmp(pFileEntry->d_name, ".", 1))
14             continue;
15 
16         struct stat tFileStatus;             //文件状态信息
17         char szAbsFile[DIR_FILE_LEN] = {0};  //文件绝对路径
18         sprintf(szAbsFile, "%s/%s", pszCurDir, pFileEntry->d_name);
19         if(stat(szAbsFile, &tFileStatus) != 0){
20             fprintf(stderr, "Call stat error(%s)!\n", strerror(errno));
21             return -1;
22         }
23 
24         if(S_ISDIR(tFileStatus.st_mode)) //文件类型为目录,递归子目录
25         {
26             TraverseDirectory(szAbsFile, pvTravInfo, fpTravFile);
27             continue;
28         }
29 
30         if(fpTravFile(szAbsFile, &tFileStatus, pvTravInfo) != 0)
31             break;
32     }
33 
34     closedir(pDir);
35     return 0;
36 }

     为求通用性,将TravFileFunc回调函数指针执行文件操作。此处回调函数原体为MoveFile()函数,该函数检查当前文件的返回时间,并将符合要求的文件分别拷贝至include和source目录。

 1 int MoveFile(char *pszAbsFile, struct stat *ptFileStatus, void* pvTravInfo)
 2 {
 3     printf("Current file: %s(atime: %s).\n", pszAbsFile, ctime(&ptFileStatus->st_atime));
 4 
 5     //跳过Include和Source目录
 6     T_DIR_TREE *ptNewDirTree = (T_DIR_TREE *)pvTravInfo;
 7     char *pSlashPos = strrchr(pszAbsFile, '/');  //目录与文件名之间的'/'号
 8     *pSlashPos = '\0';  //丢弃文件名,留取目录名
 9     if(!strcmp(pszAbsFile, ptNewDirTree->szIncDir) ||
10        !strcmp(pszAbsFile, ptNewDirTree->szSrcDir))
11         return 0;
12     *pSlashPos = '/';   //恢复文件名
13 
14     //跳过DirMover.c文件
15     char *pszFileName = pSlashPos + 1; //basename(pszAbsFile);
16     if(!strcmp(pszFileName, __FILE__))
17         return 0;
18 
19     //文件访问时间与指定时间不符,跳过
20     if(strncmp(ctime(&ptFileStatus->st_atime), gszCmpTime, strlen(gszCmpTime)))
21         return 0;
22 
23     char szCopyCmd[DIR_FILE_LEN] = {0};
24     char szAbsNewFile[DIR_FILE_LEN] = {0};
25     char szSuffix[12] = {0};
26     sscanf(pszFileName, "%*[^.].%c", szSuffix);
27     if('h' == szSuffix[0])       //文件扩展名为.h,表明为头文件
28         sprintf(szAbsNewFile, "%s/%s", ptNewDirTree->szIncDir, pszFileName);
29     else if('c' == szSuffix[0])  //文件扩展名为.c,表明为源文件
30         sprintf(szAbsNewFile, "%s/%s", ptNewDirTree->szSrcDir, pszFileName);
31     else
32     {
33         printf("Unknown extension of file: %s!\n", pszAbsFile);
34         return 0;
35     }
36     snprintf(szCopyCmd, sizeof(szCopyCmd), "cp -p %s %s", pszAbsFile, szAbsNewFile);
37     system(szCopyCmd); //将头文件拷贝至Include目录,源文件拷贝至Source目录
38 
39     return 0;
40 }

     MoveFile()函数内首先跳过Include和Source目录。该步骤存在冗余性(每次文件操作均需执行该判断),但作为回调函数除此之外别无他法。

     最后,main()函数内容如下:

 1 int main(int dwArgc, char *pArgv[])
 2 {
 3     int dwRet = -1;
 4 
 5     if(dwArgc != 2)
 6     {
 7         fprintf(stderr, "Usage: %s ['TimetobeCompared']\n"
 8                         "  ['TC']Substring of time string(Format='Mon Aug 18 09:59:46 2014')\n"
 9                         "  e.g. %s 'Mon Aug 18' -->"
10                         "  Match files whose access time is 2014-8-18\n", pArgv[0], pArgv[0]);
11         return -1;
12     }
13 
14     T_DIR_TREE tDirTree = {{0}};
15     char *pszCurDir = getcwd(NULL, DIR_LEN);
16     snprintf(tDirTree.szIncDir, sizeof(tDirTree.szIncDir), "%s/include", pszCurDir);
17     
18     dwRet = MakeCleanDir(tDirTree.szIncDir, S_IRWXU); //创建Include目录
19     if(dwRet != 0)
20         return -1;
21 
22     snprintf(tDirTree.szSrcDir, sizeof(tDirTree.szSrcDir), "%s/source", pszCurDir);
23     dwRet = MakeCleanDir(tDirTree.szSrcDir, S_IRWXU); //创建Source目录
24     if(dwRet != 0)
25         return -1;
26 
27     strcpy(gszCmpTime, pArgv[1]);
28     dwRet = TraverseDirectory(pszCurDir, &tDirTree, MoveFile);
29     printf("Rearrange Directory %s!\n", dwRet?"Incorrectly":"Successfully");
30 
31     return dwRet;
32 }

 

四  效果验证

     按照如下步骤进行代码重组:

     1) 编译上节给出的代码,生成可执行文件(假设名为DirMover)。

     2) 将该文件置入SDK代码根目录下,即subdir1(.c,.h)…subdirN(.c,.h)…Makefile…DirMover。

     3) 编译SDK代码。若SDK代码创建时间早于当日,则“指定时间”可设置为当日,格式形如"Mon Aug 18";否则若SDK代码创建时间早于当时,则可date命令查看当前时间,或通过stat命令查看必被编译的文件的访问时间,然后调整“指定时间”的精度。

     4) 运行./DirMover 'Mon Aug 18 17'之类的命令,即可将SDK在用的文件重组分置于自动生成的include-source目录内。

     其中,步骤3也可在其他步骤之前完成。此时,“指定时间”需根据必被编译的文件的访问时间而定。

     注意,'Mon Aug 18 17'必须用单引号或双引号括起,以便被Shell识别为一个命令行参数。

     当然,DirMover.c内也可调用time()函数获取time_t格式的系统当前时间,然后直接与文件的atime值做比较。命令行可输入数字指定时间精度,代码内根据精度要求调整当前时间(如除以3600转换为小时精度)。这样,易用性更好,但灵活性有所降低。

 

五  注解

     Unix系统为每个文件维护三个时间属性,其意义如下表所示:

时间属性

说明

作用函数

ls(-l)示例

st_atime

文件数据的最后访问时间

creat/open(O_CREAT), exec, mkfifo, mknod, pipe, utime, read

ls -lu file

st_mtime

文件数据的最后修改时间

creat/open(O_CREAT|O_TRUNC), mkfifo, mknod, pipe, utime, truncate/ftruncate, write

ls -l file

st_ctime

i节点状态的最后更改时间

chmod/fchmod, chown/fchown, creat/open(O_CREAT), link/unlink, mkfifo, mknod, pipe, remove, rename, truncate/ftruncate, utime, write

ls -lc file

     以下详述三种时间的区别:

     1) 访问时间(access time):表示文件中数据最近的访问时间(last accessed time)。当读取或执行文件时,如被系统进程直接使用或通过cat/ more等命令和脚本间接使用,该时间将被更新。ls和stat命令不会修改文件的访问时间。

     2) 修改时间(modification time):表示文件中数据最近的修改时间(last modified time)。当修改文件内容时,如向文件中写入数据,该时间将被更新。

     3) 更改时间(change time):表示文件i节点状态最近的修改时间(last i-node's status changed time)。当文件的访问权限、所有者、链接数等发生改变时,该时间将被更新。因为i节点中的所有信息与文件的实际内容分开存放,故此时并未更改文件内容。当然,修改文件内容时,修改时间和更改时间均会改变。

     创建新文件时,访问时间、修改时间和更改时间三者一致。读取该文件时会更新访问时间,但其修改时间和更改时间并不改变(文件本身及文件相关的信息没有被改变)。此外,系统并不保存对一个i节点的最后一次访问时间,故access和stat函数并不改变这三个时间中的任一个。 

     Windows文件有三种时间属性,即创建时间、修改时间和访问时间。而Unix中没有文件创建时间的概念。若文件创建后内容未曾修改,则修改时间等同创建时间;若文件创建后状态未曾改动,则更改时间等同创建时间;若文件创建后内容未曾读取,则访问时间等同创建时间。但通常难以判断文件是否被改过、读过、其状态是否变过,因此需要变通的方法来实现保留文件创建时间。 

     可在挂载(mount)文件系统时使用参数-o noatime,关闭系统更新atime的特性。这样,访问文件时atime就不会更新,即等同文件的创建时间。此外,在文件读操作很频繁的系统中,atime更新所带来的开销很大,因此使用noatime属性可改善文件读写性能。

     但有些程序需要根据atime进行一些判断和操作,因此Linux2.6.29后默认集成relatime属性。使用该属性挂载文件系统后,仅当mtime比atime时间更新时,才更新atime(此时atime等同mtime)。

     查看文件这三种时间的命令有:

     1) ls命令

     ls命令按三个时间值中的一个排序进行显示。系统默认(使用-l或-t选项)按文件修改时间的先后排序,-u选项使其按访问时间顺序排序,-c选项使其按更改状态时间排序。

     使用--time和--full-time选项可查看更详细的时间信息。其中,--time取值可为atime或ctime,不指定该选项时默认为修改时间。

     此外,可格式化输出三种时间值。格式化字符串形如"%AY-%Am-%Ad %AH:%AM:%AS"(访问时间),将字符串中的字符'A'改为'T'或'C'即可格式化修改时间和更改时间。

1  [wangxiaoyuan_@localhost SDK573]$ ls -lu Makefile
2 -rwxr--r-- 1 wangxiaoyuan_ users 4963 Aug 20 09:48 Makefile
3  [wangxiaoyuan_@localhost SDK573]$ ls -l --time=atime --full-time Makefile
4 -rwxr--r-- 1 wangxiaoyuan_ users 4963 2014-08-20 09:48:43.000000000 +0800 Makefile
5 [wangxiaoyuan_@localhost SDK573]$ find . -name  Makefile -printf "%AY-%Am-%Ad %AH:%AM:%AS"
6 2014-08-20 09:48:43(无换行符)

     2) stat命令

1 [wangxiaoyuan_@localhost SDK573]$ stat Makefile
2   File: `Makefile'
3   Size: 4963            Blocks: 16         IO Block: 4096   regular file
4 Device: 811h/2065d      Inode: 25640989    Links: 1
5 Access: (0744/-rwxr--r--)  Uid: (  540/wangxiaoyuan_)   Gid: (  100/   users)
6 Access: 2014-08-20 09:48:43.000000000 +0800
7 Modify: 2014-07-08 18:29:04.000000000 +0800
8 Change: 2014-08-19 12:14:41.000000000 +0800

     可通过stat *命令查看当前目录所有文件的时间信息。

 

posted @ 2014-08-20 11:47  clover_toeic  阅读(1181)  评论(0编辑  收藏  举报