Skip to content

Commit 65578c2

Browse files
committed
pgalloc: skip host holes during checkpoint
1 parent fd439eb commit 65578c2

3 files changed

Lines changed: 515 additions & 25 deletions

File tree

pkg/sentry/pgalloc/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,12 @@ go_test(
209209
srcs = [
210210
"pgalloc_64k_test.go",
211211
"pgalloc_test.go",
212+
"save_restore_test.go",
212213
],
213214
library = ":pgalloc",
214215
deps = [
215216
"//pkg/hostarch",
217+
"//pkg/memutil",
216218
"//pkg/sentry/memmap",
217219
],
218220
)

pkg/sentry/pgalloc/save_restore.go

Lines changed: 139 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ type SaveOpts struct {
185185
}
186186

187187
// SaveTo writes f's state to the given stream.
188-
func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) error {
188+
func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) (retErr error) {
189189
if err := f.AwaitLoadAll(); err != nil {
190190
return fmt.Errorf("previous async page loading failed: %w", err)
191191
}
@@ -373,6 +373,33 @@ func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) er
373373
return maseg
374374
}
375375

376+
var hostData *hostFileDataIterator
377+
if !f.opts.DiskBackedFile {
378+
fileSize := f.TotalSize()
379+
fd := int(f.file.Fd())
380+
backingFileUsageBytes, err := f.TotalUsage()
381+
if err != nil {
382+
log.Debugf("MemoryFile(%p): falling back to page scanning because backing file usage is unavailable: %v", f, err)
383+
} else if accountedBytes := uint64(f.memAcct.Span()); hostFileUsageShowsEnoughHoles(backingFileUsageBytes, accountedBytes) {
384+
off, err := unix.Seek(fd, 0, unix.SEEK_CUR)
385+
if err != nil {
386+
log.Debugf("MemoryFile(%p): falling back to page scanning because the backing file offset is unavailable: %v", f, err)
387+
} else {
388+
hostData = &hostFileDataIterator{
389+
size: fileSize,
390+
seek: func(offset int64, whence int) (int64, error) {
391+
return unix.Seek(fd, offset, whence)
392+
},
393+
}
394+
defer func() {
395+
if _, err := unix.Seek(fd, off, unix.SEEK_SET); retErr == nil && err != nil {
396+
retErr = fmt.Errorf("failed to restore host file offset: %w", err)
397+
}
398+
}()
399+
}
400+
}
401+
}
402+
376403
zeroPage := make([]byte, hostarch.PageSize)
377404
// f.mu is unlocked below, allowing concurrent calls to f.UpdateUsage() to
378405
// observe pages that we transiently commit (for comparisons to zero) or
@@ -389,30 +416,7 @@ func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) er
389416
f.commitSeq = 0
390417
maseg := f.memAcct.FirstSegment()
391418
unscannedStart := uint64(0)
392-
for maseg.Ok() {
393-
ma := maseg.ValuePtr()
394-
if ma.wasteOrReleasing {
395-
// This shouldn't be possible since we waited for memory release
396-
// above, and f shouldn't be mutated during saving.
397-
panic(fmt.Sprintf("found waste or releasing pages %v during pgalloc.MemoryFile.SaveTo()", maseg.Range()))
398-
}
399-
fr := maseg.Range()
400-
if fr.Start < unscannedStart {
401-
fr.Start = unscannedStart
402-
}
403-
unscannedStart = fr.End
404-
allocatedBytes += fr.Length()
405-
ma.commitSeq = 0
406-
wasCommitted := ma.knownCommitted
407-
if !opts.ExcludeCommittedZeroPages && wasCommitted {
408-
alreadyCommittedBytes += fr.Length()
409-
maseg = updateAddRange(maseg, fr, true /* wasCommitted */, true /* nowCommitted */)
410-
maseg = updateFlush(maseg)
411-
if maseg.End() == unscannedStart {
412-
maseg = maseg.NextSegment()
413-
}
414-
continue
415-
}
419+
scanRange := func(fr memmap.FileRange, wasCommitted bool) {
416420
f.forEachChunk(fr, func(chunk *chunkInfo, chunkFR memmap.FileRange) bool {
417421
bs := chunk.sliceAt(chunkFR)
418422
for pgoff := 0; pgoff < len(bs); pgoff += hostarch.PageSize {
@@ -444,6 +448,61 @@ func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) er
444448
f.mu.Lock()
445449
return true
446450
})
451+
}
452+
markHostHoleUncommitted := func(fr memmap.FileRange, wasCommitted bool) {
453+
if wasCommitted {
454+
newUncommittedBytes += fr.Length()
455+
} else {
456+
alreadyUncommittedBytes += fr.Length()
457+
}
458+
maseg = updateAddRange(maseg, fr, wasCommitted, false /* nowCommitted */)
459+
}
460+
for maseg.Ok() {
461+
ma := maseg.ValuePtr()
462+
if ma.wasteOrReleasing {
463+
// This shouldn't be possible since we waited for memory release
464+
// above, and f shouldn't be mutated during saving.
465+
panic(fmt.Sprintf("found waste or releasing pages %v during pgalloc.MemoryFile.SaveTo()", maseg.Range()))
466+
}
467+
fr := maseg.Range()
468+
if fr.Start < unscannedStart {
469+
fr.Start = unscannedStart
470+
}
471+
unscannedStart = fr.End
472+
allocatedBytes += fr.Length()
473+
ma.commitSeq = 0
474+
wasCommitted := ma.knownCommitted
475+
if !opts.ExcludeCommittedZeroPages && wasCommitted {
476+
alreadyCommittedBytes += fr.Length()
477+
maseg = updateAddRange(maseg, fr, true /* wasCommitted */, true /* nowCommitted */)
478+
} else if hostData != nil {
479+
off := fr.Start
480+
for off < fr.End {
481+
dataFR, ok, err := hostData.rangeAtOrAfter(off)
482+
if err != nil {
483+
log.Debugf("MemoryFile(%p): scanning remaining pages because backing file extents are unavailable: %v", f, err)
484+
hostData = nil
485+
scanRange(memmap.FileRange{Start: off, End: fr.End}, wasCommitted)
486+
off = fr.End
487+
break
488+
}
489+
if !ok || dataFR.Start >= fr.End {
490+
break
491+
}
492+
dataStart := max(off, dataFR.Start)
493+
if off < dataStart {
494+
markHostHoleUncommitted(memmap.FileRange{Start: off, End: dataStart}, wasCommitted)
495+
}
496+
dataEnd := min(fr.End, dataFR.End)
497+
scanRange(memmap.FileRange{Start: dataStart, End: dataEnd}, wasCommitted)
498+
off = dataEnd
499+
}
500+
if off < fr.End {
501+
markHostHoleUncommitted(memmap.FileRange{Start: off, End: fr.End}, wasCommitted)
502+
}
503+
} else {
504+
scanRange(fr, wasCommitted)
505+
}
447506
// We need to flush batched updates to f.memAcct whenever potentially
448507
// reaching the end of a segment, in order to maintain the invariant
449508
// that updatePendingFR corresponds to a single segment.
@@ -523,6 +582,61 @@ func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) er
523582
return nil
524583
}
525584

