Interfaces API Kernel, Parte 2: Funciones diferibles, tasklets de kernel y colas de trabajo

Introducción a las “bottom halves” ( mitades inferiores) en Linux 2.6

Para operaciones encadenadas de alta frecuencia, el kernel Linux® ofrece tasklets y colas de trabajo. Los tasklets y las colas de trabajo implementan funcionalidad diferible y reemplazan el mecanismo de mitad inferior para drivers. Este artículo explora el uso de tasklets y colas de trabajo en el kernel y le muestra cómo construir funciones diferibles con estas interfaces API.

M. Tim Jones, Consultant Engineer, Emulex Corp.

M. Tim JonesM. Tim Jones es arquitecto de firmware incrustado y es autor de Artificial Intelligence: A Systems Approach, GNU/Linux Application Programming (actualmente en su segunda edición), AI Application Programming (en su segunda edición) y BSD Sockets Programming from a Multilanguage Perspective. Sus antecedentes en la ingeniería abarcan desde el desarrollo de kernels para naves espaciales geosincrónicas hasta la arquitectura de sistemas incrustados y el desarrollo de protocolos de redes. Tim es Consultant Engineer de Emulex Corp. en Longmont, Colorado.



05-05-2010

Contáctese con Tim

Tim es uno de los autores más conocidos y prolíficos. Navegue por todos los artículos de Tim en developerWorks. Consulte el perfil de Tim y contáctese con él, con otros autores y con otros lectores en My developerWorks [Mi developerWorks].

Este artículo explora un par de métodos que se usan para diferir el procesamiento entre contextos kernel (específicamente, en el kernel Linux 2.6.27.14). Aunque estos métodos son específicos para kernel Linux, las ideas que los sustentan también son útiles desde el punto de vista arquitectónico. Por ejemplo, usted podría implementar estas ideas en sistemas incrustados tradicionales en lugar de un planificador tradicional para planificación de tareas.

Pero antes de profundizar en los métodos usados en el kernel para diferir funciones, comencemos con ciertos antecedentes del problema a resolver. Cuando se interrumpe un sistema operativo debido a un evento de hardware (como la presencia de un paquete a través de un adaptador de red), el procesamiento comienza con una interrupción. Generalmente la interrupción difiere una cantidad importante de trabajo. Parte de este trabajo se realiza en el contexto de la interrupción, y otra parte pasa a la pila de software para procesamiento adicional (ver Figura 1).

Figura 1. Procesamiento de mitad superior y de mitad inferior
Procesamiento de mitad superior y de mitad inferior

El tema es determinar cuánto trabajo se debería hacer en el contexto de interrupción. El problema con el contexto de interrupción es que algunas o todas las interrupciones se pueden desactivar durante este tiempo, lo cual aumenta la latencia para administrar otros eventos de hardware (y produce cambios en la forma que se realiza el procesamiento). Por lo tanto, es aconsejable minimizar el trabajo que se realiza durante la interrupción, derivando cierta cantidad de trabajo al contexto kernel (en donde hay una mayor probabilidad de compartir ventajosamente el procesador).

Como se muestra en la Figura 1, el procesamiento efectuado en el contexto de interrupción se llama mitad superior, y el procesamiento basado en la interrupción que se deriva afuera del contexto de interrupción se llama mitad inferior(en donde la mitad superior planifica el procesamiento posterior que realizará la mitad inferior). El procesamiento de mitad inferior se efectúa en el contexto kernel, lo cual significa que las interrupciones están activadas. Esto conduce a un rendimiento mejor debido a la capacidad de manejar rápidamente y con alta frecuencia eventos de interrupción, difiriendo tareas no sensibles en cuanto a tiempos.

Una breve reseña de las bottom halves

Versión kernel Linux

Esta descripción de tasklets y colas de trabajo usa la versión 2.6.27.14 del kernel Linux.

