Procesos

En los sistemas multiprogramados se define el proceso como entidad representante de la ejecución de un programa en un determinado contexto. Aunque hoy en día se ha generalizado este concepto para permitir flujos de ejecución concurrente dentro de un mismo programa (threads), en lo que respecta al sistema operativo, la problemática es esencialmente la misma: proporcionar una representación adecuada de los flujos y su contexto, un mecanismo de cambio de contexto, medios de comunicación entre los procesos y mecanismos eficientes para soportarlos, y políticas que eviten interbloqueos e inanición.

En los sistemas operativos multiprogramados surge el concepto de proceso, asociado a la ejecución de un programa. En general, un proceso es un flujo de ejecución, representado básicamente por un contador de programa, y su contexto de ejecución, que puede ser más o menos amplio. Así, un proceso UNIX incluye en su contexto el estado de la pila, el estado de la memoria y el estado de la E/S, mientras que un thread1 típico tiene como contexto propio poco más que la pila. En algunos sistemas es posible determinar el contexto propio de un proceso en el momento de su creación, como ocurre con la llamada al sistema clone() de Linux. En adelante, sin perder generalidad, utilizaremos siempre el término proceso, independientemente de cuál sea su contexto. Uno de los objetivos del sistema operativo es la representación de los procesos y el soporte de los cambios de contexto entre procesos, que posibilitan la compartición del recurso CPU. El acceso a otros recursos compartidos y la comunicación entre procesos relacionados (por ejemplo, de una misma aplicación) hacen necesaria la utilización de mecanismos de sincronización dentro del sistema operativo.

Cada vez que un proceso pasa al estado preparado, está compitiendo por el recurso CPU. Un segundo objetivo del sistema operativo multiprogramado es la planificación del uso del (de los) recurso(s) de proceso.

El proceso pasa por diversos estados entre los que se definen transiciones Cada vez que un proceso pasa al estado preparado, está compitiendo por el recurso CPU. Un segundo objetivo del sistema operativo multiprogramado es la planificación del uso del (de los) recurso(s) de proceso.


Se llaman procesos a todos los programas que se están ejecutando en el sistema. Son muchos más de los que podría parecer, puesto que no sólo hay que tener en cuenta los programas que ejecuta cada usuario, sino los que son necesarios para el correcto funcionamiento del sistema. Hay que considerar, por ejemplo, los deamons, que permanecen a la espera de ciertas señales para responder y desencadenar otros procesos. Cada proceso tiene asignado un número que lo identifica, llamado sencillamente PID. Mediante este número, es posible enviar señales a los procesos. El programa ps lista los procesos en ejecución. Si se ejecuta sin parámetros, sólo dirá los procesos asociados al usuario que invoca al programa. Pero ejecutado con las opciones auxw (cada letra es una opción), listará todos. A continuación se muestra un ejemplo (ligeramente retocado) de la salida de ps auxw:


Cada usuario puede eliminar (se podría decir matar) un proceso que le pertenezca. Será necesario hacerlo si se ha perdido el control del programa. Para ello se le manda la señal kill (la número 9), con el programa kill. Por ejemplo, para matar el proceso gnome-terminal, que tiene PID 644, se teclea la orden kill -9 644.
Con el programa top se puede ver cómo van evolucionando los distintos procesos, además de una estadística de la carga del sistema. En la ilustración se ve un ejemplo.


Es la versión GNOME de top, aunque incorpora muchas mejoras. Se pueden ver los procesos en ejecución, colocarlos por orden, mandarles señales (con el menú de contexto), etc. Se arranca eligiendo el menú GNOME sección SISTEMA en al entrada del monitor de sistema , la entrada ; o por su nombre de programa, gtop.

Este es un ejemplo:

Información almacenada por el kernel

Para cada proceso definido en el sistema, el kernel del sistema operativo almacena y mantiene al día varios tipos de información sobre el proceso. Esta información podemos ordenarla de la siguiente forma:
* Información general. Identificadores de proceso, usuario y grupo
* Ambiente (variables, directorio actual, etc.)
* Información de E/S
* Información de Estado
* Espacio de direcciones del proceso. Areas de trabajo (código (“text”), datos, stack)

IDentificadores
Process ID (PID)

Al crearse un nuevo proceso se le asigna un identificador de proceso único. Este número debe utilizarse por el administrador para referirse a un proceso dado al ejecutar un comando.
Los PID son asignados por el sistema a cada nuevo proceso en orden creciente comenzando desde cero. Si antes de un reboot del sistema se llega al nro. máximo, se vuelve a comenzar desde cero, salteando los procesos que aún estén activos.

