Interrupción de llamadas del sistema cuando se capta una señal


29

De la lectura de las páginas del manual sobre el read()y write()llamadas parece que estas llamadas quedan interrumpidos por las señales independientemente de si tienen o no bloquear.

En particular, asuma

  • Un proceso establece un controlador para alguna señal.
  • se abre un dispositivo (por ejemplo, un terminal) con el O_NONBLOCK no configurado (es decir, opera en modo de bloqueo)
  • el proceso realiza una read()llamada al sistema para leer desde el dispositivo y, como resultado, ejecuta una ruta de control de kernel en kernel-space.
  • mientras el preceso está ejecutando su read()espacio en el núcleo, la señal para la que se instaló el controlador anteriormente se entrega a ese proceso y se invoca su controlador de señal.

Al leer las páginas de manual y las secciones correspondientes en SUSv3 'Volumen de interfaces del sistema (XSH)' , se encuentra que:

yo. Si a read()es interrumpido por una señal antes de leer cualquier dato (es decir, tuvo que bloquear porque no había datos disponibles), devuelve -1 con errnoset en [EINTR].

ii) Si a read()es interrumpido por una señal después de haber leído con éxito algunos datos (es decir, fue posible comenzar a atender la solicitud de inmediato), devuelve el número de bytes leídos.

Pregunta A): ¿Estoy en lo cierto al suponer que en cualquier caso (bloqueo / no bloqueo) la entrega y el manejo de la señal no es completamente transparente para el read()?

Caso i. parece comprensible ya que el bloqueo read()normalmente colocaría el proceso en el TASK_INTERRUPTIBLEestado de modo que cuando se entrega una señal, el núcleo coloca el proceso en TASK_RUNNINGestado.

Sin embargo, cuando read()no es necesario bloquear (caso ii.) Y está procesando la solicitud en kernel-space, habría pensado que la llegada de una señal y su manejo serían transparentes, como la llegada y el manejo adecuado de un HW la interrupción sería En particular, habría asumido que al entregar la señal, el proceso se colocaría temporalmente en modo de usuario para ejecutar su controlador de señal desde el cual volvería finalmente para terminar de procesar el interrumpido read()(en el espacio del kernel) para que se read()ejecute su curso hasta su finalización después de lo cual el proceso vuelve al punto justo después de la llamada a read()(en el espacio de usuario), con todos los bytes disponibles leídos como resultado.

Pero ii. parece implicar que read()se interrumpe, ya que los datos están disponibles de inmediato, pero devuelve solo algunos de los datos (en lugar de todos).

Esto me lleva a mi segunda (y última) pregunta:

Pregunta B): Si mi suposición bajo A) es correcta, ¿por qué read()se interrumpe, a pesar de que no necesita bloquearse porque hay datos disponibles para satisfacer la solicitud de inmediato? En otras palabras, ¿por qué read()no se reanuda después de ejecutar el controlador de señal, y finalmente se devuelven todos los datos disponibles (que estaban disponibles después de todo)?

Respuestas:


29

Resumen: tiene razón en que recibir una señal no es transparente, ni en el caso i (interrumpido sin haber leído nada) ni en el caso ii (interrumpido después de una lectura parcial). De lo contrario, en caso de que requiera realizar cambios fundamentales tanto en la arquitectura del sistema operativo como en la arquitectura de las aplicaciones.

La vista de implementación del sistema operativo

Considere lo que sucede si una llamada al sistema se interrumpe por una señal. El controlador de señal ejecutará el código de modo de usuario. Pero el controlador syscall es código de núcleo y no confía en ningún código de modo de usuario. Así que exploremos las opciones para el controlador syscall:

  • Terminar la llamada al sistema; informar cuánto se hizo al código de usuario. Depende del código de la aplicación reiniciar la llamada al sistema de alguna manera, si lo desea. Así es como funciona Unix.
  • Guarde el estado de la llamada del sistema y permita que el código de usuario reanude la llamada. Esto es problemático por varias razones:
    • Mientras se ejecuta el código de usuario, podría suceder que algo invalide el estado guardado. Por ejemplo, si lee de un archivo, el archivo podría truncarse. Por lo tanto, el código del núcleo necesitaría mucha lógica para manejar estos casos.
    • No se puede permitir que el estado guardado mantenga ningún bloqueo, porque no hay garantía de que el código de usuario reanude la llamada al sistema, y ​​luego el bloqueo se mantendrá para siempre.
    • El núcleo debe exponer nuevas interfaces para reanudar o cancelar las llamadas al sistema en curso, además de la interfaz normal para iniciar una llamada al sistema. Esta es una gran complicación para un caso raro.
    • El estado guardado necesitaría usar recursos (memoria, al menos); esos recursos tendrían que ser asignados y retenidos por el núcleo, pero se contarán en contra de la asignación del proceso. Esto no es insuperable, pero es una complicación.
      • Tenga en cuenta que el controlador de señal puede hacer llamadas al sistema que se interrumpen; por lo que no puede simplemente tener una asignación de recursos estáticos que cubra todas las llamadas al sistema posibles.
      • ¿Y qué pasa si no se pueden asignar los recursos? Entonces el syscall tendría que fallar de todos modos. Lo que significa que la aplicación necesitaría tener un código para manejar este caso, por lo que este diseño no simplificaría el código de la aplicación.
  • Permanezca en progreso (pero suspendido), cree un nuevo hilo para el manejador de señal. Esto, nuevamente, es problemático:
    • Las primeras implementaciones de Unix tenían un solo hilo por proceso.
    • El manejador de señales correría el riesgo de sobrepasar los zapatos del syscall. Este es un problema de todos modos, pero en el diseño actual de Unix, está contenido.
    • Deberían asignarse recursos para el nuevo hilo; véase más arriba.

