C 中级 - 文件辅助操作
程序员文章站
2022-04-09 20:16:46
引言 业务有点麻烦 C 功能很强大, 同样书写起来会谨慎(拖泥带水). 不妨通过一个小问题来描述裹脚的 C 需求: 用 C 创建一个文件! 难点在于 1. 文件路径切割成 目录 + 文件名 2. 目录 多级创建 3. 跨平台 这个问题也挺适合做线下面试作业. 本文围绕下面几个重点讲述 1. 获取文件 ......
引言 - 业务有点麻烦
C 功能很强大, 同样书写起来会谨慎(拖泥带水). 不妨通过一个小问题来描述裹脚的 C 需求: 用 C 创建一个文件! 难点在于 1. 文件路径切割成 目录 + 文件名 2. 目录 多级创建 3. 跨平台 这个问题也挺适合做线下面试作业. 本文围绕下面几个重点讲述 1. 获取文件更新时间 2. 目录创建和文件删除 3. 配置自动更新 应对平台是 Winds cl 和 Linux gcc :) 在开始之前, 先介绍一些文件辅助基础函数, 构造了两个部分原始的 mkdir 和 mtime 函数. mkdir 在 shell 中用于构建目录我们很熟悉. 对于 mtime 是 Linux 对于文件最后修改时间. 最初来自于下面文件详细信息结构中
struct stat { unsigned long st_dev; /* Device. */ unsigned long st_ino; /* File serial number. */ unsigned int st_mode; /* File mode. */ unsigned int st_nlink; /* Link count. */ unsigned int st_uid; /* User ID of the file's owner. */ unsigned int st_gid; /* Group ID of the file's group. */ unsigned long st_rdev; /* Device number, if device. */ unsigned long __pad1; long st_size; /* Size of file, in bytes. */ int st_blksize; /* Optimal block size for I/O. */ int __pad2; long st_blocks; /* Number 512-byte blocks allocated. */ long st_atime; /* Time of last access. */ unsigned long st_atime_nsec; long st_mtime; /* Time of last modification. */ unsigned long st_mtime_nsec; long st_ctime; /* Time of last status change. */ unsigned long st_ctime_nsec; unsigned int __unused4; unsigned int __unused5; };
通过上面 st_atime, st_mtime, st_ctime 字段可以知道, Linux 文件有三个时间属性: 1. mtime: 文件内容最后修改时间 2. ctime: 文件状态改变时间, 如权限, 属性被更改 3. atime: 文件内容被访问时间, 如 cat, less 等 在默认情况下, ls 显示出来的是该文件的 mtime, 即文件内容最后修改时间. 如果你需要查看另外两个时间, 可以使用 ls -l --time ctime 命令. 有了上面小知识, 外加上先入为主的设计思路, 这里构建了代码前戏部分
#ifndef _H_FILE #define _H_FILE #include <stdio.h> #include <stdlib.h> #include <string.h> #ifdef __GNUC__ #include <fcntl.h> #include <unistd.h> #include <sys/stat.h> #include <sys/types.h> // // mkdir - 通用的单层目录创建函数宏, 等价于 mkdir path // path : 目录路径加名称 // return : 0表示成功, -1表示失败, 失败原因都在 errno // #undef mkdir #define mkdir(path) \ mkdir(path, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) // // mtime - 得到文件最后修改时间 // path : 文件名称 // return : 返回时间戳, -1 表示错误 // inline time_t mtime(const char * path) { struct stat ss; // 数据最后的修改时间 return stat(path, &ss) ? -1 : ss.st_mtime; } #endif #ifdef _MSC_VER #include <io.h> #include <direct.h> #include <windows.h> // int access(const char * path, int mode /* 四个检测宏 */); #ifndef F_OK # define F_OK (0) #endif #ifndef X_OK # define X_OK (1) #endif #ifndef W_OK # define W_OK (2) #endif #ifndef R_OK # define R_OK (4) #endif inline time_t mtime(const char * path) { WIN32_FILE_ATTRIBUTE_DATA wfad; if (!GetFileAttributesEx(path, GetFileExInfoStandard, &wfad)) return -1; // 基于 winds x64 sizeof(long) = 4 return *(time_t *)&wfad.ftLastWriteTime; } #endif #endif
原始的 mkdir 仿照 Winds 上面的 mkdir 接口设计.
_Check_return_ _CRT_NONSTDC_DEPRECATE(_mkdir) _ACRTIMP int __cdecl mkdir( _In_z_ char const* _Path );
最终行为和 Linux 的 mkdir 创建目录命令一致(0665,即-rw-rw-r-x). mtime 得到文件最后修改时间. 主要用于获取时间变化促使配置自动更新. F_OK, X_OK, W_OK, R_OK 在 Winds 上实现 access 接口缺少的宏定义. 更加具体可以参照
前言 - 文件辅助操作
前言部分可能最精华, 开始步入正轨 ~
// // mkdirs - 创建多级目录 // path : 目录路径 // return : < 0 is error, 0 is success // int mkdirs(const char * path) { char c, * p, * s; // 参数错误直接返回 if (!path || !*path) return -2; // 文件存在 or 文件一次创建成功 直接返回 if (!access(path, F_OK) || !mkdir(path)) return 0; // 跳过第一个 ['/'|'\\'] 检查是否是多级目录 p = (char *)path; while ((c = *++p) != '\0') if (c == '/' || c == '\\') break; if (c == '\0') return -1; // 开始循环构建多级目录 s = p = strdup(path); while ((c = *++p) != '\0') { if (c == '/' || c == '\\') { *p = '\0'; if (access(s, F_OK)) { // 文件不存在, 开始创建, 创建失败直接返回错误 if (mkdir(s)) { free(s); return -1; } } *p = c; } } // 最后善尾 c = p[-1]; free(s); if (c == '/' || c == '\\') return 0; // 剩下最后文件路径, 开始构建 return mkdir(path) ? -1 : 0; }
上面 mkdirs 一个具有实战意义的跨平台多级目录创建函数接口设计. 核心思路在于 1. 直接 mkdir 成功就返回 2. 分级 切割 路径, 循环 mkdir 有了多级目录构建操作, 那顺路写个多级目录的删除操作.
// // removes - 删除非空目录 or 文件 // path : 文件全路径 // return : < 0 is error, >=0 is success // inline int removes(const char * path) { char s[BUFSIZ]; #ifndef STR_RMRF # ifdef _MSC_VER # define STR_RMRF "rmdir /s /q \"%s\"" # else # define STR_RMRF "rm -rf '%s'" # endif #endif // path 超过缓冲区长度, 返回异常 if (snprintf(s, sizeof s, STR_RMRF, path) == sizeof s) return -1; return access(path, F_OK) ? 0 : -system(s); }
如果你不希望存在输出内容在控制台上面可以使用这种类型操作
$ rm -rf '%s' >/dev/null 2>&1 or > rmdir /s /q "%s" 1>>dev.nil 2>&1 ; del dev.nul
这里保留了输出内容, 方便日志采集发现问题.
正文 - 配置自动刷新
生活不止眼前的苟且, 很庆幸来到这里. 但感觉作用不大. 随后讲述的内容是如何构造一个动态刷新的配置系统. 整体的设计有如下考虑 1. 生命周期跟随系统. 所以只有 set 和 update 2. 线程安全 3. set null 等同于 update delete 可以通过代码来看这样的好处和细节. 首先看整体的数据结构设计
struct file { time_t last; // 文件最后修改时间点 char * path; // 文件全路径 unsigned hash; // 文件路径 hash 值 file_f func; // 执行行为 void * arg; // 行为参数 struct file * next; // 文件下一个结点 }; static struct files { atom_t lock; // 当前对象原子锁 struct file * head; // 当前文件对象集 } _s;
其中随后会用到原子锁操作, 更加详细的看下面库的设计.
对于全局的 struct files _s 对象辅助函数 add 和 get 设计内涵见下面
// files add static void _add(const char * p, unsigned h, file_f func, void * arg) { struct file * fu; if (mtime(p) == -1) { RETNIL("mtime error p = %s", p); } fu = malloc(sizeof(struct file)); fu->last = -1; fu->path = strdup(p); fu->hash = h; fu->func = func; fu->arg = arg; // 直接插入到头结点部分 atom_lock(_s.lock); fu->next = _s.head; _s.head = fu; atom_unlock(_s.lock); } // files get static struct file * _get(const char * p, unsigned * r) { struct file * fu = _s.head; unsigned h = *r = str_hash(p); while (fu) { if (fu->hash == h && strcmp(fu->path, p) == 0) break; fu = fu->next; } return fu; }
线程安全的辅助直白代码. 其中 str_hash 引用的如下函数
// // str_hash - Brian Kernighan与 Dennis Ritchie 简便快捷的 hash算法 // str : 字符串内容 // return : 返回计算后的hash值 // unsigned str_hash(const char * str) { register unsigned h = 0u; if (str) { register unsigned c; while ((c = *str++)) h = h * 131u + c; } return h; }
随后开始进入核心业务, 先是要看更新操作
// // :0 一个和程序同生存的配置文件动态刷新机制 // file_f - 文件更新行为 // typedef void (* file_f)(FILE * c, void * arg); // // file_set - 文件注册触发行为 // path : 文件路径 // func : file update -> func(path -> FILE, arg), func is NULL 标记清除 // arg : 注入的额外参数 // return : void // void file_set(const char * path, file_f func, void * arg) { unsigned h; assert(path && *path); struct file * fu = _get(path, &h); if (NULL == fu) _add(path, h, func, arg); else { atom_lock(_s.lock); fu->last = -1; fu->func = func; fu->arg = arg; atom_unlock(_s.lock); } }
file_set 只负责在全局的 _s 对象中安全的插入数据, 其它业务什么都不管. 后面看详细的 file_update 操作
// // file_update - 更新注册配置解析事件 // return : void // void file_update(void) { struct file * fu; atom_lock(_s.lock); fu = _s.head; while (fu) { struct file * next = fu->next; if (NULL == fu->func) { // 删除的是头结点 if (_s.head == fu) _s.head = next; free(fu->path); free(fu); } else { time_t last = mtime(fu->path); if (fu->last != last && last != -1) { FILE * c = fopen(fu->path, "rb+"); if (NULL == c) { CERR("fopen rb+ error = %s.", fu->path); continue; } fu->last = last; fu->func(c, fu->arg); fclose(c); } } fu = next; } atom_unlock(_s.lock); }
file_update 前提也是线程安全的. 其次应对两个业务 delete 和 update. 其实从写过的角度而言. file_f 和 file_set 就已经决定了 file_update 具体设计了. 开始的时候 就已经决定了 能够达到的最好结尾 ~ (可能是代码写多了, 总感觉看 code 就够了, 说太多容易欲盖弥彰 :)
后记 - 也许是交作业
错误是难免的欢迎指正 ~ 我不哭我已经没有 ~ 尊严能放弃 ~