Parent Process ID (PPID)

La creación de nuevos procesos en Unix se realiza por la vía de duplicar un proceso existente invocando al comando fork(). Al proceso original se le llama “padre” y al nuevo proceso “hijo”. El PPID de un proceso es el PID de su proceso padre.
El mecanismo de creación de nuevos procesos en Unix con el comando fork() se ve con más detalle en el apartado “Ciclo de vida de un proceso”.

UID y EUID

Normalmente estos dos identificadores coinciden pero hay excepciones.
El User ID (UID) del proceso identifica al creador del proceso, esto es a la persona que lo lanzó a correr. Este usuario y root son los únicos que pueden modificar al proceso. El UID se utiliza con fines de tarificación o accounting. El sistema de accounting carga a la cuenta del usuario identificado por el UID del proceso por los recursos del sistema que el proceso utilice (tiempo de CPU, impresoras, terminales, etc.).
El Effective User ID (EUID) en cambio se utiliza para determinar si el proceso tiene permiso para acceder a archivos y otros recursos del sistema.
La forma más habitual de hacer que el EUID de un proceso sea el de un usuario diferente del que lanza a correr el programa es activando el flag setuid en el archivo del programa. Un ejemplo de esto son los comandos que permiten a un usuario modificar su password, en que se debe modificar el archivo passwd o equivalente del sistema sobre el cual el usuario obviamente no tiene permiso de escritura. Habitualmente ese comando es un archivo de root con setuid y el proceso corre con EUID de root.

GID y EGID

Es totalmente análogo a los identificadores de usuario pero para grupos de usuarios. El GID se hereda del proceso padre. El EGID puede utilizarse igual que el EUID para controlar el acceso del proceso a archivos.
En la mayoría de los sabores actuales de Unix el proceso puede estar en varios grupos y se chequea contra toda la lista de grupos para definir si el proceso puede acceder o no a un recurso.

Información de ambiente
Directorio actual

El proceso mantiene actualizado cual es su directorio de trabajo.

Variables de ambiente globales.

Son heredadas por los procesos hijos.

Variables de ambiente locales.

Solamente existen en el proceso que las define.

Terminal de control.

En general los procesos están asociados a una terminal de control. Esta terminal determina el valor por defecto de los archivos stdin, stdout y stderr del proceso.
Una excepción a esto son los procesos llamados daemons, que una vez lanzados se desvinculan de su terminal de control y siguen ejecutando inclusive después de cerrada la sesión de usuario desde la cual se lanzaron a correr.

Información de E/S

El kernel mantiene descriptores de los archivos abiertos por el proceso.
Están siempre definidos los archivos de entrada, salida y error estándar (stdin, stdout y stderr). Por defecto están asociados con el teclado y la pantalla del terminal de control del proceso pero pueden ser redireccionados a un archivo cualquiera. Todos los shells prevén un mecanismo para hacer este redireccionamiento en el momento de lanzar a correr un programa.

Espacio de direcciones virtual

En la mayoría de los sistemas multiproceso como Unix, cada proceso tiene la ilusión de disponer para si el espacio de direcciones completo del procesador. En realidad el procesador ve un espacio de direcciones virtual. Este espacio está organizado en secciones para el código (text), datos, stack y otras y generalmente está dividido en páginas. En un instante dado una página puede estar residiendo en la memoria física del procesador o puede estar almacenada en disco en un procedimiento llamado “swapping”. El sistema operativo, con el auxilio del hardware, mantiene al día una tabla con el estado de cada página de memoria del proceso.

Estado de un proceso

Los estados básicos en los que puede estar un proceso son los siguientes:
* Durmiendo (asleep). En general a la espera de algún recurso compartido.
* Listo para ejecutar (runnable). A la espera que le toque el turno en el uso de la CPU.
* Ejecutando (running). Puede estar ejecutando en modo kernel o en modo usuario.
A su vez el proceso (o partes del espacio de memoria virtual del proceso) puede estar cargado en memoria o “swapped” a disco.

Ciclo de vida de un proceso

El mecanismo de creación de un proceso en Unix es un poco peculiar. Un proceso se crea invocando a una función del sistema operativo llamada fork(). La función fork() crea una copia idéntica del proceso que la invoca con excepción de:
* El nuevo proceso tiene un PID diferente * El PPID del nuevo proceso es el PID del proceso original * Se reinicia la información de tarificación del proceso (uso de CPU, etc.)
Al retorno de fork() se siguen ejecutando las siguientes sentencias del programa en forma concurrente. Para distinguir entre los dos procesos la función fork() devuelve un cero al proceso hijo y el PID del nuevo proceso al proceso padre. Normalmente el proceso hijo lanza luego un nuevo programa ejecutando alguna variante de comando exec(). En el recuadro puede verse un ejemplo del uso de fork.

