numam-dpdk/lib/eal/include/rte_seqlock.h
Mattias Rönnblom 0bee070907 eal: add seqlock
A sequence lock (seqlock) is a synchronization primitive which allows
for data-race free, low-overhead, high-frequency reads, suitable for
data structures shared across many cores and which are updated
relatively infrequently.

A seqlock permits multiple parallel readers. A spinlock is used to
serialize writers. In cases where there is only a single writer, or
writer-writer synchronization is done by some external means, the
"raw" sequence counter type (and accompanying rte_seqcount_*()
functions) may be used instead.

To avoid resource reclamation and other issues, the data protected by
a seqlock is best off being self-contained (i.e., no pointers [except
to constant data]).

One way to think about seqlocks is that they provide means to perform
atomic operations on data objects larger than what the native atomic
machine instructions allow for.

DPDK seqlocks (and the underlying sequence counters) are not
preemption safe on the writer side. A thread preemption affects
performance, not correctness.

A seqlock contains a sequence number, which can be thought of as the
generation of the data it protects.

A reader will
  1. Load the sequence number (sn).
  2. Load, in arbitrary order, the seqlock-protected data.
  3. Load the sn again.
  4. Check if the first and second sn are equal, and even numbered.
     If they are not, discard the loaded data, and restart from 1.

The first three steps need to be ordered using suitable memory fences.

A writer will
  1. Take the spinlock, to serialize writer access.
  2. Load the sn.
  3. Store the original sn + 1 as the new sn.
  4. Perform load and stores to the seqlock-protected data.
  5. Store the original sn + 2 as the new sn.
  6. Release the spinlock.

Proper memory fencing is required to make sure the first sn store, the
data stores, and the second sn store appear to the reader in the
mentioned order.

The sn loads and stores must be atomic, but the data loads and stores
need not be.

The original seqlock design and implementation was done by Stephen
Hemminger. This is an independent implementation, using C11 atomics.

For more information on seqlocks, see
https://en.wikipedia.org/wiki/Seqlock

Acked-by: Morten Brørup <mb@smartsharesystems.com>
Acked-by: Konstantin Ananyev <konstantin.ananyev@intel.com>
Reviewed-by: Ola Liljedahl <ola.liljedahl@arm.com>
Reviewed-by: Chengwen Feng <fengchengwen@huawei.com>
Signed-off-by: Mattias Rönnblom <mattias.ronnblom@ericsson.com>
2022-06-07 13:33:14 +02:00

254 lines
6.8 KiB
C

