file 类型无法取消 gc 时文件关闭的 Finalizer
go 里创建一个 file 时,会默认设置一个 Finalizer,当这个 File 回收时触发,关闭文件 fd,防止 fd 泄露。
但是我们不能通过 runtime.SetFinalizer(file, nil) 取消 File fd 回收的逻辑。原因在于 File 创建的逻辑:
go 标准库里, runtime.SetFinalizer(f.file, (*file).close) 把 Finalizer 设置在私有对象 File.file 上了,外层用户如果仅仅在 File 上取消 Finalizer,当 GC 开始时,f.file 仍然会调用 close 进行处理。
又由于 f.file 是私有类型,外层用户是不能进行 runtime.SetFinalizer(f.file, nil) 的,故无法取消 File 回收时触发,关闭文件 fd 的逻辑。
go1.12 的新逻辑
package main
import (
"errors"
"fmt"
"os"
"runtime"
"syscall"
"time"
)
func main() {
r := runner{}
if err := r.lock(); err != nil {
fmt.Printf("%s\n", err)
return
}
r.run()
}
func (r *runner) run() {
fmt.Println("vim-go")
for {
fmt.Println("trigger")
runtime.GC()
time.Sleep(time.Second)
}
}
type runner struct {
f *os.File
}
func (r *runner) lock() error {
f, err := LockUntilEx("/tmp/abc")
r.f = f
return err
}
func LockUntilEx(lockfile string) (*os.File, error) {
var mode os.FileMode = 0777
file, err := os.OpenFile(lockfile, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, mode)
if err != nil {
return nil, errors.New(fmt.Sprintf("fail to open %s ,because %s", lockfile, err))
}
err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if err != nil {
return nil, errors.New(fmt.Sprintf("fail to lock %s, because %s", lockfile, err))
}
_, _, errno := syscall.RawSyscall(syscall.SYS_FCNTL, file.Fd(), syscall.F_SETFD, syscall.FD_CLOEXEC)
if errno != 0 {
return nil, errors.New(fmt.Sprintf("fail to set FD_CLOEXEC, for ", lockfile))
}
runtime.SetFinalizer(file, func(*os.File) { fmt.Println("gc") })
return file, nil
}
在 go1.12 以前,这段代码产生的进程是可以一直持有锁的。
从 go1.12 开始,这段代码产生的进程在手动 GC 两次后就立马释放了文件锁。runner 对象在第一次 GC 时回收,runner.f 在第二次 GC 时回收。
具体可见 go1.12 release note 中编译器的改进
如何保证文件锁不会释放。
runtime.KeepAlive(f)
在进程不需要文件锁之前,必须要保证文件锁的 file 对象一直是 reachable。可以使用 runtime.KeepAlive。如:
func main() {
r := runner{}
if err := r.lock(); err != nil {
fmt.Printf("%s\n", err)
return
}
defer runtime.KeepAlive(r.f)
r.run()
}