fsutil

package
v0.0.0-...-648dfe1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 22, 2026 License: Apache-2.0, BSD-3-Clause, MIT Imports: 15 Imported by: 7

README

This package provides utilities for implementing virtual filesystem objects.

[TOC]

Page cache

CachingInodeOperations implements a page cache for files that cannot use the host page cache. Normally these are files that store their data in a remote filesystem. This also applies to files that are accessed on a platform that does not support directly memory mapping host file descriptors (e.g. the ptrace platform).

An CachingInodeOperations buffers regions of a single file into memory. It is owned by an fs.Inode, the in-memory representation of a file (all open file descriptors are backed by an fs.Inode). The fs.Inode provides operations for reading memory into an CachingInodeOperations, to represent the contents of the file in-memory, and for writing memory out, to relieve memory pressure on the kernel and to synchronize in-memory changes to filesystems.

An CachingInodeOperations enables readable and/or writable memory access to file content. Files can be mapped shared or private, see mmap(2). When a file is mapped shared, changes to the file via write(2) and truncate(2) are reflected in the shared memory region. Conversely, when the shared memory region is modified, changes to the file are visible via read(2). Multiple shared mappings of the same file are coherent with each other. This is consistent with Linux.

When a file is mapped private, updates to the mapped memory are not visible to other memory mappings. Updates to the mapped memory are also not reflected in the file content as seen by read(2). If the file is changed after a private mapping is created, for instance by write(2), the change to the file may or may not be reflected in the private mapping. This is consistent with Linux.

An CachingInodeOperations keeps track of ranges of memory that were modified (or "dirtied"). When the file is explicitly synced via fsync(2), only the dirty ranges are written out to the filesystem. Any error returned indicates a failure to write all dirty memory of an CachingInodeOperations to the filesystem. In this case the filesystem may be in an inconsistent state. The same operation can be performed on the shared memory itself using msync(2). If neither fsync(2) nor msync(2) is performed, then the dirty memory is written out in accordance with the CachingInodeOperations eviction strategy (see below) and there is no guarantee that memory will be written out successfully in full.

Memory allocation and eviction

An CachingInodeOperations implements the following allocation and eviction strategy:

  • Memory is allocated and brought up to date with the contents of a file when a region of mapped memory is accessed (or "faulted on").

  • Dirty memory is written out to filesystems when an fsync(2) or msync(2) operation is performed on a memory mapped file, for all memory mapped files when saved, and/or when there are no longer any memory mappings of a range of a file, see munmap(2). As the latter implies, in the absence of a panic or SIGKILL, dirty memory is written out for all memory mapped files when an application exits.

  • Memory is freed when there are no longer any memory mappings of a range of a file (e.g. when an application exits). This behavior is consistent with Linux for shared memory that has been locked via mlock(2).

Notably, memory is not allocated for read(2) or write(2) operations. This means that reads and writes to the file are only accelerated by an CachingInodeOperations if the file being read or written has been memory mapped and if the shared memory has been accessed at the region being read or written. This diverges from Linux which buffers memory into a page cache on read(2) proactively (i.e. readahead) and delays writing it out to filesystems on write(2) (i.e. writeback). The absence of these optimizations is not visible to applications beyond less than optimal performance when repeatedly reading and/or writing to same region of a file. See Future Work for plans to implement these optimizations.

Additionally, memory held by CachingInodeOperationss is currently unbounded in size. An CachingInodeOperations does not write out dirty memory and free it under system memory pressure. This can cause pathological memory usage.

When memory is written back, an CachingInodeOperations may write regions of shared memory that were never modified. This is due to the strategy of minimizing page faults (see below) and handling only a subset of memory write faults. In the absence of an application or sentry crash, it is guaranteed that if a region of shared memory was written to, it is written back to a filesystem.

Life of a shared memory mapping

A file is memory mapped via mmap(2). For example, if A is an address, an application may execute:

