Add support for TCP send buffer auto tuning.

Send buffer size in TCP indicates the amount of bytes available for the sender
to transmit. This change will allow TCP to update the send buffer size when
- TCP enters established state.
- ACK is received.

The auto tuning is disabled when the send buffer size is set with the
SO_SNDBUF option.

PiperOrigin-RevId: 390312274
This commit is contained in:
Nayana Bidari
2021-08-12 00:51:35 -07:00
committed by gVisor bot
parent 01cfe59528
commit 96459f5598
8 changed files with 245 additions and 6 deletions
@@ -129,9 +129,9 @@ func newConnectioned(ctx context.Context, stype linux.SockType, uid UniqueIDProv
stype: stype,
}
ep.ops.InitHandler(ep, &stackHandler{}, getSendBufferLimits, getReceiveBufferLimits)
ep.ops.SetSendBufferSize(defaultBufferSize, false /* notify */)
ep.ops.SetReceiveBufferSize(defaultBufferSize, false /* notify */)
ep.ops.InitHandler(ep, &stackHandler{}, getSendBufferLimits, getReceiveBufferLimits)
return ep
}
@@ -517,3 +517,6 @@ func (e *connectionedEndpoint) OnSetSendBufferSize(v int64) (newSz int64) {
}
return v
}
// WakeupWriters implements tcpip.SocketOptionsHandler.WakeupWriters.
func (e *connectionedEndpoint) WakeupWriters() {}
@@ -44,9 +44,9 @@ func NewConnectionless(ctx context.Context) Endpoint {
q := queue{ReaderQueue: ep.Queue, WriterQueue: &waiter.Queue{}, limit: defaultBufferSize}
q.InitRefs()
ep.receiver = &queueReceiver{readQueue: &q}
ep.ops.InitHandler(ep, &stackHandler{}, getSendBufferLimits, getReceiveBufferLimits)
ep.ops.SetSendBufferSize(defaultBufferSize, false /* notify */)
ep.ops.SetReceiveBufferSize(defaultBufferSize, false /* notify */)
ep.ops.InitHandler(ep, &stackHandler{}, getSendBufferLimits, getReceiveBufferLimits)
return ep
}
@@ -227,3 +227,6 @@ func (e *connectionlessEndpoint) OnSetSendBufferSize(v int64) (newSz int64) {
}
return v
}
// WakeupWriters implements tcpip.SocketOptionsHandler.WakeupWriters.
func (e *connectionlessEndpoint) WakeupWriters() {}
+11
View File
@@ -57,6 +57,11 @@ type SocketOptionsHandler interface {
// OnSetReceiveBufferSize is invoked by SO_RCVBUF and SO_RCVBUFFORCE.
OnSetReceiveBufferSize(v, oldSz int64) (newSz int64)
// WakeupWriters is invoked when the send buffer size for an endpoint is
// changed. The handler notifies the writers if the send buffer size is
// increased with setsockopt(2) for TCP endpoints.
WakeupWriters()
}
// DefaultSocketOptionsHandler is an embeddable type that implements no-op
@@ -98,6 +103,9 @@ func (*DefaultSocketOptionsHandler) OnSetSendBufferSize(v int64) (newSz int64) {
return v
}
// WakeupWriters implements SocketOptionsHandler.WakeupWriters.
func (*DefaultSocketOptionsHandler) WakeupWriters() {}
// OnSetReceiveBufferSize implements SocketOptionsHandler.OnSetReceiveBufferSize.
func (*DefaultSocketOptionsHandler) OnSetReceiveBufferSize(v, oldSz int64) (newSz int64) {
return v
@@ -626,6 +634,9 @@ func (so *SocketOptions) SetSendBufferSize(sendBufferSize int64, notify bool) {
sendBufferSize = so.handler.OnSetSendBufferSize(sendBufferSize)
}
so.sendBufferSize.Store(sendBufferSize)
if notify {
so.handler.WakeupWriters()
}
}
// GetReceiveBufferSize gets value for SO_RCVBUF option.
+6
View File
@@ -386,6 +386,12 @@ type TCPSndBufState struct {
// SndMTU is the smallest MTU seen in the control packets received.
SndMTU int
// AutoTuneSndBufDisabled indicates that the auto tuning of send buffer
// is disabled.
//
// Must be accessed using atomic operations.
AutoTuneSndBufDisabled uint32
}
// TCPEndpointStateInner contains the members of TCPEndpointState used directly
+65 -1
View File
@@ -1717,6 +1717,27 @@ func (e *endpoint) OnSetReceiveBufferSize(rcvBufSz, oldSz int64) (newSz int64) {
return rcvBufSz
}
// OnSetSendBufferSize implements tcpip.SocketOptionsHandler.OnSetSendBufferSize.
func (e *endpoint) OnSetSendBufferSize(sz int64) int64 {
atomic.StoreUint32(&e.sndQueueInfo.TCPSndBufState.AutoTuneSndBufDisabled, 1)
return sz
}
// WakeupWriters implements tcpip.SocketOptionsHandler.WakeupWriters.
func (e *endpoint) WakeupWriters() {
e.LockUser()
defer e.UnlockUser()
sendBufferSize := e.getSendBufferSize()
e.sndQueueInfo.sndQueueMu.Lock()
notify := (sendBufferSize - e.sndQueueInfo.SndBufUsed) >= e.sndQueueInfo.SndBufUsed>>1
e.sndQueueInfo.sndQueueMu.Unlock()
if notify {
e.waiterQueue.Notify(waiter.WritableEvents)
}
}
// SetSockOptInt sets a socket option.
func (e *endpoint) SetSockOptInt(opt tcpip.SockOptInt, v int) tcpip.Error {
// Lower 2 bits represents ECN bits. RFC 3168, section 23.1
@@ -2329,6 +2350,9 @@ func (e *endpoint) connect(addr tcpip.FullAddress, handshake bool, run bool) tcp
e.segmentQueue.mu.Unlock()
e.snd.updateMaxPayloadSize(int(e.route.MTU()), 0)
e.setEndpointState(StateEstablished)
// Set the new auto tuned send buffer size after entering
// established state.
e.ops.SetSendBufferSize(e.computeTCPSendBufferSize(), false /* notify */)
}
if run {
@@ -2763,13 +2787,20 @@ func (e *endpoint) updateSndBufferUsage(v int) {
e.sndQueueInfo.sndQueueMu.Lock()
notify := e.sndQueueInfo.SndBufUsed >= sendBufferSize>>1
e.sndQueueInfo.SndBufUsed -= v
// Get the new send buffer size with auto tuning, but do not set it
// unless we decide to notify the writers.
newSndBufSz := e.computeTCPSendBufferSize()
// We only notify when there is half the sendBufferSize available after
// a full buffer event occurs. This ensures that we don't wake up
// writers to queue just 1-2 segments and go back to sleep.
notify = notify && e.sndQueueInfo.SndBufUsed < sendBufferSize>>1
notify = notify && e.sndQueueInfo.SndBufUsed < int(newSndBufSz)>>1
e.sndQueueInfo.sndQueueMu.Unlock()
if notify {
// Set the new send buffer size calculated from auto tuning.
e.ops.SetSendBufferSize(newSndBufSz, false /* notify */)
e.waiterQueue.Notify(waiter.WritableEvents)
}
}
@@ -3091,3 +3122,36 @@ func GetTCPReceiveBufferLimits(s tcpip.StackHandler) tcpip.ReceiveBufferSizeOpti
Max: ss.Max,
}
}
// computeTCPSendBufferSize implements auto tuning of send buffer size and
// returns the new send buffer size.
func (e *endpoint) computeTCPSendBufferSize() int64 {
curSndBufSz := int64(e.getSendBufferSize())
// Auto tuning is disabled when the user explicitly sets the send
// buffer size with SO_SNDBUF option.
if disabled := atomic.LoadUint32(&e.sndQueueInfo.TCPSndBufState.AutoTuneSndBufDisabled); disabled == 1 {
return curSndBufSz
}
const packetOverheadFactor = 2
curMSS := e.snd.MaxPayloadSize
numSeg := InitialCwnd
if numSeg < e.snd.SndCwnd {
numSeg = e.snd.SndCwnd
}
// SndCwnd indicates the number of segments that can be sent. This means
// that the sender can send upto #SndCwnd segments and the send buffer
// size should be set to SndCwnd*MSS to accommodate sending of all the
// segments.
newSndBufSz := int64(numSeg * curMSS * packetOverheadFactor)
if newSndBufSz < curSndBufSz {
return curSndBufSz
}
if ss := GetTCPSendBufferLimits(e.stack); int64(ss.Max) < newSndBufSz {
newSndBufSz = int64(ss.Max)
}
return newSndBufSz
}
+3 -3
View File
@@ -1415,9 +1415,6 @@ func (s *sender) handleRcvdSegment(rcvdSeg *segment) {
ackLeft -= datalen
}
// Update the send buffer usage and notify potential waiters.
s.ep.updateSndBufferUsage(int(acked))
// Clear SACK information for all acked data.
s.ep.scoreboard.Delete(s.SndUna)
@@ -1437,6 +1434,9 @@ func (s *sender) handleRcvdSegment(rcvdSeg *segment) {
}
}
// Update the send buffer usage and notify potential waiters.
s.ep.updateSndBufferUsage(int(acked))
// It is possible for s.outstanding to drop below zero if we get
// a retransmit timeout, reset outstanding to zero but later
// get an ack that cover previously sent data.
+92
View File
@@ -7964,3 +7964,95 @@ func generateRandomPayload(t *testing.T, n int) []byte {
}
return buf
}
func TestSendBufferTuning(t *testing.T) {
const maxPayload = 536
const mtu = header.TCPMinimumSize + header.IPv4MinimumSize + maxTCPOptionSize + maxPayload
const packetOverheadFactor = 2
testCases := []struct {
name string
autoTuningDisabled bool
}{
{"autoTuningDisabled", true},
{"autoTuningEnabled", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c := context.New(t, mtu)
defer c.Cleanup()
// Set the stack option for send buffer size.
const defaultSndBufSz = maxPayload * tcp.InitialCwnd
const maxSndBufSz = defaultSndBufSz * 10
{
opt := tcpip.TCPSendBufferSizeRangeOption{Min: 1, Default: defaultSndBufSz, Max: maxSndBufSz}
if err := c.Stack().SetTransportProtocolOption(tcp.ProtocolNumber, &opt); err != nil {
t.Fatalf("SetTransportProtocolOption(%d, &%#v): %s", tcp.ProtocolNumber, opt, err)
}
}
c.CreateConnected(context.TestInitialSequenceNumber, 30000, -1 /* epRcvBuf */)
oldSz := c.EP.SocketOptions().GetSendBufferSize()
if oldSz != defaultSndBufSz {
t.Fatalf("Wrong send buffer size got %d want %d", oldSz, defaultSndBufSz)
}
if tc.autoTuningDisabled {
c.EP.SocketOptions().SetSendBufferSize(defaultSndBufSz, true /* notify */)
}
data := make([]byte, maxPayload)
for i := range data {
data[i] = byte(i)
}
w, ch := waiter.NewChannelEntry(nil)
c.WQ.EventRegister(&w, waiter.WritableEvents)
defer c.WQ.EventUnregister(&w)
bytesRead := 0
for {
// Packets will be sent till the send buffer
// size is reached.
var r bytes.Reader
r.Reset(data[bytesRead : bytesRead+maxPayload])
_, err := c.EP.Write(&r, tcpip.WriteOptions{})
if cmp.Equal(&tcpip.ErrWouldBlock{}, err) {
break
}
c.ReceiveAndCheckPacketWithOptions(data, bytesRead, maxPayload, 0)
bytesRead += maxPayload
data = append(data, data...)
}
// Send an ACK and wait for connection to become writable again.
c.SendAck(seqnum.Value(context.TestInitialSequenceNumber).Add(1), bytesRead)
select {
case <-ch:
if err := c.EP.LastError(); err != nil {
t.Fatalf("Write failed: %s", err)
}
case <-time.After(1 * time.Second):
t.Fatalf("Timed out waiting for connection")
}
outSz := int64(defaultSndBufSz)
if !tc.autoTuningDisabled {
// Calculate the new auto tuned send buffer.
var info tcpip.TCPInfoOption
if err := c.EP.GetSockOpt(&info); err != nil {
t.Fatalf("GetSockOpt failed: %v", err)
}
outSz = (int64(info.SndCwnd) * packetOverheadFactor * (maxPayload))
}
if newSz := c.EP.SocketOptions().GetSendBufferSize(); newSz != outSz {
t.Fatalf("Wrong send buffer size, got %d want %d", newSz, outSz)
}
})
}
}
+60
View File
@@ -2088,6 +2088,66 @@ TEST_P(SimpleTcpSocketTest, ConnectUnspecifiedAddress) {
}
}
// Tests that send will return EWOULDBLOCK initially with large buffer and will
// succeed after the send buffer size is increased.
TEST_P(TcpSocketTest, SendUnblocksOnSendBufferIncrease) {
// Set the FD to O_NONBLOCK.
int opts;
ASSERT_THAT(opts = fcntl(first_fd, F_GETFL), SyscallSucceeds());
opts |= O_NONBLOCK;
ASSERT_THAT(fcntl(first_fd, F_SETFL, opts), SyscallSucceeds());
// Get maximum buffer size by trying to set it to a large value.
constexpr int kSndBufSz = 0xffffffff;
ASSERT_THAT(setsockopt(first_fd, SOL_SOCKET, SO_SNDBUF, &kSndBufSz,
sizeof(kSndBufSz)),
SyscallSucceeds());
int max_buffer_sz = 0;
socklen_t max_len = sizeof(max_buffer_sz);
ASSERT_THAT(
getsockopt(first_fd, SOL_SOCKET, SO_SNDBUF, &max_buffer_sz, &max_len),
SyscallSucceeds());
int buffer_sz = max_buffer_sz >> 2;
EXPECT_THAT(setsockopt(first_fd, SOL_SOCKET, SO_SNDBUF, &buffer_sz,
sizeof(buffer_sz)),
SyscallSucceedsWithValue(0));
// Create a large buffer that will be used for sending.
std::vector<char> buffer(max_buffer_sz);
// Write until we receive an error.
while (RetryEINTR(send)(first_fd, buffer.data(), buffer.size(), 0) != -1) {
// Sleep to give linux a chance to move data from the send buffer to the
// receive buffer.
usleep(10000); // 10ms.
}
// The last error should have been EWOULDBLOCK.
ASSERT_EQ(errno, EWOULDBLOCK);
ScopedThread send_thread([this]() {
int flags = 0;
ASSERT_THAT(flags = fcntl(first_fd, F_GETFL), SyscallSucceeds());
EXPECT_THAT(fcntl(first_fd, F_SETFL, flags & ~O_NONBLOCK),
SyscallSucceeds());
// Expect the send() to succeed.
char buffer;
ASSERT_THAT(RetryEINTR(send)(first_fd, &buffer, sizeof(buffer), 0),
SyscallSucceeds());
});
// Set SO_SNDBUF to maximum buffer size allowed.
buffer_sz = max_buffer_sz >> 1;
EXPECT_THAT(setsockopt(first_fd, SOL_SOCKET, SO_SNDBUF, &buffer_sz,
sizeof(buffer_sz)),
SyscallSucceedsWithValue(0));
send_thread.Join();
}
INSTANTIATE_TEST_SUITE_P(AllInetTests, SimpleTcpSocketTest,
::testing::Values(AF_INET, AF_INET6));