Linux tiene a ser una herramienta sumamente precisa en materia de funcionalidad, y la funcionalidad diferida no escapa a esta característica. A partir de kernel 2.3, se dispone de softirqs que puede implementar un conjunto de 32 bottom halves estadísticamente definidas. Como los elementos estáticos, éstas se definen en tiempo de compilación (a diferencia de los mecanismos nuevos que son dinámicos). Softirqs se usaron para procesamiento de tiempo crítico (interrupciones de software) en un contexto de subprocesos kernel. Usted puede encontrar la fuente de la funcionalidad softirq en ./kernel/softirq.c. En kernel Linux 2.3 también se introdujeron tasklets (ver ./include/linux/interrupt.h). Los tasklets se construyen arriba de softirqs para permitir la creación dinámica de funciones diferibles. Por último, en el kernel Linux 2.5, se introdujeron las colas de trabajo (ver ./include/linux/workqueue.h). Las colas de trabajo permiten diferir tareas desde el contexto de interrupción al contexto de proceso kernel.

Ahora analicemos los mecanismos dinámicos para la postergación de trabajos, los tasklets y las colas de trabajo.


Introducción a los tasklets

Los softirqs originariamente se diseñaron como vector de 32 entradas softirq que soportan una variedad de funciones de interrupción de software. Actualmente, sólo se usan nueve vectores para softirqs, uno es el TASKLET_SOFTIRQ (see./include/linux/interrupt.h). Y aunque softirqs sigue existiendo en el kernel, se recomienda usar los tasklets y las colas de trabajo en lugar de asignar nuevos vectores softirq.

Los tasklets son un esquema de postergación que usted puede programar para que una función registrada se ejecute más tarde. La mitad superior (el controlador de interrupciones) realiza un pequeño trabajo, y luego programa el tasklet para que se ejecute posteriormente en la mitad inferior.

Listado 1. Declaración y programación de un tasklet
/* Declare a Tasklet (the Bottom-Half) */
void tasklet_function( unsigned long data );DECLARE_TASKLET( tasklet_example,
                tasklet_function, tasklet_data ); ... /* Schedule the Bottom-Half
                    */tasklet_schedule( &tasklet_example );

Una determinada tasklet se ejecutará sólo en una CPU (la CPU en la cual se programó el tasklet), y la misma tasklet nunca se ejecutará simultáneamente en más de una CPU de un determinado procesador, pero tasklets diferentes se pueden ejecutar en CPU diferentes al mismo tiempo.

Los tasklets están representadas por la estructura tasklet_struct (ver Figura 2) que incluye los datos necesarios para administrar y actualizar el tasklet (establecer, activar/desactivar a través de atomic_t, puntero de función, datos y referencia de lista vinculada).

Figura 2. Composición de la estructura interna de tasklet_struct
Composición de la estructura interna de tasklet_struct

Los tasklets se planifican a través del mecanismo softirq, a veces mediante ksoftirqd (un subproceso kernel por CPU), cuando el equipo está sometido a una pesada carga de interrupciones de software. La siguiente sección explora las distintas funciones disponibles en la interfaz (API) de programación de aplicaciones.

Legado de los sistemas incrustados

Las ideas que sustentan a los tasklets y a las colas de trabajo tienen cierta relación con los sistemas incrustados. En muchos sistemas incrustados, no hay un planificador tradicional, sólo postergación de trabajos (impulsado por entrada/salida [E/S] o procesamiento interno). En lugar de un planificador, las interrupciones y las aplicaciones difieren tareas como una manera de planificar el procesamiento posterior efectuado por otros elementos del sistema. De esta manera, el planificador se convierte en un procesador de colas de trabajo (alimentando las funciones del planificador con tareas) o máscaras de bits (lo cual indica la capacidad de un tasklet para cumplir con su trabajo).

API de tasklets

Los tasklets se definen usando una macro llamada DECLARE_TASKLET (ver Listado 2). Por debajo, esta macro simplemente brinda una inicialización tasklet_struct de la información que usted suministra (nombre de tasklet, función y datos específicos del tasklet). De manera predeterminada, el tasklet está activado, lo cual significa que puede ser planificada. También se puede declarar desactivada a un tasklet de manera predeterminada usando la macro DECLARE_TASKLET_DISABLED. Esto requiere invocar la función tasklet_enable para que el tasklet pueda ser planificable. Usted puede activar o desactivar un tasklet (desde una perspectiva de planificación) usando las funciones tasklet_enable y tasklet_disable, respectivamente. También existe una función tasklet_init que inicializa a otra funcióntasklet_struct con los datos de tasklet suministrados por el usuario.

