Вы находитесь на странице: 1из 19

Laboratorio 1

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

Linux for Programmers and Users by Graham Glass y King Ables


Cap. 12 Systems Programming
Section 12.4 Process Management

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:

algo de codigo (tambiém llamado texto)


algo de data
una pila
y un único número ID de proceso (PID)

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.

Creando un nuevo proceso: fork()


Un proceso puede duplicarse a sí mismo usando fork()

Descripción de la llamada al sistema fork().

Llamada al sistema: pid_t fork (void)


fork() causa que un proceso se duplique. El proceso hijo es un duplicado casi exacto del
proceso padre original; este hereda una copia del código, data, pila, descriptores de archivos
abiertos, y tabla de señales del padre. Sin embargo el padre y el hijo tienen diferente el
identificador (ID) de procesos.
Si fork() tiene éxito, retorna el PID del hijo al proceso padre y retorna 0 al proceso hijo. Si
este falla retorna -1 al proceso padre y el proceso hijo no es creado.

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

Descripciones de las llamadas al sistema getpid() y getppid()

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():

$ cat myfork.c ...list the program.


#include <stdio.h>
main ()
{
int pid;
printf ("I'm the original process with PID %d and PPID %d.\n",
getpid (), getppid ());
pid = fork (); /* Duplicate. Child and parent continue from here */
if (pid != 0) /* pid is non-zero, so I must be the parent */
{
printf ("I'm the parent process with PID %d and PPID %d.\n",
getpid (), getppid ());
printf ("My child's PID is %d\n", pid);
}
else /* pid is zero, so I must be the child */
{
printf ("I'm the child process with PID %d and PPID %d.\n",
getpid (), getppid ());
}
printf ("PID %d terminates.\n", getpid () ); /* Both processes
execute this */
}
$ ./myfork ...run the program.

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.

Aquí está el programa y su salida correspondiente:

$ cat orphan.c ...list the program.


#include <stdio.h>
main ()
{
int pid;
printf ("I'm the original process with PID %d and PPID %d.\n",
getpid (), getppid ());
pid = fork (); /* Duplicate. Child and parent continue from here */
if (pid != 0) /* Branch based on return value from fork () */
{
/* pid is nonzero, so I must be the parent */
printf ("I'm the parent process with PID %d and PPID %d.\n",
getpid (), getppid ());
printf ("My child's PID is %d\n", pid);
}
else
{
/* pid is zero, so I must be the child */
sleep (5); /* Make sure that the parent terminates first */
printf ("I'm the child process with PID %d and PPID %d.\n",
getpid (), getppid ());
}
printf ("PID %d terminates.\n", getpid () ); /* Both processes
execute this */
}
$ ./orphan ...run the program.
I'm the original process with PID 13364 and PPID 13346.
I'm the parent process with PID 13364 and PPID 13346.
PID 13364 terminates.
I'm the child process with PID 13365 and PPID 1. ...orphaned!
PID 13365 terminates.
$ _

Terminanando un Proceso exit()


Un proceso termina en cualquier momento ejecutando exit().

Descripción de la llamada al sistema exit().

Llamada al sistema: void exit (int status)


exit() cierra todos los descriptores, libera el área asignada a su código, data, pila, y luego el
proceso termina. Cuando un proceso hijo termina, este envía a su padre la señal SIGCHLD y
espera que su código de estado de finalización sea acptado. Sólo los 8 bits más bajos del
estado son usados, así los valores son limitados a 0255. Un proceso que está esperando que su
padre acepte su código de retorno se le denomina proceso zombi. Un padre acepta la
terminación del código de un proceso hijo ejecutando wait(), la cual se drescribe en breve.

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.

exit() nunca retorna.

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:

% cat myexit.c ...list the program.


#include <stdio.h>
main ()
{
printf ("I'm going to exit with return code 42\n");
exit (42);
}
% ./myexit ...run the program.
I'm going to exit with return code 42
% echo $status ...display the termination code.
42
% _

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
$ _

Esperando por un hijo: wait()


Un padre puede esperar a que uno de sus hijos termine y luego aceptar el código de
terminación de sus hijos ejecutando wait()

Descripción de la llamada al sistema wait()

Llamada al sistema: pid_t wait (int* status)


wait() causa que un proceso se suspenda hasta que uno de sus hijos termine. Cuando una
llamada a wait() tiene éxito retorna el pid del proceso hijo que terminó y coloca un código
de estado en la variable status que se decodifica como sigue:

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

$ cat mywait.c ...list the program.


