Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Postgres/src/backend/storage/aio/   (Postgres Database Version 18.4©)  Datei vom 11.4.2026 mit Größe 18 kB image not shown  

Quelle  method_io_uring.c

  Sprache: C
 

/*-------------------------------------------------------------------------
 *
 * method_io_uring.c
 *    AIO - perform AIO using Linux' io_uring
 *
 * For now we create one io_uring instance for each backend. These io_uring
 * instances have to be created in postmaster, during startup, to allow other
 * backends to process IO completions, if the issuing backend is currently
 * busy doing other things. Other backends may not use another backend's
 * io_uring instance to submit IO, that'd require additional locking that
 * would likely be harmful for performance.
 *
 * We likely will want to introduce a backend-local io_uring instance in the
 * future, e.g. for FE/BE network IO.
 *
 * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *    src/backend/storage/aio/method_io_uring.c
 *
 *-------------------------------------------------------------------------
 */


#include "postgres.h"

/* included early, for IOMETHOD_IO_URING_ENABLED */
#include "storage/aio.h"

#ifdef IOMETHOD_IO_URING_ENABLED

#include <sys/mman.h>
#include <unistd.h>

#include <liburing.h>

#include "miscadmin.h"
#include "storage/aio_internal.h"
#include "storage/fd.h"
#include "storage/proc.h"
#include "storage/shmem.h"
#include "storage/lwlock.h"
#include "storage/procnumber.h"
#include "utils/wait_event.h"


/* number of completions processed at once */
#define PGAIO_MAX_LOCAL_COMPLETED_IO 32


/* Entry points for IoMethodOps. */
static size_t pgaio_uring_shmem_size(void);
static void pgaio_uring_shmem_init(bool first_time);
static void pgaio_uring_init_backend(void);
static int pgaio_uring_submit(uint16 num_staged_ios, PgAioHandle **staged_ios);
static void pgaio_uring_wait_one(PgAioHandle *ioh, uint64 ref_generation);

/* helper functions */
static void pgaio_uring_sq_from_io(PgAioHandle *ioh, struct io_uring_sqe *sqe);


const IoMethodOps pgaio_uring_ops = {
 /*
  * While io_uring mostly is OK with FDs getting closed while the IO is in
  * flight, that is not true for IOs submitted with IOSQE_ASYNC.
  *
  * See
  * https://postgr.es/m/5ons2rtmwarqqhhexb3dnqulw5rjgwgoct57vpdau4rujlrffj%403fls6d2mkiwc
 */

 .wait_on_fd_before_close = true,

 .shmem_size = pgaio_uring_shmem_size,
 .shmem_init = pgaio_uring_shmem_init,
 .init_backend = pgaio_uring_init_backend,

 .submit = pgaio_uring_submit,
 .wait_one = pgaio_uring_wait_one,
};

/*
 * Per-backend state when using io_method=io_uring
 *
 * Align the whole struct to a cacheline boundary, to prevent false sharing
 * between completion_lock and prior backend's io_uring_ring.
 */

typedef struct pg_attribute_aligned (PG_CACHE_LINE_SIZE)
PgAioUringContext
{
 /*
  * Multiple backends can process completions for this backend's io_uring
  * instance (e.g. when the backend issuing IO is busy doing something
  * else).  To make that safe we have to ensure that only a single backend
  * gets io completions from the io_uring instance at a time.
 */

 LWLock  completion_lock;

 struct io_uring io_uring_ring;
} PgAioUringContext;

/*
 * Information about the capabilities that io_uring has.
 *
 * Depending on liburing and kernel version different features are
 * supported. At least for the kernel a kernel version check does not suffice
 * as various vendors do backport features to older kernels :(.
 */

typedef struct PgAioUringCaps
{
 bool  checked;
 /* -1 if io_uring_queue_init_mem() is unsupported */
 int   mem_init_size;
} PgAioUringCaps;


/* PgAioUringContexts for all backends */
static PgAioUringContext *pgaio_uring_contexts;

/* the current backend's context */
static PgAioUringContext *pgaio_my_uring_context;

static PgAioUringCaps pgaio_uring_caps =
{
 .checked = false,
 .mem_init_size = -1,
};

static uint32
pgaio_uring_procs(void)
{
 /*
  * We can subtract MAX_IO_WORKERS here as io workers are never used at the
  * same time as io_method=io_uring.
 */

 return MaxBackends + NUM_AUXILIARY_PROCS - MAX_IO_WORKERS;
}

/*
 * Initializes pgaio_uring_caps, unless that's already done.
 */

