Skip to content

Commit

Permalink
fuse: fix tests for Go 1.9
Browse files Browse the repository at this point in the history
Go 1.9 uses epoll() for more efficient file I/O. File I/O causes a
call to epoll, and the runtime makes this call take up a GOMAXPROCS
slot.

The FUSE kernel module also supports poll: polling on a file residing
in a FUSE file system causes the kernel to sends a POLL request to the
userspace process.  If the process responds with ENOSYS, the kernel
will stop forwarding poll requests to the FUSE process.

In a test for Go FUSE file systems, it is normal to serve the
filesystem out of the same process that opens files in the file
system. If this happens in Go 1.9, the epoll call can take the only
GOMAXPROCS slot left, leaving the process unable to respond to the
FUSE POLL opcode, deadlocking the process.

This change add support for a magic file "/ .go-fuse-epoll-hack" with
node ID uint64(-1), and on starting up the file system, the library
calls poll() on this file, triggering the POLL opcode before the Go
runtime had a chance to do so.

There are two problem scenarios left:

* File system tests that start I/O before calling WaitMount() still
  risk deadlocking themselves.

* The Linux kernel keeps track of feature support in fuse_conn, which notes

         * The following bitfields are only for optimization purposes
         * and hence races in setting them will not cause malfunction

  if our forced ENOSYS gets lost due to a race condition in the
  kernel, this can still trigger.

Fixes golang/go#21014 and #165
  • Loading branch information
hanwen committed Jul 15, 2017
1 parent f7e021d commit 4f10e24
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 6 deletions.
4 changes: 1 addition & 3 deletions README
Expand Up @@ -129,12 +129,10 @@ KNOWN PROBLEMS

Grep source code for TODO. Major topics:

* Support for umask in Create

* Missing support for network FS file locking: FUSE_GETLK, FUSE_SETLK,
FUSE_SETLKW

* Missing support for FUSE_INTERRUPT, CUSE, BMAP, POLL, IOCTL
* Missing support for FUSE_INTERRUPT, CUSE, BMAP, IOCTL

* In the path API, renames are racy; See also:

