配置文件动态刷新
目录
1. 背景
2. 相关知识储备
思路一: 民科 mtime 文件最后修改时间
思路二: 科班 操作系统通知特性, 例如 linux 的 inotify
3. 相关代码设计
3.1 简单实用版
3.2 尝试多线程
3.3 多线程版本
4. 总结
正文
1. 背景
配置文件动态刷新这个业务场景非常常见. 存在两个主要使用场景, 客户端和服务器.
客户端需求很直白, 我本地配置变更, 程序能及时和非及时的重刷到系统中.
服务器相比客户端做法要多些环节, 服务器本地会有一份配置兜底, 配置中心中配置发生改变会推送给触发给服务器触发内部更新操作.
我们这里主要聊场景偏向于客户端, 本地配置发生改变, 我们如何来更新内存中配置.
文章承接于: C中级 - 文件辅助操作
2. 相关知识储备
首先思考一个问题我们如何判断一个文件发生了更新 ?
这里提供两种思路.
思路一: 民科 mtime 文件最后修改时间
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 命令.
思路二: 科班 操作系统通知特性, 例如 linux 的 inotify
man inotify
inotify_init1 -> inotify_add_watch IN_MODIFY / inotify_rm_watch -> poll 监控机制 -> close
IN_MODIFY (+) File was modified (e.g., write(2), truncate(2)).
流程去注册关注修改时间, 当文件发生修改时候操作系统会通知上层应用具体修改详情.
linux inotify 是一种文件变化通知机制, 它是一个内核用于通知用户空间程序文件系统变化的机制,
以便用户态能够及时地得知内核或底层硬件设备发生了什么.
我们这里采用民科思路. linux inotify 对于我们场景有点大材小用了. 欢迎感兴趣人参照官方例子去尝试.
3. 相关代码设计
3.1 简单实用版
素材:
业务能力设计 file_set 注册和删除, file_update 触发检查和更新操作
#pragma once #include "struct.h" #include "strext.h" // // file_f - 文件更新行为 // typedef void (* file_f)(FILE * c, void * arg); // // file_set - 文件注册更新行为 // path : 文件路径 // func : NULL 标记清除, 正常 update -> func(path -> FILE, arg) // arg : func 额外参数 // return : void // extern void file_set(const char * path, file_f func, void * arg); // // file_update - 配置文件刷新操作 // return : void // extern void file_update(void);
具体思路是利用 list + mtime , 可以观察 struct 设计部分
#include "file.h" struct file { time_t last; // 文件最后修改时间点 char * path; // 文件全路径 unsigned hash; // 文件路径 hash 值 file_f func; // 执行行为 void * arg; // 行为参数 struct file * next; // 文件下一个结点 }; static struct file * file_create(const char * path, unsigned h, file_f func, void * arg) { assert(path && func); if (fmtime(path) == -1) { RETURN(NULL, "mtime error p = %s", path); } struct file * fu = malloc(sizeof(struct file)); if (NULL == fu) { return NULL; } fu->last = -1; fu->path = strdup(path); if (NULL == fu->path) { free(fu); return NULL; } fu->hash = h; fu->func = func; fu->arg = arg; // fu->next = NULL; return fu; } inline void file_delete(struct file * fu) { free(fu->path); free(fu); } static struct files { struct file * list; // 当前文件对象集 } f_s; // files add static void f_s_add(const char * path, unsigned hash, file_f func, void * arg) { struct file * fu = file_create(path, hash, func, arg); if (fu == NULL) { return; } // 直接插入到头结点部分 fu->next = f_s.list; f_s.list = fu; }
struct file 存储文件操作对象, struct files 是 struct file list 集合.
3.2 尝试多线程
我们知道 file_set 和 file_update 不是线程安全的. 依赖业务系统启动时候统一调用 file_set 无法运行时修改相关设置.
不知道是否有同学会采用如下设计
static struct files { atomic_flag lock; struct file * list; } f_s;
通过 lock 来保证线程安全.
这种思路确实能解决线程安全问题, 存在很多缺陷, file_update 业务上面很耗时, 他会阻塞 file_set 操作, 特殊情况会引发业务雪崩.
所以我们需要更针对性锁.
3.3 多线程版本
为了适配多线程情况. 首先我们明确下简单业务, 同步的 file list 就够用了.
我们这里单纯为了没事要吃蛋炒饭态度, 构造 file dict hash + atomic lock 来没事找事.
素材:
总体设计思路
#include "file.h" struct file { time_t last; // 文件最后修改时间点 file_f func; // 执行行为 void * arg; // 行为参数 }; static struct file * file_create(const char * path, file_f func, void * arg) { assert(path && func); if (fmtime(path) == -1) { RETURN(NULL, "mtime error p = %s", path); } struct file * fu = malloc(sizeof(struct file)); if (NULL == fu) { return NULL; } fu->last = -1; fu->func = func; fu->arg = arg; return fu; } static inline void file_delete(struct file * fu) { free(fu); } struct files { atomic_flag data_lock; // const char * path key -> value struct file // 用于 update 数据 volatile dict_t data; atomic_flag backup_lock; // const char * path key -> value struct file // 在 update 兜底备份数据 volatile dict_t backup; }; static struct files F = { .data_lock = ATOMIC_FLAG_INIT, .backup_lock = ATOMIC_FLAG_INIT, }; extern void file_init() { F.data = dict_create(file_delete); F.backup = dict_create(file_delete); }
我们先在 data 中添加数据, 如果 data 被 update 占用, 我们把数据放入 backup 中再去处理.
// // file_set - 文件注册更新行为 // path : 文件路径 // func : NULL 标识清除, 正常 update -> func(path -> FILE, arg) // arg : func 额外参数 // return : void // void file_set(const char * path, file_f func, void * arg) { struct file * fu = NULL; assert(path && *path); // step 1 : 尝试竞争 data lock if (atomic_flag_trylock(&F.data_lock)) { if (NULL != func) { fu = file_create(path, func, arg); } dict_set(F.data, path, fu); return atomic_flag_unlock(&F.data_lock); } // step 2 : data lock 没有竞争到, 直接竞争 backup lock atomic_flag_lock(&F.backup_lock); fu = file_create(path, func, arg); dict_set(F.backup, path, fu); atomic_flag_unlock(&F.backup_lock); }
4. 总结
去感受其中思路. 我用C写代码很顺手. 但有时候觉得 C 在现在阶段, 不是专业吃这个饭的,
可以尝试用其它更加高级语言来轻松快捷表达自己的想法和工程版本.
对于开发生涯我花了很多年找到自己定位, 我的底层核心是一名软件工程师. 然后语言和技术以及商业工程问题陆续通顺起来了.
(因为我的单元测试不充分, 错误可能很多, 欢迎在 github 给我提 commit or issure. 时间愉快)