Si este es el mecanismo para crear un proceso, entonces ¿quién lanza a correr el primer proceso? Luego del boot del sistema el kernel instala y deja corriendo un proceso llamado init con PID=1. Una de las funciones principales de init es lanzar mediante fork() intérpretes de comandos que a su vez lanzarán los scripts de inicialización del sistema y los procesos de los usuarios. Además de init el kernel lanza algunos procesos más cuyo nombre y función varía en los diferentes sabores de Unix. A excepción de estos procesos lanzados por el kernel al inicio, todos los demás son descendientes de init.
Normalmente un proceso termina invocando a la función exit() pasando como parámetro un código de salida o exit code. El destinatario de ese código de salida es el proceso padre. El proceso padre puede esperar la terminación de su proceso hijo invocando la función wait(). Esta función manda al padre a dormir hasta que el hijo ejecute su exit() y devuelve el exit code del proceso hijo.
Cuando el proceso hijo termina antes que el padre, el kernel debe conservar el valor del exit code para pasarlo al padre cuando ejecute wait(). En esta situación se dice que el proceso hijo está en el estado zombie. El kernel devuelve todas las áreas de memoria solicitadas por el proceso pero debe mantener alguna información sobre el proceso (al menos su PID y el exit code).
Cuando el proceso padre termina primero el kernel encarga a init la tarea de ejecutar el wait() necesario para terminar todo en forma ordenada. A menudo initfalla en esta función y suelen quedar procesos en estado zombie hasta un nuevo reboot. Dado que un proceso zombie no consume recursos fuera de su PID, esto por lo general no provoca problemas.

Threads

Cambio de contexto entre procesos:
* código (“text”)
* datos
* stack
* tablas varias
Varias secuencias de ejecución (threads) pueden agruparse en un proceso y compartir algunos segmentos
Ventajas:
* simplifica cambio de contexto y comunicación
* facilita el uso de múltiples procesadores
Soportado por la mayoría de los SO actuales (Digital Unix, DEC OSF/1, Solaris 2, AIX, HP-UX, SGI Irix, Linux)
Posix standard
Bibliotecas provistas por terceros

Señales (signals)

Las señales de Unix son un mecanismo para anunciar a un proceso que ha sucedido cierto evento que debe ser atendido. La lista de posibles señales a comunicar a los procesos está fija, con algunas variaciones de un sabor a otro de Unix.
La recepción de una señal en particular por parte de un proceso provoca que se ejecute una subrutina encargada de atenderla. A esa rutina se le llama el “manejador” de la señal (signal handler). Un proceso puede definir un manejador diferente para sus señales o dejar que el kernel tome las acciones predeterminadas para cada señal.
Cuando un proceso define un manejador para cierta señal se dice que “captura” (catch) esa señal.
Si se desea evitar que determinada señal sea recibida por un proceso se puede solicitar que dicha señal sea ignorada o bloqueada. Una señal ignorada simplemente se descarta sin ningún efecto posterior. Cuando alguien envía a cierto proceso una señal que está bloqueada la solicitud se mantiene encolada hasta que esa señal es explícitamente desbloqueada para ese proceso. Cuando la señal es desbloqueada la rutina de manejo de la señal es invocada una sola vez aunque la señal haya sido recibida más de una vez mientras estaba bloqueada.
Si bien un proceso tiene ciertas libertades para configurar como reacciona frente a una señal (capturando, bloqueando o ignorando la señal), el kernel se reserva ciertos derechos sobre algunas señales. Así, las señales llamadas KILL y STOP no pueden ser capturadas, ni bloqueadas, ni ignoradas y la señal CONT no puede ser bloqueada.
La señal KILL provoca la terminación de un proceso. La señal STOP provoca la detención del proceso que queda en el estado “stopped” hasta que alguien le envíe la señal CONT.
Una señal puede enviarse desde un programa utilizando llamadas al sistema operativo, o desde la línea de comandos de un shell utilizando el comando kill. Al comando kill se le pasa como parámetro el número o nombre de la señal y el PID del proceso. El uso más habitual del comando es para enviar una señal TERM o KILL para terminar un proceso, de ahí su nombre.
Para muchas de las señales la acción predeterminada consiste en terminar el proceso. En algunos casos se genera además un core dump. Un core dump es un archivo con una imagen del estado del sistema que permite al desarrollador de un programa diagnosticar problemas con la ayuda de un debugger. Al usuario final de un programa esta imagen rara vez le sirve de ayuda.
La lista de posibles señales puede obtenerse para cada sistema a través del man del comando kill o de la función kill() del sistema operativo. En la tabla se listan las utilizadas más a menudo por un administrador.