Listado 2. Creación de tasklet y funciones de activación/desactivación
DECLARE_TASKLET ( name,
                func, data ); DECLARE_TASKLET_DISABLED( name, func, data);
void tasklet_init( sruct tasklet_struct *, void (*func)(unsigned long),
unsigned long data ); void tasklet_disable_nosync( struct tasklet_struct *);
                    void tasklet_disable( struct tasklet_struct * );
                void tasklet_enable( struct tasklet_struct * ); void tasklet_hi_enable(
                struct tasklet_struct * );

Existen dos funciones para desactivar, cada una de las cuales requiere una desactivación del tasklet, pero sólo devuelve tasklet_disable después que el tasklet ha terminado (en donde tasklet_disable_nosync puede aparecer antes de que haya terminado). Las funciones de desactivación permiten que el tasklet pueda ser “enmascarada” (es decir, no ejecutada) hasta invocar a la función de activación. También existen dos funciones de activación: una para la planificación con prioridad normal (tasklet_enable) y otra para activar la planificación de mayor prioridad (tasklet_hi_enable). La planificación de prioridad normal se realiza a través del nivel TASKLET_SOFTIRQ softirq, en tanto que la de alta prioridad lo hace a través del nivel HI_SOFTIRQ softirq.

Así como para las funciones de activación normal y de alta prioridad, también hay funciones de planificación normal y de alta prioridad (ver el Listado 3). Cada función coloca en cola a el tasklet en el vector particular softirq ( tasklet_vec para prioridad normal y tasklet_hi_vec para alta prioridad). Los tasklets del vector de alta prioridad se ejecutan primero, seguidas por aquellas que se encuentran en el vector normal. Observe que cada CPU mantiene sus propios vectores softirq normales y de alta prioridad.

Listado 3. Funciones de planificación de tasklet
void tasklet_schedule( struct
                tasklet_struct * ); void tasklet_hi_schedule( struct tasklet_struct *
                );

Por último, después de que el tasklet se ha creado, se puede detenerla mediante las funciones tasklet_kill(ver el Listado 4). La función tasklet_kill asegura que el tasklet no se ejecute nuevamente, si el tasklet está actualmente planificada para ejecutarse, esperará hasta que se complete, y luego la anulará. Se usa la función tasklet_kill_immediate sólo cuando una CPU está en estado inactivo.

Listado 4. Funciones para inactivar un tasklet
 void tasklet_kill ( struct tasklet_struct *
                ); void tasklet_kill_immediate( struct tasklet_struct *, unsigned int cpu
                );

Desde la interfaz API usted puede observar que la API de tasklet es simple, y también lo es la implementación. Usted puede encontrar la implementación del mecanismo de tasklet en ./kernel/softirq.c y ./include/linux/interrupt.h.

Ejemplo de tasklet simple

Analicemos un uso simple de interfaz API de tasklets (ver el Listado 5). Como se muestra aquí, la función tasklet se crea con datos asociados (my_tasklet_function y my_tasklet_data), y luego se usa para declarar un tasklet nueva usando DECLARE_TASKLET. Cuando se inserta el módulo, el tasklet se planifica, hecho que le permite ser ejecutada en algún momento futuro. Cuando se descarga el módulo, se invoca la función tasklet_kill para garantizar que el tasklet no se encuentre en algún estado planificable.

Listado 5. Ejemplo simple de un tasklet en el contexto de módulo kernel
 #include
                <linux/kernel.h> #include <linux/module.h> #include
                <linux/interrupt.h> MODULE_LICENSE("GPL"); char
my_tasklet_data[]="my_tasklet_function was called"; /* Bottom Half Function */ void
my_tasklet_function( unsigned long data ) { printk( "%s\n", (char *)data ); return;
                    } DECLARE_TASKLET ( my_tasklet, my_tasklet_function, (unsigned long)
                &my_tasklet_data ); int init_module( void ) { /* Schedule the Bottom Half
                    */ tasklet_schedule( &my_tasklet ); return 0; } void
                cleanup_module( void ) { /* Stop the tasklet before we exit */ 
                tasklet_kill (
                &my_tasklet ); return; }

Introducción a las colas de trabajo