/* SPDX-License-Identifier: BSD-3-Clause
* Copyright(c) 2022 Ericsson AB
*/
#ifndef _RTE_SEQLOCK_H_
#define _RTE_SEQLOCK_H_
#ifdef __cplusplus
extern "C" {
#endif
/**
* @file
* RTE Seqlock
*
* A sequence lock (seqlock) is a synchronization primitive allowing
* multiple, parallel, readers to efficiently and safely (i.e., in a
* data-race free manner) access lock-protected data. The RTE seqlock
* permits multiple writers as well. A spinlock is used for
* writer-writer synchronization.
*
* A reader never blocks a writer. Very high frequency writes may
* prevent readers from making progress.
*
* A seqlock is not preemption-safe on the writer side. If a writer is
* preempted, it may block readers until the writer thread is allowed
* to continue. Heavy computations should be kept out of the
* writer-side critical section, to avoid delaying readers.
*
* Seqlocks are useful for data which are read by many cores, at a
* high frequency, and relatively infrequently written to.
*
* One way to think about seqlocks is that they provide means to
* perform atomic operations on objects larger than what the native
* machine instructions allow for.
*
* To avoid resource reclamation issues, the data protected by a
* seqlock should typically be kept self-contained (e.g., no pointers
* to mutable, dynamically allocated data).
*
* Example usage:
* @code{.c}
* #define MAX_Y_LEN 16
* // Application-defined example data structure, protected by a seqlock.
* struct config {
* rte_seqlock_t lock;
* int param_x;
* char param_y[MAX_Y_LEN];
* };
*
* // Accessor function for reading config fields.
* void
* config_read(const struct config *config, int *param_x, char *param_y)
* {
* uint32_t sn;
*
* do {
* sn = rte_seqlock_read_begin(&config->lock);
*
* // Loads may be atomic or non-atomic, as in this example.
* *param_x = config->param_x;
* strcpy(param_y, config->param_y);
* // An alternative to an immediate retry is to abort and
* // try again at some later time, assuming progress is
* // possible without the data.
* } while (rte_seqlock_read_retry(&config->lock, sn));
* }
*
* // Accessor function for writing config fields.
* void
* config_update(struct config *config, int param_x, const char *param_y)
* {
* rte_seqlock_write_lock(&config->lock);
* // Stores may be atomic or non-atomic, as in this example.
* config->param_x = param_x;
* strcpy(config->param_y, param_y);
* rte_seqlock_write_unlock(&config->lock);
* }
* @endcode
*
* In case there is only a single writer, or writer-writer
* serialization is provided by other means, the use of sequence lock
* (i.e., rte_seqlock_t) can be replaced with the use of the "raw"
* rte_seqcount_t type instead.
*
* @see
* https://en.wikipedia.org/wiki/Seqlock.
*/
#include <stdbool.h>
#include <stdint.h>
#include <rte_atomic.h>
#include <rte_branch_prediction.h>
#include <rte_compat.h>
#include <rte_seqcount.h>
#include <rte_spinlock.h>
/**
* The RTE seqlock type.
*/
typedef struct {
rte_seqcount_t count; /**< Sequence count for the protected data. */
rte_spinlock_t lock; /**< Spinlock used to serialize writers. */
} rte_seqlock_t;
/**
* A static seqlock initializer.
*/
#define RTE_SEQLOCK_INITIALIZER \
{ \
.count = RTE_SEQCOUNT_INITIALIZER, \
.lock = RTE_SPINLOCK_INITIALIZER \
}
/**
* @warning
* @b EXPERIMENTAL: this API may change without prior notice.
*
* Initialize the seqlock.
*
* This function initializes the seqlock, and leaves the writer-side
* spinlock unlocked.
*
* @param seqlock
* A pointer to the seqlock.
*/
__rte_experimental
static inline void
rte_seqlock_init(rte_seqlock_t *seqlock)
{
rte_seqcount_init(&seqlock->count);
rte_spinlock_init(&seqlock->lock);
}
/**
* @warning
* @b EXPERIMENTAL: this API may change without prior notice.
*
* Begin a read-side critical section.
*
* See rte_seqcount_read_retry() for details.
*
* @param seqlock
* A pointer to the seqlock.
* @return
* The seqlock sequence number for this critical section, to
* later be passed to rte_seqlock_read_retry().
*
* @see rte_seqlock_read_retry()
* @see rte_seqcount_read_retry()
*/
__rte_experimental
static inline uint32_t
rte_seqlock_read_begin(const rte_seqlock_t *seqlock)
{
return rte_seqcount_read_begin(&seqlock->count);
}
/**
* @warning
* @b EXPERIMENTAL: this API may change without prior notice.
*
* End a read-side critical section.
*
* See rte_seqcount_read_retry() for details.
*
* @param seqlock
* A pointer to the seqlock.
* @param begin_sn
* The seqlock sequence number returned by rte_seqlock_read_begin().
* @return
* true or false, if the just-read seqlock-protected data was
* inconsistent or consistent, respectively, at the time it was
* read.
*
* @see rte_seqlock_read_begin()
*/
__rte_experimental
static inline bool
rte_seqlock_read_retry(const rte_seqlock_t *seqlock, uint32_t begin_sn)
{
return rte_seqcount_read_retry(&seqlock->count, begin_sn);
}
/**
* @warning
* @b EXPERIMENTAL: this API may change without prior notice.
*
* Begin a write-side critical section.
*
* A call to this function acquires the write lock associated @p
* seqlock, and marks the beginning of a write-side critical section.
*
* After having called this function, the caller may go on to modify
* (both read and write) the protected data, in an atomic or
* non-atomic manner.
*
* After the necessary updates have been performed, the application
* calls rte_seqlock_write_unlock().
*
* This function is not preemption-safe in the sense that preemption
* of the calling thread may block reader progress until the writer
* thread is rescheduled.
*
* Unlike rte_seqlock_read_begin(), each call made to
* rte_seqlock_write_lock() must be matched with an unlock call.
*
* @param seqlock
* A pointer to the seqlock.
*
* @see rte_seqlock_write_unlock()
*/
__rte_experimental
static inline void
rte_seqlock_write_lock(rte_seqlock_t *seqlock)
{
/* To synchronize with other writers. */
rte_spinlock_lock(&seqlock->lock);
rte_seqcount_write_begin(&seqlock->count);
}
/**
* @warning
* @b EXPERIMENTAL: this API may change without prior notice.
*
* End a write-side critical section.
*
* A call to this function marks the end of the write-side critical
* section, for @p seqlock. After this call has been made, the protected
* data may no longer be modified.
*
* @param seqlock
* A pointer to the seqlock.
*
* @see rte_seqlock_write_lock()
*/
__rte_experimental
static inline void
rte_seqlock_write_unlock(rte_seqlock_t *seqlock)
{
rte_seqcount_write_end(&seqlock->count);
rte_spinlock_unlock(&seqlock->lock);
}
#ifdef __cplusplus
}
#endif
#endif /* _RTE_SEQLOCK_H_ */