La reentrada es un concepto de programación en el que una función o subrutina se puede interrumpir y luego reanudar antes de que termine de ejecutarse. Esto significa que se puede volver a llamar a la función antes de que complete su ejecución anterior. El código reentrante está diseñado para ser seguro y predecible cuando se llaman varias instancias de la misma función simultáneamente o en rápida sucesión. Un programa de computadora o subrutina se llama reentrante si se pueden ejecutar múltiples invocaciones de manera segura al mismo tiempo en múltiples procesadores , o si en un sistema de un solo procesador su ejecución se puede interrumpir y se puede iniciar de manera segura una nueva ejecución (se puede "reentrar" "). La interrupción podría ser causada por una acción interna como un salto o una llamada, o por una acción externa como una interrupción o una señal , a diferencia de la recursividad , donde las nuevas invocaciones solo pueden ser causadas por una llamada interna.
Esta definición se origina en entornos de multiprogramación , donde múltiples procesos pueden estar activos simultáneamente y donde el flujo de control podría ser interrumpido por una interrupción y transferido a una rutina de servicio de interrupción (ISR) o subrutina "controladora". Cualquier subrutina utilizada por el controlador que podría haberse estado ejecutando cuando se activó la interrupción debe ser reentrante. De manera similar, el código compartido por dos procesadores que acceden a datos compartidos debería ser reentrante. A menudo, las subrutinas accesibles a través del núcleo del sistema operativo no son reentrantes. Por lo tanto, las rutinas de servicio de interrupción están limitadas en las acciones que pueden realizar; por ejemplo, generalmente tienen restringido el acceso al sistema de archivos y, a veces, incluso la asignación de memoria .
La reentrada no es necesaria ni suficiente para la seguridad de los subprocesos en entornos de subprocesos múltiples. En otras palabras, una subrutina reentrante puede ser segura para subprocesos, [1] pero no se garantiza que sea [ cita necesaria ] . Por el contrario, el código seguro para subprocesos no necesita ser reentrante (consulte los ejemplos a continuación).
Otros términos utilizados para programas reentrantes incluyen "código compartible". [2] Las subrutinas reentrantes a veces están marcadas en el material de referencia como "seguras para señales". [3] Los programas reentrantes son a menudo [un] "procedimiento puro".
La reentrada no es lo mismo que la idempotencia , en la que la función se puede llamar más de una vez y aún así generar exactamente el mismo resultado como si solo se hubiera llamado una vez. En términos generales, una función produce datos de salida basados en algunos datos de entrada (aunque ambos son opcionales, en general). Cualquier función puede acceder a los datos compartidos en cualquier momento. Si los datos se pueden cambiar mediante cualquier función (y ninguna realiza un seguimiento de esos cambios), no hay garantía para quienes comparten un dato de que ese dato sea el mismo que en cualquier momento anterior.
Los datos tienen una característica llamada alcance , que describe en qué parte de un programa se pueden utilizar los datos. El alcance de los datos es global (fuera del alcance de cualquier función y con una extensión indefinida) o local (se crea cada vez que se llama a una función y se destruye al salir).
Los datos locales no son compartidos por ninguna rutina, reingresando o no; por lo tanto, no afecta el reingreso. Los datos globales se definen fuera de las funciones y pueden acceder a ellos más de una función, ya sea en forma de variables globales (datos compartidos entre todas las funciones) o como variables estáticas (datos compartidos por todas las invocaciones de la misma función). En la programación orientada a objetos , los datos globales se definen en el ámbito de una clase y pueden ser privados, lo que los hace accesibles sólo para funciones de esa clase. También existe el concepto de variables de instancia , donde una variable de clase está vinculada a una instancia de clase. Por estas razones, en la programación orientada a objetos, esta distinción suele reservarse para los datos accesibles fuera de la clase (públicos) y para los datos independientes de las instancias de la clase (estáticos).
La reentrada es distinta de la seguridad de subprocesos , pero está estrechamente relacionada con ella . Una función puede ser segura para subprocesos y aun así no ser reentrante. Por ejemplo, una función podría estar envuelta con un mutex (lo que evita problemas en entornos de subprocesos múltiples), pero, si esa función se usara en una rutina de servicio de interrupción, podría morir esperando a que la primera ejecución libere el mutex. La clave para evitar confusiones es que reentrante se refiere a que solo se ejecuta un subproceso. Es un concepto de la época en la que no existían sistemas operativos multitarea.
sig_atomic_t
este propósito, aunque con garantías solo para lecturas y escrituras simples, no para incrementos o decrementos. [5] Hay operaciones atómicas más complejas disponibles en C11 , que proporciona stdatomic.h
.Sin embargo, puede modificarse si reside en su propia y única memoria. Es decir, si cada nueva invocación utiliza una ubicación física diferente del código de máquina donde se realiza una copia del código original, no afectará a otras invocaciones incluso si se modifica durante la ejecución de esa invocación (hilo) en particular.
La reentrada de una subrutina que opera con recursos del sistema operativo o datos no locales depende de la atomicidad de las operaciones respectivas. Por ejemplo, si la subrutina modifica una variable global de 64 bits en una máquina de 32 bits, la operación puede dividirse en dos operaciones de 32 bits y, por lo tanto, si la subrutina se interrumpe mientras se ejecuta y se vuelve a llamar desde el controlador de interrupciones , la variable global puede estar en un estado en el que solo se han actualizado 32 bits. El lenguaje de programación puede proporcionar garantías de atomicidad para la interrupción causada por una acción interna como un salto o una llamada. Entonces, la función f
en una expresión como (global:=1) + (f())
, donde el orden de evaluación de las subexpresiones podría ser arbitraria en un lenguaje de programación, vería la variable global establecida en 1 o en su valor anterior, pero no en un estado intermedio donde solo se ha agregado una parte. actualizado. (Esto último puede suceder en C , porque la expresión no tiene punto de secuencia ). El sistema operativo podría proporcionar garantías de atomicidad para las señales , como una llamada al sistema interrumpida por una señal que no tiene un efecto parcial. El hardware del procesador puede proporcionar garantías de atomicidad para las interrupciones , como que las instrucciones del procesador interrumpidas no tengan efectos parciales.
Para ilustrar la reentrada, este artículo utiliza como ejemplo una función de utilidad de Cswap()
, que toma dos punteros y transpone sus valores, y una rutina de manejo de interrupciones que también llama a la función de intercambio.
Este es un ejemplo de función de intercambio que no es reentrante ni segura para subprocesos. Dado que la tmp
variable se comparte globalmente, sin sincronización, entre cualquier instancia concurrente de la función, una instancia puede interferir con los datos en los que confía otra. Como tal, no debería haberse utilizado en la rutina del servicio de interrupción isr()
:
int tmp ; intercambio vacío ( int * x , int * y ) { tmp = * x ; * x = * y ; /* La interrupción de hardware podría invocar isr() aquí. */ * y = tmp ; } vacío isr () { int x = 1 , y = 2 ; intercambiar ( & x , & y ); }
La función swap()
del ejemplo anterior se puede hacer segura para subprocesos haciendo que sea tmp
local para subprocesos . Todavía no puede ser reentrante y esto seguirá causando problemas si isr()
se llama en el mismo contexto que un hilo que ya se está ejecutando swap()
:
_Thread_local int tmp ; intercambio vacío ( int * x , int * y ) { tmp = * x ; * x = * y ; /* La interrupción de hardware podría invocar isr() aquí. */ * y = tmp ; } vacío isr () { int x = 1 , y = 2 ; intercambiar ( & x , & y ); }
Una implementación swap()
que asigna tmp
en la pila en lugar de globalmente y que se llama solo con variables no compartidas como parámetros [b] es segura para subprocesos y reentrante. Es seguro para subprocesos porque la pila es local para un subproceso y una función que actúa solo sobre datos locales siempre producirá el resultado esperado. No hay acceso a datos compartidos, por lo tanto no hay carrera de datos.
intercambio vacío ( int * x , int * y ) { int tmp ; tmp = * x ; * x = * y ; * y = tmp ; /* La interrupción de hardware podría invocar isr() aquí. */ } vacío isr () { int x = 1 , y = 2 ; intercambiar ( & x , & y ); }
Un controlador de interrupciones reentrante es un controlador de interrupciones que vuelve a habilitar las interrupciones en las primeras etapas del controlador de interrupciones. Esto puede reducir la latencia de interrupción . [6] En general, al programar rutinas de servicio de interrupciones, se recomienda volver a habilitar las interrupciones lo antes posible en el controlador de interrupciones. Esta práctica ayuda a evitar perder interrupciones. [7]
En el siguiente código, ni las funciones f
ni g
son reentrantes.
int v = 1 ; intf ( ) { v += 2 ; volver v ; } int g () { return f () + 2 ; }
En lo anterior, f()
depende de una variable global no constante v
; por lo tanto, si f()
es interrumpido durante la ejecución por un ISR que modifica v
, el reingreso f()
devolverá el valor incorrecto de v
. El valor de v
y, por lo tanto, el valor de retorno de f
, no se pueden predecir con confianza: variarán dependiendo de si se modificó una interrupción v
durante f
la ejecución de . Por tanto, f
no es reentrante. Tampoco lo es g
, porque llama a f
, que no es reentrante.
Estas versiones ligeramente modificadas son reentrantes:
int f ( int i ) { retorno i + 2 ; } int g ( int i ) { retorno f ( i ) + 2 ; }
A continuación, la función es segura para subprocesos, pero no (necesariamente) reentrante:
función int () { mutex_lock (); // ... // cuerpo de la función // ... mutex_unlock (); }
En lo anterior, function()
pueden ser llamados por diferentes hilos sin ningún problema. Pero, si la función se usa en un controlador de interrupciones reentrantes y surge una segunda interrupción dentro de la función, la segunda rutina se bloqueará para siempre. Como el servicio de interrupciones puede desactivar otras interrupciones, todo el sistema podría verse afectado.