Files

174 lines
5.6 KiB
Go
Raw Permalink Normal View History

// Copyright 2022 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package iouringfs
import (
"fmt"
"gvisor.dev/gvisor/pkg/safemem"
)
// sharedBuffer represents a memory buffer shared between the sentry and
// userspace. In many cases, this is simply an internal mmap on the underlying
// memory (aka fast mode). However in some cases the mapped region may lie
// across multiple blocks and we need to copy the region into a contiguous
// buffer (aka slow mode). The goal in either case is to present a contiguous
// slice for easy access.
//
// sharedBuffer must be initialized with init before first use.
//
// Example
// =======
/*
var sb sharedBuffer
bs := MapInternal(...)
sb.init(bs)
fetch := true
for !done {
var err error
// (Re-)Fetch the view.
var view []byte
if fetch {
view, err = sb.view(128)
}
// Use the view slice to access the region, both for read or write.
someState := dosomething(view[10])
view[20] = someState & mask
// Write back the changes.
fetch, err = sb.writeback(128)
}
*/
// In the above example, in fast mode view returns a slice that points directly
// to the underlying memory and requires no copying. Writeback is a no-op, and
// the view can be reused on subsequent loop iterations (writeback will return
// refetch == false).
//
// In slow mode, view will copy disjoint parts of the region from different
// blocks to a single contiguous slice. Writeback will also required a copy, and
// a new view will have to be fetched on every loop iteration (writeback will
// return refetch == true).
//
// sharedBuffer is *not* thread safe.
type sharedBuffer struct {
bs safemem.BlockSeq
// copy is allocated once and reused on subsequent calls to view. We don't
// use the Task's copy scratch buffer because these buffers may be accessed
// from a background context.
copy []byte
// needsWriteback indicates whether we need to copy out back data from the
// slice returned by the last view() call.
needsWriteback bool
}
// init initializes the sharedBuffer, and must be called before first use.
func (b *sharedBuffer) init(bs safemem.BlockSeq) {
b.bs = bs
}
func (b *sharedBuffer) valid() bool {
return !b.bs.IsEmpty()
}
// view returns a slice representing the shared buffer. When done, view must be
// released with either writeback{,Window} or drop.
func (b *sharedBuffer) view(n int) ([]byte, error) {
if uint64(n) > b.bs.NumBytes() {
// Mapping too short? This is a bug.
panic(fmt.Sprintf("iouringfs: mapping too short for requested len: mapping length %v, requested %d", b.bs.NumBytes(), n))
}
// Fast path: use mapping directly, no copies required.
h := b.bs.Head()
if h.Len() <= n && !h.NeedSafecopy() {
b.needsWriteback = false
return h.ToSlice()[:n], nil
}
// Buffer mapped across multiple blocks, or requires safe copy.
if len(b.copy) < n {
b.copy = make([]byte, n)
}
dst := safemem.BlockSeqOf(safemem.BlockFromSafeSlice(b.copy[:n]))
copyN, err := safemem.CopySeq(dst, b.bs)
if err != nil {
return nil, err
}
if copyN != uint64(n) {
// Short copy risks exposing stale data from view buffer. This should never happen.
panic(fmt.Sprintf("iouringfs: short copy for shared buffer view: want %d, got %d", n, copyN))
}
b.needsWriteback = true
return b.copy, nil
}
// writeback writes back the changes to the slice returned by the previous view
// call. On return, writeback indicates if the previous view may be reused, or
// needs to be refetched with a new call to view.
//
2023-10-25 12:06:44 -07:00
// Precondition: Must follow a call to view. n must match the value passed to
// view.
//
// Postcondition: Previous view is invalidated whether writeback is successful
// or not. To attempt another modification, a new view may need to be obtained,
// according to refetch.
func (b *sharedBuffer) writeback(n int) (refetch bool, err error) {
return b.writebackWindow(0, n)
}
// writebackWindow is like writeback, but only writes back a subregion. Useful
// if the caller knows only a small region has been updated, as it reduces how
// much data need to be copied. writebackWindow still potentially invalidates
// the entire view, caller must check refetch to determine if the view needs to
// be refreshed.
func (b *sharedBuffer) writebackWindow(off, len int) (refetch bool, err error) {
if uint64(off+len) > b.bs.NumBytes() {
panic(fmt.Sprintf("iouringfs: requested writeback to shared buffer from offset %d for %d bytes would overflow underlying region of size %d", off, len, b.bs.NumBytes()))
}
if !b.needsWriteback {
return false, nil
}
// Existing view invalid after this point.
b.needsWriteback = false
src := safemem.BlockSeqOf(safemem.BlockFromSafeSlice(b.copy[off : off+len]))
dst := b.bs.DropFirst(off)
copyN, err := safemem.CopySeq(dst, src)
if err != nil {
return true, err
}
if copyN != uint64(len) {
panic(fmt.Sprintf("iouringfs: short copy for shared buffer writeback: want %d, got %d", len, copyN))
}
return true, nil
}
// drop releases a view without writeback. Returns whether any existing views
// need to be refetched. Useful when caller is done with a view that doesn't
// need to be modified.
func (b *sharedBuffer) drop() bool {
wb := b.needsWriteback
b.needsWriteback = false
return wb
}