Las colas de trabajo son un mecanismo de postergación más reciente, agregado a la versión kernel Linux 2.5. En lugar de brindar un único esquema de postergación, como en el caso de los tasklets, las colas de trabajo representan un mecanismo de postergación en el cual la función de controlador para la cola de trabajo puede quedar en espera (esto no es posible en el modelo tasklet). Las colas de pueden tener una latencia mayor que los tasklets, pero incluyen una API más rica para la postergación de trabajos. La postergación se solía administrar por colas de trabajo a través de keventd pero ahora se administra mediante un subproceso de trabajo de kernel llamado events/X.

Las colas de trabajo ofrecen un método genérico para diferir funcionalidad a la bottom halves. Su núcleo es la cola de trabajo (struct workqueue_struct) que es la estructura sobre la cual se ubica el trabajo. El trabajo está representado por una estructura work_struct que identifica el trabajo a diferir y la función de postergación que se usará (ver la Figura 3). Los subprocesos kernel events/X(uno por CPU) extraen trabajos de la cola de trabajo y activan uno de los controladores de mitad inferior (como se indica mediante la función de controlador en la estructura work_struct).

Figura 3. El proceso que subyace en las colas de trabajo
El proceso que subyace en las colas de trabajo

Comowork_struct indica la función de controlador a usar, usted puede utilizar la cola de trabajo para organizar los trabajos para una variedad de controladores. Ahora, analicemos las funciones API que se pueden encontrar para colas de trabajo.

API para cola de trabajo

La interfaz API para la cola de trabajo es levemente más complicada que los tasklets, principalmente porque soporta una serie de opciones. Exploremos primero las colas de trabajo y luego examinaremos las tareas incluidas en la cola y las variantes.

Recordemos la Figura 3 en la que la estructura principal para la cola de trabajo es la cola propiamente dicha. Esta estructura se usa para colocar en cola trabajos de la mitad superior con el fin de diferir su ejecución para más adelante por parte de la mitad inferior. Las colas de trabajo se crean a través de una macro llamada create_workqueue, la cual devuelve una referencia workqueue_struct. Usted puede utilizar remotamente esta cola de trabajo más adelante (si lo necesita) invocando la función destroy_workqueue:

struct workqueue_struct *create_workqueue( name ); void
                destroy_workqueue( struct workqueue_struct * );

El trabajo que se comunicará a través de la cola de trabajo es definido por la estructura work_struct. Generalmente esta estructura es el primer elemento de una estructura de usuario de definición de trabajos (más adelante verá un ejemplo de esto). La interfaz API de la cola de trabajo brinda tres funciones para inicializar el trabajo (desde un buffer asignado); ver el Listado 6. La macro INIT_WORK brinda la inicialización y la configuración necesaria de la función del controlador (pasada por el usuario). Para aquellos casos en los cuales el desarrollador necesite un tiempo de espera antes de que el trabajo se asigne a la cola de trabajo, usted puede usar las macros INIT_DELAYED_WORK y INIT_DELAYED_WORK_DEFERRABLE.

Listado 6. Macros para la inicialización de trabajos
INIT_WORK( work, func
                    ); INIT_DELAYED_WORK( work, func ); INIT_DELAYED_WORK_DEFERRABLE(
                work, func );

Con la estructura de trabajos inicializada, el paso siguiente consiste en colocar el trabajo en la cola de trabajo. Usted puede hacer esto de unas algunas maneras (ver el Listado 7). Primero, simplemente coloque los trabajos en una cola de trabajo usando queue_work(esto vincula las trabajos a la CPU actual). O puede especificar la CPU en la cual debe ejecutarse el controlador, usando queue_work_on. Dos funciones adicionales proveen la misma funcionalidad para trabajos demorados (cuya estructura encapsula la estructura work_struct y un tiempo para la demora de trabajos).

Listado 7. Funciones de la cola de trabajo
int queue_work( struct workqueue_struct *wq,
                struct work_struct *work ); int queue_work_on( int cpu, struct
                workqueue_struct *wq, struct work_struct *work ); int queue_delayed_work(
struct workqueue_struct *wq, struct delayed_work 
*dwork, unsigned long delay );
int queue_delayed_work_on( int cpu, struct workqueue_struct *wq, struct
                delayed_work *dwork, unsigned long delay );

Usted puede usar una cola de trabajo de kernel global, con cuatro funciones que se direccionen a esta cola de trabajo. Estas funciones (que se muestran en el Listado 8) replican las del Listado 7, excepto que usted no necesita definir la estructura de la cola de trabajo.

