Los procesos se comunican con otros procesos y con el núcleo para coordinar sus actividades. Linux soporta un número de mecanismos de comunicación entre procesos (IPC). Las señales y tuberías son dos de ellos pero Linux también soporta el llamado mecanismo System V por la versión Unix en la que apareció por primera vez.
Hay un conjunto de señales definidas que puede generar el núcleo o que pueden ser generadas por otros procesos en el sistema, siempre que tengan los privilegios correctos. Usted puede listar un conjunto de señales del sistema usando el comando kill -l, en mi Máquina Linux Intel esto proporciona:
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGIOT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
Los números son diferentes para una Máquina Linux Alpha AXP . Los procesos pueden elegir ignorar la mayoría de las señales que son generadas, con dos excepciones notables: ni la señal SIGSTOP la cual causa que un proceso detenga su ejecución ni la señal SIGKILL que causa que un proceso termine pueden ser ignoradas. En cualquier otro caso un proceso puede elegir solamente de que forma manejar las diferentes señales. Los procesos pueden bloquear las señales y, si no las bloquean pueden elegir entre tratarlas ellos mismos o permitir que sea el núcleo el que lo haga. Si el núcleo maneja las señales, ejecutará las acciones requeridas por defecto para cada una. Por ejemplo, la acción por defecto cuando un proceso recibe la señal SIGFPE (execpción de punto flotante), es realizar un volcado del núcleo y después terminar. Las señales no tienen prioridades relativas inherentes. Si dos señales se generan para un proceso al mismo tiempo puede que sean presentadas al proceso o tratadas en cualquier orden. Tampoco existe mecanismo para tratar múltiples señales de la misma clase. No existe forma de que un proceso pueda informar de si recibió 1 o 42 señales SIGCONT.
Linux implementa las señales usando información almacenada en la task_struct del proceso. El número de señales soportadas está limitado al tamño de palabra del procesador. Procesadores con un tamaño de palabra de 32 bits pueden tener 32 señales mientras que procesadores de 64 bits como el Alpha AXP pueden tener hasta 64 señales Las señales actualmente pendientes son mantenidas con el campo de señal marcado con una mascara de señal bloqueada. Con la excepción de SIGSTOP y SIGKILL, todas las señales pueden ser bloqueadas. Si se genera una señal bloqueada, esta permanece pendiente hasta que es desbloqueada. Linux también mantiene información acerca de como cada proceso trata todas las posibles señales y esta es almacenada en una matriz de estructuras de datos sigaction apuntada por la task_struct de cada proceso. Entre otras cosas esta contiene o la dirección de una rutina que tratará la señal o una bandera que indica a Linux que el proceso o bien desea ignorar la señal o bien permitir que sea el núcleo el que la trate en su lugar. El proceso modifica el tratamiento por defecto de la señal realizando llamadas al sistema las cuales alteran la sigaction de la señal apropiada así como la máscara de bloqueada.
No todo proceso en el sistema puede enviar señales a todo otro proceso, el núcleo puede y los super usuarios también. Los procesos normales solo pueden enviar señales a procesos con el mismo uid y gid o a procesos en el mismo grupo de proceso1. Las señales son generadas estableciendo el bit apropiado el campo señal de la task_structSi el proceso no ha bloqueado la señal y está esperando pero es interrumpible (en estado Interrumpible) entonces es activado cambiando su estado a ejecutándose y asegurándose de que se encuentra en la cola de ejecución. De esa forma el planificador lo considerará un candidato a ejecutar cuando el sistema planifica de nuevo.
Si es necesario el tratamiento por defecto, entonces Linux puede optimizar el manejo de la señal. Por ejemplo si la señal SIGWINCH (el foco ha cambiado en X window) y se usa el manejador por defecto entonces no hay nada que hacer.
Las señales no son presentadas al proceso inmediatamente que son generadas, tienen que esperar hasta que el proceso se está ejecutándose de nuevo. Cada vez que un proceso sale de una llamada al sistema sus campos señal y bloqueado son comprobados y, si hay algunas señales no bloqueadas, ahora pueden ser comunicadas. Esto quizás parezca un método poco fiable pero todo proceso en el sistema está realizando todo el tiempo llamadas al sistema, por ejemplo para escribir un caracter en el terminal. Los procesos pueden elegir esperar a las señales si los desean, quedando suspendidos en estado interrumpible hasta que una señal les es presentada. El código de procesado de señales de Linux comprueba la estructura sigaction para cada una de las señales no bloqueadas.
Si un manejador de señal es establecido a la acción por defecto entonces el núcleo la tratará. El tratamiento por defecto de la señal SIGSTOP cambiará el estado del proceso actual a Detenido y ejecutará el planificador para seleccionar un nuevo proceso. La acción por defecto para la señal SIGFPE realizará un volcado del núcleo del proceso y entonces hará que este termine. Alternativamente, el proceso puede haber especificado su propio manejador de señales. Este es una rutina que será llamada siempre que la seãl es generada y la estructura sigaction almacena la dirección de esa rutina. El núcleo debe de llamar a la rutina de tratamiento de señales del proceso el como esto ocurre es dependiente del procesador pero todas las CPUs deben hacer frente a el hecho de que el proceso actual está ejecutándose en modo núcleo y es así como vuelve al proceso que llamó al núcleo o la rutina de sistema en modo usuario. El problema se resuelve manipulando la pila y registros del proceso. El contador de programa del proceso se establece a la dirección de la rutina de manejo de señales y los parámetros de la rutina son añadidos a la estructura de llamada o pasados en registros. Cuando el proceso reanuda la operación parece como si la rutina de tratamiento de señales hubiese sido llamada de forma normal.
Linux es compatible POSIX por tanto el proceso puede especificar que señales son bloqueadas cuando es llamada una rutina particular de tratamiento de señales. Esto significa cambiar la mascara bloqueado durante la llamada al manejador de señales del proceso. La mascara bloqueado debe ser devuelta a su valor original cuando la rutina de tratamiento de la señal ha terminado. Por lo tanto Linux añade una llamada a una rutina ordenadora la cual restaura la mascara bloqueado en la pila de llamada del proceso señalado. Linux también optimiza el caso en el que varias rutinas de tratamiento de se\ nales deben ser llamadas mediante apilamiento por eso cada vez que una rutina termina, la siguiente es llamada hasta que la rutina ordenadora es llamada.
$ ls | pr | lpr
Encauza la salida de el comando ls que lista los ficheros del directorio hacia la entrada estandar de el comando pr el cual la repagina. Finalmente la salida estandar de el comando pr es encauzada hacia la entrada estandar del comando lpr qu imprime los resultados en la impresora por defecto. Las tuberías son por tanto flujos unidireccionales de bytes que conectan la salida estandar de un proceso con la entrada estandar de otro proceso. Ningún proceso es consciente de esta redirección y actúa como lo haría normalmente. Es la shell la que establece estas tubeías temporales entre procesos.
En Linux, una tubería se implementa usando dos estructuras de datos de fichero que apuntan ambos al mismo inodo VFS que asimismo apunta a una página física de memoria. La figura 5.1 muestra que cada estructura de datos de fichero contiene punteros a distintos vectores de rutinas de tratamiento de ficheros; una para escribir en la tubería, y la otra para leer de la tubería. Esto oculta las diferencias subyacentes de las llamadas al sistema genéricas la cuales leen y escriben en ficheros ordinarios. Conforme el proceso escritor escribe en la tubería, los bytes son copiados en la página de datos compartida y cuando el proceso lector lee de la tubería los bytes son copiados de la página de datos compatida. Linux debe sincronizar el acceso a la tubería. Debe asegurarse que el lector y el escritor van al paso y para hacer esto utiliza los bloqueos, colas de espera y señales.
Cuando el escritor quiere escribir en la tubeía utiliza la funciones estandar de la librería de escritura. Estas pasan descriptores de ficheros que son índices dentro del conjunto de estructuras de fichero del proceso, cada una representando un fichero abierto o, como en este caso, una tubería abierta. Las llamadas al sitema de Linux usan las rutinas de escritura apuntadas por la estructura de datos que describe esta tubería. Esa rutina de escritura usa información mantenida en el inodo VFS que representa a esta tubería Si hay suficiente espacio para escribir todos los bytes en la tubería y, por tanto a todo su largo la tubería no es bloqueada por su lector, Linux la bloquea para el escritor y copia los bytes que deben de ser escritos desde el espacio de direcciones del proceso en la página de datos compartidos. Si la tubería es bloqueada por el lector o si no hay suficiente espacio para los datos entonces se hace dormir el proceso actual sobre la cola de espera del inodo de la tubería y se llama al planificador con lo que otro proceso puede ejecutarse. El proceso es interrumpible por lo tanto puede recibir señales y será despertado por el lector cuando haya espacio suficiente para los datos escritos o cuando la tubería sea desbloqueada. Cuando los datos han sido escritos, el inodo VFS de la tubería es desbloqueado y cualquier lector dormido que esté esperando en la cola de espera del inodo se despertará a si mismo.
La lectura de datos de la tubería es un proceso muy similar a la escritura en ella. Los procesos están autorizados a realizar lecturas no bloqueantes (depende del modo en que fué abierto el fichero o la tubería) y, en este caso, si no hay datos para ser leidos o si la tubería está bloqueada, se devolverá un error. Esto significa que el proceso puede continuar ejecutándose. La alternativa es esperar en la cola del inodo de la tubería hasta que el proceso escritor ha terminado. Cuando ambos procesos han terminado con la tubería, el inodo de la tubería es desechado junto con la página de datos compartidos.
Linux también soporta tuberías nombradas, también conocidas como FIFOs porque las tuberías funcionan según un principo Primero en Entrar, Primero en Salir. El primer dato escrito en la tubería es el primer dato leido de la tubería. A diferencia de las tuberías los FIFOs no son objetos temporales, son entidades y pueden ser creadas usando el comando makefifo. Los procesos son libres de usar un FIFO tan solo con tener derechos de acceso apropiados para ello. La forma en que los FIFOs son abiertos es un poco diferente de las tuberías. Una tubería (son dos estructuras de ficheros, su inodo VFS y la página de datos compartida) es creada en el momento mientras que un FIFO ya existe y es abierto y cerrado por sus usuarios. Linux debe de tratar los lectores que abren el FIFO antes de que los escritores lo abran así como los lectores antes de que cualquier escritor haya escrito sobre él. De esta forma, los FIFOs son manejados de forma casi idéntica que las tuberías y manejan las mismas estructuras de datos y operaciones.
Todas las estructuras de datos Linux que representan objetos IPC System V en el sistema incluyen una estructura ipc_permla cual contiene identificadores de el propietario y el usuario y grupo creador del proceso.
La forma de acceso para este objeto (propietario, grupo y otro) y la clave del objeto IPC. La clave es utilizada como una forma de encontrar el identificador de referencia del objeto IPC System V. Se soportan dos tipos de claves públicas y privadas. Si la clave es pública entonces cualquier proceso en el sistema, sujeto a chequeo de derechos, puede encontrar el identificador de referencia del objeto IPC System V. Los objetos IPC System V nunca pueden ser refernciados con una clave, solo por su identificador de referencia.
Cada estructura de datos msqid_dscontiene una estructura de datos ipc_perm y punteros a los mensajes introducidos en esta cola. Además, Linux guarda tiempos de modificación de la cola como la última vez en la que se escribió en la cola. La msqid_ds también contiene dos colas de espera; una para escritores de la cola y otra para lectores de la cola.
Cada vez que un proceso intenta escribir un mensaje en la cola de escritura sus identificadores efectivos de usuario y grupo son comparados con el modo en la estructura de datos ipc_perm de esta cola. Si el proceso puede escribir en la cola entonces el mensaje puede ser copiado desde el espacio de direcciones del proceso a una estructura de datos msgy ser puesto al final de la cola. Cada mensaje es etiquetado con un tipo específico de aplicación, acordado entre los procesos cooperantes. Sin embargo, quizás no haya espacio para el mensaje puesto que Linux restringe el número y longitud de los mensajes que pueden ser escritos. En este caso el proceso será añadido a esta cola de espera de esta cola y el planificador será llamado para que ejecute otro proceso. Será despertado cuando uno o más mensajes hayan sido leidos de la cola.
La lectura de la cola es un proceso similar. De nuevo, los derechos de acceso del proceso a la cola de escritura son chequeados. Un proceso lector puede elegir entre coger el primer mensaje de la cola sin importar el tipo o seleccionar mensajes de un determinado tipo. Si no hay mensajes que cumplan este criterio el proceso lector es añadido a la cola de espera de lectura y se ejecuta el planificador. Cuando un mensaje sea escrito a la cola este proceso será despertado y ejecutado de nuevo.
Diagmos que usted tuviera muchos procesos cooperando leyendo y escribiendo registros en un único fichero de datos. Usted querría que los accesos al fichero estuvieran estrictamente coordinados. Usted podría utilizar un semaforo con un valor incicial de 1 y, alrededor del código de operación del fichero, situar dos operaciones de semaforos, la primera para comprobar y decrementar el valor del semaforo, y la segunda para comprobar e incrementarlo. El primer proceso que acceda al fichero debería intentar decrementar el valor del semaforo y si tuviese exito, el valor del semaforo será 0. Este proceso puede ahora continuar y usar el fichero de datos pero si otro proceso que deseara usar el fichero en ese momento intentara decrementar el valor del semaforo fallaría ya que el resultado debería ser -1. Este proceso deberá ser suspendido hasta que el primer proceso haya terminado con el fichero. Cuando el primer proceso ha terminado con el fichero incrementará el valor del semaforo, poniendolo de nuevo a 1. Ahora del proceso en espera puede ser reanudado y esta vez su intento de modificar el semaforo tendrá exito.
Cada objeto semaforo IPC System V describe una matriz y Linux usa la estructura de datos semid_dspara representarlo. Todas las estructuras de datos semid_ds en el sistema están apuntadas por el semary, un vector de punteros. En cada matriz de semaforos hay sem_nsems, cada uno descrito por una estructura de datos sem que es apuntado por sem_base. Todos los procesos que están autorizados a manipular la matriz de semaforos de un objeto semaforo IPC System V puede realizar llamadas al sistema que realizan operaciones sobre ellos. Las llamadas al sistema pueden especificar muchas operaciones y cada operación está descrita por tres entradas; el índice de semaforo, el valor de la operación y un conjuto de banderas. El índice de semaforo es un índice dentro de la matriz de semaforos y el valor de la operación es un valor numérico que será añadido a el valor actual del semaforo. Linux primero comprueba si o no el total de la operación podría realizarse. Una operación tendrá exito si el valor de la operación sumado al valor actual del semaforo fuese mayor que cero o si ambos el valor de la operación y el actual del semaforo son cero. Si cualquiera de las operaciones del semaforo fallara Linux puede suspender el proceso pero solo si las banderas de la operación no han solicitado que la llamada al sistema sea no-bloqueante. Si el proceso va a ser suspendido entonces Linux debe guardar el estado de operaciones a ser realizadas en el semaforo y poner el proceso actual en cola de espera. Esto lo hace construyendo una estructura sem_queueen la pila y llenandola. La nueva estructura sem_queue se pone al final de la cola de espera de este objeto semaforo (usando los punteros sem_pending y sem_pending_last). El proceso actual es puesto en cola de espera en la estructura de datos sem_queue(sleeper) y se llama al planificador para que elija otro proceso para ejecutar.
Si todas las operaciones del semaforo hubieran tenido éxito y el proceso actual no necesita ser suspendido, Linux continúa y aplica las operaciones sobre los miembros apropiados de la matriz de semaforos. Ahora Linux debe chequear que quizás ahora cualquier proceso esperando o suspendido aplique sus operaciones. Mira cada miembro de la cola de operaciones pendientes (sem_pending) por turno, probando a ver si las operaciones pueden realizarse esta vez. Si pueden entonces borra la estructura de datos sem_queue de la lista de operaciones pendientes y realiza las operaciones de semaforos en la matriz de semaforos. Despierta los procesos dormidos haciendolos disponibles para ser continuados la próxima ves que se ejcuta el planificador. Linux se mantiene mirando a lo largo de la lista de pendientes hasta que hay un paso en que no se pueden realizar más operaciones de semaforos y por tanto no se pueden despertar más procesos.
Hay un problema con los semaforos, deadlocks. Este tiene lugar cuando un proceso ha alterado el valor de los semaforos y entra en una región critica pero no puede salir de la región critica por que se cuelga o fue matado. Linux se protege frente a esto manteniendo listas de ajustes a las matrices de semaforos. La idea es que cuando estos ajustes se aplican, los semaforos son devueltos al estado en que estaban antes de que el conjunto de operaciones del proceso sobre el semaforo fuese realizado. Estos ajustes están guardados en estructuras de datos sem_undo puestas ambas en cola en la estructura semid_ds y en la estructura task_struct de el proceso usando estas matrices de semaforos.
Cada operación individual de semaforo puede requerir que se mantenga un ajuste. Linux mantendrá como mucho una estructura de datos sem_undo por proceso para cada matriz de semaforo. Si el proceso solicitante no tiene una, entonces se le crea una cuando se necesite. La nueva estructura de datos sem_undo se pone en cola en la estructura de datos task_struct del proceso y en la estructura de datos de la matriz del semaforo semid_ds. Conforme las operaciones son aplicadas a los semaforos en la matriz del semaforo el negativo del valor de la operación se añade a la entrada de este semaforo en la matriz de ajuste de este proceso en la estructura de datos sem_undo. Por tanto, si el valor de la operación es 2, entonces se añade -2 en la entrada de ajuste de este semaforo.
Cuando los procesos son borrados, conforme terminan Linux trabaja a lo largo de su conjunto de estructuras de datos sem_undo aplicando los ajustes a las matrices de semaforos. Si se borra un conjunto de semaforos, la estructura de datos sem_undo queda en cola en la estructura de datos del proceso task_struct pero el identificador de la matriz de semaforo se hace inválido. En este caso el codigo de limpieza del semaforo simplemente desecha la estructura de datos sem_undo.
Cada nueva área creada de memoria compartida está representada por una estructura de datos shmid_ds. Estas son guardadas en el vector shm_segs. La estructura de datos shmid_ds describe lo grande que es el área de memoria compartida, cuantos procesos están usándola e información sobre esa memoria que está siendo mapeada dentro de sus espacios de direcciones. Es el creador de la memoria compartida el que controla los permisos de acceso a esa memoria y si la clave es pública o privada. Si tiene suficientes derechos de acceso puede también fijar la memoria compartida en memoria física.
Cada proceso que desee compartir la memoria debe engancharse a esa memoria virtual por medio de llamadas al sistema. Esto crea una nueva estructura de datos vm_area_struct que describe la memoria compartida para este proceso. El proceso puede elegir en su espacio virtual de direcciones donde va la memoria virtual o puede dejar a Linux que elija un area libre lo suficientemente grande. La nueva estructura vm_area_struct se coloca en la lista de vm_area_struct que es apuntada por la shmid_ds. Los punteros vm_next_shared y vm_prev_shared son usados para enlazarlos a ambos unidos. La memoria virtual no es creada actualmente durante el enganche; sucede cuando el primer proceso intenta acceder a ella.
La primera vez que un proceso accede a un de las páginas de memoria virtual compartida, tiene lugar un fallo de página. Cuando Linux corrige este fallo de página encuentra la estructura de datos vm_area_struct describiendola. Esta contiene punteros a rutinas de tratamiente para este tipo de memoria virtual compartida. El código de tratamiento de fallos de página de memoria compartida busca en las entradas de tablas de páginas de esta shmid_ds para ver si existe alguna para esta página de memoria virtual compartida. Si no existe, asignará una página física y creará una entrada en la tabla de paginas para ella.
Tan pronto como entra en la tabla de páginas del proceso en curso, esta entrada es guardada en la shmid_ds. Esto significa que cuando el siguiente proceso que intenta acceder a esta memoria obtiene un fallo de página, el codigo de tratamiento de fallos de pagina de memoria virtual usará esta página recientemente creada también para este proceso. Por tanto, el primer proceso que accede a una página de memoria compartida hace que esta sea creada y los posteriores accesos por otros procesos hacen que esa página sea añadida a sus espacios virutales de direcciones.
Cuando los procesos no desean compartir más la memoria virtual, se desencadenan de ella. Como otros procesos están usando todavía la memoria el desencadenado solo afecta al proceso actual. Su vm_area_struct es eliminada de la estructura de datos shmid_ds y desasignada. La tabla de páginas del proceso actual son acutalizadas para anular el área de memoria virtual que era utilizada para compartir. Cuando el último proceso que compartía la memoria se suelta de ella, las páginas de memoria compartida actualmente en memoria física son liberadas, de la misma forma que lo es la estructura de datos shmid_ds de esta memoria compartida.
Aparecen complicaciones adicionales cuando la memoria virtual no es bloqueada en memoria física. En este caso las páginas de memoria compartida pueden ser intercambiadas fuera al sistema de intercambio de disco durante periodos de alto uso de memoria. El como la memoria es intercambiada dentro y fuera de memoria física se describe en el capítulo Chapter mm-chapter.
1 NOTA DE REVISIÓN: Explican los grupos de procesos.