mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Flipcall refinements.
Note that some of these changes affect the protocol in backward-incompatible ways. - Replace use of "initially-active" and "initially-inactive" with "client" and "server" respectively for clarity. - Fix a race condition involving Endpoint.Shutdown() by repeatedly invoking FUTEX_WAKE until it is confirmed that no local thread is blocked in FUTEX_WAIT. - Drop flipcall.ControlMode. PiperOrigin-RevId: 260981382
This commit is contained in:
+3
-2
@@ -5,10 +5,11 @@ package(licenses = ["notice"])
|
||||
go_library(
|
||||
name = "flipcall",
|
||||
srcs = [
|
||||
"endpoint_futex.go",
|
||||
"endpoint_unsafe.go",
|
||||
"ctrl_futex.go",
|
||||
"flipcall.go",
|
||||
"flipcall_unsafe.go",
|
||||
"futex_linux.go",
|
||||
"io.go",
|
||||
"packet_window_allocator.go",
|
||||
],
|
||||
importpath = "gvisor.dev/gvisor/pkg/flipcall",
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright 2019 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 flipcall
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync/atomic"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/log"
|
||||
)
|
||||
|
||||
type endpointControlImpl struct {
|
||||
state int32
|
||||
}
|
||||
|
||||
// Bits in endpointControlImpl.state.
|
||||
const (
|
||||
epsBlocked = 1 << iota
|
||||
epsShutdown
|
||||
)
|
||||
|
||||
func (ep *Endpoint) ctrlInit(opts ...EndpointOption) error {
|
||||
if len(opts) != 0 {
|
||||
return fmt.Errorf("unknown EndpointOption: %T", opts[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ctrlHandshakeRequest struct{}
|
||||
|
||||
type ctrlHandshakeResponse struct{}
|
||||
|
||||
func (ep *Endpoint) ctrlConnect() error {
|
||||
if err := ep.enterFutexWait(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := ep.futexConnect(&ctrlHandshakeRequest{})
|
||||
ep.exitFutexWait()
|
||||
return err
|
||||
}
|
||||
|
||||
func (ep *Endpoint) ctrlWaitFirst() error {
|
||||
if err := ep.enterFutexWait(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer ep.exitFutexWait()
|
||||
|
||||
// Wait for the handshake request.
|
||||
if err := ep.futexSwitchFromPeer(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Read the handshake request.
|
||||
reqLen := atomic.LoadUint32(ep.dataLen())
|
||||
if reqLen > ep.dataCap {
|
||||
return fmt.Errorf("invalid handshake request length %d (maximum %d)", reqLen, ep.dataCap)
|
||||
}
|
||||
var req ctrlHandshakeRequest
|
||||
if err := json.NewDecoder(ep.NewReader(reqLen)).Decode(&req); err != nil {
|
||||
return fmt.Errorf("error reading handshake request: %v", err)
|
||||
}
|
||||
|
||||
// Write the handshake response.
|
||||
w := ep.NewWriter()
|
||||
if err := json.NewEncoder(w).Encode(ctrlHandshakeResponse{}); err != nil {
|
||||
return fmt.Errorf("error writing handshake response: %v", err)
|
||||
}
|
||||
*ep.dataLen() = w.Len()
|
||||
|
||||
// Return control to the client.
|
||||
if err := ep.futexSwitchToPeer(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for the first non-handshake message.
|
||||
return ep.futexSwitchFromPeer()
|
||||
}
|
||||
|
||||
func (ep *Endpoint) ctrlRoundTrip() error {
|
||||
if err := ep.futexSwitchToPeer(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ep.enterFutexWait(); err != nil {
|
||||
return err
|
||||
}
|
||||
err := ep.futexSwitchFromPeer()
|
||||
ep.exitFutexWait()
|
||||
return err
|
||||
}
|
||||
|
||||
func (ep *Endpoint) ctrlWakeLast() error {
|
||||
return ep.futexSwitchToPeer()
|
||||
}
|
||||
|
||||
func (ep *Endpoint) enterFutexWait() error {
|
||||
switch eps := atomic.AddInt32(&ep.ctrl.state, epsBlocked); eps {
|
||||
case epsBlocked:
|
||||
return nil
|
||||
case epsBlocked | epsShutdown:
|
||||
atomic.AddInt32(&ep.ctrl.state, -epsBlocked)
|
||||
return shutdownError{}
|
||||
default:
|
||||
// Most likely due to ep.enterFutexWait() being called concurrently
|
||||
// from multiple goroutines.
|
||||
panic(fmt.Sprintf("invalid flipcall.Endpoint.ctrl.state before flipcall.Endpoint.enterFutexWait(): %v", eps-epsBlocked))
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *Endpoint) exitFutexWait() {
|
||||
atomic.AddInt32(&ep.ctrl.state, -epsBlocked)
|
||||
}
|
||||
|
||||
func (ep *Endpoint) ctrlShutdown() {
|
||||
// Set epsShutdown to ensure that future calls to ep.enterFutexWait() fail.
|
||||
if atomic.AddInt32(&ep.ctrl.state, epsShutdown)&epsBlocked != 0 {
|
||||
// Wake the blocked thread. This must loop because it's possible that
|
||||
// FUTEX_WAKE occurs after the waiter sets epsBlocked, but before it
|
||||
// blocks in FUTEX_WAIT.
|
||||
for {
|
||||
// Wake MaxInt32 threads to prevent a broken or malicious peer from
|
||||
// swallowing our wakeup by FUTEX_WAITing from multiple threads.
|
||||
if err := ep.futexWakeConnState(math.MaxInt32); err != nil {
|
||||
log.Warningf("failed to FUTEX_WAKE Endpoints: %v", err)
|
||||
break
|
||||
}
|
||||
yieldThread()
|
||||
if atomic.LoadInt32(&ep.ctrl.state)&epsBlocked == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright 2019 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 flipcall
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type endpointControlState struct{}
|
||||
|
||||
func (ep *Endpoint) initControlState(ctrlMode ControlMode) error {
|
||||
if ctrlMode != ControlModeFutex {
|
||||
return fmt.Errorf("unsupported control mode: %v", ctrlMode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *Endpoint) doRoundTrip() error {
|
||||
return ep.doFutexRoundTrip()
|
||||
}
|
||||
|
||||
func (ep *Endpoint) doWaitFirst() error {
|
||||
return ep.doFutexWaitFirst()
|
||||
}
|
||||
|
||||
func (ep *Endpoint) doNotifyLast() error {
|
||||
return ep.doFutexNotifyLast()
|
||||
}
|
||||
|
||||
// Preconditions: ep.isShutdown() == true.
|
||||
func (ep *Endpoint) interruptForShutdown() {
|
||||
ep.doFutexInterruptForShutdown()
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
// Copyright 2019 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 flipcall
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// An Endpoint provides the ability to synchronously transfer data and control
|
||||
// to a connected peer Endpoint, which may be in another process.
|
||||
//
|
||||
// Since the Endpoint control transfer model is synchronous, at any given time
|
||||
// one Endpoint "has control" (designated the *active* Endpoint), and the other
|
||||
// is "waiting for control" (designated the *inactive* Endpoint). Users of the
|
||||
// flipcall package arbitrarily designate one Endpoint as initially-active, and
|
||||
// the other as initially-inactive; in a client/server protocol, the client
|
||||
// Endpoint is usually initially-active (able to send a request) and the server
|
||||
// Endpoint is usually initially-inactive (waiting for a request). The
|
||||
// initially-active Endpoint writes data to be sent to Endpoint.Data(), and
|
||||
// then synchronously transfers control to the inactive Endpoint by calling
|
||||
// Endpoint.SendRecv(), becoming the inactive Endpoint in the process. The
|
||||
// initially-inactive Endpoint waits for control by calling
|
||||
// Endpoint.RecvFirst(); receiving control causes it to become the active
|
||||
// Endpoint. After this, the protocol is symmetric: the active Endpoint reads
|
||||
// data sent by the peer by reading from Endpoint.Data(), writes data to be
|
||||
// sent to the peer into Endpoint.Data(), and then calls Endpoint.SendRecv() to
|
||||
// exchange roles with the peer, which blocks until the peer has done the same.
|
||||
type Endpoint struct {
|
||||
// shutdown is non-zero if Endpoint.Shutdown() has been called. shutdown is
|
||||
// accessed using atomic memory operations.
|
||||
shutdown uint32
|
||||
|
||||
// dataCap is the size of the datagram part of the packet window in bytes.
|
||||
// dataCap is immutable.
|
||||
dataCap uint32
|
||||
|
||||
// packet is the beginning of the packet window. packet is immutable.
|
||||
packet unsafe.Pointer
|
||||
|
||||
ctrl endpointControlState
|
||||
}
|
||||
|
||||
// Init must be called on zero-value Endpoints before first use. If it
|
||||
// succeeds, Destroy() must be called once the Endpoint is no longer in use.
|
||||
//
|
||||
// ctrlMode specifies how connected Endpoints will exchange control. Both
|
||||
// connected Endpoints must specify the same value for ctrlMode.
|
||||
//
|
||||
// pwd represents the packet window used to exchange data with the peer
|
||||
// Endpoint. FD may differ between Endpoints if they are in different
|
||||
// processes, but must represent the same file. The packet window must
|
||||
// initially be filled with zero bytes.
|
||||
func (ep *Endpoint) Init(ctrlMode ControlMode, pwd PacketWindowDescriptor) error {
|
||||
if pwd.Length < pageSize {
|
||||
return fmt.Errorf("packet window size (%d) less than minimum (%d)", pwd.Length, pageSize)
|
||||
}
|
||||
if pwd.Length > math.MaxUint32 {
|
||||
return fmt.Errorf("packet window size (%d) exceeds maximum (%d)", pwd.Length, math.MaxUint32)
|
||||
}
|
||||
m, _, e := syscall.Syscall6(syscall.SYS_MMAP, 0, uintptr(pwd.Length), syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED, uintptr(pwd.FD), uintptr(pwd.Offset))
|
||||
if e != 0 {
|
||||
return fmt.Errorf("failed to mmap packet window: %v", e)
|
||||
}
|
||||
ep.dataCap = uint32(pwd.Length) - uint32(packetHeaderBytes)
|
||||
ep.packet = (unsafe.Pointer)(m)
|
||||
if err := ep.initControlState(ctrlMode); err != nil {
|
||||
ep.unmapPacket()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewEndpoint is a convenience function that returns an initialized Endpoint
|
||||
// allocated on the heap.
|
||||
func NewEndpoint(ctrlMode ControlMode, pwd PacketWindowDescriptor) (*Endpoint, error) {
|
||||
var ep Endpoint
|
||||
if err := ep.Init(ctrlMode, pwd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ep, nil
|
||||
}
|
||||
|
||||
func (ep *Endpoint) unmapPacket() {
|
||||
syscall.Syscall(syscall.SYS_MUNMAP, uintptr(ep.packet), uintptr(ep.dataCap)+packetHeaderBytes, 0)
|
||||
ep.dataCap = 0
|
||||
ep.packet = nil
|
||||
}
|
||||
|
||||
// Destroy releases resources owned by ep. No other Endpoint methods may be
|
||||
// called after Destroy.
|
||||
func (ep *Endpoint) Destroy() {
|
||||
ep.unmapPacket()
|
||||
}
|
||||
|
||||
// Packets consist of an 8-byte header followed by an arbitrarily-sized
|
||||
// datagram. The header consists of:
|
||||
//
|
||||
// - A 4-byte native-endian sequence number, which is incremented by the active
|
||||
// Endpoint after it finishes writing to the packet window. The sequence number
|
||||
// is needed to handle spurious wakeups.
|
||||
//
|
||||
// - A 4-byte native-endian datagram length in bytes.
|
||||
const (
|
||||
sizeofUint32 = unsafe.Sizeof(uint32(0))
|
||||
packetHeaderBytes = 2 * sizeofUint32
|
||||
)
|
||||
|
||||
func (ep *Endpoint) seq() *uint32 {
|
||||
return (*uint32)(ep.packet)
|
||||
}
|
||||
|
||||
func (ep *Endpoint) dataLen() *uint32 {
|
||||
return (*uint32)((unsafe.Pointer)(uintptr(ep.packet) + sizeofUint32))
|
||||
}
|
||||
|
||||
// DataCap returns the maximum datagram size supported by ep in bytes.
|
||||
func (ep *Endpoint) DataCap() uint32 {
|
||||
return ep.dataCap
|
||||
}
|
||||
|
||||
func (ep *Endpoint) data() unsafe.Pointer {
|
||||
return unsafe.Pointer(uintptr(ep.packet) + packetHeaderBytes)
|
||||
}
|
||||
|
||||
// Data returns the datagram part of ep's packet window as a byte slice.
|
||||
//
|
||||
// Note that the packet window is shared with the potentially-untrusted peer
|
||||
// Endpoint, which may concurrently mutate the contents of the packet window.
|
||||
// Thus:
|
||||
//
|
||||
// - Readers must not assume that two reads of the same byte in Data() will
|
||||
// return the same result. In other words, readers should read any given byte
|
||||
// in Data() at most once.
|
||||
//
|
||||
// - Writers must not assume that they will read back the same data that they
|
||||
// have written. In other words, writers should avoid reading from Data() at
|
||||
// all.
|
||||
func (ep *Endpoint) Data() []byte {
|
||||
var bs []byte
|
||||
bsReflect := (*reflect.SliceHeader)((unsafe.Pointer)(&bs))
|
||||
bsReflect.Data = uintptr(ep.data())
|
||||
bsReflect.Len = int(ep.DataCap())
|
||||
bsReflect.Cap = bsReflect.Len
|
||||
return bs
|
||||
}
|
||||
|
||||
// SendRecv transfers control to the peer Endpoint, causing its call to
|
||||
// Endpoint.SendRecv() or Endpoint.RecvFirst() to return with the given
|
||||
// datagram length, then blocks until the peer Endpoint calls
|
||||
// Endpoint.SendRecv() or Endpoint.SendLast().
|
||||
//
|
||||
// Preconditions: No previous call to ep.SendRecv() or ep.RecvFirst() has
|
||||
// returned an error. ep.SendLast() has never been called.
|
||||
func (ep *Endpoint) SendRecv(dataLen uint32) (uint32, error) {
|
||||
dataCap := ep.DataCap()
|
||||
if dataLen > dataCap {
|
||||
return 0, fmt.Errorf("can't send packet with datagram length %d (maximum %d)", dataLen, dataCap)
|
||||
}
|
||||
atomic.StoreUint32(ep.dataLen(), dataLen)
|
||||
if err := ep.doRoundTrip(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
recvDataLen := atomic.LoadUint32(ep.dataLen())
|
||||
if recvDataLen > dataCap {
|
||||
return 0, fmt.Errorf("received packet with invalid datagram length %d (maximum %d)", recvDataLen, dataCap)
|
||||
}
|
||||
return recvDataLen, nil
|
||||
}
|
||||
|
||||
// RecvFirst blocks until the peer Endpoint calls Endpoint.SendRecv(), then
|
||||
// returns the datagram length specified by that call.
|
||||
//
|
||||
// Preconditions: ep.SendRecv(), ep.RecvFirst(), and ep.SendLast() have never
|
||||
// been called.
|
||||
func (ep *Endpoint) RecvFirst() (uint32, error) {
|
||||
if err := ep.doWaitFirst(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
recvDataLen := atomic.LoadUint32(ep.dataLen())
|
||||
if dataCap := ep.DataCap(); recvDataLen > dataCap {
|
||||
return 0, fmt.Errorf("received packet with invalid datagram length %d (maximum %d)", recvDataLen, dataCap)
|
||||
}
|
||||
return recvDataLen, nil
|
||||
}
|
||||
|
||||
// SendLast causes the peer Endpoint's call to Endpoint.SendRecv() or
|
||||
// Endpoint.RecvFirst() to return with the given datagram length.
|
||||
//
|
||||
// Preconditions: No previous call to ep.SendRecv() or ep.RecvFirst() has
|
||||
// returned an error. ep.SendLast() has never been called.
|
||||
func (ep *Endpoint) SendLast(dataLen uint32) error {
|
||||
dataCap := ep.DataCap()
|
||||
if dataLen > dataCap {
|
||||
return fmt.Errorf("can't send packet with datagram length %d (maximum %d)", dataLen, dataCap)
|
||||
}
|
||||
atomic.StoreUint32(ep.dataLen(), dataLen)
|
||||
if err := ep.doNotifyLast(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown causes concurrent and future calls to ep.SendRecv(),
|
||||
// ep.RecvFirst(), and ep.SendLast() to unblock and return errors. It does not
|
||||
// wait for concurrent calls to return.
|
||||
func (ep *Endpoint) Shutdown() {
|
||||
if atomic.SwapUint32(&ep.shutdown, 1) == 0 {
|
||||
ep.interruptForShutdown()
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *Endpoint) isShutdown() bool {
|
||||
return atomic.LoadUint32(&ep.shutdown) != 0
|
||||
}
|
||||
|
||||
type endpointShutdownError struct{}
|
||||
|
||||
// Error implements error.Error.
|
||||
func (endpointShutdownError) Error() string {
|
||||
return "Endpoint.Shutdown() has been called"
|
||||
}
|
||||
+211
-14
@@ -13,20 +13,217 @@
|
||||
// limitations under the License.
|
||||
|
||||
// Package flipcall implements a protocol providing Fast Local Interprocess
|
||||
// Procedure Calls.
|
||||
// Procedure Calls between mutually-distrusting processes.
|
||||
package flipcall
|
||||
|
||||
// ControlMode defines how control is exchanged across a connection.
|
||||
type ControlMode uint8
|
||||
|
||||
const (
|
||||
// ControlModeInvalid is invalid, and exists so that ControlMode fields in
|
||||
// structs must be explicitly initialized.
|
||||
ControlModeInvalid ControlMode = iota
|
||||
|
||||
// ControlModeFutex uses shared futex operations on packet control words.
|
||||
ControlModeFutex
|
||||
|
||||
// controlModeCount is the number of ControlModes in this list.
|
||||
controlModeCount
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// An Endpoint provides the ability to synchronously transfer data and control
|
||||
// to a connected peer Endpoint, which may be in another process.
|
||||
//
|
||||
// Since the Endpoint control transfer model is synchronous, at any given time
|
||||
// one Endpoint "has control" (designated the active Endpoint), and the other
|
||||
// is "waiting for control" (designated the inactive Endpoint). Users of the
|
||||
// flipcall package designate one Endpoint as the client, which is initially
|
||||
// active, and the other as the server, which is initially inactive. See
|
||||
// flipcall_example_test.go for usage.
|
||||
type Endpoint struct {
|
||||
// packet is a pointer to the beginning of the packet window. (Since this
|
||||
// is a raw OS memory mapping and not a Go object, it does not need to be
|
||||
// represented as an unsafe.Pointer.) packet is immutable.
|
||||
packet uintptr
|
||||
|
||||
// dataCap is the size of the datagram part of the packet window in bytes.
|
||||
// dataCap is immutable.
|
||||
dataCap uint32
|
||||
|
||||
// shutdown is non-zero if Endpoint.Shutdown() has been called, or if the
|
||||
// Endpoint has acknowledged shutdown initiated by the peer. shutdown is
|
||||
// accessed using atomic memory operations.
|
||||
shutdown uint32
|
||||
|
||||
// activeState is csClientActive if this is a client Endpoint and
|
||||
// csServerActive if this is a server Endpoint.
|
||||
activeState uint32
|
||||
|
||||
// inactiveState is csServerActive if this is a client Endpoint and
|
||||
// csClientActive if this is a server Endpoint.
|
||||
inactiveState uint32
|
||||
|
||||
ctrl endpointControlImpl
|
||||
}
|
||||
|
||||
// Init must be called on zero-value Endpoints before first use. If it
|
||||
// succeeds, ep.Destroy() must be called once the Endpoint is no longer in use.
|
||||
//
|
||||
// pwd represents the packet window used to exchange data with the peer
|
||||
// Endpoint. FD may differ between Endpoints if they are in different
|
||||
// processes, but must represent the same file. The packet window must
|
||||
// initially be filled with zero bytes.
|
||||
func (ep *Endpoint) Init(pwd PacketWindowDescriptor, opts ...EndpointOption) error {
|
||||
if pwd.Length < pageSize {
|
||||
return fmt.Errorf("packet window size (%d) less than minimum (%d)", pwd.Length, pageSize)
|
||||
}
|
||||
if pwd.Length > math.MaxUint32 {
|
||||
return fmt.Errorf("packet window size (%d) exceeds maximum (%d)", pwd.Length, math.MaxUint32)
|
||||
}
|
||||
m, _, e := syscall.RawSyscall6(syscall.SYS_MMAP, 0, uintptr(pwd.Length), syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED, uintptr(pwd.FD), uintptr(pwd.Offset))
|
||||
if e != 0 {
|
||||
return fmt.Errorf("failed to mmap packet window: %v", e)
|
||||
}
|
||||
ep.packet = m
|
||||
ep.dataCap = uint32(pwd.Length) - uint32(PacketHeaderBytes)
|
||||
// These will be overwritten by ep.Connect() for client Endpoints.
|
||||
ep.activeState = csServerActive
|
||||
ep.inactiveState = csClientActive
|
||||
if err := ep.ctrlInit(opts...); err != nil {
|
||||
ep.unmapPacket()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewEndpoint is a convenience function that returns an initialized Endpoint
|
||||
// allocated on the heap.
|
||||
func NewEndpoint(pwd PacketWindowDescriptor, opts ...EndpointOption) (*Endpoint, error) {
|
||||
var ep Endpoint
|
||||
if err := ep.Init(pwd, opts...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ep, nil
|
||||
}
|
||||
|
||||
// An EndpointOption configures an Endpoint.
|
||||
type EndpointOption interface {
|
||||
isEndpointOption()
|
||||
}
|
||||
|
||||
// Destroy releases resources owned by ep. No other Endpoint methods may be
|
||||
// called after Destroy.
|
||||
func (ep *Endpoint) Destroy() {
|
||||
ep.unmapPacket()
|
||||
}
|
||||
|
||||
func (ep *Endpoint) unmapPacket() {
|
||||
syscall.RawSyscall(syscall.SYS_MUNMAP, ep.packet, uintptr(ep.dataCap)+PacketHeaderBytes, 0)
|
||||
ep.packet = 0
|
||||
}
|
||||
|
||||
// Shutdown causes concurrent and future calls to ep.Connect(), ep.SendRecv(),
|
||||
// ep.RecvFirst(), and ep.SendLast() to unblock and return errors. It does not
|
||||
// wait for concurrent calls to return. The effect of Shutdown on the peer
|
||||
// Endpoint is unspecified. Successive calls to Shutdown have no effect.
|
||||
//
|
||||
// Shutdown is the only Endpoint method that may be called concurrently with
|
||||
// other methods on the same Endpoint.
|
||||
func (ep *Endpoint) Shutdown() {
|
||||
if atomic.SwapUint32(&ep.shutdown, 1) != 0 {
|
||||
// ep.Shutdown() has previously been called.
|
||||
return
|
||||
}
|
||||
ep.ctrlShutdown()
|
||||
}
|
||||
|
||||
// isShutdownLocally returns true if ep.Shutdown() has been called.
|
||||
func (ep *Endpoint) isShutdownLocally() bool {
|
||||
return atomic.LoadUint32(&ep.shutdown) != 0
|
||||
}
|
||||
|
||||
type shutdownError struct{}
|
||||
|
||||
// Error implements error.Error.
|
||||
func (shutdownError) Error() string {
|
||||
return "flipcall connection shutdown"
|
||||
}
|
||||
|
||||
// DataCap returns the maximum datagram size supported by ep. Equivalently,
|
||||
// DataCap returns len(ep.Data()).
|
||||
func (ep *Endpoint) DataCap() uint32 {
|
||||
return ep.dataCap
|
||||
}
|
||||
|
||||
// Connection state.
|
||||
const (
|
||||
// The client is, by definition, initially active, so this must be 0.
|
||||
csClientActive = 0
|
||||
csServerActive = 1
|
||||
)
|
||||
|
||||
// Connect designates ep as a client Endpoint and blocks until the peer
|
||||
// Endpoint has called Endpoint.RecvFirst().
|
||||
//
|
||||
// Preconditions: ep.Connect(), ep.RecvFirst(), ep.SendRecv(), and
|
||||
// ep.SendLast() have never been called.
|
||||
func (ep *Endpoint) Connect() error {
|
||||
ep.activeState = csClientActive
|
||||
ep.inactiveState = csServerActive
|
||||
return ep.ctrlConnect()
|
||||
}
|
||||
|
||||
// RecvFirst blocks until the peer Endpoint calls Endpoint.SendRecv(), then
|
||||
// returns the datagram length specified by that call.
|
||||
//
|
||||
// Preconditions: ep.SendRecv(), ep.RecvFirst(), and ep.SendLast() have never
|
||||
// been called.
|
||||
func (ep *Endpoint) RecvFirst() (uint32, error) {
|
||||
if err := ep.ctrlWaitFirst(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
recvDataLen := atomic.LoadUint32(ep.dataLen())
|
||||
if recvDataLen > ep.dataCap {
|
||||
return 0, fmt.Errorf("received packet with invalid datagram length %d (maximum %d)", recvDataLen, ep.dataCap)
|
||||
}
|
||||
return recvDataLen, nil
|
||||
}
|
||||
|
||||
// SendRecv transfers control to the peer Endpoint, causing its call to
|
||||
// Endpoint.SendRecv() or Endpoint.RecvFirst() to return with the given
|
||||
// datagram length, then blocks until the peer Endpoint calls
|
||||
// Endpoint.SendRecv() or Endpoint.SendLast().
|
||||
//
|
||||
// Preconditions: dataLen <= ep.DataCap(). No previous call to ep.SendRecv() or
|
||||
// ep.RecvFirst() has returned an error. ep.SendLast() has never been called.
|
||||
// If ep is a client Endpoint, ep.Connect() has previously been called and
|
||||
// returned nil.
|
||||
func (ep *Endpoint) SendRecv(dataLen uint32) (uint32, error) {
|
||||
if dataLen > ep.dataCap {
|
||||
panic(fmt.Sprintf("attempting to send packet with datagram length %d (maximum %d)", dataLen, ep.dataCap))
|
||||
}
|
||||
// This store can safely be non-atomic: Under correct operation we should
|
||||
// be the only thread writing ep.dataLen(), and ep.ctrlRoundTrip() will
|
||||
// synchronize with the receiver. We will not read from ep.dataLen() until
|
||||
// after ep.ctrlRoundTrip(), so if the peer is mutating it concurrently then
|
||||
// they can only shoot themselves in the foot.
|
||||
*ep.dataLen() = dataLen
|
||||
if err := ep.ctrlRoundTrip(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
recvDataLen := atomic.LoadUint32(ep.dataLen())
|
||||
if recvDataLen > ep.dataCap {
|
||||
return 0, fmt.Errorf("received packet with invalid datagram length %d (maximum %d)", recvDataLen, ep.dataCap)
|
||||
}
|
||||
return recvDataLen, nil
|
||||
}
|
||||
|
||||
// SendLast causes the peer Endpoint's call to Endpoint.SendRecv() or
|
||||
// Endpoint.RecvFirst() to return with the given datagram length.
|
||||
//
|
||||
// Preconditions: dataLen <= ep.DataCap(). No previous call to ep.SendRecv() or
|
||||
// ep.RecvFirst() has returned an error. ep.SendLast() has never been called.
|
||||
// If ep is a client Endpoint, ep.Connect() has previously been called and
|
||||
// returned nil.
|
||||
func (ep *Endpoint) SendLast(dataLen uint32) error {
|
||||
if dataLen > ep.dataCap {
|
||||
panic(fmt.Sprintf("attempting to send packet with datagram length %d (maximum %d)", dataLen, ep.dataCap))
|
||||
}
|
||||
*ep.dataLen() = dataLen
|
||||
if err := ep.ctrlWakeLast(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package flipcall
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
@@ -36,20 +37,21 @@ func Example() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
clientEP, err := NewEndpoint(ControlModeFutex, pwd)
|
||||
if err != nil {
|
||||
var clientEP Endpoint
|
||||
if err := clientEP.Init(pwd); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer clientEP.Destroy()
|
||||
serverEP, err := NewEndpoint(ControlModeFutex, pwd)
|
||||
if err != nil {
|
||||
var serverEP Endpoint
|
||||
if err := serverEP.Init(pwd); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer serverEP.Destroy()
|
||||
|
||||
serverDone := make(chan struct{})
|
||||
var serverRun sync.WaitGroup
|
||||
serverRun.Add(1)
|
||||
go func() {
|
||||
defer func() { serverDone <- struct{}{} }()
|
||||
defer serverRun.Done()
|
||||
i := 0
|
||||
var buf bytes.Buffer
|
||||
// wait for first request
|
||||
@@ -76,9 +78,13 @@ func Example() {
|
||||
}()
|
||||
defer func() {
|
||||
serverEP.Shutdown()
|
||||
<-serverDone
|
||||
serverRun.Wait()
|
||||
}()
|
||||
|
||||
// establish connection as client
|
||||
if err := clientEP.Connect(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
for i := 0; i < count; i++ {
|
||||
// write request
|
||||
|
||||
+178
-135
@@ -15,197 +15,240 @@
|
||||
package flipcall
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var testPacketWindowSize = pageSize
|
||||
|
||||
func testSendRecv(t *testing.T, ctrlMode ControlMode) {
|
||||
pwa, err := NewPacketWindowAllocator()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create PacketWindowAllocator: %v", err)
|
||||
}
|
||||
defer pwa.Destroy()
|
||||
pwd, err := pwa.Allocate(testPacketWindowSize)
|
||||
if err != nil {
|
||||
t.Fatalf("PacketWindowAllocator.Allocate() failed: %v", err)
|
||||
}
|
||||
type testConnection struct {
|
||||
pwa PacketWindowAllocator
|
||||
clientEP Endpoint
|
||||
serverEP Endpoint
|
||||
}
|
||||
|
||||
sendEP, err := NewEndpoint(ctrlMode, pwd)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create Endpoint: %v", err)
|
||||
func newTestConnectionWithOptions(tb testing.TB, clientOpts, serverOpts []EndpointOption) *testConnection {
|
||||
c := &testConnection{}
|
||||
if err := c.pwa.Init(); err != nil {
|
||||
tb.Fatalf("failed to create PacketWindowAllocator: %v", err)
|
||||
}
|
||||
defer sendEP.Destroy()
|
||||
recvEP, err := NewEndpoint(ctrlMode, pwd)
|
||||
pwd, err := c.pwa.Allocate(testPacketWindowSize)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create Endpoint: %v", err)
|
||||
c.pwa.Destroy()
|
||||
tb.Fatalf("PacketWindowAllocator.Allocate() failed: %v", err)
|
||||
}
|
||||
defer recvEP.Destroy()
|
||||
if err := c.clientEP.Init(pwd, clientOpts...); err != nil {
|
||||
c.pwa.Destroy()
|
||||
tb.Fatalf("failed to create client Endpoint: %v", err)
|
||||
}
|
||||
if err := c.serverEP.Init(pwd, serverOpts...); err != nil {
|
||||
c.pwa.Destroy()
|
||||
c.clientEP.Destroy()
|
||||
tb.Fatalf("failed to create server Endpoint: %v", err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
otherThreadDone := make(chan struct{})
|
||||
func newTestConnection(tb testing.TB) *testConnection {
|
||||
return newTestConnectionWithOptions(tb, nil, nil)
|
||||
}
|
||||
|
||||
func (c *testConnection) destroy() {
|
||||
c.pwa.Destroy()
|
||||
c.clientEP.Destroy()
|
||||
c.serverEP.Destroy()
|
||||
}
|
||||
|
||||
func testSendRecv(t *testing.T, c *testConnection) {
|
||||
var serverRun sync.WaitGroup
|
||||
serverRun.Add(1)
|
||||
go func() {
|
||||
defer func() { otherThreadDone <- struct{}{} }()
|
||||
t.Logf("initially-inactive Endpoint waiting for packet 1")
|
||||
if _, err := recvEP.RecvFirst(); err != nil {
|
||||
t.Fatalf("initially-inactive Endpoint.RecvFirst() failed: %v", err)
|
||||
defer serverRun.Done()
|
||||
t.Logf("server Endpoint waiting for packet 1")
|
||||
if _, err := c.serverEP.RecvFirst(); err != nil {
|
||||
t.Fatalf("server Endpoint.RecvFirst() failed: %v", err)
|
||||
}
|
||||
t.Logf("initially-inactive Endpoint got packet 1, sending packet 2 and waiting for packet 3")
|
||||
if _, err := recvEP.SendRecv(0); err != nil {
|
||||
t.Fatalf("initially-inactive Endpoint.SendRecv() failed: %v", err)
|
||||
t.Logf("server Endpoint got packet 1, sending packet 2 and waiting for packet 3")
|
||||
if _, err := c.serverEP.SendRecv(0); err != nil {
|
||||
t.Fatalf("server Endpoint.SendRecv() failed: %v", err)
|
||||
}
|
||||
t.Logf("initially-inactive Endpoint got packet 3")
|
||||
t.Logf("server Endpoint got packet 3")
|
||||
}()
|
||||
defer func() {
|
||||
t.Logf("waiting for initially-inactive Endpoint goroutine to complete")
|
||||
<-otherThreadDone
|
||||
// Ensure that the server goroutine is cleaned up before
|
||||
// c.serverEP.Destroy(), even if the test fails.
|
||||
c.serverEP.Shutdown()
|
||||
serverRun.Wait()
|
||||
}()
|
||||
|
||||
t.Logf("initially-active Endpoint sending packet 1 and waiting for packet 2")
|
||||
if _, err := sendEP.SendRecv(0); err != nil {
|
||||
t.Fatalf("initially-active Endpoint.SendRecv() failed: %v", err)
|
||||
t.Logf("client Endpoint establishing connection")
|
||||
if err := c.clientEP.Connect(); err != nil {
|
||||
t.Fatalf("client Endpoint.Connect() failed: %v", err)
|
||||
}
|
||||
t.Logf("initially-active Endpoint got packet 2, sending packet 3")
|
||||
if err := sendEP.SendLast(0); err != nil {
|
||||
t.Fatalf("initially-active Endpoint.SendLast() failed: %v", err)
|
||||
t.Logf("client Endpoint sending packet 1 and waiting for packet 2")
|
||||
if _, err := c.clientEP.SendRecv(0); err != nil {
|
||||
t.Fatalf("client Endpoint.SendRecv() failed: %v", err)
|
||||
}
|
||||
t.Logf("client Endpoint got packet 2, sending packet 3")
|
||||
if err := c.clientEP.SendLast(0); err != nil {
|
||||
t.Fatalf("client Endpoint.SendLast() failed: %v", err)
|
||||
}
|
||||
t.Logf("waiting for server goroutine to complete")
|
||||
serverRun.Wait()
|
||||
}
|
||||
|
||||
func TestFutexSendRecv(t *testing.T) {
|
||||
testSendRecv(t, ControlModeFutex)
|
||||
func TestSendRecv(t *testing.T) {
|
||||
c := newTestConnection(t)
|
||||
defer c.destroy()
|
||||
testSendRecv(t, c)
|
||||
}
|
||||
|
||||
func testRecvFirstShutdown(t *testing.T, ctrlMode ControlMode) {
|
||||
pwa, err := NewPacketWindowAllocator()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create PacketWindowAllocator: %v", err)
|
||||
}
|
||||
defer pwa.Destroy()
|
||||
pwd, err := pwa.Allocate(testPacketWindowSize)
|
||||
if err != nil {
|
||||
t.Fatalf("PacketWindowAllocator.Allocate() failed: %v", err)
|
||||
}
|
||||
|
||||
ep, err := NewEndpoint(ctrlMode, pwd)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create Endpoint: %v", err)
|
||||
}
|
||||
defer ep.Destroy()
|
||||
|
||||
otherThreadDone := make(chan struct{})
|
||||
func testShutdownConnect(t *testing.T, c *testConnection) {
|
||||
var clientRun sync.WaitGroup
|
||||
clientRun.Add(1)
|
||||
go func() {
|
||||
defer func() { otherThreadDone <- struct{}{} }()
|
||||
_, err := ep.RecvFirst()
|
||||
defer clientRun.Done()
|
||||
if err := c.clientEP.Connect(); err == nil {
|
||||
t.Errorf("client Endpoint.Connect() succeeded unexpectedly")
|
||||
}
|
||||
}()
|
||||
time.Sleep(time.Second) // to allow c.clientEP.Connect() to block
|
||||
c.clientEP.Shutdown()
|
||||
clientRun.Wait()
|
||||
}
|
||||
|
||||
func TestShutdownConnect(t *testing.T) {
|
||||
c := newTestConnection(t)
|
||||
defer c.destroy()
|
||||
testShutdownConnect(t, c)
|
||||
}
|
||||
|
||||
func testShutdownRecvFirstBeforeConnect(t *testing.T, c *testConnection) {
|
||||
var serverRun sync.WaitGroup
|
||||
serverRun.Add(1)
|
||||
go func() {
|
||||
defer serverRun.Done()
|
||||
_, err := c.serverEP.RecvFirst()
|
||||
if err == nil {
|
||||
t.Errorf("Endpoint.RecvFirst() succeeded unexpectedly")
|
||||
t.Errorf("server Endpoint.RecvFirst() succeeded unexpectedly")
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(time.Second) // to ensure ep.RecvFirst() has blocked
|
||||
ep.Shutdown()
|
||||
<-otherThreadDone
|
||||
time.Sleep(time.Second) // to allow c.serverEP.RecvFirst() to block
|
||||
c.serverEP.Shutdown()
|
||||
serverRun.Wait()
|
||||
}
|
||||
|
||||
func TestFutexRecvFirstShutdown(t *testing.T) {
|
||||
testRecvFirstShutdown(t, ControlModeFutex)
|
||||
func TestShutdownRecvFirstBeforeConnect(t *testing.T) {
|
||||
c := newTestConnection(t)
|
||||
defer c.destroy()
|
||||
testShutdownRecvFirstBeforeConnect(t, c)
|
||||
}
|
||||
|
||||
func testSendRecvShutdown(t *testing.T, ctrlMode ControlMode) {
|
||||
pwa, err := NewPacketWindowAllocator()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create PacketWindowAllocator: %v", err)
|
||||
}
|
||||
defer pwa.Destroy()
|
||||
pwd, err := pwa.Allocate(testPacketWindowSize)
|
||||
if err != nil {
|
||||
t.Fatalf("PacketWindowAllocator.Allocate() failed: %v", err)
|
||||
}
|
||||
|
||||
sendEP, err := NewEndpoint(ctrlMode, pwd)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create Endpoint: %v", err)
|
||||
}
|
||||
defer sendEP.Destroy()
|
||||
recvEP, err := NewEndpoint(ctrlMode, pwd)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create Endpoint: %v", err)
|
||||
}
|
||||
defer recvEP.Destroy()
|
||||
|
||||
otherThreadDone := make(chan struct{})
|
||||
func testShutdownRecvFirstAfterConnect(t *testing.T, c *testConnection) {
|
||||
var serverRun sync.WaitGroup
|
||||
serverRun.Add(1)
|
||||
go func() {
|
||||
defer func() { otherThreadDone <- struct{}{} }()
|
||||
if _, err := recvEP.RecvFirst(); err != nil {
|
||||
t.Fatalf("initially-inactive Endpoint.RecvFirst() failed: %v", err)
|
||||
}
|
||||
if _, err := recvEP.SendRecv(0); err == nil {
|
||||
t.Errorf("initially-inactive Endpoint.SendRecv() succeeded unexpectedly")
|
||||
defer serverRun.Done()
|
||||
if _, err := c.serverEP.RecvFirst(); err == nil {
|
||||
t.Fatalf("server Endpoint.RecvFirst() succeeded unexpectedly")
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := sendEP.SendRecv(0); err != nil {
|
||||
t.Fatalf("initially-active Endpoint.SendRecv() failed: %v", err)
|
||||
defer func() {
|
||||
// Ensure that the server goroutine is cleaned up before
|
||||
// c.serverEP.Destroy(), even if the test fails.
|
||||
c.serverEP.Shutdown()
|
||||
serverRun.Wait()
|
||||
}()
|
||||
if err := c.clientEP.Connect(); err != nil {
|
||||
t.Fatalf("client Endpoint.Connect() failed: %v", err)
|
||||
}
|
||||
time.Sleep(time.Second) // to ensure recvEP.SendRecv() has blocked
|
||||
recvEP.Shutdown()
|
||||
<-otherThreadDone
|
||||
c.serverEP.Shutdown()
|
||||
serverRun.Wait()
|
||||
}
|
||||
|
||||
func TestFutexSendRecvShutdown(t *testing.T) {
|
||||
testSendRecvShutdown(t, ControlModeFutex)
|
||||
func TestShutdownRecvFirstAfterConnect(t *testing.T) {
|
||||
c := newTestConnection(t)
|
||||
defer c.destroy()
|
||||
testShutdownRecvFirstAfterConnect(t, c)
|
||||
}
|
||||
|
||||
func benchmarkSendRecv(b *testing.B, ctrlMode ControlMode) {
|
||||
pwa, err := NewPacketWindowAllocator()
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create PacketWindowAllocator: %v", err)
|
||||
}
|
||||
defer pwa.Destroy()
|
||||
pwd, err := pwa.Allocate(testPacketWindowSize)
|
||||
if err != nil {
|
||||
b.Fatalf("PacketWindowAllocator.Allocate() failed: %v", err)
|
||||
}
|
||||
|
||||
sendEP, err := NewEndpoint(ctrlMode, pwd)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create Endpoint: %v", err)
|
||||
}
|
||||
defer sendEP.Destroy()
|
||||
recvEP, err := NewEndpoint(ctrlMode, pwd)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create Endpoint: %v", err)
|
||||
}
|
||||
defer recvEP.Destroy()
|
||||
|
||||
otherThreadDone := make(chan struct{})
|
||||
func testShutdownSendRecv(t *testing.T, c *testConnection) {
|
||||
var serverRun sync.WaitGroup
|
||||
serverRun.Add(1)
|
||||
go func() {
|
||||
defer func() { otherThreadDone <- struct{}{} }()
|
||||
defer serverRun.Done()
|
||||
if _, err := c.serverEP.RecvFirst(); err != nil {
|
||||
t.Fatalf("server Endpoint.RecvFirst() failed: %v", err)
|
||||
}
|
||||
if _, err := c.serverEP.SendRecv(0); err == nil {
|
||||
t.Errorf("server Endpoint.SendRecv() succeeded unexpectedly")
|
||||
}
|
||||
}()
|
||||
defer func() {
|
||||
// Ensure that the server goroutine is cleaned up before
|
||||
// c.serverEP.Destroy(), even if the test fails.
|
||||
c.serverEP.Shutdown()
|
||||
serverRun.Wait()
|
||||
}()
|
||||
if err := c.clientEP.Connect(); err != nil {
|
||||
t.Fatalf("client Endpoint.Connect() failed: %v", err)
|
||||
}
|
||||
if _, err := c.clientEP.SendRecv(0); err != nil {
|
||||
t.Fatalf("client Endpoint.SendRecv() failed: %v", err)
|
||||
}
|
||||
time.Sleep(time.Second) // to allow serverEP.SendRecv() to block
|
||||
c.serverEP.Shutdown()
|
||||
serverRun.Wait()
|
||||
}
|
||||
|
||||
func TestShutdownSendRecv(t *testing.T) {
|
||||
c := newTestConnection(t)
|
||||
defer c.destroy()
|
||||
testShutdownSendRecv(t, c)
|
||||
}
|
||||
|
||||
func benchmarkSendRecv(b *testing.B, c *testConnection) {
|
||||
var serverRun sync.WaitGroup
|
||||
serverRun.Add(1)
|
||||
go func() {
|
||||
defer serverRun.Done()
|
||||
if b.N == 0 {
|
||||
return
|
||||
}
|
||||
if _, err := recvEP.RecvFirst(); err != nil {
|
||||
b.Fatalf("initially-inactive Endpoint.RecvFirst() failed: %v", err)
|
||||
if _, err := c.serverEP.RecvFirst(); err != nil {
|
||||
b.Fatalf("server Endpoint.RecvFirst() failed: %v", err)
|
||||
}
|
||||
for i := 1; i < b.N; i++ {
|
||||
if _, err := recvEP.SendRecv(0); err != nil {
|
||||
b.Fatalf("initially-inactive Endpoint.SendRecv() failed: %v", err)
|
||||
if _, err := c.serverEP.SendRecv(0); err != nil {
|
||||
b.Fatalf("server Endpoint.SendRecv() failed: %v", err)
|
||||
}
|
||||
}
|
||||
if err := recvEP.SendLast(0); err != nil {
|
||||
b.Fatalf("initially-inactive Endpoint.SendLast() failed: %v", err)
|
||||
if err := c.serverEP.SendLast(0); err != nil {
|
||||
b.Fatalf("server Endpoint.SendLast() failed: %v", err)
|
||||
}
|
||||
}()
|
||||
defer func() { <-otherThreadDone }()
|
||||
defer func() {
|
||||
c.serverEP.Shutdown()
|
||||
serverRun.Wait()
|
||||
}()
|
||||
|
||||
if err := c.clientEP.Connect(); err != nil {
|
||||
b.Fatalf("client Endpoint.Connect() failed: %v", err)
|
||||
}
|
||||
runtime.GC()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := sendEP.SendRecv(0); err != nil {
|
||||
b.Fatalf("initially-active Endpoint.SendRecv() failed: %v", err)
|
||||
if _, err := c.clientEP.SendRecv(0); err != nil {
|
||||
b.Fatalf("client Endpoint.SendRecv() failed: %v", err)
|
||||
}
|
||||
}
|
||||
b.StopTimer()
|
||||
}
|
||||
|
||||
func BenchmarkFutexSendRecv(b *testing.B) {
|
||||
benchmarkSendRecv(b, ControlModeFutex)
|
||||
func BenchmarkSendRecv(b *testing.B) {
|
||||
c := newTestConnection(b)
|
||||
defer c.destroy()
|
||||
benchmarkSendRecv(b, c)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2019 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 flipcall
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Packets consist of an 8-byte header followed by an arbitrarily-sized
|
||||
// datagram. The header consists of:
|
||||
//
|
||||
// - A 4-byte native-endian connection state.
|
||||
//
|
||||
// - A 4-byte native-endian datagram length in bytes.
|
||||
const (
|
||||
sizeofUint32 = unsafe.Sizeof(uint32(0))
|
||||
|
||||
// PacketHeaderBytes is the size of a flipcall packet header in bytes. The
|
||||
// maximum datagram size supported by a flipcall connection is equal to the
|
||||
// length of the packet window minus PacketHeaderBytes.
|
||||
//
|
||||
// PacketHeaderBytes is exported to support its use in constant
|
||||
// expressions. Non-constant expressions may prefer to use
|
||||
// PacketWindowLengthForDataCap().
|
||||
PacketHeaderBytes = 2 * sizeofUint32
|
||||
)
|
||||
|
||||
func (ep *Endpoint) connState() *uint32 {
|
||||
return (*uint32)((unsafe.Pointer)(ep.packet))
|
||||
}
|
||||
|
||||
func (ep *Endpoint) dataLen() *uint32 {
|
||||
return (*uint32)((unsafe.Pointer)(ep.packet + sizeofUint32))
|
||||
}
|
||||
|
||||
// Data returns the datagram part of ep's packet window as a byte slice.
|
||||
//
|
||||
// Note that the packet window is shared with the potentially-untrusted peer
|
||||
// Endpoint, which may concurrently mutate the contents of the packet window.
|
||||
// Thus:
|
||||
//
|
||||
// - Readers must not assume that two reads of the same byte in Data() will
|
||||
// return the same result. In other words, readers should read any given byte
|
||||
// in Data() at most once.
|
||||
//
|
||||
// - Writers must not assume that they will read back the same data that they
|
||||
// have written. In other words, writers should avoid reading from Data() at
|
||||
// all.
|
||||
func (ep *Endpoint) Data() []byte {
|
||||
var bs []byte
|
||||
bsReflect := (*reflect.SliceHeader)((unsafe.Pointer)(&bs))
|
||||
bsReflect.Data = ep.packet + PacketHeaderBytes
|
||||
bsReflect.Len = int(ep.dataCap)
|
||||
bsReflect.Cap = int(ep.dataCap)
|
||||
return bs
|
||||
}
|
||||
+63
-46
@@ -17,78 +17,95 @@
|
||||
package flipcall
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/abi/linux"
|
||||
"gvisor.dev/gvisor/pkg/log"
|
||||
)
|
||||
|
||||
func (ep *Endpoint) doFutexRoundTrip() error {
|
||||
ourSeq, err := ep.doFutexNotifySeq()
|
||||
if err != nil {
|
||||
return err
|
||||
func (ep *Endpoint) futexConnect(req *ctrlHandshakeRequest) (ctrlHandshakeResponse, error) {
|
||||
var resp ctrlHandshakeResponse
|
||||
|
||||
// Write the handshake request.
|
||||
w := ep.NewWriter()
|
||||
if err := json.NewEncoder(w).Encode(req); err != nil {
|
||||
return resp, fmt.Errorf("error writing handshake request: %v", err)
|
||||
}
|
||||
return ep.doFutexWaitSeq(ourSeq)
|
||||
}
|
||||
*ep.dataLen() = w.Len()
|
||||
|
||||
func (ep *Endpoint) doFutexWaitFirst() error {
|
||||
return ep.doFutexWaitSeq(0)
|
||||
}
|
||||
|
||||
func (ep *Endpoint) doFutexNotifyLast() error {
|
||||
_, err := ep.doFutexNotifySeq()
|
||||
return err
|
||||
}
|
||||
|
||||
func (ep *Endpoint) doFutexNotifySeq() (uint32, error) {
|
||||
ourSeq := atomic.AddUint32(ep.seq(), 1)
|
||||
if err := ep.futexWake(1); err != nil {
|
||||
return ourSeq, fmt.Errorf("failed to FUTEX_WAKE peer Endpoint: %v", err)
|
||||
// Exchange control with the server.
|
||||
if err := ep.futexSwitchToPeer(); err != nil {
|
||||
return resp, err
|
||||
}
|
||||
return ourSeq, nil
|
||||
if err := ep.futexSwitchFromPeer(); err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Read the handshake response.
|
||||
respLen := atomic.LoadUint32(ep.dataLen())
|
||||
if respLen > ep.dataCap {
|
||||
return resp, fmt.Errorf("invalid handshake response length %d (maximum %d)", respLen, ep.dataCap)
|
||||
}
|
||||
if err := json.NewDecoder(ep.NewReader(respLen)).Decode(&resp); err != nil {
|
||||
return resp, fmt.Errorf("error reading handshake response: %v", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (ep *Endpoint) doFutexWaitSeq(prevSeq uint32) error {
|
||||
nextSeq := prevSeq + 1
|
||||
func (ep *Endpoint) futexSwitchToPeer() error {
|
||||
// Update connection state to indicate that the peer should be active.
|
||||
if !atomic.CompareAndSwapUint32(ep.connState(), ep.activeState, ep.inactiveState) {
|
||||
return fmt.Errorf("unexpected connection state before FUTEX_WAKE: %v", atomic.LoadUint32(ep.connState()))
|
||||
}
|
||||
|
||||
// Wake the peer's Endpoint.futexSwitchFromPeer().
|
||||
if err := ep.futexWakeConnState(1); err != nil {
|
||||
return fmt.Errorf("failed to FUTEX_WAKE peer Endpoint: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *Endpoint) futexSwitchFromPeer() error {
|
||||
for {
|
||||
if ep.isShutdown() {
|
||||
return endpointShutdownError{}
|
||||
switch cs := atomic.LoadUint32(ep.connState()); cs {
|
||||
case ep.activeState:
|
||||
return nil
|
||||
case ep.inactiveState:
|
||||
// Continue to FUTEX_WAIT.
|
||||
default:
|
||||
return fmt.Errorf("unexpected connection state before FUTEX_WAIT: %v", cs)
|
||||
}
|
||||
if err := ep.futexWait(prevSeq); err != nil {
|
||||
if ep.isShutdownLocally() {
|
||||
return shutdownError{}
|
||||
}
|
||||
if err := ep.futexWaitConnState(ep.inactiveState); err != nil {
|
||||
return fmt.Errorf("failed to FUTEX_WAIT for peer Endpoint: %v", err)
|
||||
}
|
||||
seq := atomic.LoadUint32(ep.seq())
|
||||
if seq == nextSeq {
|
||||
return nil
|
||||
}
|
||||
if seq != prevSeq {
|
||||
return fmt.Errorf("invalid packet sequence number %d (expected %d or %d)", seq, prevSeq, nextSeq)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *Endpoint) doFutexInterruptForShutdown() {
|
||||
// Wake MaxInt32 threads to prevent a malicious or broken peer from
|
||||
// swallowing our wakeup by FUTEX_WAITing from multiple threads.
|
||||
if err := ep.futexWake(math.MaxInt32); err != nil {
|
||||
log.Warningf("failed to FUTEX_WAKE Endpoint: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *Endpoint) futexWake(numThreads int32) error {
|
||||
if _, _, e := syscall.RawSyscall(syscall.SYS_FUTEX, uintptr(ep.packet), linux.FUTEX_WAKE, uintptr(numThreads)); e != 0 {
|
||||
func (ep *Endpoint) futexWakeConnState(numThreads int32) error {
|
||||
if _, _, e := syscall.RawSyscall(syscall.SYS_FUTEX, ep.packet, linux.FUTEX_WAKE, uintptr(numThreads)); e != 0 {
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *Endpoint) futexWait(seq uint32) error {
|
||||
_, _, e := syscall.Syscall6(syscall.SYS_FUTEX, uintptr(ep.packet), linux.FUTEX_WAIT, uintptr(seq), 0, 0, 0)
|
||||
func (ep *Endpoint) futexWaitConnState(curState uint32) error {
|
||||
_, _, e := syscall.Syscall6(syscall.SYS_FUTEX, ep.packet, linux.FUTEX_WAIT, uintptr(curState), 0, 0, 0)
|
||||
if e != 0 && e != syscall.EAGAIN && e != syscall.EINTR {
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func yieldThread() {
|
||||
syscall.Syscall(syscall.SYS_SCHED_YIELD, 0, 0, 0)
|
||||
// The thread we're trying to yield to may be waiting for a Go runtime P.
|
||||
// runtime.Gosched() will hand off ours if necessary.
|
||||
runtime.Gosched()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright 2019 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 flipcall
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// DatagramReader implements io.Reader by reading a datagram from an Endpoint's
|
||||
// packet window. Its use is optional; users that can use Endpoint.Data() more
|
||||
// efficiently are advised to do so.
|
||||
type DatagramReader struct {
|
||||
ep *Endpoint
|
||||
off uint32
|
||||
end uint32
|
||||
}
|
||||
|
||||
// Init must be called on zero-value DatagramReaders before first use.
|
||||
//
|
||||
// Preconditions: dataLen is 0, or was returned by a previous call to
|
||||
// ep.RecvFirst() or ep.SendRecv().
|
||||
func (r *DatagramReader) Init(ep *Endpoint, dataLen uint32) {
|
||||
r.ep = ep
|
||||
r.Reset(dataLen)
|
||||
}
|
||||
|
||||
// Reset causes r to begin reading a new datagram of the given length from the
|
||||
// associated Endpoint.
|
||||
//
|
||||
// Preconditions: dataLen is 0, or was returned by a previous call to the
|
||||
// associated Endpoint's RecvFirst() or SendRecv() methods.
|
||||
func (r *DatagramReader) Reset(dataLen uint32) {
|
||||
if dataLen > r.ep.dataCap {
|
||||
panic(fmt.Sprintf("invalid dataLen (%d) > ep.dataCap (%d)", dataLen, r.ep.dataCap))
|
||||
}
|
||||
r.off = 0
|
||||
r.end = dataLen
|
||||
}
|
||||
|
||||
// NewReader is a convenience function that returns an initialized
|
||||
// DatagramReader allocated on the heap.
|
||||
//
|
||||
// Preconditions: dataLen was returned by a previous call to ep.RecvFirst() or
|
||||
// ep.SendRecv().
|
||||
func (ep *Endpoint) NewReader(dataLen uint32) *DatagramReader {
|
||||
r := &DatagramReader{}
|
||||
r.Init(ep, dataLen)
|
||||
return r
|
||||
}
|
||||
|
||||
// Read implements io.Reader.Read.
|
||||
func (r *DatagramReader) Read(dst []byte) (int, error) {
|
||||
n := copy(dst, r.ep.Data()[r.off:r.end])
|
||||
r.off += uint32(n)
|
||||
if r.off == r.end {
|
||||
return n, io.EOF
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// DatagramWriter implements io.Writer by writing a datagram to an Endpoint's
|
||||
// packet window. Its use is optional; users that can use Endpoint.Data() more
|
||||
// efficiently are advised to do so.
|
||||
type DatagramWriter struct {
|
||||
ep *Endpoint
|
||||
off uint32
|
||||
}
|
||||
|
||||
// Init must be called on zero-value DatagramWriters before first use.
|
||||
func (w *DatagramWriter) Init(ep *Endpoint) {
|
||||
w.ep = ep
|
||||
}
|
||||
|
||||
// Reset causes w to begin writing a new datagram to the associated Endpoint.
|
||||
func (w *DatagramWriter) Reset() {
|
||||
w.off = 0
|
||||
}
|
||||
|
||||
// NewWriter is a convenience function that returns an initialized
|
||||
// DatagramWriter allocated on the heap.
|
||||
func (ep *Endpoint) NewWriter() *DatagramWriter {
|
||||
w := &DatagramWriter{}
|
||||
w.Init(ep)
|
||||
return w
|
||||
}
|
||||
|
||||
// Write implements io.Writer.Write.
|
||||
func (w *DatagramWriter) Write(src []byte) (int, error) {
|
||||
n := copy(w.ep.Data()[w.off:w.ep.dataCap], src)
|
||||
w.off += uint32(n)
|
||||
if n != len(src) {
|
||||
return n, fmt.Errorf("datagram would exceed maximum size of %d bytes", w.ep.dataCap)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Len returns the length of the written datagram.
|
||||
func (w *DatagramWriter) Len() uint32 {
|
||||
return w.off
|
||||
}
|
||||
@@ -34,10 +34,10 @@ func init() {
|
||||
// This is depended on by roundUpToPage().
|
||||
panic(fmt.Sprintf("system page size (%d) is not a power of 2", pageSize))
|
||||
}
|
||||
if uintptr(pageSize) < packetHeaderBytes {
|
||||
if uintptr(pageSize) < PacketHeaderBytes {
|
||||
// This is required since Endpoint.Init() imposes a minimum packet
|
||||
// window size of 1 page.
|
||||
panic(fmt.Sprintf("system page size (%d) is less than packet header size (%d)", pageSize, packetHeaderBytes))
|
||||
panic(fmt.Sprintf("system page size (%d) is less than packet header size (%d)", pageSize, PacketHeaderBytes))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ type PacketWindowDescriptor struct {
|
||||
// PacketWindowLengthForDataCap returns the minimum packet window size required
|
||||
// to accommodate datagrams of the given size in bytes.
|
||||
func PacketWindowLengthForDataCap(dataCap uint32) int {
|
||||
return roundUpToPage(int(dataCap) + int(packetHeaderBytes))
|
||||
return roundUpToPage(int(dataCap) + int(PacketHeaderBytes))
|
||||
}
|
||||
|
||||
func roundUpToPage(x int) int {
|
||||
|
||||
Reference in New Issue
Block a user