0% encontró este documento útil (0 votos)
126 vistas14 páginas

T Procesos Pipes Ango

Descargar como pdf o txt
Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1/ 14

Área de Ingeniería Telemática

Departamento de Ingeniería de Sistemas y Automática


Escuela Superior de Ingenieros
Universidad de Sevilla
Modificado por ango para IES

PROCESOS
1. OBJETIVO

El objetivo es entender las llamadas al sistema operativo para la identificación y


ejecución de procesos. Para ello se van a estudiar algunas de las llamadas de la familia exec, así
como la llamada a fork y otras llamadas al sistema relacionadas con los procesos.

2. DESCRIPCIÓN

Identificación de procesos:

Cada proceso se identifica mediante un número entero único denominado identificador de


proceso (de tipo pid_t). Los servicios relativos a la identificación de procesos son:

pid_t getpid(void);

Devuelve el identificador del proceso que realiza la llamada.

pid_t getppid(void);

Devuelve el identificador del proceso padre.

⇒Ejemplo:

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

void main(void)
{
pid_t id_proceso;
pid_t id_padre;

id_proceso = getpid();
id_padre = getppid();

printf("Identificador de proceso: %d\n", id_proceso);


printf("Identificador del proceso padre: %d\n", id_padre);
}

p1_prueba1.c

1
Además de la identificación del proceso, un proceso tiene asociado un identificador de usuario
real (identificador de usuario del propietario), un identificador de usuario efectivo (que
determina los privilegios que un proceso tiene cuando se ejecuta), un identificador de grupo (al
que pertenece el usuario) y un identificador de grupo efectivo.

Los servicios relacionados para obtener estos identificadores son:

uid_t getuid(void);

Devuelve el identificador de usuario real del proceso que realiza la llamada.

uid_t geteuid(void);

Devuelve el identificador de usuario efectivo del proceso que realiza la llamada.

gid_t getgid(void);

Devuelve el identificador del grupo del proceso que realiza la llamada.

gid_t getegid(void);

Devuelve el identificador del grupo efectivo del proceso que realiza la llamada.

⇒ Ejemplo:

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

void main(void)
{
printf("Identificador de usuario: %d\n", getuid());
printf("Identificador de usuario efectivo: %d\n", geteuid());
printf("Identificador de grupo: %d\n", getgid());
printf("Identificador de grupo efectivo: %d\n", getegid());
}

p1_prueba2.c

Creación de procesos:

Para la creación de procesos se utiliza el siguiente servicio:

pid_t fork (void);


Devuelve la identificación del proceso creado en el padre y 0 en el
proceso hijo.

La llamada a fork lo que hace es crear una copia del proceso que ha realizado
la llamada. Se puede decir que se realiza una clonación del proceso. El
proceso que hace la llamada a fork se convierte en el proceso padre del
proceso creado. Una vez realizada la copia, tanto padre e hijo continúan de
forma independiente la ejecución en el mismo punto del programa, es decir, en

2
la siguiente instrucción al fork. Es un error pensar que el hijo comienza la
ejecución por el principio del programa. Esto es así porque el proceso hijo
hereda del padre los datos y la pila que tuviera en el momento de la
ejecución del fork, así como el valor de los registros.

⇒Ejemplo:

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

void main(void)
{
pid_t pid;

pid = fork();
switch(pid)
{
case -1: /* error del fork() */
perror("fork");
break;
case 0: /* proceso hijo */
printf("Proceso %d; padre = %d \n", getpid(), getppid());
break;
default: /* padre */
printf("Proceso %d; padre = %d \n", getpid(), getppid());
}
}

p1_prueba3.c

Se ha utilizado la función perror para imprimir un mensaje de error por la


salida estándar de errores.

Ejecución de un programa:

El servicio exec tiene por objetivo cambiar el programa que está ejecutando un proceso. No es
como el servicio fork ya que exec no crea un nuevo proceso, sino que permite que un proceso
pase a ejecutar un programa diferente. Recuerde que fork crea un nuevo proceso que ejecuta el
mismo programa que el proceso padre.

Algunas de las funciones exec son las siguientes:

int execlp(char *file, const char *arg, …);


int execvp(char *file, char **argv);

Estas funciones reemplazan la imagen del proceso actual por una nue va imagen. Esta imagen se
construye a partir de un archivo ejecutable (file). Si la llamada se ejecuta con éxito, no devolverá
ningún valor puesto que la imagen del proceso habrá sido reemplazada, es decir, de una llamada
con éxito no hay retorno, en caso contrario devuelve –1.
El argumento arg es la dirección del primer elemento de una cadena de caracteres que contiene el
primer argumento que se le pasa al programa. Los puntos suspensivos indican que hay que poner
también los demás argumentos de la misma forma.

