On Mon, Oct 7, 2024 at 3:16 PM David Wei <dw@xxxxxxxxxxx> wrote: > > From: Pavel Begunkov <asml.silence@xxxxxxxxx> > > Implement a page pool memory provider for io_uring to receieve in a > zero copy fashion. For that, the provider allocates user pages wrapped > around into struct net_iovs, that are stored in a previously registered > struct net_iov_area. > > Unlike with traditional receives, for which pages from a page pool can > be deallocated right after the user receives data, e.g. via recv(2), > we extend the lifetime by recycling buffers only after the user space > acknowledges that it's done processing the data via the refill queue. > Before handing buffers to the user, we mark them by bumping the refcount > by a bias value IO_ZC_RX_UREF, which will be checked when the buffer is > returned back. When the corresponding io_uring instance and/or page pool > are destroyed, we'll force back all buffers that are currently in the > user space in ->io_pp_zc_scrub by clearing the bias. > This is an interesting design choice. In my experience the page_pool works the opposite way, i.e. all the netmems in it are kept alive until the user is done with them. Deviating from that requires custom behavior (->scrub), which may be fine, but why do this? Isn't it better for uapi perspective to keep the memory alive until the user is done with it? > Refcounting and lifetime: > > Initially, all buffers are considered unallocated and stored in > ->freelist, at which point they are not yet directly exposed to the core > page pool code and not accounted to page pool's pages_state_hold_cnt. > The ->alloc_netmems callback will allocate them by placing into the > page pool's cache, setting the refcount to 1 as usual and adjusting > pages_state_hold_cnt. > > Then, either the buffer is dropped and returns back to the page pool > into the ->freelist via io_pp_zc_release_netmem, in which case the page > pool will match hold_cnt for us with ->pages_state_release_cnt. Or more > likely the buffer will go through the network/protocol stacks and end up > in the corresponding socket's receive queue. From there the user can get > it via an new io_uring request implemented in following patches. As > mentioned above, before giving a buffer to the user we bump the refcount > by IO_ZC_RX_UREF. > > Once the user is done with the buffer processing, it must return it back > via the refill queue, from where our ->alloc_netmems implementation can > grab it, check references, put IO_ZC_RX_UREF, and recycle the buffer if > there are no more users left. As we place such buffers right back into > the page pools fast cache and they didn't go through the normal pp > release path, they are still considered "allocated" and no pp hold_cnt > is required. Why is this needed? In general the provider is to allocate free memory and logic as to where the memory should go (to fast cache, to normal pp release path, etc) should remain in provider agnostic code paths in the page_pool. Not maintainable IMO in the long run to have individual pp providers customizing non-provider specific code or touching pp private structs. > For the same reason we dma sync buffers for the device > in io_zc_add_pp_cache(). > > Signed-off-by: Pavel Begunkov <asml.silence@xxxxxxxxx> > Signed-off-by: David Wei <dw@xxxxxxxxxxx> > --- > include/linux/io_uring/net.h | 5 + > io_uring/zcrx.c | 229 +++++++++++++++++++++++++++++++++++ > io_uring/zcrx.h | 6 + > 3 files changed, 240 insertions(+) > > diff --git a/include/linux/io_uring/net.h b/include/linux/io_uring/net.h > index b58f39fed4d5..610b35b451fd 100644 > --- a/include/linux/io_uring/net.h > +++ b/include/linux/io_uring/net.h > @@ -5,6 +5,11 @@ > struct io_uring_cmd; > > #if defined(CONFIG_IO_URING) > + > +#if defined(CONFIG_PAGE_POOL) > +extern const struct memory_provider_ops io_uring_pp_zc_ops; > +#endif > + > int io_uring_cmd_sock(struct io_uring_cmd *cmd, unsigned int issue_flags); > > #else > diff --git a/io_uring/zcrx.c b/io_uring/zcrx.c > index 8382129402ac..6cd3dee8b90a 100644 > --- a/io_uring/zcrx.c > +++ b/io_uring/zcrx.c > @@ -2,7 +2,11 @@ > #include <linux/kernel.h> > #include <linux/errno.h> > #include <linux/mm.h> > +#include <linux/nospec.h> > +#include <linux/netdevice.h> > #include <linux/io_uring.h> > +#include <net/page_pool/helpers.h> > +#include <trace/events/page_pool.h> > > #include <uapi/linux/io_uring.h> > > @@ -16,6 +20,13 @@ > > #if defined(CONFIG_PAGE_POOL) && defined(CONFIG_INET) > > +static inline struct io_zcrx_area *io_zcrx_iov_to_area(const struct net_iov *niov) > +{ > + struct net_iov_area *owner = net_iov_owner(niov); > + > + return container_of(owner, struct io_zcrx_area, nia); Similar to other comment in the other patch, why are we sure this doesn't return garbage (i.e. it's accidentally called on a dmabuf net_iov?) > +} > + > static int io_allocate_rbuf_ring(struct io_zcrx_ifq *ifq, > struct io_uring_zcrx_ifq_reg *reg) > { > @@ -101,6 +112,9 @@ static int io_zcrx_create_area(struct io_ring_ctx *ctx, > goto err; > > for (i = 0; i < nr_pages; i++) { > + struct net_iov *niov = &area->nia.niovs[i]; > + > + niov->owner = &area->nia; > area->freelist[i] = i; > } > > @@ -233,4 +247,219 @@ void io_shutdown_zcrx_ifqs(struct io_ring_ctx *ctx) > lockdep_assert_held(&ctx->uring_lock); > } > > +static bool io_zcrx_niov_put(struct net_iov *niov, int nr) > +{ > + return atomic_long_sub_and_test(nr, &niov->pp_ref_count); > +} > + > +static bool io_zcrx_put_niov_uref(struct net_iov *niov) > +{ > + if (atomic_long_read(&niov->pp_ref_count) < IO_ZC_RX_UREF) > + return false; > + > + return io_zcrx_niov_put(niov, IO_ZC_RX_UREF); > +} > + > +static inline void io_zc_add_pp_cache(struct page_pool *pp, > + struct net_iov *niov) > +{ > + netmem_ref netmem = net_iov_to_netmem(niov); > + > +#if defined(CONFIG_HAS_DMA) && defined(CONFIG_DMA_NEED_SYNC) > + if (pp->dma_sync && dma_dev_need_sync(pp->p.dev)) { IIRC we force that dma_sync == true for memory providers, unless you changed that and I missed it. > + dma_addr_t dma_addr = page_pool_get_dma_addr_netmem(netmem); > + > + dma_sync_single_range_for_device(pp->p.dev, dma_addr, > + pp->p.offset, pp->p.max_len, > + pp->p.dma_dir); > + } > +#endif > + > + page_pool_fragment_netmem(netmem, 1); > + pp->alloc.cache[pp->alloc.count++] = netmem; IMO touching pp internals in a provider should not be acceptable. pp->alloc.cache is a data structure private to the page_pool and should not be touched at all by any specific memory provider. Not maintainable in the long run tbh for individual pp providers to mess with pp private structs and we hunt for bugs that are reproducible with 1 pp provider or another, or have to deal with the mental strain of provider specific handling in what is supposed to be generic page_pool paths. IMO the provider must implement the 4 'ops' (alloc, free, init, destroy) and must not touch pp privates while doing so. If you need to change how pp recycling works then it needs to be done in a provider agnostic way. I think both the dmabuf provider and Jakub's huge page provider both implemented the ops while never touching pp internals. I wonder if we can follow this lead. -- Thanks, Mina