diff --git a/README.es.md b/README.es.md index e348694..12d58e7 100644 --- a/README.es.md +++ b/README.es.md @@ -11,21 +11,23 @@ Idioma: [English](./README.md) | [简体中文](./README.zh-CN.md) | **Español* ## Descripción general -`uring` es el paquete del workspace que expone la interfaz de `io_uring` frente al kernel de Linux. Se encarga de crear +`uring` es el paquete del espacio de trabajo que expone la interfaz de `io_uring` frente al kernel de Linux. Se encarga +de crear y arrancar rings, preparar SQE, decodificar CQE, transportar la identidad de envío a través de `user_data`, y ofrecer -registro de buffers, operaciones multishot y primitivas de configuración de listeners. +registro de búferes, operaciones multishot y primitivas de configuración de escuchas. El diseño sigue un principio de frontera explícita: la mecánica orientada al kernel y los hechos observables de completado permanecen en el borde de la API, mientras que la política y la composición quedan por encima de esa -frontera. El código runtime del llamador posee la correlación de completados, retry/backoff, el enrutamiento de handlers -y sesiones, el ciclo de vida de conexiones y la liberación terminal de recursos. +frontera. El código de ejecución del llamador posee la correlación de completados, los reintentos y la espera +progresiva, +el enrutamiento de manejadores y sesiones, el ciclo de vida de conexiones y la liberación terminal de recursos. Las superficies principales son: - `Uring`, el handle del ring activo y su conjunto de operaciones - `SQEContext`, la identidad de envío transportada en `user_data` - `CQEView`, la vista prestada de completado que devuelve `Wait` -- provisión de buffers mediante buffers registrados y grupos de buffers de varios tamaños +- provisión de búferes mediante búferes registrados y grupos de búferes de varios tamaños ## Instalación @@ -35,15 +37,19 @@ Las superficies principales son: uname -r ``` +`uring` asume la línea base 6.18+ y no incluye ramas de reserva para kernels anteriores. Use un kernel compatible en +lugar +de esperar ramas de compatibilidad dentro del paquete. + En Debian 13, la rama estable del kernel puede estar aún por debajo de esa línea base. Consulte la sección de -actualización de kernel en Debian 13 si necesita instalar el kernel más reciente empaquetado por Debian que cumpla el +actualización del kernel en Debian 13 si necesita instalar el kernel más reciente empaquetado por Debian que cumpla el requisito de 6.18. ```bash go get code.hybscloud.com/uring ``` -### Actualización de kernel en Debian 13 +### Actualización del kernel en Debian 13 La rama estable de Debian 13 incluye el kernel 6.12. La suite `trixie-backports` proporciona un kernel 6.18+ empaquetado por Debian. Consulte [SETUP.md](./SETUP.md) para las instrucciones paso a paso. @@ -51,14 +57,16 @@ por Debian. Consulte [SETUP.md](./SETUP.md) para las instrucciones paso a paso. ### Resolución de problemas La creación del ring puede devolver `ENOMEM`, `EPERM` o `ENOSYS` según los límites de memlock, la configuración de -sysctl o el soporte del kernel. Los runtimes de contenedores bloquean las llamadas al sistema de `io_uring` por defecto. +sysctl o el soporte del kernel. Los entornos de ejecución de contenedores bloquean las llamadas al sistema de `io_uring` +por defecto. Consulte [SETUP.md](./SETUP.md) para el diagnóstico y la resolución. ## Ciclo de vida del ring `New` devuelve un ring sin iniciar. Antes de enviar operaciones es necesario llamar a `Start`. `Start` registra los -recursos del ring y lo habilita; `New`, por su parte, construye los pools de contexto de forma anticipada. En Linux, -`uring` asume la línea base fija de 6.18+ y no mantiene ramas de fallback para versiones anteriores. +recursos del ring y lo habilita; `New`, por su parte, construye los pools de contexto de forma anticipada. El ejemplo +siguiente envía una lectura de archivo, espera el CQE correspondiente y usa `iox.Classify` para conservar +`ErrWouldBlock` como resultado semántico de falta de progreso, no como fallo. ```go ring, err := uring.New(func(o *uring.Options) { @@ -71,26 +79,51 @@ if err != nil { if err := ring.Start(); err != nil { return err } +defer ring.Stop() -cqes := make([]uring.CQEView, 64) -n, err := ring.Wait(cqes) -if err != nil && !errors.Is(err, iox.ErrWouldBlock) { +fd := iofd.NewFD(int(file.Fd())) +buf := make([]byte, 4096) +ctx := uring.PackDirect(uring.IORING_OP_READ, 0, 0, 0).WithFD(fd) +if err := ring.Read(ctx, buf); err != nil { return err } -for i := range n { - cqe := cqes[i] - if cqe.Res < 0 { - return fmt.Errorf("completion failed: op=%d fd=%d res=%d", cqe.Op(), cqe.FD(), cqe.Res) +cqes := make([]uring.CQEView, 64) +var backoff iox.Backoff + +for { + n, err := ring.Wait(cqes) + switch iox.Classify(err) { + case iox.OutcomeWouldBlock: + backoff.Wait() + continue + case iox.OutcomeFailure: + return err + } + if n == 0 { + backoff.Wait() + continue + } + + backoff.Reset() + for i := range n { + cqe := cqes[i] + if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { + continue + } + if cqe.Res < 0 { + return fmt.Errorf("uring read failed: res=%d", cqe.Res) + } + handle(buf[:int(cqe.Res)]) + return nil } - fmt.Printf("completed op=%d on fd=%d with res=%d\n", cqe.Op(), cqe.FD(), cqe.Res) } ``` -`Wait` vacía los envíos pendientes antes de recoger completados. En rings de emisor único, también realiza el enter del -kernel necesario para que el deferred task work avance una vez vaciada la SQ; el llamador debe serializar `Wait`/`enter` -con las operaciones de submit-state. `iox.ErrWouldBlock` indica que no hay ningún completado observable en el límite -actual. Este error está definido en `code.hybscloud.com/iox`. +`Wait` vacía los envíos pendientes antes de recoger completados. En rings de emisor único, también realiza la entrada al +kernel necesaria para que el trabajo diferido avance una vez vaciada la SQ; el llamador debe serializar `Wait`/`enter` +con las operaciones de estado de envío. Si `iox.Classify(err) == iox.OutcomeWouldBlock`, eso indica que no hay ningún +completado observable en el límite actual. `Start` y `Stop` forman el par de ciclo de vida del ring. `Stop` es idempotente y deja el ring permanentemente inutilizable, por lo que solo debe llamarse tras drenar todas las operaciones en vuelo, recoger los CQE pendientes y @@ -98,21 +131,21 @@ detener las suscripciones multishot activas. ## Tipos y operaciones -| Tipo | Papel | -|------|-------| -| `Uring` | Inicialización del anillo, envío, recolección de completados y métodos de operación | -| `Options` | Entradas del anillo, presupuesto de buffers registrados, escala de grupos de buffers y visibilidad de completados | -| `SQEContext` | Identidad compacta de envío almacenada en `user_data` | -| `CQEView` | Registro prestado de completado con accesores para contexto decodificado | -| `ListenerOp` | Handle de una operación de creación de listener con FD y helpers de accept | -| `BundleIterator` | Itera sobre buffers consumidos en una recepción bundle | -| `IncrementalReceiver` | Gestiona recepciones incrementales de buffer-ring (`IOU_PBUF_RING_INC`) | -| `ZCTracker` | Rastrea el ciclo de vida de dos CQEs del envío zero-copy | -| `ContextPools` | Pools para contextos de envío indirectos y extendidos | -| `ZCRXReceiver` | Ciclo de vida de recepción zero-copy sobre una cola RX de NIC | -| `ZCRXConfig` | Configuración de una instancia de recepción ZCRX | -| `ZCRXHandler` | Interfaz de callback para datos, errores y cierre ZCRX | -| `ZCRXBuffer` | Vista de recepción zero-copy entregada, con reposición del kernel al liberar | +| Tipo | Papel | +|-----------------------|-------------------------------------------------------------------------------------------------------------------| +| `Uring` | Inicialización del anillo, envío, recolección de completados y métodos de operación | +| `Options` | Entradas del anillo, presupuesto de búferes registrados, escala de grupos de búferes y visibilidad de completados | +| `SQEContext` | Identidad compacta de envío almacenada en `user_data` | +| `CQEView` | Registro prestado de completado con accesores para contexto decodificado | +| `ListenerOp` | Manejador de una operación de creación de escucha con FD y auxiliares de accept | +| `BundleIterator` | Itera sobre búferes consumidos en una recepción agrupada | +| `IncrementalReceiver` | Gestiona recepciones incrementales de buffer-ring (`IOU_PBUF_RING_INC`) | +| `ZCTracker` | Rastrea el ciclo de vida de dos CQEs del envío de copia cero | +| `ContextPools` | Pools para contextos de envío indirectos y extendidos | +| `ZCRXReceiver` | Ciclo de vida de recepción de copia cero sobre una cola RX de NIC | +| `ZCRXConfig` | Configuración de una instancia de recepción ZCRX | +| `ZCRXHandler` | Interfaz de devolución de llamada para datos, errores y cierre ZCRX | +| `ZCRXBuffer` | Vista de recepción de copia cero entregada, con reposición del kernel al liberar | Operaciones: @@ -140,8 +173,8 @@ opcodes correspondientes. De lo contrario, devuelven `ErrNotSupported`. ## Transporte de contexto -`SQEContext` es el token de identidad principal en `uring`. En modo directo, empaqueta el opcode, las flags del SQE, el -identificador de grupo de buffers y el descriptor de archivo en un único valor de 64 bits. +`SQEContext` es el token de identidad principal en `uring`. En modo directo, empaqueta el opcode, las marcas del SQE, el +identificador de grupo de búferes y el descriptor de archivo en un único valor de 64 bits. ```go sqeCtx := uring.ForFD(fd). @@ -158,14 +191,15 @@ Los tres modos de contexto son: | Extended | Puntero a `ExtSQE` | SQE completo más 64 bytes de datos de usuario | En la ruta habitual, parta de `ForFD` o `PackDirect` y añada solo los bits que desee volver a observar tras el -completado. `WithFlags` reemplaza el conjunto completo de flags, por lo que conviene calcular la unión antes de +completado. `WithFlags` reemplaza el conjunto completo de marcas, por lo que conviene calcular la unión antes de invocarlo. Cuando se necesiten metadatos del llamador que no caben en el layout directo de 64 bits, tome prestado un `ExtSQE`, escriba en su `UserData` mediante `Ctx*Of` o `ViewCtx*`, y vuelva a empaquetarlo como `SQEContext`. Es preferible usar -payloads escalares. Si un overlay raw o una vista tipada almacena punteros de Go, interfaces, valores func, slices, +cargas escalares. Si una superposición sin procesar o una vista tipada almacena punteros de Go, interfaces, valores de +función, slices, strings, maps, chans o structs que los contengan, mantenga las raíces vivas fuera de `UserData`, ya que el GC no rastrea -esos bytes raw. +esos bytes sin procesar. ```go ext := ring.ExtSQE() @@ -188,9 +222,15 @@ invoque `cqe.Context()` cuando necesite recuperar el token de envío original. cqes := make([]uring.CQEView, 64) n, err := ring.Wait(cqes) -if err != nil && !errors.Is(err, iox.ErrWouldBlock) { +switch iox.Classify(err) { +case iox.OutcomeWouldBlock: + return iox.ErrWouldBlock +case iox.OutcomeFailure: return err } +if n == 0 { + return iox.ErrWouldBlock +} for i := 0; i < n; i++ { cqe := cqes[i] @@ -214,16 +254,21 @@ for i := 0; i < n; i++ { ``` Al completarse la operación, `CQEView` decodifica el modo de contexto correspondiente bajo demanda. `CQEView`, -`IndirectSQE`, `ExtSQE` y los buffers prestados no deben sobrevivir más allá de su tiempo de vida documentado. +`IndirectSQE`, `ExtSQE` y los búferes prestados no deben sobrevivir más allá de su tiempo de vida documentado. -## Provisión de buffers +## Provisión de búferes -`uring` ofrece dos estrategias de buffers de recepción: +`uring` ofrece tres rutas prácticas para búferes. Los búferes registrados quedan fijados durante el arranque del ring y +se usan con I/O de archivo con búfer fijo. Los anillos de búferes provistos permiten que el kernel elija un búfer de +recepción y +devuelva su ID en el CQE. Las recepciones agrupadas consumen un rango lógico contiguo de búferes provistos y lo exponen +mediante `BundleIterator`. -- buffers provistos de tamaño fijo mediante `ReadBufferSize` y `ReadBufferNum` -- grupos de buffers de varios tamaños mediante `MultiSizeBuffer` +- búferes provistos de tamaño fijo mediante `ReadBufferSize` y `ReadBufferNum` +- grupos de búferes de varios tamaños mediante `MultiSizeBuffer` +- búferes fijos registrados mediante `LockedBufferMem`, `RegisteredBuffer`, `ReadFixed` y `WriteFixed` -En la mayoría de los sistemas, los helpers de configuración ofrecen un punto de partida directo: +En la mayoría de los sistemas, las funciones auxiliares de configuración ofrecen un punto de partida directo: ```go opts := uring.OptionsForSystem(uring.MachineMemory4GB) @@ -233,17 +278,68 @@ ring, err := uring.New(func(o *uring.Options) { ``` Use `OptionsForBudget` para partir de un presupuesto de memoria explícito, y `BufferConfigForBudget` para inspeccionar -la distribución por niveles elegida para dicho presupuesto. +la distribución por niveles elegida para dicho presupuesto: + +```go +cfg, scale := uring.BufferConfigForBudget(256 * uring.MiB) +fmt.Printf("buffer tiers=%+v scale=%d\n", cfg, scale) +``` -Los buffers registrados requieren memoria fijada (pinned). Si el registro de buffers grandes falla, aumente +El I/O con búfer fijo usa un búfer registrado por índice. La slice devuelta pertenece al ring; manténgala viva hasta que +termine la operación fija: + +```go +buf := ring.RegisteredBuffer(0) +copy(buf, payload) + +fd := iofd.NewFD(int(file.Fd())) +ctx := uring.PackDirect(uring.IORING_OP_WRITE_FIXED, 0, 0, 0).WithFD(fd) +if err := ring.WriteFixed(ctx, 0, len(payload)); err != nil { + return err +} +``` + +Para recibir en socket con selección de búfer por el kernel, pase `nil` como búfer de recepción y solicite la clase de +tamaño deseada. La finalización indica qué búfer fue elegido: + +```go +recvCtx := uring.PackDirect(uring.IORING_OP_RECV, 0, 0, 0) + +if err := ring.Receive(recvCtx, &socketFD, nil, uring.WithReadBufferSize(uring.BufferSizeSmall)); err != nil { + return err +} + +// Más tarde, cuando Wait devuelva el CQE correspondiente: +if cqe.HasBuffer() { + fmt.Printf("kernel selected group=%d id=%d\n", cqe.BufGroup(), cqe.BufID()) +} +``` + +Las recepciones agrupadas usan el mismo almacenamiento de búferes provistos, pero pueden consumir más de un búfer en un +solo CQE. Procese el iterador y después recicle los slots consumidos: + +```go +if err := ring.ReceiveBundle(recvCtx, &socketFD, uring.WithReadBufferSize(uring.BufferSizeSmall)); err != nil { + return err +} + +if it, ok := ring.BundleIterator(cqe, cqe.BufGroup()); ok { + for buf := range it.All() { + handle(buf) + } + it.Recycle(ring) +} +``` + +Los búferes registrados requieren memoria fijada. Si el registro de búferes grandes falla, aumente `RLIMIT_MEMLOCK` o reduzca el presupuesto. -## Operaciones multishot y de listener +## Operaciones multishot y de escucha `AcceptMultishot`, `ReceiveMultishot`, `SubmitAcceptMultishot`, `SubmitAcceptDirectMultishot`, `SubmitReceiveMultishot` y `SubmitReceiveBundleMultishot` envían operaciones de socket multishot. -`uring` deja fuera del paquete la política de enrutamiento de CQE. La configuración del listener avanza a través de +`uring` deja fuera del paquete la política de enrutamiento de CQE. La configuración del escucha avanza a través de `DecodeListenerCQE`, `PrepareListenerBind`, `PrepareListenerListen` y `SetListenerReady`; es el llamador quien decide cómo se despachan los completados y cuándo se detiene la cadena. @@ -251,26 +347,30 @@ cómo se despachan los completados y cuándo se detiene la cadena. La frontera de implementación se define así: -1. `New` construye un ring del kernel deshabilitado, crea los pools de contexto y elige la estrategia de buffers. -2. `Start` registra los buffers y habilita el ring para la línea base fija de Linux 6.18+. +1. `New` construye un ring del kernel deshabilitado, crea los pools de contexto y elige la estrategia de búferes. +2. `Start` registra los búferes y habilita el ring para la línea base fija de Linux 6.18+. 3. Los métodos de operación declaran intención escribiendo SQE. 4. `Wait` vacía los envíos y devuelve observaciones prestadas de CQE. -5. El código runtime del llamador decide planificación, reintentos, parking, enrutamiento de conexión/sesión y política +5. El código de ejecución del llamador decide planificación, reintentos, espera, enrutamiento de conexión/sesión y + política terminal de recursos. De este modo, `uring` se mantiene centrado en la mecánica frente al kernel y preserva el significado de los completados a través de la frontera. -## Frontera de runtime +## Frontera de ejecución -Las capas runtime por encima de `uring` deben usarlo como backend de kernel, no como planificador. La frontera ideal es -unidireccional: `uring` prepara SQEs, recoge CQEs, preserva `user_data`, expone `res` y flags de CQE, e informa hechos -de propiedad; el código runtime del llamador correlaciona esas observaciones con sus propios tokens, aplica -retry/backoff, enruta handlers y sesiones, agrupa envíos y libera recursos terminales. +Las capas de ejecución por encima de `uring` deben usarlo como backend del kernel, no como planificador. La frontera +ideal es +unidireccional: `uring` prepara SQEs, recoge CQEs, preserva `user_data`, expone `res` y marcas de CQE, e informa hechos +de propiedad; el código de ejecución del llamador correlaciona esas observaciones con sus propios tokens, aplica +reintentos y espera progresiva, enruta manejadores y sesiones, agrupa envíos y libera recursos terminales. -Un puente runtime puede consumir CQEs en modo Extended cuando la ejecución abstracta necesita hechos de completado. Un -runtime por conexión también puede sondear CQEs Extended raw directamente cuando necesita resultado de CQE, flags, -buffer ID y token codificado antes de reducir el evento a callbacks de handler. +Un puente de ejecución puede consumir CQEs en modo Extended cuando la ejecución abstracta necesita hechos de completado. +Un +entorno de ejecución por conexión también puede sondear CQEs Extended sin procesar directamente cuando necesita +resultado de CQE, marcas, +ID de búfer y token codificado antes de reducir el evento a devoluciones de llamada de manejador. Las capas de contexto y ejecución abstracta por encima de esta frontera no cambian el rol de `uring` como frontera del kernel. @@ -279,11 +379,11 @@ kernel. `uring` expone los mecanismos orientados al kernel; la planificación, los reintentos, el seguimiento de conexiones y la interpretación del protocolo corresponden a las capas superiores. Los patrones siguientes describen la frontera que debe -preservar un runtime del llamador. +preservar un entorno de ejecución del llamador. ### Bucle de eventos propietario del ring -En modo single-issuer (el predeterminado), una goroutine serializa todas las operaciones de submit. Un bucle típico +En modo de emisor único (el predeterminado), una goroutine serializa todas las operaciones de envío. Un bucle típico emite trabajo pendiente, aplica un `iox.Backoff` propiedad del llamador cuando `Wait` no informa progreso observable y despacha las finalizaciones: @@ -299,11 +399,11 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { } n, err := ring.Wait(cqes) - if errors.Is(err, iox.ErrWouldBlock) { + switch iox.Classify(err) { + case iox.OutcomeWouldBlock: backoff.Wait() continue - } - if err != nil { + case iox.OutcomeFailure: return err } if n == 0 { @@ -320,14 +420,15 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { ``` Todos los métodos del ring, incluidos `Send`, `Receive`, `AcceptMultishot` y `Wait`, se ejecutan en esta goroutine. El -trabajo procedente de otras goroutines entra en el bucle a través de un canal o una cola lock-free; no se deben invocar +trabajo procedente de otras goroutines entra en el bucle a través de un canal o una cola sin bloqueos; no se deben +invocar los métodos del ring directamente. `iox.Backoff` sigue siendo propiedad del llamador: use `backoff.Wait()` cuando `Wait` -devuelva `iox.ErrWouldBlock` o no recoja ningún CQE, y `backoff.Reset()` tras cualquier lote con `n > 0`. +se clasifique como `iox.OutcomeWouldBlock` o no recoja ningún CQE, y `backoff.Reset()` tras cualquier lote con `n > 0`. ### Ciclo de vida de suscripciones multishot Una operación multishot genera un flujo de CQEs hasta que el kernel envía uno final (sin `IORING_CQE_F_MORE`). El código -runtime del llamador rastrea las suscripciones y gestiona la reemisión: +de ejecución del llamador rastrea las suscripciones y gestiona la reemisión: ```go handler := uring.NewMultishotSubscriber(). @@ -354,7 +455,7 @@ resuscripción condicional. ### Estado por conexión con contextos tipados -Los contextos extendidos transportan referencias por conexión a lo largo del ciclo completo submit → complete, sin +Los contextos extendidos transportan referencias por conexión a lo largo del ciclo completo envío → completado, sin necesidad de una tabla de búsqueda global: ```go @@ -387,12 +488,13 @@ ring.PutExtSQE(ext) Mantenga las raíces de punteros Go activas accesibles fuera de `UserData`. El GC no rastrea esos bytes crudos. El conjunto de raíces sidecar adjunto a cada slot `ExtSQE` se encarga de esto para los protocolos internos multishot y -listener, pero el código de runtime del llamador que coloca refs tipados debe mantenerlos accesibles de forma +de escucha, pero el código de ejecución del llamador que coloca refs tipados debe mantenerlos accesibles de forma independiente. ### Composición de plazos -`LinkTimeout` adjunta un plazo al SQE anterior a través de una cadena `IOSQE_IO_LINK`. La operación y el timeout +`LinkTimeout` adjunta un plazo al SQE anterior a través de una cadena `IOSQE_IO_LINK`. La operación y el tiempo de +espera compiten: exactamente uno se completa y el otro se cancela. ```go @@ -410,12 +512,13 @@ if err := ring.LinkTimeout(timeoutCtx, 5*time.Second); err != nil { } ``` -La capa de runtime del llamador maneja ambos resultados: una recepción exitosa cancela el timeout, y un timeout +La capa de ejecución del llamador maneja ambos resultados: una recepción exitosa cancela el tiempo de espera, y un +tiempo de espera disparado cancela la recepción. Ambos producen CQEs que el bucle de despacho debe observar. ## Patrones de uso en TCP -Los siguientes son los flujos más cortos, pensados para leer junto con los tests: +Los siguientes son los flujos más cortos, pensados para leerse junto con las pruebas: | Escenario | API principales | Referencia | |---------------|------------------------------------------------------------------|-----------------------------------------------------------------------------------| @@ -424,7 +527,7 @@ Los siguientes son los flujos más cortos, pensados para leer junto con los test ### Servidor echo TCP -Use `ListenerManager` para que el paquete prepare la cadena socket → bind → listen; a continuación, arranque multishot +Use `ListenerManager` para que el paquete prepare la cadena socket → bind → listen; a continuación, inicie multishot accept y multishot receive sobre los FD de conexión activos. ```go @@ -450,8 +553,8 @@ if err != nil { defer recvSub.Cancel() ``` -`listener_example_test.go` cubre la preparación del listener y el accept multishot, `examples/multishot_test.go` muestra -los CQE del lado del handler en multishot receive, y `examples/echo_test.go` ilustra el flujo echo completo sobre +`listener_example_test.go` cubre la preparación del escucha y el accept multishot, `examples/multishot_test.go` muestra +los CQE del lado del manejador en multishot receive, y `examples/echo_test.go` ilustra el flujo echo completo sobre loopback. ### Cliente TCP @@ -486,19 +589,19 @@ if err := ring.Receive(recvCtx, &clientFD, buf); err != nil { Reutilice el bucle `Wait` de la sección de ciclo de vida del ring tras cada envío para observar el completado correspondiente. El archivo `socket_integration_linux_test.go` cubre el flujo de connect/send. -## Recepción zero-copy (ZCRX) +## Recepción de copia cero (ZCRX) -`ZCRXReceiver` gestiona la recepción zero-copy desde una cola RX de hardware de NIC mediante `io_uring`. +`ZCRXReceiver` gestiona la recepción de copia cero desde una cola RX de hardware de NIC mediante `io_uring`. `NewZCRXReceiver` está preparado para rings con CQE de 32 bytes (`IORING_SETUP_CQE32`). La superficie actual de -`Options` no expone ese flag de configuración, de modo que los rings creados por la ruta estándar de `New` provocan que +`Options` no expone esa marca de configuración, de modo que los rings creados por la ruta estándar de `New` provocan que este constructor devuelva `ErrNotSupported`. Hasta que se exponga una ruta de configuración CQE32, esta sección documenta el contrato de frontera del receptor y no una receta pública ejecutable. ### Ciclo de vida 1. Con un ring habilitado para CQE32, cree el receptor con `NewZCRXReceiver`. El constructor registra la cola de - interfaz ZCRX, mapea el área de refill y prepara el refill ring. + interfaz ZCRX, mapea el área de reposición y prepara el ring de reposición. 2. Llame a `Start` para enviar la operación extendida `RECV_ZC` en el ring. 3. En la ruta de despacho de CQE, los completados ZCRX se enrutan al `ZCRXHandler`: - `OnData` entrega un `ZCRXBuffer` que apunta al área mapeada por la NIC. Llame a `Release` al terminar para reponer @@ -507,7 +610,7 @@ documenta el contrato de frontera del receptor y no una receta pública ejecutab - `OnStopped` se ejecuta una vez durante la retirada terminal, antes de que el estado pase a `Stopped`. 4. Llame a `Stop` para enviar un async cancel. El receptor transita por `Stopping` → `Retiring` → `Stopped`. 5. Consulte `Stopped` hasta que devuelva `true`, detenga el ring propietario y llame entonces a `Close` para liberar el - área mapeada y el mapeo del refill ring. + área mapeada y el mapeo del ring de reposición. ### Máquina de estados @@ -517,7 +620,7 @@ Idle → Active → Stopping → Retiring → Stopped `Stop` revierte a `Active` si el envío de cancelación falla. `Close` es idempotente. -### Contrato del handler +### Contrato del manejador - `OnData` y `OnError` se invocan en serie desde el goroutine de despacho de CQE. - `Release` es de productor único; llámelo exclusivamente desde el goroutine de despacho. @@ -526,21 +629,21 @@ Idle → Active → Stopping → Retiring → Stopped ## Ejemplos -Los tests de ejemplo en `uring/examples/` ilustran la API en la práctica. +Las pruebas de ejemplo en `uring/examples/` ilustran la API en la práctica. - `multishot_test.go`, accept multishot, receive multishot y parada de suscripciones -- `file_io_test.go`, lecturas, escrituras y batching de archivos -- `fixed_buffers_test.go`, buffers registrados e I/O con buffers fijos +- `file_io_test.go`, lecturas, escrituras y procesamiento por lotes de archivos +- `fixed_buffers_test.go`, búferes registrados e I/O con búferes fijos - `vectored_io_test.go`, operaciones de lectura y escritura vectorizadas -- `splice_tee_test.go`, transferencia de datos zero-copy con splice y tee -- `zerocopy_test.go`, rutas de envío zero-copy y seguimiento de completados -- `poll_test.go`, flujos de preparación basados en poll -- `buffer_ring_test.go`, provisión de buffer rings y grupos de buffers de varios tamaños +- `splice_tee_test.go`, transferencia de datos de copia cero con splice y tee +- `zerocopy_test.go`, rutas de envío de copia cero y seguimiento de completados +- `poll_test.go`, flujos de disponibilidad basados en poll +- `buffer_ring_test.go`, provisión de rings de búferes y grupos de búferes de varios tamaños - `context_test.go`, flujos `SQEContext` direct, indirect y extended, con acceso desde `CQEView` - `echo_test.go`, flujos de servidor echo TCP y UDP ping-pong -- `timeout_linux_test.go`, operaciones de timeout y linked-timeout +- `timeout_linux_test.go`, operaciones de tiempo de espera y tiempo de espera enlazado -A nivel de paquete, `listener_example_test.go` cubre la creación de listeners con accept multishot, y +A nivel de paquete, `listener_example_test.go` cubre la creación de escuchas con accept multishot, y `socket_integration_linux_test.go` cubre el flujo del cliente TCP de connect/send. ## Notas operativas @@ -549,19 +652,20 @@ A nivel de paquete, `listener_example_test.go` cubre la creación de listeners c - `ring.Features` informa de las entradas reales de SQ y CQ, el ancho de la ranura SQE y el orden de bytes que usa este paquete al interpretar `user_data`. - Deje `MultiIssuers` desactivado en la configuración predeterminada de emisor único (`SINGLE_ISSUER` + - `DEFER_TASKRUN`), en la que una sola ruta de ejecución del llamador serializa las operaciones de submit-state ( - `submit`, `Wait`/`enter`, `Stop` y resize). Actívelo solo cuando varios goroutines necesiten envío concurrente o enter - del lado wait; esto conmuta el ring a la configuración de envío compartido con `COOP_TASKRUN`. + `DEFER_TASKRUN`), en la que una sola ruta de ejecución del llamador serializa las operaciones de estado de envío ( + `submit`, `Wait`/`enter`, `Stop` y resize). Actívelo solo cuando varios goroutines necesiten envío concurrente o + entrada + del lado de espera; esto conmuta el ring a la configuración de envío compartido con `COOP_TASKRUN`. - `EpollWait` requiere que `timeout` sea `0`; use `LinkTimeout` cuando necesite un plazo. - Las vistas prestadas de completado y los contextos en pool deben liberarse o descartarse con prontitud. -- `ListenerOp.Close` cierra el FD del listener de inmediato. Si aún hay un CQE de configuración pendiente, drene ese CQE +- `ListenerOp.Close` cierra el FD del escucha de inmediato. Si aún hay un CQE de configuración pendiente, drene ese CQE y vuelva a llamar a `Close` para devolver el `ExtSQE` prestado al pool. ## Soporte de plataforma `uring` apunta a Go 1.26+ y Linux 6.18+ en la ruta real respaldada por el kernel. La mayoría de los archivos de -implementación y tests de ejemplo están protegidos con `//go:build linux`. Los archivos de Darwin proporcionan solo -stubs de compilación para la superficie compartida; las capacidades exclusivas de Linux siguen siendo exclusivas de +implementación y pruebas de ejemplo están protegidos con `//go:build linux`. Los archivos de Darwin proporcionan solo +resguardos de compilación para la superficie compartida; las capacidades exclusivas de Linux siguen siendo exclusivas de Linux y no alteran la línea base de ejecución en Linux descrita arriba. ## Licencia diff --git a/README.fr.md b/README.fr.md index 8a165df..979be30 100644 --- a/README.fr.md +++ b/README.fr.md @@ -13,19 +13,19 @@ Langue: [English](./README.md) | [简体中文](./README.zh-CN.md) | [Español]( `uring` est le package de l'espace de travail qui expose l'interface noyau Linux `io_uring`. Il crée et démarre les rings, prépare les SQE, décode les CQE, achemine l'identité de soumission via `user_data`, et fournit l'enregistrement -de buffers, les opérations multishot ainsi que les primitives de mise en place des listeners. +de tampons, les opérations multishot ainsi que les primitives de mise en place des écouteurs. `uring` repose sur une conception à interface explicite : la mécanique côté noyau et les faits de complétion observables -se situent au bord de l'API, tandis que la politique et la composition relèvent des couches supérieures. Le code runtime -côté appelant possède la corrélation des complétions, retry/backoff, le routage des handlers et des sessions, le cycle -de vie des connexions et la libération terminale des ressources. +se situent au bord de l'API, tandis que la politique et la composition relèvent des couches supérieures. Le code +d'exécution côté appelant possède la corrélation des complétions, les tentatives de reprise et l'attente progressive, le +routage des gestionnaires et des sessions, le cycle de vie des connexions et la libération terminale des ressources. Les surfaces principales sont : - `Uring`, le handle du ring actif et son jeu d'opérations - `SQEContext`, l'identité de soumission acheminée dans `user_data` - `CQEView`, la vue de complétion empruntée renvoyée par `Wait` -- la fourniture de buffers, via buffers enregistrés et groupes de buffers multi-tailles +- la fourniture de tampons, via tampons enregistrés et groupes de tampons multi-tailles ## Installation @@ -35,8 +35,12 @@ Les surfaces principales sont : uname -r ``` +`uring` suppose la base 6.18+ et ne contient aucune branche de repli pour les noyaux plus anciens. Démarrez un noyau +pris en charge +plutôt que d'attendre des branches de compatibilité dans ce package. + Sous Debian 13, le noyau de la branche stable peut rester en deçà de ce seuil. Consultez la section sur la mise à niveau -du noyau Debian 13 ci-dessous si vous avez besoin du noyau Debian le plus récent satisfaisant l'exigence 6.18. +du noyau Debian 13 ci-dessous si vous avez besoin d'un noyau Debian plus récent satisfaisant l'exigence 6.18. ```bash go get code.hybscloud.com/uring @@ -50,14 +54,15 @@ empaqueté par Debian. Consultez [SETUP.md](./SETUP.md) pour la marche à suivre ### Dépannage La création du ring peut renvoyer `ENOMEM`, `EPERM` ou `ENOSYS` selon les limites memlock, la configuration sysctl ou le -support noyau. Les runtimes de conteneurs bloquent les appels système `io_uring` par défaut. +support noyau. Les environnements d'exécution de conteneurs bloquent les appels système `io_uring` par défaut. Consultez [SETUP.md](./SETUP.md) pour le diagnostic et la résolution. ## Cycle de vie du ring `New` renvoie un ring non démarré. Il faut appeler `Start` avant de soumettre des opérations. `Start` enregistre les -ressources du ring et l'active ; `New`, de son côté, construit les pools de contexte de manière anticipée. Sous Linux, -`uring` présuppose la base fixe 6.18+ et ne conserve aucune branche de repli pour les noyaux antérieurs. +ressources du ring et l'active ; `New`, de son côté, construit les pools de contexte de manière anticipée. L'exemple +ci-dessous soumet une lecture de fichier, attend le CQE correspondant et utilise `iox.Classify` afin que `ErrWouldBlock` +reste un résultat sémantique d'absence de progrès, et non une défaillance. ```go ring, err := uring.New(func(o *uring.Options) { @@ -70,26 +75,51 @@ if err != nil { if err := ring.Start(); err != nil { return err } +defer ring.Stop() -cqes := make([]uring.CQEView, 64) -n, err := ring.Wait(cqes) -if err != nil && !errors.Is(err, iox.ErrWouldBlock) { +fd := iofd.NewFD(int(file.Fd())) +buf := make([]byte, 4096) +ctx := uring.PackDirect(uring.IORING_OP_READ, 0, 0, 0).WithFD(fd) +if err := ring.Read(ctx, buf); err != nil { return err } -for i := range n { - cqe := cqes[i] - if cqe.Res < 0 { - return fmt.Errorf("completion failed: op=%d fd=%d res=%d", cqe.Op(), cqe.FD(), cqe.Res) +cqes := make([]uring.CQEView, 64) +var backoff iox.Backoff + +for { + n, err := ring.Wait(cqes) + switch iox.Classify(err) { + case iox.OutcomeWouldBlock: + backoff.Wait() + continue + case iox.OutcomeFailure: + return err + } + if n == 0 { + backoff.Wait() + continue + } + + backoff.Reset() + for i := range n { + cqe := cqes[i] + if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { + continue + } + if cqe.Res < 0 { + return fmt.Errorf("uring read failed: res=%d", cqe.Res) + } + handle(buf[:int(cqe.Res)]) + return nil } - fmt.Printf("completed op=%d on fd=%d with res=%d\n", cqe.Op(), cqe.FD(), cqe.Res) } ``` `Wait` purge les soumissions en attente avant de récupérer les complétions. Sur un ring mono-émetteur, il émet aussi -l'appel enter vers le noyau, nécessaire pour que le deferred task work progresse une fois la SQ vidée ; l'appelant doit -sérialiser `Wait`/`enter` avec les opérations de submit-state. `iox.ErrWouldBlock` signale qu'aucune complétion n'est -observable à l'interface courante. Cette erreur est définie dans `code.hybscloud.com/iox`. +l'entrée noyau nécessaire pour que le travail différé progresse une fois la SQ vidée ; l'appelant doit +sérialiser `Wait`/`enter` avec les opérations d'état de soumission. Lorsque `iox.Classify(err)` produit +`iox.OutcomeWouldBlock`, aucune complétion n'est observable à l'interface courante. `Start` et `Stop` constituent la paire de cycle de vie du ring. `Stop` est idempotent et rend le ring définitivement inutilisable ; on ne doit donc l'appeler qu'après avoir drainé toutes les opérations en vol, récupéré les CQE en attente @@ -97,21 +127,21 @@ et arrêté les abonnements multishot encore actifs. ## Types et opérations -| Type | Rôle | -|------|------| -| `Uring` | Initialisation du ring, soumission, récupération des complétions, et méthodes d'opération | -| `Options` | Entrées du ring, budget de buffers enregistrés, échelle des groupes de buffers et visibilité des complétions | -| `SQEContext` | Identité de soumission compacte stockée dans `user_data` | -| `CQEView` | Enregistrement de complétion emprunté avec accesseurs de contexte décodé | -| `ListenerOp` | Handle d'une opération de création de listener avec FD et helpers accept | -| `BundleIterator` | Itère sur les buffers consommés lors d'une réception bundle | -| `IncrementalReceiver` | Gère les réceptions incrémentales de buffer-ring (`IOU_PBUF_RING_INC`) | -| `ZCTracker` | Suit le cycle de vie à deux CQE de l'envoi zero-copy | -| `ContextPools` | Pools pour contextes de soumission indirects et étendus | -| `ZCRXReceiver` | Cycle de vie de réception zero-copy sur une file RX de NIC | -| `ZCRXConfig` | Configuration d'une instance de réception ZCRX | -| `ZCRXHandler` | Interface de rappel pour données, erreurs et arrêt ZCRX | -| `ZCRXBuffer` | Vue de réception zero-copy livrée, avec remplissage par le kernel à la libération | +| Type | Rôle | +|-----------------------|--------------------------------------------------------------------------------------------------------------| +| `Uring` | Initialisation du ring, soumission, récupération des complétions, et méthodes d'opération | +| `Options` | Entrées du ring, budget de tampons enregistrés, échelle des groupes de tampons et visibilité des complétions | +| `SQEContext` | Identité de soumission compacte stockée dans `user_data` | +| `CQEView` | Enregistrement de complétion emprunté avec accesseurs de contexte décodé | +| `ListenerOp` | Gestionnaire d'une opération de création d'écouteur avec FD et auxiliaires accept | +| `BundleIterator` | Itère sur les tampons consommés lors d'une réception groupée | +| `IncrementalReceiver` | Gère les réceptions incrémentales de buffer-ring (`IOU_PBUF_RING_INC`) | +| `ZCTracker` | Suit le cycle de vie à deux CQE de l'envoi sans copie | +| `ContextPools` | Pools pour contextes de soumission indirects et étendus | +| `ZCRXReceiver` | Cycle de vie de réception sans copie sur une file RX de NIC | +| `ZCRXConfig` | Configuration d'une instance de réception ZCRX | +| `ZCRXHandler` | Interface de rappel pour données, erreurs et arrêt ZCRX | +| `ZCRXBuffer` | Vue de réception sans copie livrée, avec remplissage par le noyau à la libération | Opérations : @@ -140,8 +170,8 @@ destruction du ring. ## Transport du contexte -`SQEContext` est le jeton d'identité principal de `uring`. En mode direct, il encode l'opcode, les flags SQE, -l'identifiant de groupe de buffers et le descripteur de fichier dans une seule valeur de 64 bits. +`SQEContext` est le jeton d'identité principal de `uring`. En mode direct, il encode l'opcode, les fanions SQE, +l'identifiant de groupe de tampons et le descripteur de fichier dans une seule valeur de 64 bits. ```go sqeCtx := uring.ForFD(fd). @@ -158,13 +188,13 @@ Les trois modes de contexte sont : | Extended | Pointeur vers `ExtSQE` | SQE complet plus 64 octets de données utilisateur | Sur le chemin courant, on part de `ForFD` ou `PackDirect` en n'ajoutant que les bits que l'on souhaite retrouver à la -complétion. `WithFlags` remplace la totalité des flags : il convient donc de calculer les unions avant l'appel. +complétion. `WithFlags` remplace la totalité des fanions : il convient donc de calculer les unions avant l'appel. Lorsqu'on a besoin de métadonnées contrôlées par l'appelant au-delà du layout direct sur 64 bits, on emprunte un `ExtSQE`, on écrit dans son champ `UserData` via `Ctx*Of` ou `ViewCtx*`, puis on le ré-encode en `SQEContext`. Préférez -des charges scalaires à cet endroit. Si un overlay brut ou une vue typée y stocke des pointeurs Go, des interfaces, des -valeurs func, des slices, des strings, des maps, des chans ou des structs qui en contiennent, conservez les racines -vivantes en dehors de `UserData`, car le GC ne trace pas ces octets bruts. +des charges scalaires à cet endroit. Si une superposition brute ou une vue typée y stocke des pointeurs Go, des +interfaces, des valeurs de fonction, des tranches, des chaînes, des tables de hachage, des canaux ou des structures qui +en contiennent, conservez les racines vivantes en dehors de `UserData`, car le GC ne trace pas ces octets bruts. ```go ext := ring.ExtSQE() @@ -180,16 +210,22 @@ empruntés et uniquement si vous souhaitez réutiliser l'ensemble de pools. ### Dispatch des complétions avec `CQEView` -`uring` n'expose pas de type dédié au contexte de complétion. Le dispatch des complétions passe par `CQEView` ; on +`uring` n'expose pas de type dédié au contexte de complétion. La distribution des complétions passe par `CQEView` ; on appelle `cqe.Context()` pour récupérer le jeton de soumission d'origine. ```go cqes := make([]uring.CQEView, 64) n, err := ring.Wait(cqes) -if err != nil && !errors.Is(err, iox.ErrWouldBlock) { +switch iox.Classify(err) { +case iox.OutcomeWouldBlock: + return iox.ErrWouldBlock +case iox.OutcomeFailure: return err } +if n == 0 { + return iox.ErrWouldBlock +} for i := 0; i < n; i++ { cqe := cqes[i] @@ -212,17 +248,22 @@ for i := 0; i < n; i++ { } ``` -À la complétion, `CQEView` décode le mode de contexte à la demande. `CQEView`, `IndirectSQE`, `ExtSQE` et les buffers +À la complétion, `CQEView` décode le mode de contexte à la demande. `CQEView`, `IndirectSQE`, `ExtSQE` et les tampons empruntés ne doivent pas survivre au-delà de leur durée de vie documentée. -## Fourniture de buffers +## Fourniture de tampons -`uring` propose deux stratégies de fourniture des buffers de réception : +`uring` propose trois chemins pratiques pour les tampons. Les tampons enregistrés sont épinglés au démarrage du ring et +servent aux I/O fichier sur tampon fixe. Les anneaux de tampons fournis laissent le noyau choisir un tampon de réception +et +renvoyer son ID dans le CQE. Les réceptions groupées consomment une plage logique contiguë de tampons fournis et +l'exposent via `BundleIterator`. -- des buffers fournis de taille fixe via `ReadBufferSize` et `ReadBufferNum` -- des groupes de buffers multi-tailles via `MultiSizeBuffer` +- des tampons fournis de taille fixe via `ReadBufferSize` et `ReadBufferNum` +- des groupes de tampons multi-tailles via `MultiSizeBuffer` +- des tampons fixes enregistrés via `LockedBufferMem`, `RegisteredBuffer`, `ReadFixed` et `WriteFixed` -Pour la plupart des systèmes, les helpers de configuration offrent un point d'entrée direct : +Pour la plupart des systèmes, les fonctions auxiliaires de configuration offrent un point d'entrée direct : ```go opts := uring.OptionsForSystem(uring.MachineMemory4GB) @@ -232,16 +273,67 @@ ring, err := uring.New(func(o *uring.Options) { ``` On utilise `OptionsForBudget` pour partir d'un budget mémoire explicite, et `BufferConfigForBudget` pour inspecter la -répartition par niveaux retenue pour ce budget. +répartition par niveaux retenue pour ce budget : + +```go +cfg, scale := uring.BufferConfigForBudget(256 * uring.MiB) +fmt.Printf("buffer tiers=%+v scale=%d\n", cfg, scale) +``` -Les buffers enregistrés nécessitent de la mémoire épinglée. En cas d'échec de l'enregistrement de buffers volumineux, +L'I/O sur tampon fixe utilise un tampon enregistré par index. La tranche renvoyée appartient au ring ; gardez-la vivante +jusqu'à la complétion de l'opération fixe : + +```go +buf := ring.RegisteredBuffer(0) +copy(buf, payload) + +fd := iofd.NewFD(int(file.Fd())) +ctx := uring.PackDirect(uring.IORING_OP_WRITE_FIXED, 0, 0, 0).WithFD(fd) +if err := ring.WriteFixed(ctx, 0, len(payload)); err != nil { + return err +} +``` + +Pour recevoir sur un socket avec sélection de tampon par le noyau, passez `nil` comme tampon de réception et demandez la +classe de taille voulue. La complétion indique quel tampon a été choisi : + +```go +recvCtx := uring.PackDirect(uring.IORING_OP_RECV, 0, 0, 0) + +if err := ring.Receive(recvCtx, &socketFD, nil, uring.WithReadBufferSize(uring.BufferSizeSmall)); err != nil { + return err +} + +// Plus tard, après que Wait a renvoyé le CQE correspondant : +if cqe.HasBuffer() { + fmt.Printf("kernel selected group=%d id=%d\n", cqe.BufGroup(), cqe.BufID()) +} +``` + +Les réceptions groupées utilisent le même stockage de tampons fournis, mais peuvent consommer plusieurs tampons avec un +seul CQE. Traitez l'itérateur, puis recyclez les slots consommés : + +```go +if err := ring.ReceiveBundle(recvCtx, &socketFD, uring.WithReadBufferSize(uring.BufferSizeSmall)); err != nil { + return err +} + +if it, ok := ring.BundleIterator(cqe, cqe.BufGroup()); ok { + for buf := range it.All() { + handle(buf) + } + it.Recycle(ring) +} +``` + +Les tampons enregistrés nécessitent de la mémoire épinglée. En cas d'échec de l'enregistrement de tampons volumineux, augmentez `RLIMIT_MEMLOCK` ou réduisez le budget mémoire. -## Opérations multishot et listener +## Opérations multishot et écouteur `AcceptMultishot`, `ReceiveMultishot`, `SubmitAcceptMultishot`, `SubmitAcceptDirectMultishot`, `SubmitReceiveMultishot` et `SubmitReceiveBundleMultishot` soumettent des opérations socket multishot. -La politique de routage des CQE reste hors du package. La mise en place du listener progresse via `DecodeListenerCQE`, +La politique de routage des CQE reste hors du package. La mise en place de l'écouteur progresse via `DecodeListenerCQE`, `PrepareListenerBind`, `PrepareListenerListen` et `SetListenerReady` ; c'est l'appelant qui décide de la distribution des complétions et de l'arrêt de la chaîne. @@ -249,27 +341,33 @@ des complétions et de l'arrêt de la chaîne. L'implémentation se structure autour des couches suivantes : -1. `New` construit un ring noyau désactivé, crée les pools de contexte et détermine la stratégie de buffers. -2. `Start` enregistre les buffers et active le ring conformément à la base fixe Linux 6.18+. +1. `New` construit un ring noyau désactivé, crée les pools de contexte et détermine la stratégie de tampons. +2. `Start` enregistre les tampons et active le ring conformément à la base fixe Linux 6.18+. 3. Les méthodes d'opération publient l'intention en écrivant des SQE. 4. `Wait` purge les soumissions et renvoie des vues CQE empruntées. -5. Le code runtime côté appelant décide de l'ordonnancement, des reprises, du parking, du routage connexion/session et +5. Le code d'exécution côté appelant décide de l'ordonnancement, des reprises, de l'attente, du routage + connexion/session et de la politique terminale des ressources. De cette manière, `uring` reste focalisé sur la mécanique côté noyau tout en préservant la sémantique des complétions au travers de l'interface. -## Frontière runtime +## Frontière d'exécution -Les couches runtime au-dessus de `uring` doivent l'utiliser comme backend noyau, pas comme ordonnanceur. La frontière -idéale est unidirectionnelle : `uring` prépare les SQE, récupère les CQE, préserve `user_data`, expose `res` et flags -des CQE, et rapporte les faits de propriété ; le code runtime côté appelant corrèle ces observations avec ses propres -tokens, applique retry/backoff, route les handlers et sessions, regroupe les soumissions et libère les ressources +Les couches d'exécution au-dessus de `uring` doivent l'utiliser comme backend noyau, pas comme ordonnanceur. La +frontière +idéale est unidirectionnelle : `uring` prépare les SQE, récupère les CQE, préserve `user_data`, expose `res` et fanions +des CQE, et rapporte les faits de propriété ; le code d'exécution côté appelant corrèle ces observations avec ses +propres +jetons, applique les reprises et l'attente progressive, route les gestionnaires et sessions, regroupe les soumissions et +libère les ressources terminales. -Un pont runtime peut consommer les CQE en mode Extended lorsque l'exécution abstraite a besoin des faits de complétion. -Un runtime par connexion peut aussi sonder directement les CQE Extended bruts lorsqu'il a besoin du résultat CQE, des -flags, du buffer ID et du token encodé avant de réduire l'événement en callbacks de handler. +Un pont d'exécution peut consommer les CQE en mode Extended lorsque l'exécution abstraite a besoin des faits de +complétion. +Un environnement d'exécution par connexion peut aussi sonder directement les CQE Extended bruts lorsqu'il a besoin du +résultat CQE, des +fanions, de l'ID de tampon et du jeton encodé avant de réduire l'événement en rappels de gestionnaire. Les couches de contexte et d'exécution abstraite au-dessus de cette frontière ne modifient pas le rôle de frontière noyau de `uring`. @@ -278,11 +376,12 @@ noyau de `uring`. `uring` expose les mécanismes tournés vers le noyau ; l'ordonnancement, les tentatives de reprise, le suivi de connexions et l'interprétation du protocole relèvent des couches supérieures. Les patrons ci-dessous décrivent la -frontière qu'un runtime côté appelant doit préserver. +frontière qu'un environnement d'exécution côté appelant doit préserver. ### Boucle d'événements propriétaire du ring -En mode single-issuer (le mode par défaut), une seule goroutine sérialise toutes les opérations côté submit. Une boucle +En mode mono-émetteur (le mode par défaut), une seule goroutine sérialise toutes les opérations côté soumission. Une +boucle classique soumet le travail en attente, applique un `iox.Backoff` détenu par l'appelant quand `Wait` ne signale aucun progrès observable, puis distribue les complétions : @@ -298,11 +397,11 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { } n, err := ring.Wait(cqes) - if errors.Is(err, iox.ErrWouldBlock) { + switch iox.Classify(err) { + case iox.OutcomeWouldBlock: backoff.Wait() continue - } - if err != nil { + case iox.OutcomeFailure: return err } if n == 0 { @@ -319,14 +418,15 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { ``` Les méthodes du ring, dont `Send`, `Receive`, `AcceptMultishot` et `Wait`, s'exécutent sur cette goroutine. Le travail -provenant d'autres goroutines entre dans la boucle via un canal ou une file lock-free ; on n'appelle pas directement les -méthodes du ring depuis l'extérieur. `iox.Backoff` reste côté appelant : on appelle `backoff.Wait()` quand `Wait` -renvoie `iox.ErrWouldBlock` ou ne récupère aucun CQE, puis `backoff.Reset()` après tout lot avec `n > 0`. +provenant d'autres goroutines entre dans la boucle via un canal ou une file sans verrou ; on n'appelle pas directement +les +méthodes du ring depuis l'extérieur. `iox.Backoff` reste côté appelant : on appelle `backoff.Wait()` quand `Wait` se +classe comme `iox.OutcomeWouldBlock` ou ne récupère aucun CQE, puis `backoff.Reset()` après tout lot avec `n > 0`. ### Cycle de vie des souscriptions multishot Une opération multishot produit un flux de CQEs jusqu'à ce que le noyau envoie un CQE final (sans `IORING_CQE_F_MORE`). -Le code runtime côté appelant suit les souscriptions et gère la re-soumission : +Le code d'exécution côté appelant suit les souscriptions et gère la re-soumission : ```go handler := uring.NewMultishotSubscriber(). @@ -353,7 +453,8 @@ et la re-souscription conditionnelle. ### État par connexion via des contextes typés -Les contextes étendus transportent les références par connexion tout au long du cycle submit → complete, sans recourir à +Les contextes étendus transportent les références par connexion tout au long du cycle soumission → complétion, sans +recourir à une table de correspondance globale : ```go @@ -386,11 +487,12 @@ ring.PutExtSQE(ext) On maintient les racines de pointeurs Go actives accessibles en dehors de `UserData`. Le GC ne trace pas ces octets bruts. Le jeu de racines sidecar rattaché à chaque slot `ExtSQE` s'en charge pour les protocoles internes multishot et -listener, mais le code runtime appelant qui place des refs typés doit les garder accessibles de manière indépendante. +d'écouteur, mais le code d'exécution appelant qui place des refs typés doit les garder accessibles de manière +indépendante. ### Composition de délais -`LinkTimeout` attache un délai au SQE précédent via une chaîne `IOSQE_IO_LINK`. L'opération et le timeout entrent en +`LinkTimeout` attache un délai au SQE précédent via une chaîne `IOSQE_IO_LINK`. L'opération et le délai entrent en concurrence : l'un aboutit, l'autre est annulé. ```go @@ -408,7 +510,7 @@ if err := ring.LinkTimeout(timeoutCtx, 5*time.Second); err != nil { } ``` -La couche runtime appelante gère les deux issues : une réception réussie annule le timeout, et un timeout déclenché +La couche d'exécution appelante gère les deux issues : une réception réussie annule le délai, et un délai déclenché annule la réception. Les deux produisent des CQEs que la boucle de distribution doit observer. ## Parcours TCP courants @@ -448,8 +550,9 @@ if err != nil { defer recvSub.Cancel() ``` -`listener_example_test.go` couvre la mise en place du listener avec accept multishot, `examples/multishot_test.go` -détaille les CQE côté handler pour le multishot receive, et `examples/echo_test.go` illustre le parcours echo loopback +`listener_example_test.go` couvre la mise en place de l'écouteur avec accept multishot, `examples/multishot_test.go` +détaille les CQE côté gestionnaire pour le multishot receive, et `examples/echo_test.go` illustre le parcours echo +loopback complet. ### Client TCP @@ -484,28 +587,29 @@ if err := ring.Receive(recvCtx, &clientFD, buf); err != nil { On réutilise la boucle `Wait` décrite dans la section cycle de vie du ring après chaque soumission pour observer la complétion correspondante. Le fichier `socket_integration_linux_test.go` couvre le flux connect/send côté client TCP. -## Réception zero-copy (ZCRX) +## Réception sans copie (ZCRX) -`ZCRXReceiver` gère la réception zero-copy depuis une file RX matérielle de NIC via `io_uring`. +`ZCRXReceiver` gère la réception sans copie depuis une file RX matérielle de NIC via `io_uring`. `NewZCRXReceiver` est prévu pour les rings créés avec des CQE de 32 octets (`IORING_SETUP_CQE32`). La surface `Options` -actuelle n'expose pas ce drapeau de configuration ; par conséquent, les rings créés via le chemin standard de `New` +actuelle n'expose pas ce fanion de configuration ; par conséquent, les rings créés via le chemin standard de `New` conduisent ce constructeur à renvoyer `ErrNotSupported`. Tant qu'un chemin de configuration CQE32 n'est pas exposé, cette section documente le contrat de frontière du récepteur plutôt qu'une recette publique exécutable. ### Cycle de vie 1. Avec un ring compatible CQE32, on crée le récepteur via `NewZCRXReceiver`. Le constructeur enregistre la file - d'interface ZCRX, mappe la zone de remplissage et prépare le refill ring. + d'interface ZCRX, mappe la zone de remplissage et prépare le ring de remplissage. 2. Appelez `Start` pour soumettre l'opération étendue `RECV_ZC` sur le ring. -3. Sur le chemin de dispatch des CQE, les complétions ZCRX sont acheminées vers le `ZCRXHandler` : +3. Sur le chemin de distribution des CQE, les complétions ZCRX sont acheminées vers le `ZCRXHandler` : - `OnData` livre un `ZCRXBuffer` pointant vers la zone mappée par la NIC. On appelle `Release` une fois le traitement terminé pour restituer le slot au noyau. Renvoyer `false` demande un arrêt au mieux. - `OnError` livre les erreurs CQE. Renvoyer `false` demande un arrêt au mieux. - `OnStopped` s'exécute une fois lors du retrait terminal avant que l'état ne devienne `Stopped`. -4. On appelle `Stop` pour soumettre un async cancel. Le récepteur transite par `Stopping` → `Retiring` → `Stopped`. +4. On appelle `Stop` pour soumettre une annulation asynchrone. Le récepteur transite par `Stopping` → `Retiring` → + `Stopped`. 5. On interroge `Stopped` jusqu'à ce qu'il renvoie `true`, on arrête le ring propriétaire, puis on appelle `Close` pour - libérer la zone mappée et le mapping du refill ring. + libérer la zone mappée et le mappage du ring de remplissage. ### Machine à états @@ -515,11 +619,12 @@ Idle → Active → Stopping → Retiring → Stopped `Stop` revient à l'état `Active` si la soumission d'annulation échoue. `Close` est idempotent. -### Contrat du handler +### Contrat du gestionnaire -- `OnData` et `OnError` sont appelés en série depuis la goroutine de dispatch CQE. -- `Release` est mono-producteur : il ne doit être appelé que depuis la goroutine de dispatch. -- `Stop` ne doit être appelé que lorsqu'il est garanti non concurrent avec le dispatch CQE. Il s'agit d'un contrat de +- `OnData` et `OnError` sont appelés en série depuis la goroutine de distribution CQE. +- `Release` est mono-producteur : il ne doit être appelé que depuis la goroutine de distribution. +- `Stop` ne doit être appelé que lorsqu'il est garanti non concurrent avec la distribution CQE. Il s'agit d'un contrat + de sérialisation côté appelant. ## Exemples @@ -528,37 +633,41 @@ Les tests d'exemple dans `uring/examples/` illustrent l'API en situation concrè - `multishot_test.go`, accept multishot, réception multishot et arrêt d'abonnement - `file_io_test.go`, lectures, écritures et traitement par lots -- `fixed_buffers_test.go`, buffers enregistrés et I/O sur buffers fixes +- `fixed_buffers_test.go`, tampons enregistrés et I/O sur tampons fixes - `vectored_io_test.go`, opérations de lecture et écriture vectorisées -- `splice_tee_test.go`, transfert de données zero-copy avec splice et tee -- `zerocopy_test.go`, chemins d'envoi zero-copy et suivi des complétions +- `splice_tee_test.go`, transfert de données sans copie avec splice et tee +- `zerocopy_test.go`, chemins d'envoi sans copie et suivi des complétions - `poll_test.go`, flux de disponibilité par poll -- `buffer_ring_test.go`, fourniture de buffer rings et groupes de buffers multi-tailles +- `buffer_ring_test.go`, fourniture de rings de tampons et groupes de tampons multi-tailles - `context_test.go`, flux `SQEContext` direct, indirect et extended, plus accès via `CQEView` - `echo_test.go`, parcours serveur echo TCP et ping-pong UDP -- `timeout_linux_test.go`, opérations de timeout et linked-timeout +- `timeout_linux_test.go`, opérations de délai et de délai lié -Au niveau du package, `listener_example_test.go` couvre la création de listener et l'accept multishot, tandis que +Au niveau du package, `listener_example_test.go` couvre la création d'écouteur et l'accept multishot, tandis que `socket_integration_linux_test.go` couvre le flux client TCP connect/send. ## Notes opérationnelles - Activez `NotifySucceed` pour obtenir un CQE visible à chaque opération réussie. -- `ring.Features` indique le nombre effectif d'entrées SQ et CQ, la largeur des slots SQE, ainsi que l'ordre des octets +- `ring.Features` indique le nombre effectif d'entrées SQ et CQ, la largeur des emplacements SQE, ainsi que l'ordre des + octets utilisé par le package pour interpréter `user_data`. - Laissez `MultiIssuers` désactivé pour la configuration mono-émetteur par défaut (`SINGLE_ISSUER` + `DEFER_TASKRUN`), - dans laquelle un seul chemin d'exécution de l'appelant sérialise les opérations de submit-state (`submit`, `Wait`/ - `enter`, `Stop` et resize). N'activez ce drapeau que lorsque plusieurs goroutines nécessitent une soumission - concurrente ou un enter côté wait ; cela bascule le ring vers la configuration de soumission partagée `COOP_TASKRUN`. + dans laquelle un seul chemin d'exécution de l'appelant sérialise les opérations d'état de soumission (`submit`, + `Wait`/ + `enter`, `Stop` et resize). N'activez ce fanion que lorsque plusieurs goroutines nécessitent une soumission + concurrente ou une entrée côté attente ; cela bascule le ring vers la configuration de soumission partagée + `COOP_TASKRUN`. - `EpollWait` exige que `timeout` vaille `0` ; utilisez `LinkTimeout` si vous avez besoin d'une échéance. - Les vues de complétion empruntées et les contextes issus des pools doivent être libérés ou abandonnés sans délai. -- `ListenerOp.Close` ferme le FD du listener immédiatement. Si un CQE de mise en place est encore en attente, drainez-le +- `ListenerOp.Close` ferme le FD de l'écouteur immédiatement. Si un CQE de mise en place est encore en attente, + drainez-le d'abord, puis rappelez `Close` pour restituer le `ExtSQE` emprunté au pool. ## Support de plateforme `uring` cible Go 1.26+ et Linux 6.18+ pour le chemin réel adossé au noyau. La plupart des fichiers d'implémentation et -des tests d'exemple portent la directive `//go:build linux`. Les fichiers Darwin fournissent uniquement des stubs de +des tests d'exemple portent la directive `//go:build linux`. Les fichiers Darwin fournissent uniquement des bouchons de compilation pour la surface partagée ; les capacités propres à Linux restent propres à Linux et ne modifient en rien la base d'exécution Linux décrite ci-dessus. diff --git a/README.ja.md b/README.ja.md index c64f6d4..93c93c9 100644 --- a/README.ja.md +++ b/README.ja.md @@ -15,8 +15,7 @@ Go で Linux 6.18+ の `io_uring` カーネル境界を扱うパッケージ。 の解釈、`user_data` による発行元識別の伝達を担い、バッファ登録・マルチショット操作・リスナー構築の基本操作も備えています。 設計方針として、カーネル側の機構と完了結果の観測を API -境界に留め、スケジューリングやリトライなどの方針決定は上位層に委ねます。呼び出し側のランタイムコードが、完了の相関付け、retry/backoff、handler -と session のルーティング、コネクションライフサイクル、終端時のリソース解放を担当します。 +境界に留め、スケジューリングやリトライなどの方針決定は上位層に委ねます。呼び出し側のランタイムコードが、完了の相関付け、再試行とバックオフ、ハンドラとセッションのルーティング、コネクションライフサイクル、終端時のリソース解放を担当します。 主な API は以下のとおりです。 @@ -33,6 +32,8 @@ Go で Linux 6.18+ の `io_uring` カーネル境界を扱うパッケージ。 uname -r ``` +`uring` は 6.18+ のベースラインを前提とし、古いカーネル向けのフォールバック分岐を持ちません。そのため、古いカーネル向けの互換分岐ではなく、要件を満たすカーネルを起動してください。 + Debian 13 の stable カーネルはこの要件を満たさない場合があります。6.18 以上の Debian パッケージ版カーネルが必要な場合は、下記の [Debian 13 カーネル更新](#debian-13-カーネル更新) を参照してください。 @@ -42,7 +43,7 @@ go get code.hybscloud.com/uring ### Debian 13 カーネル更新 -Debian 13 の安定版トラックが提供するカーネルは 6.12 です。`trixie-backports` suite から Debian パッケージ済みの 6.18+ +Debian 13 の安定版トラックが提供するカーネルは 6.12 です。`trixie-backports` スイートから Debian パッケージ済みの 6.18+ カーネルを導入できます。手順の詳細は [SETUP.md](./SETUP.md) を参照してください。 ### トラブルシューティング @@ -53,7 +54,8 @@ Debian 13 の安定版トラックが提供するカーネルは 6.12 です。` ## リングのライフサイクル `New` は未開始状態のリングを返します。操作の発行に先立ち `Start` を呼び出してください。`New` がコンテキストプールを即座に構築し、 -`Start` がリングリソースの登録と有効化を行います。Linux 6.18+ を固定のベースラインとしており、それ以前のカーネル向けのフォールバックは持ちません。 +`Start` がリングリソースの登録と有効化を行います。次の例ではファイル read を発行し、対応する CQE を待ち、`iox.Classify` +によって `ErrWouldBlock` を失敗ではなく「進展なし」の意味として扱います。 ```go ring, err := uring.New(func(o *uring.Options) { @@ -66,28 +68,54 @@ if err != nil { if err := ring.Start(); err != nil { return err } +defer ring.Stop() -cqes := make([]uring.CQEView, 64) -n, err := ring.Wait(cqes) -if err != nil && !errors.Is(err, iox.ErrWouldBlock) { +fd := iofd.NewFD(int(file.Fd())) +buf := make([]byte, 4096) +ctx := uring.PackDirect(uring.IORING_OP_READ, 0, 0, 0).WithFD(fd) +if err := ring.Read(ctx, buf); err != nil { return err } -for i := range n { - cqe := cqes[i] - if cqe.Res < 0 { - return fmt.Errorf("completion failed: op=%d fd=%d res=%d", cqe.Op(), cqe.FD(), cqe.Res) +cqes := make([]uring.CQEView, 64) +var backoff iox.Backoff + +for { + n, err := ring.Wait(cqes) + switch iox.Classify(err) { + case iox.OutcomeWouldBlock: + backoff.Wait() + continue + case iox.OutcomeFailure: + return err + } + if n == 0 { + backoff.Wait() + continue + } + + backoff.Reset() + for i := range n { + cqe := cqes[i] + if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { + continue + } + if cqe.Res < 0 { + return fmt.Errorf("uring read failed: res=%d", cqe.Res) + } + handle(buf[:int(cqe.Res)]) + return nil } - fmt.Printf("completed op=%d on fd=%d with res=%d\n", cqe.Op(), cqe.FD(), cqe.Res) } ``` -`Wait` は未送信の SQE をフラッシュしてから完了を回収します。単一発行者リングでは、SQ が空になったあとも deferred task work -を駆動するための kernel enter を併せて発行します。呼び出し側は `Wait`/`enter` と他の submit-state 操作を直列化する必要があります。 -`iox.ErrWouldBlock` は、現時点で観測可能な完了がないことを示します。このエラーは `code.hybscloud.com/iox` で定義されています。 +`Wait` は未送信の SQE をフラッシュしてから完了を回収します。単一発行者リングでは、SQ が空になったあとも遅延タスクを進めるためのカーネル +enter も発行します。呼び出し側は `Wait`/`enter` と他の発行状態操作を直列化する必要があります。 +`Wait` が返した `err` を `iox.Classify(err)` で分類して `iox.OutcomeWouldBlock` になった場合、それは現時点で +境界上に観測可能な完了がないことを示します。 -`Start` と `Stop` がリングのライフサイクルを構成します。`Stop` は冪等ですが、 呼び出すとリングは恒久的に使用不能になります。in-flight -操作をすべて完了させ、 未回収の CQE を回収し、multishot サブスクリプションを停止してから 呼び出してください。 +`Start` と `Stop` がリングのライフサイクルを構成します。`Stop` は冪等ですが、呼び出すとリングは恒久的に使用不能になります。進行中の操作をすべて完了させ、未回収の +CQE を回収し、multishot サブスクリプションを停止してから呼び出してください。 ## 型と操作 @@ -105,7 +133,7 @@ for i := range n { | `ZCRXReceiver` | NIC RX キューからのゼロコピー受信ライフサイクルの管理 | | `ZCRXConfig` | ZCRX 受信インスタンスの設定 | | `ZCRXHandler` | ZCRX のデータ・エラー・シャットダウンに対するコールバックインターフェース | -| `ZCRXBuffer` | 配送済みゼロコピー受信ビュー。解放時にカーネルが自動 refill | +| `ZCRXBuffer` | 配送済みゼロコピー受信ビュー。解放時にカーネルが自動補充 | 操作メソッド: @@ -144,19 +172,19 @@ sqeCtx := uring.ForFD(fd). コンテキストには 3 つのモードがあります。 -| モード | 表現形式 | 主な用途 | -|----------|-----------------------|--------------------------| -| Direct | 64 ビット inline payload | 一般的な発行・回収経路。ゼロアロケーション | -| Indirect | `IndirectSQE` へのポインタ | 64 ビットでは SQE 全体を表現できない場合 | -| Extended | `ExtSQE` へのポインタ | 完全な SQE と 64 バイトのユーザーデータ | +| モード | 表現形式 | 主な用途 | +|----------|----------------------|--------------------------| +| Direct | 64 ビットのインライン負荷 | 一般的な発行・回収経路。ゼロアロケーション | +| Indirect | `IndirectSQE` へのポインタ | 64 ビットでは SQE 全体を表現できない場合 | +| Extended | `ExtSQE` へのポインタ | 完全な SQE と 64 バイトのユーザーデータ | 通常は `ForFD` または `PackDirect` から開始し、完了時に参照したい情報だけを付加します。`WithFlags` はフラグ集合全体を上書きするため、論理和は事前に計算してから渡してください。 direct の 64 ビット配置に収まらない独自メタデータが必要な場合は、`ExtSQE` を借用し、`Ctx*Of` や `ViewCtx*` で `UserData` に書き込んだうえで `SQEContext` に再パックします。`UserData` にはスカラー値を格納するのが望ましく、Go -ポインタ・interface・func・slice・string・map・chan やそれらを含む struct を raw overlay や typed view 経由で置く場合は、GC -が raw bytes を走査しないため、参照元を `UserData` の外に保持する必要があります。 +ポインタ、インターフェース値、関数値、スライス、文字列、マップ、チャネル、またはそれらを含む構造体を生のオーバーレイや型付きビュー経由で置く場合は、GC +が生バイトを走査しないため、参照元を `UserData` の外に保持する必要があります。 ```go ext := ring.ExtSQE() @@ -178,9 +206,15 @@ fmt.Printf("sqe context mode=%d seq=%d\n", sqeCtx.Mode(), meta.Val1) cqes := make([]uring.CQEView, 64) n, err := ring.Wait(cqes) -if err != nil && !errors.Is(err, iox.ErrWouldBlock) { +switch iox.Classify(err) { +case iox.OutcomeWouldBlock: + return iox.ErrWouldBlock +case iox.OutcomeFailure: return err } +if n == 0 { + return iox.ErrWouldBlock +} for i := 0; i < n; i++ { cqe := cqes[i] @@ -208,10 +242,13 @@ for i := 0; i < n; i++ { ## バッファ供給 -`uring` は 2 種類の受信バッファ戦略に対応しています。 +`uring` には実用上 3 つのバッファ経路があります。登録バッファはリング開始時に固定され、固定バッファのファイル I/O +で使います。提供バッファリングではカーネルが受信バッファを選び、選択されたバッファ ID を CQE に返します。バンドル受信は +1 つの CQE で連続した論理バッファ範囲を消費し、その範囲を `BundleIterator` で公開します。 -- `ReadBufferSize`・`ReadBufferNum` による固定サイズの provided buffer -- `MultiSizeBuffer` によるマルチサイズ buffer group +- `ReadBufferSize`・`ReadBufferNum` による固定サイズの提供バッファ +- `MultiSizeBuffer` によるマルチサイズバッファグループ +- `LockedBufferMem`・`RegisteredBuffer`・`ReadFixed`・`WriteFixed` による登録固定バッファ 多くの場合、設定ヘルパーから始めるのが簡単です。 @@ -222,9 +259,59 @@ ring, err := uring.New(func(o *uring.Options) { }) ``` -メモリ予算を明示的に指定したい場合は `OptionsForBudget` を、予算に対して選択される tier 構成を確認したい場合は +メモリ予算を明示的に指定したい場合は `OptionsForBudget` を、予算に対して選択される階層構成を確認したい場合は `BufferConfigForBudget` を使用してください。 +```go +cfg, scale := uring.BufferConfigForBudget(256 * uring.MiB) +fmt.Printf("buffer tiers=%+v scale=%d\n", cfg, scale) +``` + +固定バッファ I/O は登録済みバッファをインデックスで参照します。返されるスライスはリング所有のメモリなので、固定操作が完了するまで有効に保ってください。 + +```go +buf := ring.RegisteredBuffer(0) +copy(buf, payload) + +fd := iofd.NewFD(int(file.Fd())) +ctx := uring.PackDirect(uring.IORING_OP_WRITE_FIXED, 0, 0, 0).WithFD(fd) +if err := ring.WriteFixed(ctx, 0, len(payload)); err != nil { + return err +} +``` + +ソケット受信でカーネルのバッファ選択を使う場合は、受信バッファとして `nil` を渡し、必要なサイズクラスを指定します。完了 +CQE には選択されたバッファが記録されます。 + +```go +recvCtx := uring.PackDirect(uring.IORING_OP_RECV, 0, 0, 0) + +if err := ring.Receive(recvCtx, &socketFD, nil, uring.WithReadBufferSize(uring.BufferSizeSmall)); err != nil { + return err +} + +// 後で、Wait が対応する CQE を返したあと: +if cqe.HasBuffer() { + fmt.Printf("kernel selected group=%d id=%d\n", cqe.BufGroup(), cqe.BufID()) +} +``` + +バンドル受信は同じ提供バッファストレージを使いますが、1 つの CQE +で複数のバッファを消費することがあります。イテレータを処理したら、消費したスロットを回収してください。 + +```go +if err := ring.ReceiveBundle(recvCtx, &socketFD, uring.WithReadBufferSize(uring.BufferSizeSmall)); err != nil { + return err +} + +if it, ok := ring.BundleIterator(cqe, cqe.BufGroup()); ok { + for buf := range it.All() { + handle(buf) + } + it.Recycle(ring) +} +``` + 登録バッファにはピン留めされたメモリが必要です。大きなバッファの登録に失敗する場合は `RLIMIT_MEMLOCK` を引き上げるか、メモリ予算を小さくしてください。 @@ -244,18 +331,19 @@ CQE のルーティング方針はパッケージ外に委ねています。リ 2. `Start`: バッファを登録し、Linux 6.18+ ベースライン向けにリングを有効化する。 3. 操作メソッド: SQE を書き込んで操作の意図を発行する。 4. `Wait`: 未送信分をフラッシュし、借用された CQE 完了結果を返す。 -5. 呼び出し側ランタイムコード: スケジューリング、リトライ、parking、コネクション/session ルーティング、終端時のリソース方針を担う。 +5. 呼び出し側ランタイムコード: スケジューリング、リトライ、待機、コネクションとセッションのルーティング、終端時のリソース方針を担う。 この構造により、`uring` はカーネル境界の機構に専念しつつ、境界を越えた完了結果の意味を保持します。 ## ランタイム境界 `uring` の上位にあるランタイム層は、これをスケジューラではなくカーネルバックエンドとして扱うべきです。理想的な境界は一方向です。 -`uring` は SQE の準備、CQE の回収、`user_data` の保持、CQE `res` と flags の公開、所有権事実の報告を担います。呼び出し側ランタイムコードは、それらの観測を自身の -token と相関付け、retry/backoff を適用し、handler と session をルーティングし、submit をバッチ化し、終端リソースを解放します。 +`uring` は SQE の準備、CQE の回収、`user_data` の保持、CQE `res` とフラグの公開、所有権事実の報告を担います。呼び出し側ランタイムコードは、それらの観測を自身の +トークンと相関付け、再試行とバックオフを適用し、ハンドラとセッションをルーティングし、発行をバッチ化し、終端リソースを解放します。 抽象実行が完了事実を必要とする場合、ランタイムブリッジは Extended モード CQE を消費できます。コネクション単位のランタイムは、CQE -の結果、flags、buffer ID、エンコード済み token を必要とする場合、イベントを handler callback に還元する前に raw Extended CQE +の結果、フラグ、バッファ ID、エンコード済みトークンを必要とする場合、イベントをハンドラコールバックに還元する前に生の +Extended CQE を直接ポーリングできます。 この境界の上にあるコンテキスト層と抽象実行層は、`uring` のカーネル境界としての役割を変更しません。 @@ -266,7 +354,7 @@ token と相関付け、retry/backoff を適用し、handler と session をル ### リング所有イベントループ -シングルイシュアーモード(デフォルト)では、1 つの goroutine がすべての submit 側操作を直列化します。一般的なループは保留中のワークを発行し、 +シングルイシュアーモード(デフォルト)では、1 つの goroutine がすべての発行側操作を直列化します。一般的なループは保留中のワークを発行し、 `Wait` が観測可能な進展を返さないときは呼び出し側の `iox.Backoff` を適用し、完了を振り分けます。 ```go @@ -281,11 +369,11 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { } n, err := ring.Wait(cqes) - if errors.Is(err, iox.ErrWouldBlock) { + switch iox.Classify(err) { + case iox.OutcomeWouldBlock: backoff.Wait() continue - } - if err != nil { + case iox.OutcomeFailure: return err } if n == 0 { @@ -303,7 +391,7 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { `Send`、`Receive`、`AcceptMultishot`、`Wait` などのリングメソッドはすべてこの goroutine 上で実行します。他の goroutine からの作業はチャネルまたはロックフリーキューを経由してループに渡します。リングメソッドを直接呼び出してはなりません。 -`iox.Backoff` は呼び出し側が所有します。`Wait` が `iox.ErrWouldBlock` を返した場合、または CQE を 1 件も回収できなかった場合は +`iox.Backoff` は呼び出し側が所有します。`Wait` が `iox.OutcomeWouldBlock` に分類された場合、または CQE を 1 件も回収できなかった場合は `backoff.Wait()` を呼び、`n > 0` のバッチを回収できたら `backoff.Reset()` を呼びます。 ### マルチショットサブスクリプションのライフサイクル @@ -335,7 +423,7 @@ _, err := ring.AcceptMultishot(acceptCtx, handler.Handler()) ### 型付きコンテキストによるコネクション状態 -拡張コンテキストは、グローバルルックアップテーブルを介さずに、submit → complete のラウンドトリップ全体にわたってコネクション単位の参照を伝搬します。 +拡張コンテキストは、グローバルルックアップテーブルを介さずに、発行 → 完了のラウンドトリップ全体にわたってコネクション単位の参照を伝搬します。 ```go type ConnState struct { @@ -366,7 +454,7 @@ ring.PutExtSQE(ext) ``` ライブな Go ポインタルートは `UserData` の外側で到達可能に保ってください。GC はそれらの生バイトをトレースしません。内部のマルチショットおよびリスナープロトコルでは各 -`ExtSQE` スロットに付属するサイドカールートセットが処理しますが、型付き ref を配置するフレームワークコードは独自に到達可能性を維持する必要があります。 +`ExtSQE` スロットに付属するサイドカーのルートセットが処理しますが、型付き参照を配置するフレームワークコードは独自に到達可能性を維持する必要があります。 ### デッドライン合成 @@ -467,21 +555,21 @@ if err := ring.Receive(recvCtx, &clientFD, buf); err != nil { `ZCRXReceiver` は `io_uring` を通じて NIC ハードウェア RX キューからゼロコピー受信を行う仕組みを管理します。 `NewZCRXReceiver` は 32 バイト CQE(`IORING_SETUP_CQE32`)で作成されたリング向けに用意されています。現在の `Options` はこの -setup flag を公開していないため、標準の `New` で作成したリングに対してはこのコンストラクタは `ErrNotSupported` -を返します。CQE32 の setup 経路が公開されるまでは、この節は実行可能な公開セットアップ手順ではなく、レシーバの境界契約を説明するものです。 +設定フラグを公開していないため、標準の `New` で作成したリングに対してはこのコンストラクタは `ErrNotSupported` +を返します。CQE32 の設定経路が公開されるまでは、この節は実行可能な公開セットアップ手順ではなく、レシーバの境界契約を説明するものです。 ### ライフサイクル -1. CQE32 対応リング上で `NewZCRXReceiver` を生成する。コンストラクタが ZCRX インターフェースキューの登録、refill - 領域のマッピング、refill ring の準備を行う。 +1. CQE32 対応リング上で `NewZCRXReceiver` を生成する。コンストラクタが ZCRX インターフェースキューの登録、補充 + 領域のマッピング、補充リングの準備を行う。 2. `Start` を呼び出し、リング上で拡張 `RECV_ZC` 操作を発行する。 3. CQE のディスパッチ経路で ZCRX 完了が `ZCRXHandler` に届く。 - `OnData`: NIC マップ領域を指す `ZCRXBuffer` を受け取る。処理後に `Release` を呼んでカーネルにスロットを返却する。ベストエフォートでの停止を要求するには `false` を返す。 - `OnError`: CQE エラーを受け取る。ベストエフォートでの停止を要求するには `false` を返す。 - `OnStopped`: 状態が `Stopped` に移行する直前に一度だけ呼ばれる。 -4. `Stop` を呼び出して async cancel を発行する。レシーバは `Stopping` → `Retiring` → `Stopped` と遷移する。 -5. `Stopped` が `true` を返すまでポーリングし、リングを停止してから `Close` でマップ領域と refill ring のマッピングを解放する。 +4. `Stop` を呼び出して非同期キャンセルを発行する。レシーバは `Stopping` → `Retiring` → `Stopped` と遷移する。 +5. `Stopped` が `true` を返すまでポーリングし、リングを停止してから `Close` でマップ領域と補充リングのマッピングを解放する。 ### 状態機械 @@ -489,7 +577,7 @@ setup flag を公開していないため、標準の `New` で作成したリ Idle → Active → Stopping → Retiring → Stopped ``` -cancel の発行に失敗した場合、`Stop` は `Active` に復帰します。`Close` は冪等です。 +キャンセルの発行に失敗した場合、`Stop` は `Active` に復帰します。`Close` は冪等です。 ### ハンドラの規約 @@ -507,8 +595,8 @@ cancel の発行に失敗した場合、`Stop` は `Active` に復帰します - `vectored_io_test.go`: ベクトル化 read/write - `splice_tee_test.go`: splice・tee によるゼロコピーデータ転送 - `zerocopy_test.go`: ゼロコピー send と完了の追跡 -- `poll_test.go`: poll による readiness ワークフロー -- `buffer_ring_test.go`: バッファリング供給とマルチサイズ buffer group +- `poll_test.go`: poll による準備完了ワークフロー +- `buffer_ring_test.go`: バッファリング供給とマルチサイズバッファグループ - `context_test.go`: direct・indirect・extended の各 `SQEContext` フローと `CQEView` アクセス - `echo_test.go`: TCP echo サーバと UDP ping-pong - `timeout_linux_test.go`: タイムアウトとリンクタイムアウト @@ -521,7 +609,7 @@ TCP クライアントの connect/send フローをそれぞれ示していま - すべての成功操作に可視の CQE が必要な場合は `NotifySucceed` を有効にする。 - `ring.Features` は実際の SQ エントリ数・CQ エントリ数・SQE スロット幅・`user_data` 解釈時のバイト順を返す。 - 既定では `MultiIssuers` を無効のまま、単一発行者構成(`SINGLE_ISSUER` + `DEFER_TASKRUN`)を使用する。この構成では、呼び出し側が - 1 つの実行パスで submit-state 操作(`submit`・`Wait`/`enter`・`Stop`・resize)を直列化する。複数 goroutine から並行して + 1 つの実行パスで発行状態操作(`submit`・`Wait`/`enter`・`Stop`・resize)を直列化する。複数 goroutine から並行して submit や wait 側 enter を行う必要がある場合にのみ `MultiIssuers` を有効にし、`COOP_TASKRUN` 構成に切り替える。 - `EpollWait` の `timeout` は `0` のままにすること。期限が必要な場合は `LinkTimeout` を使う。 - 借用中の完了ビューやプール由来のコンテキストは速やかに解放すること。 @@ -530,8 +618,8 @@ TCP クライアントの connect/send フローをそれぞれ示していま ## 対応プラットフォーム -`uring` の実カーネルパスは Go 1.26+ / Linux 6.18+ を対象としています。 実装ファイルとテストの大半は `//go:build linux` -で保護されています。 Darwin 向けファイルは共有サーフェス向けのコンパイルスタブのみを提供します。 Linux 専用機能は引き続き +`uring` の実カーネルパスは Go 1.26+ / Linux 6.18+ を対象としています。実装ファイルとテストの大半は `//go:build linux` +で保護されています。Darwin 向けファイルは共有 API 面向けのコンパイルスタブのみを提供します。Linux 専用機能は引き続き Linux 専用であり、上述の Linux 実行時ベースラインを変えません。 ## ライセンス diff --git a/README.md b/README.md index 87c9e3f..2fbb892 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ Language: **English** | [简体中文](./README.zh-CN.md) | [Español](./README. ## Overview -`uring` handles the kernel-facing boundary for Linux `io_uring`. It creates and starts rings, prepares SQEs, decodes -CQEs, carries submission identity through `user_data`, and provides buffer registration, multishot operations, and -listener-setup primitives. +`uring` is the kernel-facing boundary for Linux `io_uring`. It creates and starts rings, prepares SQEs, decodes CQEs, +carries submission identity through `user_data`, and exposes buffer registration, multishot operations, and +listener-setup primitives without turning them into a scheduler. -`uring` draws a clear boundary: kernel-facing mechanics and observable completion facts live at the API edge; policy and -composition stay above it. Caller-side runtime code owns completion correlation, retry/backoff, handler and session +The package keeps the boundary explicit: kernel mechanics and observable completion facts live here; policy and +composition live above it. Caller-side runtime code owns completion correlation, retry/backoff, handler and session routing, connection lifecycle, and terminal resource release. The primary surfaces are: @@ -34,6 +34,9 @@ The primary surfaces are: uname -r ``` +`uring` assumes the 6.18+ baseline and carries no fallback branches for older kernels. Boot a supported kernel instead +of expecting compatibility shims inside this package. + Debian 13's stable kernel track may still be below 6.18. See [Debian 13 kernel upgrade](#debian-13-kernel-upgrade) for the backports path to a kernel that meets the requirement. @@ -54,8 +57,8 @@ Container runtimes block `io_uring` syscalls by default. See [SETUP.md](./SETUP. ## Ring lifecycle `New` returns an unstarted ring and eagerly constructs the context pools. Call `Start` before submitting operations; it -registers ring resources and enables the ring. `uring` assumes the 6.18+ baseline and carries no fallback branches for -older kernels. +registers ring resources and enables the ring. The example below submits a read, waits for the matching CQE, and uses +`iox.Classify` so `ErrWouldBlock` stays a semantic no-progress result rather than a failure. ```go ring, err := uring.New(func(o *uring.Options) { @@ -68,26 +71,51 @@ if err != nil { if err := ring.Start(); err != nil { return err } +defer ring.Stop() -cqes := make([]uring.CQEView, 64) -n, err := ring.Wait(cqes) -if err != nil && !errors.Is(err, iox.ErrWouldBlock) { +fd := iofd.NewFD(int(file.Fd())) +buf := make([]byte, 4096) +ctx := uring.PackDirect(uring.IORING_OP_READ, 0, 0, 0).WithFD(fd) +if err := ring.Read(ctx, buf); err != nil { return err } -for i := range n { - cqe := cqes[i] - if cqe.Res < 0 { - return fmt.Errorf("completion failed: op=%d fd=%d res=%d", cqe.Op(), cqe.FD(), cqe.Res) +cqes := make([]uring.CQEView, 64) +var backoff iox.Backoff + +for { + n, err := ring.Wait(cqes) + switch iox.Classify(err) { + case iox.OutcomeWouldBlock: + backoff.Wait() + continue + case iox.OutcomeFailure: + return err + } + if n == 0 { + backoff.Wait() + continue + } + + backoff.Reset() + for i := range n { + cqe := cqes[i] + if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { + continue + } + if cqe.Res < 0 { + return fmt.Errorf("uring read failed: res=%d", cqe.Res) + } + handle(buf[:int(cqe.Res)]) + return nil } - fmt.Printf("completed op=%d on fd=%d with res=%d\n", cqe.Op(), cqe.FD(), cqe.Res) } ``` `Wait` flushes pending submissions, then reaps completions. On single-issuer rings it also issues the kernel enter that keeps deferred task work moving once the SQ drains; the caller must serialize `Wait`/`enter` with submit-state -operations. `iox.ErrWouldBlock` signals that no completion is currently observable at the boundary. The error is defined -in `code.hybscloud.com/iox`. +operations. If `iox.Classify(err)` yields `iox.OutcomeWouldBlock`, no completion is currently observable at the +boundary. `Start` and `Stop` form the ring lifecycle pair. `Stop` is idempotent and renders the ring permanently unusable; call it only after you have drained all in-flight operations, reaped outstanding CQEs, and quiesced live multishot @@ -183,9 +211,15 @@ recover the original submission token. cqes := make([]uring.CQEView, 64) n, err := ring.Wait(cqes) -if err != nil && !errors.Is(err, iox.ErrWouldBlock) { +switch iox.Classify(err) { +case iox.OutcomeWouldBlock: + return iox.ErrWouldBlock +case iox.OutcomeFailure: return err } +if n == 0 { + return iox.ErrWouldBlock +} for i := 0; i < n; i++ { cqe := cqes[i] @@ -213,10 +247,13 @@ borrowed buffers must not outlive their documented lifetimes. ## Buffer provisioning -Two receive-buffer strategies are supported: +`uring` has three practical buffer paths. Registered buffers are pinned during ring setup and used by fixed-buffer file +I/O. Provided buffer rings let the kernel choose a receive buffer and report the selected buffer ID in the CQE. Bundle +receives consume a contiguous logical range of provided buffers and expose that range through `BundleIterator`. - fixed-size provided buffers through `ReadBufferSize` and `ReadBufferNum` - multi-size buffer groups through `MultiSizeBuffer` +- registered fixed buffers through `LockedBufferMem`, `RegisteredBuffer`, `ReadFixed`, and `WriteFixed` For most systems the configuration helpers are the easiest entry point: @@ -228,7 +265,58 @@ ring, err := uring.New(func(o *uring.Options) { ``` Use `OptionsForBudget` to start from an explicit memory budget, or `BufferConfigForBudget` to inspect the tier layout -chosen for a given budget. +chosen for a given budget: + +```go +cfg, scale := uring.BufferConfigForBudget(256 * uring.MiB) +fmt.Printf("buffer tiers=%+v scale=%d\n", cfg, scale) +``` + +Fixed-buffer I/O uses a registered buffer by index. The returned slice is ring-owned memory; keep it live until the +fixed operation completes: + +```go +buf := ring.RegisteredBuffer(0) +copy(buf, payload) + +fd := iofd.NewFD(int(file.Fd())) +ctx := uring.PackDirect(uring.IORING_OP_WRITE_FIXED, 0, 0, 0).WithFD(fd) +if err := ring.WriteFixed(ctx, 0, len(payload)); err != nil { + return err +} +``` + +For socket receive with kernel buffer selection, pass `nil` as the receive buffer and request the size class you want. +The completion reports which buffer was selected: + +```go +recvCtx := uring.PackDirect(uring.IORING_OP_RECV, 0, 0, 0) + +if err := ring.Receive(recvCtx, &socketFD, nil, uring.WithReadBufferSize(uring.BufferSizeSmall)); err != nil { + return err +} + +// Later, after Wait returns the matching CQE: +if cqe.HasBuffer() { + fmt.Printf("kernel selected group=%d id=%d\n", cqe.BufGroup(), cqe.BufID()) +} +``` + +Bundle receives use the same provided-buffer storage but may consume more than one buffer in a single CQE. Process the +iterator, then recycle the consumed slots: + +```go +if err := ring.ReceiveBundle(recvCtx, &socketFD, uring.WithReadBufferSize(uring.BufferSizeSmall)); err != nil { + return err +} + +if it, ok := ring.BundleIterator(cqe, cqe.BufGroup()); ok { + for buf := range it.All() { + handle(buf) + } + it.Recycle(ring) +} +``` Registered buffers require pinned memory. If large buffer registration fails, increase `RLIMIT_MEMLOCK` or use a smaller memory budget. @@ -289,11 +377,11 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { } n, err := ring.Wait(cqes) - if errors.Is(err, iox.ErrWouldBlock) { + switch iox.Classify(err) { + case iox.OutcomeWouldBlock: backoff.Wait() continue - } - if err != nil { + case iox.OutcomeFailure: return err } if n == 0 { @@ -311,8 +399,8 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { All ring methods, including `Send`, `Receive`, `AcceptMultishot`, and `Wait`, run on this goroutine. Work from other goroutines enters the loop through a channel or a lock-free queue, not by calling ring methods directly. `iox.Backoff` -stays caller-owned: call `backoff.Wait()` on `iox.ErrWouldBlock` or when `Wait` returns no CQEs, and `backoff.Reset()` -after any batch with `n > 0`. +stays caller-owned: call `backoff.Wait()` on `iox.OutcomeWouldBlock` or when `Wait` returns no CQEs, and +`backoff.Reset()` after any batch with `n > 0`. ### Multishot subscription lifecycle diff --git a/README.zh-CN.md b/README.zh-CN.md index d83f19d..d7353ca 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -14,8 +14,8 @@ Go `io_uring` 内核接口包,面向 Linux 6.18+。 `uring` 负责与 Linux 内核的 `io_uring` 接口交互:创建并启动 ring、填充 SQE、解码 CQE,借助 `user_data` 传递提交标识,并提供缓冲区注册、multishot 操作及监听器初始化等原语。 -设计原则是明确划分边界:内核侧的机制与可观测的完成事实留在 API 层面,调度策略与组合逻辑由上层负责。调用方运行时代码负责完成关联、retry/backoff、handler -与 session 路由、连接生命周期和终态资源释放。 +设计原则是明确划分边界:内核侧的机制与可观测的完成事实留在 API +层面,调度策略与组合逻辑由上层负责。调用方运行时代码负责完成关联、重试与退避、处理器与会话路由、连接生命周期以及终态资源释放。 核心类型: @@ -32,7 +32,9 @@ Go `io_uring` 内核接口包,面向 Linux 6.18+。 uname -r ``` -Debian 13 的 stable 源中内核版本可能低于此要求。如需升级,请参见下方 Debian 13 内核升级一节。 +`uring` 假定运行环境满足 6.18+ 基线,且不为旧内核保留任何回退分支。请启动满足要求的内核,而不是期待此包为旧内核提供兼容分支。 + +Debian 13 的稳定源中内核版本可能低于此要求。如需升级,请参见下方 Debian 13 内核升级一节。 ```bash go get code.hybscloud.com/uring @@ -40,7 +42,7 @@ go get code.hybscloud.com/uring ### Debian 13 内核升级 -Debian 13 稳定版提供的内核为 6.12。`trixie-backports` suite 提供经过 Debian 打包的 6.18+ +Debian 13 稳定版提供的内核为 6.12。`trixie-backports` 软件源提供经过 Debian 打包的 6.18+ 内核。详细步骤见 [SETUP.md](./SETUP.md)。 ### 常见问题排查 @@ -50,8 +52,8 @@ Ring 创建可能返回 `ENOMEM`、`EPERM` 或 `ENOSYS`,原因分别涉及 mem ## Ring 生命周期 -`New` 返回一个未启动的 ring,提交操作前须先调用 `Start`。`New` 会预先构建上下文池,`Start` 则负责注册资源并启用 ring。 -`uring` 固定以 Linux 6.18+ 为基线,启动路径中不包含低版本回退逻辑。 +`New` 返回一个未启动的 ring,提交操作前须先调用 `Start`。`New` 会预先构建上下文池,`Start` 则负责注册资源并启用 +ring。下面的示例提交一次文件读取,等待匹配的 CQE,并使用 `iox.Classify` 保持 `ErrWouldBlock` 的“暂无进展”语义,而不是把它当作失败处理。 ```go ring, err := uring.New(func(o *uring.Options) { @@ -64,28 +66,53 @@ if err != nil { if err := ring.Start(); err != nil { return err } +defer ring.Stop() -cqes := make([]uring.CQEView, 64) -n, err := ring.Wait(cqes) -if err != nil && !errors.Is(err, iox.ErrWouldBlock) { +fd := iofd.NewFD(int(file.Fd())) +buf := make([]byte, 4096) +ctx := uring.PackDirect(uring.IORING_OP_READ, 0, 0, 0).WithFD(fd) +if err := ring.Read(ctx, buf); err != nil { return err } -for i := range n { - cqe := cqes[i] - if cqe.Res < 0 { - return fmt.Errorf("completion failed: op=%d fd=%d res=%d", cqe.Op(), cqe.FD(), cqe.Res) +cqes := make([]uring.CQEView, 64) +var backoff iox.Backoff + +for { + n, err := ring.Wait(cqes) + switch iox.Classify(err) { + case iox.OutcomeWouldBlock: + backoff.Wait() + continue + case iox.OutcomeFailure: + return err + } + if n == 0 { + backoff.Wait() + continue + } + + backoff.Reset() + for i := range n { + cqe := cqes[i] + if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { + continue + } + if cqe.Res < 0 { + return fmt.Errorf("uring read failed: res=%d", cqe.Res) + } + handle(buf[:int(cqe.Res)]) + return nil } - fmt.Printf("completed op=%d on fd=%d with res=%d\n", cqe.Op(), cqe.FD(), cqe.Res) } ``` -`Wait` 先刷新待提交项,再回收完成事件。在单提交者 ring 上,它还会在 SQ 排空后向内核发起 enter 调用以推进 deferred task -work;调用方需保证 `Wait`/`enter` 与 submit-state 操作的串行执行。`iox.ErrWouldBlock` 表示当前没有可回收的完成事件,该错误定义在 -`code.hybscloud.com/iox` 中。 +`Wait` 先刷新待提交项,再回收完成事件。在单提交者 ring 上,它还会在 SQ 排空后向内核发起进入调用,以推进延迟任务执行;调用方需保证 +`Wait`/`enter` 与提交状态操作串行执行。当 `Wait` 返回的 `err` 经 `iox.Classify(err)` 分类为 +`iox.OutcomeWouldBlock` 时,表示当前边界上没有可观察到的完成事件。 -`Start` 与 `Stop` 是 ring 生命周期的配对操作。`Stop` 幂等但不可逆,调用后 ring 将永久不可用。调用 `Stop` 前,需确保所有 -in-flight 操作已完成、未处理的 CQE 已回收、活跃的 multishot 订阅已终止。 +`Start` 与 `Stop` 是 ring 生命周期的配对操作。`Stop` 幂等但不可逆,调用后 ring 将永久不可用。调用 `Stop` +前,需确保所有进行中的操作已完成、未处理的 CQE 已回收、活跃的 multishot 订阅已终止。 ## 类型与操作 @@ -149,8 +176,8 @@ sqeCtx := uring.ForFD(fd). 常见路径下,从 `ForFD` 或 `PackDirect` 开始,只填入完成时需要回溯的信息。`WithFlags` 会整体替换 flag 集合,调用前应先计算好联合值。 当 direct 的 64 位布局不足以携带所需元数据时,可从池中借用 `ExtSQE`,通过 `Ctx*Of` 或 `ViewCtx*` 写入 `UserData`,再打包为 -`SQEContext`。此处应优先使用标量 payload。若通过 raw overlay 或 typed view 存放了 Go 指针、interface、func -value、slice、string、map、chan 或含有这些成员的 struct,必须将 live root 保留在 `UserData` 之外,因为 GC 不会扫描这段原始字节。 +`SQEContext`。此处应优先使用标量负载。若通过原始覆盖视图或类型化视图存放了 Go 指针、接口值、函数值、切片、字符串、映射、通道或含有这些成员的结构体,必须将活跃根保留在 +`UserData` 之外,因为 GC 不会扫描这段原始字节。 ```go ext := ring.ExtSQE() @@ -171,9 +198,15 @@ fmt.Printf("sqe context mode=%d seq=%d\n", sqeCtx.Mode(), meta.Val1) cqes := make([]uring.CQEView, 64) n, err := ring.Wait(cqes) -if err != nil && !errors.Is(err, iox.ErrWouldBlock) { +switch iox.Classify(err) { +case iox.OutcomeWouldBlock: + return iox.ErrWouldBlock +case iox.OutcomeFailure: return err } +if n == 0 { + return iox.ErrWouldBlock +} for i := 0; i < n; i++ { cqe := cqes[i] @@ -200,10 +233,13 @@ for i := 0; i < n; i++ { ## 缓冲区供给 -`uring` 提供两种接收缓冲区策略: +`uring` 提供三类常用缓冲区路径:注册缓冲区在 ring 启动时固定并用于固定缓冲区文件 I/O;提供缓冲区环 +让内核在接收完成时选择缓冲区,并在 CQE 中返回缓冲区 ID;捆绑接收可以在一个 CQE 中消耗一段连续的逻辑缓冲区范围,并通过 +`BundleIterator` 暴露该范围。 -- 通过 `ReadBufferSize` 与 `ReadBufferNum` 配置固定尺寸提供缓冲区 +- 通过 `ReadBufferSize` 与 `ReadBufferNum` 配置固定尺寸的提供缓冲区 - 通过 `MultiSizeBuffer` 启用多尺寸缓冲区组 +- 通过 `LockedBufferMem`、`RegisteredBuffer`、`ReadFixed` 与 `WriteFixed` 使用注册固定缓冲区 多数场景下,直接使用配置辅助函数即可: @@ -214,7 +250,55 @@ ring, err := uring.New(func(o *uring.Options) { }) ``` -需要从显式内存预算出发时使用 `OptionsForBudget`;需要查看某预算对应的分层布局时使用 `BufferConfigForBudget`。 +需要从显式内存预算出发时使用 `OptionsForBudget`;需要查看某预算对应的分层布局时使用 `BufferConfigForBudget`: + +```go +cfg, scale := uring.BufferConfigForBudget(256 * uring.MiB) +fmt.Printf("buffer tiers=%+v scale=%d\n", cfg, scale) +``` + +Fixed-buffer I/O 通过索引使用注册缓冲区。返回的切片属于 ring 内存;在 fixed 操作完成前保持其有效: + +```go +buf := ring.RegisteredBuffer(0) +copy(buf, payload) + +fd := iofd.NewFD(int(file.Fd())) +ctx := uring.PackDirect(uring.IORING_OP_WRITE_FIXED, 0, 0, 0).WithFD(fd) +if err := ring.WriteFixed(ctx, 0, len(payload)); err != nil { + return err +} +``` + +Socket 接收若要使用内核缓冲区选择,传入 `nil` 作为接收缓冲区,并指定所需的尺寸级别。完成事件会报告内核选择的缓冲区: + +```go +recvCtx := uring.PackDirect(uring.IORING_OP_RECV, 0, 0, 0) + +if err := ring.Receive(recvCtx, &socketFD, nil, uring.WithReadBufferSize(uring.BufferSizeSmall)); err != nil { + return err +} + +// 稍后,在 Wait 返回匹配 CQE 后: +if cqe.HasBuffer() { + fmt.Printf("kernel selected group=%d id=%d\n", cqe.BufGroup(), cqe.BufID()) +} +``` + +捆绑接收使用同一套提供缓冲区存储,但一个 CQE 可能消耗多个缓冲区。处理迭代器后,将消耗的槽位回收: + +```go +if err := ring.ReceiveBundle(recvCtx, &socketFD, uring.WithReadBufferSize(uring.BufferSizeSmall)); err != nil { + return err +} + +if it, ok := ring.BundleIterator(cqe, cqe.BufGroup()); ok { + for buf := range it.All() { + handle(buf) + } + it.Recycle(ring) +} +``` 注册缓冲区需要锁定内存。若大规模注册失败,可提高 `RLIMIT_MEMLOCK` 或缩减预算。 @@ -234,18 +318,17 @@ CQE 路由策略由调用方自行实现,不在本包范围内。监听器的 2. `Start` 注册缓冲区并启用 ring(固定 Linux 6.18+ 基线)。 3. 操作方法通过写入 SQE 表达提交意图。 4. `Wait` 刷新提交并返回借用式 CQE。 -5. 调用方运行时代码负责调度、重试、挂起、连接/session 路由和终态资源策略。 +5. 调用方运行时代码负责调度、重试、挂起、连接与会话路由以及终态资源策略。 如此分工使 `uring` 专注于内核侧机制,同时在边界上保持清晰的完成语义。 ## 运行时边界 `uring` 之上的运行时层应将其用作内核后端,而不是调度器。理想边界是单向的:`uring` 准备 SQE、回收 CQE、保留 `user_data`、暴露 -CQE `res` 与 flags,并报告所有权事实;调用方运行时代码将这些观测与自身 token 关联,应用 retry/backoff,路由 handler 与 -session,批量提交,并释放终态资源。 +CQE `res` 与标志,并报告所有权事实;调用方运行时代码将这些观测与自身令牌关联,应用重试与退避,路由处理器与会话,批量提交,并释放终态资源。 -当抽象执行需要完成事实时,运行时桥接层可以消费 Extended 模式 CQE。连接级运行时也可以在需要 CQE 结果、flags、buffer ID 和编码 -token 时直接轮询 raw Extended CQE,然后再将事件归约为 handler callback。 +当抽象执行需要完成事实时,运行时桥接层可以消费 Extended 模式 CQE。连接级运行时也可以在需要 CQE 结果、标志、缓冲区 ID 和编码 +令牌时直接轮询原始 Extended CQE,然后再将事件归约为处理器回调。 位于该边界之上的上下文层与抽象执行层不会改变 `uring` 的内核边界职责。 @@ -255,7 +338,7 @@ token 时直接轮询 raw Extended CQE,然后再将事件归约为 handler cal ### Ring 所有者事件循环 -在单发行者模式(默认)下,一个 goroutine 串行化所有 submit 侧操作。典型循环为:发行待处理工作,在 `Wait` 没有返回可观察进展时使用调用方持有的 +在单发行者模式(默认)下,一个 goroutine 串行化所有提交侧操作。典型循环为:发行待处理工作,在 `Wait` 没有返回可观察进展时使用调用方持有的 `iox.Backoff`,然后分发完成事件。 ```go @@ -270,11 +353,11 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { } n, err := ring.Wait(cqes) - if errors.Is(err, iox.ErrWouldBlock) { + switch iox.Classify(err) { + case iox.OutcomeWouldBlock: backoff.Wait() continue - } - if err != nil { + case iox.OutcomeFailure: return err } if n == 0 { @@ -291,7 +374,7 @@ func runLoop(ring *uring.Uring, stop <-chan struct{}) error { ``` 所有 ring 方法,包括 `Send`、`Receive`、`AcceptMultishot` 和 `Wait`,均在该 goroutine 上执行。来自其他 goroutine 的工作通过 -channel 或无锁队列进入循环,不可直接调用 ring 方法。`iox.Backoff` 由调用方持有:`Wait` 返回 `iox.ErrWouldBlock`,或一次 +通道或无锁队列进入循环,不可直接调用 ring 方法。`iox.Backoff` 由调用方持有:`Wait` 分类为 `iox.OutcomeWouldBlock`,或一次 `Wait` 没有回收到任何 CQE 时,调用 `backoff.Wait()`;回收到 `n > 0` 的 CQE 批次后,调用 `backoff.Reset()`。 ### Multishot 订阅生命周期 @@ -322,7 +405,7 @@ _, err := ring.AcceptMultishot(acceptCtx, handler.Handler()) ### 类型化上下文承载连接状态 -扩展上下文通过 submit → complete 往返全程携带连接级引用,无需全局查找表。 +扩展上下文通过提交 → 完成往返全程携带连接级引用,无需全局查找表。 ```go type ConnState struct { @@ -352,8 +435,8 @@ seq := ctx.Val1 ring.PutExtSQE(ext) ``` -活跃的 Go 指针根须在 `UserData` 之外保持可达。GC 不会追踪裸字节。内部 multishot 和 listener 协议由每个 `ExtSQE` 槽上的 -sidecar 根集处理,但放置类型化 ref 的框架代码须自行维护可达性。 +活跃的 Go 指针根须在 `UserData` 之外保持可达。GC 不会追踪裸字节。内部 multishot 和监听器协议由每个 `ExtSQE` 槽上的 +旁路根集处理,但放置类型化引用的框架代码须自行维护可达性。 ### 截止时间组合 @@ -413,7 +496,7 @@ if err != nil { defer recvSub.Cancel() ``` -`listener_example_test.go` 演示监听器创建与 multishot accept;`examples/multishot_test.go` 演示 handler 侧的 multishot +`listener_example_test.go` 演示监听器创建与 multishot accept;`examples/multishot_test.go` 演示处理器侧的 multishot receive CQE 处理流程;`examples/echo_test.go` 则给出更完整的 loopback echo 示例。 ### TCP 客户端 @@ -451,12 +534,12 @@ connect/send 流程。 `ZCRXReceiver` 管理通过 `io_uring` 从 NIC 硬件 RX 队列进行的零拷贝接收。 -`NewZCRXReceiver` 面向以 32 字节 CQE(`IORING_SETUP_CQE32`)创建的 ring。当前 `Options` 尚未暴露该 setup flag,因此通过标准 -`New` 创建的 ring 会使该构造器返回 `ErrNotSupported`。在 CQE32 setup 路径公开前,本节记录的是接收器边界契约,而不是可直接执行的公开设置流程。 +`NewZCRXReceiver` 面向以 32 字节 CQE(`IORING_SETUP_CQE32`)创建的 ring。当前 `Options` 尚未暴露该设置标志,因此通过标准 +`New` 创建的 ring 会使该构造器返回 `ErrNotSupported`。在 CQE32 设置路径公开前,本节记录的是接收器边界契约,而不是可直接执行的公开设置流程。 ### 生命周期 -1. 在支持 CQE32 的 ring 上调用 `NewZCRXReceiver` 创建接收器。构造器会注册 ZCRX 接口队列、映射 refill 区域并准备 refill +1. 在支持 CQE32 的 ring 上调用 `NewZCRXReceiver` 创建接收器。构造器会注册 ZCRX 接口队列、映射补充区域并准备补充 ring。 2. 调用 `Start`,在 ring 上提交扩展 `RECV_ZC` 操作。 3. CQE 分发时,ZCRX 完成事件路由至 `ZCRXHandler`: @@ -464,7 +547,7 @@ connect/send 流程。 - `OnError` 交付 CQE 错误。返回 `false` 请求尽力停止。 - `OnStopped` 在进入 `Stopped` 前的终态退出阶段调用一次。 4. 调用 `Stop` 提交异步取消,接收器依次经历 `Stopping` → `Retiring` → `Stopped`。 -5. 轮询 `Stopped` 直至返回 `true`,停止所属 ring,再调用 `Close` 释放映射区域和 refill ring。 +5. 轮询 `Stopped` 直至返回 `true`,停止所属 ring,再调用 `Close` 释放映射区域和补充 ring。 ### 状态机 @@ -474,11 +557,11 @@ Idle → Active → Stopping → Retiring → Stopped 取消提交失败时,`Stop` 回退至 `Active`。`Close` 幂等。 -### Handler 契约 +### 处理器契约 -- `OnData` 与 `OnError` 在 CQE dispatch goroutine 中串行调用。 -- `Release` 为单生产者操作,仅限在 dispatch goroutine 中调用。 -- 调用 `Stop` 时须保证与 CQE dispatch 不并发,这是调用方侧的串行化约定。 +- `OnData` 与 `OnError` 在 CQE 分发 goroutine 中串行调用。 +- `Release` 为单生产者操作,仅限在分发 goroutine 中调用。 +- 调用 `Stop` 时须保证与 CQE 分发不并发,这是调用方侧的串行化约定。 ## 示例 @@ -492,7 +575,7 @@ Idle → Active → Stopping → Retiring → Stopped - `zerocopy_test.go`,零拷贝发送路径与完成跟踪 - `poll_test.go`,基于 poll 的就绪通知 - `buffer_ring_test.go`,缓冲区环供给与多尺寸缓冲区组 -- `context_test.go`,`SQEContext` 的 direct、indirect、extended 模式及 `CQEView` 访问 +- `context_test.go`,`SQEContext` 的 Direct、Indirect、Extended 模式及 `CQEView` 访问 - `echo_test.go`,TCP echo 服务器与 UDP ping-pong 流程 - `timeout_linux_test.go`,超时与链式超时操作 @@ -504,16 +587,16 @@ connect/send 流程。 - 若需为每个成功操作生成可见的 CQE,启用 `NotifySucceed`。 - `ring.Features` 报告实际 SQ/CQ 条目数、SQE 槽宽以及本包解析 `user_data` 的字节序。 - 默认不启用 `MultiIssuers`,此时采用单提交者配置(`SINGLE_ISSUER` + `DEFER_TASKRUN`),由调用方的单一执行路径串行化 - submit-state 操作(`submit`、`Wait`/`enter`、`Stop` 及 resize)。仅当多个 goroutine 需并发提交或执行 wait 侧 enter 时才启用 + 提交状态操作(`submit`、`Wait`/`enter`、`Stop` 及 resize)。仅当多个 goroutine 需并发提交或执行等待侧进入时才启用 `MultiIssuers`,这会切换为共享提交的 `COOP_TASKRUN` 配置。 - `EpollWait` 要求 `timeout` 为 `0`;如需设置截止时间,使用 `LinkTimeout`。 - 借用式完成视图与池化上下文应及时释放。 -- `ListenerOp.Close` 会立即关闭监听 FD。若仍有 setup CQE 待处理,需先回收该 CQE,再调用 `Close` 将借用的 `ExtSQE` 归还池中。 +- `ListenerOp.Close` 会立即关闭监听 FD。若仍有设置 CQE 待处理,需先回收该 CQE,再调用 `Close` 将借用的 `ExtSQE` 归还池中。 ## 平台支持 `uring` 的真实内核路径目标为 Go 1.26+ / Linux 6.18+。大部分实现文件和示例测试由 `//go:build linux` 约束。Darwin 文件只为共享 -API 表面提供编译 stub;Linux 专属能力仍然仅限 Linux,不改变上述 Linux 运行时基线。 +API 表面提供编译桩;Linux 专属能力仍然仅限 Linux,不改变上述 Linux 运行时基线。 ## 许可证 diff --git a/doc.go b/doc.go index 64cd49b..ddff635 100644 --- a/doc.go +++ b/doc.go @@ -3,36 +3,66 @@ // license that can be found in the LICENSE file. // Package uring provides the kernel-boundary `io_uring` surface for Linux 6.18+. -// Its core Linux `io_uring` implementation was refactored from +// `uring` assumes the 6.18+ baseline and carries no fallback branches for older +// kernels. Its core Linux `io_uring` implementation was refactored from // `code.hybscloud.com/sox` into this dedicated package. It prepares SQEs, // decodes CQEs, transports submission context through `user_data`, and exposes // kernel-boundary facts. Dispatch, retry, completion correlation, and // connection/session orchestration stay in caller-side runtime code above this // boundary. // -// // Create TCP socket -// socketCtx := uring.PackDirect(uring.IORING_OP_SOCKET, 0, 0, 0) -// ring.TCP4Socket(socketCtx) +// A typical caller starts the ring, submits an operation, and then treats the +// completion queue as the source of truth for kernel results. Semantic +// no-progress conditions such as [iox.ErrWouldBlock] are classified through +// [iox.Classify] instead of being treated as ordinary failures. // -// // Submit low-level multishot accept (one SQE, multiple CQEs) -// acceptCtx := uring.PackDirect(uring.IORING_OP_ACCEPT, 0, 0, listenerFD) -// ring.SubmitAcceptMultishot(acceptCtx) -// -// // Start multishot receive with buffer selection -// recvCtx := uring.ForFD(clientFD).WithBufGroup(bufGroupID) -// sub, err := ring.ReceiveMultishot(recvCtx, recvHandler) +// ring, err := uring.New(func(opt *uring.Options) { +// opt.Entries = uring.EntriesMedium +// }) +// if err != nil { +// return err +// } +// if err := ring.Start(); err != nil { +// return err +// } +// defer ring.Stop() // -// Completions return the kernel result together with the submission context. +// fd := iofd.NewFD(int(file.Fd())) +// buf := make([]byte, 4096) +// ctx := uring.PackDirect(uring.IORING_OP_READ, 0, 0, 0).WithFD(fd) +// if err := ring.Read(ctx, buf); err != nil { +// return err +// } // // cqes := make([]uring.CQEView, 64) -// n, err := ring.Wait(cqes) // Poll CQ, returns iox.ErrWouldBlock if empty +// var backoff iox.Backoff +// +// for { +// n, err := ring.Wait(cqes) +// switch iox.Classify(err) { +// case iox.OutcomeWouldBlock: +// backoff.Wait() +// continue +// case iox.OutcomeFailure: +// return err +// } +// if n == 0 { +// backoff.Wait() +// continue +// } // -// for i := range n { -// cqe := cqes[i] -// if cqe.Res < 0 { -// return fmt.Errorf("completion failed: op=%d fd=%d res=%d", cqe.Op(), cqe.FD(), cqe.Res) +// backoff.Reset() +// for i := range n { +// cqe := cqes[i] +// if cqe.Op() != uring.IORING_OP_READ || cqe.FD() != fd { +// continue +// } +// if cqe.Res < 0 { +// return fmt.Errorf("uring read failed: res=%d", cqe.Res) +// } +// handle(buf[:int(cqe.Res)]) +// return nil // } -// fmt.Printf("completed op=%d on fd=%d with res=%d\n", cqe.Op(), cqe.FD(), cqe.Res) // } // // [Uring.SubmitAcceptMultishot], [Uring.SubmitReceiveMultishot], and @@ -161,12 +191,60 @@ // // Buffer groups enable kernel-side buffer selection for receive operations. // The kernel picks an available buffer from the group at completion time; -// userspace does not select or assign buffers per receive. +// userspace does not select or assign buffers per receive. The package exposes +// three practical buffer-management paths: registered fixed buffers for +// fixed-buffer file I/O, provided buffers selected by the kernel, and bundle +// receives over contiguous ranges of provided buffers. // // opts := uring.OptionsForBudget(256 * uring.MiB) -// ring, _ := uring.New(func(opt *uring.Options) { +// ring, err := uring.New(func(opt *uring.Options) { // *opt = opts // }) +// if err != nil { +// return err +// } +// +// cfg, scale := uring.BufferConfigForBudget(256 * uring.MiB) +// fmt.Printf("buffer tiers=%+v scale=%d\n", cfg, scale) +// +// A registered fixed buffer is ring-owned memory addressed by index. Keep the +// buffer live until the fixed operation completes. +// +// fd := iofd.NewFD(int(file.Fd())) +// buf := ring.RegisteredBuffer(0) +// copy(buf, payload) +// +// writeCtx := uring.PackDirect(uring.IORING_OP_WRITE_FIXED, 0, 0, 0).WithFD(fd) +// if err := ring.WriteFixed(writeCtx, 0, len(payload)); err != nil { +// return err +// } +// +// For socket receive with kernel buffer selection, pass nil as the receive +// buffer and request the desired read-buffer size class. The matching CQE +// reports which buffer group and buffer ID were consumed. +// +// recvCtx := uring.PackDirect(uring.IORING_OP_RECV, 0, 0, 0) +// if err := ring.Receive(recvCtx, &socketFD, nil, uring.WithReadBufferSize(uring.BufferSizeSmall)); err != nil { +// return err +// } +// +// if cqe.HasBuffer() { +// fmt.Printf("kernel selected group=%d id=%d\n", cqe.BufGroup(), cqe.BufID()) +// } +// +// Bundle receives may consume more than one provided buffer in one CQE. +// Process the iterator and then recycle the consumed slots. +// +// if err := ring.ReceiveBundle(recvCtx, &socketFD, uring.WithReadBufferSize(uring.BufferSizeSmall)); err != nil { +// return err +// } +// +// if it, ok := ring.BundleIterator(cqe, cqe.BufGroup()); ok { +// for buf := range it.All() { +// handle(buf) +// } +// it.Recycle(ring) +// } // // # Supported Operations //