#include <stdio.h>
main ()
{
int pid, status, childPid;
printf ("I'm the parent process and my PID is %d\n", getpid ());
pid = fork (); /* Duplicate */
if (pid != 0) /* Branch based on return value from fork () */
{
printf ("I'm the parent process with PID %d and PPID %d\n",
getpid (), getppid ());
childPid = wait (&status); /* Wait for a child to terminate. */
printf ("A child with PID %d terminated with exit code %d\n",
childPid, status >> 8);
}
else
{
printf ("I'm the child process with PID %d and PPID %d\n",
getpid (), getppid ());
exit (42); /* Exit with a silly number */
}
printf ("PID %d terminates\n", getpid () );
}

$ ./mywait ...run the program.


I'm the parent process and my PID is 13464
I'm the child process with PID 13465 and PPID 13464
I'm the parent process with PID 13464 and PPID 13409
A child with PID 13465 terminated with exit code 42
PID 13465 terminates
$ _

Diferenciando un Proceso: exec


Un proceso puede reemplazar su actual código, data y pila con el de otro ejecutable usando
una función de librería que pertenece a la familia de la llamada al sistema exec. Cuando un
proceso ejecuta un exec, sus números de PID y PPID permanecen iguales, solo el código del
proceso que se está ejecutando cambia. La familia de la llamada al sitema exec trabaja como
se decribe a continuación:
Descripción las funciones de librería de la familia exec.

Funciones de Libreria:

int execl(const char* path,const char* arg0,const char* arg1,...,const


char* argn, NULL)
int execv(const char* path, const char* argv[ ])
int execlp(const char* path, const char* arg0, const char* arg1,..., const
char* argn,NULL)
int execvp(const char* path, const char* argv[ ])

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.

execvl() es identica a execlp(), y execv() es identico a execvp(), excepto que execl()


y execv() requieren la ruta del nombre del archivo ejecutable a ser proporcionado, mientras
que execlp() y execvp() usan la variable de ambiente $PATH para encontrar la ruta.

Si el ejecutable no es encontrado, la llamada al sistema retorna -1; de otra forma el proceso


que lo invocó reemplaza su código, data y pila desde el ejecutable y empieza a ejecutar el
nuevo código. Una llamda con éxito para alguna de las llamadas al sistema exec nunca
retorna.

execl() y execlp() invocan el ejecutable con la cadena de argumentos apuntado por


arg1..argn. arg0 debe ser el nombre del archivo ejecutable y la lista de argumentos debe ser
terminado con el caracter nulo.

execv() y execvp() invocan el ejecutable con la cadena de argumentos apuntado por


argv[1] .. argv[n], donde argv[n+1] is NULL. argv[0] debe ser el nombre del archivo
ejecutable.

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.

En el siguiente ejemplo, el programa muestra un pequeño mnesaje y luego su código es


reemplazado con el del ejecutable "ls". Note que execl() tuvo éxito y nunca retornó:

$ cat myexec.c ...list the program.


#include <stdio.h>
main ()
{
printf ("I'm process %d and I'm about to exec an ls -l\n",getpid ());
execl ("/bin/ls", "ls", "-l", NULL); /* Execute ls */
printf ("This line should never be executed\n");
}
$ ./myexec ...run the program.
I'm process 13623 and I'm about to exec an ls -l
total 125
-rw-r--r-- 1 glass cs 277 Feb 15 00:47 myexec.c
-rwxr-xr-x 1 glass cs 24576 Feb 15 00:48 myexec
$ _

Cambiando de directorio: chdir ()

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().

Descripción de la llamada al sistema chdir().

Llamada al sistemal: int chdir (const char* pathname)


chdir() coloca el directoiro actual de un proceso al directorio indicado en pathname. El
proceso debe tener permiso de ejecución desde el directorio para tener éxito.

chdir() retorna 0 si tiene éxito; de otra forma retorna -1.

En el siguiente ejemplo, el proceso imprime su directorio actual de trabajo, antes y después de


ejecutar chdir(), con el comando pwd usando la función de librería system():

$ cat mychdir.c ...list the source code.


#include <stdio.h>
main ()
{
system ("pwd"); /* Display current working directory */
chdir ("/"); /* Change working directory to root directory */
system ("pwd"); /* Display new working directory */
chdir ("/home/glass"); /* Change again */
system ("pwd"); /* Display again */
}

$ ./mychdir ...execute the program.


/home/glass
/
/home/glass
$ _

Cambiando prioridades: nice ()


