[PATCH v10 00/30] Introduce discontiguous frames management
This series introduces iov_tail to convey frame information between functions. v10: - rename iov_tail_drop() to iov_drop_header() - rename pool_full() to pool_can_fit() - replace NOLINTNEXTLINE(clang-analyzer-core.NonNullParamChecker) by ASSERT(iov[i].iov_base); - update iov_tail_clone() comment header v9: - address comments from David v8: - rebase - rework the two last patches to store the iovec in the p->pkt array v7: - Add a patch to fix comment style of 'Return:' - Fix ignore_arp()/accept_arp() - Fix coverity error - Fix several comments v6: - Replaced iov_slice() with the clearer iov_tail_clone() for creating iovec subsets. - Standardized local header variable names (to *_storage suffix). - Renamed functions for better semantics (e.g., ignore_arp to accept_arp, packet_data to packet_get). - Corrected OPTLEN_MAX definition in TCP. - Addressed minor logic issues (e.g., DHCPv6 FQDN flags, NDP null check). - Updated ipv6_l4hdr() return type to boolean. - Improved comments and documentation across several modules. v5: - store in the pool iovec array with several entries v4: Prepare to introduce iovec array in the pool: - passe iov_tail rather than pool to ndp,icmp, dhcp, dhcpv6 and arp - remove unused pool macros - add memory regions in the pool structure, this will allow us to use the buf pointer to store the iovec array for vhost-user v3: Address comments from David Laurent Vivier (30): arp: Don't mix incoming and outgoing buffers iov: Introduce iov_tail_clone() and iov_drop_header(). iov: Update IOV_REMOVE_HEADER() and IOV_PEEK_HEADER() tap: Use iov_tail with tap_add_packet() packet: Use iov_tail with packet_add() packet: Add packet_data() arp: Convert to iov_tail ndp: Convert to iov_tail icmp: Convert to iov_tail udp: Convert to iov_tail tcp: Convert tcp_tap_handler() to use iov_tail tcp: Convert tcp_data_from_tap() to use iov_tail dhcpv6: move offset initialization out of dhcpv6_opt() dhcpv6: Extract sending of NotOnLink status dhcpv6: Convert to iov_tail dhcpv6: Use iov_tail in dhcpv6_opt() dhcp: Convert to iov_tail ip: Use iov_tail in ipv6_l4hdr() tap: Convert tap4_handler() to iov_tail tap: Convert tap6_handler() to iov_tail packet: rename packet_data() to packet_get() arp: use iov_tail rather than pool dhcp: use iov_tail rather than pool dhcpv6: use iov_tail rather than pool icmp: use iov_tail rather than pool ndp: use iov_tail rather than pool packet: remove PACKET_POOL() and PACKET_POOL_P() packet: remove unused parameter from PACKET_POOL_DECL() packet: Refactor vhost-user memory region handling packet: Add support for multi-vector packets arp.c | 86 +++++++++++++------- arp.h | 2 +- dhcp.c | 48 ++++++----- dhcp.h | 2 +- dhcpv6.c | 223 +++++++++++++++++++++++++++++++-------------------- dhcpv6.h | 2 +- icmp.c | 40 +++++---- icmp.h | 2 +- iov.c | 101 ++++++++++++++++++++--- iov.h | 58 ++++++++++---- ip.c | 33 ++++---- ip.h | 3 +- ndp.c | 16 +++- ndp.h | 4 +- packet.c | 144 ++++++++++++++++++--------------- packet.h | 45 ++++------- pcap.c | 1 + tap.c | 119 +++++++++++++++------------ tap.h | 4 +- tcp.c | 61 +++++++++----- tcp_buf.c | 2 +- udp.c | 33 +++++--- vhost_user.c | 28 +++---- virtio.c | 4 +- virtio.h | 18 ++++- vu_common.c | 48 ++++------- 26 files changed, 691 insertions(+), 436 deletions(-) -- 2.49.0
Don't use the memory of the incoming packet to build the outgoing buffer
as it can be memory of the TX queue in the case of vhost-user.
Moreover with vhost-user, the packet can be split across several
iovec and it's easier to rebuild it in a buffer than updating an
existing iovec array.
Signed-off-by: Laurent Vivier
These utilities enhance iov_tail manipulation, useful for
efficient packet processing by enabling iovec array cloning and
header stripping without data copies.
- iov_drop_header(): Discards a specified number of bytes from the
beginning of an iov_tail by advancing its internal offset and pruning
consumed elements.
- iov_tail_clone(): Clone an iov_tail into an iovec array, adjusting the
first iovec entry to remove the iov_tail offset.
Signed-off-by: Laurent Vivier
Provide a temporary variable of the wanted type to store
the header if the memory in the iovec array is not contiguous.
Signed-off-by: Laurent Vivier
On Thu, Aug 14, 2025 at 11:48:22AM +0200, Laurent Vivier wrote:
Provide a temporary variable of the wanted type to store the header if the memory in the iovec array is not contiguous.
Signed-off-by: Laurent Vivier
Reviewed-by: David Gibson
--- iov.c | 53 ++++++++++++++++++++++++++++++++++++++++++++--------- iov.h | 35 +++++++++++++++++++++-------------- tcp_buf.c | 2 +- 3 files changed, 66 insertions(+), 24 deletions(-)
diff --git a/iov.c b/iov.c index 9a91eb63026a..d20fbb854933 100644 --- a/iov.c +++ b/iov.c @@ -109,7 +109,7 @@ size_t iov_from_buf(const struct iovec *iov, size_t iov_cnt, * * Return: the number of bytes successfully copied. */ -/* cppcheck-suppress unusedFunction */ +/* cppcheck-suppress [staticFunction] */ size_t iov_to_buf(const struct iovec *iov, size_t iov_cnt, size_t offset, void *buf, size_t bytes) { @@ -127,6 +127,7 @@ size_t iov_to_buf(const struct iovec *iov, size_t iov_cnt, /* copying data */ for (copied = 0; copied < bytes && i < iov_cnt; i++) { size_t len = MIN(iov[i].iov_len - offset, bytes - copied); + ASSERT(iov[i].iov_base); memcpy((char *)buf + copied, (char *)iov[i].iov_base + offset, len); copied += len; @@ -208,7 +209,7 @@ bool iov_drop_header(struct iov_tail *tail, size_t len) }
/** - * iov_peek_header_() - Get pointer to a header from an IOV tail + * iov_check_header() - Check if a header can be accessed * @tail: IOV tail to get header from * @len: Length of header to get, in bytes * @align: Required alignment of header, in bytes @@ -219,8 +220,7 @@ bool iov_drop_header(struct iov_tail *tail, size_t len) * overruns the IO vector, is not contiguous or doesn't have the * requested alignment. */ -/* cppcheck-suppress [staticFunction,unmatchedSuppression] */ -void *iov_peek_header_(struct iov_tail *tail, size_t len, size_t align) +static void *iov_check_header(struct iov_tail *tail, size_t len, size_t align) { char *p;
@@ -240,27 +240,62 @@ void *iov_peek_header_(struct iov_tail *tail, size_t len, size_t align) return p; }
+/** + * iov_peek_header_() - Get pointer to a header from an IOV tail + * @tail: IOV tail to get header from + * @v: Temporary memory to use if the memory in @tail + * is discontinuous + * @len: Length of header to get, in bytes + * @align: Required alignment of header, in bytes + * + * @tail may be pruned, but will represent the same bytes as before. + * + * Return: pointer to the first @len logical bytes of the tail, or to + * a copy if that overruns the IO vector, is not contiguous or + * doesn't have the requested alignment. NULL if that overruns the + * IO vector. + */ +/* cppcheck-suppress [staticFunction,unmatchedSuppression] */ +void *iov_peek_header_(struct iov_tail *tail, void *v, size_t len, size_t align) +{ + char *p = iov_check_header(tail, len, align); + size_t l; + + if (p) + return p; + + l = iov_to_buf(tail->iov, tail->cnt, tail->off, v, len); + if (l != len) + return NULL; + + return v; +} + /** * iov_remove_header_() - Remove a header from an IOV tail * @tail: IOV tail to remove header from (modified) + * @v: Temporary memory to use if the memory in @tail + * is discontinuous * @len: Length of header to remove, in bytes * @align: Required alignment of header, in bytes * * On success, @tail is updated so that it longer includes the bytes of the * returned header. * - * Return: pointer to the first @len logical bytes of the tail, NULL if that - * overruns the IO vector, is not contiguous or doesn't have the - * requested alignment. + * Return: pointer to the first @len logical bytes of the tail, or to + * a copy if that overruns the IO vector, is not contiguous or + * doesn't have the requested alignment. NULL if that overruns the + * IO vector. */ -void *iov_remove_header_(struct iov_tail *tail, size_t len, size_t align) +void *iov_remove_header_(struct iov_tail *tail, void *v, size_t len, size_t align) { - char *p = iov_peek_header_(tail, len, align); + char *p = iov_peek_header_(tail, v, len, align);
if (!p) return NULL;
tail->off = tail->off + len; + return p; }
diff --git a/iov.h b/iov.h index c94fabe1eb43..1821b40b1cb5 100644 --- a/iov.h +++ b/iov.h @@ -73,38 +73,45 @@ struct iov_tail { bool iov_tail_prune(struct iov_tail *tail); size_t iov_tail_size(struct iov_tail *tail); bool iov_drop_header(struct iov_tail *tail, size_t len); -void *iov_peek_header_(struct iov_tail *tail, size_t len, size_t align); -void *iov_remove_header_(struct iov_tail *tail, size_t len, size_t align); +void *iov_peek_header_(struct iov_tail *tail, void *v, size_t len, size_t align); +void *iov_remove_header_(struct iov_tail *tail, void *v, size_t len, size_t align); ssize_t iov_tail_clone(struct iovec *dst_iov, size_t dst_iov_cnt, struct iov_tail *tail);
/** * IOV_PEEK_HEADER() - Get typed pointer to a header from an IOV tail * @tail_: IOV tail to get header from - * @type_: Data type of the header + * @var_: Temporary buffer of the type of the header to use if + * the memory in the iovec array is not contiguous. * * @tail_ may be pruned, but will represent the same bytes as before. * - * Return: pointer of type (@type_ *) located at the start of @tail_, NULL if - * we can't get a contiguous and aligned pointer. + * Return: pointer of type (@type_ *) located at the start of @tail_ + * or to @var_ if iovec memory is not contiguous, NULL if + * that overruns the iovec. */ -#define IOV_PEEK_HEADER(tail_, type_) \ - ((type_ *)(iov_peek_header_((tail_), \ - sizeof(type_), __alignof__(type_)))) + +#define IOV_PEEK_HEADER(tail_, var_) \ + ((__typeof__(var_) *)(iov_peek_header_((tail_), &(var_), \ + sizeof(var_), \ + __alignof__(var_))))
/** * IOV_REMOVE_HEADER() - Remove and return typed header from an IOV tail * @tail_: IOV tail to remove header from (modified) - * @type_: Data type of the header to remove + * @var_: Temporary buffer of the type of the header to use if + * the memory in the iovec array is not contiguous. * * On success, @tail_ is updated so that it longer includes the bytes of the * returned header. * - * Return: pointer of type (@type_ *) located at the old start of @tail_, NULL - * if we can't get a contiguous and aligned pointer. + * Return: pointer of type (@type_ *) located at the start of @tail_ + * or to @var_ if iovec memory is not contiguous, NULL if + * that overruns the iovec. */ -#define IOV_REMOVE_HEADER(tail_, type_) \ - ((type_ *)(iov_remove_header_((tail_), \ - sizeof(type_), __alignof__(type_)))) + +#define IOV_REMOVE_HEADER(tail_, var_) \ + ((__typeof__(var_) *)(iov_remove_header_((tail_), &(var_), \ + sizeof(var_), __alignof__(var_))))
#endif /* IOVEC_H */ diff --git a/tcp_buf.c b/tcp_buf.c index d1fca676c9a7..bc898de86919 100644 --- a/tcp_buf.c +++ b/tcp_buf.c @@ -160,7 +160,7 @@ static void tcp_l2_buf_fill_headers(const struct tcp_tap_conn *conn, uint32_t seq, bool no_tcp_csum) { struct iov_tail tail = IOV_TAIL(&iov[TCP_IOV_PAYLOAD], 1, 0); - struct tcphdr *th = IOV_REMOVE_HEADER(&tail, struct tcphdr); + struct tcphdr th_storage, *th = IOV_REMOVE_HEADER(&tail, th_storage); struct tap_hdr *taph = iov[TCP_IOV_TAP].iov_base; const struct flowside *tapside = TAPFLOW(conn); const struct in_addr *a4 = inany_v4(&tapside->oaddr);
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
Use IOV_PEEK_HEADER() to get the ethernet header from the iovec.
Move the workaround about multiple iovec array from vu_handle_tx() to
tap_add_packet(). Removing the offset out of the iovec array should
reduce the iovec count to 1.
Signed-off-by: Laurent Vivier
Modify the interface of packet_add_do() to take an iov_tail
rather than a memory pointer and length.
Internally it only supports iovec array with only one entry,
after being pruned. We can accept iovec array with several
entries if the offset allows the function to reduce the number
of entries to 1.
tap4_handler() is updated to create an iov_tail value using
IOV_TAIL_FROM_BUF() from the buffer and the length.
Signed-off-by: Laurent Vivier
packet_data() gets the data range from a packet descriptor from a
given pool.
It uses iov_tail to return the packet memory.
packet_data() will be renamed to replace packet_get() later.
Signed-off-by: Laurent Vivier
Use packet_data() and extract headers using IOV_REMOVE_HEADER()
rather than packet_get().
Signed-off-by: Laurent Vivier
Use packet_data() and extract headers using IOV_REMOVE_HEADER()
rather than packet_get().
Signed-off-by: Laurent Vivier
Use packet_data() and extract headers using IOV_PEEK_HEADER()
rather than packet_get().
Signed-off-by: Laurent Vivier
Use packet_data() and extract headers using IOV_REMOVE_HEADER()
and IOV_PEEK_HEADER() rather than packet_get().
Signed-off-by: Laurent Vivier
Use packet_data() and extract headers using IOV_REMOVE_HEADER()
and iov_remove_header_() rather than packet_get().
Signed-off-by: Laurent Vivier
Use packet_data() and extract headers using IOV_PEEK_HEADER()
rather than packet_get().
Signed-off-by: Laurent Vivier
No functional change.
Currently, if dhcpv6_opt() is called with offset set to 0, it will set the
offset to point to DHCPv6 options offset.
To simplify the use of iovec_tail in a later patch, move the initialization
out of the function. Replace all the call using 0 by a call using
the offset of the DHCPv6 options.
Signed-off-by: Laurent Vivier
Extract code from dhcpv6() into a new function, dhcpv6_send_ia_notonlink()
Signed-off-by: Laurent Vivier
Use packet_data() and extract headers using IOV_REMOVE_HEADER()
and IOV_PEEK_HEADER() rather than packet_get().
Signed-off-by: Laurent Vivier
dhcpv6_opt() and its callers are refactored for iov_tail option parsing,
replacing direct offset management for improved robustness.
Its signature is now `bool dhcpv6_opt(iov_tail *data, type)`. `*data` (in/out)
points to a found option on `true` return or is restored on `false`.
The main dhcpv6() function uses IOV_REMOVE_HEADER for the msg_hdr, then
passes the iov_tail (now at options start) to the new dhcpv6_opt().
Signed-off-by: Laurent Vivier
Use packet_data() and extract headers using IOV_REMOVE_HEADER()
and IOV_PEEK_HEADER() rather than packet_get().
Signed-off-by: Laurent Vivier
Use packet_data() and extract headers using IOV_REMOVE_HEADER()
and IOV_PEEK_HEADER() rather than packet_get().
Signed-off-by: Laurent Vivier
Use packet_data() and extract headers using IOV_PEEK_HEADER()
rather than packet_get().
Signed-off-by: Laurent Vivier
Use packet_data() and extract headers using IOV_REMOVE_HEADER()
and IOV_PEEK_HEADER() rather than packet_get().
Remove packet_get() as it is not used anymore.
Signed-off-by: Laurent Vivier
As we have removed packet_get(), we can rename packet_data() to packet_get()
as the name is clearer.
Signed-off-by: Laurent Vivier
The arp() function signature is changed to accept `struct iov_tail *data`
directly, replacing the previous `const struct pool *p` parameter.
Consequently, arp() no longer fetches packet data internally using
packet_data(), streamlining its logic.
This simplifies callers like tap4_handler(), which now pass the iov_tail
for the L2 ARP frame directly, removing intermediate pool handling.
Signed-off-by: Laurent Vivier
This patch refactors the dhcp() function to accept `struct iov_tail *data`
directly as its packet input, replacing the previous `const struct pool *p`
parameter. Consequently, dhcp() no longer fetches packet data internally
using packet_data().
This change simplifies callers, such as tap4_handler(), which now pass
the iov_tail representing the L2 frame directly to dhcp(). This removes
the need for intermediate packet pool handling for DHCP processing.
Signed-off-by: Laurent Vivier
This patch refactors the dhcpv6() function to accept `struct iov_tail *data`
directly as its packet input, replacing the `const struct pool *p` parameter.
Consequently, dhcpv6() no longer fetches packet data internally using
packet_data().
This change simplifies callers, such as tap6_handler(), which now pass
the iov_tail representing the L4 UDP segment (DHCPv6 message) directly.
This removes the need for intermediate packet pool handling.
Signed-off-by: Laurent Vivier
This patch refactors the icmp_tap_handler() function to accept
`struct iov_tail *data` directly as its packet input, replacing the
`const struct pool *p` parameter.
This change simplifies callers, such as tap4_handler(), which now pass
the iov_tail representing the L4 ICMP message directly.
This removes the need for intermediate packet pool handling.
Signed-off-by: Laurent Vivier
The ndp() function signature is changed to accept `struct iov_tail *data`
directly, replacing the previous `const struct pool *p` and
`const struct icmp6hdr *ih` parameters.
This change simplifies callers, like tap6_handler(), which now provide
the iov_tail representing the L4 ICMPv6 segment directly to ndp().
Signed-off-by: Laurent Vivier
These macros are no longer used following the refactoring of packet
handlers to directly use iov_tail. Callers no longer require PACKET_POOL_P
for temporary pools, and PACKET_POOL can be replaced by PACKET_POOL_DECL
and separate initialization if needed.
Signed-off-by: Laurent Vivier
_buf is not used in the macro. Remove it.
Remove it also from PACKET_POOL_NOINIT() as it was needed
for PACKET_POOL_DECL().
Signed-off-by: Laurent Vivier
On Thu, Aug 14, 2025 at 11:48:47AM +0200, Laurent Vivier wrote:
_buf is not used in the macro. Remove it. Remove it also from PACKET_POOL_NOINIT() as it was needed for PACKET_POOL_DECL().
Signed-off-by: Laurent Vivier
Reviewed-by: David Gibson
--- packet.h | 6 +++--- tap.c | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/packet.h b/packet.h index 286b6b9994db..43b9022075d1 100644 --- a/packet.h +++ b/packet.h @@ -43,7 +43,7 @@ void pool_flush(struct pool *p); #define packet_get(p, idx, data) \ packet_get_do(p, idx, data, __func__, __LINE__)
-#define PACKET_POOL_DECL(_name, _size, _buf) \ +#define PACKET_POOL_DECL(_name, _size) \ struct _name ## _t { \ char *buf; \ size_t buf_size; \ @@ -62,7 +62,7 @@ struct _name ## _t { \ #define PACKET_INIT(name, size, buf, buf_size) \ (struct name ## _t) PACKET_POOL_INIT_NOCAST(size, buf, buf_size)
-#define PACKET_POOL_NOINIT(name, size, buf) \ - PACKET_POOL_DECL(name, size, buf) name ## _storage; \ +#define PACKET_POOL_NOINIT(name, size) \ + PACKET_POOL_DECL(name, size) name ## _storage; \ static struct pool *name = (struct pool *)&name ## _storage #endif /* PACKET_H */ diff --git a/tap.c b/tap.c index 99fe810980fd..4e09beac364b 100644 --- a/tap.c +++ b/tap.c @@ -95,8 +95,8 @@ CHECK_FRAME_LEN(L2_MAX_LEN_VU); ETH_HLEN + sizeof(struct ipv6hdr) + sizeof(struct udphdr))
/* IPv4 (plus ARP) and IPv6 message batches from tap/guest to IP handlers */ -static PACKET_POOL_NOINIT(pool_tap4, TAP_MSGS_IP4, pkt_buf); -static PACKET_POOL_NOINIT(pool_tap6, TAP_MSGS_IP6, pkt_buf); +static PACKET_POOL_NOINIT(pool_tap4, TAP_MSGS_IP4); +static PACKET_POOL_NOINIT(pool_tap6, TAP_MSGS_IP6);
#define TAP_SEQS 128 /* Different L4 tuples in one batch */ #define FRAGMENT_MSG_RATE 10 /* # seconds between fragment warnings */ @@ -555,7 +555,7 @@ void eth_update_mac(struct ethhdr *eh, memcpy(eh->h_source, eth_s, sizeof(eh->h_source)); }
-PACKET_POOL_DECL(pool_l4, UIO_MAXIOV, pkt_buf); +PACKET_POOL_DECL(pool_l4, UIO_MAXIOV);
/** * struct l4_seq4_t - Message sequence for one protocol handler call, IPv4
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
This patch refactors the handling of vhost-user memory regions by
introducing a new `struct vdev_memory` to encapsulate the regions
array and their count (`nregions`) within the main `vu_dev` structure.
This new `vdev_memory` structure is then passed to the packet pool by
re-using the existing `p->buf` field. A `p->buf_size` of 0 indicates
that `p->buf` holds a pointer to `struct vdev_memory` instead of a
regular packet buffer. A new helper, `get_vdev_memory()`, is added to
abstract this access pattern.
Previous implementation was using a marker at the end of the memory
regions array. We can now uses all the slots.
Signed-off-by: Laurent Vivier
The packet pool was previously limited to handling packets contained
within a single buffer.
This patch extends the packet pool to support iovec array,
allowing a single logical packet to be composed of multiple iovec.
To accommodate this, the storage format within the pool is modified.
For a multi-vector packet, a header entry is now stored first with
iov_base = NULL and iov_len holding the number of subsequent
vectors. The actual data vectors are then stored in the following
pool slots.
The packet_add_do() and packet_get_do() functions are updated to
manage this new format for storing and retrieving packets. The
pool_full() check is also adjusted to ensure there is enough
space for all vectors of a new packet before adding it.
Signed-off-by: Laurent Vivier
On Thu, Aug 14, 2025 at 11:48:49AM +0200, Laurent Vivier wrote:
The packet pool was previously limited to handling packets contained within a single buffer.
This patch extends the packet pool to support iovec array, allowing a single logical packet to be composed of multiple iovec.
To accommodate this, the storage format within the pool is modified. For a multi-vector packet, a header entry is now stored first with iov_base = NULL and iov_len holding the number of subsequent vectors. The actual data vectors are then stored in the following pool slots.
The packet_add_do() and packet_get_do() functions are updated to manage this new format for storing and retrieving packets. The pool_full() check is also adjusted to ensure there is enough space for all vectors of a new packet before adding it.
Signed-off-by: Laurent Vivier
--- packet.c | 52 ++++++++++++++++++++++++++++++++++------------------ packet.h | 2 +- tap.c | 4 ++-- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/packet.c b/packet.c index 27693c55a138..c88e726c94a6 100644 --- a/packet.c +++ b/packet.c @@ -91,14 +91,15 @@ static int packet_check_range(const struct pool *p, const char *ptr, size_t len, return 0; } /** - * pool_full() - Is a packet pool full? + * pool_can_fit() - Is a new packet can fit in the pool?
"Can a new packet fit in the pool?"
* @p: Pointer to packet pool + * @data: check data can fit in the pool * - * Return: true if the pool is full, false if more packets can be added + * Return: true if the pool is full, false if data can be added
With the name changed, the sense needs to be inverted as well. Also s/data/@data/ to make it clear we're talking specifically about the parameter, not "data" in the broad sense.
*/ -bool pool_full(const struct pool *p) +bool pool_can_fit(const struct pool *p, const struct iov_tail *data) { - return p->count >= p->size; + return p->count + data->cnt + (data->cnt > 1) >= p->size;
Still an off-by-one here, see comments in the other thread. ..and this is still only valid if @data is already pruned. I really think a bunch of things would become a bit clearer if you add a change earlier in the series clarifying that iov_tails should never be passed around unpruned. -- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
participants (2)
-
David Gibson
-
Laurent Vivier