585+
const extentScanMinHoleFractionDivisor = 8
586+
587+
// hostFileUsageShowsEnoughHoles reports whether the host file allocation proves
588+
// that more than one eighth of the accounted MemoryFile ranges are holes.
589+
// Finding holes in a dense tmpfs file may scan the same pages as SaveTo. File
590+
// allocation outside the accounted ranges can only prevent this optimization.
591+
func hostFileUsageShowsEnoughHoles(backingFileUsageBytes, accountedBytes uint64) bool {
592+
if accountedBytes == 0 {
593+
return false
594+
}
595+
return backingFileUsageBytes < accountedBytes-accountedBytes/extentScanMinHoleFractionDivisor
596+
}
597+
598+
// hostFileDataIterator iterates over page-aligned ranges reported by SEEK_DATA.
599+
// SaveTo still scans these ranges because they may contain zero pages. The
600+
// iterator holds at most one range regardless of file fragmentation.
601+
type hostFileDataIterator struct {
602+
size uint64
603+
current memmap.FileRange
604+
done bool
605+
seek func(offset int64, whence int) (int64, error)
606+
}
607+
608+
func (it *hostFileDataIterator) rangeAtOrAfter(off uint64) (memmap.FileRange, bool, error) {
609+
for it.current.End <= off {
610+
it.current = memmap.FileRange{}
611+
if it.done || off >= it.size {
612+
return memmap.FileRange{}, false, nil
613+
}
614+
data, err := it.seek(int64(off), unix.SEEK_DATA)
615+
if err == unix.ENXIO {
616+
it.done = true
617+
return memmap.FileRange{}, false, nil
618+
}
619+
if err != nil {
620+
return memmap.FileRange{}, false, fmt.Errorf("SEEK_DATA from %#x: %w", off, err)
621+
}
622+
if data < int64(off) || uint64(data) >= it.size {
623+
return memmap.FileRange{}, false, fmt.Errorf("SEEK_DATA from %#x returned %#x for file size %#x", off, data, it.size)
624+
}
625+
hole, err := it.seek(data, unix.SEEK_HOLE)
626+
if err != nil {
627+
return memmap.FileRange{}, false, fmt.Errorf("SEEK_HOLE from %#x: %w", data, err)
628+
}
629+
if hole <= data || uint64(hole) > it.size {
630+
return memmap.FileRange{}, false, fmt.Errorf("SEEK_HOLE from %#x returned %#x for file size %#x", data, hole, it.size)
631+
}
632+
it.current = memmap.FileRange{
633+
Start: hostarch.PageRoundDown(uint64(data)),
634+
End: hostarch.MustPageRoundUp(uint64(hole)),
635+
}.Intersect(memmap.FileRange{End: it.size})
636+
}
637+
return it.current, true, nil
638+
}
639+
526640
// AsyncPagesFileSave holds async page saving state for a single pages file.
527641
type AsyncPagesFileSave struct {
528642
mu apfsMutex

0 commit comments

Comments
 (0)