URBs: la unidad de transferencia asíncrona
El URB (USB Request Block) como descriptor asíncrono de una transferencia, análogo al bio del block layer. Reservarlo con usb_alloc_urb, rellenarlo con usb_fill_bulk_urb, enviarlo con usb_submit_urb y procesar el resultado en el callback de completado que corre en contexto de interrupción. Buffers coherentes, cancelación con usb_kill_urb y anchors.
Nunca escribes bytes directamente en un cable USB. Rellenas un URB —un USB Request Block— que describe una transferencia, se lo entregas a usbcore con usb_submit_urb y te marchas. Cuando el host controller termina, te llama de vuelta a tu callback de completado. El URB es a USB lo que el bio es al block layer: el descriptor asíncrono de una operación de E/S, la pieza donde el DMA, las interrupciones y el contrato del driver se dan la mano.
- Entender el URB como descriptor asíncrono de una transferencia y su ciclo de vida.
- Reservar y rellenar un URB con
usb_alloc_urbyusb_fill_bulk_urb. - Enviarlo con
usb_submit_urby procesar el resultado en el callback de completado. - Gestionar buffers coherentes con DMA y la cancelación con
usb_kill_urby anchors.
El URB: anatomía y ciclo de vida
Un URB es una estructura que reúne todo lo que define una transferencia: a qué dispositivo y por qué tubería, dónde está el buffer y cuánto mide, y a quién avisar al terminar. Es un objeto con conteo de referencias (kref) y asíncrono por naturaleza: enviarlo no bloquea. Su ciclo de vida es un rombo: reservar, rellenar, enviar, y en algún momento futuro el HCD invoca tu callback, tras lo cual liberas el URB o lo reenvías.
/* include/linux/usb.h — los campos publicos que maneja un driver */
struct urb {
struct usb_device *dev; /* (in) dispositivo destino */
unsigned int pipe; /* (in) endpoint mas direccion mas tipo */
int status; /* (out) resultado, 0 = exito */
unsigned int transfer_flags; /* (in) URB_NO_TRANSFER_DMA_MAP... */
void *transfer_buffer; /* (in) buffer de datos */
dma_addr_t transfer_dma; /* (in) su direccion de bus */
u32 transfer_buffer_length; /* (in) bytes a transferir */
u32 actual_length; /* (out) bytes realmente movidos */
usb_complete_t complete; /* (in) callback de completado */
void *context; /* (in) dato privado para el callback */
/* ... campos de iso y de uso interno del core ... */
};
El campo pipe es un entero compacto que codifica el endpoint, su dirección y su tipo; se construye con macros como usb_rcvbulkpipe. Los campos marcados (out) los rellena el núcleo al completar: status con el veredicto y actual_length con cuántos bytes se movieron de verdad, que puede ser menos que lo pedido (una lectura corta).
Reservar y rellenar
Se reserva con usb_alloc_urb, cuyo primer argumento es el número de paquetes isócronos (cero para el resto de tipos). El buffer conviene pedirlo con usb_alloc_coherent, que devuelve memoria ya lista para DMA y su dirección de bus en transfer_dma, ahorrándote el mapeo de la DMA API (nivel 27). Luego un helper usb_fill_* rellena los campos según el tipo de transferencia:
urb = usb_alloc_urb(0, GFP_KERNEL); /* 0 = sin paquetes iso */
if (!urb)
return -ENOMEM;
buf = usb_alloc_coherent(udev, len, GFP_KERNEL, &urb->transfer_dma);
if (!buf) {
usb_free_urb(urb);
return -ENOMEM;
}
usb_fill_bulk_urb(urb, udev,
usb_rcvbulkpipe(udev, dev->bulk_in_addr),
buf, len, skel_read_complete, dev);
urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; /* el buffer ya es coherente */
La bandera URB_NO_TRANSFER_DMA_MAP le dice al núcleo que no vuelva a mapear el buffer, porque usb_alloc_coherent ya te dio una dirección de bus válida. Es la conexión directa entre este nivel y la DMA API: el URB lleva tanto el puntero del kernel (transfer_buffer) como la dirección que ve el hardware (transfer_dma).
Enviar y el callback de completado
usb_submit_urb encola la transferencia y retorna de inmediato; el argumento GFP describe si puede dormir para reservar recursos. A partir de ese momento el URB pertenece al núcleo y no debes tocarlo hasta que te lo devuelvan.
ret = usb_submit_urb(urb, GFP_KERNEL);
if (ret) {
dev_err(&intf->dev, "submit fallo: %d\n", ret);
usb_free_coherent(udev, len, buf, urb->transfer_dma);
usb_free_urb(urb);
}
El callback corre en contexto de interrupción (softirq del HCD), y esa única frase gobierna todo lo que puedes hacer dentro: no dormir, no reservar con GFP_KERNEL, no tomar un mutex. Lo primero es examinar urb->status, distinguiendo el éxito de los códigos que significan cancelación o desconexión:
static void skel_read_complete(struct urb *urb)
{
struct usb_skel *dev = urb->context;
switch (urb->status) {
case 0:
break; /* exito: urb->actual_length bytes validos */
case -ECONNRESET: /* URB cancelado con usb_unlink_urb */
case -ENOENT: /* URB cancelado con usb_kill_urb */
case -ESHUTDOWN: /* el dispositivo se desconecto */
return; /* no reenviar: nos vamos */
default:
dev_warn(&dev->intf->dev, "estado %d\n", urb->status);
}
/* procesar dev->buf[0 .. urb->actual_length] */
/* para una lectura continua, reenviar aqui el mismo urb */
usb_submit_urb(urb, GFP_ATOMIC); /* atomico: estamos en interrupcion */
}
Fíjate en el segundo usb_submit_urb: para una entrada continua —un teclado, un sensor— el patrón canónico es reenviar el URB desde su propio callback, creando un bucle perpetuo de sondeo. Y como el callback es contexto atómico, el reenvío usa GFP_ATOMIC, no GFP_KERNEL.
Cancelar y sincronizar: kill y anchors
Cuando llega la desconexión (nivel 31.3) hay que abortar los URBs en vuelo. usb_kill_urb cancela un URB y bloquea hasta que su callback termina, por lo que solo puede llamarse en contexto que pueda dormir; su primo usb_unlink_urb es asíncrono. Para no llevar la cuenta de cada URB a mano, usbcore ofrece los anchors: un ancla agrupa un conjunto de URBs en vuelo y permite matarlos todos de golpe.
usb_anchor_urb(urb, &dev->submitted); /* registrar antes de enviar */
ret = usb_submit_urb(urb, GFP_KERNEL);
if (ret)
usb_unanchor_urb(urb);
/* en disconnect o suspend, cancelar todo lo pendiente de una vez */
usb_kill_anchored_urbs(&dev->submitted);
Para transferencias simples de una sola vez que sí pueden dormir, existen los envoltorios síncronos usb_bulk_msg y usb_control_msg: internamente arman un URB, lo envían y esperan con una completion (nivel 16). Úsalos cuando quieras la sencillez de una llamada bloqueante; usa URBs crudos cuando necesites asincronía, alto rendimiento o sondeo continuo.
El URB no es una peculiaridad de USB: es una encarnación más del patrón que vertebra toda la E/S rápida del kernel. Enviar un descriptor, seguir con tu vida, y ser llamado de vuelta cuando el hardware termina es exactamente lo que hace el bio en el block layer, lo que hace NAPI en la pila de red y lo que hace la E/S asíncrona en el VFS. La razón de que este patrón aparezca una y otra vez es física: el hardware es órdenes de magnitud más lento que la CPU, así que bloquear un hilo esperándolo es desperdiciar un recurso carísimo. En su lugar, describes el trabajo, lo entregas y liberas la CPU; el completado te encuentra por interrupción. Pero esa elegancia tiene un precio que debes interiorizar: el callback corre en contexto de interrupción, y de ahí desciende toda la disciplina de sincronización que aprendiste en los niveles 14 a 16. No puedes dormir, no puedes usar GFP_KERNEL, no puedes tomar un mutex; solo spinlocks y GFP_ATOMIC. El URB es, por tanto, el punto exacto donde convergen tres niveles enteros de esta guía: el DMA del nivel 27 (que da al buffer una dirección de bus), las interrupciones (que traen el completado) y la concurrencia (que dicta qué puedes hacer al recibirlo). Cuando ves el URB como lo que es —un descriptor de E/S asíncrona en un reactor dirigido por interrupciones— dejas de aprender USB y empiezas a reconocer la arquitectura universal de todo dispositivo rápido bajo Linux.
- Reserva un URB con
usb_alloc_urby un buffer conusb_alloc_coherent, y rellénalo conusb_fill_bulk_urbsobre un pipeusb_rcvbulkpipe. - Implementa el callback con el
switch (urb->status)que distingue éxito de los códigos de cancelación, y reenvía el URB conGFP_ATOMICpara una lectura perpetua. - Registra cada URB en un anchor antes de enviarlo y cancélalos todos con
usb_kill_anchored_urbsen.disconnect. - Reescribe la misma lectura de una sola vez con
usb_bulk_msgy explica en qué contexto puede usarse cada versión y por qué el callback asíncrono no puede dormir.