Repository: cilium/ebpf
Pull request #207 (2021-01-29 22:42:56)

Add BPF Batch Ops Methods
As of kernel v5.6 batch methods allow for the
fast lookup, deletion, and updating of bpf maps
so that the syscall overhead (repeatedly calling
into any of these methods) can be avoided.

Add support for BatchUpdate, BatchLookup, BatchLookupDelete, and BatchDelete
as well as tests for all of the above.

Signed-off-by: Nate Sweet <nathanjsweet@pm.me>

ti-mo: Hi Nate, I realize I'm a little late to the party, sorry about that. I have some gripes with the API proposed here, but that should not necessarily be a blocker to merge this as long as we don't slap a version number on it.

- It feels more like C than Go (taking mostly references as function arguments, etc.)
- `interface{}` everywhere and generous use of `reflect`
- It expects the caller to allocate everything (I know this was likely done to avoid allocs)
- Function documentation is rather sparse and there are no examples provided at all

I would suggest a `MapIterator`-like approach here, although I would also like to propose some changes to `MapIterator` to reduce its reliance on `interface{}`.

Let's call it.. `MapBatchIterator`? Instead of `Next()` returning a slice of results, it pulls the results into an internal preallocated scratch buffer to avoid any iterator-related allocations. After a successful pull, the caller invokes `Batch()` or `Results()` or something similar, which returns the result set. The efficiency gains here may be non-existent, but it's meant to be consistent with what I had in mind for `MapIterator`, where this approach could eliminate any use of `interface{}` for primitive k/v types.

Iterator pagination state is kept internally, and must be reset using a `Reset()` method to restart the iteration, allowing them to be re-used. Iterators are not thread-safe, that would be up to the caller to implement.

I know this omits a lot of the details like how we would design typing for arbitrary key/value types, but I think that's a separate discussion we should have for the lib in general.

WDYT?
lmb: You're right, it's not a very nice API. For better or worse I think it's useful to have it though: there is always something lost in translation when we build a higher-level interface, some use case that we can't foresee. This kind of bare bones API is a pressure release valve for such situations: users can just drop down to the ugly interface and aren't blocked. `Map.Update` and `Map.NextKey` are other examples of such an API. Their downside is that the ugly interfaces make it harder to find and use the nicer ones. I don't have a good solution for this except moving low level stuff in to a separate package. That seems like a lot of boilerplate though.

So my take is: we should merge this once we've got the array vs hashmap ErrNotExist thing figured out.

> I would suggest a MapIterator-like approach here, although I would also like to propose some changes to MapIterator to reduce its reliance on interface{}.
> Let's call it.. MapBatchIterator? Instead of Next() returning a slice of results, it pulls the results into an internal preallocated scratch buffer to avoid any iterator-related allocations.

I kind of figured that we could make MapIterator use batch lookups behind the scenes, if they are available. So `Next()` still operates on single elements but in the background we only issue one batch lookup every X calls to Next.

Is this something you were considering @nathanjsweet?

> I know this omits a lot of the details like how we would design typing for arbitrary key/value types, but I think that's a separate discussion we should have for the lib in general.

Can you create an issue and describe your ideas for MapIterator and/or key value typing? I agonized about `interface{}` in the beginning, but in practice I can't find much wrong with it. Also keep in mind that Go generics are on the horizon (another year?) which will surely make this a lot nicer.
nathanjsweet: > Hi Nate, I realize I'm a little late to the party, sorry about that. I have some gripes with the API proposed here, but that should not necessarily be a blocker to merge this as long as we don't slap a version number on it.
> 
> It feels more like C than Go (taking mostly references as function arguments, etc.)
> interface{} everywhere and generous use of reflect
> It expects the caller to allocate everything (I know this was likely done to avoid allocs)
> Function documentation is rather sparse and there are no examples provided at all

I share your concerns @ti-mo. I'm not wild about it either. All I can say is that I'm excited for generics to land. My only real defense is that I tried to be generous in the error messaging. Also, I wouldn't say there are no examples. The tests offer some decent examples for the 3 different map types batch supports.