ID Nombre Uso habitual
1 SIGHUP Usualmente para releer configuración
9 SIGKILL El kernel destruye el proceso
15 SIGTERM Terminación “elegante”, en general termina enviándose un KILLL a sí mismo
SIGSTOP El kernel pasa el proceso a stopped
SIGCONT
SIGUSR Definidas por el usuario o más bien por quien programó el proceso
Prioridad de ejecución y valor “nice”

Cuando hay más de un proceso en el estado “listo para ejecutar”, el kernel le asigna el uso de la CPU al de mayor prioridad en ese momento. En el caso de Unix esta prioridad varía dinámicamente. Las diferentes versiones y sabores de Unix utilizan diferentes algoritmos de planificación del uso de la CPU (algoritmos de scheduling), pero en todos los casos tienen características similares:
– procuran ser justos con los diferentes procesos
– procuran dar buena respuesta a programas interactivos
Para eso los algoritmos consideran parámetros como cuanto uso de CPU ha hecho el proceso recientemente, si pasa mucho tiempo dormido a la espera de un evento de teclado (sería un proceso interactivo), etc..
El administrador del sistema o el usuario dueño de un proceso pueden influir en el algoritmo de scheduling a través del llamado valor nice. Este es un número que se asigna a cada proceso e indica que tan “nice” es el proceso para con los demás.
Este valor es considerado por el algoritmo de scheduling de manera que un proceso con valor nice alto estará en desventaja frente a otro con valor nice menor a la hora de decidir a quien asignar la CPU. Como ejemplo veamos el algoritmo utilizado por alguna versión de AIX:
P = min + nice + (0.5 x recent)
Donde P indica la prioridad dinámica (a menor P mayor prioridad) y recent es una medida de cuanto ha recibido la CPU el proceso recientemente. Recent se calcula de la siguiente forma:
* Inicialmente vale 0
* Al final de cada time slice (aprox. 10 milisegundos) recent se incrementa en 1 para el proceso que está usando la CPU
* Una vez por segundo se divide por dos el valor recent para todos los procesos
Normalmente el valor nice se hereda del proceso padre. El dueño del proceso o el propio proceso pueden elevar su valor nice (menor prioridad). El superusuario puede modificar el valor nice de todos los procesos a gusto.
En los sistemas “a la BSD” el valor del número nice puede variar entre -20 y +20, siendo por defecto 0.
En System V en cambio los valores posibles van de 0 a 39, siendo 20 el valor por defecto.
Se puede modificar el valor nice por defecto en el momento de lanzar un programa lanzándolo a correr con el comando nice, o posteriormente utilizando el comando renice.

Supervisión de procesos
Comando ps (process status)

La herramienta básica para diagnosticar problemas relacionados con procesos es el comando ps. Este comando genera un reporte de un renglón por cada proceso, brindando abundante información sobre cada uno.
El comando ps en los dos sabores básicos de Unix (BSD y System V) difiere en el nombre y función de los parámetros, en la información que brindan sobre cada proceso y en los criterios de ordenación de los procesos.
En ambos casos si ejecuto ps sin parámetros solamente se listará información básica sobre los procesos que pertenecen a mi usuario. A través del uso de parámetros adecuados puedo agregar más columnas (más información sobre cada proceso) o más filas (más procesos) al reporte. Un conjunto de parámetros que permite ver todos los procesos con un grado de detalle que en general es adecuado es:
ps -uax (BSD)
ps -elf (System V)
Los campos de información más importantes desplegados por ps para cada proceso son:
* Usuario (USER)
* Identificadores de proceso (PID, PPID)
* Uso de recursos reciente y acumulado (%CPU, %MEM, TIME)
* Estado del proceso (STAT, S)
* comando invocado (COMMAND)

Comando top

El comando top actualiza un reporte similar al generado por ps a intervalos periódicos. Normalmente top ordena los procesos por uso de CPU en forma decreciente. Permite además de manera amigable invocar los comandos kill y renice sobre el proceso.

Comando pstree

En algunos sistemas está disponible el comando pstree, que lista los procesos y sus descendientes en forma de árbol. Esto permite visualizar rápidamente los procesos que están corriendo en el sistema.

Carga del sistema reportada por w o uptime