Listado 8. Funciones de cola de trabajo de kernel global
int schedule_work( struct work_struct
                *work ); int schedule_work_on( int cpu, struct work_struct *work );
                    int scheduled_delayed_work( struct delayed_work *dwork, unsigned long
                delay ); int scheduled_delayed_work_on
                ( int cpu, struct delayed_work *dwork,
                unsigned long delay );

También hay una cantidad de funciones de ayuda que puede usar para vaciar o cancelar trabajos en listas de trabajos. Para vaciar un concepto en particular y bloquearlo hasta que el trabajo se haya completado, puede llamar a flush_work. Todos los trabajos en una determinada cola de trabajo se pueden completar usando una llamada a flush_workqueue. En ambos casos, el llamador bloquea hasta que la operación se haya completado. Para vaciar la cola de trabajo de kernel global, llame a flush_scheduled_work.

int flush_work( struct work_struct *work );
                    int flush_workqueue( struct workqueue_struct *wq );
                    void flush_scheduled_work( void );

Usted puede cancelar trabajo si todavía no se está ejecutando en un controlador. Una llamada a cancel_work_sync terminará el trabajo en la cola o lo bloqueará hasta que la devolución de llamada haya finalizado (si el trabajo ya se ejecutándose en el controlador). Si el trabajo está demorado, usted puede llamar a cancel_delayed_work_sync.

int cancel_work_sync( struct work_struct *work );
                    int cancel_delayed_work_sync( struct delayed_work *dwork );

Por último, puede averiguar si un trabajo está pendiente (todavía no fue ejecutado por el controlador) con una llamada a work_pending o delayed_work_pending.

work_pending( work );delayed_work_pending( work
                );

Éste es el núcleo de la interfaz API de cola de trabajo. Usted puede encontrar la implementación de la interfaz API de cola de trabajo en ./kernel/workqueue.c, con definiciones API en ./include/linux/workqueue.h. Ahora sigamos con un ejemplo simple de la interfaz API de cola de trabajo.

Ejemplo de cola de trabajo simple

El ejemplo siguiente ilustra algunas funciones de interfaz API de cola de trabajo. Como en el ejemplo de tasklets, a los efectos de simplificar, usted implementa este ejemplo en el contexto de un módulo kernel.

Primero, mire su estructura de trabajos y la función del controlador que usará para implementar la mitad inferior (ver el Listado 9). Lo primero que observará aquí es una definición de su referencia de estructura de cola de trabajo (my_wq) y la definición my_work_t. La typedef my_work_t incluye la estructura work_struct en la cabecera y un número entero que representa su elemento de trabajo. Su controlador (una función de devolución de llamada) elimina las referencias del puntero work_struct volviendo al tipo my_work_t. Después de emitir el elemento de trabajo (entero de la estructura), el puntero de trabajos queda liberado.

Listado 9. Estructura de trabajo y controlador de mitad inferior
 #include
                <linux/kernel.h> #include <linux/module.h> #include
                <linux/workqueue.h> MODULE_LICENSE("GPL");
                static
                    struct workqueue_struct 
                *my_wq;typedef struct {
                struct work_struct my_work;int x; } my_work_t;
                my_work_t *work, *work2; static
                void my_wq_function( struct work_struct *work)
                { my_work_t *my_work = (my_work_t
                *)work; printk( "my_work.x %d\n", my_work->x );
                kfree( (void *)work );
                return; }

Listado 10 es su función init_module que comienza con la creación de la cola de trabajo usando la función API create_workqueue. Después de crear correctamente la cola de trabajo, usted crea dos elementos de trabajo (asignados mediante kmalloc). Cada elemento de trabajo luego se inicializa con INIT_WORK, el trabajo definido, y luego se coloca en la cola de trabajo, llamando a queue_work. El proceso de mitad superior (simulado aquí) ahora está completo. Entonces el trabajo será procesado, algún tiempo después, por el controlador, como se muestra en el Listado 10.