> I would suggest a MapIterator-like approach here, although I would also like to propose some changes to MapIterator to reduce its reliance on interface{}.
> 
> Let's call it.. MapBatchIterator? Instead of Next() returning a slice of results, it pulls the results into an internal preallocated scratch buffer to avoid any iterator-related allocations. After a successful pull, the caller invokes Batch() or Results() or something similar, which returns the result set. The efficiency gains here may be non-existent, but it's meant to be consistent with what I had in mind for MapIterator, where this approach could eliminate any use of interface{} for primitive k/v types.
> 
> Iterator pagination state is kept internally, and must be reset using a Reset() method to restart the iteration, allowing them to be re-used. Iterators are not thread-safe, that would be up to the caller to implement.

I'm not opposed to this at all. It's a good idea, but I really don't like obfuscating basic operations from the users of this library. There are just too many variables with using the batch operations that prevent us from abstracting them in a way that would be satisfactory to everyone. Philosophically, I don't think we should be too scared about lots of methods piling up. There **are** lots methods/operations in eBPF. I do think we can go beyond the harshness of libbpf, but I never want to hide basic functionality.

> I know this omits a lot of the details like how we would design typing for arbitrary key/value types, but I think that's a separate discussion we should have for the lib in general.

We do need to have a better design philosophy that we can spell out in the repository. It's getting big enough that we should have a document we can all point to for justifying an approach. Maybe we could do a megathread on slack or have a zoom meeting.

nathanjsweet: > This is roughly how I would expect the API to behave based on our conversation. Here is what I get:

> === RUN   TestBatchAPIArray
    /home/lorenz/dev/ebpf/map_test.go:223: Expected ErrKeyNotExist when batch runs into end of array, got <nil>
    /home/lorenz/dev/ebpf/map_test.go:226: Expected a single result, got 0
    /home/lorenz/dev/ebpf/map_test.go:229: Expected first key to be 1, got 0
    /home/lorenz/dev/ebpf/map_test.go:232: Expected first value to be 4242, got 0
    /home/lorenz/dev/ebpf/map_test.go:237: Expected ErrKeyNotExist, got <nil>
    /home/lorenz/dev/ebpf/map_test.go:240: Expected no result, got 2

There's a few things wrong with your assumption.
1. `startKey` is excluded from the batch processing (i.e. it starts with the key after `startKey`).
2. The kernel returns ENOENT is used to indicate the end of the list even if a successful partial result set is returned. I decided against returning the error because it is not typical in golang to return an error even if everything went fine.

A possible workaround is that we can add a `done` return value that is "true" when the batch operation has reached the end of the map or array. Otherwise, I think the way the library is working maps to libbpf's behavior pretty cleanly.
nathanjsweet: @lmb I don't understand why the push test didn't work, but the PR one did.
lmb: @nathanjsweet I pushed three small clean up commits, if you agree with them feel free to squash + merge.