Cada proceso tiene su valor de prioridad entre -20 y +19 que afecta la cantidad de tiempo de
CPU que le es asignado. En general, cuanto menor sea el valor de prioridad, más rápido se
ejecutará el proceso. Solo el superusuario y los procesos del kernel pueden tener prioridad
con valor negativa, y el login empiezan con prioridada 0. Un proceso hijo hereda el valor de
su prioridad de su padre, y puede cambiarlo usando nice().

Descripción de la función de librería nice ().

Función de librería: int nice (int delta)


nice() añade un delta al valor actual de prioridad. Solo el superusuario puede modificar un
delta que conduzca a un valor de prioridad negativo. Un valor de prioridad legal estriba entre
-19 y +20. Si se especifica un delta que conduce a un valor más allá del límite, el valor de
prioridad es truncado al límite.

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.

En el siguiente ejemplo, el proceso ejecuta el comando ps antes y después de un par de


llamadas nice(). Note que cuando la prioridad del proceso cambia, la siguiente invocación
del comando ps también tiene la prioridad baja.

$ cat mynice.c ...list the source code.


#include <stdio.h>
main ()
{
printf ("original priority\n");
system ("ps -l"); /* Execute a ps */
nice (0); /* Add 0 to my priority */
printf ("running at priority 0\n");
system ("ps -l"); /* Execute another ps */
nice (10); /* Add 10 to my priority */
printf ("running at priority 10\n");
system ("ps -l"); /* Execute the last ps */
}
$ mynice ...execute the program.
original priority
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY CMD
0 S 500 1290 1288 0 76 0 - 552 rt_sig pts/4 ksh
0 S 500 1549 1290 0 76 0 - 583 wait4 pts/4 a.out
0 S 500 1550 1549 0 80 0 - 889 - pts/4 ps
running at priority 0 ...adding 0 doesn't change it.
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY CMD
0 S 500 1290 1288 0 76 0 - 552 rt_sig pts/4 ksh
0 S 500 1549 1290 0 75 0 - 583 wait4 pts/4 a.out
0 S 500 1551 1549 0 78 0 - 638 - pts/4 ps
running at priority 10 ...adding 10 makes them run slower.
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY CMD
0 S 500 1290 1288 0 76 0 - 552 rt_sig pts/4 ksh
0 S 500 1549 1290 0 90 10 - 583 wait4 pts/4 a.out
0 S 500 1552 1549 0 87 10 - 694 pts/4 ps
$ _
Accediendo a los identificadores (IDs) de Usuario y Grupo
Lista de las llamadas al sistema que permiten leer los IDs real y efectivo de un proceso

Descripción de las llamada al sistema getuid(), geteuid(), getgid(), y getegid().

Llamadas al sistema:
uid_t getuid()
uid_t geteuid()
gid_t getgid()
gid_t getegid()

getuid() y geteuid() retorna el ID del usuario real y el usuario efectivo respectivamente,


del proceso que lo invocó.
getgid() y getegid() retorna el ID del grupo real y grupo efectivo respectivamente, del
proceso que lo invocó. Los números IDs corresponden a los IDs de usuarios y grupos listados
en los archivos "/etc/passwd" y "/etc/group" respectivamente.

Estas llamadas siempre tienen éxito.

A continuación se listan las llamadas al sistema que te permiten colocar los IDs real y
efectivo de un proceso.

Descripción de las llamadas al sistema setuid(), seteuid(), setgid(), y setegid().

Llamadas al sistemal:
int setuid(uid_t id)
int seteuid(uid_t id)
int setgid(gid_t id)
int setegid(gid_t id)

seteuid() y setegid() colocan el ID del usuario y grupo efectivo, respectivamente, del


proceso que lo invoca.
setuid() y setgid() colocan el ID del usuario y grupo efectivo y real, respectivamente, del
proceso que lo invoca, al valor especificado.

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.

Un programa simple: Procesando en segundo plano (Background)


A continuación un programa simple que hace uso de fork() y exec() para ejecutar un
programa en backgroound. El proceso original crea un hijo para que haga un "exec" del
ejecutable especificado y luego termine. El hijo herfano es automaticamente adoptado por
"init". Note como yo pase astutamente la lista de argumentoos desde main() a execvp()
pasando &argv[1] como el segundo argumento de execvp(). Note también que yo use
execvp() en lugar de execv() de modo que el programa pueda usar $PATH para encontrar el
archivo ejecutable.

$ cat background.c ...list the program.