Listado 10. Cola de trabajo y creación de trabajos
 int init_module( void ) { int ret; my_wq
                    =create_workqueue("my_queue");if (my_wq) 
                { /* Queue some work (item 1) */
                work = (my_work_t *)kmalloc(sizeof(my_work_t), GFP_KERNEL); if (work)
                    {INIT_WORK( (struct work_struct *)work, my_wq_function ); work->x
                = 1; ret = queue_work( my_wq, (struct work_struct *)work ); 
                } /* Queue some
additional work (item 2) */ work2 = (my_work_t *)kmalloc(sizeof(my_work_t),
                GFP_KERNEL); if (work2) {INIT_WORK( (struct work_struct *)work2,
                my_wq_function ); work2->x = 2; ret = queue_work( my_wq, (struct
                work_struct *)work2 ); } } return 0; }

Los elementos finales se muestran en el Listado 11. Aquí, en la limpieza de módulo, usted vacía la cola de trabajo específica (la cual está bloqueada hasta que el controlador haya completado el procesamiento del trabajo), y luego destruye la cola de trabajo.

Listado 11. Proceso para vaciar y destruir cola de trabajo
void cleanup_module( void )
                    {flush_workqueue( my_wq ); destroy_workqueue( my_wq ); return;
                }

Diferencias entre tasklets y colas de trabajo

A través de esta pequeña introducción a tasklets y colas de trabajo, usted puede observar dos esquemas diferentes para diferir trabajos de mitades superiores a mitades inferiores. Los tasklets ofrecen un mecanismo de baja latencia que es simple y sencillo, en tanto que las colas de trabajo brindan una API flexible que permite colocar en cola múltiples elementos de trabajos. Cada uno difiere trabajos a partir del contexto de interrupción, pero sólo los tasklets se ejecutan atómicamente de manera de ejecutarse hasta ser completadas, en cambio las colas de trabajo permiten a los controladores permanecer en espera, si fuera necesario. Cualquiera de estos métodos es útil para diferir trabajos, y por ende el método elegido se basa en sus necesidades particulares.


Avanzando

Los métodos para diferir trabajos explorados aquí representan los métodos antiguos y actuales usados en kernel Linux (excluyendo los temporizadores que se describirán en un artículo futuro). Por cierto no son nuevos—en realidad, existían con otras formas en el pasado,—pero representan un patrón arquitectónico interesante que es útil en Linux y en otros sistemas. Pasando de softirqs a tasklets a colas de trabajo a colas de trabajo demorados, Linux sigue evolucionando en todas las áreas del kernel, brindando al mismo tiempo una experiencia de espacio para usuarios homogéneo y compatible.

Recursos

Aprender

Obtener los productos y tecnologías

  • Evalúe los productos IBM de la manera que le parezca más conveniente: Descargue una prueba de producto, pruebe un producto de manera online, use un producto en un entorno de nube, o dedíquele algunas horas a SOA Sandbox para aprender cómo implementar eficazmente Arquitectura orientada a servicios (SOA).

Comentar

  • Participe en My developerWorks community. Contáctese con otros usuarios developerWorks explorando al mismo tiempo los blogs, foros, grupos y wikis impulsados por desarrolladores.

Comentarios

developerWorks: Ingrese

Los campos obligatorios están marcados con un asterisco (*).


¿Necesita un IBM ID?
¿Olvidó su IBM ID?


¿Olvidó su Password?
Cambie su Password

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


La primera vez que inicie sesión en developerWorks, se creará un perfil para usted. La información en su propio perfil (nombre, país/región y nombre de la empresa) se muestra al público y acompañará a cualquier contenido que publique, a menos que opte por la opción de ocultar el nombre de su empresa. Puede actualizar su cuenta de IBM en cualquier momento.

Toda la información enviada es segura.

Elija su nombre para mostrar



La primera vez que inicia sesión en developerWorks se crea un perfil para usted, teniendo que elegir un nombre para mostrar en el mismo. Este nombre acompañará el contenido que usted publique en developerWorks.

Por favor elija un nombre de 3 - 31 caracteres. Su nombre de usuario debe ser único en la comunidad developerWorks y debe ser distinto a su dirección de email por motivos de privacidad.

Los campos obligatorios están marcados con un asterisco (*).

(Por favor elija un nombre de 3 - 31 caracteres.)

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


Toda la información enviada es segura.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=90
Zone=Linux
ArticleID=487834
ArticleTitle=Interfaces API Kernel, Parte 2: Funciones diferibles, tasklets de kernel y colas de trabajo
publish-date=05052010