Files
visor/pkg/sentry/kernel/fd_table_unsafe.go
T
Andrei VaginandgVisor bot 890a66acd4 fdtable: avoid large arrays
FDTable.descriptorTable is a slice of unsafe.Pointer-s and its maximum length
is MaxInt32. It requires up to 16GB of memory. A process can use just a few
descriptors but sets one or more of them to high numbers. In this case,
FDTable.descriptorTable is extended to the maximum size.

The problem here is that go-runtime zeros memory regions when they are reused.
In the case of fdtable, the memory region is 16GB, so it is a time consuming
operation. Second, it forces the kernel to allocate physical pages to
the entire region.

This change adds another level to descriptorTable, so the first level is
a slice of buckets where each bucket is a slice of descriptors. The bucket
size is fixed to 512 entries to fit one page.

Before:
BenchmarkFDLookupAndDecRef-12              	50834290	        23.70 ns/op
BenchmarkCreateWithMaxFD-12                	       2	7194873988 ns/op
BenchmarkFDLookupAndDecRefConcurrent-12    	23775555	        49.68 ns/op
BenchmarkTableLookup-12                    	412888780	         2.835 ns/op
BenchmarkTableMapLookup-12                 	87944782	        12.84 ns/op

After:
BenchmarkFDLookupAndDecRef-12              	46229940	        25.03 ns/op
BenchmarkCreateWithMaxFD-12                	      13	  82573899 ns/op
BenchmarkFDLookupAndDecRefConcurrent-12    	21889380	        54.13 ns/op
BenchmarkTableLookup-12                    	415851230	         2.821 ns/op
BenchmarkTableMapLookup-12                 	97236267	        11.89 ns/op

Reported-by: [email protected]
PiperOrigin-RevId: 539138632
2023-06-09 11:49:28 -07:00

148 lines
4.0 KiB
Go

// Copyright 2018 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 kernel
import (
"math"
"gvisor.dev/gvisor/pkg/bitmap"
"gvisor.dev/gvisor/pkg/sentry/vfs"
)
type descriptorBucket [fdsPerBucket]descriptorAtomicPtr
type descriptorBucketSlice []descriptorBucketAtomicPtr
// descriptorTable is a two level table. The first level is a slice of
// *descriptorBucket where each bucket is a slice of *descriptor.
//
// All objects are updated atomically.
type descriptorTable struct {
// Changes to the slice itself requiring holding FDTable.mu.
slice descriptorBucketSliceAtomicPtr `state:".(map[int32]*descriptor)"`
}
// initNoLeakCheck initializes the table without enabling leak checking.
//
// This is used when loading an FDTable after S/R, during which the ref count
// object itself will enable leak checking if necessary.
func (f *FDTable) initNoLeakCheck() {
var slice descriptorBucketSlice // Empty slice.
f.slice.Store(&slice)
}
// init initializes the table with leak checking.
func (f *FDTable) init() {
f.initNoLeakCheck()
f.InitRefs()
f.fdBitmap = bitmap.New(uint32(math.MaxUint16))
}
const (
// fdsPerBucketShift is chosen in such a way that the size of bucket is
// equal to one page.
fdsPerBucketShift = 9
fdsPerBucket = 1 << fdsPerBucketShift
fdsPerBucketMask = fdsPerBucket - 1
)
// get gets a file entry.
//
// The boolean indicates whether this was in range.
//
//go:nosplit
func (f *FDTable) get(fd int32) (*vfs.FileDescription, FDFlags, bool) {
slice := *f.slice.Load()
bucketN := fd >> fdsPerBucketShift
if bucketN >= int32(len(slice)) {
return nil, FDFlags{}, false
}
bucket := slice[bucketN].Load()
if bucket == nil {
return nil, FDFlags{}, false
}
d := bucket[fd&fdsPerBucketMask].Load()
if d == nil {
return nil, FDFlags{}, true
}
return d.file, d.flags, true
}
// CurrentMaxFDs returns the number of file descriptors that may be stored in f
// without reallocation.
func (f *FDTable) CurrentMaxFDs() int {
slice := *f.slice.Load()
return len(slice) * fdsPerBucket
}
// set sets the file description referred to by fd to file. If
// file is non-nil, it takes a reference on them. If setAll replaces
// an existing file description, it returns it with the FDTable's reference
// transferred to the caller, which must call f.drop on the returned
// file after unlocking f.mu.
//
// Precondition: mu must be held.
func (f *FDTable) set(fd int32, file *vfs.FileDescription, flags FDFlags) *vfs.FileDescription {
slicePtr := f.slice.Load()
bucketN := fd >> fdsPerBucketShift
// Grow the table as required.
if length := len(*slicePtr); int(bucketN) >= length {
newLen := int(bucketN) + 1
if newLen < 2*length {
// Ensure the table at least doubles in size without going over the limit.
newLen = 2 * length
if newLen > int(MaxFdLimit) {
newLen = int(MaxFdLimit)
}
}
newSlice := append(*slicePtr, make([]descriptorBucketAtomicPtr, newLen-length)...)
slicePtr = &newSlice
f.slice.Store(slicePtr)
}
slice := *slicePtr
bucket := slice[bucketN].Load()
if bucket == nil {
bucket = &descriptorBucket{}
slice[bucketN].Store(bucket)
}
var desc *descriptor
if file != nil {
desc = &descriptor{
file: file,
flags: flags,
}
}
// Update the single element.
orig := bucket[fd%fdsPerBucket].Swap(desc)
// Acquire a table reference.
if desc != nil && desc.file != nil {
if orig == nil || desc.file != orig.file {
desc.file.IncRef()
}
}
if orig != nil && orig.file != nil {
if desc == nil || desc.file != orig.file {
return orig.file
}
}
return nil
}