La reentrada es un concepto de programación en el que una función o subrutina puede ser interrumpida y luego reanudada antes de que termine de ejecutarse. Esto significa que la función puede ser llamada nuevamente antes de que complete su ejecución anterior. El código reentrante está diseñado para ser seguro y predecible cuando se llaman múltiples instancias de la misma función simultáneamente o en rápida sucesión. Un programa de computadora o subrutina se llama reentrante si múltiples invocaciones pueden ejecutarse de manera segura de manera concurrente en múltiples procesadores , o si en un sistema de un solo procesador su ejecución puede ser interrumpida y una nueva ejecución de la misma puede iniciarse de manera segura (puede ser "reingresada"). 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 recursión , donde las nuevas invocaciones solo pueden ser causadas por una llamada interna.
Esta definición se origina en entornos de multiprogramación , donde varios 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 potencialmente podría haber estado ejecutándose cuando se activó la interrupción debe ser reentrante. De manera similar, el código compartido por dos procesadores que acceden a datos compartidos debe 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 se les restringe 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 subprocesos en entornos multiproceso. En otras palabras, una subrutina reentrante puede ser segura para subprocesos, [1] pero no se garantiza que lo sea [ cita requerida ] . Por el contrario, el código seguro para subprocesos no necesita ser reentrante (consulte los ejemplos a continuación).
Otros términos utilizados para los programas reentrantes incluyen "código compartible". [2] Las subrutinas reentrantes a veces se marcan en el material de referencia como "seguras para señales". [3] Los programas reentrantes son a menudo " procedimientos puros".
La reentrada no es lo mismo que la idempotencia , en la que la función puede ser llamada más de una vez y, sin embargo, generar exactamente la misma salida que si sólo 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 cualquier función puede cambiar los datos (y ninguna lleva un registro 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 denominada ámbito , que describe en qué parte de un programa se pueden utilizar. El ámbito de los datos puede ser global (fuera del ámbito 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 afectan el reingreso. Los datos globales se definen fuera de las funciones y pueden ser accedidos por 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, haciéndolos accesibles solo 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 generalmente se reserva para los datos accesibles fuera de la clase (públicos), y para los datos independientes de las instancias de 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 en un mutex (lo que evita problemas en entornos multiproceso), pero, si esa función se usara en una rutina de servicio de interrupción, podría morir de hambre mientras espera a que la primera ejecución libere el mutex. La clave para evitar confusiones es que la reentrada se refiere a la ejecución de un solo 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 incrementar o decrementar. [5] Hay operaciones atómicas más complejas disponibles en C11 , que proporciona stdatomic.h
.Sin embargo, puede modificarse a sí mismo si reside en su propia memoria única. Es decir, si cada nueva invocación utiliza una ubicación de código de máquina física diferente donde se realiza una copia del código original, no afectará a otras invocaciones incluso si se modifica a sí mismo durante la ejecución de esa invocación (subproceso) en particular.
La reentrada de una subrutina que opera sobre 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 llama nuevamente 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 puede ser arbitrario 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 actualizado una parte. (Esto último puede ocurrir en C , porque la expresión no tiene un punto de secuencia ). El sistema operativo podría proporcionar garantías de atomicidad para señales , como una llamada al sistema interrumpida por una señal que no tiene un efecto parcial. El hardware del procesador podría proporcionar garantías de atomicidad para interrupciones , como instrucciones del procesador interrumpidas que no tienen 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 todas las instancias concurrentes de la función, una instancia puede interferir con los datos en los que confía otra. Por lo tanto, no debería haberse utilizado en la rutina de servicio de interrupción isr()
:
int temporal ; void swap ( int * x , int * y ) { tmp = * x ; * x = * y ; /* La interrupción de hardware podría invocar isr() aquí. */ * y = tmp ; } void isr () { int x = 1 , y = 2 ; intercambiar ( & x , & y ); }
La función swap()
del ejemplo anterior se puede convertir en segura para subprocesos haciendo que tmp
sea local para subprocesos . Aún así, no es reentrante y esto seguirá causando problemas si isr()
se la llama en el mismo contexto que un subproceso que ya se está ejecutando swap()
:
_Thread_local int tmp ; void swap ( int * x , int * y ) { tmp = * x ; * x = * y ; /* La interrupción de hardware podría invocar isr() aquí. */ * y = tmp ; } void isr () { int x = 1 , y = 2 ; intercambiar ( & x , & y ); }
Una implementación swap()
que asigna tmp
en la pila en lugar de hacerlo globalmente y que se llama solo con variables no compartidas como parámetros [b] es segura para subprocesos y reentrante. Es segura 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.
void swap ( int * x , int * y ) { int tmp ; tmp = * x ; * x = * y ; * y = tmp ; /* La interrupción de hardware podría invocar isr() aquí. */ } void isr () { int x = 1 , y = 2 ; intercambiar ( & x , & y ); }
Un manejador de interrupciones reentrante es un manejador de interrupciones que vuelve a habilitar las interrupciones al principio del manejador de interrupciones. Esto puede reducir la latencia de las interrupciones . [6] En general, al programar rutinas de servicio de interrupciones, se recomienda volver a habilitar las interrupciones lo antes posible en el manejador de interrupciones. Esta práctica ayuda a evitar la pérdida de interrupciones. [7]
En el siguiente código, ni la función f
ni g
la función son reentrantes.
int v = 1 ; int f () { v += 2 ; devolver v ; } int g () { devolver f () + 2 ; }
En lo anterior, f()
depende de una variable global no constante v
; por lo tanto, si f()
se interrumpe durante la ejecución por una ISR que modifica v
, entonces la reentrada en 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 una interrupción se modificó v
durante f
la ejecución de . Por lo 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 ) { devolver i + 2 ; } int g ( int i ) { devolver f ( i ) + 2 ; }
A continuación, la función es segura para subprocesos, pero no (necesariamente) reentrante:
int función () { mutex_lock (); // ... // cuerpo de la función // ... desbloqueo mutex (); }
En el ejemplo anterior, function()
se puede llamar desde distintos subprocesos sin ningún problema. Pero, si la función se utiliza en un controlador de interrupción reentrante y surge una segunda interrupción dentro de la función, la segunda rutina se bloqueará para siempre. Como el servicio de interrupciones puede deshabilitar otras interrupciones, todo el sistema podría verse afectado.