本文主要是介绍Apple - File System Events Programming Guide,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
本文翻译整理自:File System Events Programming Guide (pdated: 2012-12-13)
https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/FSEvents_ProgGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40005289
文章目录
- 一、介绍
- 本文档的结构
- 二、技术概述
- 三、使用文件系统事件API
- 1、添加包含指令
- 2、创建事件流
- 3、处理事件
- 4、使用持久事件
- 5、创建目录层次结构快照
- 6、清理
- 7、针对每台设备的流媒体的特殊考虑
- 四、文件系统事件安全
- 1、文件系统权限和文件系统事件
- 2、已删除文件和文件系统事件
- 3、防止文件系统事件存储
- 五、内核队列:文件系统事件的替代方案
- 1、选择事件机制
- 2、使用内核队列
- 3、简单的例子
一、介绍
文件系统事件 API 为应用程序提供了一种获取目录层次结构内容更改通知的方式。
例如,您的应用程序可以使用此功能快速检测用户是否使用其他应用程序修改了项目包中的文件。
它还提供了一种轻量级的方法,用于确定自您的应用程序上次检查目录层次结构以来其内容是否发生了变化。
例如,备份应用程序可以使用此方法来确定自给定的时间戳或事件ID以来哪些文件发生了变化。
如果你的应用程序需要处理大量文件,尤其是当应用程序需要处理大量文件的层次结构时,你应该阅读此文档。
本文档的结构
本文件分为以下章节:
- 技术概览——描述了文件系统事件 API,并从高层次上解释了其工作原理。
- 使用文件系统事件API - 介绍如何使用文件系统事件API,从创建事件流到编写处理程序,并提供代码示例帮助您快速入门。
- 文件系统事件安全 - 描述文件系统事件 API 的安全功能。
- 内核队列:文件系统事件的替代方案 - 介绍了内核队列机制,并解释了在何种情况下可以使用内核队列API代替文件系统事件API,还提供了一个简短的代码示例,以演示如何使用它。
二、技术概述
文件系统事件API是OS X v10.5及以后版本中提供的一项新技术。
使用该API,您可以注册通知,以便在目录层次结构或其内容发生更改时接收通知。
文件系统事件机制由三个部分组成:通过一个特殊的设备(也用于 Spotlight)将原始事件通知传递给用户空间的内核代码,过滤此流并发送通知的守护进程,以及存储所有更改记录的持久性数据库。
当您的应用程序注册通知时,文件系统事件守护进程将在任何受监控目录层次结构中的文件发生更改时,每隔一次发送一个通知。
如果目录本身被修改(例如,如果其权限发生变化或添加了新文件),也会发送通知。
值得注意的是,通知的粒度是目录级别的。
它只会告诉您目录中发生了某些变化,但不会告诉您具体是什么发生了变化。
除了将多个文件的更改通知合并为针对每个目录的单一通知外,文件系统事件守护进程还会在短时间内发生多个通知时将它们合并到给定目录的单个通知中。
在最后一次更改之后,您始终会收到至少一个通知。
除此之外,时间粒度可以是您想要的任何粗细程度;在注册通知时,您可以选择事件之间的最小时间间隔。
为了更好地理解这项技术,你首先需要了解它不是什么。
它不是用于细粒度通知文件系统变化的注册机制。
它不是为病毒检查器或其他需要立即了解文件变化并根据需要阻止这些变化的技术而设计的。
支持这些功能的最好方法是通过内核扩展,在VFS级别注册对变化的兴趣。
文件系统事件API也不是专门用于查找特定文件何时发生变化的。
对于此类用途,kqueues机制更为适用。
文件系统事件API旨在被动监控大量文件树中的更改。
这项技术最明显的用途是用于备份软件。
事实上,文件系统事件API为苹果的备份技术提供了基础。
文件系统事件API的另一个好处是为那些将数据存储为松散集合(包含项目和数十或数百个相关文件)的应用程序提供一致性保证。
当另一个应用程序修改了相关文件中的一个时,您的应用程序需要知道此事,以便选择如何将更改融入项目中。
当然,这种用法也可以通过使用kqueues来满足,但文件系统事件提供了一个便利,即即使在应用程序未运行时,也可以看到发生的变化,这使得应用程序在打开项目时进行一致性检查更加容易。
三、使用文件系统事件API
文件系统事件API 包含多个不同的函数组。
您可以使用以 FSEvents
开头的函数获取卷和事件的一般信息。
您可以使用以 FSEventStream
开头的函数创建新的事件流、对流进行操作等等。
文件系统事件流的生命周期如下:
-
该应用程序通过调用
FSEventStreamCreate
或FSEventStreamCreateRelativeToDevice
来创建一个流。 -
该应用程序通过调用
FSEventStreamScheduleWithRunLoop
在运行循环中安排流的播放。 -
该应用程序通过调用
FSEventStreamStart
告诉文件系统事件守护进程开始发送事件。 -
该应用程序在事件到达时对其进行处理。
API通过调用步骤1中指定的回调函数来发布事件。 -
该应用程序通过调用
FSEventStreamStop
告诉守护进程停止发送事件。 -
如果应用程序需要重新启动流,请转至步骤3。
-
该应用程序通过调用
FSEventStreamUnscheduleFromRunLoop
取消了该事件在运行循环中的执行。 -
该应用程序通过调用
FSEventStreamInvalidate
来使流无效。 -
该应用程序通过调用
FSEventStreamRelease
来释放对流的引用。
接下来的几个部分将详细说明这些步骤。
1、添加包含指令
在使用文件系统事件流API之前,您必须按照以下方式包含Core Services框架:
#include <CoreServices/CoreServices.h>
当你编译代码时,必须在Xcode中将Core Services Framework添加到目标中,或者在命令行或Makefile中添加 -framework CoreServices
链接标志。
2、创建事件流
文件系统事件 API 支持两种类型的事件流:基于主机的事件流和基于磁盘的事件流。
在创建流之前,您必须决定要创建哪种类型的流:基于主机的事件流还是基于磁盘的事件流。
您可以通过分别调用函数 FSEventStreamCreate
和 FSEventStreamCreateRelativeToDevice
来创建这些流。
每台主机的事件流由相对于主机上其他事件的ID不断增加的事件组成。
这些ID在除一个例外情况外的情况下都是唯一的:如果从运行OS X v10.5或更高版本的另一台计算机添加了额外的磁盘,这些卷的历史ID可能会相互冲突。
任何新的事件都会自动从任何已连接驱动器的最高历史ID之后开始。
相比之下,每块磁盘的事件流由相对于该磁盘上先前事件的ID不断增加的事件组成。
它与其他磁盘上的其他事件没有关系,因此您必须为每个希望监控的物理设备创建单独的事件流。
通常来说,如果您编写的软件需要持久化,那么应该使用基于磁盘的流来避免因ID冲突而引起的任何混淆。
相反,如果您在正常执行期间监视目录或目录树中的更改,例如监视队列目录,那么基于主机的流最为方便。
注意:由于磁盘可以被运行较早版本的OS X(或其他操作系统)的计算机进行修改,因此您应该将事件列表视为建议列表,而不是所有更改的绝对列表。
如果磁盘被运行较早版本的OS X的计算机进行修改,则历史日志将被丢弃。
例如,备份软件仍然需要定期对任何卷进行完全扫描,以确保没有变化被遗漏。
如果你正在监控根文件系统的文件,那么这两种机制的行为将相似。
例如,下面的代码片段演示了如何创建一个事件流:
/* Define variables and create a CFArray object containingCFString objects containing paths to watch.*/
CFStringRef mypath = CFSTR("/path/to/scan");
CFArrayRef pathsToWatch = CFArrayCreate(NULL, (const void **)&mypath, 1, NULL);
void *callbackInfo = NULL; // could put stream-specific data here.
FSEventStreamRef stream;
CFAbsoluteTime latency = 3.0; /* Latency in seconds *//* Create the stream, passing in a callback */
stream = FSEventStreamCreate(NULL,&myCallbackFunction,callbackInfo,pathsToWatch,kFSEventStreamEventIdSinceNow, /* Or a previous event ID */latency,kFSEventStreamCreateFlagNone /* Flags explained in reference */
);
一旦创建了事件流,您必须在应用程序的运行循环中对其进行调度。
要这样做,请调用 FSEventStreamScheduleWithRunLoop
,并将新创建的流、运行循环的引用以及运行循环模式作为参数传递给它。
有关运行循环的更多信息,请参阅运行循环。
如果你还没有一个运行循环,你需要为这个任务分配一个线程。
使用你选择的API创建一个线程后,调用 CFRunLoopGetCurrent
来为该线程分配一个初始的运行循环。
任何将来对 CFRunLoopGetCurrent
的调用都将返回相同的运行循环。
例如,下面的代码片段演示了如何在当前线程的运行循环(尚未运行)上安排一个名为 stream
的流:
FSEventStreamRef stream;
/* Create the stream before calling this. */
FSEventStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
设置事件流的最后一步是调用函数 FSEventStreamStart
。
该函数告诉事件流开始发送事件。
该函数只有一个参数,即要开始的事件流。
一旦创建并安排好事件流后,如果您的运行循环尚未启动,则应通过调用 CFRunLoopRun
来启动它。
3、处理事件
你的事件处理回调函数必须符合 FSEventStreamCallback
的原型。
参数在 FSEventStreamCallback
数据类型的参考文档中有描述。
您的事件处理程序会接收三个列表:一个包含路径的列表、一个包含标识符的列表和一个包含标志的列表。
实际上,这些列表代表了一系列事件。
第一个事件包含来自每个数组的第一个元素,以此类推。
您的处理程序必须遍历这些列表,按需处理事件。
对于每个事件,您应该扫描指定路径的目录,按照所需的方式处理其内容。
通常,您只需要扫描指定路径的精确目录。
然而,有三种情况并非如此:
- 如果一个目录中的事件与该目录下的一个或多个子目录中的事件几乎同时发生,这些事件可能会合并为一个事件。
在这种情况下,您将收到一个带有kFSEventStreamEventFlagMustScanSubDirs
标记的事件。
当您收到此类事件时,必须递归地重新扫描事件中列出的路径。
额外的变化并不一定在列出的路径的直接子目录中。 - 如果内核与用户空间守护进程之间出现通信错误,您可能会收到带有
kFSEventStreamEventFlagKernelDropped
或kFSEventStreamEventFlagUserDropped
标志的警报事件。
在任何情况下,您都必须对正在监控的任何目录进行彻底扫描,因为无法确定可能发生了什么变化。
注意:当一个事件被丢弃时,也会设置 kFSEventStreamEventFlagMustScanSubDirs
标志。
因此,在决定是否需要对路径进行完全扫描时,没有必要专门检查是否发生了丢弃事件标志。
丢弃的事件标志仅用于提供信息。
-
如果你正在监视的根目录被删除、移动或重命名(或者其父目录中有任何一个被移动或重命名),该目录可能会消失。
如果你关心这个问题,在创建流时应该传递kFSEventStreamCreateFlagWatchRoot
标志。
在这种情况下,你会收到带有kFSEventStreamEventFlagRootChanged
标志和事件ID为零(0
)的事件。
在这种情况下,你必须重新扫描整个目录,因为它可能已经不存在了。如果你需要找出目录移动到了哪里,你可以使用“
open
”打开根目录,然后将 “F_GETPATH
” 传递给“fcntl
” 来查找其当前路径。
有关“fcntl
”的更多信息,请参阅其手册页。 -
如果事件数量接近2^64,事件标识符将循环。
当这种情况发生时,您将收到带有kFSEventStreamEventFlagEventIdsWrapped
标志的事件。
幸运的是,至少在短期内,这在实践中不太可能发生,因为64位足以为地球上每个擦除器大小的区域(包括水)提供足够的空间,用于存储一个事件,并且需要大约2000艾字节(20亿亿千兆字节)的存储空间来存储所有这些事件。
然而,如果您收到该标志,仍然应该检查并采取适当的措施。
作为事件处理程序的一部分,你有时可能需要获取当前事件流正在监视的路径列表。
你可以通过调用 FSEventStreamCopyPathsBeingWatched
来获取该列表。
有时候,你可能希望监控当前在流中所处的位置。
例如,如果你的代码明显落后于其他代码,你可能希望减少处理量。
可以通过调用 FSEventStreamGetLatestEventId
(或者检查列表中的最后一个事件)来获取当前批量事件中最新的事件。
然后,你可以将该值与 FSEventsGetCurrentEventId
返回的值进行比较,该函数返回系统中编号最高的事件。
例如,下面的代码片段展示了一个非常简单的处理程序。
void mycallback(ConstFSEventStreamRef streamRef,void *clientCallBackInfo,size_t numEvents,void *eventPaths,const FSEventStreamEventFlags eventFlags[],const FSEventStreamEventId eventIds[])
{int i;char **paths = eventPaths;// printf("Callback called\n");for (i=0; i<numEvents; i++) {int count;/* flags are unsigned long, IDs are uint64_t */printf("Change %llu in %s, flags %lu\n", eventIds[i], paths[i], eventFlags[i]);}
}
注意:如果您在创建流时传递了 kFSEventStreamCreateFlagUseCFTypes
标志,则需要将 eventPaths
值转换为 CFArrayRef
对象。
4、使用持久事件
文件系统事件最强大的功能之一是它们能在重启后保持不变。
这意味着您的应用程序可以轻松地找出自特定时间或过去某个特定事件以来发生的事情。
通过这样做,您可以在应用程序未运行时找出哪些文件已被修改。
这可以大大简化备份已修改文件、检查多文件项目中已更改的依赖项等任务。
要处理持久事件,您的应用程序应该定期存储它处理的最后一个事件ID。
然后,当它需要回溯查看哪些文件已更改时,只需查看自已知的最后一个事件之后发生的事件即可。
要获取自过去某个特定事件以来的所有事件,您可以将事件ID作为 sinceWhen
参数传递给 FSEventStreamCreate
或 FSEventStreamCreateRelativeToDevice
。
在单个设备层面上,您还可以通过使用时间戳来轻松确定要包含哪些事件。
要这样做,您必须首先调用 FSEventsGetLastEventIdForDeviceBeforeTime
以获取 sinceWhen
参数,然后将其作为 FSEventStreamCreateRelativeToDevice
的参数。
在单个设备层面上,您也可以轻松地使用时间戳来确定要包含哪些事件。
要这样做,您必须首先调用 FSEventsGetLastEventIdForDeviceBeforeTime
获取指定时间戳之前的该设备的最后事件ID。
然后将所得到的值传递给 FSEventStreamCreateRelativeToDevice
。
有关单个设备流的特殊考虑事项的详细说明,请参见 Special Considerations for Per-Device Streams。
在处理持久事件时,一种常用的技术是将文件系统事件通知与树中文件元数据的缓存“快照”结合使用。
有关如何构建文件层次结构快照的详细信息,请参阅 Building a Directory Hierarchy Snapshot。
5、创建目录层次结构快照
文件系统事件会告诉你某个指定目录中的某个文件发生了变化。
在某些情况下,这已经足够了——例如,如果您的应用程序是一个打印或打印队列程序,它只需要知道该目录中添加了一个文件。
然而,在某些情况下,这还不够,你需要精确地了解目录中发生了哪些变化。
解决这个问题的最简单方法是创建一个目录层次结构的快照,在某个特定时间点存储系统状态的副本。
例如,你可以存储一个文件名列表和最后修改日期,从而使你能够确定自上次备份以来哪些文件已被修改。
你可以通过遍历文件系统层次结构并构建你选择的数据结构来实现这一点。
在缓存元数据的过程中,如果你在缓存过程中发现更改,你可以重新读取更改的目录或目录以获取更新的快照。
一旦你拥有了一个准确反映你所关心的文件系统层次结构当前状态的缓存元数据树,你就可以在文件系统事件通知后比较当前目录状态与快照来确定目录或层次结构中哪个或哪些文件发生了更改。
重要提示:为了避免遗漏更改,您必须在开始扫描目录之前开始监视该目录。
由于多任务操作系统上任何通知机制的固有非确定性延迟,可能无法明确确定导致事件发生的操作是在子目录扫描之前还是之后发生的。
为了确保不会丢失任何更改,最好在扫描期间对任何被修改的子目录重新进行扫描,而不是为每个子目录获取时间戳并尝试将这些时间戳与事件时间戳进行比较。
OS X提供了一些API,可以使这个过程更加简单。
b0
函数返回一个目录项数组,您可以快速遍历该数组。
这比手动使用b1
、b2
等逐个读取目录要简单一些,而且由于您始终会遍历整个目录,因此略微提高了效率。
函数 tsearch
、 tfind
、 twalk
和 tdelete
可以简化处理大型搜索树的工作。
特别是,二叉树是一种快速查找特定目录中缓存文件信息的简单方法。
下面的代码片段演示了正确调用这些函数的方法:
示例 2-1 使用 tsearch、tfind、twalk 和 tdelete API。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <dirent.h>
#include <sys/stat.h>
#include <string.h>
#include <search.h>int array[] = { 1, 17, 2432, 645, 2456, 1234, 6543, 214, 3, 45, 34 };
void *dirtree;static int cmp(const void *a, const void *b) {if (*(int *)a < *(int *)b) return -1;if (*(int *)a > *(int *)b) return 1;return 0;
}void printtree(void);/* Pass in a directory as an argument. */
int main(int argc, char *argv[])
{int i;for (i=0; i< sizeof(array) / sizeof(array[0]); i++) {void *x = tsearch(&array[i], &dirtree, &cmp);printf("Inserted %p\n", x);}printtree();void *deleted_node = tdelete(&array[2], &dirtree, &cmp);printf("Deleted node %p with value %d (parent node contains %d)\n",deleted_node, array[2], **(int**)deleted_node);for (i=0; i< sizeof(array) / sizeof(array[0]); i++) {void *node = tfind(&array[i], &dirtree, &cmp);if (node) {int **x = node;printf("Found %d (%d) at %p\n", array[i], **x, node);} else {printf("Not found: %d\n", array[i]);}}exit(0);
}static void printme(const void *node, VISIT v, int k)
{const void *myvoid = *(void **)node;const int *myint = (const int *)myvoid;// printf("x\n");if (v != postorder && v != leaf) return;printf("%d\n", *myint);
}void printtree(void)
{twalk(dirtree, &printme);
}
如果您以前未在其他基于UNIX或UNIX类似操作系统上使用过该API,其中两个不寻常的设计决定可能会使正确使用变得棘手:
tsearch
和tdelete
函数获取的是树变量的地址,而不是树变量本身。
这是因为当它们创建或删除初始根节点时,必须修改存储在树变量中的值。
虽然 tfind
不会修改根节点的值,但它仍然会将根节点的地址作为其参数,而不是根指针本身。
一个常见的错误是传递 dirtree
指针。
实际上,你必须传递 &dirtree
(根指针的地址)。
注意:虽然看似一致,但
twalk函数并不获取根节点的地址,因此不需要使用
&运算符,实际上,如果使用了
&` 运算符可能会导致程序崩溃。
- 由
twalk
传递给回调函数的值以及由tfind
和tsearch
返回的值是数据指针所指向的地址,而不是数据本身的值。
由于该代码传递的是整数的地址,因此需要两次进行解引用操作——一次是为原始的address-of
操作,一次是解引用这些函数返回的指向该指针的指针。
然而,与其他函数不同的是,函数 tdelete
并不返回数据在树中存储的地址。
这是因为数据不再存储在树中。
相反,它返回它删除的节点的父节点。
POSIX 函数 stat
和 lstat
提供了访问文件元数据的简单方法。
这两个函数在处理符号链接时有所不同。
函数 lstat
提供关于链接本身的信息,而函数 stat
提供关于链接指向的文件的信息。
一般来说,在处理文件系统事件通知时,您可能希望使用 lstat
,因为对包含符号链接指向的文件的底层文件的更改不会导致包含该符号链接的目录的更改通知。
然而,如果您在监视树中使用受控文件结构,其中符号链接始终指向您监视的目录中的文件,则可能有理由使用 stat
。
有关构建目录快照的工具示例,请参见 Watcher 示例代码。
6、清理
当你不再需要文件系统事件流时,应该总是清理该流以避免泄漏内存和描述符。
但是在清理之前,你必须先通过调用 FSEventStreamStop
停止运行循环。
接下来,您应该调用 FSEventStreamInvalidate
函数。
该函数通过一次调用即可将流从所有运行循环中取消调度。
如果您只需要将流从单个运行循环中取消调度,或者需要在两个运行循环之间移动事件流,则应调用 FSEventStreamUnscheduleFromRunLoop
函数。
如果需要,您还可以通过调用 FSEventStreamScheduleWithRunLoop
函数重新调度事件流。
一旦你取消了事件流,可以通过调用 FSEventStreamRelease
来释放它。
当流的释放和保留计数相等并且不再有流被保留的实例时,流将被释放。
在某些情况下,还有其他三个与清理相关的函数值得您注意。
如果您的应用程序需要确保文件系统在清理流之前已经达到稳定状态,您可能会发现flush流很有用。
您可以使用以下两个函数中的任意一个来实现这一操作: FSEventStreamFlushAsync
和 FSEventStreamFlushSync
。
当执行刷新操作时,同步调用将在所有待刷新的事件都完成后才返回。
异步调用则会立即返回,并返回最后一个尚未完成的事件的ID(类型为 FSEventStreamEventId
)。
您可以在回调函数中使用此值,以便在需要时确定最后一个事件是否已处理完毕。
与清理相关的最后一个函数是 FSEventsPurgeEventsForDeviceUpToEventId
。
该函数只能由根用户调用,因为它会删除指定事件ID之前的卷上的事件历史记录。
一般来说,你不应该调用这个函数,因为你无法保证你的应用程序是唯一一个事件数据的消费者。
如果你正在编写一个专业应用程序(例如企业备份解决方案),那么可以调用此函数来将事件记录剪裁到合理的大小,以防止其任意增大。
但是,你应该只在管理员明确要求这种行为的情况下这样做,并且在执行任何操作之前(或在启用任何会在稍后执行该操作的规则之前),都应该请求确认。
7、针对每台设备的流媒体的特殊考虑
除了《处理事件》一文中所述的考虑因素外,使用
FSEventStreamCreateRelativeToDevice`创建的针对单个设备的流还有一些特殊的特性,您应该对此有所了解:
- 所有的路径都是相对于您正在监控的卷的根目录,而不是相对于系统根目录。
这适用于创建流时使用的路径,也适用于您的回调函数在事件中接收到的任何路径。 - 设备ID在重启后可能不再保持不变(尤其是对于可移除设备)。
因此,您有责任通过比较UUID来确保您正在查看的卷是正确的卷。
除了系统级流提供的功能外,您还可以通过调用 FSEventStreamGetDeviceBeingWatched
获取与流相关联的设备的UUID。
通过调用 FSEventsCopyUUIDForDevice
可以获取设备的唯一ID。
如果该唯一ID与上一次运行时获取的ID不同,这意味着很多事情。
这可能意味着用户有两个具有相同名称的卷,用户已对具有相同名称的卷进行了格式化,或者已清除了该卷的事件ID。
在任何一种情况下,该卷之前的事件都不适用于该特定卷,但它们可能仍然适用于其他卷。
如果你发现一个卷的UUID与上一次运行时存储的UUID匹配,但事件ID低于你上一次存储的最新版本,这可能意味着用户从备份中还原了该卷,或者可能意味着ID已经循环或已被清除。
无论哪种情况,您可能为该设备存储的事件均已无效。
最后,如果您正在使用持久事件,也可以使用 FSEventsGetLastEventIdForDeviceBeforeTime
函数查找一个时间戳之前的最后一个事件。
这个事件 ID 是持久的,对于执行增量备份特别有用。
使用的时间格式是一个 CFAbsoluteTime
值,以自 2001 年 1 月 1 日以来的秒数来衡量。
对于其他时间戳格式,您必须按照以下方式将其转换为此格式:
- 如果你正在编写一个Cocoa应用程序,那么你应该使用一个
NSDate
对象来进行任何转换,然后使用CFDateGetAbsoluteTime
来获取相应的CFAbsoluteTime
值。
(你可以透明地将NSDate
对象作为CFDateRef
传递。) - 如果你在非Cocoa应用程序中使用POSIX时间戳,你应该从该值中减去
kCFAbsoluteTimeIntervalSince1970
来将其转换为CFAbsoluteTime
值。
请务必始终使用基于格林威治标准时间的时间戳。 - 如果你在非 Cocoa 应用程序中使用遗留的 Carbon 时间戳,则需要从中减去
kCFAbsoluteTimeIntervalSince1904
。
请务必始终使用基于 GMT 的时间戳。
有关日期和时间类型的更多信息,您应该阅读 Core Foundation 的日期和时间编程指南。
四、文件系统事件安全
文件系统事件API在安全性方面提出了一个有趣的挑战。
因为它提供了指向已更改内容的文件系统路径,并将该信息存储在持久性数据库中,因此它为信息泄露开辟了新的途径,尽管仅限于目录名称。
文件系统事件 API 通过两种方式解决这一问题:权限和预防。
1、文件系统权限和文件系统事件
与文件系统事件相关的最明显的安全问题是隐私问题。
如果鲍勃可以看到艾莉丝主目录中发生的更改列表,鲍勃可能会看到艾莉丝不想让任何人看到的内容。
例如,艾莉丝可能有一个目录名称与尚未发布的苹果产品代码名相同。
为了防止这种潜在的安全漏洞,除非用户可以通过标准文件系统权限访问已修改的目录,否则用户不会收到任何事件。
注意:即使用户正在监视所有目录(从根目录开始)的所有事件,提交给文件系统事件客户端的事件ID也不一定连续。
只有以根用户身份运行的应用程序才能保证接收到所有事件。
2、已删除文件和文件系统事件
当文件或目录被删除时,这些事件与任何其他文件系统事件看起来都非常相似。
这意味着即使文件被删除后,目录名称仍会保留在您的计算机上。
无法通过编程方式删除单个记录。
唯一可以删除数据库中先前记录的方法是将所有先于特定记录ID的记录全部删除。
您可以通过在应用程序中调用 FSEventsPurgeEventsForDeviceUpToEventId
来实现这一点。
虽然压缩后的数据以一系列文件的形式存储在每个卷的根目录下的 .fseventsd
目录中(仅供根用户访问),但您不应该直接处理这些数据,因为这些文件的格式可能随时发生变化。
3、防止文件系统事件存储
在某些情况下,卷中的内容具有足够的机密性,不适合记录它们。
要在卷级别上禁用记录(例如,创建备份卷时),您必须执行以下操作:
- 在卷的根目录下创建一个名为
.fseventsd
的目录。 - 在该目录中创建一个名为“b0.txt”的空文件。
所以如果您的卷挂载在 /Volumes/MyDisk
,您需要创建一个名为 /Volumes/MyDisk/.fseventsd/no_log
的空文件。
五、内核队列:文件系统事件的替代方案
内核队列API 为应用程序提供了一种方式,可以在文件或目录以任何方式被修改时(包括文件内容、属性、名称或长度的更改)接收通知。
如果您正在监视块或字符设备,并且通过调用 revoke
撤销了对该设备的访问权限,您的应用程序也可以接收到通知。
内核队列API还提供了一种监控子进程的方法,并可以找出它们何时调用 exit
、 fork
、 exec
等函数。
使用内核队列的方法超出了本文档的范围。
有关内核队列和进程的更多信息,您应该阅读FreeBSD的内核队列文档。
您可以在http://people.freebsd.org/~jmg/kq.html找到这些文档的链接。
1、选择事件机制
文件系统事件旨在提供以目录级粒度的更改通知。
对于大多数用途来说,这已经足够了。
然而,在某些情况下,您可能需要更细粒度的通知。
例如,您可能需要只监控单个文件的更改。
为此,内核队列(kqueue)通知系统更合适。
如果你正在监控一个庞大的内容层次结构,那么应该使用文件系统事件,因为与内核事件相比,内核队列要复杂一些,而且由于涉及到额外的用户内核通信,因此可能更加资源密集。
2、使用内核队列
内核队列(kqueue)和内核事件(kevent)机制非常强大且灵活,允许您接收一系列内核级事件(包括文件修改),并定义一组过滤器,以限制哪些事件被发送到您的应用程序。
要使用内核队列,您必须做以下四件事:
- 使用函数
kqueue(2) OS X Developer Tools Manual Page
创建一个内核事件队列。
该函数返回一个新分配的事件队列的文件描述符。 - 为您想要监视的每个文件打开一个文件描述符。
- 创建一个需要关注的事件列表。
要实现此功能,请使用EV_SET
来填充内核事件结构的各个字段。
其原型如下:
EV_SET(&kev, ident, filter, flags, fflags, data, udata);
其中,type
字段指定事件的类型,event_number
字段指定事件的编号,event_data
字段指定事件的数据。
第一个参数
kev是结构体本身的地址。 第二个参数
ident
包含一个指向您正在监视的文件的文件描述符。
第三个参数( filter
)包含了您想要查看其结果的内核过滤器的名称。
例如,您可以使用 EVFILT_VNODE
来监视文件上的 vnode 操作。
剩下的这些参数都是针对特定的过滤器的,详细说明请参阅 kevent
的手册页。
- 循环调用函数
kevent
。
该函数会监视内核事件队列中的事件,并将它们存储在您提供的缓冲区中。
其原型如下:
int kevent(int kq, const struct kevent *changelist,int nchanges, struct kevent *eventlist,int nevents, const struct timespec *timeout);
它的参数依次是队列文件描述符、上一步中需要监视的事件列表、该列表中的事件数量、用于存储最终事件数据的临时存储空间大小、该存储空间的大小以及超时时间。
在成功完成后,kevent
函数返回返回的事件数量。
如果在发生任何事件之前超时时间已过,则返回 0。
根据错误的性质,错误可能以带有 EV_ERROR
标记的事件的形式报告,并将系统错误存储在 data
字段中,或者以返回值为 -1 并将错误存储在 errno
字段中的方式报告。
3、简单的例子
A-1清单是一个简短的例子,演示了如何使用内核队列来监视单个文件。
若要查看一个更复杂的例子,该例子监视目录,请查看FileNotification示例代码。
A-1 使用内核队列监视文件
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/event.h>
#include <sys/time.h>
#include <errno.h>
#include <string.h>
#include <inttypes.h>#define NUM_EVENT_SLOTS 1
#define NUM_EVENT_FDS 1char *flagstring(int flags);int main(int argc, char *argv[])
{char *path = argv[1];int kq;int event_fd;struct kevent events_to_monitor[NUM_EVENT_FDS];struct kevent event_data[NUM_EVENT_SLOTS];void *user_data;struct timespec timeout;unsigned int vnode_events;if (argc != 2) {fprintf(stderr, "Usage: monitor <file_path>\n");exit(-1);}/* Open a kernel queue. */if ((kq = kqueue()) < 0) {fprintf(stderr, "Could not open kernel queue. Error was %s.\n", strerror(errno));}/*Open a file descriptor for the file/directory that youwant to monitor.*/event_fd = open(path, O_EVTONLY);if (event_fd <=0) {fprintf(stderr, "The file %s could not be opened for monitoring. Error was %s.\n", path, strerror(errno));exit(-1);}/*The address in user_data will be copied into a field in theevent. If you are monitoring multiple files, you could,for example, pass in different data structure for each file.For this example, the path string is used.*/user_data = path;/* Set the timeout to wake us every half second. */timeout.tv_sec = 0; // 0 secondstimeout.tv_nsec = 500000000; // 500 milliseconds/* Set up a list of events to monitor. */vnode_events = NOTE_DELETE | NOTE_WRITE | NOTE_EXTEND | NOTE_ATTRIB | NOTE_LINK | NOTE_RENAME | NOTE_REVOKE;EV_SET( &events_to_monitor[0], event_fd, EVFILT_VNODE, EV_ADD | EV_CLEAR, vnode_events, 0, user_data);/* Handle events. */int num_files = 1;int continue_loop = 40; /* Monitor for twenty seconds. */while (--continue_loop) {int event_count = kevent(kq, events_to_monitor, NUM_EVENT_SLOTS, event_data, num_files, &timeout);if ((event_count < 0) || (event_data[0].flags == EV_ERROR)) {/* An error occurred. */fprintf(stderr, "An error occurred (event count %d). The error was %s.\n", event_count, strerror(errno));break;}if (event_count) {printf("Event %" PRIdPTR " occurred. Filter %d, flags %d, filter flags %s, filter data %" PRIdPTR ", path %s\n",event_data[0].ident,event_data[0].filter,event_data[0].flags,flagstring(event_data[0].fflags),event_data[0].data,(char *)event_data[0].udata);} else {printf("No event.\n");}/* Reset the timeout. In case of a signal interrruption, thevalues may change. */timeout.tv_sec = 0; // 0 secondstimeout.tv_nsec = 500000000; // 500 milliseconds}close(event_fd);return 0;
}/* A simple routine to return a string for a set of flags. */
char *flagstring(int flags)
{static char ret[512];char *or = "";ret[0]='\0'; // clear the string.if (flags & NOTE_DELETE) {strcat(ret,or);strcat(ret,"NOTE_DELETE");or="|";}if (flags & NOTE_WRITE) {strcat(ret,or);strcat(ret,"NOTE_WRITE");or="|";}if (flags & NOTE_EXTEND) {strcat(ret,or);strcat(ret,"NOTE_EXTEND");or="|";}if (flags & NOTE_ATTRIB) {strcat(ret,or);strcat(ret,"NOTE_ATTRIB");or="|";}if (flags & NOTE_LINK) {strcat(ret,or);strcat(ret,"NOTE_LINK");or="|";}if (flags & NOTE_RENAME) {strcat(ret,or);strcat(ret,"NOTE_RENAME");or="|";}if (flags & NOTE_REVOKE) {strcat(ret,or);strcat(ret,"NOTE_REVOKE");or="|";}return ret;
}
更多信息可参阅:
- kqueue
- FileNotification sample code
- FreeBSD documentation for kernel queues at http://people.freebsd.org/~jmg/kq.html
2024-06-09(日)
这篇关于Apple - File System Events Programming Guide的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!