本文主要介绍巧妙利用访问时间提取和重组某嵌入式产品SDK代码的实践。
目前产品平台为便于编译管理,要求各模块组织为include-source目录结构,分别存放头文件和源文件。但芯片厂家提供的SDK按功能划分为众多子目录,如subdir1(.c,.h)…subdirN(.c,.h)…Makefile,并将头文件和源文件一并存放在各子目录内。
此外,厂家SDK支持多种管理场景,通过选项开关编译不同的目录和文件。而产品硬件定板后管理场景固定,其他场景所涉及的代码将不再需要。
因此,需要遍历厂家SDK目录,提取在用的文件并重组为include-source目录结构。
根据重组要求,有四种解决方案。
某些产品已将厂家SDK重组为include-source目录结构。因此,可人工比对现有产品已重组的SDK,从新的SDK中摘取所需文件,重组后再次编译,根据编译结果进行必要的修改和调整。
显然,该方案效率很低(尤其是甄别不同场景所用的同名文件时),而且不安全(可能遗漏SDK新增内容)。
编译厂家SDK后,解析编译输出中出现的文件(表明在用),手工或脚本提取后重组。
该方案解析难度稍大,且需考虑重复出现的文件(尤其是嵌套包含的头文件出现编译警告时)。
可参考《将模块代码量精简为2%的实践》一文中剔除未使用条件编译分支的思路,稍加修改即可用于解决本文所提问题。
其原理如下:
1) 创建include和source目录。
2) 在SDK待处理代码(所有源文件和头文件)的首行插入gcc扩展预编译头#warning。
3) 编译待处理代码获取gcc编译输出并进行分析。
4) 编译结果中”#warning”警告所涉及的文件表明在用,可删除编译头后按源文件和头文件类型分别拷贝至include和source目录。
其中,步骤1和3可手工进行,步骤2和4对《实践》一文中提供的Python脚本稍加修改即可实现。
编译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 *命令查看当前目录所有文件的时间信息。