Expand Down
22 changes: 22 additions & 0 deletions fuse/opcode.go
Expand Up @@ -129,6 +129,22 @@ func doOpen(server *Server, req *request) {

func doCreate(server *Server, req *request) {
out := (*CreateOut)(req.outData)
if req.filenames[0] == pollHackName && req.inHeader.NodeId == FUSE_ROOT_ID {
out.EntryOut = EntryOut{
NodeId: pollHackInode,
Attr: Attr{
Ino: pollHackInode,
Mode: S_IFREG | 0644,
Nlink: 1,
},
}
out.OpenOut = OpenOut{
Fh: pollHackInode,
}
req.status = OK
return
}

status := server.fileSystem.Create((*CreateIn)(req.inData), req.filenames[0], out)
req.status = status
}
Expand Down Expand Up @@ -246,6 +262,9 @@ func doGetAttr(server *Server, req *request) {

// doForget - forget one NodeId
func doForget(server *Server, req *request) {
if req.inHeader.NodeId == pollHackInode {
return
}
if !server.opts.RememberInodes {
server.fileSystem.Forget(req.inHeader.NodeId, (*ForgetIn)(req.inData).Nlookup)
}
Expand All @@ -272,6 +291,9 @@ func doBatchForget(server *Server, req *request) {
if server.opts.Debug {
log.Printf("doBatchForget: forgetting %d of %d: NodeId: %d, Nlookup: %d", i+1, len(forgets), f.NodeId, f.Nlookup)
}
if f.NodeId == pollHackInode {
continue
}
server.fileSystem.Forget(f.NodeId, f.Nlookup)
}
}
Expand Down
42 changes: 39 additions & 3 deletions fuse/server.go
Expand Up @@ -15,6 +15,8 @@ import (
"syscall"
"time"
"unsafe"

"golang.org/x/sys/unix"
)

const (
Expand Down Expand Up @@ -55,6 +57,9 @@ type Server struct {
ready chan error
}

const pollHackName = ".go-fuse-epoll-hack"
const pollHackInode = ^uint64(0)

// SetDebug is deprecated. Use MountOptions.Debug instead.
func (ms *Server) SetDebug(dbg bool) {
// This will typically trigger the race detector.
Expand Down Expand Up @@ -161,6 +166,7 @@ func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server
// FUSE device: on unmount, sometime some reads do not
// error-out, meaning that unmount will hang.
singleReader: runtime.GOOS == "darwin",
ready: make(chan error, 1),
}
ms.reqPool.New = func() interface{} { return new(request) }
ms.readPool.New = func() interface{} { return make([]byte, o.MaxWrite+pageSize) }
Expand All @@ -173,7 +179,6 @@ func NewServer(fs RawFileSystem, mountPoint string, opts *MountOptions) (*Server
}
mountPoint = filepath.Clean(filepath.Join(cwd, mountPoint))
}
ms.ready = make(chan error, 1)
fd, err := mount(mountPoint, &o, ms.ready)
if err != nil {
return nil, err
Expand Down Expand Up @@ -389,7 +394,13 @@ func (ms *Server) handleRequest(req *request) Status {
log.Println(req.InputDebug())
}

if req.status.Ok() && req.handler.Func == nil {
if req.inHeader.Opcode == _OP_POLL {
req.status = ENOSYS
} else if req.inHeader.NodeId == pollHackInode {
// We want to avoid switching off features through our
// poll hack, so don't use ENOSYS
req.status = EIO
} else if req.status.Ok() && req.handler.Func == nil {
log.Printf("Unimplemented opcode %v", operationName(req.inHeader.Opcode))
req.status = ENOSYS
}
Expand Down Expand Up @@ -582,5 +593,30 @@ func init() {
// Currently, this call only necessary on OSX.
func (ms *Server) WaitMount() error {
err := <-ms.ready
return err
if err != nil {
return err
}
return pollHack(ms.mountPoint)
}

// Go 1.9 introduces polling for file I/O. The implementation causes
// the runtime's epoll to take up the last GOMAXPROCS slot, and if
// that happens, we won't have any threads left to service FUSE's
// _OP_POLL request. Prevent this by forcing _OP_POLL to happen, so we
// can say ENOSYS and prevent further _OP_POLL requests.
func pollHack(mountPoint string) error {
fd, err := syscall.Creat(filepath.Join(mountPoint, pollHackName), syscall.O_CREAT)
if err != nil {
return err
}
pollData := []unix.PollFd{{
Fd: int32(fd),
Events: unix.POLLIN | unix.POLLPRI | unix.POLLOUT,
}}

// Trigger _OP_POLL, so we can say ENOSYS. We don't care about
// the return value.
unix.Poll(pollData, 0)
syscall.Close(fd)
return nil
}

5 comments on commit 4f10e24

@Thireus
Copy link

@Thireus Thireus commented on 4f10e24 Dec 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about Read-Only mounts?

@hanwen
Copy link
Owner Author

@hanwen hanwen commented on 4f10e24 Dec 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, R/O mounts are not treated differently by the VFS. It's up to the filesystem to reject R/W operations.

@Thireus
Copy link

@Thireus Thireus commented on 4f10e24 Dec 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the mount is read-only, then .go-fuse-epoll-hack won't be created, will it? If not, then Fuse volumes cannot be mounted as read-only.

@hanwen
Copy link
Owner Author

@hanwen hanwen commented on 4f10e24 Dec 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the go-fuse library itself that creates .go-fuse-epoll-hack

If you can provide a repro scenario where it works for r/w but doesn't for r/o, I can have a closer look.

@rfjakob
Copy link
Contributor

@rfjakob rfjakob commented on 4f10e24 Dec 3, 2020 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.