Jens Axboe propone intercambiar identidades de hilos en io_uring
El mantenedor de io_uring publica un RFC que evita el traspaso a un hilo trabajador salvo que la operación se bloquee de verdad, intercambiando la identidad de dos hilos dentro del kernel.
El mantenedor de io_uring, Jens Axboe, ha publicado un conjunto de parches RFC para resolver uno de los problemas clásicos del subsistema: qué hacer cuando una operación enviada desde el anillo resulta bloquear. Su propuesta pasa por intercambiar la identidad de dos hilos en pleno vuelo, algo que él mismo plantea como radical y potencialmente peligroso.
El traspaso a un worker no es gratis
io_uring promete ejecución asíncrona: la API se apoya en que una llamada a io_uring_enter() no bloquea salvo que se pida de forma explícita. Sostener esa garantía nunca ha sido fácil, porque muchas rutas del kernel se diseñaron sin pensar en ejecución asíncrona. fdatasync(), statx(), algunas rutas de openat() —el flag O_NONBLOCK incluido— y otras operaciones equivalentes no tienen soporte asíncrono por dentro.
Hoy, cualquier operación que pueda bloquear se delega a un hilo trabajador. Así el hilo que envía sigue su camino y el worker se bloquea si hace falta. El problema es el coste fijo: despertar ese hilo y hacer un cambio de contexto. Si la operación acaba bloqueándose, el gasto se diluye. Si no se bloquea, y pasa a menudo, el overhead se come buena parte del tiempo total. Y quien recurre a io_uring suele hacerlo buscando rendimiento.
La otra salida sería reescribir todas las rutas de llamadas al sistema para que no bloqueen. Se ha hecho en algunos casos con los años, pero es una tarea enorme y lenta.
Intercambio de identidades
La vía de Axboe es dejar que la operación continúe y detectar el bloqueo cuando ocurre de verdad. Para eso añade un flag, PF_IO_HANDOFF, en el campo de flags de task_struct (la definición en el árbol del kernel). Si un hilo marcado con ese flag está a punto de bloquearse por cualquier motivo, el planificador llama a io_uring_task_sleeping(), que avisa a io_uring. Engancharse al planificador permite enterarse del bloqueo en cualquier punto del kernel sin tocar cada ruta.
Con el aviso en la mano, io_uring toma un hilo de su pool y, en vez de cederle el trabajo —imposible a esas alturas—, intercambia las identidades. El worker pasa a parecer el hilo que hizo la llamada, con su thread ID, su gestión de señales y todo lo demás, y termina de procesar el anillo antes de volver a espacio de usuario. El hilo original adopta la identidad del worker, se bloquea como habría hecho, y al despertar completa la operación y ocupa su sitio en el pool. El coste del worker solo se paga cuando hay bloqueo real.
El propio Axboe señala el riesgo: antes de intercambiar dos task_struct hay que garantizar que nadie más en el kernel guarda referencias a esas estructuras. Si alguna queda suelta, se acabará trabajando con el task_struct equivocado. Es la parte que el RFC tendrá que resolver en la lista de correo.
Por qué importa
io_uring está en el corazón de buena parte del I/O asíncrono moderno, de bases de datos a servidores web, y su rendimiento depende de detalles como este. Que el coste de un worker se pague solo cuando hace falta separa una promesa de una medición. Queda por ver si el intercambio de identidades sobrevive a la revisión, porque lo de las referencias al task_struct no es un asunto menor.
