stringtranslate.com

Reentrada (informática)

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".

Fondo

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.

Reglas para el reingreso

El código reentrante no puede contener ningún dato estático o global no constante sin sincronización .
Las funciones reentrantes pueden trabajar con datos globales. Por ejemplo, una rutina de servicio de interrupción reentrante podría tomar una parte del estado del hardware para trabajar con ella (por ejemplo, el búfer de lectura del puerto serie) que no solo es global, sino también volátil. Aún así, no se recomienda el uso típico de variables estáticas y datos globales, en el sentido de que, excepto en secciones de código que están sincronizadas , solo se deben usar instrucciones atómicas de lectura-modificación-escritura en estas variables (no debería ser posible que una interrupción o señal llegue durante la ejecución de dicha instrucción). Tenga en cuenta que en C, ni siquiera se garantiza que una lectura o escritura sea atómica; puede dividirse en varias lecturas o escrituras. [4] El estándar C y SUSv3 prevén sig_atomic_teste 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.
El código reentrante no puede modificarse sin sincronización.
El sistema operativo puede permitir que un proceso modifique su código. Existen varias razones para ello (por ejemplo, copiar gráficos rápidamente), pero esto generalmente requiere sincronización para evitar problemas con la reentrada.

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.

El código reentrante no puede llamar a programas o rutinas de computadora no reentrantes sin sincronización.
Los múltiples niveles de prioridad de usuario, objeto o proceso o el multiprocesamiento suelen complicar el control del código reentrante. Es importante realizar un seguimiento de cualquier acceso o efecto secundario que se realice dentro de una rutina diseñada para ser reentrante.

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 fen 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.

Ejemplos

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.

Ni reentrante ni seguro para subprocesos

Este es un ejemplo de función de intercambio que no es reentrante ni segura para subprocesos. Dado que la tmpvariable 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 ); }          

Seguro para subprocesos pero no reentrante

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 ); }          

Reentrante y seguro para subprocesos

Una implementación swap()que asigna tmpen 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 ); }          

Manejador de interrupciones reentrantes

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]

Más ejemplos

En el siguiente código, ni la función fni gla 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 vy, 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ó vdurante fla ejecución de . Por lo tanto, fno 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.

Notas

  1. ^ Un programa que serializa la automodificación puede ser reentrante, y un procedimiento puro que actualiza datos globales sin una serialización adecuada puede no ser reentrante.
  2. ^ Si isr() llamara a swap() con una o dos variables globales como parámetros, entonces swap() no sería reentrante

Véase también

Referencias

  1. ^ Kerrisk 2010, pág. 657.
  2. ^ Ralston 2000, pág. 1514–1515.
  3. ^ "pthread_cond_init()--Inicializar variable de condición". IBM Knowledge Center . Consultado el 5 de octubre de 2019 .
  4. ^ Preshing, Jeff (18 de junio de 2013). «Operaciones atómicas frente a operaciones no atómicas». Preshing on Programming . Archivado desde el original el 3 de diciembre de 2014. Consultado el 24 de abril de 2018 .
  5. ^ Kerrisk 2010, pág. 428.
  6. ^ Sloss y otros. 2004, pág. 342.
  7. ^ Regehr, John (2006). "Uso seguro y estructurado de interrupciones en software integrado y en tiempo real" (PDF) . Manual de sistemas integrados y en tiempo real . CRC Press . Archivado (PDF) desde el original el 24 de agosto de 2007, a través del sitio web del autor en la Facultad de Informática de la Universidad de Utah.

Obras citadas

Lectura adicional