3
El argumento argv es la dirección de comienzo de un vector de cadenas de caracteres que
contiene los argumentos pasados al programa y debería acabar con un NULL.
La diferencia entre execlp y execvp es únicamente la forma de dar los argumentos del programa.

La función main del nuevo programa llamado tendrá la forma:

int main(int argc, char **argv)

donde argc representa el número de argumentos que se pasan al programa, incluido el propio
nombre del programa, y argv es un vector de cadenas de caracteres, conteniendo cada elemento
de este vector un argumento pasado al programa. El primer componente de este vector (argv[0])
representa el nombre del programa.

⇒ Ejemplo de execlp:

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

void main(void)
{
pid_t pid;
int status;

pid = fork();
switch(pid)
{
case -1: /* error del fork() */
perror("fork");
break;
case 0: /* proceso hijo */
execlp("ls","ls","- l",NULL);
perror("exec");
break;
default: /* padre */
printf("Proceso padre\n");
}
}

p1_prueba4.c

4
⇒ Ejemplo de execvp:

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

void main(int argc, char **argv)


{
pid_t pid;
char *argumentos[3];

argumentos[0] = "ls";
argumentos[1] = "- l";
argumentos[2] = NULL;

pid = fork();
switch(pid)
{
case -1: /* error del fork() */
perror("fork");
break;
case 0: /* proceso hijo */
execvp(argumentos[0], argumentos);
perror("exec");
break;
default: /* padre */
printf("Proceso padre\n");
}
}

p1_prueba5.c

⇒ Otro ejemplo de execvp:

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

void main(int argc, char **argv)


{
pid_t pid;

pid = fork();
switch(pid)
{
case -1: /* error del fork() */
perror("fork");
break;
case 0: /* proceso hijo */
if (execvp(argv[1], &argv[1])< 0)

5
perror("exec");
break;
default: /* padre */
printf("Proceso padre\n");
}
}

p1_prueba6.c

Espera por la finalización de un proceso:

El servicio wait permite a un proceso padre esperar hasta que termine la ejecución de un proceso
hijo. El proceso padre se queda bloqueado hasta que termina un proceso hijo. La forma de wait
es la siguiente:

Pid_t wait(int *status);

Esta llamada espera la finalización de un proceso hijo y permite obtener información sobre el
estado de terminación del mismo. Devuelve el identificador del proceso hijo cuya ejecución ha
finalizado. Si status es distindo de NULL, entonces se almacena en esta variable información
relativa al proceso que ha terminado. Se puede usar esta variable para ver como ha terminado el
proceso utilizando macros. Algunas de estas macros son las siguientes:

♦ WIFEXITED(status): devuelve un valor verdadero (distinto de cero) si el hijo terminó


normalmente.
♦ WEXITSTATUS(status): permite obtener el valor devuelto por el proceso hijo. Sólo puede
ser utilizada cuando WIFEXITED devuelve un valor verdadero.
♦ WIFSIGNALED(status): devuelve un valor verdadero si el proceso hijo finalizó su ejecución
como consecuencia de la recepción de una señal para la cual no se había programado
manejador.
♦ WTERMSIG(status): devuelve el número de la señal que provocó la finalización del proceso
hijo. Sólo puede ser utilizada si WIFSIGNALED devuelve un valor verdadero.

⇒ Ejemplo:

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>

void main(int argc, char **argv)


{
pid_t pid;
int valor;

pid = fork();
switch(pid)
{
case -1: /* error del fork() */

6
perror("fork");
break;
case 0: /* proceso hijo */
if(argc>1)
if (execvp(argv[1], &argv[1]) < 0)
perror("exec");
break;
default: /* padre */
while (wait(&valor) != pid);

if (valor == 0)
printf("El mandato se ejecuto de forma normal\n");
else
{
if (WIFEXITED(valor))
printf("El hijo termino normalmente y su valor devuelto fue %d\n",
WEXITSTATUS(valor));

if (WIFSIGNALED(valor))
printf("El hijo termino al recibir la señal %d\n", WTERMSIG(valor));
}
}
}
p1_prueba7.c

7
COMUNICACIÓN BÁSICA ENTRE PROCESOS

1. OBJETIVO

Se pretende mostrar al alumno mecanismos simples de comunicación entre procesos.


Para ello se van a estudiar las tuberías (pipes).

Tuberías:

Una tubería es un canal de comunicación entre procesos emparentados (padre e hijo). El acceso
se realiza como en una cola (FIFO)

El servicio que crea una tubería es:

Formato:
#include <unistd.h>
int pipe (int descriptores[2]);
Parámetros:
Tabla que recibirá los descriptores de entrada (0, para lectura) y de
salida (1, para escritura) de la tubería
Devuelve:
0 si se ha completado correctamente; -1 si error

