Skip to content

Commit 1a8b555

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

3 files changed

Lines changed: 543 additions & 29 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: 152 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,16 @@ type SaveOpts struct {
177177
// ExcludeCommittedZeroPages is false, SaveTo() will scan only
178178
// possibly-committed pages to find zero pages.
179179
//
180-
// Enabling ExcludeCommittedZeroPages will usually increase the time taken
181-
// by SaveTo() (due to the larger number of pages that must be scanned),
182-
// but may instead improve SaveTo() and LoadFrom() time, and checkpoint
183-
// size, if the application has many committed zero pages.
180+
// Enabling ExcludeCommittedZeroPages may increase the time taken by
181+
// SaveTo() when committed pages must be scanned, but may instead improve
182+
// SaveTo() and LoadFrom() time, and checkpoint size, if the application has
183+
// many committed zero pages. SaveTo() avoids scanning host file holes when
184+
// the backing file is sufficiently sparse.
184185
ExcludeCommittedZeroPages bool
185186
}
186187

187188
// SaveTo writes f's state to the given stream.
188-
func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) error {
189+
func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) (retErr error) {
189190
if err := f.AwaitLoadAll(); err != nil {
190191
return fmt.Errorf("previous async page loading failed: %w", err)
191192
}
@@ -373,6 +374,33 @@ func (f *MemoryFile) SaveTo(ctx context.Context, w io.Writer, opts *SaveOpts) er
373374
return maseg
374375
}
375376

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

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

0 commit comments

Comments
 (0)