扩展 boost::filesystem 以支持 Windows 和 Linux:第一部分。






4.93/5 (15投票s)
扩展 boost::filesystem 以支持 Windows 和 Linux。
- 下载 FSHelper-VS2012.zip - 49.1 KB
- 下载 FSHelper Qt 项目 - 59.1 KB
- 下载适用于 Windows 和 Linux 的 FSHelper 应用程序 - 2.3 MB
引言
我是一名 .NET 开发者,已经有几年经验了,这是我第一次在 CodeProject 上发表文章(老实说,这是我写的第一篇文章!),而这篇文章的内容与 .NET Framework 没有任何关系。在过去的几年里,我主要从事 .NET 相关工作,因此我在这方面算是有一定经验。我之前也曾在一家公司从事过 Delphi 开发,所以我对非托管语言并不完全陌生。我最近的一份工作除了 .NET 外,还需要大量使用 C++,我必须熟悉并快速掌握它。这大约是一年前的事情。从那时起,我将大部分业余时间都投入到了学习 C++ 上。对我来说,这是一条艰难的道路,但我相信所有花费的时间最终都得到了回报,因为我现在写代码时,对“幕后”发生的事情有了比以前深刻得多的理解。
尽管如此,在习惯了 .NET Framework 提供的便利之后,我仍然觉得用 C++ 工作很困难。我发现,像在主流操作系统上处理文件系统这样的日常编程问题,竟然没有容易找到且覆盖全面的解决方案,这特别令人沮丧,所以我决定做些有用的事情——运用我新掌握的 C++ 技能来挑战一个更复杂的任务,并为这个我刚开始探索旅程时几乎每隔五分钟就会去查阅答案和解释的社区回馈一些东西。我希望你们也能觉得它有用。
使用代码
在本文中,您将找到一个 QT Creator 解决方案的下载链接,该解决方案可以使用 GCC 4.6.3 - 4.7.2 和 MinGW 4.6.2 进行编译,以及适用于 Linux (x64) 和 Windows (x86) 的完整、已编译的命令行应用程序,没有任何额外的依赖项。我的解决方案试图扩展 `boost::filesystem` 的功能,因此您需要 Boost 库源代码 1.47 - 1.52,并使用 `-std=c++0x` 或 `-std=c++11` (GCC ) 标志进行编译。为了向像我一样的 C++ 新手说明,我尝试使用静态链接进行编译,以避免任何额外的依赖项,就像这样
./b2 cxxflags="-std=c++0x" toolset=gcc link=static runtime-link=static
threading=multi --libdir="<path to your boost libraries folder>
" --includedir="<path to your boost include folder> "
您并不一定需要静态链接,那只是我当时的选择,但请记住 C++11 支持是必需的。下载的解决方案是一个完全可用的命令行/终端应用程序,它演示了我在主 `FileSystemController` 类中公开的功能。要将其用于您自己的项目,您不需要 `main.cpp`、`COMMAND_PROCESSOR.hpp` 和 `CustomLocale.hpp` 文件中的任何内容,因此您可以安全地排除它们。`COMMAND_PROCESSOR.hpp` 文件还包含 `FileSystemController` 类的用法示例,这是提供的代码下载的主要目标之一。总之,我试图为标准文件浏览器创建后端支持,包含所有基本功能,如复制、移动、删除、移至回收站/废纸篓、监视指定位置的变化以及搜索。我希望它能涵盖您在 Windows 或 Linux 上管理文件时可能需要的大部分功能。
我使用了 Qt Creator,它似乎是一个非常棒的工具,即使对于非 Qt 的 C++ 项目来说也是如此,而且它是免费使用的,最重要的是项目可以在不同平台之间几乎不加调整地使用。要自己编译项目,您需要在 Qt Creator 的 `.pro` 文件中设置 Boost 的 include/lib 路径。我相信当包含 `-std=C++0x` 标志时,您可能会在 Windows 上使用 MinGW 编译时遇到一些问题。在这种情况下,请使用 `--with=...` bjam 选项单独编译有问题的库,排除之前的 C++11 标志,然后使用它们,至少我就是这样做的,并且到目前为止没有遇到任何问题。在提供的 .pro 文件中,有两个注释掉的 `QMAKE_CXXFLAGS` 设置,您在不同平台(GCC-Linux 或 MinGW-Windows)上编译时也需要它们。要设置特定于平台的代码进行编译,请打开“*BaseDecl.h*”头文件,并取消注释/注释掉指示目标平台的适当 `#define` 语句(`#define WINDOWS` 或 `#define LINUX`)。
补充:我将之前的 Qt 项目适配为可以与 VS2012 编译,通过创建 VS 解决方案并修改代码,并添加了相应的下载链接。程序和 FileSystemController 依赖项的整体结构保持不变,只是移除了几个特定于 Linux 的文件,因为在这种情况下它们不需要了。如果可移植性是强制要求,您仍然可以使用之前提供的代码,该代码可以使用 GCC 或 MinGW 编译器在 Windows 和 Linux 平台上编译。请记住,您仍然需要使用 VC++ 编译的 Boost 库,并在项目属性中修改“附加包含目录”、“附加库目录”和“附加依赖项”(如果编译时 Boost 命名约定发生更改),以使提供的 VS 项目正常工作。
最后,我需要提到的是,我已经测试了 `FileSystemController` 类(以及 FSHelper 应用程序)在 Windows XP SP3 到 Windows 8 的版本(不包括 Vista,但我不认为它在那里不起作用),以及多个 Linux 发行版,如 OPEN SUSE 12.2 (kernel:3.4), Fedora 17 (kernel:3.6), Ubuntu 12.04 (kernel:3.2), 和 Linux Mint(kernel:3.2)。
关于文章内容
尽管我提供了或多或少完整的标准文件系统功能解决方案,但我将只介绍其中两项,在我看来,这两项几乎被忽略了:**监视文件系统变化**和**在 Linux 上发送/恢复文件到回收站**。我将通过一个两部分的系列文章,来阐述我在编写和测试这些可移植代码时所发现的内容。
在 Linux 上实现 .NET 风格的 FileSystemWatcher 解决方案
唯一能找到开箱即用的完整可用解决方案的地方是 Qt 框架,它很棒,但会引入您在处理 C++ 时可能出于各种原因想要避免的依赖项。我最终使用了特定于平台的 API 调用、C++ 标准库以及 Boost 库,这些库被广泛使用,并且在我看来是前者的自然扩展。当我开始编写 **FileSystemWatcher** 时,在 Linux 上编写第一个可用的示例并不难,因为文档非常详尽。您可以轻松找到许多代码示例,展示如何使用 `read()` 函数调用来读取文件系统事件。这部分很简单。我们只需要一个缓冲区来存储 `read()` 调用返回的结果,以及两个变量——一个用于存储缓冲区内容的实际长度,另一个用于在读取缓冲区时跟踪当前位置,因为一次读取可能包含多个事件。
ssize_t len = 0;
ssize_t i = 0;
char buff[BUFF_LEN]={0};
len = read(fileDescriptor, &buff, BUFF_LEN);
while((i < len) || stopLoop)
{
struct inotify_event* event = (struct inotify_event*)&buff[i];
...
i += EVENT_SIZE + event->len;
}
我们提供给 `read()` 函数的缓冲区可能一次返回多个事件,因此我们需要将每个事件解析为 `struct inotify_event` 的形式。问题在于 `read()` 函数调用是一个阻塞调用,我无法想象谁会需要这种有限的解决方案,因为它不允许轻松中断,所以我试图找到一个更可接受的方法。通过进一步研究,我偶然发现了 `select()` 函数系统调用,它应该能提供我们在这个场景中需要的功能——也就是说,它能提供文件系统发生事件的通知,从而我们只需要在必要时调用阻塞的 `read()` 函数。当我以为我终于构建好了解决方案时,我不愉快地发现它在我测试的 Ubuntu 12.04 x64 机器上并没有按预期工作。通过反复试验和进一步研究,我找到了我认为最终可以接受的解决方案。在我为下载提供的解决方案中的主类(**FileSystemController**)中,您可以看到我使用了一个专用线程来监听文件系统事件,并且仅在需要文件系统监视时才实例化这个类(所有其他功能都通过静态函数调用提供)。这只是为了方便您,省去您在完成事件监听后实现自己的清理工作。Linux 实现的主要部分是 `process_events()` 函数,它运行在一个专用线程上。
//Read FS events on LINUX
void FileSystemWatcher::process_events()
{
m_wathcerThreadActive = true;
int fileDescriptor = inotify_init();
if(fileDescriptor < 0)
{
boost::unique_lock<boost::mutex> lock(m_ResultsLock);
m_results.AddResult(OperationResult(false, TO_STRING_TYPE("FileSytemWatcher::process_events"),
TO_STRING_TYPE("FILE DESCRIPTOR ERROR")));
return;
}
auto watchEvents = m_changesToWatch == ChangeKind::BasicChanges ? IN_CREATE | IN_MOVED_FROM | IN_MOVED_TO
| IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF
: IN_ALL_EVENTS;
int watchDescriptor = inotify_add_watch(fileDescriptor,m_locationPath.c_str(), watchEvents);
if(watchDescriptor < 0)
{
boost::unique_lock<boost::mutex> lock(m_ResultsLock);
m_results.AddResult(OperationResult(false, TO_STRING_TYPE("FileSytemWatcher::process_events"),
TO_STRING_TYPE("FILE WATCH DESCRIPTOR ERROR")));
return;
}
register bool stopLoop = false;
//define file descriptor sets for select() func.
//You don't need them if you choose to use ppoll()
//function like I did
//fd_set read_fds, write_fds, except_fds;
//pselect() uses this struct...
struct timespec timeout;
timeout.tv_sec=0;
timeout.tv_nsec = WAIT_FOR_EACH_EVENT_DURATION;//nanoseconds resolution
//We just need notification that there is something for blocking read()
//function to do, we won't actually use event notification from pollfd array
//This is because I wanted to get affeced file path for event which we can
//obtain from buffer provided to read() as in/out param
struct pollfd pfd;
pfd.events = POLLIN;//watchEvents;
pfd.fd = fileDescriptor;
while((!m_stopThread))
{
//if previous event gives instruction to exit loop
if(stopLoop)
break;
//Check location existence
if(!boost::filesystem::exists(m_locationPath))
break;
ssize_t len = 0;
register ssize_t i = 0;
char buff[BUFF_LEN]={0};
/*
FD_ZERO(&read_fds);
FD_ZERO(&write_fds);
FD_ZERO(&except_fds);
FD_SET(fileDescriptor, &read_fds);
FD_SET(fileDescriptor, &write_fds); //MUST be used to catch event
FD_SET(fileDescriptor, &except_fds);
//This NEEDS to be reinitialized each time since select() func.
//makes corrections to this value
struct timeval timeout;
timeout.tv_sec = 0; //seconds
timeout.tv_usec = WAIT_FOR_EACH_EVENT_DURATION; //microseconds
*/
/*** Check if there is something to read in time period of *timeout ***/
//This approach seems to be unreliable on some distros like Ubuntu 12.04 x64 (kernel 3.2.0.35) where I tested it
//On others like Open SUSE 12.2 x64 (kernel 3.4) it works fine
//auto result = select(fileDescriptor, &read_fds, &write_fds, &except_fds, &timeout,);
//This call SUCCEEDED on all test configurations unlike select/pselect
int result = ppoll(&pfd, 1, &timeout,NULL);
if(result == -1)
{
boost::unique_lock<boost::mutex> lock(m_ResultsLock);
m_results.AddResult(OperationResult(false, TO_STRING_TYPE("FileSytemWatcher::process_events"),
TO_STRING_TYPE("ERROR IN EVENT PROCESSING")));
continue;
}
//this can be 0 if select timeout elapsed before anything is caught
//In that case we want to avoid read() blocking call
if( result > 0)
{
//read is a blocking function, we don't want it to block
//the thread until there is something to read - e.g. event ocurred
len = read(fileDescriptor, &buff, BUFF_LEN);
//blocking sys. func. call - won't return until got something
while((i < len) || stopLoop)
{
struct inotify_event* event = (struct inotify_event*)&buff[i];
stopLoop = fireEvent(event);
//move ptr to position of next successful event if there are more
i += EVENT_SIZE + event->len;
}
}
//remove file descriptor from sets
/*
FD_CLR(fileDescriptor, &read_fds);
FD_CLR(fileDescriptor, &write_fds);
FD_CLR(fileDescriptor, &except_fds);
*/
}
//remove watch descriptor and close file descriptor instance
//and send event notifying that watcher is stopping
string empty;
m_changed(_EDATA(EventKind::watcherSignOut, empty));
//remove file descriptor from watch descriptor and close it
inotify_rm_watch(fileDescriptor, watchDescriptor);
close(fileDescriptor);
m_pause = false;
m_stopThread = false;
//Notify FileSystemController that dedicated thread if finished
m_wathcerThreadActive = false;
m_finalizer.notify_one();
}
基本上,该函数首先设置一个信号标志,表示监听线程已启动(`m_watcherThreadStarted`),然后尝试获取我们要监视的文件系统位置的文件描述符整数值。这是通过 Linux 系统调用 `inotify_init()` 完成的。
int fileDescriptor = inotify_init();
if(fileDescriptor < 0)
{
//error handling...
}
这也是函数的一个关键部分,它可能会返回一个小于零的文件描述符值作为错误。这可能出于各种原因发生,虽然它通常会成功,但在极少数情况下,它会失败,我并不清楚原因。因此,我们应该检查返回值,如果失败则停止进一步执行。之后,我们需要通过将之前获取的文件描述符与我们要监视的实际文件系统位置关联起来,来获取一个监视描述符值。我们还需要提供要监视的事件类型。这些事件由预定义的位值标识,如 `IN_DELETE`、`IN_CREATE` 等,它们的组合定义了信号掩码。这对于后续事件处理也很关键,如果此调用返回错误,我们就无法继续。此函数部分失败最常见的原因是指定位置的读取权限不足。
int watchDescriptor = inotify_add_watch(fileDescriptor,m_locationPath.c_str(),watchEvents);
if(watchDescriptor < 0)
{
//error handling...
}
接下来是如何以非阻塞的方式处理阻塞的 `read()` 函数调用。您可以使用 `select()` / `pselect()` Linux 函数来检查 `read()` 函数是否有可读内容,或者您可以使用我选择的方法,使用 `poll()` / `ppoll()` 函数调用来实现相同目的。所有这些都提供了提供超时值的方法,因此监听循环可以继续并检查是否需要停止监视。我必须警告您,我将要讨论的第一种方法(`select()` / `pselect()`)在我测试过的所有 Linux 发行版上工作得并不好——确切地说,它在 Ubuntu 12.04 x64 内核版本 3.2.0.35 上没有检测到事件。另一方面,第二种方法(`poll()` / `ppoll()`)应该可以安全地从内核版本 2.6.16(`ppoll()` 文档)开始使用,但我没有机会在较低内核版本上测试来支持这些说法。
使用 select() 系统调用
您可以在我上面提供的代码示例中看到此方法的一部分被注释掉了。如果您有和我一样的需要解决的问题,您可以在互联网上的各种论坛上找到类似下面这样的解决方案。首先,您需要创建三个文件描述符集,用于调用 `select()` Linux API 函数,因为 Linux man 文档明确指出,该函数可用于监视多个文件描述符,等待其中至少一个变得“就绪”,在我们的情况下,这意味着可以调用 `read()` 函数而不会阻塞。
fd_set read_fds, write_fds, except_fds;
正如文件描述符集的名称所示,您可以猜测它们的用途。`read` 文件描述符集用于监视表示有可读内容而不会阻塞的更改,而 `write_fds` 和 `except_fds` 则监视我们是否能够非阻塞地写入或是否发生了异常。令我有些惊讶的是,我需要同时提供 `write_fds` 和 `except_fds` 集以及 `read_fds` 才能使 `select()` 调用成功读取通知。
之后,我们应该启动一个带有几个信号标志的循环,这些标志将用于在需要时停止进一步的事件处理。基本上,如果我们要停止监视指定位置,或者该位置不再存在,监听循环就应该停止。这就是为什么我们必须处理由位 `IN_DELETE_SELF` 和 `IN_MOVE_SELF` 定义的事件(在处理事件通知时),这意味着我们当前正在监视的位置被永久删除、移至回收站、移动到文件系统的其他位置,或者被重命名(后者通常会带来一对由 `IN_MOVE_FROM` 和 `IN_MOVE_TO` 标识的事件)。有关详细信息,请参阅我的 `fireEvent()` 函数实现。
对 `select()` 的调用还需要与每个文件描述符集关联的文件描述符。我的代码示例(`process_events()` 函数的注释部分)显示我在循环中进行了这种关联,因为我发现 `select()` 调用在处理过程中也会以破坏性的方式修改文件描述符集。要使 `select()` 调用正常工作,还需要另一件事,那就是定义 `select()` 应该阻塞并监听可能事件的超时周期。我在搜索各种论坛上的答案时,看到很多人在实现这个超时时犯了同样的错误。如果您选择使用 `select()` 调用,您应该知道它会调整 `struct timeval` 指定的时间间隔,以指示距离超时还有多少时间。因此,我们需要在每次循环时重新初始化此值。另一方面,如果您选择使用 `pselect()` 调用,它使用具有更高时间分辨率(纳秒)的 `struct timespec`,您不必担心这一点,因为它不会对超时进行任何更改。
while((!m_stopThread))
{
...
FD_ZERO(&read_fds);
FD_ZERO(&write_fds);
FD_ZERO(&except_fds);
FD_SET(fileDescriptor, &read_fds);
FD_SET(fileDescriptor, &write_fds); //MUST be used to catch event
FD_SET(fileDescriptor, &except_fds);
struct timeval timeout;
timeout.tv_sec = 0; //seconds
timeout.tv_usec = WAIT_FOR_EACH_EVENT_DURATION; //microseconds*/
auto result = select(fileDescriptor, &read_fds, &write_fds, &except_fds, &timeout,);
...
}
最后,我们应该检查 `select()` 函数调用的最终结果。如果发生错误,它返回 **-1**;如果超时,返回值是 **0**;而我们最感兴趣的是大于 0 的值,它表示 `read()` 调用不会阻塞。
使用 poll() / ppoll() API 调用
如前所述,我在一个 Linux 测试配置上使用前一种方法时遇到了问题。在仔细研究了 Linux 文档后,我发现使用 `poll()` / `ppoll()` 函数调用可以实现相同的功能。这种方法被证明更加可靠,我没有遇到任何检测更改的问题。`poll()` 和 `ppoll()` 函数之间的区别在于,后者可以防止与信号到达时间和实际底层 `poll()` 调用有关的某些竞态条件。至少我的理解是这样的,因为说实话,我没有 Linux 信号方面的经验。由于我没有使用信号处理程序,我当时选择 `ppoll()` 而不是 `poll()` 的主要原因是能够进行更精细的超时控制(纳秒 vs 毫秒分辨率)。不过,随着时间的推移,这个论点也就不那么重要了,因为我最终设置了 0.1 秒的时间间隔,这很容易用毫秒的较低时间分辨率来实现。我甚至没有设置 `ppoll()` 的 `sigmask` 参数,而是将其留空(`NULL`),根据文档,这与调用 `poll()` 相同,所以我认为您也可以在没有问题的情况下使用 `poll()` 调用。这些系统调用接受一个 `struct pollfd` 数组,其中有一个字段可以设置文件描述符,还有一个 `events` 字段,我们可以在其中设置要监视的内容。根据文档,`POLLIN` 值应该足以通知指定文件描述符上有可读内容。在我的 `FileSystemWatcher` 示例中,我只监视一个位置(文件描述符)的变化,所以我只需要 `poll()` /`ppoll()` 提供的可靠通知方法。我没有使用此系统调用从 `struct pollfd` 的 `revents` 字段中获取事件的能力,因为我想提取受影响文件系统项的实际路径,这可以通过 `read()` 系统调用返回的事件信息轻松实现。
std::string getNotificationFileName(const inotify_event *event, const std::string& path)
{
std::string fName(event->name, event->len / sizeof(char));
//trailing slash handling
if(fName[fName.length() - 1] != '/')
fName = path + "/" + fName;
else
fName = path + fName;
return fName;
}
如果您查看我完整的 C++ FileSystemWatcher 实现,您会发现我使用了 `boost::signals2` 命名空间中的 boost 信号,这些信号应该可以在不同线程之间安全使用,以发送实际事件以及受监视位置上受影响的文件/文件夹的完整路径。我唯一建议的是,您应该正确实现消费者模式的实现,而我为了简化起见,在注册的处理程序中没有这样做,以便以顺序方式处理通过信号发送的事件。
Windows FileSystemWatcher 在 C++ 中的简要概述
尽管这个实现值得比“简要”介绍更详细的解释,但我不会那样做,因为我改编了另一个很棒的文章 这里 中已经很好描述的内容,所以大部分功劳都归功于写这篇文章的人。我只能强调几个可能令人困惑的步骤,或者至少对我来说是这样。监视是在一个专用线程上进行的,带有循环,就像在 Linux 中一样,不同之处在于我们实际上是为异步调用 `ReadDirectoryChangesW()` WinAPI 函数打开了另一个额外的线程。第二个线程应该用于通过在回调函数中再次调用 `ReadDirectoryChangesW()` 来继续监视,并且它应该通过 Boost 信号(boost/signals2)发送实际的事件数据。第一个线程用于接受停止监视的潜在请求,并检查我们要监视的位置是否仍然存在。我未能获得在我们要监视的实际文件系统位置被删除、移动或重命名时发送的通知,这就是为什么我决定在循环中定期检查它是否确实存在。
如果您想查看我的实现,请在 Qt Creator 项目中的 `FileSystemWatcher_Windows.cpp` 文件中查找,该项目可在下载中使用。
第一部分总结
下次,我将讨论如何实现 Linux 的 Trash 功能以及如何使用 C++ 和 Boost 从中恢复文件。这似乎是一个被广泛忽略的领域,几乎没有解释可供参考。我个人至今还没有找到解决这个问题的方法。我还会简要提及我在 Windows 上实现相同功能时遇到的问题,目标是 Recycle Bin。