static void
pgaio_uring_check_capabilities(void)
{
 if (pgaio_uring_caps.checked)
  return;

 /*
  * By default io_uring creates a shared memory mapping for each io_uring
  * instance, leading to a large number of memory mappings. Unfortunately a
  * large number of memory mappings slows things down, backend exit is
  * particularly affected.  To address that, newer kernels (6.5) support
  * using user-provided memory for the memory, by putting the relevant
  * memory into shared memory we don't need any additional mappings.
  *
  * To know whether this is supported, we unfortunately need to probe the
  * kernel by trying to create a ring with userspace-provided memory. This
  * also has a secondary benefit: We can determine precisely how much
  * memory we need for each io_uring instance.
 */

#if defined(HAVE_IO_URING_QUEUE_INIT_MEM) && defined(IORING_SETUP_NO_MMAP)
 {
  struct io_uring test_ring;
  size_t  ring_size;
  void    *ring_ptr;
  struct io_uring_params p = {0};
  int   ret;

  /*
   * Liburing does not yet provide an API to query how much memory a
   * ring will need. So we over-estimate it here. As the memory is freed
   * just below that's small temporary waste of memory.
   *
   * 1MB is more than enough for rings within io_max_concurrency's
   * range.
 */

  ring_size = 1024 * 1024;

  /*
   * Hard to believe a system exists where 1MB would not be a multiple
   * of the page size. But it's cheap to ensure...
 */

  ring_size -= ring_size % sysconf(_SC_PAGESIZE);

  ring_ptr = mmap(NULL, ring_size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -10);
  if (ring_ptr == MAP_FAILED)
   elog(ERROR,
     "mmap(%zu) to determine io_uring_queue_init_mem() support failed: %m",
     ring_size);

  ret = io_uring_queue_init_mem(io_max_concurrency, &test_ring, &p, ring_ptr, ring_size);
  if (ret > 0)
  {
   pgaio_uring_caps.mem_init_size = ret;

   elog(DEBUG1,
     "can use combined memory mapping for io_uring, each ring needs %d bytes",
     ret);

   /* clean up the created ring, it was just for a test */
   io_uring_queue_exit(&test_ring);
  }
  else
  {
   /*
    * There are different reasons for ring creation to fail, but it's
    * ok to treat that just as io_uring_queue_init_mem() not being
    * supported. We'll report a more detailed error in
    * pgaio_uring_shmem_init().
 */

   errno = -ret;
   elog(DEBUG1,
     "cannot use combined memory mapping for io_uring, ring creation failed: %m");

  }

  if (munmap(ring_ptr, ring_size) != 0)
   elog(ERROR, "munmap() failed: %m");
 }
#else
 {
  elog(DEBUG1,
    "can't use combined memory mapping for io_uring, kernel or liburing too old");
 }
#endif

 pgaio_uring_caps.checked = true;
}

/*
 * Memory for all PgAioUringContext instances
 */

static size_t
pgaio_uring_context_shmem_size(void)
{
 return mul_size(pgaio_uring_procs(), sizeof(PgAioUringContext));
}

/*
 * Memory for the combined memory used by io_uring instances. Returns 0 if
 * that is not supported by kernel/liburing.
 */

static size_t
pgaio_uring_ring_shmem_size(void)
{
 size_t  sz = 0;

 if (pgaio_uring_caps.mem_init_size > 0)
 {
  /*
   * Memory for rings needs to be allocated to the page boundary,
   * reserve space. Luckily it does not need to be aligned to hugepage
   * boundaries, even if huge pages are used.
 */

  sz = add_size(sz, sysconf(_SC_PAGESIZE));
  sz = add_size(sz, mul_size(pgaio_uring_procs(),
           pgaio_uring_caps.mem_init_size));
 }

 return sz;
}

static size_t
pgaio_uring_shmem_size(void)
{
 size_t  sz;

 /*
  * Kernel and liburing support for various features influences how much
  * shmem we need, perform the necessary checks.
 */

 pgaio_uring_check_capabilities();

 sz = pgaio_uring_context_shmem_size();
 sz = add_size(sz, pgaio_uring_ring_shmem_size());

 return sz;
}