#include <stdio.h>
main (argc, argv)
int argc;
char* argv [];
{
if (fork () == 0) /* Child */
{
execvp (argv[1], &argv[1]); /* Execute other program */
fprintf (stderr, "Could not execute %s\n", argv[1]);
}
}
$ background sleep 60 ...run the program.
$ ps ...confirm that it is in background.
PID TTY TIME CMD
10742 pts0 00:00:00 bash
10936 pts0 00:00:01 ksh
15669 pts0 00:00:00 csh
16073 pts0 00:00:00 sleep 60
16074 pts0 00:00:00 ps
$ _

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.

El shell hijo entonces duplica el descriptor de archivo de "ls.out" al descriptor de archivo de la


salida estándar, el número del descriptor es 1, y luego cierra el descriptor original de "ls.out".
Toda la salida estándar es por tanto redirijida a "ls.out".
El shell hijo entonces invoca a exec para ejecutar la utilidad ls. Debido a que los descriptores
de archivos son heredados durante un exec(), toda la salida estándar de ls va a "ls.out".

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.

A continuación un pequeño programa que hace aproximadamente el mismo tipo de


redirecciones como un shell de Linux. Cuando invocamos con el nombre de un archivo como
el primer parámetro y una secuencia de parámetros como el resto de parámetros, el programa
"redirect" redirije la salida estándar del comando hacia el archivo nombrado.

$ cat redirect.c ...list the program.


#include <stdio.h>
#include <fcntl.h>
main (argc, argv)
int argc;
char* argv [];
{
int fd;
/* Open file for redirection */
fd = open (argv[1], O_CREAT | O_TRUNC | O_WRONLY, 0600);
dup2 (fd, 1); /* Duplicate descriptor to standard output */
close (fd); /*Close original descriptor to save descriptor space*/
execvp (argv[2], &argv[2]);/*Invoke program; will inherit stdout*/
perror ("main"); /* Should never execute */
}

$ redirect ls.out ls -lG ...redirect "ls -lG" to "ls.out".


$ cat ls.out ...list the output file.
total 5
-rw-r-xr-x 1 glass 0 Feb 15 10:35 ls.out
-rw-r-xr-x 1 glass 449 Feb 15 10:35 redirect.c
-rwxr-xr-x 1 glass 3697 Feb 15 10:33 redirect
$ _

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)

Pipes sin nombre: pipe()


Un pipe sin nombre es un enlace de comunicación unidireccional que automaticamente
baferiza su entrada y puede ser creado usando la llamda al sistema pipe(). Cada extremo del
pipe tiene asociado un descriptor de archivo. El extremo "write" del pipe puede ser escrito
usando la llamada al sitema write(), y el extremo "read" puede ser leído usando la llamada
al sistema read(). Cuando un proceso ha terminado con los descriptores de archivos del pipe,
estos deberían ser cerrados usando la llamada al sistema close(). A conitucación se explica
cómo trabaja la llamada al sistema pipe().

Descripción de la llamada al sistema: pipe ()

Llamada al sistema : int pipe (int fd [2])


pipe() crea un pipe sin nombre y retorna dos descriptores de archivos; el descriptor asociado
con la lectura del pipe es almacenado en fd[0], y el descriptor asociado con la escritura es
almacenado en fd[1].
Las siguientes reglas se aplican a los procesos que leen de un pipe:

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.

Las siguientes reglas se aplican a procesos que escriben en un pipe:

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.

[2] En situaciones avanzadas, es posible pasar descriptores de archivos a procesos no


relacionados vía pipe.

Si el siguiente codigo se ejecutó:

int fd[2];
pipe(fd);

entonces la estructura mostrada en la siguiente figura debe ser creada.


Los pipes sin nombre son usualmente empleados para la comnicación entre un proceso padre
y su hijo, con un proceso escribiendo y con un proceso leyendo: La tipica secuencia de
eventos es como sigue:

1. El proceso padre crea un pipe sin nombre usando pipe().


2. El proceso padre invoca fork().
3. El escritor cierra su lectura al pipe, y el lector designado cierra su escritura del pipe.
4. El proceso se comunica usando las llamadas write() y read().
5. Cada proceso cierra su decsriptor de pipe activo cuando finaliza con este.

La comunicación bidereccional sólo es posible usando dos pipes.

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:

- enviar la longitud de un mensaje (en bytes) antes de enviar el mensaje en sí mismo.


- finalizar un mensaje con un caracter especial tal como una nuebva línea o NULL

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.
$ _

El laboratorio consistirá en elaborar programas que demuestren que se ha entendido cómo


funciona cada una de las llamadas al sistema y conceptos explicados en esta página.

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

Вам также может понравиться