Patch:
diff --git a/internal/unix/types_linux.go b/internal/unix/types_linux.go
index 215f671..86d2a10 100644
--- a/internal/unix/types_linux.go
+++ b/internal/unix/types_linux.go
@@ -10,16 +10,19 @@ import (
 )
 
 const (
-	ENOENT                   = linux.ENOENT
-	EEXIST                   = linux.EEXIST
-	EAGAIN                   = linux.EAGAIN
-	ENOSPC                   = linux.ENOSPC
-	EINVAL                   = linux.EINVAL
-	EPOLLIN                  = linux.EPOLLIN
-	EINTR                    = linux.EINTR
-	EPERM                    = linux.EPERM
-	ESRCH                    = linux.ESRCH
-	ENODEV                   = linux.ENODEV
+	ENOENT  = linux.ENOENT
+	EEXIST  = linux.EEXIST
+	EAGAIN  = linux.EAGAIN
+	ENOSPC  = linux.ENOSPC
+	EINVAL  = linux.EINVAL
+	EPOLLIN = linux.EPOLLIN
+	EINTR   = linux.EINTR
+	EPERM   = linux.EPERM
+	ESRCH   = linux.ESRCH
+	ENODEV  = linux.ENODEV
+	// ENOTSUPP is not the same as ENOTSUP or EOPNOTSUP
+	ENOTSUPP = syscall.Errno(0x20c)
+
 	EBADF                    = linux.EBADF
 	BPF_F_NO_PREALLOC        = linux.BPF_F_NO_PREALLOC
 	BPF_F_NUMA_NODE          = linux.BPF_F_NUMA_NODE
diff --git a/internal/unix/types_other.go b/internal/unix/types_other.go
index 529800d..8c29179 100644
--- a/internal/unix/types_other.go
+++ b/internal/unix/types_other.go
@@ -11,16 +11,19 @@ import (
 var errNonLinux = fmt.Errorf("unsupported platform %s/%s", runtime.GOOS, runtime.GOARCH)
 
 const (
-	ENOENT                   = syscall.ENOENT
-	EEXIST                   = syscall.EEXIST
-	EAGAIN                   = syscall.EAGAIN
-	ENOSPC                   = syscall.ENOSPC
-	EINVAL                   = syscall.EINVAL
-	EINTR                    = syscall.EINTR
-	EPERM                    = syscall.EPERM
-	ESRCH                    = syscall.ESRCH
-	ENODEV                   = syscall.ENODEV
-	EBADF                    = syscall.Errno(0)
+	ENOENT = syscall.ENOENT
+	EEXIST = syscall.EEXIST
+	EAGAIN = syscall.EAGAIN
+	ENOSPC = syscall.ENOSPC
+	EINVAL = syscall.EINVAL
+	EINTR  = syscall.EINTR
+	EPERM  = syscall.EPERM
+	ESRCH  = syscall.ESRCH
+	ENODEV = syscall.ENODEV
+	EBADF  = syscall.Errno(0)
+	// ENOTSUPP is not the same as ENOTSUP or EOPNOTSUP
+	ENOTSUPP = syscall.Errno(0x20c)
+
 	BPF_F_NO_PREALLOC        = 0
 	BPF_F_NUMA_NODE          = 0
 	BPF_F_RDONLY_PROG        = 0
diff --git a/map.go b/map.go
index 658be27..75abc7a 100644
--- a/map.go
+++ b/map.go
@@ -6,6 +6,7 @@ import (
 	"io"
 	"os"
 	"path/filepath"
+	"reflect"
 	"strings"
 
 	"github.com/cilium/ebpf/internal"
@@ -418,7 +419,6 @@ func (m *Map) Info() (*MapInfo, error) {
 // Returns an error if the key doesn't exist, see ErrKeyNotExist.
 func (m *Map) Lookup(key, valueOut interface{}) error {
 	valuePtr, valueBytes := makeBuffer(valueOut, m.fullValueSize)
-
 	if err := m.lookup(key, valuePtr); err != nil {
 		return err
 	}
@@ -582,6 +582,158 @@ func (m *Map) nextKey(key interface{}, nextKeyOut internal.Pointer) error {
 	return nil
 }
 
+// BatchLookup looks up many elements in a map at once.
+//
+// "keysOut" and "valuesOut" must be of type slice, a pointer
+// to a slice or buffer will not work.
+// "prevKey" is the key to start the batch lookup from, it will
+// *not* be included in the results. Use nil to start at the first key.
+//
+// ErrKeyNotExist is returned when the batch lookup has reached
+// the end of all possible results, even when partial results
+// are returned. It should be used to evaluate when lookup is "done".
+func (m *Map) BatchLookup(prevKey, nextKeyOut, keysOut, valuesOut interface{}, opts *BatchOptions) (int, error) {
+	return m.batchLookup(internal.BPF_MAP_LOOKUP_BATCH, prevKey, nextKeyOut, keysOut, valuesOut, opts)
+}
+
+// BatchLookupAndDelete looks up many elements in a map at once,
+//
+// It then deletes all those elements.
+// "keysOut" and "valuesOut" must be of type slice, a pointer
+// to a slice or buffer will not work.
+// "prevKey" is the key to start the batch lookup from, it will
+// *not* be included in the results. Use nil to start at the first key.
+//
+// ErrKeyNotExist is returned when the batch lookup has reached
+// the end of all possible results, even when partial results
+// are returned. It should be used to evaluate when lookup is "done".
+func (m *Map) BatchLookupAndDelete(prevKey, nextKeyOut, keysOut, valuesOut interface{}, opts *BatchOptions) (int, error) {
+	return m.batchLookup(internal.BPF_MAP_LOOKUP_AND_DELETE_BATCH, prevKey, nextKeyOut, keysOut, valuesOut, opts)
+}
+
+func (m *Map) batchLookup(cmd internal.BPFCmd, startKey, nextKeyOut, keysOut, valuesOut interface{}, opts *BatchOptions) (int, error) {
+	if err := haveBatchAPI(); err != nil {
+		return 0, err
+	}
+	if m.typ.hasPerCPUValue() {
+		return 0, ErrNotSupported
+	}
+	keysValue := reflect.ValueOf(keysOut)
+	if keysValue.Kind() != reflect.Slice {
+		return 0, fmt.Errorf("keys must be a slice")
+	}
+	valuesValue := reflect.ValueOf(valuesOut)
+	if valuesValue.Kind() != reflect.Slice {
+		return 0, fmt.Errorf("valuesOut must be a slice")
+	}
+	count := keysValue.Len()
+	if count != valuesValue.Len() {
+		return 0, fmt.Errorf("keysOut and valuesOut must be the same length")
+	}
+	keyBuf := make([]byte, count*int(m.keySize))
+	keyPtr := internal.NewSlicePointer(keyBuf)
+	valueBuf := make([]byte, count*int(m.fullValueSize))
+	valuePtr := internal.NewSlicePointer(valueBuf)
+
+	var (
+		startPtr internal.Pointer
+		err      error
+		retErr   error
+	)
+	if startKey != nil {
+		startPtr, err = marshalPtr(startKey, int(m.keySize))
+		if err != nil {
+			return 0, err
+		}
+	}
+	nextPtr, nextBuf := makeBuffer(nextKeyOut, int(m.keySize))
+
+	ct, err := bpfMapBatch(cmd, m.fd, startPtr, nextPtr, keyPtr, valuePtr, uint32(count), opts)
+	if err != nil {
+		if !errors.Is(err, ErrKeyNotExist) {
+			return 0, err
+		}
+		retErr = ErrKeyNotExist
+	}
+
+	err = m.unmarshalKey(nextKeyOut, nextBuf)
+	if err != nil {
+		return 0, err
+	}
+	err = unmarshalBytes(keysOut, keyBuf)
+	if err != nil {
+		return 0, err
+	}
+	err = unmarshalBytes(valuesOut, valueBuf)
+	if err != nil {
+		retErr = err
+	}
+	return int(ct), retErr
+}
+
+// BatchUpdate updates the map with multiple keys and values
+// simultaneously.
+// "keys" and "values" must be of type slice, a pointer
+// to a slice or buffer will not work.
+func (m *Map) BatchUpdate(keys, values interface{}, opts *BatchOptions) (int, error) {
+	if err := haveBatchAPI(); err != nil {
+		return 0, err
+	}
+	if m.typ.hasPerCPUValue() {
+		return 0, ErrNotSupported
+	}
+	keysValue := reflect.ValueOf(keys)
+	if keysValue.Kind() != reflect.Slice {
+		return 0, fmt.Errorf("keys must be a slice")
+	}
+	valuesValue := reflect.ValueOf(values)
+	if valuesValue.Kind() != reflect.Slice {
+		return 0, fmt.Errorf("values must be a slice")
+	}
+	var (
+		count    = keysValue.Len()
+		valuePtr internal.Pointer
+		err      error
+	)
+	if count != valuesValue.Len() {
+		return 0, fmt.Errorf("keys and values must be the same length")
+	}
+	keyPtr, err := marshalPtr(keys, count*int(m.keySize))
+	if err != nil {
+		return 0, err
+	}
+	valuePtr, err = marshalPtr(values, count*int(m.valueSize))
+	if err != nil {
+		return 0, err
+	}
+	var nilPtr internal.Pointer
+	ct, err := bpfMapBatch(internal.BPF_MAP_UPDATE_BATCH, m.fd, nilPtr, nilPtr, keyPtr, valuePtr, uint32(count), opts)
+	return int(ct), err
+}
+
+// BatchDelete batch deletes entries in the map by keys.
+// "keys" must be of type slice, a pointer to a slice or buffer will not work.
+func (m *Map) BatchDelete(keys interface{}, opts *BatchOptions) (int, error) {
+	if err := haveBatchAPI(); err != nil {
+		return 0, err
+	}
+	if m.typ.hasPerCPUValue() {
+		return 0, ErrNotSupported
+	}
+	keysValue := reflect.ValueOf(keys)
+	if keysValue.Kind() != reflect.Slice {
+		return 0, fmt.Errorf("keys must be a slice")
+	}
+	count := keysValue.Len()
+	keyPtr, err := marshalPtr(keys, count*int(m.keySize))
+	if err != nil {
+		return 0, fmt.Errorf("cannot marshal keys: %v", err)
+	}
+	var nilPtr internal.Pointer
+	ct, err := bpfMapBatch(internal.BPF_MAP_DELETE_BATCH, m.fd, nilPtr, nilPtr, keyPtr, nilPtr, uint32(count), opts)
+	return int(ct), err
+}
+
 // Iterate traverses a map.
 //
 // It's safe to create multiple iterators at the same time.
diff --git a/syscalls.go b/syscalls.go
index 714a78a..271798b 100644
--- a/syscalls.go
+++ b/syscalls.go
@@ -11,9 +11,7 @@ import (
 )
 
 // Generic errors returned by BPF syscalls.
-var (
-	ErrNotExist = errors.New("requested object does not exist")
-)
+var ErrNotExist = errors.New("requested object does not exist")
 
 // bpfObjName is a null-terminated string made up of
 // 'A-Za-z0-9_' characters.
@@ -68,6 +66,17 @@ type bpfMapOpAttr struct {
 	flags   uint64
 }
 
+type bpfBatchMapOpAttr struct {
+	inBatch   internal.Pointer
+	outBatch  internal.Pointer
+	keys      internal.Pointer
+	values    internal.Pointer
+	count     uint32
+	mapFd     uint32
+	elemFlags uint64
+	flags     uint64
+}
+
 type bpfMapInfo struct {
 	map_type                  uint32 // since 4.12 1e2709769086
 	id                        uint32
@@ -321,6 +330,29 @@ func objGetNextID(cmd internal.BPFCmd, start uint32) (uint32, error) {
 	return attr.nextID, wrapObjError(err)
 }
 
+func bpfMapBatch(cmd internal.BPFCmd, m *internal.FD, inBatch, outBatch, keys, values internal.Pointer, count uint32, opts *BatchOptions) (uint32, error) {
+	fd, err := m.Value()
+	if err != nil {
+		return 0, err
+	}
+
+	attr := bpfBatchMapOpAttr{
+		inBatch:  inBatch,
+		outBatch: outBatch,
+		keys:     keys,
+		values:   values,
+		count:    count,
+		mapFd:    fd,
+	}
+	if opts != nil {
+		attr.elemFlags = opts.ElemFlags
+		attr.flags = opts.Flags
+	}
+	_, err = internal.BPF(cmd, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
+	// always return count even on an error, as things like update might partially be fulfilled.
+	return attr.count, wrapMapError(err)
+}
+
 func wrapObjError(err error) error {
 	if err == nil {
 		return nil
@@ -345,6 +377,10 @@ func wrapMapError(err error) error {
 		return ErrKeyExist
 	}
 
+	if errors.Is(err, unix.ENOTSUPP) {
+		return ErrNotSupported
+	}
+
 	return errors.New(err.Error())
 }
 
@@ -418,6 +454,32 @@ var objNameAllowsDot = internal.FeatureTest("dot in object names", "5.2", func()
 	return nil
 })
 
+var haveBatchAPI = internal.FeatureTest("map batch api", "5.6", func() error {
+	var maxEntries uint32 = 2
+	attr := bpfMapCreateAttr{
+		mapType:    Hash,
+		keySize:    4,
+		valueSize:  4,
+		maxEntries: maxEntries,
+	}
+
+	fd, err := bpfMapCreate(&attr)
+	if err != nil {
+		return internal.ErrNotSupported
+	}
+	defer fd.Close()
+	keys := []uint32{1, 2}
+	values := []uint32{3, 4}
+	kp, _ := marshalPtr(keys, 8)
+	vp, _ := marshalPtr(values, 8)
+	nilPtr := internal.NewPointer(nil)
+	_, err = bpfMapBatch(internal.BPF_MAP_UPDATE_BATCH, fd, nilPtr, nilPtr, kp, vp, maxEntries, nil)
+	if err != nil {
+		return internal.ErrNotSupported
+	}
+	return nil
+})
+
 func bpfObjGetFDByID(cmd internal.BPFCmd, id uint32) (*internal.FD, error) {
 	attr := bpfGetFDByIDAttr{
 		id: id,
diff --git a/types.go b/types.go
index 53e27ee..6927e2a 100644
--- a/types.go
+++ b/types.go
@@ -201,3 +201,13 @@ const (
 	// Pin an object by using its name as the filename.
 	PinByName
 )
+
+// BatchOptions batch map operations options
+//
+// Mirrors libbpf struct bpf_map_batch_opts
+// Currently BPF_F_FLAG is the only supported
+// flag (for ElemFlags).
+type BatchOptions struct {
+	ElemFlags uint64
+	Flags     uint64
+}


Test patch:
diff --git a/map_test.go b/map_test.go
index bf4d0f0..6fab94f 100644
--- a/map_test.go
+++ b/map_test.go
@@ -7,6 +7,7 @@ import (
 	"math"
 	"os"
 	"path/filepath"
+	"reflect"
 	"sort"
 	"strings"
 	"testing"
@@ -81,6 +82,197 @@ func TestMap(t *testing.T) {
 	}
 }
 
+func TestBatchAPIArray(t *testing.T) {
+	if err := haveBatchAPI(); err != nil {
+		t.Skipf("batch api not available: %v", err)
+	}
+	m, err := NewMap(&MapSpec{
+		Type:       Array,
+		KeySize:    4,
+		ValueSize:  4,
+		MaxEntries: 2,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer m.Close()
+
+	var (
+		nextKey      uint32
+		keys         = []uint32{0, 1}
+		values       = []uint32{42, 4242}
+		lookupKeys   = make([]uint32, 2)
+		lookupValues = make([]uint32, 2)
+		deleteKeys   = make([]uint32, 2)
+		deleteValues = make([]uint32, 2)
+	)
+
+	count, err := m.BatchUpdate(keys, values, nil)
+	if err != nil {
+		t.Fatalf("BatchUpdate: %v", err)
+	}
+	if count != len(keys) {
+		t.Fatalf("BatchUpdate: expected count, %d, to be %d", count, len(keys))
+	}
+
+	var v uint32
+	if err := m.Lookup(uint32(0), &v); err != nil {
+		t.Fatal("Can't lookup 0:", err)
+	}
+	if v != 42 {
+		t.Error("Want value 42, got", v)
+	}
+
+	count, err = m.BatchLookup(nil, &nextKey, lookupKeys, lookupValues, nil)
+	if err != nil {
+		t.Fatalf("BatchLookup: %v", err)
+	}
+	if count != len(lookupKeys) {
+		t.Fatalf("BatchLookup: returned %d results, expected %d", count, len(lookupKeys))
+	}
+	if nextKey != lookupKeys[1] {
+		t.Fatalf("BatchLookup: expected nextKey, %d, to be the same as the lastKey returned, %d", nextKey, lookupKeys[1])
+	}
+	if !reflect.DeepEqual(keys, lookupKeys) {
+		t.Errorf("BatchUpdate and BatchLookup keys disagree: %v %v", keys, lookupKeys)
+	}
+	if !reflect.DeepEqual(values, lookupValues) {
+		t.Errorf("BatchUpdate and BatchLookup values disagree: %v %v", values, lookupValues)
+	}
+
+	_, err = m.BatchLookupAndDelete(nil, &nextKey, deleteKeys, deleteValues, nil)
+	if !errors.Is(err, ErrNotSupported) {
+		t.Fatalf("BatchLookUpDelete: expected error %v, but got %v", ErrNotSupported, err)
+	}
+}
+
+func TestBatchAPIHash(t *testing.T) {
+	if err := haveBatchAPI(); err != nil {
+		t.Skipf("batch api not available: %v", err)
+	}
+	m, err := NewMap(&MapSpec{
+		Type:       Hash,
+		KeySize:    4,
+		ValueSize:  4,
+		MaxEntries: 10,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer m.Close()
+
+	var (
+		nextKey      uint32
+		keys         = []uint32{0, 1}
+		values       = []uint32{42, 4242}
+		lookupKeys   = make([]uint32, 2)
+		lookupValues = make([]uint32, 2)
+		deleteKeys   = make([]uint32, 2)
+		deleteValues = make([]uint32, 2)
+	)
+
+	count, err := m.BatchUpdate(keys, values, nil)
+	if err != nil {
+		t.Fatalf("BatchUpdate: %v", err)
+	}
+	if count != len(keys) {
+		t.Fatalf("BatchUpdate: expected count, %d, to be %d", count, len(keys))
+	}
+
+	var v uint32
+	if err := m.Lookup(uint32(0), &v); err != nil {
+		t.Fatal("Can't lookup 0:", err)
+	}
+	if v != 42 {
+		t.Error("Want value 42, got", v)
+	}
+
+	count, err = m.BatchLookup(nil, &nextKey, lookupKeys, lookupValues, nil)
+	if !errors.Is(err, ErrKeyNotExist) {
+		t.Fatalf("BatchLookup: expected %v got %v", ErrKeyNotExist, err)
+	}
+	if count != len(lookupKeys) {
+		t.Fatalf("BatchLookup: returned %d results, expected %d", count, len(lookupKeys))
+	}
+	sort.Slice(lookupKeys, func(i, j int) bool { return lookupKeys[i] < lookupKeys[j] })
+	if !reflect.DeepEqual(keys, lookupKeys) {
+		t.Errorf("BatchUpdate and BatchLookup keys disagree: %v %v", keys, lookupKeys)
+	}
+	sort.Slice(lookupValues, func(i, j int) bool { return lookupValues[i] < lookupValues[j] })
+	if !reflect.DeepEqual(values, lookupValues) {
+		t.Errorf("BatchUpdate and BatchLookup values disagree: %v %v", values, lookupValues)
+	}
+
+	count, err = m.BatchLookupAndDelete(nil, &nextKey, deleteKeys, deleteValues, nil)
+	if !errors.Is(err, ErrKeyNotExist) {
+		t.Fatalf("BatchLookupAndDelete: expected %v got %v", ErrKeyNotExist, err)
+	}
+	if count != len(deleteKeys) {
+		t.Fatalf("BatchLookupAndDelete: returned %d results, expected %d", count, len(deleteKeys))
+	}
+	sort.Slice(deleteKeys, func(i, j int) bool { return deleteKeys[i] < deleteKeys[j] })
+	if !reflect.DeepEqual(keys, deleteKeys) {
+		t.Errorf("BatchUpdate and BatchLookupAndDelete keys disagree: %v %v", keys, deleteKeys)
+	}
+	sort.Slice(deleteValues, func(i, j int) bool { return deleteValues[i] < deleteValues[j] })
+	if !reflect.DeepEqual(values, deleteValues) {
+		t.Errorf("BatchUpdate and BatchLookupAndDelete values disagree: %v %v", values, deleteValues)
+	}
+
+	if err := m.Lookup(uint32(0), &v); !errors.Is(err, ErrKeyNotExist) {
+		t.Fatalf("Lookup should have failed with error, %v, instead error is %v", ErrKeyNotExist, err)
+	}
+}
+
+func TestBatchAPIMapDelete(t *testing.T) {
+	if err := haveBatchAPI(); err != nil {
+		t.Skipf("batch api not available: %v", err)
+	}
+	m, err := NewMap(&MapSpec{
+		Type:       Hash,
+		KeySize:    4,
+		ValueSize:  4,
+		MaxEntries: 10,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer m.Close()
+
+	var (
+		keys   = []uint32{0, 1}
+		values = []uint32{42, 4242}
+	)
+
+	count, err := m.BatchUpdate(keys, values, nil)
+	if err != nil {
+		t.Fatalf("BatchUpdate: %v", err)
+	}
+	if count != len(keys) {
+		t.Fatalf("BatchUpdate: expected count, %d, to be %d", count, len(keys))
+	}
+
+	var v uint32
+	if err := m.Lookup(uint32(0), &v); err != nil {
+		t.Fatal("Can't lookup 0:", err)
+	}
+	if v != 42 {
+		t.Error("Want value 42, got", v)
+	}
+
+	count, err = m.BatchDelete(keys, nil)
+	if err != nil {
+		t.Fatalf("BatchDelete: %v", err)
+	}
+	if count != len(keys) {
+		t.Fatalf("BatchDelete: expected %d deletions got %d", len(keys), count)
+	}
+
+	if err := m.Lookup(uint32(0), &v); !errors.Is(err, ErrKeyNotExist) {
+		t.Fatalf("Lookup should have failed with error, %v, instead error is %v", ErrKeyNotExist, err)
+	}
+}
+
 func TestMapClose(t *testing.T) {
 	m := createArray(t)
 
diff --git a/syscalls_test.go b/syscalls_test.go
index 499d215..4424719 100644
--- a/syscalls_test.go
+++ b/syscalls_test.go
@@ -33,6 +33,10 @@ func TestObjName(t *testing.T) {
 	}
 }
 
+func TestHaveBatchAPI(t *testing.T) {
+	testutils.CheckFeatureTest(t, haveBatchAPI)
+}
+
 func TestHaveObjName(t *testing.T) {
 	testutils.CheckFeatureTest(t, haveObjName)
 }