mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Merge branch 'google:master' into patch-1
This commit is contained in:
@@ -13,8 +13,8 @@ function install_pkgs() {
|
||||
done
|
||||
}
|
||||
install_pkgs make linux-libc-dev graphviz jq curl binutils gnupg gnupg-agent \
|
||||
gcc pkg-config apt-transport-https ca-certificates software-properties-common \
|
||||
rsync kmod
|
||||
gcc pkg-config apt-transport-https ca-certificates \
|
||||
software-properties-common rsync kmod systemd
|
||||
|
||||
# Install headers, only if available.
|
||||
if test -n "$(apt-cache search --names-only "^linux-headers-$(uname -r)$")"; then
|
||||
@@ -34,9 +34,20 @@ export TOTAL_PARTITIONS=${BUILDKITE_PARALLEL_JOB_COUNT:-1}
|
||||
export RUNTIME="${BUILDKITE_BRANCH}-${BUILDKITE_BUILD_ID}"
|
||||
|
||||
# Ensure Docker has experimental enabled.
|
||||
EXPERIMENTAL=$(sudo docker version --format='{{.Server.Experimental}}')
|
||||
make sudo TARGETS=//runsc:runsc \
|
||||
ARGS="install --experimental=true --runtime=${RUNTIME} -- ${RUNTIME_ARGS:-}"
|
||||
if [[ -n "${STAGED_BINARIES:-}" ]]; then
|
||||
# Used `runsc` from STAGED_BINARIES instead of building it from scratch.
|
||||
tmpdir="$(mktemp -d)"
|
||||
gsutil cat "$(STAGED_BINARIES)" | tar -C "$tmpdir" -zxvf - runsc
|
||||
chmod +x "$tmpdir/runsc"
|
||||
"$tmpdir/runsc" install --experimental=true --runtime="${RUNTIME}" \
|
||||
-- "${RUNTIME_ARGS:-}"
|
||||
rm -rf "$tmpdir"
|
||||
else
|
||||
make sudo TARGETS=//runsc:runsc \
|
||||
ARGS="install --experimental=true --runtime=${RUNTIME} -- ${RUNTIME_ARGS:-}"
|
||||
fi
|
||||
# WARNING: We may be running in a container when this command executes.
|
||||
# This only makes sense if Docker's `live-restore` feature is enabled.
|
||||
sudo systemctl restart docker
|
||||
|
||||
# Helper for benchmarks, based on the branch.
|
||||
|
||||
@@ -59,6 +59,13 @@ steps:
|
||||
label: ":fire: Smoke race tests"
|
||||
command: make smoke-race-tests
|
||||
|
||||
# Build everything.
|
||||
- <<: *common
|
||||
label: ":world_map: Build everything"
|
||||
command: "make build OPTIONS=--build_tag_filters=-nogo TARGETS=//..."
|
||||
agents:
|
||||
arch: "amd64"
|
||||
|
||||
# Check that the Go branch builds. This is not technically required, as this build is maintained
|
||||
# as a GitHub action in order to preserve this maintaince across forks. However, providing the
|
||||
# action here may provide easier debuggability and diagnosis on failure.
|
||||
@@ -364,13 +371,6 @@ steps:
|
||||
arch: "amd64"
|
||||
os: "ubuntu"
|
||||
|
||||
# Build everything.
|
||||
- <<: *common
|
||||
label: ":world_map: Build everything"
|
||||
command: "make build OPTIONS=--build_tag_filters=-nogo TARGETS=//..."
|
||||
agents:
|
||||
arch: "amd64"
|
||||
|
||||
# Run basic benchmarks smoke tests (no upload).
|
||||
- <<: *common
|
||||
label: ":fire: Benchmarks smoke test"
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
load("//tools:defs.bzl", "go_library", "go_test")
|
||||
load("//tools/go_generics:defs.bzl", "go_template_instance")
|
||||
|
||||
package(licenses = ["notice"])
|
||||
|
||||
go_template_instance(
|
||||
name = "chunk_refs",
|
||||
out = "chunk_refs.go",
|
||||
package = "buffer",
|
||||
prefix = "chunk",
|
||||
template = "//pkg/refsvfs2:refs_template",
|
||||
types = {
|
||||
"T": "chunk",
|
||||
},
|
||||
)
|
||||
|
||||
go_template_instance(
|
||||
name = "view_list",
|
||||
out = "view_list.go",
|
||||
package = "buffer",
|
||||
prefix = "view",
|
||||
template = "//pkg/ilist:generic_list",
|
||||
types = {
|
||||
"Element": "*View",
|
||||
"Linker": "*View",
|
||||
},
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "buffer",
|
||||
srcs = [
|
||||
"buffer.go",
|
||||
"buffer_state.go",
|
||||
"chunk.go",
|
||||
"chunk_refs.go",
|
||||
"view.go",
|
||||
"view_list.go",
|
||||
"view_unsafe.go",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/atomicbitops",
|
||||
"//pkg/bits",
|
||||
"//pkg/context",
|
||||
"//pkg/ilist",
|
||||
"//pkg/log",
|
||||
"//pkg/pool",
|
||||
"//pkg/refsvfs2",
|
||||
"//pkg/sync",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "buffer_test",
|
||||
size = "small",
|
||||
srcs = [
|
||||
"buffer_test.go",
|
||||
"view_test.go",
|
||||
],
|
||||
library = ":buffer",
|
||||
deps = [
|
||||
"//pkg/state",
|
||||
"@com_github_google_go_cmp//cmp:go_default_library",
|
||||
],
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
// 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 buffer
|
||||
|
||||
// saveBuf is invoked by stateify.
|
||||
func (b *Buffer) saveData() []byte {
|
||||
return b.Flatten()
|
||||
}
|
||||
|
||||
// loadBuf is invoked by stateify.
|
||||
func (b *Buffer) loadData(data []byte) {
|
||||
v := NewViewWithData(data)
|
||||
b.Append(v)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,113 @@
|
||||
// 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 buffer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/bits"
|
||||
"gvisor.dev/gvisor/pkg/sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// This is log2(baseChunkSize). This number is used to calculate which pool
|
||||
// to use for a payload size by right shifting the payload size by this
|
||||
// number and passing the result to MostSignificantOne64.
|
||||
baseChunkSizeLog2 = 6
|
||||
|
||||
// This is the size of the buffers in the first pool. Each subsquent pool
|
||||
// creates payloads 2^(pool index) times larger than the first pool's
|
||||
// payloads.
|
||||
baseChunkSize = 1 << baseChunkSizeLog2 // 64
|
||||
|
||||
// The largest payload size that we pool. Payloads larger than this will
|
||||
// allocated from the heap and garbage collected as normal.
|
||||
maxChunkSize = baseChunkSize << (numPools - 1) // 65536
|
||||
|
||||
// The number of chunk pools we have for use.
|
||||
numPools = 11
|
||||
)
|
||||
|
||||
// chunkPools is a collection of pools for payloads of different sizes. The
|
||||
// size of the payloads doubles in each successive pool.
|
||||
var chunkPools [numPools]sync.Pool
|
||||
|
||||
func init() {
|
||||
for i := 0; i < numPools; i++ {
|
||||
chunkSize := baseChunkSize * (1 << i)
|
||||
chunkPools[i].New = func() interface{} {
|
||||
return &chunk{
|
||||
data: make([]byte, chunkSize),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Precondition: 0 <= size <= maxChunkSize
|
||||
func getChunkPool(size int) *sync.Pool {
|
||||
idx := 0
|
||||
if size > baseChunkSize {
|
||||
idx = bits.MostSignificantOne64(uint64(size) >> baseChunkSizeLog2)
|
||||
if size > 1<<(idx+baseChunkSizeLog2) {
|
||||
idx++
|
||||
}
|
||||
}
|
||||
if idx >= numPools {
|
||||
panic(fmt.Sprintf("pool for chunk size %d does not exist", size))
|
||||
}
|
||||
return &chunkPools[idx]
|
||||
}
|
||||
|
||||
// Chunk represents a slice of pooled memory.
|
||||
type chunk struct {
|
||||
chunkRefs
|
||||
data []byte
|
||||
}
|
||||
|
||||
func newChunk(size int) *chunk {
|
||||
var c *chunk
|
||||
if size > maxChunkSize {
|
||||
c = &chunk{
|
||||
data: make([]byte, size),
|
||||
}
|
||||
} else {
|
||||
pool := getChunkPool(size)
|
||||
c = pool.Get().(*chunk)
|
||||
for i := range c.data {
|
||||
c.data[i] = 0
|
||||
}
|
||||
}
|
||||
c.InitRefs()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *chunk) destroy() {
|
||||
if len(c.data) > maxChunkSize {
|
||||
c.data = nil
|
||||
return
|
||||
}
|
||||
pool := getChunkPool(len(c.data))
|
||||
pool.Put(c)
|
||||
}
|
||||
|
||||
func (c *chunk) DecRef() {
|
||||
c.chunkRefs.DecRef(c.destroy)
|
||||
}
|
||||
|
||||
func (c *chunk) Clone() *chunk {
|
||||
cpy := newChunk(len(c.data))
|
||||
copy(cpy.data, c.data)
|
||||
return cpy
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
// 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 buffer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/sync"
|
||||
)
|
||||
|
||||
var viewPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &View{}
|
||||
},
|
||||
}
|
||||
|
||||
// View is a window into a shared chunk. Views are held by Buffers in
|
||||
// viewLists to represent contiguous memory.
|
||||
//
|
||||
// A View must be created with NewView, NewViewWithData, or Clone. Owners are
|
||||
// responsible for maintaining ownership over their views. When Views need to be
|
||||
// shared or copied, the owner should create a new View with Clone. Clone must
|
||||
// only ever be called on a owned View, not a borrowed one.
|
||||
//
|
||||
// Users are responsible for calling Release when finished with their View so
|
||||
// that its resources can be returned to the pool.
|
||||
//
|
||||
// Users must not write directly to slices returned by AsSlice. Instead, they
|
||||
// must use Write/WriteAt/CopyIn to modify the underlying View. This preserves
|
||||
// the safety guarantees of copy-on-write.
|
||||
type View struct {
|
||||
sync.NoCopy
|
||||
|
||||
viewEntry
|
||||
read int
|
||||
write int
|
||||
chunk *chunk
|
||||
}
|
||||
|
||||
// NewView creates a new view with capacity at least as big as cap. It is
|
||||
// analogous to make([]byte, 0, cap).
|
||||
func NewView(cap int) *View {
|
||||
c := newChunk(cap)
|
||||
v := viewPool.Get().(*View)
|
||||
*v = View{chunk: c}
|
||||
return v
|
||||
}
|
||||
|
||||
// NewViewSize creates a new view with capacity at least as big as size and
|
||||
// length that is exactly size. It is analogous to make([]byte, size).
|
||||
func NewViewSize(size int) *View {
|
||||
v := NewView(size)
|
||||
v.Grow(size)
|
||||
return v
|
||||
}
|
||||
|
||||
// NewViewWithData creates a new view and initializes it with data. This
|
||||
// function should be used with caution to avoid unnecessary []byte allocations.
|
||||
// When in doubt use NewWithView to maximize chunk reuse in production
|
||||
// environments.
|
||||
func NewViewWithData(data []byte) *View {
|
||||
c := newChunk(len(data))
|
||||
v := viewPool.Get().(*View)
|
||||
*v = View{chunk: c}
|
||||
v.Write(data)
|
||||
return v
|
||||
}
|
||||
|
||||
// Clone creates a shallow clone of v where the underlying chunk is shared.
|
||||
//
|
||||
// The caller must own the View to call Clone. It is not safe to call Clone
|
||||
// on a borrowed or shared View because it can race with other View methods.
|
||||
func (v *View) Clone() *View {
|
||||
v.chunk.IncRef()
|
||||
newV := viewPool.Get().(*View)
|
||||
newV.chunk = v.chunk
|
||||
newV.read = v.read
|
||||
newV.write = v.write
|
||||
return newV
|
||||
}
|
||||
|
||||
// Release releases the chunk held by v and returns v to the pool.
|
||||
func (v *View) Release() {
|
||||
v.chunk.DecRef()
|
||||
*v = View{}
|
||||
viewPool.Put(v)
|
||||
}
|
||||
|
||||
func (v *View) sharesChunk() bool {
|
||||
return v.chunk.refCount.Load() > 1
|
||||
}
|
||||
|
||||
// Full indicates the chunk is full.
|
||||
//
|
||||
// This indicates there is no capacity left to write.
|
||||
func (v *View) Full() bool {
|
||||
return v == nil || v.write == len(v.chunk.data)
|
||||
}
|
||||
|
||||
// Capacity returns the total size of this view's chunk.
|
||||
func (v *View) Capacity() int {
|
||||
return len(v.chunk.data)
|
||||
}
|
||||
|
||||
// Size returns the size of data written to the view.
|
||||
func (v *View) Size() int {
|
||||
return v.write - v.read
|
||||
}
|
||||
|
||||
// TrimFront advances the read index by the given amount.
|
||||
func (v *View) TrimFront(n int) {
|
||||
v.read += n
|
||||
}
|
||||
|
||||
// AsSlice returns a slice of the data written to this view.
|
||||
func (v *View) AsSlice() []byte {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return v.chunk.data[v.read:v.write]
|
||||
}
|
||||
|
||||
// AvailableSize returns the number of bytes available for writing.
|
||||
func (v *View) AvailableSize() int {
|
||||
return len(v.chunk.data) - v.write
|
||||
}
|
||||
|
||||
// Read reads v's data into p.
|
||||
//
|
||||
// Implements the io.Reader interface.
|
||||
func (v *View) Read(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if v.Size() == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := copy(p, v.AsSlice())
|
||||
v.TrimFront(n)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// ReadAt reads data to the p starting at offset.
|
||||
//
|
||||
// Implements the io.ReaderAt interface.
|
||||
func (v *View) ReadAt(p []byte, off int) (int, error) {
|
||||
if off < 0 || off > v.Size() {
|
||||
return 0, fmt.Errorf("ReadAt(): offset out of bounds: want 0 < off < %d, got off=%d", v.Size(), off)
|
||||
}
|
||||
n := copy(p, v.AsSlice()[off:])
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Write writes data to the view's chunk starting at the v.write index. If the
|
||||
// view's chunk has a reference count greater than 1, the chunk is copied first
|
||||
// and then written to.
|
||||
//
|
||||
// Implements the io.Writer interface.
|
||||
func (v *View) Write(p []byte) (int, error) {
|
||||
if v.sharesChunk() {
|
||||
defer v.chunk.DecRef()
|
||||
v.chunk = v.chunk.Clone()
|
||||
}
|
||||
n := copy(v.chunk.data[v.write:], p)
|
||||
v.write += n
|
||||
if n < len(p) {
|
||||
return n, fmt.Errorf("could not finish write: want len(p) <= v.AvailableSize(), got len(p)=%d, v.AvailableSize()=%d", len(p), v.AvailableSize())
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// WriteAt writes data to the views's chunk starting at start. If the
|
||||
// view's chunk has a reference count greater than 1, the chunk is copied first
|
||||
// and then written to.
|
||||
//
|
||||
// Implements the io.WriterAt interface.
|
||||
func (v *View) WriteAt(p []byte, off int) (int, error) {
|
||||
if off < 0 || off > v.Size() {
|
||||
return 0, fmt.Errorf("write offset out of bounds: want 0 < off < %d, got off=%d", v.Size(), off)
|
||||
}
|
||||
if v.sharesChunk() {
|
||||
defer v.chunk.DecRef()
|
||||
v.chunk = v.chunk.Clone()
|
||||
}
|
||||
n := copy(v.AsSlice()[off:], p)
|
||||
if n < len(p) {
|
||||
return n, fmt.Errorf("could not finish write: want off + len(p) < v.Capacity(), got off=%d, len(p)=%d ,v.Size() = %d", off, len(p), v.Size())
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Grow advances the write index by the given amount.
|
||||
func (v *View) Grow(n int) {
|
||||
if n+v.write > v.Capacity() {
|
||||
panic("cannot grow view past capacity")
|
||||
}
|
||||
v.write += n
|
||||
}
|
||||
|
||||
// CapLength caps the length of the view's read slice to n. If n > v.Size(),
|
||||
// the function is a no-op.
|
||||
func (v *View) CapLength(n int) {
|
||||
if n < 0 {
|
||||
panic("n must be >= 0")
|
||||
}
|
||||
if n > v.Size() {
|
||||
n = v.Size()
|
||||
}
|
||||
v.write = v.read + n
|
||||
}
|
||||
|
||||
func (v *View) availableSlice() []byte {
|
||||
if v.sharesChunk() {
|
||||
defer v.chunk.DecRef()
|
||||
c := v.chunk.Clone()
|
||||
v.chunk = c
|
||||
}
|
||||
return v.chunk.data[v.write:]
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// 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 buffer
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestNewView(t *testing.T) {
|
||||
for sz := baseChunkSize; sz < maxChunkSize; sz <<= 1 {
|
||||
v := NewView(sz - 1)
|
||||
defer v.Release()
|
||||
|
||||
if v.Capacity() != sz {
|
||||
t.Errorf("v.Capacity() = %d, want %d", v.Capacity(), sz)
|
||||
}
|
||||
if v.AvailableSize() != sz {
|
||||
t.Errorf("v.WriteSize() = %d, want %d", v.AvailableSize(), sz)
|
||||
}
|
||||
if v.Size() != 0 {
|
||||
t.Errorf("v.ReadSize() = %d, want %d", v.Size(), 0)
|
||||
}
|
||||
|
||||
v1 := NewView(sz)
|
||||
defer v1.Release()
|
||||
|
||||
if v1.Capacity() != sz {
|
||||
t.Errorf("v.Capacity() = %d, want %d", v.Capacity(), sz)
|
||||
}
|
||||
if v1.AvailableSize() != sz {
|
||||
t.Errorf("v.WriteSize() = %d, want %d", v.AvailableSize(), sz)
|
||||
}
|
||||
if v1.Size() != 0 {
|
||||
t.Errorf("v.ReadSize() = %d, want %d", v.Size(), 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Allocating from heap should produce a chunk with the exact size requested
|
||||
// instead of a chunk where the size is contingent on the pool it came from.
|
||||
viewSize := maxChunkSize + 1
|
||||
v := NewView(viewSize)
|
||||
defer v.Release()
|
||||
if v.Capacity() != viewSize {
|
||||
t.Errorf("v.Capacity() = %d, want %d", v.Capacity(), viewSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClone(t *testing.T) {
|
||||
orig := NewView(100)
|
||||
clone := orig.Clone()
|
||||
if orig.chunk != clone.chunk {
|
||||
t.Errorf("orig.Clone().chunk = %p, want %p", clone.chunk, orig.chunk)
|
||||
}
|
||||
if orig.chunk.refCount.Load() != 2 {
|
||||
t.Errorf("got orig.chunk.chunkRefs.Load() = %d, want 2", orig.chunk.refCount.Load())
|
||||
}
|
||||
orig.Release()
|
||||
if clone.chunk.refCount.Load() != 1 {
|
||||
t.Errorf("got clone.chunk.chunkRefs.Load() = %d, want 1", clone.chunk.refCount.Load())
|
||||
}
|
||||
clone.Release()
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
view *View
|
||||
initData []byte
|
||||
writeSize int
|
||||
}{
|
||||
{
|
||||
name: "empty view",
|
||||
view: NewView(100),
|
||||
writeSize: 50,
|
||||
},
|
||||
{
|
||||
name: "full view",
|
||||
view: NewView(100),
|
||||
initData: make([]byte, 100),
|
||||
writeSize: 50,
|
||||
},
|
||||
{
|
||||
name: "full write to partially full view",
|
||||
view: NewView(100),
|
||||
initData: make([]byte, 20),
|
||||
writeSize: 50,
|
||||
},
|
||||
{
|
||||
name: "partial write to partially full view",
|
||||
view: NewView(100),
|
||||
initData: make([]byte, 80),
|
||||
writeSize: 50,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.view.Write(tc.initData)
|
||||
defer tc.view.Release()
|
||||
origWriteSize := tc.view.AvailableSize()
|
||||
|
||||
var orig []byte
|
||||
orig = append(orig, tc.view.AsSlice()...)
|
||||
toWrite := make([]byte, tc.writeSize)
|
||||
rand.Read(toWrite)
|
||||
|
||||
n, _ := tc.view.Write(toWrite)
|
||||
|
||||
if n > origWriteSize {
|
||||
t.Errorf("got tc.view.Write() = %d, want <=%d", n, origWriteSize)
|
||||
}
|
||||
if tc.writeSize > origWriteSize {
|
||||
toWrite = toWrite[:origWriteSize]
|
||||
}
|
||||
if tc.view.AvailableSize() != tc.view.Capacity()-(len(toWrite)+len(orig)) {
|
||||
t.Errorf("got tc.view.WriteSize() = %d, want %d", tc.view.AvailableSize(), tc.view.Capacity()-(len(toWrite)+len(orig)))
|
||||
}
|
||||
if !cmp.Equal(tc.view.AsSlice(), append(orig, toWrite...)) {
|
||||
t.Errorf("got tc.view.ReadSlice() = %d, want %d", tc.view.AsSlice(), toWrite)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteToCloned(t *testing.T) {
|
||||
orig := NewView(100)
|
||||
toWrite := make([]byte, 20)
|
||||
rand.Read(toWrite)
|
||||
orig.Write(toWrite)
|
||||
|
||||
clone := orig.Clone()
|
||||
clone.Write(toWrite)
|
||||
|
||||
if !cmp.Equal(orig.AsSlice(), toWrite) {
|
||||
t.Errorf("got orig.ReadSlice() = %v, want %v", orig.AsSlice(), toWrite)
|
||||
}
|
||||
|
||||
toWrite = append(toWrite, toWrite...)
|
||||
if !cmp.Equal(clone.AsSlice(), toWrite) {
|
||||
t.Errorf("got clone.ReadSlice() = %v, want %v", clone.AsSlice(), toWrite)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteAt(t *testing.T) {
|
||||
size := 10
|
||||
off := 5
|
||||
v := NewViewSize(size)
|
||||
p := make([]byte, 20)
|
||||
rand.Read(p)
|
||||
orig := v.Clone()
|
||||
|
||||
if n, _ := v.WriteAt(p, off); n != size-off {
|
||||
t.Errorf("got v.CopyIn()= %v, want %v", n, size-off)
|
||||
}
|
||||
if !cmp.Equal(v.AsSlice()[off:], p[:size-off]) {
|
||||
t.Errorf("got v.AsSlice()[off:] = %v, want %v", v.AsSlice()[off:], p[off:size])
|
||||
}
|
||||
if !cmp.Equal(v.AsSlice()[:off], orig.AsSlice()[:off]) {
|
||||
t.Errorf("got v.AsSlice()[:off] = %v, want %v", v.AsSlice()[:off], orig.AsSlice()[:off])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2020 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 buffer
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// minBatch is the smallest Read or Write operation that the
|
||||
// WriteFromReader and ReadToWriter functions will use.
|
||||
//
|
||||
// This is defined as the size of a native pointer.
|
||||
const minBatch = int(unsafe.Sizeof(uintptr(0)))
|
||||
|
||||
// BasePtr returns a pointer to the view's chunk.
|
||||
func (v *View) BasePtr() *byte {
|
||||
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&v.chunk.data))
|
||||
return (*byte)(unsafe.Pointer(hdr.Data))
|
||||
}
|
||||
@@ -9,6 +9,7 @@ go_library(
|
||||
"json.go",
|
||||
"json_k8s.go",
|
||||
"log.go",
|
||||
"rate_limited.go",
|
||||
],
|
||||
marshal = False,
|
||||
stateify = False,
|
||||
@@ -18,6 +19,7 @@ go_library(
|
||||
deps = [
|
||||
"//pkg/linewriter",
|
||||
"//pkg/sync",
|
||||
"@org_golang_x_time//rate:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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 log
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type rateLimitedLogger struct {
|
||||
logger Logger
|
||||
limit *rate.Limiter
|
||||
}
|
||||
|
||||
func (rl *rateLimitedLogger) Debugf(format string, v ...interface{}) {
|
||||
if rl.limit.Allow() {
|
||||
rl.logger.Debugf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *rateLimitedLogger) Infof(format string, v ...interface{}) {
|
||||
if rl.limit.Allow() {
|
||||
rl.logger.Infof(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *rateLimitedLogger) Warningf(format string, v ...interface{}) {
|
||||
if rl.limit.Allow() {
|
||||
rl.logger.Warningf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *rateLimitedLogger) IsLogging(level Level) bool {
|
||||
return rl.logger.IsLogging(level)
|
||||
}
|
||||
|
||||
// BasicRateLimitedLogger returns a Logger that logs to the global logger no
|
||||
// more than once per the provided duration.
|
||||
func BasicRateLimitedLogger(every time.Duration) Logger {
|
||||
return RateLimitedLogger(Log(), every)
|
||||
}
|
||||
|
||||
// RateLimitedLogger returns a Logger that logs to the provided logger no more
|
||||
// than once per the provided duration.
|
||||
func RateLimitedLogger(logger Logger, every time.Duration) Logger {
|
||||
return &rateLimitedLogger{
|
||||
logger: logger,
|
||||
limit: rate.NewLimiter(rate.Every(every), 1),
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
|
||||
"gvisor.dev/gvisor/pkg/sentry/limits"
|
||||
"gvisor.dev/gvisor/pkg/sentry/vfs"
|
||||
"gvisor.dev/gvisor/pkg/sync"
|
||||
"gvisor.dev/gvisor/pkg/urpc"
|
||||
)
|
||||
|
||||
@@ -34,13 +35,19 @@ type Lifecycle struct {
|
||||
// Kernel is the kernel where the tasks belong to.
|
||||
Kernel *kernel.Kernel
|
||||
|
||||
// Sends a message to the sentry that the task has been started.
|
||||
// StartedCh is the channel used to send a message to the sentry that
|
||||
// all the containers in the sandbox have been started.
|
||||
StartedCh chan struct{}
|
||||
|
||||
// TODO(b/202052732): Root mount namespace. When running multiple
|
||||
// containers, create the mount namespace using the mount spec in
|
||||
// the StartContainerArgs.
|
||||
MountNamespaceVFS2 *vfs.MountNamespace
|
||||
// mu protects the fields below.
|
||||
mu sync.RWMutex
|
||||
|
||||
// containersStarted is the number of containers started in the sandbox.
|
||||
containersStarted int32
|
||||
|
||||
// MountNamespacesMap is a map of container id/names and the mount
|
||||
// namespaces.
|
||||
MountNamespacesMap map[string]*vfs.MountNamespace
|
||||
}
|
||||
|
||||
// StartContainerArgs is the set of arguments to start a container.
|
||||
@@ -127,12 +134,22 @@ func (l *Lifecycle) StartContainer(args *StartContainerArgs, _ *uint32) error {
|
||||
AbstractSocketNamespace: l.Kernel.RootAbstractSocketNamespace(),
|
||||
ContainerID: args.ContainerID,
|
||||
PIDNamespace: l.Kernel.RootPIDNamespace(),
|
||||
MountNamespaceVFS2: l.MountNamespaceVFS2,
|
||||
}
|
||||
|
||||
ctx := initArgs.NewContext(l.Kernel)
|
||||
defer fdTable.DecRef(ctx)
|
||||
|
||||
// VFS2 is supported in multi-container mode by default.
|
||||
l.mu.RLock()
|
||||
mntns, ok := l.MountNamespacesMap[initArgs.ContainerID]
|
||||
if !ok {
|
||||
l.mu.RUnlock()
|
||||
return fmt.Errorf("mount namespace is nil for %s", initArgs.ContainerID)
|
||||
}
|
||||
initArgs.MountNamespaceVFS2 = mntns
|
||||
l.mu.RUnlock()
|
||||
initArgs.MountNamespaceVFS2.IncRef()
|
||||
|
||||
resolved, err := user.ResolveExecutablePath(ctx, &initArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -154,12 +171,17 @@ func (l *Lifecycle) StartContainer(args *StartContainerArgs, _ *uint32) error {
|
||||
return err
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
numContainers := int32(len(l.MountNamespacesMap))
|
||||
|
||||
// Start the newly created process.
|
||||
l.Kernel.StartProcess(tg)
|
||||
|
||||
log.Infof("Started the new container")
|
||||
|
||||
l.StartedCh <- struct{}{}
|
||||
log.Infof("Started the new container %v ", l.containersStarted)
|
||||
l.containersStarted++
|
||||
if numContainers == l.containersStarted {
|
||||
l.StartedCh <- struct{}{}
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -268,9 +268,6 @@ func (d *pidsMaxData) Generate(ctx context.Context, buf *bytes.Buffer) error {
|
||||
|
||||
// Write implements vfs.WritableDynamicBytesSource.Write.
|
||||
func (d *pidsMaxData) Write(ctx context.Context, _ *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) {
|
||||
d.c.mu.Lock()
|
||||
defer d.c.mu.Unlock()
|
||||
|
||||
t := kernel.TaskFromContext(ctx)
|
||||
buf := t.CopyScratchBuffer(hostarch.PageSize)
|
||||
ncpy, err := src.CopyIn(ctx, buf)
|
||||
@@ -278,6 +275,8 @@ func (d *pidsMaxData) Write(ctx context.Context, _ *vfs.FileDescription, src use
|
||||
return 0, err
|
||||
}
|
||||
if strings.TrimSpace(string(buf)) == "max" {
|
||||
d.c.mu.Lock()
|
||||
defer d.c.mu.Unlock()
|
||||
d.c.max = pidLimitUnlimited
|
||||
return int64(ncpy), nil
|
||||
}
|
||||
@@ -290,6 +289,8 @@ func (d *pidsMaxData) Write(ctx context.Context, _ *vfs.FileDescription, src use
|
||||
return 0, linuxerr.EINVAL
|
||||
}
|
||||
|
||||
d.c.mu.Lock()
|
||||
defer d.c.mu.Unlock()
|
||||
d.c.max = val
|
||||
return int64(n), nil
|
||||
}
|
||||
|
||||
@@ -146,6 +146,13 @@ func (d *dentry) createSyntheticChildLocked(opts *createSyntheticOpts) {
|
||||
d.syntheticChildren++
|
||||
}
|
||||
|
||||
// Preconditions:
|
||||
// - d.dirMu must be locked.
|
||||
func (d *dentry) clearDirentsLocked() {
|
||||
d.dirents = nil
|
||||
d.childrenSet = nil
|
||||
}
|
||||
|
||||
// +stateify savable
|
||||
type directoryFD struct {
|
||||
fileDescription
|
||||
@@ -346,6 +353,10 @@ func (d *dentry) getDirents(ctx context.Context) ([]vfs.Dirent, error) {
|
||||
// Cache dirents for future directoryFDs if permitted.
|
||||
if d.cachedMetadataAuthoritative() {
|
||||
d.dirents = dirents
|
||||
d.childrenSet = make(map[string]struct{}, len(dirents))
|
||||
for _, dirent := range d.dirents {
|
||||
d.childrenSet[dirent.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
return dirents, nil
|
||||
}
|
||||
|
||||
@@ -276,6 +276,13 @@ func (fs *filesystem) getChildAndWalkPathLocked(ctx context.Context, parent *den
|
||||
return child, nil
|
||||
}
|
||||
|
||||
if parent.childrenSet != nil {
|
||||
// Is the first child even there? Don't make RPC if not.
|
||||
if _, ok := parent.childrenSet[first]; !ok {
|
||||
return nil, linuxerr.ENOENT
|
||||
}
|
||||
}
|
||||
|
||||
// Walk as much of the path as possible in 1 RPC.
|
||||
names := []string{first}
|
||||
for pit = pit.Next(); pit.Ok(); pit = pit.Next() {
|
||||
@@ -369,6 +376,13 @@ func (fs *filesystem) getChildLocked(ctx context.Context, parent *dentry, name s
|
||||
return child, nil
|
||||
}
|
||||
|
||||
if parent.childrenSet != nil {
|
||||
// Is the child even there? Don't make RPC if not.
|
||||
if _, ok := parent.childrenSet[name]; !ok {
|
||||
return nil, linuxerr.ENOENT
|
||||
}
|
||||
}
|
||||
|
||||
var child *dentry
|
||||
if fs.opts.lisaEnabled {
|
||||
childInode, err := parent.controlFDLisa.Walk(ctx, name)
|
||||
@@ -512,6 +526,11 @@ func (fs *filesystem) doCreateAt(ctx context.Context, rp *vfs.ResolvingPath, dir
|
||||
if child, ok := parent.children[name]; ok && child != nil {
|
||||
return linuxerr.EEXIST
|
||||
}
|
||||
if parent.childrenSet != nil {
|
||||
if _, ok := parent.childrenSet[name]; ok {
|
||||
return linuxerr.EEXIST
|
||||
}
|
||||
}
|
||||
checkExistence := func() error {
|
||||
if child, err := fs.getChildLocked(ctx, parent, name, &ds); err != nil && !linuxerr.Equals(linuxerr.ENOENT, err) {
|
||||
return err
|
||||
@@ -549,7 +568,7 @@ func (fs *filesystem) doCreateAt(ctx context.Context, rp *vfs.ResolvingPath, dir
|
||||
return err
|
||||
}
|
||||
parent.touchCMtime()
|
||||
parent.dirents = nil
|
||||
parent.clearDirentsLocked()
|
||||
ev := linux.IN_CREATE
|
||||
if dir {
|
||||
ev |= linux.IN_ISDIR
|
||||
@@ -569,7 +588,7 @@ func (fs *filesystem) doCreateAt(ctx context.Context, rp *vfs.ResolvingPath, dir
|
||||
delete(parent.children, name)
|
||||
}
|
||||
parent.touchCMtime()
|
||||
parent.dirents = nil
|
||||
parent.clearDirentsLocked()
|
||||
}
|
||||
ev := linux.IN_CREATE
|
||||
if dir {
|
||||
@@ -622,6 +641,12 @@ func (fs *filesystem) unlinkAt(ctx context.Context, rp *vfs.ResolvingPath, dir b
|
||||
parent.dirMu.Lock()
|
||||
defer parent.dirMu.Unlock()
|
||||
|
||||
if parent.childrenSet != nil {
|
||||
if _, ok := parent.childrenSet[name]; !ok {
|
||||
return linuxerr.ENOENT
|
||||
}
|
||||
}
|
||||
|
||||
// Load child if sticky bit is set because we need to determine whether
|
||||
// deletion is allowed.
|
||||
var child *dentry
|
||||
@@ -745,7 +770,7 @@ func (fs *filesystem) unlinkAt(ctx context.Context, rp *vfs.ResolvingPath, dir b
|
||||
}
|
||||
parent.cacheNegativeLookupLocked(name)
|
||||
if parent.cachedMetadataAuthoritative() {
|
||||
parent.dirents = nil
|
||||
parent.clearDirentsLocked()
|
||||
parent.touchCMtime()
|
||||
if dir {
|
||||
parent.decLinks()
|
||||
@@ -1391,7 +1416,7 @@ func (d *dentry) createAndOpenChildLocked(ctx context.Context, rp *vfs.Resolving
|
||||
appendNewChildDentry(ds, d, child)
|
||||
if d.cachedMetadataAuthoritative() {
|
||||
d.touchCMtime()
|
||||
d.dirents = nil
|
||||
d.clearDirentsLocked()
|
||||
}
|
||||
|
||||
// Finally, construct a file description representing the created file.
|
||||
@@ -1623,14 +1648,14 @@ func (fs *filesystem) RenameAt(ctx context.Context, rp *vfs.ResolvingPath, oldPa
|
||||
renamed.touchCtime()
|
||||
}
|
||||
if oldParent.cachedMetadataAuthoritative() {
|
||||
oldParent.dirents = nil
|
||||
oldParent.clearDirentsLocked()
|
||||
oldParent.touchCMtime()
|
||||
if renamed.isDir() {
|
||||
oldParent.decLinks()
|
||||
}
|
||||
}
|
||||
if newParent.cachedMetadataAuthoritative() {
|
||||
newParent.dirents = nil
|
||||
newParent.clearDirentsLocked()
|
||||
newParent.touchCMtime()
|
||||
if renamed.isDir() && (replaced == nil || !replaced.isDir()) {
|
||||
// Increase the link count if we did not replace another directory.
|
||||
|
||||
@@ -850,8 +850,11 @@ type dentry struct {
|
||||
// If this dentry represents a directory,
|
||||
// dentry.cachedMetadataAuthoritative() == true, and dirents is not nil, it
|
||||
// is a cache of all entries in the directory, in the order they were
|
||||
// returned by the server. dirents is protected by dirMu.
|
||||
dirents []vfs.Dirent
|
||||
// returned by the server. childrenSet just stores the `Name` field of all
|
||||
// dirents in a set for fast query. dirents and childrenSet are protected by
|
||||
// dirMu and share the same lifecycle.
|
||||
dirents []vfs.Dirent
|
||||
childrenSet map[string]struct{}
|
||||
|
||||
// Cached metadata; protected by metadataMu.
|
||||
// To access:
|
||||
|
||||
@@ -318,7 +318,7 @@ func (fs *filesystem) revalidateHelper(ctx context.Context, vfsObj *vfs.VirtualF
|
||||
*ds = appendDentry(*ds, d)
|
||||
|
||||
d.parent.syntheticChildren--
|
||||
d.parent.dirents = nil
|
||||
d.parent.clearDirentsLocked()
|
||||
}
|
||||
|
||||
// Since the dirMu was released and reacquired, re-check that the
|
||||
|
||||
@@ -1,8 +1,65 @@
|
||||
load("//tools:defs.bzl", "go_library")
|
||||
load("//tools/go_generics:defs.bzl", "go_template_instance")
|
||||
load("//pkg/sync/locking:locking.bzl", "declare_mutex", "declare_rwmutex")
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
declare_mutex(
|
||||
name = "dir_mutex",
|
||||
out = "dir_mutex.go",
|
||||
package = "overlay",
|
||||
prefix = "dir",
|
||||
)
|
||||
|
||||
declare_mutex(
|
||||
name = "dev_mutex",
|
||||
out = "dev_mutex.go",
|
||||
package = "overlay",
|
||||
prefix = "dev",
|
||||
)
|
||||
|
||||
declare_mutex(
|
||||
name = "dir_cache_mutex",
|
||||
out = "dir_cache_mutex.go",
|
||||
package = "overlay",
|
||||
prefix = "dirInoCache",
|
||||
)
|
||||
|
||||
declare_mutex(
|
||||
name = "reg_file_fd_mutex",
|
||||
out = "req_file_fd_mutex.go",
|
||||
package = "overlay",
|
||||
prefix = "regularFileFD",
|
||||
)
|
||||
|
||||
declare_mutex(
|
||||
name = "dir_fd_mutex",
|
||||
out = "dir_fd_mutex.go",
|
||||
package = "overlay",
|
||||
prefix = "directoryFD",
|
||||
)
|
||||
|
||||
declare_rwmutex(
|
||||
name = "rename_rwmutex",
|
||||
out = "rename_rwmutex.go",
|
||||
package = "overlay",
|
||||
prefix = "rename",
|
||||
)
|
||||
|
||||
declare_rwmutex(
|
||||
name = "data_rwmutex",
|
||||
out = "data_rwmutex.go",
|
||||
package = "overlay",
|
||||
prefix = "data",
|
||||
)
|
||||
|
||||
declare_mutex(
|
||||
name = "maps_mutex",
|
||||
out = "maps_mutex.go",
|
||||
package = "overlay",
|
||||
prefix = "maps",
|
||||
)
|
||||
|
||||
go_template_instance(
|
||||
name = "fstree",
|
||||
out = "fstree.go",
|
||||
@@ -18,11 +75,19 @@ go_library(
|
||||
name = "overlay",
|
||||
srcs = [
|
||||
"copy_up.go",
|
||||
"data_rwmutex.go",
|
||||
"dev_mutex.go",
|
||||
"dir_cache_mutex",
|
||||
"dir_fd_mutex.go",
|
||||
"dir_mutex.go",
|
||||
"directory.go",
|
||||
"filesystem.go",
|
||||
"fstree.go",
|
||||
"maps_mutex.go",
|
||||
"overlay.go",
|
||||
"regular_file.go",
|
||||
"rename_rwmutex.go",
|
||||
"req_file_fd_mutex.go",
|
||||
"save_restore.go",
|
||||
],
|
||||
visibility = ["//pkg/sentry:internal"],
|
||||
@@ -43,6 +108,7 @@ go_library(
|
||||
"//pkg/sentry/socket/unix/transport",
|
||||
"//pkg/sentry/vfs",
|
||||
"//pkg/sync",
|
||||
"//pkg/sync/locking",
|
||||
"//pkg/usermem",
|
||||
"//pkg/waiter",
|
||||
],
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/errors/linuxerr"
|
||||
"gvisor.dev/gvisor/pkg/fspath"
|
||||
"gvisor.dev/gvisor/pkg/sentry/vfs"
|
||||
"gvisor.dev/gvisor/pkg/sync"
|
||||
)
|
||||
|
||||
func (d *dentry) isDir() bool {
|
||||
@@ -104,7 +103,7 @@ type directoryFD struct {
|
||||
vfs.DirectoryFileDescriptionDefaultImpl
|
||||
vfs.DentryMetadataFileDescriptionImpl
|
||||
|
||||
mu sync.Mutex `state:"nosave"`
|
||||
mu directoryFDMutex `state:"nosave"`
|
||||
off int64
|
||||
dirents []vfs.Dirent
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user