static void
pgaio_uring_shmem_init(bool first_time)
{
 int   TotalProcs = pgaio_uring_procs();
 bool  found;
 char    *shmem;
 size_t  ring_mem_remain = 0;
 char    *ring_mem_next = 0;

 /*
  * We allocate memory for all PgAioUringContext instances and, if
  * supported, the memory required for each of the io_uring instances, in
  * one ShmemInitStruct().
 */

 shmem = ShmemInitStruct("AioUringContext", pgaio_uring_shmem_size(), &found);
 if (found)
  return;

 pgaio_uring_contexts = (PgAioUringContext *) shmem;
 shmem += pgaio_uring_context_shmem_size();

 /* if supported, handle memory alignment / sizing for io_uring memory */
 if (pgaio_uring_caps.mem_init_size > 0)
 {
  ring_mem_remain = pgaio_uring_ring_shmem_size();
  ring_mem_next = (char *) shmem;

  /* align to page boundary, see also pgaio_uring_ring_shmem_size() */
  ring_mem_next = (char *) TYPEALIGN(sysconf(_SC_PAGESIZE), ring_mem_next);

  /* account for alignment */
  ring_mem_remain -= ring_mem_next - shmem;
  shmem += ring_mem_next - shmem;

  shmem += ring_mem_remain;
 }

 for (int contextno = 0; contextno < TotalProcs; contextno++)
 {
  PgAioUringContext *context = &pgaio_uring_contexts[contextno];
  int   ret;

  /*
   * Right now a high TotalProcs will cause problems in two ways:
   *
   * - RLIMIT_NOFILE needs to be big enough to allow all
   * io_uring_queue_init() calls to succeed.
   *
   * - RLIMIT_NOFILE needs to be big enough to still have enough file
   * descriptors to satisfy set_max_safe_fds() left over. Or, even
   * better, have max_files_per_process left over FDs.
   *
   * We probably should adjust the soft RLIMIT_NOFILE to ensure that.
   *
   *
   * XXX: Newer versions of io_uring support sharing the workers that
   * execute some asynchronous IOs between io_uring instances. It might
   * be worth using that - also need to evaluate if that causes
   * noticeable additional contention?
 */


  /*
   * If supported (c.f. pgaio_uring_check_capabilities()), create ring
   * with its data in shared memory. Otherwise fall back io_uring
   * creating a memory mapping for each ring.
 */

#if defined(HAVE_IO_URING_QUEUE_INIT_MEM) && defined(IORING_SETUP_NO_MMAP)
  if (pgaio_uring_caps.mem_init_size > 0)
  {
   struct io_uring_params p = {0};

   ret = io_uring_queue_init_mem(io_max_concurrency, &context->io_uring_ring, &p, ring_mem_next, ring_mem_remain);

   ring_mem_remain -= ret;
   ring_mem_next += ret;
  }
  else
#endif
  {
   ret = io_uring_queue_init(io_max_concurrency, &context->io_uring_ring, 0);
  }

  if (ret < 0)
  {
   char    *hint = NULL;
   int   err = ERRCODE_INTERNAL_ERROR;

   /* add hints for some failures that errno explains sufficiently */
   if (-ret == EPERM)
   {
    err = ERRCODE_INSUFFICIENT_PRIVILEGE;
    hint = _("Check if io_uring is disabled via /proc/sys/kernel/io_uring_disabled.");
   }
   else if (-ret == EMFILE)
   {
    err = ERRCODE_INSUFFICIENT_RESOURCES;
    hint = psprintf(_("Consider increasing \"ulimit -n\" to at least %d."),
        TotalProcs + max_files_per_process);
   }
   else if (-ret == ENOSYS)
   {
    err = ERRCODE_FEATURE_NOT_SUPPORTED;
    hint = _("The kernel does not support io_uring.");
   }

   /* update errno to allow %m to work */
   errno = -ret;

   ereport(ERROR,
     errcode(err),
     errmsg("could not setup io_uring queue: %m"),
     hint != NULL ? errhint("%s", hint) : 0);
  }

  LWLockInitialize(&context->completion_lock, LWTRANCHE_AIO_URING_COMPLETION);
 }
}

static void
pgaio_uring_init_backend(void)
{
 Assert(MyProcNumber < pgaio_uring_procs());

 pgaio_my_uring_context = &pgaio_uring_contexts[MyProcNumber];
}

