hazptr: Implement Hazard Pointers

This API provides existence guarantees of objects through Hazard
Pointers [1] (hazptr).

Its main benefit over RCU is that it allows fast reclaim of
HP-protected pointers without needing to wait for a grace period.

This implementation has 4 statically allocated hazard pointer slots per
cpu for the fast path, and relies on a on-stack backup slot allocated by
the hazard pointer user as fallback in case no per-cpu slot is
available.

It integrates with the scheduler to migrate per-CPU slots to the backup
slot on context switch. This ensures that the per-CPU slots won't be
used by blocked or preempted tasks holding on hazard pointers for a long
time.

References:

[1]: M. M. Michael, "Hazard pointers: safe memory reclamation for
     lock-free objects," in IEEE Transactions on Parallel and
     Distributed Systems, vol. 15, no. 6, pp. 491-504, June 2004

Link: https://lpc.events/event/19/contributions/2082/
Link: https://lore.kernel.org/lkml/j3scdl5iymjlxavomgc6u5ndg3svhab6ga23dr36o4f5mt333w@7xslvq6b6hmv/
Link: https://lpc.events/event/18/contributions/1731/
Signed-off-by: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: Nicholas Piggin <npiggin@gmail.com>
Cc: Michael Ellerman <mpe@ellerman.id.au>
Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Sebastian Andrzej Siewior <bigeasy@linutronix.de>
Cc: "Paul E. McKenney" <paulmck@kernel.org>
Cc: Will Deacon <will@kernel.org>
Cc: Peter Zijlstra <peterz@infradead.org>
Cc: Boqun Feng <boqun@kernel.org>
Cc: Alan Stern <stern@rowland.harvard.edu>
Cc: John Stultz <jstultz@google.com>
Cc: Linus Torvalds <torvalds@linux-foundation.org>
Cc: Andrew Morton <akpm@linux-foundation.org>
Cc: Frederic Weisbecker <frederic@kernel.org>
Cc: Joel Fernandes <joel@joelfernandes.org>
Cc: Josh Triplett <josh@joshtriplett.org>
Cc: Uladzislau Rezki <urezki@gmail.com>
Cc: Steven Rostedt <rostedt@goodmis.org>
Cc: Lai Jiangshan <jiangshanlai@gmail.com>
Cc: Zqiang <qiang.zhang1211@gmail.com>
Cc: Ingo Molnar <mingo@redhat.com>
Cc: Waiman Long <longman@redhat.com>
Cc: Mark Rutland <mark.rutland@arm.com>
Cc: Thomas Gleixner <tglx@linutronix.de>
Cc: Vlastimil Babka <vbabka@suse.cz>
Cc: maged.michael@gmail.com
Cc: Mateusz Guzik <mjguzik@gmail.com>
Cc: Jonas Oberhauser <jonas.oberhauser@huaweicloud.com>
Cc: <rcu@vger.kernel.org>
Cc: <linux-mm@kvack.org>
Cc: <lkmm@lists.linux.dev>
Signed-off-by: Paul E. McKenney <paulmck@kernel.org>
This commit is contained in:
Mathieu Desnoyers
2026-07-23 09:58:54 -07:00
committed by Paul E. McKenney
parent a13c140cc2
commit 9d30872a4c
5 changed files with 444 additions and 1 deletions
+197
View File
@@ -0,0 +1,197 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
//
// SPDX-FileCopyrightText: 2024 Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
#ifndef _LINUX_HAZPTR_H
#define _LINUX_HAZPTR_H
/*
* hazptr: Hazard Pointers
*
* This API provides existence guarantees of objects through hazard
* pointers.
*
* Its main benefit over RCU is that it allows fast reclaim of
* HP-protected pointers without needing to wait for a grace period.
*
* References:
*
* [1]: M. M. Michael, "Hazard pointers: safe memory reclamation for
* lock-free objects," in IEEE Transactions on Parallel and
* Distributed Systems, vol. 15, no. 6, pp. 491-504, June 2004
*/
#include <linux/percpu.h>
#include <linux/types.h>
#include <linux/cleanup.h>
#include <linux/sched.h>
/* 4 slots (each sizeof(hazptr_slot_item)) fit in a single 64-byte cache line. */
#define NR_HAZPTR_PERCPU_SLOTS 4
#define HAZPTR_WILDCARD ((void *) 0x1UL)
/*
* Hazard pointer slot.
*/
struct hazptr_slot {
void *addr;
};
struct hazptr_overflow_list;
struct hazptr_backup_slot {
struct hlist_node overflow_node;
struct hazptr_slot slot;
/* Overflow list where the backup slot is added. */
struct hazptr_overflow_list *overflow_list;
};
struct hazptr_ctx {
struct hazptr_slot *slot;
/* Backup slot in case all per-CPU slots are used. */
struct hazptr_backup_slot backup_slot;
struct hlist_node preempt_node;
};
struct hazptr_slot_ctx {
struct hazptr_ctx *ctx;
};
struct hazptr_slot_item {
struct hazptr_slot slot;
struct hazptr_slot_ctx ctx;
};
struct hazptr_percpu_slots {
struct hazptr_slot_item items[NR_HAZPTR_PERCPU_SLOTS];
} ____cacheline_aligned;
DECLARE_PER_CPU(struct hazptr_percpu_slots, hazptr_percpu_slots);
void *__hazptr_acquire(struct hazptr_ctx *ctx, void * const *addr_p);
/*
* hazptr_synchronize: Wait until @addr is released from all slots.
*
* Wait to observe that each slot contains a value that differs from
* @addr before returning.
* Should be called from preemptible context.
*/
void hazptr_synchronize(void *addr);
/*
* hazptr_chain_backup_slot: Chain backup slot into overflow list.
*
* Set backup slot address to @addr, and chain it into the overflow
* list.
*/
struct hazptr_slot *hazptr_chain_backup_slot(struct hazptr_ctx *ctx);
/*
* hazptr_unchain_backup_slot: Unchain backup slot from overflow list.
*/
void hazptr_unchain_backup_slot(struct hazptr_ctx *ctx);
static inline
bool hazptr_slot_is_backup(struct hazptr_ctx *ctx, struct hazptr_slot *slot)
{
return slot == &ctx->backup_slot.slot;
}
static inline
void hazptr_note_context_switch(void)
{
struct hazptr_percpu_slots *percpu_slots = this_cpu_ptr(&hazptr_percpu_slots);
unsigned int idx;
for (idx = 0; idx < NR_HAZPTR_PERCPU_SLOTS; idx++) {
struct hazptr_slot_item *item = &percpu_slots->items[idx];
struct hazptr_slot *slot = &item->slot, *backup_slot;
struct hazptr_ctx *ctx;
if (!slot->addr)
continue;
ctx = item->ctx.ctx;
backup_slot = hazptr_chain_backup_slot(ctx);
/*
* Move hazard pointer from the per-CPU slot to the
* backup slot. This requires hazard pointer
* synchronize to iterate on per-CPU slots with
* load-acquire before iterating on the overflow list.
*/
WRITE_ONCE(backup_slot->addr, slot->addr);
/*
* store-release orders store to backup slot addr before
* store to per-CPU slot addr.
*/
smp_store_release(&slot->addr, NULL);
/* Use the backup slot for context. */
ctx->slot = backup_slot;
}
}
/*
* hazptr_acquire: Load pointer at address and protect with hazard pointer.
*
* Load @addr_p, and protect the loaded pointer with hazard pointer.
* When using hazptr_acquire from interrupt handlers, the acquired slots
* need to be released before returning from the interrupt handler.
*
* Returns a non-NULL protected address if the loaded pointer is non-NULL.
* Returns NULL if the loaded pointer is NULL.
*
* On success the protected hazptr slot is stored in @ctx->slot.
*/
static inline
void *hazptr_acquire(struct hazptr_ctx *ctx, void * const *addr_p)
{
struct hazptr_percpu_slots *percpu_slots;
struct hazptr_slot_item *slot_item;
struct hazptr_slot *slot;
void *addr;
guard(preempt)();
percpu_slots = this_cpu_ptr(&hazptr_percpu_slots);
slot_item = &percpu_slots->items[0];
slot = &slot_item->slot;
if (unlikely(slot->addr))
return __hazptr_acquire(ctx, addr_p);
WRITE_ONCE(slot->addr, HAZPTR_WILDCARD); /* Store B */
/* Memory ordering: Store B before Load A. */
smp_mb();
/*
* Load @addr_p after storing wildcard to the hazard pointer slot.
*/
addr = READ_ONCE(*addr_p); /* Load A */
/*
* We don't care about ordering of Store C. It will simply
* replace the wildcard by a more specific address. If addr is
* NULL, we simply store NULL into the slot.
*/
WRITE_ONCE(slot->addr, addr); /* Store C */
slot_item->ctx.ctx = ctx;
ctx->slot = slot;
return addr;
}
/* Release the protected hazard pointer from @slot. */
static inline
void hazptr_release(struct hazptr_ctx *ctx, void *addr)
{
struct hazptr_slot *slot;
if (!addr)
return;
guard(preempt)();
slot = ctx->slot;
smp_store_release(&slot->addr, NULL);
if (unlikely(hazptr_slot_is_backup(ctx, slot)))
hazptr_unchain_backup_slot(ctx);
}
void hazptr_init(void);
#endif /* _LINUX_HAZPTR_H */
+2
View File
@@ -107,6 +107,7 @@
#include <linux/time_namespace.h>
#include <linux/unaligned.h>
#include <linux/vdso_datastore.h>
#include <linux/hazptr.h>
#include <net/net_namespace.h>
#include <asm/io.h>
@@ -1065,6 +1066,7 @@ void start_kernel(void)
workqueue_init_early();
rcu_init();
hazptr_init();
kvfree_rcu_init();
/* Trace events are available after this */
+1 -1
View File
@@ -7,7 +7,7 @@ obj-y = fork.o exec_domain.o exec_state.o panic.o \
cpu.o exit.o softirq.o resource.o \
sysctl.o capability.o ptrace.o user.o \
signal.o sys.o umh.o workqueue.o pid.o task_work.o \
extable.o params.o \
extable.o params.o hazptr.o \
kthread.o sys_ni.o nsproxy.o nstree.o nscommon.o \
notifier.o ksysfs.o cred.o reboot.o \
async.o range.o smpboot.o ucount.o regset.o ksyms_common.o
+242
View File
@@ -0,0 +1,242 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
//
// SPDX-FileCopyrightText: 2024 Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
/*
* hazptr: Hazard Pointers
*/
#include <linux/hazptr.h>
#include <linux/percpu.h>
#include <linux/spinlock.h>
#include <linux/mutex.h>
#include <linux/list.h>
#include <linux/export.h>
struct hazptr_overflow_list {
raw_spinlock_t lock; /* Lock protecting overflow list and list generation. */
struct hlist_head head; /* Overflow list head. */
uint64_t gen; /* Overflow list generation. */
};
/*
* Flip between two lists to guarantee list scan forward progress even
* with frequent generation counter increments. The list additions are
* always done on a different list than the one used for scan. The scan
* successively iterates on both lists. Therefore, only list removals
* can cause the iteration to retry, and the number of removals is
* limited to the number of list elements.
*/
struct hazptr_overflow_list_flip {
struct mutex lock; /* Mutex protecting add_idx from concurrent updates. */
unsigned int add_idx; /* Index of current flip-list to add to. */
struct hazptr_overflow_list array[2];
};
static DEFINE_PER_CPU(struct hazptr_overflow_list_flip, percpu_overflow_list_flip);
DEFINE_PER_CPU(struct hazptr_percpu_slots, hazptr_percpu_slots);
EXPORT_PER_CPU_SYMBOL_GPL(hazptr_percpu_slots);
static
struct hazptr_slot *hazptr_get_free_percpu_slot(struct hazptr_ctx *ctx)
{
struct hazptr_percpu_slots *percpu_slots = this_cpu_ptr(&hazptr_percpu_slots);
unsigned int idx;
for (idx = 0; idx < NR_HAZPTR_PERCPU_SLOTS; idx++) {
struct hazptr_slot_item *item = &percpu_slots->items[idx];
struct hazptr_slot *slot = &item->slot;
if (!slot->addr) {
item->ctx.ctx = ctx;
return slot;
}
}
/* All slots are in use. */
return NULL;
}
/*
* Hazard pointer acquire slow path.
* Called with preemption disabled.
*/
void *__hazptr_acquire(struct hazptr_ctx *ctx, void * const *addr_p)
{
struct hazptr_slot *slot = hazptr_get_free_percpu_slot(ctx);
void *addr;
/*
* If all the per-CPU slots are already in use, fallback
* to the backup slot.
*/
if (unlikely(!slot))
slot = hazptr_chain_backup_slot(ctx);
WRITE_ONCE(slot->addr, HAZPTR_WILDCARD); /* Store B */
/* Memory ordering: Store B before Load A. */
smp_mb();
/*
* Load @addr_p after storing wildcard to the hazard pointer slot.
*/
addr = READ_ONCE(*addr_p); /* Load A */
/*
* We don't care about ordering of Store C. It will simply
* replace the wildcard by a more specific address. If addr is
* NULL, we simply store NULL into the slot.
*/
WRITE_ONCE(slot->addr, addr); /* Store C */
ctx->slot = slot;
if (!addr && hazptr_slot_is_backup(ctx, slot))
hazptr_unchain_backup_slot(ctx);
return addr;
}
EXPORT_SYMBOL_GPL(__hazptr_acquire);
/*
* Perform piecewise iteration on overflow list waiting until "addr" is
* not present. Raw spinlock is released and taken between each list
* item and busy loop iteration. The overflow list generation is checked
* each time the lock is taken to validate that the list has not changed
* before resuming iteration or busy wait. If the generation has
* changed, retry the entire list traversal.
*/
static
void hazptr_synchronize_overflow_list(struct hazptr_overflow_list *overflow_list, void *addr)
{
struct hazptr_backup_slot *backup_slot;
uint64_t snapshot_gen;
unsigned long flags;
raw_spin_lock_irqsave(&overflow_list->lock, flags);
retry:
snapshot_gen = overflow_list->gen;
hlist_for_each_entry(backup_slot, &overflow_list->head, overflow_node) {
/* Busy-wait if node is found. */
for (;;) {
void *load_addr = smp_load_acquire(&backup_slot->slot.addr); /* Load B */
if (load_addr != addr && load_addr != HAZPTR_WILDCARD)
break;
raw_spin_unlock_irqrestore(&overflow_list->lock, flags);
cpu_relax();
raw_spin_lock_irqsave(&overflow_list->lock, flags);
if (overflow_list->gen != snapshot_gen)
goto retry;
}
raw_spin_unlock_irqrestore(&overflow_list->lock, flags);
/*
* Release raw spinlock, validate generation after
* re-acquiring the lock.
*/
raw_spin_lock_irqsave(&overflow_list->lock, flags);
if (overflow_list->gen != snapshot_gen)
goto retry;
}
raw_spin_unlock_irqrestore(&overflow_list->lock, flags);
}
static
void hazptr_synchronize_cpu_slots(int cpu, void *addr)
{
struct hazptr_percpu_slots *percpu_slots = per_cpu_ptr(&hazptr_percpu_slots, cpu);
unsigned int idx;
for (idx = 0; idx < NR_HAZPTR_PERCPU_SLOTS; idx++) {
struct hazptr_slot_item *item = &percpu_slots->items[idx];
/* Busy-wait if node is found. */
smp_cond_load_acquire(&item->slot.addr, VAL != addr && VAL != HAZPTR_WILDCARD); /* Load B */
}
}
/*
* hazptr_synchronize: Wait until @addr is released from all slots.
*
* Wait to observe that each slot contains a value that differs from
* @addr before returning.
* Should be called from preemptible context.
*/
void hazptr_synchronize(void *addr)
{
int cpu;
/*
* Busy-wait should only be done from preemptible context.
*/
lockdep_assert_preemption_enabled();
/*
* Store A precedes hazptr_scan(): it unpublishes addr (sets it to
* NULL or to a different value), and thus hides it from hazard
* pointer readers.
*/
if (!addr)
return;
/* Memory ordering: Store A before Load B. */
smp_mb();
/* Scan all CPUs slots. */
for_each_possible_cpu(cpu) {
struct hazptr_overflow_list_flip *overflow_list_flip = per_cpu_ptr(&percpu_overflow_list_flip, cpu);
unsigned int scan_idx;
/* Scan CPU slots. */
hazptr_synchronize_cpu_slots(cpu, addr);
/*
* Scan backup slots in percpu overflow lists.
* Forward progress is guaranteed by scanning one list
* while new elements are added into the other list.
*/
guard(mutex)(&overflow_list_flip->lock);
scan_idx = overflow_list_flip->add_idx ^ 1;
hazptr_synchronize_overflow_list(&overflow_list_flip->array[scan_idx], addr);
/* Flip current list. */
WRITE_ONCE(overflow_list_flip->add_idx, scan_idx);
hazptr_synchronize_overflow_list(&overflow_list_flip->array[scan_idx ^ 1], addr);
}
}
EXPORT_SYMBOL_GPL(hazptr_synchronize);
struct hazptr_slot *hazptr_chain_backup_slot(struct hazptr_ctx *ctx)
{
struct hazptr_overflow_list_flip *overflow_list_flip = this_cpu_ptr(&percpu_overflow_list_flip);
unsigned int list_idx = READ_ONCE(overflow_list_flip->add_idx);
struct hazptr_overflow_list *overflow_list = &overflow_list_flip->array[list_idx];
struct hazptr_slot *slot = &ctx->backup_slot.slot;
slot->addr = NULL;
guard(raw_spinlock_irqsave)(&overflow_list->lock);
overflow_list->gen++;
hlist_add_head(&ctx->backup_slot.overflow_node, &overflow_list->head);
ctx->backup_slot.overflow_list = overflow_list;
return slot;
}
EXPORT_SYMBOL_GPL(hazptr_chain_backup_slot);
void hazptr_unchain_backup_slot(struct hazptr_ctx *ctx)
{
struct hazptr_overflow_list *overflow_list = ctx->backup_slot.overflow_list;
guard(raw_spinlock_irqsave)(&overflow_list->lock);
overflow_list->gen++;
hlist_del(&ctx->backup_slot.overflow_node);
}
EXPORT_SYMBOL_GPL(hazptr_unchain_backup_slot);
void __init hazptr_init(void)
{
int cpu;
for_each_possible_cpu(cpu) {
struct hazptr_overflow_list_flip *overflow_list_flip = per_cpu_ptr(&percpu_overflow_list_flip, cpu);
mutex_init(&overflow_list_flip->lock);
for (int i = 0; i < 2; i++) {
raw_spin_lock_init(&overflow_list_flip->array[i].lock);
INIT_HLIST_HEAD(&overflow_list_flip->array[i].head);
}
}
}
+2
View File
@@ -60,6 +60,7 @@
#include <linux/profile.h>
#include <linux/psi.h>
#include <linux/rcuwait_api.h>
#include <linux/hazptr.h>
#include <linux/rseq.h>
#include <linux/sched/wake_q.h>
#include <linux/scs.h>
@@ -7087,6 +7088,7 @@ static void __sched notrace __schedule(int sched_mode)
local_irq_disable();
rcu_note_context_switch(preempt);
migrate_disable_switch(rq, prev);
hazptr_note_context_switch();
/*
* Make sure that signal_pending_state()->signal_pending() below