我有一个Linux应用程序,它使用inotify跟踪文件系统更改。我想为它编写一个功能测试套件,从最终用户角度测试应用程序,作为其中的一部分,我想测试文件系统失败的情况,特别是我想测试inotify失败。
我需要进行inotify_init()
,inotify_add_watch()
,inotify_rm_watch()
调用以及read()
调用inotify文件描述符在测试中需要时返回错误。< / p>
但问题是我找不到如何模拟inotify失败的方法。我想知道是否有人遇到过这样的问题,并且知道一些解决方案。
答案 0 :(得分:2)
如果你想避免任何嘲弄,你最好的选择就是直接触及操作系统限制来引发错误。例如,如果调用进程已达到对打开文件描述符数量的限制,inotify_init
可能会失败EMFILE
errno。要达到100%精度的条件,您可以使用两个技巧:
inotify的所有可能错误条件都记录在inotify
,inotify_init
和inotify_add_watch
的手册页中(我不认为inotify_rm_watch
可能会失败,除了纯编程代码中的错误。)
除了普通的错误(例如转过/proc/sys/fs/inotify/max_user_watches
)之外,inotify有几种错误模式(队列空间耗尽,监视ID重用),但严格意义上说这些不是“失败”。
当某人执行文件系统更改的速度超过您的反应速度时,就会发生队列耗尽。它很容易重现:使用cgroups暂停程序,同时打开inotify描述符(因此事件队列不会被耗尽)并通过修改观察到的文件/目录快速生成批次通知。一旦您有/proc/sys/fs/inotify/max_queued_events
未处理的事件,并取消暂停您的程序,它将收到IN_Q_OVERFLOW
(可能会遗漏一些不适合队列的事件)。
监视ID重用是繁琐的重现,因为现代内核从类似文件描述符的行为切换到监视ID的类似PID的行为。您应该使用与测试PID重用时相同的方法 - 创建并销毁inotify监视的批次,直到整数监视ID换行。
Inotify还有一些棘手的角落案例,在正常操作期间很少发生(例如,我所知道的所有Java绑定,包括Android和OpenJDK,都无法正确处理所有这些):同样的inode问题和处理IN_UNMOUNT
。
inotify文档中详细解释了相同的inode问题:
对inotify_add_watch()的成功调用会为此inotify实例返回与路径名对应的文件系统对象(inode)的唯一监视描述符。如果此inotify实例先前未监视文件系统对象,则新分配监视描述符。如果文件系统对象已被监视(可能通过指向同一对象的不同链接),则返回现有监视的描述符。
简单来说:如果你看到同一个文件的两个硬链接,他们的数字手表ID将是相同的。如果您将手表存储在类似hashmap的内容中,则此行为很容易导致丢失第二个inotify手表的跟踪,并使用整数手表ID。
第二个问题更难以观察,因此尽管甚至不是错误模式,但很少得到适当的支持:卸载当前通过inotify观察到的分区。棘手的部分是:Linux文件系统不允许您在打开文件描述符时卸载它们,但通过inotify 观察文件并不会阻止文件系统卸载。如果您的应用程序在单独的文件系统上观察文件,并且用户卸载该文件系统,则必须准备好处理生成的IN_UNMOUNT
事件。
上述所有测试都应该可以在tmpfs文件系统上执行。
答案 1 :(得分:1)
经过一番思考后,我想出了另一个解决方案。您可以使用Linux“seccomp”工具来“模拟”与单个inotify相关的系统调用的结果。这种方法的优点是简单,强大和完全非侵入性。您可以有条件地调整系统调用的行为,同时在其他情况下仍使用原始操作系统行为。从技术上讲,这仍然算作模拟,但是模拟层在内核代码和用户空间系统调用接口之间非常深入。
您不需要修改程序代码,只需编写一个包装器,在exec
之前安装适合的seccomp过滤器(下面的代码使用libseccomp):
// pass control to kernel syscall code by default
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
if (!ctx) exit(1);
// modify behavior of specific system call to return `EMFILE` error
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EMFILE), __NR_inotify_init, 0));
execve(...
Seccomp本质上是一个有限的解释器,运行BPF字节码的扩展版本,所以它的功能非常广泛。 libseccomp允许您安装有限的条件过滤器(例如,将系统调用的整数参数与常量值进行比较)。如果您想要实现更令人印象深刻的条件行为(例如比较文件路径,传递给inotify_add_watch
到预定义值),您可以将seccomp()系统调用与kernel bpf() facility的直接使用结合起来编写复杂的过滤程序在eBPF方言中。
编写系统调用过滤器可能很繁琐,而且受seccomp影响的程序行为实际上并不依赖于内核实现(在将控件传递给内核系统调用处理程序之前,内核会调用seccomp过滤器)。因此,您可能希望将seccomp的稀疏使用与更有机的方法相结合,在my other answer中进行了概述。
答案 2 :(得分:1)
可能不是您想要的非侵入性,但来自INotify
的{{1}}类很小。您可以完全包装它,委托所有方法,并注入错误。
代码看起来像这样:
inotify_simple
使用此代码,您可以在其他地方执行此操作:
from inotify_simple.inotify_simple import INotify
class WrapINotify(object):
init_error_list = []
add_watch_error_list = []
rm_watch_error_list = []
read_error_list = []
def raise_if_error(self, error_list):
if not error_list:
return
# Simulate INotify raising an exception
exception = error_list.pop(0)
raise exception
def __init__(self):
self.raise_if_error(WrapINotify.init_error_list)
self.inotify = INotify()
def add_watch(self, path, mask):
self.raise_if_error(WrapINotify.add_watch_error_list)
self.inotify.add_watch(path, mask)
def rm_watch(self, wd):
self.raise_if_error(WrapINotify.rm_watch_error_list)
return self.inotify.rm_watch(wd)
def read(self, timeout=None, read_delay=None):
self.raise_if_error(WrapINotify.read_error_list)
return self.inotify.read(timeout, read_delay)
def close(self):
self.inotify.close()
def __enter__(self):
return self.inotify.__enter__()
def __exit__(self, exc_type, exc_value, traceback):
self.inotify.__exit__(exc_type, exc_value, traceback)
注入错误。当然,您可以向包装类添加更多代码以实现不同的错误注入方案。