Академический Документы
Профессиональный Документы
Культура Документы
El objetivo del presente laboratorio tiene como finalidad reforzar los conceptos acerca de
procesos que se han visto en clase. Siempre ha resultado dificil explicar estos conceptos, por
eso en esta oportunidad me he decidido en hacer una traducción libre del libro
Procesos
Un proceso en Linux es una única instancia de un programa ejecutandose o ejecutable. Cada
proceso en un sistema Linux tiene los siguientes atributos:
Cuando Linux recién inicia, hay sólo un proceso visible en el sistema. Este proceso es
llamado 'init', y su PID es 1. La única forma de crear un proceso nuevo en Linux es duplicar
un proceso existente, así 'init' es el ancestro de todos los subsequentes procesos. Cuando un
proceso se duplica, el proceso padre y el proceso hijo son virtualmente iguales (excepto por
cosas como el PIDs, PPIDs y tiempos de ejecución); el código, data y pila del proceso hijo
son una copia del proceso padre, y ellos continuan ejecutando el mismo código. Un proceso
hijo puede, sin embargo, reemplazar su código con el de otro archivo ejecutable, logrando
diferenciarse conel de su padre. Cuando un proceso hijo termina, su muerte es comunicada a
su padre de modo que el padre pueda tomar alguna decisión apropiada. Es muy común para
un proceso padre suspenderse hasta que uno de sus hijos termine. Por ejemplo cuando un
shell ejecuta alguna utilidad en primer plano (foreground), primero este se duplica en dos
procesos shells; el proceso shell hijo reemplaza su código con el de la utilidad, mientras tanto
el shell padre espera por el proceso hijo a que termine. Cuando el hijo termina, el proceso
padre se despierta y presenta al usuario el siguiente promp de shell.
La siguiente figura ilustra la forma en que el shell ejecuta una utilidad; se ha indicado las
llamadas al sistema que son responsables por cada fase de la ejecución.
Cómo un shell ejecuta una utilidad
Observemos algunos programas simples que introducen llamadas al sistema una por una. A
continuación algunas llamadas al sistema que se explicaran luego:
Nombre Función
fork Duplica un proceso.
getpid Obtiene el numero identificador (ID) del proceso.
getppid Obtiene el número identificador (ID) del proceso padre.
exit Termina un proceso.
wait Espera por un proceso hijo.
exec.. Reeplaza el código, data, y pila de un proceso.
fork() es una llamada al sistema extraña, porque un proceso (el original) lo invoca, pero dos
procesos (el original y su hijo) retornan de él. Ambos procesos continuan ejecutando el
mismo código de forma concurrente, pero ellos tienen espacios de data y pila completamente
separados.
Esto me hace recordar una gran historia de ciencia ficción que una vez leí, de un hombre que
pasa frente a un facinante stand de un circo. El vendedor en el stand le dice al hombre que el
stand es un replicador de materia, cualquiera que camina a través del stand se duplica. La
persona original sale del stand ileso, pero la persona duplicada sale caminando sobre la
superficie de Marte, como un esclavo de la tripulación marciana. El vendedor entonces le dijo
al hombre que le daría un millón de dólares si el permitía duplicarse a sí mmismo, y el estuvo
de auerdo. El hombre camino feliz a través de la maquina, esperando juntar su millón de
dólares. . . y salió sobre la superficie de Marte. Mientras que de regreso a la Tierra, su
duplicado está alejandose con un alijo de dinero. La pregunta es si pasases frente al stand, que
hubieras hecho?
Linux añade la llamada al sistema clone(), que para diferentes usos, es lo mismo que
fork(). Sin emabargo clone() comparte algo de su contexto de ejecución (en lugar de una
simple copia como con fork()) con el proceso padre, tales como el espacio de memoria, la
table de decriptores de archivos y la tabla de señales.
Un proceso puede obtener su propio PID y el PID del proceso padre epleando las llamadas al
sistema getpid() y getppid(), respectivamente
Llamadas al sistema:
pid_t getpid(void)
pid_t getppid(void)
getpid() y getppid() retornan los números ID del proceso y del proceso padre
respectivamente. Ellos siempre retornan con éxito. El número ID del padre del proceso que
tiene PID 1 es 1.
Para ilustrar la operación fork(), se presenta un pequeño programa que duplica y luego se
ramifica basado en el valor de retorno de fork():
I'm the original process with PID 13292 and PPID 13273.
I'm the parent process with PID 13292 and PPID 13273.
My child's PID is 13293.
I'm the child process with PID 13293 and PPID 13292.
PID 13293 terminates. ...child terminates.
PID 13292 terminates. ...parent terminates.
$ _
El PPID del padre se refiere al PID del shell que ejecutó el programa "myfork".
Aquí una advertencia. Como tu pronto lo verás, es peligroso para un padre terminar sin
esperar por la muerte de un hijo. La única razón que el padre no espera por su hijo es por que
aún no se ha descrito la llamada al sistema wait().
Procesos huérfanos
Si un padre muere antes que su hijo, el hijo es automaticamente adoptado por el proceso
"init", con PID 1. Para ilustrar esto he modificado el programa previo insertando la sentencia
sleep() en el código del hijo. Esto asegura que el proceso padre termine antes que el hijo.
El kernel se asegura que todos los hijos de procesos que terminaron y que están huerfanos y
adoptados por "init" se coloque su PPID a 1. El proceso "init" siempre acepta la terminación
de los códigos de sus hijos.
El código de terminación de un proceso hijo puede ser usado para una variedad de propositos
por el proceso padre. Los shells puede acceder al código de terminación de su último proceso
hijo vía una de sus variables especiales. Por ejemplo, el C shell almacena el código de
terminación del último comando en la variable $status:
En todos los otros shells, el valor de retorno es devuelto en la variable especial del shell $?.
Procesos Zombis
Un proceso que termina no puede abandonar el sistema hasta que su padre acepte su código
de retorno. Si el proceso padre ya está muerto tendrá que ser adoptado por el proceso "init", el
cual siempre acepta el código de retorno de sus hijos. Sin embargo si el padre de un proceso
está vivo pero nunca ejecuta un wait(), el código de retorna del proceso nunca será aceptado y
el proceso permanecerá zombi. Un proceso zombi no tiene código, data o pila algun, así que
no consume muchos recursos del sistema, pero sí ocupa espacio en la lista de tareas del
sistema. Muchos procesos zombis pueden requerir que el administrador del sistema
intervenga (1).
(1) El algunos sistemas operativos el administrador de procesos liberan las entradas de los
procesos zombis.
El siguiente programa creó un proceso zombi, el cual fue indicado en la salida del programa
ps. Cuando maté el proceso padre, el hijo fue adoptado por "ini" y se le permitió descansar en
paz.
$ cat zombie.c ...list the program.
#include <stdio.h>
main ()
{
int pid;
pid = fork (); /* Duplicate */
if (pid != 0) /* Branch based on return value from fork () */
{
while (1) /* Never terminate, and never execute a wait () */
sleep (1000);
}
else
{
exit (42); /* Exit with a silly number */
}
}
$ ./zombie & ...execute the program in the background.
[1] 15896
$ ps ...obtain process status.
PID TTY TIME CMD
15870 pts2 00:00:00 bash ...the shell.
15896 pts2 00:00:00 zombie ...the parent.
15897 pts2 00:00:00 zombie <defunct> ...the zombie.
15898 pts2 00:00:00 ps
$ kill 15896 ...kill the parent process.
[1] + Terminated ./zombie
$ ps ...notice the zombie is gone now.
PID TTY TIME CMD
15870 pts2 00:00:00 bash
15901 pts2 00:00:00 ps
$ _
Si el byte más a la drecha de status es 0, el byte más a la izquierda contiene los ocho bits
más bajos del valor retornado por la invocación del hijo a exit() o return().
Si el byte más a la derecha no es cero, los 7 bits más a la derecha son iguales al número de la
señal que causó que el hijo termine y el bit restante del byte más a la derecha es colocado a 1
si el hijo procduce un core dump.
Si un proceso ejecuta un wait() y no tiene hijos, wait() retorna inmediatamente con -1. Si
un proceso ejecuta un wait() y uno o más de sus hijos ya están zombis, wait() retorna
inmediatamente con el estado de uno de los zombis.
En el siguiente ejemplo, el proceso hijo terminó antes del final del programa ejecutando un
exit() con código de retorno 42. Mientras tanto el proceso padre ejecuta un wait() y se
suspende hasta que reciba el código de terminación de su hijo. En este punto, el padre muestra
información sobre la desaparición de su proceso hijo y ejecuta el resto del programa.:
Funciones de Libreria:
Las funciones de libreria de la llamada al sistema exec reemplaza el código, la data y la pila
del proceso que lo invoca, por el de un ejecutable cuya ruta de ubicación es almacenada en
path.
La familia exec mostrada anteriormente no son realmente llamdas al sistema, ellas son
funciones de libreria de C que invocan la llamada al sistema execve(). execve() es dificil
usarlo directamente, debido a que contiene algunas opciones raramente usadas.
Cada proceso tiene un directorio actual de trabajo que es usado cuando se procesa una ruta
relativa. Un proceso hijo hereda el directorio de trabajo actual de su padre. Por ejemplo
cuando alguna utilidad es ejecutada desde un shell, su proceso hereda el directorio de trabajo
actual del shell. Para cambiar el directorio actual de trabajo de un proceso, usa chdir().
Si nice() tiene éxito retorna el nuevo valor de prioridad; en caso contrario retorna -1. Note
que esto puede causar problemas, debido a que un valor de nice() de -1 es legal.
Llamadas al sistema:
uid_t getuid()
uid_t geteuid()
gid_t getgid()
gid_t getegid()
A continuación se listan las llamadas al sistema que te permiten colocar los IDs real y
efectivo de un proceso.
Llamadas al sistemal:
int setuid(uid_t id)
int seteuid(uid_t id)
int setgid(gid_t id)
int setegid(gid_t id)
Estas llamadas tienen éxito sólo si son ejecutadas por el superusuario, o si el parametro id es
el usuario (grupo) real o efectivo con ID del proceso que lo invoca. Ellos retornan 0 si tienen
éxito; en caso contarrio ellos retornan -1.
Redirección
Cuando un proceso ejecuta un fork(), el hijo hereda una copia de los descriptores de
archivos de su padre. Cuando un proceso ejecuta un exec(), todos los descriptores de
archivos con la propiedad de non-close-on-exec permanecen sin afectarse, incluyendo los
canales de entrada estándar, salida estándar y error estándar. El shell de Linux usa estos dos
pedazos de información para implementar redirección. Por ejemplo, digamos que usted
escribe el siguiente comando en la terminal:
$ ls > ls.out
Para llevar a cabo la redirección, el shell lleva a cabo las siguientes series de acciones:
El shell padre invoca a fork() y luego espera por el shell hijo para terminar.
El shell hijo abre el archivo "ls.out," creandolo o truncandolo, lo que sea necesario.
Cuando el shell hijo termina, el padre reanuda su ejecución. Los descriptores de archivos del
padre no son afectados por las acciones del hijo, debido a que cada hijo mantiene su propia
tabla privada de descriptores.
Para redireccionar el canal del error estándar adicionalmente a la salida estándar, el shell
debería simplemente tener que duplicar dos veces el descriptor de "ls.out"; una vez para el
decriptor 1 y o tra vez para el descriptor 2.
Pipes
Los pipes son mecanismos de comunicación entre procesos permitiendo a dos o más procesos
enviar información el uno al otro. Ellos son comunmente usados dentro de los shells para
conectar la salida estándar de una utilidad a la entrada estándar de otra. Por ejemplo, aquí hay
un simple comando shell que determina cuántos usuarios están en el sistema::
$ who | wc -1
La utilidad who genera una línea de salida por usuario. esta salida luego es "entubada"
("piped") dentro de la utilidad wc, la cual cuando se invocó con la opción -l ("ele"), muestra
el total del número de líneas de su entrada estándar. Así, el comando entubado astutamente
calcula el total de número de usuarios contando el número de líneas que el comando who
generó.
Diagrama de un entubamiento
Es importante recalcar que tanto el proceso que escribe como el proceso que lee en el pipe se
ejecutan concurrentemente; un pipe automaticamente baferiza la salida del escritor y suspende
la escritura si el pipe está lleno. De forma análoga, si un pipe está vacío, el lector se suspende
hasta alguna salida llegue a estar disponible
Linux provee dos tipos de pipes. El más simple es el pipe sin nombre, que es el que usa el
shell para entubar data entre procesos. Un pipe con nombre permite que dos procesos
independientes encuentren el pipe. (NdT para los laboratorios solo abarcaremos hasta pipes
sin nombre)
Si un proceso lee de un pipe cuya escritura ha sido cerrada, el read() retorna 0, indicando el
final de la entrada.
Si un proceso lee de un pipe vacío cuya escritura está aún abierta, este se bloquea hasta que
alguna entrada llegue a estar disponible.
Si un proceso trata de leer más bytes de un pipe que los que está presentes, todo el contenido
son retornados y read() devuelve la cantidad de bytes leidos.
Si un proceso escribe a un pipe cuyo read ha sido cerrado, el write falla y al escritor le es
enviado la señal SIGPIPE. La acción por defecto de esta señal es terminar el escritor.
Si un escritor escribe menos bytes a un pipe de los que un pipe pueda manejar, se garantiza
que el write() sea atómico; esto es, el proceso escritor completará su llamada al sistema sin
que sea interrumpido por otro proceso. Si un proceso escrinbe más bytes de los que un pipe
pueda manejar, no se puede aplicar la garantia de atomicidad.
Debido a que el mecanismo de acceso a un pipe sin nombre es a través del descriptor de
archivos, tipicamente sólo el proceso que crea un pipe y su descendientes pueden usar el pipe
[2]. lseek() no tiene significado cuando se aplica a un pipe.
Si el kernel no puede asignar suficiente espacio para un nuevo pipe, pipe() retorna -1; de
otra forma este retorna 0.
int fd[2];
pipe(fd);
A continuación un pequeño programa que usa un pipe para permitir al padre leer un mensaje
de su hijo:
$ cat talk.c ...list the program.
#include <stdio.h>
#define READ 0 /* The index of the read end of the pipe */
#define WRITE 1 /* The index of the write end of the pipe */
char* phrase = "Stuff this in your pipe and smoke it";
main ()
{
int fd [2], bytesRead;
char message [100]; /* Parent process' message buffer */
pipe (fd); /*Create an unnamed pipe */
if (fork () == 0) /* Child, writer */
{
close(fd[READ]); /* Close unused end */
write (fd[WRITE],phrase, strlen (phrase) + 1); /* include NULL*/
close (fd[WRITE]); /* Close used end*/
}
else /* Parent, reader*/
{
close (fd[WRITE]); /* Close unused end */
[Page 504]
bytesRead = read (fd[READ], message, 100);
printf ("Read %d bytes: %s\n", bytesRead, message); /* Send */
close (fd[READ]); /* Close used end */
}
}
$ ./talk ...run the program.
Read 37 bytes: Stuff this in your pipe and smoke it
$ _
Note que el hijo incluye el terminador NULL en la variable phrase como parte del mensaje
de modo que el padre pueda facilmente presentarlos en pantalla. Cuando un proceso envía
más de un mensaje de longitud variable en un pipe, este debe usar un protocolo para indicar al
lector el final de mensaje. Los métodos pra hacer esto incluyen:
El shell en Linux usa pipes sin nombre para construir pipelines. Ellos usan un truco similar al
mecanismo de la redirección descrito anteriormente para conectar la salida estándar de un
proceso a la entrada estándar de otro. Para ilustrar este enfoque, a coninuación se muestra el
código fuente de un programa que ejecuta dos programas conectando la salida estándar del
primero con la entrada estándar del segundo. Este asume que ningún programa es invocado
con opciones, y que los nombres de los programas son listados en la línea de comandos.
$ cat connect.c ...list the program.
#include <stdio.h>
#define READ 0
#define WRITE 1
main (argc, argv)
int argc;
char* argv [];
{
int fd [2];
pipe (fd); /* Create an unnamed pipe */
if (fork () != 0) /* Parent, writer */
{
close (fd[READ]); /* Close unused end */
dup2 (fd[WRITE], 1); /* Duplicate used end to stdout */
close (fd[WRITE]); /* Close original used end */
execlp (argv[1], argv[1], NULL); /* Execute writer program */
perror ("connect"); /* Should never execute */
}
else /* Child, reader */
{
close (fd[WRITE]); /* Close unused end */
dup2 (fd[READ], 0); /* Duplicate used end to stdin */
close (fd[READ]); /* Close original used end */
execlp (argv[2], argv[2], NULL); /* Execute reader program */
perror ("connect"); /* Should never execute */
}
}
$ who ...execute "who" by itself.
glass pts/1 Feb 15 18:45 (:0.0)
$ ./connect who wc ...pipe "who" through "wc".
1 6 42 ...1 line, 6 words, 42 chars.
$ _
Su tarea será probar estos programas tanto en Linux (Ubuntu) como en Minix, tratando de
observar cuál es la diferencia entre ambos. El laboratorio se llevará a cabo en Minix.
Nota: Para compilar cualquiera de los programas arriba escritos, escriba en el prompt la
siguiente línea:
cc -o nombre_ejecutable nombre_fuente.c