ERROR USUARIO NO VALIDADO
El módulo de sincronización y comunicación se estructura en los siguientes apartados:
Este módulo incide fundamentalmente en los servicios POSIX para la sincronización y la comunicación, tanto de procesos ligeros como pesados. Unas veces serán servicios del sistema operativo y otros serán servicios proporcionados a nivel de biblioteca (por ejemplo por la biblioteca pthread).
Se utilizarán los siguientes programas, que deberán descargarse de la web y compilar para su ejecución:
Los servicios y funciones de biblioteca utilizados en dichos programas son los siguientes:
En un sistema UNIX la concurrencia sucede a muchos niveles.
Actualmente, en la mayoría de los sistemas operativos modernos encontramos las siguientes características relacionadas con su capacidad de concurrencia:
Un proceso multihilo, sobre un sistema multihilo, sobre un multiprocesador, podría estar ejecutando simultáneamente en paralelo en más de una CPU, esto es, obteniendo tasas de utilización de CPU de más del 100%.
Este mismo proceso multihilo (usando una biblioteca multihilo), sobre un sistema monohilo, ejecutaría como máximo al 100% de CPU, aunque el sistema sea multiprocesador.
Se pide que se introduzca usted en un sistema UNIX donde tenga cuenta abierta.
Un sistema UNIX es multiproceso y multiusuario.
Analicemos ahora el tipo de sistema con el que está trabajando. El directorio /proc, como ya habíamos visto anteriormente, recoge información sobre el sistema. Entre esta información se encuentra las características del procesador, en concreto, en el fichero cpuinfo.
Dada la información contenida en /proc/cpuinfo , conteste a las siguientes preguntas:
Además de la información proporcionada por /proc/cpuinfo, proporcionamos dos programas CPUs_pth y CPUs_prc, que nos van a permitir conocer (en cierto modo) la capacidad de computo del sistema (entendida como número total de incrementos de una variable de 64 bits) que pueden conseguir el número de procesos (o hilos) indicado en el número de segundos establecidos. Como se puede imaginar, este no es un método fiable, puesto que depende de la carga (de procesos) a la que esté sometida la máquina. Si en el sistema hay otros procesos que usan intensivamente la CPU es fácil que las medidas que usted obtenga sean muy dispares.
Analice el código del programa CPUs_prc. Comprobará que se crean tantos procesos (mediante fork) como se indique en el primer argumento, y que cada uno de estos procesos estará en ejecución durante un tiempo determinado por el segundo argumento.
Compile y ejecute el programa, por ejemplo, con los argumentos 1 10:
alumno@maquinaLinux~/PracticasAnalisis/ModuloSinCom$ ./CPUs_prc 1 10 Total work done by 1 process in 10 seconds is = 498176612
Además podemos utilizar el programa Times_C.c que nos proporciona los tiempos de ejecución de un proceso. Distinguimos entre tiempos “real”, “user” y “sys” (ver man 7 time). El tiempo “real” es el transcurrido desde el inicio de la ejecución hasta la terminación del proceso. Los tiempos “user” y “sys” son tiempos de procesador que ha utilizado el proceso. “user” es el tiempo de procesador ejecutando en modo usuario (sin privilegios de sistema). “sys” es el tiempo de procesador ejecutando en modo sistema en nombre del proceso, es decir, ejecutando los servicios invocados por el proceso. Si nuestro proceso es multihilo (como es el caso de CPUs_prc.c ), y nuestro sistema es multiprocesador, podrá suceder que cada uno de los hilos de ejecución se asignen a procesadores diferentes por lo que el tiempo “user” de procesador será superior al tiempo “real”.
alumno@maquinaLinux~/PracticasAnalisis/ModuloSinCom$ ./Times_C ./CPUs_prc 1 10 Total work done by 1 process in 10 seconds is = 4982684141 Times_C: »»» Real:10.000" User:10.000" Syst:0.000"
El resultado muestra como durante 10 segundos, 1 proceso en ejecución ha logrado realizar un total aproximado de 5000 millones de sumas. ¿Cuánto cómputo pueden realizar el doble de procesos en la mitad de tiempo? ¿El mismo o la mitad? Esto depende de si tenemos una única CPU o tenemos más (y obviamente de la carga del sistema).
alumno@maquinaLinux~/PracticasAnalisis/ModuloSinCom$ ./Times_C ./CPUs_prc 2 5 Total work done by 2 process in 5 seconds is = 4979025368 Times_C: »»» Real:5.010" User:10.000" Syst:0.000"
Resulta que efectivamente se alcanza una cantidad de cómputo prácticamente idéntica (del orden de los 5000 millones). De ello podríamos deducir que nuestra máquina es, al menos, un biprocesador. Además vemos como el tiempo “user” es el doble que el tiempo “real”, lo cual implica que cada proceso ha estado ejecutando en un procesador diferente.
Recuerde que estamos haciendo todo este tipo de pruebas sobre un sistema multiproceso y multiusuario, luego las medidas de tiempos que podamos hacer serán siempre aproximadas y prácticamente irrepetibles, al estar sometidas al indeterminismo del resto de la carga en el sistema.
El programa CPUs_pth.c presenta la misma funcionalidad que el programa CPUs_prc.c, pero utilizando procesos ligeros o threads. Siguiendo el mismo procedimiento que para CPUs_prc.c, podemos determinar si nuestro sistema es multihilo o si por el contrario el soporte para los threads es mediante una biblioteca multihilo.
Ciertos programas que ya se utilizaron en el capítulo de procesos nos permiten observar cómo sucede la concurrencia en o entre los procesos existentes en un sistema. Nos estamos refiriendo a:
Compart_*, Concurr_* y UnSegundo.c.
El acceso concurrente sin coordinación a recursos compartidos puede NO producir los resultados esperados.
En sistemas multiproceso, no es posible garantizar la velocidad relativa de los diferentes procesos en ejecución, es decir, no vamos a conocer a priori el orden en que se ejecutarán los procesos en nuestro sistema. Este hecho provoca que un acceso no coordinado a los recursos conpartidos por los procesos pueda producir resultados diferentes a los esperados. Este fenómeno se conoce como condición de carrera.
Se ha desarrollado el programa Carreras.c que pretende demostrar la existencia de condiciones de carrera, cuando no existe coordinación en el acceso a un recurso compartido por varios procesos.
Analice el código del programa Carreras.c
C-01.- void shared_free(void * ptr, unsigned size) C-02.- { C-03.- munmap(ptr, size); C-04.- } C-05.- C-06.- void * shared_alloc(unsigned size) C-07.- { C-08.- void * ptr = NULL; C-09.- int fd = open("/dev/zero", O_RDWR); C-10.- if (fd < 0) return NULL; C-11.- ptr = mmap(0, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); C-12.- close(fd); C-13.- return ptr; C-14.- } C-15.- C-16.- C-17.- C-18.- int main(void) C-19.- { C-20.- int i, v; C-21.- volatile int * Variable = shared_alloc(sizeof(int)); C-22.- if(Variable == NULL) return 1; C-23.- for (i = 0; i < 50; i++) C-24.- { C-25.- Variable[0] = 0; C-26.- switch(fork()) C-27.- { C-28.- case -1: C-29.- perror(MYNAME": fork()"); exit(1); C-30.- case 0: C-31.- for (v = 0; v < 2500000; v++) Variable[0]++; exit(0); C-32.- default: C-33.- for (v = 0; v < 2500000; v++) Variable[0]--; wait(NULL); C-34.- } C-35.- printf("\t%10d\n ", Variable[0]); C-36.- } C-37.- C-38.- shared_free((void*)Variable, sizeof(int)); C-49.- return 0; C-40.- }
Para evitar las condiciones de carrera, es necesario coordinar los accesos al recurso compartido.
Coordinación implícita: es aquella que realiza el Sistema Operativo para garantizar que no suceden condiciones de carrera entre procesos aparentemente independientes, pero que en realidad están compartiendo recursos del sistema. Por ejemplo, en la coutilización de ficheros los procesos comparten la posición sobre el archivo mediante descriptores de fichero duplicados. Aunque en ningún momento se utilice de manera explicita ningún mecanismo que coordine el acceso al recurso compartido, el sistema operativo se encarga de garantizar que mientras un proceso esté accediendo al fichero, ninguno más pueda modificarlo.
Coordinación explícita: es aquella que realiza explícitamente el programador, utilizando los mecanismos de sincronización adecuados, proporcionados por el lenguaje o por el Sistema Operativo, para conseguir que no sucedan condiciones de carrera entre procesos concurrentes que está programando y que están compartiendo recursos.
En clase se ven ejemplos de la utilización de diversos mecanismos de sincronización para la implementación de determinados modelos “clásicos” de programación concurrente.
Este modelo de interacción entre dos procesos concurrentes resuelve el problema de cómo comunicar (ordenadamente) información de uno a otro (en un sentido), cuando lo único que los dos procesos tienen en común es un espacio de almacenamiento de tamaño fijo limitado.
Utilizaremos en este apartado, el programa PC_PLs_mc.c, el cual constituye un ejemplo del modelo productor|consumidor utilizando procesos ligeros.
PC-01.- #define BUFF_SIZE 1024 PC-02.- #define TOTAL_DATOS 100000 PC-03.- PC-04.- int n_datos; PC-05.- int buffer[BUFF_SIZE]; PC-06.- pthread_mutex_t mutex; PC-07.- pthread_cond_t no_lleno, no_vacio; PC-08.- PC-09.- void Productor(void) PC-10.- { PC-11.- int i, dato; PC-12.- PC-13.- for (i = 0; i < TOTAL_DATOS; i++) PC-14.- { PC-15.- /*Producir el dato*/ PC-16.- dato = i; PC-17.- pthread_mutex_lock(&mutex); PC-18.- while (n_datos == BUFF_SIZE) PC-19.- pthread_cond_wait(&no_lleno, &mutex); PC-20.- buffer[i % BUFF_SIZE] = dato; PC-21.- n_datos++; PC-22.- PC-23.- pthread_cond_signal(&no_vacio); PC-24.- pthread_mutex_unlock(&mutex); PC-25.- } PC-26.- } PC-27.- PC-28.- void Consumidor(void) PC-29.- { PC-30.- int i, dato; PC-31.- PC-32.- for (i = 0; i < TOTAL_DATOS; i++) PC-33.- { PC-34.- pthread_mutex_lock(&mutex); PC-35.- while (n_datos == 0) PC-36.- pthread_cond_wait(&no_vacio, &mutex); PC-37.- dato = buffer[i % BUFF_SIZE]; PC-38.- n_datos--; PC-39.- pthread_cond_signal(&no_lleno); PC-40.- pthread_mutex_unlock(&mutex); PC-41.- PC-42.- /*Consumir el dato*/ PC-43.- printf("\r %d \r", dato); PC-44.- fflush(stdout); PC-45.- } PC-46.- printf("\n"); PC-47.- fflush(stdout); PC-48.- } PC-49.- PC-50.- int main(void) PC-51.- { PC-52.- pthread_t th1, th2; PC-53.- PC-54.- pthread_mutex_init(&mutex, NULL); /* Situación inicial */ PC-55.- pthread_cond_init(&no_lleno, NULL); PC-56.- pthread_cond_init(&no_vacio, NULL); PC-57.- pthread_create(&th1, NULL, (void*)Productor, NULL); /* Arranque */ PC-58.- pthread_create(&th2, NULL, (void*)Consumidor, NULL); PC-59.- pthread_join(th1, NULL); /* Esperar terminación */ PC-60.- pthread_join(th2, NULL); PC-61.- pthread_mutex_destroy(&mutex); /* Destruir */ PC-62.- pthread_cond_destroy(&no_lleno); PC-63.- pthread_cond_destroy(&no_vacio); PC-64.- PC-65.- return 0; PC-66.- }
Compile (tenga en cuenta que utiliza procesos ligeros) y ejecute el programa:
Este modelo de interacción entre procesos concurrentes resuelve el problema de ordenar los accesos a un repositorio común de información, de manera que se garantice una visión coherente de dicha información. Se debe permitir que varios lectores puedan acceder simultáneamente al recurso, pero al mismo tiempo se debe garantizar que cada escritor acceda en exclusiva, es decir, mientras un proceso escritor accede al recurso ningún otro proceso (ya sea lector o escritor) deberá acceder al mismo.
Para mostrar el funcionamiento de este modelo, se plantea la utilización de dos programas: Lector.c y Escritor.c. El primer realiza lecturas sobre un fichero compartido, que se ha definido de manera estática como BD, mientras que el proceso escritor, realiza lecturas sobre dicho fichero compartido.
A continuación se muestra el código del programa Escritor.c
E-01.- #define MYNAME "Escritor" E-02.- #include <fcntl.h> E-03.- #include <stdio.h> E-04.- #include <stdlib.h> E-05.- #include <unistd.h> E-06.- E-07.- int main(void) E-08.- { E-09.- int fd, val=0, cnt; E-10.- struct flock fl; E-11.- E-12.- fl.l_whence = SEEK_SET; E-13.- fl.l_start = 0; E-14.- fl.l_len = 0; E-15.- fl.l_pid = getpid(); E-16.- fd = open("BD", O_RDWR); E-17.- for (cnt = 0; cnt < 10; cnt++) E-18.- { E-19.- fl.l_type = F_WRLCK; E-20.- fcntl(fd, F_SETLKW, &fl); E-21.- lseek(fd, 0, SEEK_SET); E-22.- read(fd, &val, sizeof(int)); E-23.- val++; E-24.- lseek(fd, 0, SEEK_SET); E-25.- write(fd, &val, sizeof(int)); E-26.- fl.l_type = F_UNLCK; E-27.- fcntl(fd, F_SETLK, &fl); E-28.- } E-29.- return 0; E-30.- }
Los programas Escritor.c y Lector.c están pensados para ser ejecutados de manera simultanea. En principio, podrán existir simultaneamente tantos procesos escritores y lectores como queramos.
Ejecute la siguiente secuencia. Primero creamos el archivo BD (que estará vacio) y a continuación lanzamos la ejecución simultanea de 3 procesos lectores y dos procesos escritores.
alumno@maquinaLinux~/PracticasAnalisis/ModuloSinCom$ > BD alumno@maquinaLinux~/PracticasAnalisis/ModuloSinCom$ ./Lector & ./Escritor & ./Lector & ./Escritor & ./Lector ...
Este modelo de interacción entre procesos concurrentes resuelve el problema de acceder a un recurso gestionado por un proceso gestor (servidor), local o remoto. En el modelo cliente-servidor, este último es el encargado de proporcionar acceso a un servicio y por tanto de gestionar el acceso al/los recursos. El cliente accederá al servicio proporcionado por el servidor siguiendo un protocolo de petición-respuesta.
Para comprobar el funcionamiento del modelo cliente-servidor, se han diseñado dos programas: TCP_s.c y TCP_c.c que se corresponden con la aplicación servidor y cliente respectivamente.
A continuación se muestra el código de la aplicación TCP_s.c
TS-01.- #define MYNAME "TCP_s" TS-02.- #include <arpa/inet.h> TS-03.- #include <netinet/in.h> TS-04.- #include <netinet/tcp.h> TS-05.- #include <sys/socket.h> TS-06.- #include <sys/types.h> TS-07.- #include <sys/un.h> TS-08.- #include <sys/wait.h> TS-09.- #include <netdb.h> TS-10.- #include <ctype.h> TS-11.- #include <signal.h> TS-12.- #include <stdio.h> TS-13.- #include <stdlib.h> TS-14.- #include <unistd.h> TS-15.- TS-16.- void Timeout_Control(int signo) TS-17.- { TS-18.- int timeout; TS-19.- char * text = "TIMEOUT!...Terminating now.\n"; TS-20.- TS-21.- if (!signo) TS-22.- { TS-23.- text = getenv("TIMEOUT"); TS-24.- if (text) TS-25.- { TS-26.- signal(SIGALRM, Timeout_Control); TS-27.- timeout = atoi(text); TS-28.- if (timeout >= 0) alarm(timeout); TS-29.- } TS-30.- } TS-31.- else TS-32.- { TS-33.- write(2, text, strlen(text)); exit(0); TS-34.- } TS-35.- } TS-36.- TS-37.- void SIGCHLD_Handler(int signo) TS-38.- { TS-39.- signal(SIGCHLD, SIGCHLD_Handler); TS-40.- if (signo == SIGCHLD) waitpid(-1, NULL, WNOHANG); TS-41.- } TS-42.- TS-43.- int main(int argc, char * argv[]) TS-44.- { TS-45.- int sd, cd, size, ret; TS-46.- char ch; TS-47.- short int port; TS-48.- struct sockaddr_in s_ain, c_ain; TS-49.- TS-50.- Timeout_Control(0); TS-51.- SIGCHLD_Handler(0); TS-52.- TS-53.- sd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); TS-54.- if (sd == -1) perror(argv[0]); TS-55.- TS-56.- if (argc > 1) port = atoi(argv[1]); TS-57.- else port = 7777; TS-58.- TS-59.- bzero((char *) &s_ain, sizeof(s_ain)); TS-60.- s_ain.sin_family = AF_INET; TS-61.- s_ain.sin_addr.s_addr = INADDR_ANY; TS-62.- s_ain.sin_port = htons(port); /* 7 == echo port */ TS-63.- TS-64.- ret = bind(sd, (struct sockaddr *) &s_ain, sizeof(s_ain)); TS-65.- TS-66.- if (ret == -1) perror(argv[0]); TS-67.- TS-68.- listen(sd, 5); TS-69.- while (1) TS-70.- { TS-71.- size = sizeof(c_ain); TS-72.- cd = accept(sd, (struct sockaddr *) &c_ain, &size); TS-73.- switch (fork()) TS-74.- { TS-75.- case -1: TS-76.- perror("echo server"); TS-77.- return 1; TS-78.- case 0: TS-79.- close(sd); TS-80.- while (recv(cd, &ch, 1, 0) == 1) TS-81.- { TS-82.- ch = toupper(ch); TS-83.- send(cd, &ch, 1, 0); TS-84.- } TS-85.- close(cd); TS-86.- return 0; TS-87.- default: TS-88.- close(cd); TS-89.- } /* switch */ TS-90.- } /* while */ TS-91.- } /* main */
Compile el programa TCP_s.c y ejecutelo en segundo plano (background).
alumno@maquinaLinux:~/PracticasAnalisis/ModuloSinCom$ ./TCP_s 18231 & [1234]
En este ejemplo se utiliza el argumento 18231 para lanzar el proceso servidor, pero puede utilizar otros valores.
Utilizaremos el programa TCP_c.c como proceso cliente. Este programa puede recibir dos parámetros, uno o ninguno.
Realice un par de conexiones interactivas con su servidor, desde la máquina local. Y compruebe el funcionamiento del modelo cliente-servidor.
alumno@maquinaLinux:~/PracticasAnalisis/ModuloSinCom$ ./TCP_c 127.0.0.1 18231 Hola, ¿qué tal? ^D
Recuerde que hemos arrancado el programa servidor como un demonio, esto es, en segundo plano y a la escucha en un canal de comunicación con dirección conocida. Para evitar que se nos quede este proceso latente en el sistema (consumiendo el puerto al que está asociado) deberemos matarlo.
Ejecute el mandato fg, para traerlo a primer plano y luego ^C para matarlo.
alumno@maquinaLinux:~/PracticasAnalisis/ModuloSinCom$ fg ./TCP_s 18231 ^C [1234] Interrupted
O bien, consulte su PID con un ps y luego láncele una señal con el mandato kill.
alumno@maquinaLinux:~/PracticasAnalisis/ModuloSinCom$ kill -9 1234 [1234] Killed
Intentemos ahora medir las prestaciones de este tipo de comunicación local.
Cree y tenga a mano un fichero de 1 Mega byte (por ejemplo, puede utilizar el programa Robot_B que se utilizó en el módulo de ficheros). Veremos cuanto tiempo (aproximado) tarda en trasferirse en ida y vuelta este fichero. Para medir el tiempo utilizaremos el mandato Times_C.
alumno@maquinaLinux:~/PracticasAnalisis/ModuloSinCom$ ./Times_C ./TCP_c 127.0.0.1 18231 < 1MB.dat > /dev/null Times_C: »»» Real:X.xxx" User:XX.xxx" Syst:XX.xxx"
Intentemos ahora medir las prestaciones de la comunicación remota. Busque una máquina remota donde pueda compilar y ejecutar el servidor TCP (digamos que se llama triqui.fi.upm.es). Láncelo en dicha máquina remota y desde la máquina local (que debe ser otra), desde la que está realizando estás prácticas, vuelva a contactar y a medir el tiempo de transferir un megabyte.
alumno@maquinaLinux:~/PracticasAnalisis/ModuloSinCom$ Times_C ./TCP_c triqui.fi.upm.es 18231 <1MB.dat >/dev/null Times_C: »»» Real:X.xxx" User:XX.xxx" Syst:XX.xxx"
No olvide terminar los procesos servidores que haya arrancado. De otro modo no podrán arrancar otro en el mismo puerto.