github.com/cilium/ebpf@v0.15.1-0.20240517100537-8079b37aa138/ringbuf/reader_test.go (about)

     1  package ringbuf
     2  
     3  import (
     4  	"errors"
     5  	"os"
     6  	"syscall"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/cilium/ebpf"
    11  	"github.com/cilium/ebpf/asm"
    12  	"github.com/cilium/ebpf/internal"
    13  	"github.com/cilium/ebpf/internal/testutils"
    14  	"github.com/cilium/ebpf/internal/testutils/fdtrace"
    15  	"github.com/cilium/ebpf/internal/unix"
    16  	"github.com/google/go-cmp/cmp"
    17  )
    18  
    19  type sampleMessage struct {
    20  	size  int
    21  	flags int32
    22  }
    23  
    24  func TestMain(m *testing.M) {
    25  	fdtrace.TestMain(m)
    26  }
    27  
    28  func TestRingbufReader(t *testing.T) {
    29  	testutils.SkipOnOldKernel(t, "5.8", "BPF ring buffer")
    30  
    31  	readerTests := []struct {
    32  		name     string
    33  		messages []sampleMessage
    34  		want     map[int][]byte
    35  	}{
    36  		{
    37  			name:     "send one short sample",
    38  			messages: []sampleMessage{{size: 5}},
    39  			want: map[int][]byte{
    40  				5: {1, 2, 3, 4, 4},
    41  			},
    42  		},
    43  		{
    44  			name:     "send three short samples, the second is discarded",
    45  			messages: []sampleMessage{{size: 5}, {size: 10}, {size: 15}},
    46  			want: map[int][]byte{
    47  				5:  {1, 2, 3, 4, 4},
    48  				15: {1, 2, 3, 4, 4, 3, 2, 1, 1, 2, 3, 4, 4, 3, 2},
    49  			},
    50  		},
    51  	}
    52  	for _, tt := range readerTests {
    53  		t.Run(tt.name, func(t *testing.T) {
    54  			prog, events := mustOutputSamplesProg(t, tt.messages...)
    55  
    56  			rd, err := NewReader(events)
    57  			if err != nil {
    58  				t.Fatal(err)
    59  			}
    60  			defer rd.Close()
    61  
    62  			if uint32(rd.BufferSize()) != 2*events.MaxEntries() {
    63  				t.Errorf("expected %d BufferSize, got %d", events.MaxEntries(), rd.BufferSize())
    64  			}
    65  
    66  			ret, _, err := prog.Test(internal.EmptyBPFContext)
    67  			testutils.SkipIfNotSupported(t, err)
    68  			if err != nil {
    69  				t.Fatal(err)
    70  			}
    71  
    72  			if errno := syscall.Errno(-int32(ret)); errno != 0 {
    73  				t.Fatal("Expected 0 as return value, got", errno)
    74  			}
    75  
    76  			raw := make(map[int][]byte)
    77  
    78  			for len(raw) < len(tt.want) {
    79  				record, err := rd.Read()
    80  				if err != nil {
    81  					t.Fatal("Can't read samples:", err)
    82  				}
    83  				raw[len(record.RawSample)] = record.RawSample
    84  				if len(raw) == len(tt.want) {
    85  					if record.Remaining != 0 {
    86  						t.Errorf("expected 0 Remaining, got %d", record.Remaining)
    87  					}
    88  				} else {
    89  					if record.Remaining == 0 {
    90  						t.Error("expected non-zero Remaining, got 0")
    91  					}
    92  				}
    93  			}
    94  
    95  			if diff := cmp.Diff(tt.want, raw); diff != "" {
    96  				t.Errorf("Read samples mismatch (-want +got):\n%s", diff)
    97  			}
    98  		})
    99  	}
   100  }
   101  
   102  func outputSamplesProg(sampleMessages ...sampleMessage) (*ebpf.Program, *ebpf.Map, error) {
   103  	events, err := ebpf.NewMap(&ebpf.MapSpec{
   104  		Type:       ebpf.RingBuf,
   105  		MaxEntries: 4096,
   106  	})
   107  	if err != nil {
   108  		return nil, nil, err
   109  	}
   110  
   111  	var maxSampleSize int
   112  	for _, sampleMessage := range sampleMessages {
   113  		if sampleMessage.size > maxSampleSize {
   114  			maxSampleSize = sampleMessage.size
   115  		}
   116  	}
   117  
   118  	insns := asm.Instructions{
   119  		asm.LoadImm(asm.R0, 0x0102030404030201, asm.DWord),
   120  		asm.Mov.Reg(asm.R9, asm.R1),
   121  	}
   122  
   123  	bufDwords := (maxSampleSize / 8) + 1
   124  	for i := 0; i < bufDwords; i++ {
   125  		insns = append(insns,
   126  			asm.StoreMem(asm.RFP, int16(i+1)*-8, asm.R0, asm.DWord),
   127  		)
   128  	}
   129  
   130  	for sampleIdx, sampleMessage := range sampleMessages {
   131  		insns = append(insns,
   132  			asm.LoadMapPtr(asm.R1, events.FD()),
   133  			asm.Mov.Imm(asm.R2, int32(sampleMessage.size)),
   134  			asm.Mov.Imm(asm.R3, int32(0)),
   135  			asm.FnRingbufReserve.Call(),
   136  			asm.JEq.Imm(asm.R0, 0, "exit"),
   137  			asm.Mov.Reg(asm.R5, asm.R0),
   138  		)
   139  		for i := 0; i < sampleMessage.size; i++ {
   140  			insns = append(insns,
   141  				asm.LoadMem(asm.R4, asm.RFP, int16(i+1)*-1, asm.Byte),
   142  				asm.StoreMem(asm.R5, int16(i), asm.R4, asm.Byte),
   143  			)
   144  		}
   145  
   146  		// discard every even sample
   147  		if sampleIdx&1 != 0 {
   148  			insns = append(insns,
   149  				asm.Mov.Reg(asm.R1, asm.R5),
   150  				asm.Mov.Imm(asm.R2, sampleMessage.flags),
   151  				asm.FnRingbufDiscard.Call(),
   152  			)
   153  		} else {
   154  			insns = append(insns,
   155  				asm.Mov.Reg(asm.R1, asm.R5),
   156  				asm.Mov.Imm(asm.R2, sampleMessage.flags),
   157  				asm.FnRingbufSubmit.Call(),
   158  			)
   159  		}
   160  	}
   161  
   162  	insns = append(insns,
   163  		asm.Mov.Imm(asm.R0, int32(0)).WithSymbol("exit"),
   164  		asm.Return(),
   165  	)
   166  
   167  	prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{
   168  		License:      "MIT",
   169  		Type:         ebpf.XDP,
   170  		Instructions: insns,
   171  	})
   172  	if err != nil {
   173  		events.Close()
   174  		return nil, nil, err
   175  	}
   176  
   177  	return prog, events, nil
   178  }
   179  
   180  func mustOutputSamplesProg(tb testing.TB, sampleMessages ...sampleMessage) (*ebpf.Program, *ebpf.Map) {
   181  	tb.Helper()
   182  
   183  	prog, events, err := outputSamplesProg(sampleMessages...)
   184  	if err != nil {
   185  		tb.Fatal(err)
   186  	}
   187  
   188  	tb.Cleanup(func() {
   189  		prog.Close()
   190  		events.Close()
   191  	})
   192  
   193  	return prog, events
   194  }
   195  
   196  func TestReaderBlocking(t *testing.T) {
   197  	testutils.SkipOnOldKernel(t, "5.8", "BPF ring buffer")
   198  
   199  	prog, events := mustOutputSamplesProg(t, sampleMessage{size: 5, flags: 0})
   200  	ret, _, err := prog.Test(internal.EmptyBPFContext)
   201  	testutils.SkipIfNotSupported(t, err)
   202  	if err != nil {
   203  		t.Fatal(err)
   204  	}
   205  
   206  	if errno := syscall.Errno(-int32(ret)); errno != 0 {
   207  		t.Fatal("Expected 0 as return value, got", errno)
   208  	}
   209  
   210  	rd, err := NewReader(events)
   211  	if err != nil {
   212  		t.Fatal(err)
   213  	}
   214  	defer rd.Close()
   215  
   216  	if _, err := rd.Read(); err != nil {
   217  		t.Fatal("Can't read first sample:", err)
   218  	}
   219  
   220  	errs := make(chan error, 1)
   221  	go func() {
   222  		_, err := rd.Read()
   223  		errs <- err
   224  	}()
   225  
   226  	select {
   227  	case err := <-errs:
   228  		t.Fatal("Read returns error instead of blocking:", err)
   229  	case <-time.After(100 * time.Millisecond):
   230  	}
   231  
   232  	// Close should interrupt blocking Read
   233  	if err := rd.Close(); err != nil {
   234  		t.Fatal(err)
   235  	}
   236  
   237  	select {
   238  	case err := <-errs:
   239  		if !errors.Is(err, ErrClosed) {
   240  			t.Fatal("Expected os.ErrClosed from interrupted Read, got:", err)
   241  		}
   242  	case <-time.After(time.Second):
   243  		t.Fatal("Close doesn't interrupt Read")
   244  	}
   245  
   246  	// And we should be able to call it multiple times
   247  	if err := rd.Close(); err != nil {
   248  		t.Fatal(err)
   249  	}
   250  
   251  	if _, err := rd.Read(); !errors.Is(err, ErrClosed) {
   252  		t.Fatal("Second Read on a closed RingbufReader doesn't return ErrClosed")
   253  	}
   254  }
   255  
   256  func TestReaderNoWakeup(t *testing.T) {
   257  	testutils.SkipOnOldKernel(t, "5.8", "BPF ring buffer")
   258  
   259  	prog, events := mustOutputSamplesProg(t,
   260  		sampleMessage{size: 5, flags: unix.BPF_RB_NO_WAKEUP}, // Read after timeout
   261  		sampleMessage{size: 6, flags: unix.BPF_RB_NO_WAKEUP}, // Discard
   262  		sampleMessage{size: 7, flags: unix.BPF_RB_NO_WAKEUP}) // Read won't block
   263  
   264  	rd, err := NewReader(events)
   265  	if err != nil {
   266  		t.Fatal(err)
   267  	}
   268  	defer rd.Close()
   269  
   270  	ret, _, err := prog.Test(internal.EmptyBPFContext)
   271  	testutils.SkipIfNotSupported(t, err)
   272  	if err != nil {
   273  		t.Fatal(err)
   274  	}
   275  
   276  	if errno := syscall.Errno(-int32(ret)); errno != 0 {
   277  		t.Fatal("Expected 0 as return value, got", errno)
   278  	}
   279  
   280  	rd.SetDeadline(time.Now())
   281  	record, err := rd.Read()
   282  
   283  	if err != nil {
   284  		t.Error("Expected no error from first Read, got:", err)
   285  	}
   286  	if len(record.RawSample) != 5 {
   287  		t.Errorf("Expected to read 5 bytes bot got %d", len(record.RawSample))
   288  	}
   289  
   290  	record, err = rd.Read()
   291  
   292  	if err != nil {
   293  		t.Error("Expected no error from second Read, got:", err)
   294  	}
   295  	if len(record.RawSample) != 7 {
   296  		t.Errorf("Expected to read 7 bytes bot got %d", len(record.RawSample))
   297  	}
   298  }
   299  
   300  func TestReaderSetDeadline(t *testing.T) {
   301  	testutils.SkipOnOldKernel(t, "5.8", "BPF ring buffer")
   302  
   303  	_, events := mustOutputSamplesProg(t, sampleMessage{size: 5, flags: 0})
   304  	rd, err := NewReader(events)
   305  	if err != nil {
   306  		t.Fatal(err)
   307  	}
   308  	defer rd.Close()
   309  
   310  	rd.SetDeadline(time.Now().Add(-time.Second))
   311  	if _, err := rd.Read(); !errors.Is(err, os.ErrDeadlineExceeded) {
   312  		t.Error("Expected os.ErrDeadlineExceeded from first Read, got:", err)
   313  	}
   314  	if _, err := rd.Read(); !errors.Is(err, os.ErrDeadlineExceeded) {
   315  		t.Error("Expected os.ErrDeadlineExceeded from second Read, got:", err)
   316  	}
   317  }
   318  
   319  func BenchmarkReader(b *testing.B) {
   320  	testutils.SkipOnOldKernel(b, "5.8", "BPF ring buffer")
   321  
   322  	readerBenchmarks := []struct {
   323  		name  string
   324  		flags int32
   325  	}{
   326  		{
   327  			name: "normal epoll with timeout -1",
   328  		},
   329  	}
   330  
   331  	for _, bm := range readerBenchmarks {
   332  		b.Run(bm.name, func(b *testing.B) {
   333  			prog, events := mustOutputSamplesProg(b, sampleMessage{size: 80, flags: bm.flags})
   334  
   335  			rd, err := NewReader(events)
   336  			if err != nil {
   337  				b.Fatal(err)
   338  			}
   339  			defer rd.Close()
   340  
   341  			buf := internal.EmptyBPFContext
   342  
   343  			b.ResetTimer()
   344  			b.ReportAllocs()
   345  
   346  			for i := 0; i < b.N; i++ {
   347  				ret, _, err := prog.Test(buf)
   348  				if err != nil {
   349  					b.Fatal(err)
   350  				} else if errno := syscall.Errno(-int32(ret)); errno != 0 {
   351  					b.Fatal("Expected 0 as return value, got", errno)
   352  				}
   353  				_, err = rd.Read()
   354  				if err != nil {
   355  					b.Fatal("Can't read samples:", err)
   356  				}
   357  			}
   358  		})
   359  	}
   360  }
   361  
   362  func BenchmarkReadInto(b *testing.B) {
   363  	testutils.SkipOnOldKernel(b, "5.8", "BPF ring buffer")
   364  
   365  	prog, events := mustOutputSamplesProg(b, sampleMessage{size: 80, flags: 0})
   366  
   367  	rd, err := NewReader(events)
   368  	if err != nil {
   369  		b.Fatal(err)
   370  	}
   371  	defer rd.Close()
   372  
   373  	buf := internal.EmptyBPFContext
   374  
   375  	b.ResetTimer()
   376  	b.ReportAllocs()
   377  
   378  	var rec Record
   379  	for i := 0; i < b.N; i++ {
   380  		ret, _, err := prog.Test(buf)
   381  		if err != nil {
   382  			b.Fatal(err)
   383  		} else if errno := syscall.Errno(-int32(ret)); errno != 0 {
   384  			b.Fatal("Expected 0 as return value, got", errno)
   385  		}
   386  
   387  		if err := rd.ReadInto(&rec); err != nil {
   388  			b.Fatal("Can't read samples:", err)
   389  		}
   390  	}
   391  }