什么是FUSE_INTERRUPT
FUSE_INTERRUPT是FUSE协议中的一个控制消息(opcode 36),不是普通文件操作,而是内核发给用户态daemon的一条指令:”发起某个请求的那个进程被信号打断了,请尽快终止那个在途请求“。
按协议定义(linux/include/uapi/linux/fuse.h):
FUSE_INTERRUPT = 36,
struct fuse_interrupt_in {
uint64_t unique; // 指向被中断的原始请求的唯一 ID
};
它由内核发起,携带一个unique字段,指明要终止的是哪一个在途请求。核心语义是“尽力而为的提示”, daemon可以终止该请求,也可以无视它继续做完。
发送中断后,内核仍然会等待daemon恢复原始请求。
为什么需要FUSE_INTERRUPT
FUSE的daemon往往是网络文件系统,例如当前的gcsfuse。单个请求可能挂在远端RPC上几百毫秒甚至更久,没有中断机制就会出现很糟糕的体验:
用户Ctrl+C -> 进程收到SIGINT -> 进程正阻塞在某个FUSE系统调用上 -> 该调用要等daemon完成远端RPC才能返回 -> 进程“杀不死”,而是“卡死”。
FUSE_INTERRUPT就是为此设计的:宕内核发现阻塞在FUSE请求上的进程收到了信号,它通知daemon “这个请求的发起者被打断了,能终止就终止“,让系统调用尽快以EINTR返回,进程得以响应信号。这是内核吧“中断语义”从本地块设备(请求交给硬件后无法撤销)延伸到“可编程的daemon"的手段。
FUSE_INTERRUPT如何工作
1. 内核侧:信号触发中断
(linux/fs/fuse/dev.c:368-386):
if (!fc->no_interrupt) {
/* Any signal may interrupt this */
err = wait_event_interruptible(req->waitq, test_bit(FR_FINISHED, &req->flags));
if (!err)
return;
set_bit(FR_INTERRUPTED, &req->flags);
if (test_bit(FR_SENT, &req->flags))
queue_interrupt(req);
}
任何未屏蔽的信号(SIGCHLD, SIGURG, SIGINT,...)都可能触发,且触发后继续wait_event_killable等原始请求的回复,所以中断不等于请求失败。
2. 内核发出FUSE_INTERRUPT
通过fuse_interrupt_in.unique指认原始请求。
3. 用户态jacobasa/fuse库:每个请求配一个可取消的ctx
内核 -> jacobsa/fuse库中的实现如下所示:
a. Connection.beginOp中,为每个正常op创建一个可cancel的context.WithCancel,并把cancel函数记录在c.cancelFuncs[fuseID]里。
func (c *Connection) beginOp(
opCode uint32,
fuseID uint64) context.Context {
// Start with the parent context.
ctx := c.cfg.OpContext
if opCode != fusekernel.OpForget {
var cancel func()
ctx, cancel = context.WithCancel(ctx)
c.recordCancelFunc(fuseID, cancel)
}
return ctx
}
b. Connection.ReadOp在读到OpInterrupt消息时特殊处理:调用handleInterrupt,查表拿到对应的cancel并调用它,这会让传给gcsfuse业务方法的ctx.Done()触发。
func (c *Connection) ReadOp() (_ context.Context, op interface{}, _ error) {
...
// connection.go:489-493 ReadOp 里特殊处理 interruptOp:不返回给用户,
// 而是内联调用 handleInterrupt 后 continue 循环
if interruptOp, ok := op.(*interruptOp); ok {
c.handleInterrupt(interruptOp.FuseID)
continue
}
...
}
func (c *Connection) handleInterrupt(fuseID uint64) {
c.mu.Lock()
defer c.mu.Unlock()
cancel, ok := c.cancelFuncs[fuseID]
if !ok {
// 已经Reply过了,interrupt 来迟了,忽略即可
return
}
cancel()
}
c. finishOp在正常回复前清理map。
这一层gcsfuse无法关闭,因为这是vendor库内部行为,中断信号照常会被内核发出、被库处理、cancel掉传入的context。
// connection.go:336-357 finishOp 在原请求 Reply 之后清理 cancelFuncs,
// 避免 map 泄漏,也避免 unique id 被复用时状态串了
func (c *Connection) finishOp(opCode uint32, fuseID uint64) {
...
cancel, ok := c.cancelFuncs[fuseID]
cancel()
delete(c.cancelFuncs, fuseID)
}
最终这个cancel()触发的是暴露给用户文件系统实现的ctx.Done()
4. FUSE daemon侧:
gcsfuse使用--ignore-interrupts(默认为true)来实现“gcsfuse是否忽略中断请求“的功能。
若该值设置为false,则流程为:FS op拿到的ctx被取消 --> 在途GCS FUSE RPC被终止 --> 返回context canceled --> 内核收到EIO。该值默认为true,是如何实现“不受到内核中断的影响”呢?
gcsfuse自己的“忽略”逻辑:
在拿到已经被库cancel掉的ctx之后,重新包一层不受它影响的新context:
func (fs *fileSystem) getInterruptlessContext(ctx context.Context) context.Context {
if fs.newConfig.FileSystem.IgnoreInterrupts {
// When ignore interrupts config is set, we are creating a new context not
// cancellable by parent context.
newCtx := context.Background()
return fs.traceHandle.PropagateTraceContext(newCtx, ctx)
}
return ctx
}
几乎每个FUSE操作入口(LookUpInode, ReadFile, WriteFile, FlushFile, SyncFile,...共18个调用点)都会先执行ctx = getInterruptlessContext(),这一行的效果就是当配置为true时,丢弃库传进来的、会被中断打断的ctx,换成一个基于context.Background()的全新context(只保留trace信息,不保留取消关系),这样即使内核后续对该请求发了FUSE_INTERRUPT并cancel了原ctx,gcsfuse内部真正用来做GCS I/O的context也不会被cancel,操作会一直跑到完成。
为什么GCSFUSE要设置ignore-interrupts默认值为true?
这是因为GCSFUSE后端为远端对象存储,其后端操作不是幂等/原子可回滚的本地内存操作,而是远端网络I/O(上传/下载/rename的copy+delete等)。如果一个Ctrl-C导致gcsfuse的WriteFile/FlushFile/SyncFile的context被cancel,正在进行的GCS请求就会在半途中被掐断。造成:
- 文件在 GCS 端处于不完整/不一致状态;
- 缓存文件与远端对象不一致;
- 用户看到的错误令人困惑(不是"写失败"而是莫名其妙的 context canceled)。
而本地文件系统这种“半途取消”通常是安全的(内存操作可回滚),但gcsfuse的很多操作一旦发往GCS就很难“干净地”半途撤销,所以开发者选择让gcsfuse默认不理会内核转发过来的中断请求,让原有的文件系统操作(如上传)无论如何都跑完,以保证数据的完整性,牺牲的是“用户按Ctrl-C能立刻打断卡住的操作“这一响应性。
对应的历史演进(PR 时间线,来自 git log):
- #1863/#1860:先加 flag/config,再实现"忽略中断"的逻辑;
- #2034 "Ignore interrupts by default":把默认值从 false 改成 true ——说明团队观察到默认响应中断反而带来更多问题(数据不一致 / 中断态奇怪报错),所以把"忽略"设为默认行为,只给需要"可被 Ctrl-C 打断"语义的用户开一个开关关闭它。
总结
本质上gcsfuse 中的ignore-interrupts的参数时在“POSIX中断语义(可被Ctrl-C打断)“与”远端写操作的原子性/一致性“之间做的一个全局粗粒度的取舍。默认对所有操作类型的中断都是”不理会中断“。