mmap(A, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

This creates a shared mapping of fd that reflects 4k of the contents of fd starting at offset 0, accessible at address A. This in turn creates a virtual memory area region ("vma") which indicates that [A, A+0x1000) is now a valid address range for this application to access.

At this point, memory has not been allocated in the file's CachingInodeOperations. It is also the case that the address range [A, A+0x1000) has not been mapped on the host on behalf of the application. If the application then tries to modify 8 bytes of the shared memory:

char buffer[] = "aaaaaaaa";
memcpy(A, buffer, 8);

The host then sends a SIGSEGV to the sentry because the address range [A, A+8) is not mapped on the host. The SIGSEGV indicates that the memory was accessed writable. The sentry looks up the vma associated with [A, A+8), finds the file that was mapped and its CachingInodeOperations. It then calls CachingInodeOperations.Translate which allocates memory to back [A, A+8). It may choose to allocate more memory (i.e. do "readahead") to minimize subsequent faults.

Memory that is allocated comes from a host tmpfs file (see pgalloc.MemoryFile). The host tmpfs file memory is brought up to date with the contents of the mapped file on its filesystem. The region of the host tmpfs file that reflects the mapped file is then mapped into the host address space of the application so that subsequent memory accesses do not repeatedly generate a SIGSEGV.

The range that was allocated, including any extra memory allocation to minimize faults, is marked dirty due to the write fault. This overcounts dirty memory if the extra memory allocated is never modified.

To make the scenario more interesting, imagine that this application spawns another process and maps the same file in the exact same way:

mmap(A, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

Imagine that this process then tries to modify the file again but with only 4 bytes:

char buffer[] = "bbbb";
memcpy(A, buffer, 4);

Since the first process has already mapped and accessed the same region of the file writable, CachingInodeOperations.Translate is called but returns the memory that has already been allocated rather than allocating new memory. The address range [A, A+0x1000) reflects the same cached view of the file as the first process sees. For example, reading 8 bytes from the file from either process via read(2) starting at offset 0 returns a consistent "bbbbaaaa".

When this process no longer needs the shared memory, it may do:

munmap(A, 0x1000);

At this point, the modified memory cached by the CachingInodeOperations is not written back to the file because it is still in use by the first process that mapped it. When the first process also does:

munmap(A, 0x1000);

Then the last memory mapping of the file at the range [0, 0x1000) is gone. The file's CachingInodeOperations then starts writing back memory marked dirty to the file on its filesystem. Once writing completes, regardless of whether it was successful, the CachingInodeOperations frees the memory cached at the range [0, 0x1000).

Subsequent read(2) or write(2) operations on the file go directly to the filesystem since there no longer exists memory for it in its CachingInodeOperations.

Future Work

Page cache

The sentry does not yet implement the readahead and writeback optimizations for read(2) and write(2) respectively. To do so, on read(2) and/or write(2) the sentry must ensure that memory is allocated in a page cache to read or write into. However, the sentry cannot boundlessly allocate memory. If it did, the host would eventually OOM-kill the sentry+application process. This means that the sentry must implement a page cache memory allocation strategy that is bounded by a global user or container imposed limit. When this limit is approached, the sentry must decide from which page cache memory should be freed so that it can allocate more memory. If it makes a poor decision, the sentry may end up freeing and re-allocating memory to back regions of files that are frequently used, nullifying the optimization (and in some cases causing worse performance due to the overhead of memory allocation and general management). This is a form of "cache thrashing".

In Linux, much research has been done to select and implement a lightweight but optimal page cache eviction algorithm. Linux makes use of hardware page bits to keep track of whether memory has been accessed. The sentry does not have direct access to hardware. Implementing a similarly lightweight and optimal page cache eviction algorithm will need to either introduce a kernel interface to obtain these page bits or find a suitable alternative proxy for access events.

In Linux, readahead happens by default but is not always ideal. For instance, for files that are not read sequentially, it would be more ideal to simply read from only those regions of the file rather than to optimistically cache some number of bytes ahead of the read (up to 2MB in Linux) if the bytes cached won't be accessed. Linux implements the fadvise64(2) system call for applications to specify that a range of a file will not be accessed sequentially. The advice bit FADV_RANDOM turns off the readahead optimization for the given range in the given file. However fadvise64 is rarely used by applications so Linux implements a readahead backoff strategy if reads are not sequential. To ensure that application performance is not degraded, the sentry must implement a similar backoff strategy.

Documentation

Overview

Package fsutil provides utilities for implementing vfs.FileDescriptionImpl and vfs.FilesystemImpl.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func SyncDirty

func SyncDirty(ctx context.Context, mr memmap.MappableRange, cache *FileRangeSet, dirty *DirtySet, max uint64, mem memmap.File, writeAt func(ctx context.Context, srcs safemem.BlockSeq, offset uint64) (uint64, error)) error

SyncDirty passes pages in the range mr that are stored in cache and identified as dirty to writeAt, updating dirty to reflect successful writes. If writeAt returns a successful partial write, SyncDirty will call it repeatedly until all bytes have been written. max is the true size of the cached object; offsets beyond max will not be passed to writeAt, even if they are marked dirty.

func SyncDirtyAll

func SyncDirtyAll(ctx context.Context, cache *FileRangeSet, dirty *DirtySet, max uint64, mem memmap.File, writeAt func(ctx context.Context, srcs safemem.BlockSeq, offset uint64) (uint64, error)) error

SyncDirtyAll passes all pages stored in cache identified as dirty to writeAt, updating dirty to reflect successful writes. If writeAt returns a successful partial write, SyncDirtyAll will call it repeatedly until all bytes have been written. max is the true size of the cached object; offsets beyond max will not be passed to writeAt, even if they are marked dirty.

Types

type DirtyInfo

type DirtyInfo struct {
	// Keep is true if the represented offset is concurrently writable, such
	// that writing the data for that offset back to the source does not
	// guarantee that the offset is clean (since it may be concurrently
	// rewritten after the writeback).
	Keep bool
}

DirtyInfo is the value type of DirtySet, and represents information about a Mappable offset that is dirty (the cached data for that offset is newer than its source).

+stateify savable

type FileRangeSetFunctions

type FileRangeSetFunctions struct{}

FileRangeSetFunctions implements segment.Functions for FileRangeSet.

func (FileRangeSetFunctions) ClearValue

func (FileRangeSetFunctions) ClearValue(_ *uint64)

ClearValue implements segment.Functions.ClearValue.

func (FileRangeSetFunctions) MaxKey

func (FileRangeSetFunctions) MaxKey() uint64

MaxKey implements segment.Functions.MaxKey.

func (FileRangeSetFunctions) Merge

func (FileRangeSetFunctions) Merge(mr1 memmap.MappableRange, frstart1 uint64, _ memmap.MappableRange, frstart2 uint64) (uint64, bool)

Merge implements segment.Functions.Merge.

func (FileRangeSetFunctions) MinKey

func (FileRangeSetFunctions) MinKey() uint64

MinKey implements segment.Functions.MinKey.

func (FileRangeSetFunctions) Split

func (FileRangeSetFunctions) Split(mr memmap.MappableRange, frstart uint64, split uint64) (uint64, uint64)

Split implements segment.Functions.Split.

type FrameRefSegInfo

type FrameRefSegInfo struct {
	// contains filtered or unexported fields
}

FrameRefSegInfo holds reference count and memory cgroup id of the segment.

+stateify savable

type FrameRefSetFunctions

type FrameRefSetFunctions struct{}

FrameRefSetFunctions implements segment.Functions for FrameRefSet.

func (FrameRefSetFunctions) ClearValue

func (FrameRefSetFunctions) ClearValue(val *FrameRefSegInfo)

ClearValue implements segment.Functions.ClearValue.

func (FrameRefSetFunctions) MaxKey

func (FrameRefSetFunctions) MaxKey() uint64

MaxKey implements segment.Functions.MaxKey.

func (FrameRefSetFunctions) Merge

Merge implements segment.Functions.Merge.

func (FrameRefSetFunctions) MinKey

func (FrameRefSetFunctions) MinKey() uint64

MinKey implements segment.Functions.MinKey.

func (FrameRefSetFunctions) Split

Split implements segment.Functions.Split.

type MmapCachedFile

type MmapCachedFile struct {
	memmap.DefaultMemoryType
	memmap.NoBufferedIOFallback
	// contains filtered or unexported fields
}

MmapCachedFile implements MmapFile. It differs from MmapPreciseFile in the following notable ways:

  • MmapCachedFile tracks referenced pages as host page cache usage in sentry memory accounting. (This is the "cache" referred to by the name of MmapCachedFile.) The AddMapping and RemoveMapping methods manipulate reference counts without memory accounting.
  • MmapCachedFile creates sentry mappings of referenced pages at aligned units called "chunks", to expedite mapping reference counting and lookup.

SetFD must be called on zero-value MmapCachedFiles before first use.

+stateify savable

func (*MmapCachedFile) AddMapping

func (f *MmapCachedFile) AddMapping(ar hostarch.AddrRange, offset uint64)

AddMapping is called by implementations of memmap.Mappable.AddMapping to increment the reference count on the pages mapped by ar, starting at the given file offset, without affecting memory accounting. This is not needed for correctness, but ensures that mappings of those pages will be maintained if all references obtained using IncRef are dropped. The reference must be released using RemoveMapping when no longer needed.

AddMapping may be called on pages without an existing reference as long as f.MappableRelease() has not been called.

Preconditions: As for memmap.Mappable.AddMapping.

func (*MmapCachedFile) DataFD

func (f *MmapCachedFile) DataFD(fr memmap.FileRange) (int, error)

DataFD implements memmap.File.DataFD.

func (*MmapCachedFile) DecRef

func (f *MmapCachedFile) DecRef(fr memmap.FileRange)

DecRef implements memmap.File.DecRef.

func (*MmapCachedFile) FD

func (f *MmapCachedFile) FD() int

FD implements memmap.File.FD.

func (*MmapCachedFile) IncRef

func (f *MmapCachedFile) IncRef(fr memmap.FileRange, memCgID uint32)

IncRef implements memmap.File.IncRef.

func (*MmapCachedFile) MapInternal

MapInternal implements memmap.File.MapInternal.

func (*MmapCachedFile) MappableRelease

func (f *MmapCachedFile) MappableRelease()

MappableRelease implements MmapFile.MappableRelease.

func (*MmapCachedFile) RegenerateMappings

func (f *MmapCachedFile) RegenerateMappings() error

RegenerateMappings must be called when the file description mapped by f changes, to replace existing mappings of the previous file description.

Preconditions:

  • f.MappableRelease() is not called concurrently with, or before, f.RegenerateMappings().

func (*MmapCachedFile) RemoveMapping

func (f *MmapCachedFile) RemoveMapping(ar hostarch.AddrRange, offset uint64)

RemoveMapping is called by implementations of memmap.Mappable.RemoveMapping to release page references acquired by AddMapping.

Preconditions: As for memmap.Mappable.RemoveMapping.

func (*MmapCachedFile) SetFD

func (f *MmapCachedFile) SetFD(fd int)

SetFD implements MmapFile.SetFD.

It is legal to call SetFD on a MmapCachedFile more than once; however, after the first call to SetFD that passes a non-negative fd, all calls to SetFD must pass the same fd.

type MmapFile

type MmapFile interface {
	memmap.File

	// SetFD sets the file descriptor represented by the MmapFile. It must be
	// called on zero-value (implementations of) MmapFile before first use. If
	// fd >= 0, the MmapFile takes ownership of the file descriptor fd, i.e. fd
	// will be closed when the MmapFile is no longer in use. If fd < 0,
	// MapInternal and DataFD can never succeed, but the MmapFile is
	// nevertheless a valid memmap.File.
	//
	// After save/restore, the file descriptor is reset to -1, and SetFD may be
	// called to set it again before first post-restore use.
	//
	// Preconditions: MappableRelease has never been called.
	SetFD(fd int)

	// MappableRelease is called by the memmap.Mappable that is the nominal
	// owner of the represented file descriptor, to indicate that the Mappable
	// is being destroyed. The MmapFile retains ownership of the file
	// descriptor, and remains a valid memmap.File, until all page references
	// are dropped.
	MappableRelease()
}

A MmapFile implements memmap.File by owning a host file descriptor on behalf of an implementation of memmap.Mappable.

type MmapFileRefs

type MmapFileRefs struct {
	// Closer.Close is called when MappableRelease has been called and all page
	// references have been released.
	Closer io.Closer
	// contains filtered or unexported fields
}

MmapFileRefs provides reference counting for implementations of MmapFile that don't unmap pages with zero references, and consequently don't need to keep track of per-page reference counts.

+stateify savable

func (*MmapFileRefs) DecRef

func (r *MmapFileRefs) DecRef(fr memmap.FileRange)

DecRef implements memmap.File.DecRef.

func (*MmapFileRefs) IncRef

func (r *MmapFileRefs) IncRef(fr memmap.FileRange, memCgID uint32)

IncRef implements memmap.File.IncRef.

func (*MmapFileRefs) MappableRelease

func (r *MmapFileRefs) MappableRelease()

MappableRelease implements MmapFile.MappableRelease.

type MmapNoInternalFile

type MmapNoInternalFile struct {
	memmap.NoMapInternal
	MmapFileRefs
	// contains filtered or unexported fields
}

MmapNoInternalFile implements MmapFile by causing calls to MapInternal to fail. The File may still be mapped into platform.AddressSpaces, i.e. application address spaces, except on platform/kvm.

MmapFileRefs.Closer must be set to MmapNoInternalFile.Close before calling MmapNoInternalFile.MappableRelease.

MmapNoInternalFile is used for device files that are not mappable into the sentry for esoteric reasons (which should be documented for each such file), and for device files for which sentry mapping has not been tested (but might work with MmapPreciseFile, or a custom implementation of memmap.File).

+stateify savable

func (*MmapNoInternalFile) Close

func (f *MmapNoInternalFile) Close() error

Close implements io.Closer.Close for f.MmapFileRefs.Closer.

func (*MmapNoInternalFile) DataFD

func (f *MmapNoInternalFile) DataFD(fr memmap.FileRange) (int, error)

DataFD implements memmap.File.DataFD.

func (*MmapNoInternalFile) FD

func (f *MmapNoInternalFile) FD() int

FD implements memmap.File.FD.

func (*MmapNoInternalFile) SetFD

func (f *MmapNoInternalFile) SetFD(fd int)

SetFD implements MmapFile.SetFD.

type MmapPreciseFile

type MmapPreciseFile struct {
	memmap.NoBufferedIOFallback
	// contains filtered or unexported fields
}

MmapPreciseFile implements MmapFile. It differs from MmapCachedFile in the following notable ways:

  • MmapPreciseFile does not track referenced pages in sentry memory accounting.
  • MmapPreciseFile creates sentry mappings of referenced pages at exact page boundaries specified in a file range.

SetFD must be called on zero-value MmapPreciseFiles before first use.

+stateify savable

func (*MmapPreciseFile) DataFD

func (f *MmapPreciseFile) DataFD(fr memmap.FileRange) (int, error)

DataFD implements memmap.File.DataFD.

func (*MmapPreciseFile) DecRef

func (f *MmapPreciseFile) DecRef(fr memmap.FileRange)

DecRef implements memmap.File.DecRef.

func (*MmapPreciseFile) FD

func (f *MmapPreciseFile) FD() int

FD implements memmap.File.FD.

func (*MmapPreciseFile) IncRef

func (f *MmapPreciseFile) IncRef(fr memmap.FileRange, memCgID uint32)

IncRef implements memmap.File.IncRef.

func (*MmapPreciseFile) MapInternal

MapInternal implements memmap.File.MapInternal.

func (*MmapPreciseFile) MappableRelease

func (f *MmapPreciseFile) MappableRelease()

MappableRelease implements MmapFile.MappableRelease.

func (*MmapPreciseFile) MemoryType

func (f *MmapPreciseFile) MemoryType() hostarch.MemoryType

MemoryType implements memmap.File.MemoryType.

func (*MmapPreciseFile) RequireAddrEqualsFileOffset

func (f *MmapPreciseFile) RequireAddrEqualsFileOffset()

RequireAddrEqualsFileOffset causes the MmapPreciseFile to map the host file descriptor at addresses equal to the corresponding file offsets. If used, it must be called before first use of the MmapPreciseFile.

func (*MmapPreciseFile) SetFD

func (f *MmapPreciseFile) SetFD(fd int)

SetFD implements MmapFile.SetFD.

func (*MmapPreciseFile) SetMemType

func (f *MmapPreciseFile) SetMemType(mt hostarch.MemoryType)

SetMemType sets the value returned by MemoryType. If used, it must be called before first use of the MmapPreciseFile.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL