diff --git a/file_system.go b/file_system.go index a0253ad..52f8998 100644 --- a/file_system.go +++ b/file_system.go @@ -218,9 +218,12 @@ type FileSystem interface { // // * (http://goo.gl/IQkWZa) sys_fsync calls do_fsync, calls vfs_fsync, calls // vfs_fsync_range. + // // * (http://goo.gl/5L2SMy) vfs_fsync_range calls f_op->fsync. // - // Note that this is also called by fdatasync(2) (cf. http://goo.gl/01R7rF). + // Note that this is also called by fdatasync(2) (cf. http://goo.gl/01R7rF), + // and may be called for msync(2) with the MS_SYNC flag (see the notes on + // FlushFile). // // See also: FlushFile, which may perform a similar purpose when closing a // file (but which is not used in "real" file systems). @@ -242,11 +245,28 @@ type FileSystem interface { // case of close(2), a flush error is returned to the user. For dup2(2), it // is not. // - // Note that one potentially significant case where this is *not* called is - // munmap(2). (Cf. http://goo.gl/7n1r9X, fuse-devel mailing list thread from - // Han-Wen Nienhuys on 2014-10-08.) Even if users close(2) after writing to - // an mmap'd file, on OS X the contents are not immediately flushed (cf. - // https://github.com/osxfuse/osxfuse/issues/202). + // One potentially significant case where this may not be called is mmap'd + // files, where the behavior is complicated: + // + // * munmap(2) does not cause flushes (cf. http://goo.gl/j8B9g0). + // + // * On OS X, if a user modifies a mapped file via the mapping before + // closing the file with close(2), the WriteFile calls for the + // modifications may not be received before the FlushFile request for the + // close(2) (cf. http://goo.gl/kVmNcx). + // + // * However, even on OS X you can arrange for writes via a mapping to be + // flushed by calling msync(2) followed by close(2). On OS X msync(2) + // will cause a WriteFile to go through and close(2) will cause a + // FlushFile as usual (cf. http://goo.gl/kVmNcx). On Linux, msync(2) does + // nothing unless you set the MS_SYNC flag, in which case it causes a + // SyncFile (cf. http://goo.gl/P3mErk). + // + // In summary: if you make data durable in both FlushFile and SyncFile, then + // your users can get safe behavior from mapped files by calling msync(2) + // with MS_SYNC, followed by munmap(2), followed by close(2). On Linux, the + // msync(2) appears to be optional because close(2) implies dirty page + // writeback (cf. http://goo.gl/HyzLTT). // // Because of cases like dup2(2), calls to FlushFile are not necessarily one // to one with calls to OpenFile. They should not be used for reference diff --git a/samples/flushfs/flush_fs_test.go b/samples/flushfs/flush_fs_test.go index 7f8bba5..927e693 100644 --- a/samples/flushfs/flush_fs_test.go +++ b/samples/flushfs/flush_fs_test.go @@ -24,6 +24,9 @@ import ( "runtime" "syscall" "testing" + "unsafe" + + "golang.org/x/sys/unix" "github.com/jacobsa/bazilfuse" "github.com/jacobsa/fuse/fsutil" @@ -107,6 +110,8 @@ func (t *flushFSTest) TearDown() { // Helpers //////////////////////////////////////////////////////////////////////// +var isDarwin = runtime.GOOS == "darwin" + func readReports(f *os.File) (reports []string, err error) { // Seek the file to the start. _, err = f.Seek(0, 0) @@ -173,6 +178,23 @@ func dup2(oldfd int, newfd int) (err error) { return } +// Call msync(2) with the MS_SYNC flag on a slice previously returned by +// mmap(2). +func msync(p []byte) (err error) { + _, _, errno := unix.Syscall( + unix.SYS_MSYNC, + uintptr(unsafe.Pointer(&p[0])), + uintptr(len(p)), + unix.MS_SYNC) + + if errno != 0 { + err = errno + return + } + + return +} + //////////////////////////////////////////////////////////////////////// // No errors //////////////////////////////////////////////////////////////////////// @@ -456,7 +478,6 @@ func (t *NoErrorsTest) Dup() { var n int var err error - isDarwin := runtime.GOOS == "darwin" var expectedFlushes []interface{} // Open the file. @@ -542,7 +563,7 @@ func (t *NoErrorsTest) Dup2() { ExpectThat(t.getFsyncs(), ElementsAre()) } -func (t *NoErrorsTest) Mmap_MunmapBeforeClose() { +func (t *NoErrorsTest) Mmap_NoMsync_MunmapBeforeClose() { var n int var err error @@ -564,9 +585,10 @@ func (t *NoErrorsTest) Mmap_MunmapBeforeClose() { AssertEq(nil, err) AssertEq("taco", string(data)) - // Modify then unmap. + // Modify the contents. data[0] = 'p' + // Unmap. err = syscall.Munmap(data) AssertEq(nil, err) @@ -580,7 +602,7 @@ func (t *NoErrorsTest) Mmap_MunmapBeforeClose() { t.f1 = nil AssertEq(nil, err) - if runtime.GOOS == "darwin" { + if isDarwin { ExpectThat(t.getFlushes(), ElementsAre("taco")) ExpectThat(t.getFsyncs(), ElementsAre()) } else { @@ -589,7 +611,7 @@ func (t *NoErrorsTest) Mmap_MunmapBeforeClose() { } } -func (t *NoErrorsTest) Mmap_CloseBeforeMunmap() { +func (t *NoErrorsTest) Mmap_NoMsync_CloseBeforeMunmap() { var n int var err error @@ -619,9 +641,10 @@ func (t *NoErrorsTest) Mmap_CloseBeforeMunmap() { AssertThat(t.getFlushes(), ElementsAre("taco")) AssertThat(t.getFsyncs(), ElementsAre()) - // Modify then unmap. + // Modify the contents. data[0] = 'p' + // Unmap. err = syscall.Munmap(data) AssertEq(nil, err) @@ -630,6 +653,117 @@ func (t *NoErrorsTest) Mmap_CloseBeforeMunmap() { ExpectThat(t.getFsyncs(), ElementsAre()) } +func (t *NoErrorsTest) Mmap_WithMsync_MunmapBeforeClose() { + var n int + var err error + + var expectedFsyncs []interface{} + + // Open the file. + t.f1, err = os.OpenFile(path.Join(t.Dir, "foo"), os.O_RDWR, 0) + AssertEq(nil, err) + + // Write some contents to the file. + n, err = t.f1.Write([]byte("taco")) + AssertEq(nil, err) + AssertEq(4, n) + + // mmap the file. + data, err := syscall.Mmap( + int(t.f1.Fd()), 0, 4, + syscall.PROT_READ|syscall.PROT_WRITE, + syscall.MAP_SHARED) + + AssertEq(nil, err) + AssertEq("taco", string(data)) + + // Modify the contents. + data[0] = 'p' + + // msync. This causes an fsync, except on OS X (cf. + // https://github.com/osxfuse/osxfuse/issues/202). + err = msync(data) + ExpectEq(nil, err) + + if !isDarwin { + expectedFsyncs = append(expectedFsyncs, "paco") + } + + ExpectThat(t.getFlushes(), ElementsAre()) + ExpectThat(t.getFsyncs(), ElementsAre(expectedFsyncs...)) + + // Unmap. This does not cause anything. + err = syscall.Munmap(data) + AssertEq(nil, err) + + ExpectThat(t.getFlushes(), ElementsAre()) + ExpectThat(t.getFsyncs(), ElementsAre(expectedFsyncs...)) + + // Close the file. We should now see a flush with the modified contents, even + // on OS X. + err = t.f1.Close() + t.f1 = nil + AssertEq(nil, err) + + ExpectThat(t.getFlushes(), ElementsAre("paco")) + ExpectThat(t.getFsyncs(), ElementsAre(expectedFsyncs...)) +} + +func (t *NoErrorsTest) Mmap_WithMsync_CloseBeforeMunmap() { + var n int + var err error + + var expectedFsyncs []interface{} + + // Open the file. + t.f1, err = os.OpenFile(path.Join(t.Dir, "foo"), os.O_RDWR, 0) + AssertEq(nil, err) + + // Write some contents to the file. + n, err = t.f1.Write([]byte("taco")) + AssertEq(nil, err) + AssertEq(4, n) + + // mmap the file. + data, err := syscall.Mmap( + int(t.f1.Fd()), 0, 4, + syscall.PROT_READ|syscall.PROT_WRITE, + syscall.MAP_SHARED) + + AssertEq(nil, err) + AssertEq("taco", string(data)) + + // Close the file. We should see a flush. + err = t.f1.Close() + t.f1 = nil + AssertEq(nil, err) + + AssertThat(t.getFlushes(), ElementsAre("taco")) + AssertThat(t.getFsyncs(), ElementsAre()) + + // Modify the contents. + data[0] = 'p' + + // msync. This causes an fsync, except on OS X (cf. + // https://github.com/osxfuse/osxfuse/issues/202). + err = msync(data) + ExpectEq(nil, err) + + if !isDarwin { + expectedFsyncs = append(expectedFsyncs, "paco") + } + + ExpectThat(t.getFlushes(), ElementsAre("taco")) + ExpectThat(t.getFsyncs(), ElementsAre(expectedFsyncs...)) + + // Unmap. Again, this does not cause a flush. + err = syscall.Munmap(data) + AssertEq(nil, err) + + ExpectThat(t.getFlushes(), ElementsAre("taco")) + ExpectThat(t.getFsyncs(), ElementsAre(expectedFsyncs...)) +} + func (t *NoErrorsTest) Directory() { var err error @@ -706,7 +840,7 @@ func (t *FlushErrorTest) Dup() { err = t.f1.Close() t.f1 = nil - if runtime.GOOS == "darwin" { + if isDarwin { AssertEq(nil, err) } else { ExpectThat(err, Error(HasSubstr("no such file"))) @@ -780,3 +914,32 @@ func (t *FsyncErrorTest) Fdatasync() { ExpectThat(err, Error(HasSubstr("no such file"))) } + +func (t *FsyncErrorTest) Msync() { + var err error + + // On OS X, msync does not cause SyncFile. + if isDarwin { + return + } + + // Open the file. + t.f1, err = os.OpenFile(path.Join(t.Dir, "foo"), os.O_RDWR, 0) + AssertEq(nil, err) + + // mmap the file. + data, err := syscall.Mmap( + int(t.f1.Fd()), 0, 4, + syscall.PROT_READ|syscall.PROT_WRITE, + syscall.MAP_SHARED) + + AssertEq(nil, err) + + // msync the mapping. + err = msync(data) + ExpectThat(err, Error(HasSubstr("no such file"))) + + // Unmap. + err = syscall.Munmap(data) + AssertEq(nil, err) +}