父目录监视能看到替换后的名字,旧 inode 监视不会自动跟踪当前文件。

inotify 报告变化,但不是完整历史记录

inotify 通过一个带注册监视项和可读事件队列的实例,投递 Linux 文件系统通知。每条记录包含监视编号、事件掩码,也可能包含名字和移动 cookie。这能减少对未变化路径的反复检查,但事件流不是无限审计日志。正确消费者应把通知理解为相关状态可能需要重新检查的证据,而不能默认它保存了文件发生过的每一次操作。 坏掉的样子 监视目录不会递归监视后代。它报告直接目录项上的相关活动,因此需要观察更深变化时,新子目录也需要独立监视。目录树监视器必须处理初次枚举和之后加入的目录。假定只注册根目录就覆盖整棵树,会悄悄漏掉更深层变化;能收到子目录被创建,并不代表能收到它内部之后的全部操作。 文件监视跟随具体对象,不会永久绑定路径。编辑器可以先写完并关闭临时文件,再用 rename 替换目标。父目录会报告替换,旧文件监视却不会转移到新 inode。如果消费者等待目标名字之后再出现 close-write,可能永远等不到,因为替代文件在发布前已经关闭。不能把一种编辑器保存顺序当成所有更新方式的固定流程。 尚未读取的相同事件可以合并,队列满后会丢弃事件并报告 IN_Q_OVERFLOW。因此,事件数量不是可靠写入计数。溢出后,消费者无法仅靠剩余记录重建全部缺失操作,必须重新核对当前监视状态,不能继续假定根据事件维护的模型完整。历史丢失与当前状态仍可读取,是需要分别理解的两件事。 快速验证 把脚本保存为 inotify-events.py,在一次性 Linux 环境中运行 python3 inotify-events.py。实测使用 Linux 6.12.76、Python 3.11.2 和 ext4。样本创建 ASCII 名称的临时条目,并有意填满自己的队列。配置的队列上限必须不超过三万二千七百六十八;脚本只读取上限,不修改任何 sysctl: from pathlib import Path from tempfile import TemporaryDirectory import ctypes,os,struct MODIFY=0x2;ATTRIB=0x4;CLOSE_WRITE=0x8;MOVED_FROM=0x40;MOVED_TO=0x80 CREATE=0x100;DELETE=0x200;DELETE_SELF=0x400;MOVE_SELF=0x800;OVERFLOW=0x4000;IGNORED=0x8000 libc=ctypes.CDLL(None,use_errno=True) libc.inotify_init1.argtypes=[ctypes.c_int];libc.inotify_init1.restype=ctypes.c_int libc.inotify_add_watch.argtypes=[ctypes.c_int,ctypes.c_char_p,ctypes.c_uint32];libc.inotify_add_watch.restype=ctypes.c_int header=struct.Struct('iIII') with TemporaryDirectory(prefix='xbcatm-inotify-') as directory: root=Path(directory);fd=libc.inotify_init1(os.O_NONBLOCK|os.O_CLOEXEC);assert fd>=0,os.strerror(ctypes.get_errno()) def watch(path,mask): wd=libc.inotify_add_watch(fd,os.fsencode(path),mask);assert wd>=0,os.strerror(ctypes.get_errno());return wd def drain(): events=[] while True: try:data=os.read(fd,65536) except BlockingIOError:return events offset=0 while offset<len(data): wd,mask,cookie,length=header.unpack_from(data,offset);offset+=header.size name=data[offset:offset+length].split(b'\0',1)[0].decode();offset+=length events.append((wd,mask,cookie,name)) try: parent=watch(root,MODIFY|CLOSE_WRITE|CREATE|DELETE|MOVED_FROM|MOVED_TO) sub=root/'sub';sub.mkdir();drain();(sub/'unwatched').touch();assert drain()==[] nested=watch(sub,CREATE);(sub/'watched').touch();assert any(wd==nested and mask&CREATE and name=='watched' for wd,mask,_,name in drain()) print('a directory watch is not recursive; adding a child-directory watch observes its new entries') counter=root/'counter';stream=os.open(counter,os.O_CREAT|os.O_WRONLY,0o600);drain() try: for _ in range(3):os.write(stream,b'x') finally:os.close(stream) events=drain();assert sum(wd==parent and bool(mask&MODIFY) and name=='counter' for wd,mask,_,name in events)==1 moved=root/'moved';counter.rename(moved);events=drain() before=next(e for e in events if e[1]&MOVED_FROM and e[3]=='counter');after=next(e for e in events if e[1]&MOVED_TO and e[3]=='moved') assert before[2]==after[2]!=0 print('three unread identical modifications coalesce; rename FROM/TO events share a nonzero cookie') target=root/'target';target.write_bytes(b'old');file_watch=watch(target,MODIFY|ATTRIB|DELETE_SELF|MOVE_SELF);drain() replacement=root/'replacement';replacement.write_bytes(b'new');drain();replacement.replace(target);events=drain() assert any(wd==file_watch and mask&IGNORED for wd,mask,_,_ in events) assert any(wd==parent and mask&MOVED_TO and name=='target' for wd,mask,_,name in events) assert not any(wd==parent and mask&CLOSE_WRITE and name=='target' for wd,mask,_,name in events) target.write_bytes(b'newer');events=drain() assert any(wd==parent and mask&MODIFY and name=='target' for wd,mask,_,name in events) assert all(wd!=file_watch for wd,_,_,_ in events) print('replacement invalidates the old file watch; the parent-directory watch continues to observe the pathname') burst=root/'burst';burst.mkdir();drain();watch(burst,CREATE) limit=int(Path('/proc/sys/fs/inotify/max_queued_events').read_text());assert 0<limit<=32768,'use a disposable environment with a bounded queue limit' for number in range(limit+8):(burst/str(number)).touch() events=drain();assert any(wd==-1 and mask&OVERFLOW for wd,mask,_,_ in events) current=len(list(burst.iterdir()));assert current==limit+8 print('actual queue overflow:',len(events),'events retained for',current,'creates; rescan sees the current entries') finally:os.close(fd) 第一组监视父目录,创建子目录,并读走父级事件。随后在子目录内部创建文件,不会向父监视项产生事件;显式增加子目录监视后,下一次创建才被看到。这在同一个 inotify 实例中区分直接目录项覆盖和递归监视,避免只检查注册调用成功就以为整棵树都已进入观察范围。 ...

十月 13, 2027 · 2 分钟 · XB Field Notes