mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
synced 2026-08-09 06:14:34 +02:00
A driver that has popped a handle from an FRMR pool can hit failures
that leave the handle in a state where it can't safely be returned
for reuse. The driver destroys the handle itself, but the pool has
no way to learn about it, so the in_use counter drifts upward.
Add ib_frmr_pool_drop to balance the pool's accounting in this case.
Every pop is now balanced by exactly one push or drop.
Fixes: 36680ef7bc ("RDMA/mlx5: Switch from MR cache to FRMR pools")
Link: https://patch.msgid.link/r/20260610000145.820592-9-michaelgur@nvidia.com
Signed-off-by: Michael Guralnik <michaelgur@nvidia.com>
Signed-off-by: Jason Gunthorpe <jgg@nvidia.com>
596 lines
14 KiB
C
596 lines
14 KiB
C
// SPDX-License-Identifier: GPL-2.0 OR Linux-OpenIB
|
|
/*
|
|
* Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
*/
|
|
|
|
#include <linux/slab.h>
|
|
#include <linux/rbtree.h>
|
|
#include <linux/sort.h>
|
|
#include <linux/spinlock.h>
|
|
#include <rdma/ib_verbs.h>
|
|
#include <linux/timer.h>
|
|
|
|
#include "frmr_pools.h"
|
|
|
|
#define FRMR_POOLS_DEFAULT_AGING_PERIOD_SECS 60
|
|
|
|
static int push_handle_to_queue_locked(struct frmr_queue *queue, u32 handle)
|
|
{
|
|
u32 tmp = queue->ci % NUM_HANDLES_PER_PAGE;
|
|
struct frmr_handles_page *page;
|
|
|
|
if (queue->ci >= queue->num_pages * NUM_HANDLES_PER_PAGE) {
|
|
page = kzalloc_obj(*page, GFP_ATOMIC);
|
|
if (!page)
|
|
return -ENOMEM;
|
|
queue->num_pages++;
|
|
list_add_tail(&page->list, &queue->pages_list);
|
|
} else {
|
|
page = list_last_entry(&queue->pages_list,
|
|
struct frmr_handles_page, list);
|
|
}
|
|
|
|
page->handles[tmp] = handle;
|
|
queue->ci++;
|
|
return 0;
|
|
}
|
|
|
|
static u32 pop_handle_from_queue_locked(struct frmr_queue *queue)
|
|
{
|
|
u32 tmp = (queue->ci - 1) % NUM_HANDLES_PER_PAGE;
|
|
struct frmr_handles_page *page;
|
|
u32 handle;
|
|
|
|
page = list_last_entry(&queue->pages_list, struct frmr_handles_page,
|
|
list);
|
|
handle = page->handles[tmp];
|
|
queue->ci--;
|
|
|
|
if (!tmp) {
|
|
list_del(&page->list);
|
|
queue->num_pages--;
|
|
kfree(page);
|
|
}
|
|
|
|
return handle;
|
|
}
|
|
|
|
static bool pop_frmr_handles_page(struct ib_frmr_pool *pool,
|
|
struct frmr_queue *queue,
|
|
struct frmr_handles_page **page, u32 *count)
|
|
{
|
|
spin_lock(&pool->lock);
|
|
if (list_empty(&queue->pages_list)) {
|
|
spin_unlock(&pool->lock);
|
|
return false;
|
|
}
|
|
|
|
*page = list_first_entry(&queue->pages_list, struct frmr_handles_page,
|
|
list);
|
|
list_del(&(*page)->list);
|
|
queue->num_pages--;
|
|
|
|
/* If this is the last page, count may be less than
|
|
* NUM_HANDLES_PER_PAGE.
|
|
*/
|
|
if (queue->ci >= NUM_HANDLES_PER_PAGE)
|
|
*count = NUM_HANDLES_PER_PAGE;
|
|
else
|
|
*count = queue->ci;
|
|
|
|
queue->ci -= *count;
|
|
spin_unlock(&pool->lock);
|
|
return true;
|
|
}
|
|
|
|
static void destroy_all_handles_in_queue(struct ib_device *device,
|
|
struct ib_frmr_pool *pool,
|
|
struct frmr_queue *queue)
|
|
{
|
|
struct ib_frmr_pools *pools = device->frmr_pools;
|
|
struct frmr_handles_page *page;
|
|
u32 count;
|
|
|
|
while (pop_frmr_handles_page(pool, queue, &page, &count)) {
|
|
pools->pool_ops->destroy_frmrs(device, page->handles, count);
|
|
kfree(page);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Bulk-move all handles from @src into @dst without allocating new pages.
|
|
* If @dst has a partial tail page, fill it handle-by-handle from @src first
|
|
* to preserve the invariant that only the tail page is partial, then splice
|
|
* the remaining @src pages onto @dst. On return @src is empty.
|
|
*
|
|
* Caller must hold the lock protecting both queues.
|
|
*/
|
|
static void splice_frmr_queue_locked(struct frmr_queue *dst,
|
|
struct frmr_queue *src)
|
|
{
|
|
u32 free_in_tail = dst->ci % NUM_HANDLES_PER_PAGE;
|
|
u32 handle;
|
|
|
|
if (free_in_tail) {
|
|
free_in_tail = NUM_HANDLES_PER_PAGE - free_in_tail;
|
|
while (free_in_tail && src->ci) {
|
|
handle = pop_handle_from_queue_locked(src);
|
|
push_handle_to_queue_locked(dst, handle);
|
|
free_in_tail--;
|
|
}
|
|
}
|
|
|
|
if (src->ci > 0) {
|
|
list_splice_tail_init(&src->pages_list, &dst->pages_list);
|
|
dst->num_pages += src->num_pages;
|
|
dst->ci += src->ci;
|
|
src->num_pages = 0;
|
|
src->ci = 0;
|
|
}
|
|
}
|
|
|
|
static bool age_pinned_pool(struct ib_device *device, struct ib_frmr_pool *pool)
|
|
{
|
|
struct ib_frmr_pools *pools = device->frmr_pools;
|
|
u32 total, to_destroy, destroyed = 0;
|
|
bool has_work = false;
|
|
u32 *handles;
|
|
|
|
spin_lock(&pool->lock);
|
|
total = pool->queue.ci + pool->inactive_queue.ci + pool->in_use;
|
|
if (total <= pool->pinned_handles) {
|
|
spin_unlock(&pool->lock);
|
|
return false;
|
|
}
|
|
|
|
to_destroy = min(total - pool->pinned_handles, pool->inactive_queue.ci);
|
|
|
|
handles = kcalloc(to_destroy, sizeof(*handles), GFP_ATOMIC);
|
|
if (!handles) {
|
|
spin_unlock(&pool->lock);
|
|
return true;
|
|
}
|
|
|
|
/* Destroy all excess handles in the inactive queue */
|
|
for (; destroyed < to_destroy; destroyed++)
|
|
handles[destroyed] = pop_handle_from_queue_locked(
|
|
&pool->inactive_queue);
|
|
|
|
/* Move all handles from regular queue to inactive queue */
|
|
if (pool->queue.ci > 0) {
|
|
splice_frmr_queue_locked(&pool->inactive_queue, &pool->queue);
|
|
has_work = true;
|
|
}
|
|
|
|
spin_unlock(&pool->lock);
|
|
|
|
if (destroyed)
|
|
pools->pool_ops->destroy_frmrs(device, handles, destroyed);
|
|
kfree(handles);
|
|
return has_work;
|
|
}
|
|
|
|
static void pool_aging_work(struct work_struct *work)
|
|
{
|
|
struct ib_frmr_pool *pool = container_of(
|
|
to_delayed_work(work), struct ib_frmr_pool, aging_work);
|
|
struct ib_frmr_pools *pools = pool->device->frmr_pools;
|
|
bool has_work = false;
|
|
|
|
if (pool->pinned_handles) {
|
|
has_work = age_pinned_pool(pool->device, pool);
|
|
goto out;
|
|
}
|
|
|
|
destroy_all_handles_in_queue(pool->device, pool, &pool->inactive_queue);
|
|
|
|
/* Move all pages from regular queue to inactive queue */
|
|
spin_lock(&pool->lock);
|
|
if (pool->queue.ci > 0) {
|
|
splice_frmr_queue_locked(&pool->inactive_queue, &pool->queue);
|
|
has_work = true;
|
|
}
|
|
spin_unlock(&pool->lock);
|
|
|
|
out:
|
|
/* Reschedule if there are handles to age in next aging period */
|
|
if (has_work)
|
|
queue_delayed_work(
|
|
pools->aging_wq, &pool->aging_work,
|
|
secs_to_jiffies(READ_ONCE(pools->aging_period_sec)));
|
|
}
|
|
|
|
static void destroy_frmr_pool(struct ib_device *device,
|
|
struct ib_frmr_pool *pool)
|
|
{
|
|
cancel_delayed_work_sync(&pool->aging_work);
|
|
destroy_all_handles_in_queue(device, pool, &pool->queue);
|
|
destroy_all_handles_in_queue(device, pool, &pool->inactive_queue);
|
|
|
|
kfree(pool);
|
|
}
|
|
|
|
/*
|
|
* Initialize the FRMR pools for a device.
|
|
*
|
|
* @device: The device to initialize the FRMR pools for.
|
|
* @pool_ops: The pool operations to use.
|
|
*
|
|
* Returns 0 on success, negative error code on failure.
|
|
*/
|
|
int ib_frmr_pools_init(struct ib_device *device,
|
|
const struct ib_frmr_pool_ops *pool_ops)
|
|
{
|
|
struct ib_frmr_pools *pools;
|
|
|
|
pools = kzalloc_obj(*pools);
|
|
if (!pools)
|
|
return -ENOMEM;
|
|
|
|
pools->rb_root = RB_ROOT;
|
|
rwlock_init(&pools->rb_lock);
|
|
pools->pool_ops = pool_ops;
|
|
pools->aging_wq = create_singlethread_workqueue("frmr_aging_wq");
|
|
if (!pools->aging_wq) {
|
|
kfree(pools);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
pools->aging_period_sec = FRMR_POOLS_DEFAULT_AGING_PERIOD_SECS;
|
|
|
|
device->frmr_pools = pools;
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL(ib_frmr_pools_init);
|
|
|
|
/*
|
|
* Clean up the FRMR pools for a device.
|
|
*
|
|
* @device: The device to clean up the FRMR pools for.
|
|
*
|
|
* Call cleanup only after all FRMR handles have been pushed back to the pool
|
|
* and no other FRMR operations are allowed to run in parallel.
|
|
* Ensuring this allows us to save synchronization overhead in pop and push
|
|
* operations.
|
|
*/
|
|
void ib_frmr_pools_cleanup(struct ib_device *device)
|
|
{
|
|
struct ib_frmr_pools *pools = device->frmr_pools;
|
|
struct ib_frmr_pool *pool, *next;
|
|
|
|
if (!pools)
|
|
return;
|
|
|
|
rbtree_postorder_for_each_entry_safe(pool, next, &pools->rb_root, node)
|
|
destroy_frmr_pool(device, pool);
|
|
|
|
destroy_workqueue(pools->aging_wq);
|
|
kfree(pools);
|
|
device->frmr_pools = NULL;
|
|
}
|
|
EXPORT_SYMBOL(ib_frmr_pools_cleanup);
|
|
|
|
int ib_frmr_pools_set_aging_period(struct ib_device *device, u32 period_sec)
|
|
{
|
|
struct ib_frmr_pools *pools = device->frmr_pools;
|
|
struct ib_frmr_pool *pool;
|
|
struct rb_node *node;
|
|
|
|
if (!pools)
|
|
return -EINVAL;
|
|
|
|
if (period_sec == 0)
|
|
return -EINVAL;
|
|
|
|
WRITE_ONCE(pools->aging_period_sec, period_sec);
|
|
|
|
read_lock(&pools->rb_lock);
|
|
for (node = rb_first(&pools->rb_root); node; node = rb_next(node)) {
|
|
pool = rb_entry(node, struct ib_frmr_pool, node);
|
|
mod_delayed_work(pools->aging_wq, &pool->aging_work,
|
|
secs_to_jiffies(period_sec));
|
|
}
|
|
read_unlock(&pools->rb_lock);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static inline int compare_keys(struct ib_frmr_key *key1,
|
|
struct ib_frmr_key *key2)
|
|
{
|
|
int res;
|
|
|
|
res = cmp_int(key1->ats, key2->ats);
|
|
if (res)
|
|
return res;
|
|
|
|
res = cmp_int(key1->access_flags, key2->access_flags);
|
|
if (res)
|
|
return res;
|
|
|
|
res = cmp_int(key1->vendor_key, key2->vendor_key);
|
|
if (res)
|
|
return res;
|
|
|
|
res = cmp_int(key1->kernel_vendor_key, key2->kernel_vendor_key);
|
|
if (res)
|
|
return res;
|
|
|
|
/*
|
|
* allow using handles that support more DMA blocks, up to twice the
|
|
* requested number
|
|
*/
|
|
res = cmp_int(key1->num_dma_blocks, key2->num_dma_blocks);
|
|
if (res > 0) {
|
|
if (key1->num_dma_blocks - key2->num_dma_blocks <
|
|
key2->num_dma_blocks)
|
|
return 0;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
static int frmr_pool_cmp_find(const void *key, const struct rb_node *node)
|
|
{
|
|
struct ib_frmr_pool *pool = rb_entry(node, struct ib_frmr_pool, node);
|
|
|
|
return compare_keys(&pool->key, (struct ib_frmr_key *)key);
|
|
}
|
|
|
|
static int frmr_pool_cmp_add(struct rb_node *new, const struct rb_node *node)
|
|
{
|
|
struct ib_frmr_pool *new_pool =
|
|
rb_entry(new, struct ib_frmr_pool, node);
|
|
struct ib_frmr_pool *pool = rb_entry(node, struct ib_frmr_pool, node);
|
|
|
|
return compare_keys(&pool->key, &new_pool->key);
|
|
}
|
|
|
|
static struct ib_frmr_pool *ib_frmr_pool_find(struct ib_frmr_pools *pools,
|
|
struct ib_frmr_key *key)
|
|
{
|
|
struct ib_frmr_pool *pool;
|
|
struct rb_node *node;
|
|
|
|
/* find operation is done under read lock for performance reasons.
|
|
* The case of threads failing to find the same pool and creating it
|
|
* is handled by the create_frmr_pool function.
|
|
*/
|
|
read_lock(&pools->rb_lock);
|
|
node = rb_find(key, &pools->rb_root, frmr_pool_cmp_find);
|
|
pool = rb_entry_safe(node, struct ib_frmr_pool, node);
|
|
read_unlock(&pools->rb_lock);
|
|
|
|
return pool;
|
|
}
|
|
|
|
static struct ib_frmr_pool *create_frmr_pool(struct ib_device *device,
|
|
struct ib_frmr_key *key)
|
|
{
|
|
struct ib_frmr_pools *pools = device->frmr_pools;
|
|
struct ib_frmr_pool *pool;
|
|
struct rb_node *existing;
|
|
|
|
pool = kzalloc_obj(*pool);
|
|
if (!pool)
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
memcpy(&pool->key, key, sizeof(*key));
|
|
INIT_LIST_HEAD(&pool->queue.pages_list);
|
|
INIT_LIST_HEAD(&pool->inactive_queue.pages_list);
|
|
spin_lock_init(&pool->lock);
|
|
INIT_DELAYED_WORK(&pool->aging_work, pool_aging_work);
|
|
pool->device = device;
|
|
|
|
write_lock(&pools->rb_lock);
|
|
existing = rb_find_add(&pool->node, &pools->rb_root, frmr_pool_cmp_add);
|
|
write_unlock(&pools->rb_lock);
|
|
|
|
/* If a different thread has already created the pool, return it.
|
|
* The insert operation is done under the write lock so we are sure
|
|
* that the pool is not inserted twice.
|
|
*/
|
|
if (existing) {
|
|
kfree(pool);
|
|
return rb_entry(existing, struct ib_frmr_pool, node);
|
|
}
|
|
|
|
return pool;
|
|
}
|
|
|
|
int ib_frmr_pools_set_pinned(struct ib_device *device, struct ib_frmr_key *key,
|
|
u32 pinned_handles)
|
|
{
|
|
struct ib_frmr_pools *pools = device->frmr_pools;
|
|
struct ib_frmr_key driver_key = {};
|
|
struct ib_frmr_pool *pool;
|
|
u32 needed_handles;
|
|
u32 current_total;
|
|
int i, ret = 0;
|
|
u32 *handles;
|
|
|
|
if (!pools)
|
|
return -EINVAL;
|
|
|
|
ret = ib_check_mr_access(device, key->access_flags);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (pools->pool_ops->build_key) {
|
|
ret = pools->pool_ops->build_key(device, key, &driver_key);
|
|
if (ret)
|
|
return ret;
|
|
} else {
|
|
memcpy(&driver_key, key, sizeof(*key));
|
|
}
|
|
|
|
pool = ib_frmr_pool_find(pools, &driver_key);
|
|
if (!pool) {
|
|
pool = create_frmr_pool(device, &driver_key);
|
|
if (IS_ERR(pool))
|
|
return PTR_ERR(pool);
|
|
}
|
|
|
|
spin_lock(&pool->lock);
|
|
current_total = pool->in_use + pool->queue.ci + pool->inactive_queue.ci;
|
|
|
|
if (current_total < pinned_handles)
|
|
needed_handles = pinned_handles - current_total;
|
|
else
|
|
needed_handles = 0;
|
|
|
|
pool->pinned_handles = pinned_handles;
|
|
spin_unlock(&pool->lock);
|
|
|
|
if (!needed_handles)
|
|
goto schedule_aging;
|
|
|
|
handles = kcalloc(needed_handles, sizeof(*handles), GFP_KERNEL);
|
|
if (!handles)
|
|
return -ENOMEM;
|
|
|
|
ret = pools->pool_ops->create_frmrs(device, &driver_key, handles,
|
|
needed_handles);
|
|
if (ret) {
|
|
kfree(handles);
|
|
return ret;
|
|
}
|
|
|
|
spin_lock(&pool->lock);
|
|
for (i = 0; i < needed_handles; i++) {
|
|
ret = push_handle_to_queue_locked(&pool->queue,
|
|
handles[i]);
|
|
if (ret)
|
|
break;
|
|
}
|
|
spin_unlock(&pool->lock);
|
|
|
|
if (ret) {
|
|
/* Destroy handles created but never pushed to the pool. */
|
|
pools->pool_ops->destroy_frmrs(device, &handles[i],
|
|
needed_handles - i);
|
|
}
|
|
|
|
kfree(handles);
|
|
|
|
schedule_aging:
|
|
/* Ensure aging is scheduled to adjust to new pinned handles count */
|
|
mod_delayed_work(pools->aging_wq, &pool->aging_work, 0);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int get_frmr_from_pool(struct ib_device *device,
|
|
struct ib_frmr_pool *pool, struct ib_mr *mr)
|
|
{
|
|
struct ib_frmr_pools *pools = device->frmr_pools;
|
|
u32 handle;
|
|
int err;
|
|
|
|
spin_lock(&pool->lock);
|
|
if (pool->queue.ci == 0) {
|
|
if (pool->inactive_queue.ci > 0) {
|
|
handle = pop_handle_from_queue_locked(
|
|
&pool->inactive_queue);
|
|
} else {
|
|
spin_unlock(&pool->lock);
|
|
err = pools->pool_ops->create_frmrs(device, &pool->key,
|
|
&handle, 1);
|
|
if (err)
|
|
return err;
|
|
spin_lock(&pool->lock);
|
|
}
|
|
} else {
|
|
handle = pop_handle_from_queue_locked(&pool->queue);
|
|
}
|
|
|
|
pool->in_use++;
|
|
if (pool->in_use > pool->max_in_use)
|
|
pool->max_in_use = pool->in_use;
|
|
|
|
spin_unlock(&pool->lock);
|
|
|
|
mr->frmr.pool = pool;
|
|
mr->frmr.handle = handle;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Pop an FRMR handle from the pool.
|
|
*
|
|
* @device: The device to pop the FRMR handle from.
|
|
* @mr: The MR to pop the FRMR handle from.
|
|
*
|
|
* Returns 0 on success, negative error code on failure.
|
|
*/
|
|
int ib_frmr_pool_pop(struct ib_device *device, struct ib_mr *mr)
|
|
{
|
|
struct ib_frmr_pools *pools = device->frmr_pools;
|
|
struct ib_frmr_pool *pool;
|
|
|
|
if (WARN_ON_ONCE(!pools))
|
|
return -EINVAL;
|
|
|
|
pool = ib_frmr_pool_find(pools, &mr->frmr.key);
|
|
if (!pool) {
|
|
pool = create_frmr_pool(device, &mr->frmr.key);
|
|
if (IS_ERR(pool))
|
|
return PTR_ERR(pool);
|
|
}
|
|
|
|
return get_frmr_from_pool(device, pool, mr);
|
|
}
|
|
EXPORT_SYMBOL(ib_frmr_pool_pop);
|
|
|
|
/*
|
|
* Push an FRMR handle back to the pool.
|
|
*
|
|
* @device: The device to push the FRMR handle to.
|
|
* @mr: The MR containing the FRMR handle to push back to the pool.
|
|
*
|
|
*/
|
|
void ib_frmr_pool_push(struct ib_device *device, struct ib_mr *mr)
|
|
{
|
|
struct ib_frmr_pool *pool = mr->frmr.pool;
|
|
struct ib_frmr_pools *pools = device->frmr_pools;
|
|
bool schedule_aging = false;
|
|
int ret;
|
|
|
|
spin_lock(&pool->lock);
|
|
pool->in_use--;
|
|
ret = push_handle_to_queue_locked(&pool->queue, mr->frmr.handle);
|
|
|
|
/* Schedule aging every time an empty pool becomes non-empty */
|
|
if (!ret && pool->queue.ci == 1)
|
|
schedule_aging = true;
|
|
|
|
spin_unlock(&pool->lock);
|
|
|
|
if (ret) {
|
|
pools->pool_ops->destroy_frmrs(device, &mr->frmr.handle, 1);
|
|
return;
|
|
}
|
|
|
|
if (schedule_aging)
|
|
queue_delayed_work(pools->aging_wq, &pool->aging_work,
|
|
secs_to_jiffies(READ_ONCE(pools->aging_period_sec)));
|
|
|
|
}
|
|
EXPORT_SYMBOL(ib_frmr_pool_push);
|
|
|
|
/*
|
|
* Drop a handle previously popped from the pool without returning it for
|
|
* reuse. The caller is responsible for destroying the underlying hardware
|
|
* resource.
|
|
*/
|
|
void ib_frmr_pool_drop(struct ib_mr *mr)
|
|
{
|
|
struct ib_frmr_pool *pool = mr->frmr.pool;
|
|
|
|
spin_lock(&pool->lock);
|
|
pool->in_use--;
|
|
spin_unlock(&pool->lock);
|
|
}
|
|
EXPORT_SYMBOL(ib_frmr_pool_drop);
|