使用libev監視資料夾下檔案(夾)屬性變動的方案和實現
我們先看個最簡單方案,下面的程式碼會監視/home/work下檔案(夾)的新增、刪除等操作。
void call_back(ev::stat &w, int revents) { std::cout << "watch " << w.path << std::endl; } int main() { ev::default_loop loop; ev::stat state; state.set(call_back); state.set(loop); state.start("/home/work/"); loop.run(); return 0; }
第6行,我們使用了預設的loop。除了default_loop,libev還提供了dynamic_loop。如果我們沒有指定loop,則libev會使用預設的。
第7行,我們聲明瞭檔案(夾)監視器state。
第8行,將回調函式call_back和監視器關聯。
第9行,將loop和監視器關聯。
第10行,監視器開始監視目錄/home/work。
第11行,讓loop執行起來以阻塞住程序。
這樣一旦目錄下有檔案(夾)變動,則會呼叫回撥函式call_back。
假如這種方式可以涵蓋所有情況,那麼也不會存在這篇博文了。因為上述方案存在如下缺陷:
- 堵塞主執行緒
- call_back的stat::path一直指向被監視的檔案(夾)路徑。這樣在監控一個資料夾時,如果有子檔案(夾)新增或者刪除,我們都將無法從回撥函式中得知變動的是誰。
- 如果監視一個資料夾時發生子檔案的複製覆蓋行為,將監視不到。
第1個問題並不嚴重,我們只要啟動一個執行緒便可解決。第2個問題,我們可以通過對比變動前後的目錄結構去解決,也不算太複雜。第3個問題則比較嚴重了。要解決第三個問題,我們需要對資料夾的監視精細到具體的檔案級別,也就是說不是籠統的對某個目錄進行監視,而是還要對目錄下每個檔案進行監視。
於是對一個資料夾的監視,需要做到:
- 監視該資料夾,以獲取新增檔案(夾)資訊。
- 監視該檔案下所有檔案,以獲取複製覆蓋資訊。
- 對於新增的檔案,需要新增監視。
- 對於刪除的檔案,需要刪除監視。
- 對於資料夾監視器和檔案監視器重複上報的行為(刪除檔案)需要去重處理。
由於loop會堵塞住執行緒,所以我們讓一個loop佔用一個執行緒。多個監視器可關聯到一個loop。但是監視器和loop的關係存在如下情況:
- 如果有多個監視器關聯到一個loop,則一個監視器停止後,loop仍會堵塞住執行緒。
- 如果只有一個監視器關聯到loop,那這個監視器停止後,loop會從堵塞狀態中跳出。
我希望監視器可以關聯到同一個loop,於是對loop做了如下封裝
class LibevLoop { public: LibevLoop(); ~LibevLoop(); template<class T> friend void bind(T& a, LibevLoop* b); public: void run_loop(); private: ev::dynamic_loop loop_; std::timed_mutex timed_mutex_; }; template<class T> void bind(T& a, LibevLoop* b) { a.set(b->loop_); } LibevLoop::~LibevLoop() { loop_.break_loop(ev::ALL); } void LibevLoop::run_loop() { if (timed_mutex_.try_lock_for(std::chrono::milliseconds(1))) { std::thread t([this]{ timed_mutex_.lock(); loop_.run(); timed_mutex_.unlock(); }); t.detach(); timed_mutex_.unlock(); } }
由於ev::dynamic_loop是內部管理物件,我並不希望暴露出它,於是提供了一個友元函式bind供外部使用。其實這個地方使用模板函式並不是很合適,最好是針對具體類的方法。
run_loop函式內部使用超時鎖檢測loop是否在執行,從而可以保證各個執行緒呼叫該函式時只有一個執行緒被執行。
我們再封裝了一個監視器類
class Watcher { public: using callback = std::function<void(ev::stat&, int)>; Watcher() = delete; explicit Watcher(const std::string& path, callback c, LibevLoop* loop = nullptr); ~Watcher(); private: void callback_(ev::stat &w, int revents); private: callback cb_; ev::stat state_; }; Watcher::Watcher(const std::string& path, callback c, LibevLoop* loop) { if (!loop) { static LibevLoop loop_; loop = &loop_; } cb_.swap(c); state_.set<Watcher, &Watcher::callback_>(this); bind(state_, loop); state_.start(path.c_str()); loop->run_loop(); } Watcher::~Watcher() { state_.stop(); } void Watcher::callback_(ev::stat &w, int revents) { cb_(w, revents); }
Watcher的建構函式執行的是文中最開始給出的libev的呼叫過程。區別是loop被替換為之前定義的LibevLoop,從而不會在該步堵塞執行緒。
現在我們可以實現監視器中最基礎的檔案監視器。
class FileWatcher { public: using callback = std::function<void(const std::string& path, FileWatcherAction action)>; FileWatcher() = delete; ~FileWatcher(); explicit FileWatcher(const std::string& path, callback cb, LibevLoop* loop = nullptr); private: void watch_(ev::stat&, int); private: callback cb_; std::string file_path_; std::time_t last_write_time_ = 0; std::shared_ptr<Watcher> watcher_; }; FileWatcher::~FileWatcher() { } FileWatcher::FileWatcher(const std::string& path, callback cb, LibevLoop* loop) { file_path_ = absolute(path); cb_ = std::move(cb); if (boost::filesystem::is_directory(file_path_)) { return; } if (boost::filesystem::is_regular_file(file_path_)) { last_write_time_ = boost::filesystem::last_write_time(file_path_); } watcher_ = std::make_shared<Watcher>(file_path_, std::bind(&FileWatcher::watch_, this, std::placeholders::_1, std::placeholders::_2), loop); } void FileWatcher::watch_(ev::stat &w, int revents) { if (!boost::filesystem::is_regular_file(file_path_)) { if (last_write_time_ != 0) { cb_(file_path_, FILE_DEL); } return; } std::time_t t = boost::filesystem::last_write_time(file_path_); if (last_write_time_ != t) { FileWatcherAction ac = (last_write_time_ == 0) ? FILE_NEW : FILE_MODIFY; cb_(file_path_, ac); } }
由於libev需要監視的路徑是絕對路徑,所以FileWatcher函式會先通過absolute函式修正路徑。
std::string absolute(const std::string& path) { if (boost::filesystem::path(path).is_absolute()) { return path; } std::string absolute_path = boost::filesystem::system_complete(path).string(); return absolute_path; }
然後獲取該檔案的最後修改時間。
FileWatcher::watch_函式是回撥函式,它一開始檢測檔案是否存在,如果不存在且之前存在(最後修改時間不為0),則發起通知。如果檔案存在,則通過通過對比最後修改時間來確定發生的行為是“新增”還是“修改”。
接下來就要接觸到比較複雜的資料夾監視。之前我們提到過,需要對目錄下所有檔案進行監視,並且需要遍歷整個目錄以確定新增的是哪個檔案。於是就設計了一個遍歷目錄的方法
using callback = std::function<void(const std::string&)>; void folder_scan(const std::string& path, callback file_cb, callback folder_cb) { if (!boost::filesystem::is_directory(path)) { return; } if (!boost::filesystem::exists(path)) { return; } boost::filesystem::directory_iterator it(path); boost::filesystem::directory_iterator end; for (; it != end; it++) { if (boost::filesystem::is_directory(*it)) { folder_cb(it->path().string()); folder_scan(it->path().string(), file_cb, folder_cb); } else { file_cb(it->path().string()); } } }
folder_scan方法提供了兩個回撥,一個是在掃描到檔案時呼叫,一個是掃描到資料夾時呼叫。
對比資料夾下檔案(夾)新增的類將使用上述方法實現對比操作。
enum PathType { E_FILE = 0, E_FOLDER, }; struct PathInfo { std::string path; PathType type; bool operator < (const PathInfo & right) const { return path.compare(right.path) < 0; } }; using PathInfoSet = std::set<PathInfo>; class FolderDiff { public: explicit FolderDiff(const std::string& path); void diff(PathInfoSet & add, PathInfoSet & remove); private: void scan_(const std::string& path, PathType type, PathInfoSet& path_infos); private: std::string folder_path_; PathInfoSet path_infos_; }; FolderDiff::FolderDiff(const std::string& path){ folder_path_ = absolute(path); PathInfoSet path_infos; folder_scan(folder_path_, std::bind(&FolderDiff::scan_, this, std::placeholders::_1, E_FILE, std::ref(path_infos_)), std::bind(&FolderDiff::scan_, this, std::placeholders::_1, E_FOLDER, std::ref(path_infos_))); } void FolderDiff::scan_(const std::string& path, PathType type, PathInfoSet& path_infos) { PathInfo pi{path, type}; path_infos.insert(pi); } void FolderDiff::diff(PathInfoSet & add, PathInfoSet & remove) { PathInfoSet path_infos; folder_scan(folder_path_, std::bind(&FolderDiff::scan_, this, std::placeholders::_1, E_FILE, std::ref(path_infos)), std::bind(&FolderDiff::scan_, this, std::placeholders::_1, E_FOLDER, std::ref(path_infos))); std::set_difference(path_infos.begin(), path_infos.end(), path_infos_.begin(), path_infos_.end(), std::inserter(add, add.begin())); std::set_difference(path_infos_.begin(), path_infos_.end(), path_infos.begin(), path_infos.end(), std::inserter(remove, remove.begin())); path_infos_ = path_infos; }
Folder::diff方法將計算出和之前目錄狀態的對比結果。
FolderWatcher是最終實現資料夾監視的類。它的建構函式第8行構建了一個資料夾對比類;第10行遍歷整個目錄,對目錄下資料夾和檔案設定監視器。由於子資料夾不用監視,所以資料夾監視函式watch_folder_實際什麼都沒幹。第14行啟動了path路徑資料夾監視器。
FolderWatcher::FolderWatcher(const std::string& path, callback c, LibevLoop* loop) { folder_path_ = absolute(path); if (boost::filesystem::is_regular_file(folder_path_)) { return; } cb_ = std::move(c); fdiff_ = std::make_shared<FolderDiff>(folder_path_); folder_scan(folder_path_, std::bind(&FolderWatcher::watch_file_, this, std::placeholders::_1), std::bind(&FolderWatcher::watch_folder_, this, std::placeholders::_1)); watcher_ = std::make_shared<Watcher>(folder_path_, std::bind(&FolderWatcher::watch_, this, std::placeholders::_1, std::placeholders::_2), loop); } void FolderWatcher::watch_folder_(const std::string& path) { }
對每個子檔案的監視使用watch_file_回撥,它在底層使用了之前定義的FileWatcher檔案監視器類。
void FolderWatcher::watch_file_(const std::string& path){ std::unique_lock<std::mutex> lock(mutex_); files_last_modify_time_[path] = boost::filesystem::last_write_time(path); file_watchers_[path] = std::make_shared<FileWatcher>(path, std::bind(&FolderWatcher::file_watcher_, this, std::placeholders::_1, std::placeholders::_2)); } void FolderWatcher::file_watcher_(const std::string& path, FileWatcherAction action) { PathInfo pi{path, E_FILE}; WatcherAction ac = (WatcherAction)action; notify_filewatcher_change_(pi, ac); }
對主目錄的監視使用watch_回撥函式,它內部是通過之前定義的FolderDiff類實現的。
void FolderWatcher::watch_(ev::stat &w, int revents) { PathInfoSet add; PathInfoSet remove; fdiff_->diff(add, remove); for (auto& it : add) { notify_folderwatcher_change_(it, true); } for (auto& it : remove) { notify_folderwatcher_change_(it, false); } } void FolderWatcher::notify_folderwatcher_change_(const PathInfo& pi, bool add) { if (pi.type == E_FOLDER) { cb_(pi, add ? NEW : DEL); } else { notify_filewatcher_change_(pi, add ? NEW : DEL); } }
如果新增的資料夾,則直接呼叫回撥函式;否則使用notify_filewatcher_change方法去通知。
notify_filewatcher_change方法比較複雜,它底層呼叫的change_filewatchers_方法根據檔案的新增和刪除來管理檔案監視器。
void FolderWatcher::change_filewatchers_(const std::string& path, WatcherAction action) { if (action == DEL) { std::unique_lock<std::mutex> lock(mutex_); auto it = file_watchers_.find(path); if (it != file_watchers_.end()) { file_watchers_.erase(it); } } else if (action == NEW) { std::unique_lock<std::mutex> lock(mutex_); auto it = file_watchers_.find(path); if (it != file_watchers_.end()) { file_watchers_.erase(it); } file_watchers_[path] = std::make_shared<FileWatcher>(path, std::bind(&FolderWatcher::file_watcher_, this, std::placeholders::_1, std::placeholders::_2)); } }
由於對於檔案的刪除行為,檔案監視器和資料夾監視器都會上報,所以需要對其進行去重。於是我們使用最後修改時間來做統一。
void FolderWatcher::notify_filewatcher_change_(const PathInfo& pi, WatcherAction action) { change_filewatchers_(pi.path, action); bool notify = true; if (action == DEL) { std::unique_lock<std::mutex> lock(mutex_); auto it = files_last_modify_time_.find(pi.path); if (it == files_last_modify_time_.end()) { notify = false; } else { files_last_modify_time_.erase(it); } } else if (action == NEW || action == MODIFY) { std::unique_lock<std::mutex> lock(mutex_); auto it = files_last_modify_time_.find(pi.path); if (it != files_last_modify_time_.end()) { if (boost::filesystem::last_write_time(pi.path) == it->second) { notify = false; } } else { files_last_modify_time_[pi.path] = boost::filesystem::last_write_time(pi.path); } } if (notify) { cb_(pi, action); } }
最後需要指出的是,這套程式碼,在不同的作業系統上有不一致的行為。比如在Centos上,如果我們監視一個不存在的檔案路徑,然後新建該檔案,則會發起通知。而在Ubuntu上,該行為則監視不到。但是這個差異也可以理解。
最後附上程式碼庫,其中的單元測試基於Centos的。https://github.com/f304646673/filewatcher.git