La principal diferencia con una interrupción es que el código de interrupción es confiable y altamente restringido. Por lo general, no está permitido asignar recursos, o ejecutarse para siempre, o tomar bloqueos y no liberarlos, o hacer cualquier otro tipo de cosas desagradables; dado que el manejador de interrupciones está escrito por el mismo implementador del sistema operativo, él sabe que no hará nada malo. Por otro lado, el código de la aplicación puede hacer cualquier cosa.

La vista de diseño de la aplicación

Cuando una aplicación se interrumpe en medio de una llamada al sistema, ¿debería continuar la finalización de la llamada del sistema? No siempre. Por ejemplo, considere un programa como un shell que lee una línea desde el terminal y el usuario presiona Ctrl+C, activando SIGINT. La lectura no debe completarse, de eso se trata la señal. Tenga en cuenta que este ejemplo muestra que la readllamada al sistema debe ser interrumpible incluso si todavía no se ha leído ningún byte.

Por lo tanto, debe haber una manera para que la aplicación le diga al núcleo que cancele la llamada al sistema. Bajo el diseño de Unix, eso sucede automáticamente: la señal hace que regrese la llamada del sistema. Otros diseños requerirían una forma para que la aplicación reanude o cancele la llamada al sistema cuando lo desee.

La readllamada al sistema es como es porque es la primitiva lo que tiene sentido, dado el diseño general del sistema operativo. Lo que significa es, más o menos, "leer todo lo que pueda, hasta un límite (el tamaño del búfer), pero deténgase si sucede algo más". Para leer realmente un búfer completo implica correr readen un bucle hasta que se hayan leído tantos bytes como sea posible; esto es una función de nivel más alto, fread(3). A diferencia de lo read(2)que es una llamada al sistema, freades una función de biblioteca, implementada en el espacio del usuario read. Es adecuado para una aplicación que lee un archivo o muere en el intento; no es adecuado para un intérprete de línea de comandos o para un programa en red que debe regular las conexiones limpiamente, ni para un programa en red que tenga conexiones concurrentes y no use hilos.

El ejemplo de lectura en bucle se proporciona en la Programación del sistema Linux de Robert Love:

ssize_t ret;
while (len != 0 && (ret = read (fd, buf, len)) != 0) {
  if (ret == -1) {
    if (errno == EINTR)
      continue;
    perror ("read");
    break;
  }
  len -= ret;
  buf += ret;
}

Cuida case iy case iiy pocos más.


Muchas gracias Gilles por una respuesta muy concisa y clara que corrobora puntos de vista similares presentados en un artículo sobre la filosofía de diseño de UNIX. Me parece muy convincente que el comportamiento de interrupción de syscall tiene que ver con la filosofía de diseño de UNIX en lugar de las limitaciones o impedimentos
técnicos

@darbehdar Son los tres: filosofía de diseño de Unix (aquí principalmente que los procesos son menos confiables que el núcleo y pueden ejecutar código arbitrario, también que los procesos y los hilos no se crean implícitamente), restricciones técnicas (en las asignaciones de recursos) y diseño de aplicaciones (hay son casos en los que la señal debe cancelar la llamada al sistema).
Gilles 'SO- deja de ser malvado'

2

Para responder la pregunta A :

Sí, la entrega y el manejo de la señal no es completamente transparente para el read().

La read()carrera a mitad de camino puede estar ocupando algunos recursos mientras está interrumpida por la señal. Y el manejador de señal de la señal también puede llamar a otro read()(o a cualquier otra llamada al sistema segura de señal asíncrona ). Por lo tanto, lo read()interrumpido por la señal debe detenerse primero para liberar los recursos que utiliza, de lo contrario, la read()llamada del manejador de señales accederá a los mismos recursos y causará problemas reentrantes.

Debido a que las llamadas al sistema read()pueden ser llamadas desde el controlador de señales y también pueden ocupar un conjunto idéntico de recursos como lo read()hace. Para evitar los problemas de reentrada anteriores, el diseño más simple y seguro es detener la interrupción read()cada vez que ocurre una señal durante su ejecución.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.