static int
pgaio_uring_submit(uint16 num_staged_ios, PgAioHandle **staged_ios)
{
 struct io_uring *uring_instance = &pgaio_my_uring_context->io_uring_ring;
 int   in_flight_before = dclist_count(&pgaio_my_backend->in_flight_ios);

 Assert(num_staged_ios <= PGAIO_SUBMIT_BATCH_SIZE);

 for (int i = 0; i < num_staged_ios; i++)
 {
  PgAioHandle *ioh = staged_ios[i];
  struct io_uring_sqe *sqe;

  sqe = io_uring_get_sqe(uring_instance);

  if (!sqe)
   elog(ERROR, "io_uring submission queue is unexpectedly full");

  pgaio_io_prepare_submit(ioh);
  pgaio_uring_sq_from_io(ioh, sqe);

  /*
   * io_uring executes IO in process context if possible. That's
   * generally good, as it reduces context switching. When performing a
   * lot of buffered IO that means that copying between page cache and
   * userspace memory happens in the foreground, as it can't be
   * offloaded to DMA hardware as is possible when using direct IO. When
   * executing a lot of buffered IO this causes io_uring to be slower
   * than worker mode, as worker mode parallelizes the copying. io_uring
   * can be told to offload work to worker threads instead.
   *
   * If an IO is buffered IO and we already have IOs in flight or
   * multiple IOs are being submitted, we thus tell io_uring to execute
   * the IO in the background. We don't do so for the first few IOs
   * being submitted as executing in this process' context has lower
   * latency.
 */

  if (in_flight_before > 4 && (ioh->flags & PGAIO_HF_BUFFERED))
   io_uring_sqe_set_flags(sqe, IOSQE_ASYNC);

  in_flight_before++;
 }

 while (true)
 {
  int   ret;

  pgstat_report_wait_start(WAIT_EVENT_AIO_IO_URING_SUBMIT);
  ret = io_uring_submit(uring_instance);
  pgstat_report_wait_end();

  if (ret == -EINTR)
  {
   pgaio_debug(DEBUG3,
      "aio method uring: submit EINTR, nios: %d",
      num_staged_ios);
  }
  else if (ret < 0)
  {
   /*
    * The io_uring_enter() manpage suggests that the appropriate
    * reaction to EAGAIN is:
    *
    * "The application should wait for some completions and try
    * again"
    *
    * However, it seems unlikely that that would help in our case, as
    * we apply a low limit to the number of outstanding IOs and thus
    * also outstanding completions, making it unlikely that we'd get
    * EAGAIN while the OS is in good working order.
    *
    * Additionally, it would be problematic to just wait here, our
    * caller might hold critical locks. It'd possibly lead to
    * delaying the crash-restart that seems likely to occur when the
    * kernel is under such heavy memory pressure.
    *
    * Update errno to allow %m to work.
 */

   errno = -ret;
   elog(PANIC, "io_uring submit failed: %m");
  }
  else if (ret != num_staged_ios)
  {
   /* likely unreachable, but if it is, we would need to re-submit */
   elog(PANIC, "io_uring submit submitted only %d of %d",
     ret, num_staged_ios);
  }
  else
  {
   pgaio_debug(DEBUG4,
      "aio method uring: submitted %d IOs",
      num_staged_ios);
   break;
  }
 }

 return num_staged_ios;
}

static void
pgaio_uring_completion_error_callback(void *arg)
{
 ProcNumber owner;
 PGPROC    *owner_proc;
 int32  owner_pid;
 PgAioHandle *ioh = arg;

 if (!ioh)
  return;

 /* No need for context if a backend is completing the IO for itself */
 if (ioh->owner_procno == MyProcNumber)
  return;

 owner = ioh->owner_procno;
 owner_proc = GetPGProcByNumber(owner);
 owner_pid = owner_proc->pid;

 errcontext("completing I/O on behalf of process %d", owner_pid);
}

static void
pgaio_uring_drain_locked(PgAioUringContext *context)
{
 int   ready;
 int   orig_ready;
 ErrorContextCallback errcallback = {0};

 Assert(LWLockHeldByMeInMode(&context->completion_lock, LW_EXCLUSIVE));

 errcallback.callback = pgaio_uring_completion_error_callback;
 errcallback.previous = error_context_stack;
 error_context_stack = &errcallback;

 /*
  * Don't drain more events than available right now. Otherwise it's
  * plausible that one backend could get stuck, for a while, receiving CQEs
  * without actually processing them.
 */

 orig_ready = ready = io_uring_cq_ready(&context->io_uring_ring);

 while (ready > 0)
 {
  struct io_uring_cqe *cqes[PGAIO_MAX_LOCAL_COMPLETED_IO];
  uint32  ncqes;

  START_CRIT_SECTION();
  ncqes =
   io_uring_peek_batch_cqe(&context->io_uring_ring,
         cqes,
         Min(PGAIO_MAX_LOCAL_COMPLETED_IO, ready));
  Assert(ncqes <= ready);

  ready -= ncqes;

  for (int i = 0; i < ncqes; i++)
  {
   struct io_uring_cqe *cqe = cqes[i];
   PgAioHandle *ioh = io_uring_cqe_get_data(cqe);
   int   result = cqe->res;

   errcallback.arg = ioh;

   io_uring_cqe_seen(&context->io_uring_ring, cqe);

   pgaio_io_process_completion(ioh, result);
   errcallback.arg = NULL;
  }

  END_CRIT_SECTION();

  pgaio_debug(DEBUG3,
     "drained %d/%d, now expecting %d",
     ncqes, orig_ready, io_uring_cq_ready(&context->io_uring_ring));
 }

 error_context_stack = errcallback.previous;
}