Los comandos w (who) y uptime reportan tres números que indican la carga promedio del sistema en un intervalo de 1, 5 y 15 minutos respectivamente. Más precisamente el parámetro reportado es el promedio de la cantidad de procesos listos para correr. Este valor es un indicador rápido de la actividad del sistema y suele vigilarse para detectar sobrecargas en el mismo, o registrarse periódicamente para un análisis posterior en caso de problemas. La relación entre este número y que tan “pesado” se vuelve el sistema para los usuarios varía fuertemente dependiendo de las características del sistema (de la cantidad de procesadores p. ej.) y de los procesos que se están ejecutando.

Comando vmstat (virtual memory stats)

El comando vmstat reporta varias estadísticas que mantiene el kernel sobre los procesos, la memoria y otros recursos del sistema. Se puede tomar una instantánea de esas estadísticas o repetirlo cierta cantidad de veces a intervalos de algunos segundos.
Alguna de la información reportada por vmstat es la siguiente:
* Cantidad de procesos en diferentes estados (listo para correr, bloqueado, “swapeado” a disco.
* Valores totales de memoria asignada a procesos y libre.
* Estadísticas sobre paginado de memoria (page faults, paginas llevadas o traídas a disco, páginas liberadas)
* Operaciones de disco
* Uso de CPU reciente clasificado en inactivo, ejecutando en modo usuario y ejecutando en modo kernel

Tomando acciones

A menudo, por algún bug en el programa o por algún error de operación, los procesos no terminan correctamente y es necesario terminarlos por algún método más violento. El procedimiento usual en estos casos es obtener el PID del proceso con la ayuda de ps y luego terminarlo con el comando kill. Enseñarle a los usuarios a hacer estas operaciones con sus procesos colgados es una muy buena inversión de tiempo para un administrador de sistema.
También suele suceder que algún proceso quede fuera de control y comience a acaparar algún recurso del sistema (memoria, disco o CPU). Esto puede suceder por un error de programación, por un error de configuración, por intentar correr algún proceso que necesita más recursos que los disponibles o directamente por mala intención.
Sea cual sea el caso se hace necesario que el dueño del proceso o el administrador del sistema tomen medidas para frenar o terminar a ese proceso.
En general el primer síntoma es “el sistema está muy pesado” (o el teléfono sonando con reclamos de los usuarios). El primer paso será identificar a el o los procesos problemáticos utilizando las herramientas ya vistas. Si la situación es tan grave que dificulta la operación del administrador, puede ser recomendable lanzar un shell de root con prioridad alta utilizando el comando nice.
Si no está claro por qué el proceso puede estar acaparando recursos se puede intentar detenerlo con la señal STOP hasta ubicar al usuario dueño del proceso. Una vez ubicado al usuario si se considera necesario que el proceso continúe se le puede bajar la prioridad de ejecución con el comando renice.
Las siguientes opciones son o bien pedirle al proceso que tenga a bien terminar con la señal TERM (kill -15 PID) o bien terminarlo por la fuerza con la señal KILL (kill -9 PID).

Funciones:

int getpid() regresará el pid de un proceso que llame a esta función.

execl()  realiza la ejecución (execute) y sale (leave), es decir que un proceso será ejecutado y entonces terminado por execl.

int fork() cambia un proceso único en 2 procesos idénticos, conocidos como el padre (parent) y el hijo (child). En caso de éxito, fork() regresa 0 al proceso hijo y regresa el identificador del proceso hijo al proceso padre. En caso de falla, fork() regresa -1 al proceso padre, pone un valor a errno, y no se crea un proceso hijo. El proceso hijo tendrá su propio identificador único (PID).

int wait() (int *status) forzará a un proceso padre para que espere a un proceso hijo que se detenga o termine. La función regresa el PID del hijo o -1 en caso de errror. El estado de la salida del hijo es regresado en status.

void exit(int status) termina el proceso que llama a esta función y regresa en la salida el valor de status.

Proceso DAEMON:

Los procesos daemon no se asocian con ningún usuario pero hace funciones del sistema, tales como la administración y control de redes, ejecución de actividades dependientes del tiempo, spooling de impresión, etc… Los procesos daemon se ejecutan en modo usuario.

Proceso ZOMBI:

Son procesos que han terminado pero que siguen apareciendo en la tabla de procesos. Se pueden deber a errores de programación y pueden ser el síntoma de un sistema lento o que provoca problemas. Y que nunca recibió una señal por parte del proceso padre que lo creo.

Esta entrada fue publicada en Consultas, Otros. Guarda el enlace permanente.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s