A continuación se da un ejemplo de tubería entre un padre y un hijo:

#include <unistd.h>
#define LECTURA 0
#define ESCRITURA 1

int main()
{

int descr[2], bytesleidos;


char mensaje[101];
char *frase="Veremos si la transferencia es buena";
pipe(descr);
if(fork()==0) /* Proceso hijo */
{
close(descr[LECTURA]);
write(descr[ESCRITURA], frase, strlen(frase));
close(descr[ESCRITURA]);
}
else /* Proceso padre */
{
close(descr[ESCRITURA]);
bytesleidos=read (descr[LECTURA], mensaje, 100);
while(bytesleidos)
{
mensaje[bytesleidos]='\0';
printf("Bytes leidos: %d \n",bytesleidos);
printf("Mensaje: %s \n",mensaje);
bytesleidos=read (descr[LECTURA], mensaje, 100);
}
close(descr[LECTURA]);

8
}
}

p2_prueba4.c

⇒ Estudiar el ejemplo e indicar el resultado.


⇒ Realizar un ejemplo de productor/consumidor que se comuniquen y sincronicen mediante una
tubería. Para ello:
♦ Crear un proceso hijo con fork. El padre será el productor y el hijo el consumidor.
♦ El productor escribe en la tubería enteros cada segundo. Se puede escribir por ejemplo en la
tubería 100 enteros, desde el 0 al 99.
♦ El consumidor lee de la tubería enteros.

Propuesta: Modificar para escribir número aleatorios.

9
PIPES CON NOMBRE
1. OBJETIVO

Una vez que se ha entendido el funcionamiento de una tubería (pipe), se complementan


los conocimientos adquiridos estudiando los pipes con nombre.

2. DESCRIPCIÓN

Pipes con nombre:

Los pipes (tuberías) con nombre son una vía de intercambio de datos. Igual que los pipes
estudiados se gestionan mediante el método FIFO, es decir, el primer byte introducido por el
emisor será el primer byte en ser extraído por el receptor. Las tuberías con nombre también
reciben el nombre de FIFOs. También son una vía de comunicación unidireccional como los
pipes.

Diferencias con los pipes:


♦ Tienen un nombre, ocupando una entrada en el sistema de ficheros. Se accede a él mediante
un nombre de camino, igual que un fichero (se puede ver con la orden ls).
♦ Pueden intercomunicar procesos sin ningún tipo de relación, es decir, no tienen que estar
emparentados como en los pipes. Esta característica permite que los FIFOs puedan utilizarse
para comunicar y sincronizar procesos de la misma máquina, sin necesidad de que lo hereden
por medio de la llamada fork.
♦ Existen hasta que son borrados explícitamente como cualquier fichero.

Creación:

Hay varias formas de crear un FIFO. Se puede crear mediante mkfifo o bien mediante mknod.
Tanto mkfifo como mknod se pueden utilizar desde la línea de órdenes o bien llamando al
servicio correspondiente, como se detalla a continuación:

♦ mkfifo en la línea de órdenes:

mkfifo –m=modo_de_acceso nombre

modo_de_acceso representa los permisos asociados al FIFO. Por ejemplo:

mkfifo –m=660 mififo


o
mkfifo –m=”u+g=rw” mififo

donde se está creando un FIFO de nombre mi mififo con permisos de acceso para el usuario y
para el grupo de lectura y escritura.

♦ Servicio mkfifo:

10
El prototipo del servicio que permite crear una tubería con nombre es el siguiente:

int mkfifo( char *nombre, mode_t mode);

Los parámetros proporcionados son el nombre del FIFO y los permisos asociados. La llamada
devuelve 0 si se ejecutó con éxito o –1 en caso de error. Por ejemplo:

Resultado= mkfifo(mififo, 660);

Crea el FIFO de nombre mififo con permisos de lectura y escritura para el usuario y el grupo.

♦ mknod en la línea de órdenes:

mknod sirve para crear ficheros especiales. Un FIFO es considerado un fichero especial.

mknod –m=modo_de_acceso nombre tipo_fich_especial

Se indica el nombre, los permisos y el tipo de fichero especial, que para la creación de un FIFO
debe ser p indicando que es un pipe. Por ejemplo:

mknod –m=660 mififo p

♦ Servicio mknod:

El prototipo de este servicio es el siguiente:

int mknod( char *nombre, mode_t mode, dev_t dev);

Los parámetros proporcionados son el nombre del FIFO y el modo de acceso incluyendo los
permisos (para un FIFO se debe indicar S_IFIFO), el tercer parámetro en un FIFO es ignorado.
La llamada devuelve 0 si se ejecutó con éxito o –1 en caso de error. Por ejemplo:

Resultado= mknod(mififo, S_IFIFO|660, 0);

Crea el FIFO de nombre mififo con permisos de lectura y escritura para el usuario y el grupo.

Una vez creada una tubería con nombre se utiliza exactamente como un fichero. Recordamos a
continuación los servicios utilizados:

Apertura:

Se utiliza el servicio open. Su prototipo es el siguiente:

int open(char *nombre, int flag);

El primer argumento indica el nombre del FIFO y el segundo la forma en la que se va a acceder.
Los posibles valores de flag son los siguientes:

O_RDONLY: se abre sólo para realizar operaciones de lectura.


O_WRONLY: se abre sólo para realizar operaciones de escritura.

11
O_RDWR: se obre para realizar operaciones de lectura y escritura.

El servicio open devuelve un descriptor de archivo que se puede utilizar para leer y escribir del
FIFO. En el caso de error devuelve –1. La llamada open bloquea al proceso que la ejecuta hasta
que haya algún otro proceso en el otro extremo del FIFO. Si no interesa este comportamiento, se
puede usar la bandera O_NONBLOCK en la llamada a open para desactivar esta opción por
defecto.

Cierre:

Para cerrar un FIFO se utiliza el servicio close. Su prototipo es el siguiente:

int close(int fd);

El argumento es el descriptor del archivo que se quiere cerrar, en este caso, el descriptor del
FIFO devuelto en la apertura. La llamada devuelve 0 si se ejecutó con éxito o –1 en caso de
error.

Lectura:

Para leer de un FIFO se utiliza read. El prototipo es el siguiente:

int read(int fd, char *buffer, int n);

El primer argumento es el descriptor del FIFO. El segundo argumento es la dirección del buffer
de usuario donde se van a guardar los datos leídos. El último argumento es el número de bytes
que se quieren leer. La llamada devuelve el número de bytes leídos o –1 en caso de error.

Escritura:

Para escribir en un FIFO se utiliza write. El prototipo es el siguiente:

int write(int fd, char *buffer, int n);

El primer argumento representa el descriptor de archivo del FIFO. El segundo argumento


especifica el buffer de usuario donde se encuentran los datos que se van a escribir al FIFO. El
último argumento indica el número de bytes a escribir.

Una tubería debe tener un lector y un escritor. Si un proceso trata de escribir en una tubería que
no tiene lectores asociados, el núcleo enviará la señal SIGPIPE.

NOTA: también se pueden utilizar las funciones fopen, fclose, fputs, fgets … que
utilizan FILE * en vez de int para indicar un fichero.

Borrado:

El prototipo del servicio utilizado para borrar un FIFO es:

int unlink(char *fifo);

12
Esta llamada pospone la destrucción del FIFO hasta que todos los procesos que lo estén
utilizando lo hayan cerrado con la función close.

Para borrar una tubería con nombre también se pueden utilizar la orden correspondiente del
sistema operativo para el borrado de ficheros (rm).

⇒ Ejemplo: El siguiente programa crea un fifo de nombre mififo. En cada iteración del bucle
lee una cadena enviada por el proceso escritor.
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

#define NOMBREFIFO "mififo"


#define TAM_BUF 80
#define TRUE 1

int main(void)
{
int fp;
char buffer[TAM_BUF];
int nbytes;

mknod(NOMBREFIFO,S_IFIFO|0660,0);

while(TRUE)
{
fp=open(NOMBREFIFO,O_RDONLY);
nbytes=read(fp,buffer,TAM_BUF-1);
buffer[nbytes]='\0';
printf("Cadena recibida: %s \n",buffer);
close(fp);
}

return 0;
}

p3_prueba1.c

⇒ Ejemplo: programa que escribe en el fifo de nombre mififo.


#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

#define NOMBREFIFO "mififo"

int main(int argc, char *argv[])


{
int fp;
int result=1;

if(argc!=2)
printf("uso: %s cadena \n", argv[0]);

13
else if ((fp=open(NOMBREFIFO,O_WRONLY))==-1)
perror("fopen");
else
{
write(fp,argv[1],strlen(argv[1]));
close(fp);
result=0;
}

return result;

p3_prueba2.c

Ejercicio:

⇒ Realizar el problema del productor/consumidor mediante tuberías con nombre. El consumidor


es el que se debe encargar de crear y borrar la tubería. El productor debe escribir en la tubería
con nombre caracteres proporcionados por el usuario desde teclado. Cuando el usuario
proporcione un carácter punto desde teclado se supone que debe terminar la comunicación. El
consumidor por su parte, debe leer desde la tubería con nombre hasta que no quede nada en la
tubería.

14

También podría gustarte