static void
pgaio_uring_wait_one(PgAioHandle *ioh, uint64 ref_generation)
{
 PgAioHandleState state;
 ProcNumber owner_procno = ioh->owner_procno;
 PgAioUringContext *owner_context = &pgaio_uring_contexts[owner_procno];
 bool  expect_cqe;
 int   waited = 0;

 /*
  * XXX: It would be nice to have a smarter locking scheme, nearly all the
  * time the backend owning the ring will consume the completions, making
  * the locking unnecessarily expensive.
 */

 LWLockAcquire(&owner_context->completion_lock, LW_EXCLUSIVE);

 while (true)
 {
  pgaio_debug_io(DEBUG3, ioh,
        "wait_one io_gen: %" PRIu64 ", ref_gen: %" PRIu64 ", cycle %d",
        ioh->generation,
        ref_generation,
        waited);

  if (pgaio_io_was_recycled(ioh, ref_generation, &state) ||
   state != PGAIO_HS_SUBMITTED)
  {
   /* the IO was completed by another backend */
   break;
  }
  else if (io_uring_cq_ready(&owner_context->io_uring_ring))
  {
   /* no need to wait in the kernel, io_uring has a completion */
   expect_cqe = true;
  }
  else
  {
   int   ret;
   struct io_uring_cqe *cqes;

   /* need to wait in the kernel */
   pgstat_report_wait_start(WAIT_EVENT_AIO_IO_URING_EXECUTION);
   ret = io_uring_wait_cqes(&owner_context->io_uring_ring, &cqes, 1, NULL, NULL);
   pgstat_report_wait_end();

   if (ret == -EINTR)
   {
    continue;
   }
   else if (ret != 0)
   {
    /* see comment after io_uring_submit() */
    errno = -ret;
    elog(PANIC, "io_uring wait failed: %m");
   }
   else
   {
    Assert(cqes != NULL);
    expect_cqe = true;
    waited++;
   }
  }

  if (expect_cqe)
  {
   pgaio_uring_drain_locked(owner_context);
  }
 }

 LWLockRelease(&owner_context->completion_lock);

 pgaio_debug(DEBUG3,
    "wait_one with %d sleeps",
    waited);
}

static void
pgaio_uring_sq_from_io(PgAioHandle *ioh, struct io_uring_sqe *sqe)
{
 struct iovec *iov;

 switch ((PgAioOp) ioh->op)
 {
  case PGAIO_OP_READV:
   iov = &pgaio_ctl->iovecs[ioh->iovec_off];
   if (ioh->op_data.read.iov_length == 1)
   {
    io_uring_prep_read(sqe,
           ioh->op_data.read.fd,
           iov->iov_base,
           iov->iov_len,
           ioh->op_data.read.offset);
   }
   else
   {
    io_uring_prep_readv(sqe,
         ioh->op_data.read.fd,
         iov,
         ioh->op_data.read.iov_length,
         ioh->op_data.read.offset);

   }
   break;

  case PGAIO_OP_WRITEV:
   iov = &pgaio_ctl->iovecs[ioh->iovec_off];
   if (ioh->op_data.write.iov_length == 1)
   {
    io_uring_prep_write(sqe,
         ioh->op_data.write.fd,
         iov->iov_base,
         iov->iov_len,
         ioh->op_data.write.offset);
   }
   else
   {
    io_uring_prep_writev(sqe,
          ioh->op_data.write.fd,
          iov,
          ioh->op_data.write.iov_length,
          ioh->op_data.write.offset);
   }
   break;

  case PGAIO_OP_INVALID:
   elog(ERROR, "trying to prepare invalid IO operation for execution");
 }

 io_uring_sqe_set_data(sqe, ioh);
}

#endif       /* IOMETHOD_IO_URING_ENABLED */

Messung V0.5 in Prozent
C=93 H=89 G=90

¤ Dauer der Verarbeitung: 0.19 Sekunden  (vorverarbeitet am  2026-08-08) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover

NIST Cobol Testsuite

Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.