Skip to content

Commit 8fad46f

Browse files
committed
feat(fs): implement inotify filesystem event notification
Implements inotify (issue #2151): the fsnotify core notification layer, the inotify pseudo-device, 4 syscalls (init/init1/add_watch/rm_watch), and VFS write-path hooks for all standard events (create/delete/move/ modify/access/close/attrib/self events). Architecture: - fsnotify/ unified dispatch layer: global inode_id -> Weak<mark> index, TOTAL_WATCHES atomic fast-path (zero cost when no watches), lock-family separation (global index lock / events lock / wd lock never nested). - inotify.rs device: InotifyInode implements IndexNode + PollableInode, epoll-integrated via LockedEPItemLinkedList, exact inotify_event layout (name field aligned to sizeof(inotify_event)=16, matching Linux ABI). - VFS hooks placed in syscall-core layer (vcore/open/rename_utils/...), NOT per-filesystem: single anchor covers ext4/tmpfs/overlayfs/fuse. Hooks fire only after success and never alter syscall return values. Review fixes incorporated: - Directory watches receive child content events (issue B): IN_MODIFY/ ACCESS/OPEN/CLOSE delivered to parent dir watch with child name. - Guard DELETE_SELF on hardlink unlink/rename-over: only emit when i_nlink reaches 0, matching Linux fsnotify_link_count() semantics. - Fix MountFSInode downcast: MountFSInode::as_any_ref() returns the inner inode's Any, so downcast_ref::<MountFSInode>() always fails. Use downcast_arc instead so parent resolution works for child content event delivery. - TOTAL_WATCHES counter: avoid double-decrement / double-increment. Test: user/apps/tests/dunitest/suites/normal/inotify_dir_watch.cc covers directory-watch child content events and self-watch MODIFY. Design doc: docs/kernel/filesystem/inotify.md Signed-off-by: sparkzky <sparkhhhhhhhhhh@outlook.com>
1 parent 4ce6e06 commit 8fad46f

17 files changed

Lines changed: 2582 additions & 16 deletions

File tree

docs/kernel/filesystem/inotify.md

Lines changed: 570 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//! [`FsNotifyGroup`]:一个通知消费者(一个 inotify fd 对应一个 group)。
2+
3+
use alloc::boxed::Box;
4+
use alloc::sync::Arc;
5+
use alloc::vec::Vec;
6+
7+
use crate::filesystem::epoll::event_poll::LockedEPItemLinkedList;
8+
use crate::libs::mutex::Mutex;
9+
use crate::libs::wait_queue::WaitQueue;
10+
11+
use super::mark::FsNotifyMark;
12+
use super::FsNotifyBackend;
13+
14+
/// 一个通知消费者。一个 inotify fd 对应一个 group。
15+
///
16+
/// - `backend`:具体后端(自带内部锁),fsnotify 层只依赖 [`FsNotifyBackend`] trait;
17+
/// - `marks`:group 拥有的所有 mark(强引用,pin 住被监听 inode);
18+
/// - `wait_queue` / `epitems`:read 阻塞唤醒与 epoll 集成。
19+
#[derive(Debug)]
20+
pub struct FsNotifyGroup {
21+
pub backend: Box<dyn FsNotifyBackend>,
22+
pub marks: Mutex<Vec<Arc<FsNotifyMark>>>,
23+
pub wait_queue: WaitQueue,
24+
pub epitems: LockedEPItemLinkedList,
25+
}
26+
27+
impl FsNotifyGroup {
28+
pub fn new(backend: Box<dyn FsNotifyBackend>) -> Arc<Self> {
29+
Arc::new(Self {
30+
backend,
31+
marks: Mutex::new(Vec::new()),
32+
wait_queue: WaitQueue::default(),
33+
epitems: LockedEPItemLinkedList::default(),
34+
})
35+
}
36+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//! [`FsNotifyMark`]:一个 watch(group + inode + mask + wd)及其生命周期管理。
2+
3+
use alloc::sync::{Arc, Weak};
4+
use core::sync::atomic::{AtomicBool, AtomicU32};
5+
6+
use crate::filesystem::vfs::{IndexNode, InodeId};
7+
8+
use super::{adjust_total_watches, index_remove, FsNotifyGroup};
9+
10+
/// 一个 watch:连接 group 与 inode。
11+
///
12+
/// 生命周期:由 `group.marks` 持有强引用(pin 住被监听 inode),全局索引持 `Weak`。
13+
/// 撤销时机:`rm_watch`、`IN_DELETE_SELF`/`IN_UNMOUNT` 触发、group 销毁。
14+
#[derive(Debug)]
15+
pub struct FsNotifyMark {
16+
/// watch descriptor,group 内唯一。
17+
pub wd: i32,
18+
/// 所属 group(弱引用,避免环引用)。
19+
pub group: Weak<FsNotifyGroup>,
20+
/// 强引用:watch 期间 pin 住 inode(防 evict,保证 InodeId 不复用)。
21+
pub inode: Arc<dyn IndexNode>,
22+
/// 订阅 mask(`IN_MASK_ADD` 并发改,必须原子读)。
23+
pub mask: AtomicU32,
24+
/// `IN_ONESHOT`:触发一次后自动撤销。
25+
pub oneshot: AtomicBool,
26+
/// `IN_EXCL_UNLINK`:已 unlink 子项不再产生事件。
27+
pub excl_unlink: bool,
28+
}
29+
30+
impl FsNotifyMark {
31+
/// 取被监听 inode 的 `InodeId`(inode 被 pin,id 稳定)。
32+
pub fn inode_id(&self) -> InodeId {
33+
self.inode
34+
.metadata()
35+
.map(|m| m.inode_id)
36+
.unwrap_or(InodeId::new(0))
37+
}
38+
}
39+
40+
/// 撤销一个 mark:从 group.marks、全局索引移除,并维护全局计数。
41+
///
42+
/// 在 `rm_watch`、`DELETE_SELF`/`UNMOUNT` dispatch、group 销毁时调用。
43+
/// 注意:不取 events 锁,故与 read 路径互不阻塞(锁族分离)。
44+
pub fn destroy_mark(mark: &Arc<FsNotifyMark>) {
45+
let Some(group) = mark.group.upgrade() else {
46+
// group 已销毁,mark 仅可能残留在 snapshot 中;直接清索引即可。
47+
index_remove(mark);
48+
return;
49+
};
50+
51+
// 从 group.marks 移除(按指针相等)。
52+
let mut marks = group.marks.lock();
53+
let before = marks.len();
54+
marks.retain(|m| !Arc::ptr_eq(m, mark));
55+
let removed = before != marks.len();
56+
drop(marks);
57+
58+
if removed {
59+
// 投递 IN_IGNORED:watch 被撤销(rm_watch/oneshot/DELETE_SELF/UNMOUNT 均经此路径)。
60+
// shutdown(fd close) 不调用 destroy_mark,故不误发。
61+
group.backend.notify_ignored(&group, mark);
62+
// 通知后端从其内部结构(wd 表)移除。
63+
group.backend.free_mark(mark);
64+
// 从全局索引移除。
65+
index_remove(mark);
66+
// 维护全局 watch 计数(唯一计数器,覆盖上限检查 + 快速路径)。
67+
adjust_total_watches(-1);
68+
}
69+
}
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
//! 文件系统事件通知统一层(fsnotify)。
2+
//!
3+
//! 本模块是 VFS 写路径 hook 与具体后端(当前仅 inotify)之间的解耦层。
4+
//! VFS hook 只调用 [`fsnotify`],由它查全局 mark 索引并把事件分发给匹配的 watch。
5+
//!
6+
//! 设计原则(见 `docs/kernel/filesystem/inotify.md` §0/§3):
7+
//! - `fsnotify()` 尽力而为,绝不影响 syscall 返回值;
8+
//! - `fsnotify()` 内部只取本层自旋锁与 group 队列锁,绝不回调 `IndexNode` 写方法;
9+
//! - 锁序:`MountFSInode/File 锁` → `FSNOTIFY 全局锁` → `group 队列锁`,永不反向。
10+
11+
pub mod group;
12+
pub mod mark;
13+
14+
use alloc::sync::Arc;
15+
use alloc::vec::Vec;
16+
use core::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
17+
18+
use hashbrown::HashMap;
19+
20+
use crate::filesystem::vfs::{FileType, IndexNode, InodeId};
21+
use crate::libs::spinlock::SpinLock;
22+
use system_error::SystemError;
23+
24+
pub use group::FsNotifyGroup;
25+
pub use mark::FsNotifyMark;
26+
27+
// 事件 mask:对应 Linux 内核 `FS_*` 事件,其比特位与用户态 `IN_*` 完全一致,
28+
// 故可直接作为用户态 mask 使用(仅 `ISDIR` 由 dispatch 按需设置)。
29+
//
30+
// 参考:Linux `include/uapi/linux/inotify.h`、`include/linux/fsnotify_backend.h`。
31+
bitflags::bitflags! {
32+
pub struct FsEvent: u32 {
33+
const ACCESS = 0x00000001; // IN_ACCESS
34+
const MODIFY = 0x00000002; // IN_MODIFY
35+
const ATTRIB = 0x00000004; // IN_ATTRIB
36+
const CLOSE_WRITE = 0x00000008; // IN_CLOSE_WRITE
37+
const CLOSE_NOWRITE= 0x00000010; // IN_CLOSE_NOWRITE
38+
const OPEN = 0x00000020; // IN_OPEN
39+
const MOVED_FROM = 0x00000040; // IN_MOVED_FROM
40+
const MOVED_TO = 0x00000080; // IN_MOVED_TO
41+
const CREATE = 0x00000100; // IN_CREATE
42+
const DELETE = 0x00000200; // IN_DELETE
43+
const DELETE_SELF = 0x00000400; // IN_DELETE_SELF
44+
const MOVE_SELF = 0x00000800; // IN_MOVE_SELF
45+
const UNMOUNT = 0x00002000; // IN_UNMOUNT(文件系统卸载)
46+
const Q_OVERFLOW = 0x00004000; // IN_Q_OVERFLOW(队列溢出)
47+
const IN_IGNORED = 0x00008000; // watch 被撤销(inode 删除/卸载)
48+
const ISDIR = 0x40000000; // 事件对象是目录(由 dispatch 设置)
49+
}
50+
}
51+
52+
/// 后端接口(最小抽象)。
53+
///
54+
/// fsnotify 层通过此 trait 调用具体后端,保持 VFS → fsnotify → inotify 单向依赖。
55+
pub trait FsNotifyBackend: Send + Sync + core::fmt::Debug {
56+
/// 处理一个事件:格式化、(可选)合并、入队,并唤醒等待者。
57+
fn handle_event(
58+
&self,
59+
group: &FsNotifyGroup,
60+
mark: &FsNotifyMark,
61+
mask: FsEvent,
62+
name: Option<&str>,
63+
cookie: u32,
64+
);
65+
/// mark 销毁时从后端内部结构(如 wd 表)移除。
66+
fn free_mark(&self, mark: &FsNotifyMark);
67+
/// mark 被撤销时向消费者投递一个 IN_IGNORED 事件(rm_watch/oneshot/DELETE_SELF/UNMOUNT)。
68+
/// fd close(shutdown) 路径不调用此方法。
69+
fn notify_ignored(&self, group: &FsNotifyGroup, mark: &FsNotifyMark);
70+
/// poll 用:队列是否非空。
71+
fn queue_nonempty(&self) -> bool;
72+
}
73+
74+
/// 全局 watch 计数:绝大多数时刻为 0。`fsnotify` 的第一道闸门——
75+
/// 无 watch 时零锁开销(对齐 Linux `i_fsnotify_mask` 快速跳过)。
76+
static TOTAL_WATCHES: AtomicUsize = AtomicUsize::new(0);
77+
78+
/// move 事件 cookie 分配器:每次 rename 取一个,FROM/TO 共享。
79+
/// 0 表示「无 move」,故从 1 开始,回绕时跳过 0。
80+
static NEXT_COOKIE: AtomicU32 = AtomicU32::new(1);
81+
82+
/// 取一个新的非零 move cookie。
83+
pub fn next_cookie() -> u32 {
84+
loop {
85+
let c = NEXT_COOKIE.fetch_add(1, Ordering::Relaxed);
86+
if c != 0 {
87+
return c;
88+
}
89+
}
90+
}
91+
// 全局 mark 索引:用 `InodeId` 反查「挂在该 inode 上的所有 mark」。
92+
//
93+
// 存 `Weak<FsNotifyMark>`:group 拥有 mark(强引用),索引只做查找,不阻止回收。
94+
// dispatch 时 `Weak::upgrade()` 失败的死引用会被惰性剔除。
95+
lazy_static::lazy_static! {
96+
static ref FSNOTIFY_MARKS: SpinLock<HashMap<InodeId, Vec<alloc::sync::Weak<FsNotifyMark>>>> =
97+
SpinLock::new(HashMap::new());
98+
}
99+
100+
/// 记录一次 watch 计数变更(add +1,撤销 -1)。仅用于短路,Relaxed 即可。
101+
pub(crate) fn adjust_total_watches(delta: i32) {
102+
if delta >= 0 {
103+
TOTAL_WATCHES.fetch_add(delta as usize, Ordering::Relaxed);
104+
} else {
105+
TOTAL_WATCHES.fetch_sub((-delta) as usize, Ordering::Relaxed);
106+
}
107+
}
108+
109+
/// 系统中是否存在任意 inotify watch。供 VFS 热路径(open/read/write/close)做廉价短路:
110+
/// 无 watch 时完全跳过 parent 解析与 fsnotify 调用(零开销)。
111+
pub fn has_any_watch() -> bool {
112+
TOTAL_WATCHES.load(Ordering::Relaxed) != 0
113+
}
114+
115+
/// 原子地预留一个 watch 槽位(用于上限检查)。
116+
/// 成功时 TOTAL_WATCHES 已 +1;超限时回退并返回 ENOSPC。
117+
/// 这是唯一的全局 watch 计数器,同时服务「快速路径短路」和「max_user_watches 上限检查」。
118+
pub(crate) fn try_reserve_watch(max: usize) -> Result<(), SystemError> {
119+
let prev = TOTAL_WATCHES.fetch_add(1, Ordering::Relaxed);
120+
if prev >= max {
121+
TOTAL_WATCHES.fetch_sub(1, Ordering::Relaxed);
122+
return Err(SystemError::ENOSPC);
123+
}
124+
Ok(())
125+
}
126+
127+
/// 把 mark 加入全局索引(add_watch 调用)。
128+
pub(crate) fn index_add(mark: &Arc<FsNotifyMark>) {
129+
let id = mark.inode_id();
130+
let mut idx = FSNOTIFY_MARKS.lock_irqsave();
131+
idx.entry(id).or_default().push(Arc::downgrade(mark));
132+
}
133+
134+
/// 把 mark 从全局索引移除(按指针相等匹配,rm_watch / 撤销时调用)。
135+
pub(crate) fn index_remove(mark: &FsNotifyMark) {
136+
let id = mark.inode_id();
137+
let self_ptr = mark as *const FsNotifyMark;
138+
let mut idx = FSNOTIFY_MARKS.lock_irqsave();
139+
if let Some(vec) = idx.get_mut(&id) {
140+
let mut i = 0;
141+
while i < vec.len() {
142+
// 剔除:指针相等(同一个 mark),或 Weak 已死。
143+
let drop_it = match vec[i].upgrade() {
144+
Some(arc) => alloc::sync::Arc::as_ptr(&arc) as *const FsNotifyMark == self_ptr,
145+
None => true,
146+
};
147+
if drop_it {
148+
vec.swap_remove(i);
149+
} else {
150+
i += 1;
151+
}
152+
}
153+
if vec.is_empty() {
154+
idx.remove(&id);
155+
}
156+
}
157+
}
158+
159+
/// 统一事件投递入口。在 VFS 操作**成功之后**调用,尽力而为,不影响调用方返回值。
160+
///
161+
/// - `parent`:对子项事件(CREATE/DELETE/MOVED_*),传 `(父目录 inode, 子项名)`;
162+
/// - `child`:对自身事件(DELETE_SELF/MOVE_SELF/MODIFY/CLOSE/OPEN/ATTRIB),传目标 inode;
163+
/// - 二者可同时非空(如 unlink:父目录得 `IN_DELETE`,子项得 `IN_DELETE_SELF`)。
164+
///
165+
/// # 安全性
166+
/// 内部只读 `inode.metadata()`(inode 活着、metadata 只读不锁),不调用任何写方法,
167+
/// 避免在调用方持有的 VFS/File 锁下重入。
168+
pub fn fsnotify(
169+
mask: FsEvent,
170+
parent: Option<(&Arc<dyn IndexNode>, &str)>,
171+
child: Option<&Arc<dyn IndexNode>>,
172+
cookie: u32,
173+
) {
174+
// ① 快速路径:系统无任何 watch → 直接返回(read/write/close 热路径零成本)。
175+
if TOTAL_WATCHES.load(Ordering::Relaxed) == 0 {
176+
return;
177+
}
178+
179+
// ② 预取 inode 元数据(inode 活着、metadata 只读不锁,安全)。
180+
// 事件的「主体」是 child(被创建/删除/移动/修改的对象);IN_ISDIR 由主体是否为
181+
// 目录决定,对 parent/child 两类 watch 一视同仁。若无 child(仅父目录自身事件
182+
// 的退化情况),ISDIR 不置位。
183+
let (child_id, event_is_dir, child_unlinked) = match child {
184+
Some(c) => match c.metadata() {
185+
Ok(md) => (
186+
Some(md.inode_id),
187+
md.file_type == FileType::Dir,
188+
md.nlinks == 0,
189+
),
190+
Err(_) => (None, false, false),
191+
},
192+
None => (None, false, false),
193+
};
194+
let parent_id = parent.and_then(|(p, _)| p.metadata().ok().map(|md| md.inode_id));
195+
196+
if child_id.is_none() && parent_id.is_none() {
197+
return;
198+
}
199+
200+
// ③ 收集候选 mark 快照:临界区仅做哈希查表(秒放),不做后端工作。
201+
// (mark 强引用, name, is_parent)
202+
let mut snapshot: Vec<(Arc<FsNotifyMark>, Option<&str>, bool)> = Vec::new();
203+
{
204+
let idx = FSNOTIFY_MARKS.lock_irqsave();
205+
if let Some(pid) = parent_id {
206+
let name = parent.map(|(_, n)| n);
207+
if let Some(vec) = idx.get(&pid) {
208+
for w in vec.iter() {
209+
if let Some(m) = w.upgrade() {
210+
snapshot.push((m, name, true));
211+
}
212+
}
213+
}
214+
}
215+
if let Some(cid) = child_id {
216+
if let Some(vec) = idx.get(&cid) {
217+
for w in vec.iter() {
218+
if let Some(m) = w.upgrade() {
219+
snapshot.push((m, None, false));
220+
}
221+
}
222+
}
223+
}
224+
}
225+
// 注:死 Weak 在 lock 内 upgrade 失败时被跳过;惰性清理留给 index_remove。
226+
227+
// 事件路由(Linux 模型):
228+
// - 命名空间事件 CREATE/DELETE/MOVED_FROM/MOVED_TO:仅父目录 watch 收(带 name);
229+
// - 自身事件 DELETE_SELF/MOVE_SELF:仅子项自身 watch 收;
230+
// - 内容类事件 ACCESS/MODIFY/ATTRIB/CLOSE_*/OPEN:父目录 watch(带 name)与子项自身 watch
231+
// 均收——使「监听目录」能收到子文件被读/写/开关/改属性的事件(inotify 头号用例)。
232+
// 一次 fsnotify 调用可同时通知父目录与子项(如 unlink:父得 IN_DELETE,子得 IN_DELETE_SELF)。
233+
let self_only = FsEvent::DELETE_SELF | FsEvent::MOVE_SELF;
234+
let parent_only = FsEvent::CREATE | FsEvent::DELETE | FsEvent::MOVED_FROM | FsEvent::MOVED_TO;
235+
// 内容类事件(IN_EXCL_UNLINK 抑制对象)。
236+
let content_type = FsEvent::MODIFY
237+
| FsEvent::ACCESS
238+
| FsEvent::ATTRIB
239+
| FsEvent::CLOSE_WRITE
240+
| FsEvent::CLOSE_NOWRITE
241+
| FsEvent::OPEN;
242+
243+
// ④ 锁外投递。
244+
for (mark, name, is_parent) in snapshot {
245+
// 父 mark 收除 self_only 外的全部;自身 mark 收除 parent_only 外的全部。
246+
let routed = if is_parent {
247+
mask & !self_only
248+
} else {
249+
mask & !parent_only
250+
};
251+
if routed.is_empty() {
252+
continue;
253+
}
254+
let subscribed = mark.mask.load(Ordering::Relaxed);
255+
if (subscribed & routed.bits()) == 0 {
256+
// 该 watch 未订阅此事件
257+
continue;
258+
}
259+
260+
// IN_EXCL_UNLINK:仅对子项的「内容类」事件、且子项已 unlink 时抑制。
261+
if is_parent && mark.excl_unlink && routed.intersects(content_type) && child_unlinked {
262+
continue;
263+
}
264+
265+
// dispatch 设置 ISDIR(主体是目录时)。
266+
let mut delivered = routed;
267+
if event_is_dir {
268+
delivered |= FsEvent::ISDIR;
269+
}
270+
271+
let destroy = delivered.contains(FsEvent::DELETE_SELF)
272+
|| delivered.contains(FsEvent::UNMOUNT)
273+
|| mark.oneshot.load(Ordering::Relaxed);
274+
275+
if let Some(group) = mark.group.upgrade() {
276+
group
277+
.backend
278+
.handle_event(&group, &mark, delivered, name, cookie);
279+
}
280+
281+
if destroy {
282+
mark::destroy_mark(&mark);
283+
}
284+
}
285+
}

0 commit comments

Comments
 (0)