Prácticas de Ejercicios de Software de Comunicaciones Unix Joaquín Seoane. 11 de Marzo de 1998 Esta documentación puede obtenerse tanto en HTML, en http://www.dit.upm.es/~joaquin/comunix/comunix.html, como en PostScript, en http://www.dit.upm.es/~joaquin/comunix/comunix.ps, y texto Latin-1, en http://www.dit.upm.es/~joaquin/comunix/comu­ nix.txt. ______________________________________________________________________ Table of Contents 1. Bibliografía y material didáctico 2. Prácticas 2.1 Entrada/salida básica 2.1.1 Objetivos 2.1.2 Especificación 2.2 Procesos 2.2.1 Objetivos 2.2.2 Especificación 2.2.2.1 Creación simple de procesos 2.2.2.2 Espera por la terminación 2.2.2.3 Copia de ficheros hecha por proceso hijo 2.3 Comunicación y sincronización 2.3.1 Objetivos 2.3.2 Especificación 2.3.2.1 Tratamiento de señales 2.3.2.2 Sincronización con señales 2.3.2.3 Comunicación con tuberías 2.3.2.4 Programa de copia de ficheros con control de avance 2.3.2.5 Programa de copia de ficheros concurrente 2.4 Recubrimientos y redirecciones 2.4.1 Objetivos 2.4.2 Especificación 2.5 Memoria virtual 2.5.1 Objetivos 2.5.2 Especificación 2.5.2.1 Determinación de direcciones virtuales 2.5.2.2 Exploración del espacio de direcciones virtuales ______________________________________________________________________ 1. Bibliografía y material didáctico · Douglas Comer y David L. Stevens. Internetworking with TCP/IP, Volume III, Client-Server Programming and Applications (BSD version). Prentice-Hall International, 1996. Describe minuciosamente la programación de aplicaciones distribuidas. Coste: mediano. · W. Richard Stevens, Unix Network Programming., Prentice-Hall, 1990. Libro muy especializado en aplicaciones distribuidas sobre Unix. Coste: caro. · W. Richard Stevens, Advanced Programming in the Unix Environment., Addison-Wesley, 1992. Libro muy especializado en aplicaciones no distribuidas sobre Unix. Necesita ser complementado con uno de aplicaciones distribuidas. Coste: caro. · F. M. Márquez, Unix: Programación Avanzada, Segunda Edición. Ra-Ma, 1996. Describe gran parte de la interfaz de programación unix (centralizada y distribuida) y algo de programación en C en un castellano bastante correcto. Coste: mediano. 2. Prácticas Aunque lo que sigue es un conjunto de prácticas interesante para entender el núcleo de Unix y las comunicaciones con sockets, se anima a los alumnos a explorar y experimentar, haciendo su propio camino en el aprendizaje. 2.1. Entrada/salida básica 2.1.1. Objetivos Comprender las operaciones básicas de entrada/salida de un Unix y ver algunos aspectos sobre la eficiencia. 2.1.2. Especificación 1. Estúdiese las operaciones open, read, write, close y exit y estudie y ejecute esta versión simplificada del programa cp, de copia de ficheros, que toma como parámetros un fichero origen y uno destino, solamente. Note que si el fichero destino existe, es reemplazado, y si su tamaño era mayor, es acortado. ______________________________________________________________________ /* cpsimple.c */ #define TAMANO 1024 #include #include #include char buf[TAMANO]; static void error(char* mensaje) { write(2, mensaje, strlen(mensaje)); exit(1); } int main(int argc, char* argv[]) { int leidos, escritos, origen, destino; if (argc != 3) error("Error en argumentos\n"); if ((origen = open(argv[1], O_RDONLY)) < 0) error("Error en origen\n"); if ((destino = open(argv[2], O_CREAT | O_WRONLY | O_TRUNC, 0644)) < 0) error("Error en destino\n"); while ((leidos = read(origen, buf, TAMANO)) > 0) escritos = write(destino, buf, leidos); if (leidos < 0 ) error("Error en lectura\n"); exit(0); } ______________________________________________________________________ 2. Verifique que el núcleo guarda en su cache las últimas lecturas. Para ello realice dos copias sucesivas del mismo fichero a /dev/null y determine el número de octetos por segundo transferidos en ambas copias. Utilice un fichero suficientemente grande para que la medida sea significativa y lo suficientemente pequeño para que quepa en la cache (por ejemplo /usr/bin/emacs). 3. Espíe el funcionamiento del programa a nivel de llamadas al sistema con strace. 4. Cambie la rutina error por lo siguiente, que utiliza una rutina de biblioteca, en lugar de una llamada directa al núcleo. Verifique que funcionan de forma similar y las trazas son equivalentes. ______________________________________________________________________ #include static void error(char* mensaje) { fprintf(stderr, "%s", mensaje); exit(1); } ______________________________________________________________________ 2.2. Procesos 2.2.1. Objetivos Entender los mecanismos de creación y terminación de procesos de Unix. 2.2.2. Especificación 2.2.2.1. Creación simple de procesos Compile el programa fork.c: ______________________________________________________________________ /* fork.c */ #include #include int main(int argc, char* argv[]) { int i, j, pid, salida; char c; for (i=1; i #include #include int main(int argc, char* argv[]) { int i, j, pid, salida, estado; char c; for (i=1; i #include void confirmar(int sig) { char resp[100]; write(1, "¿Quiere terminar? (s/n):", 24); read(0, resp, 100); if (resp[0]=='s') exit(0); signal(SIGINT, confirmar); } int main(void) { int i, j; signal(SIGINT, SIG_IGN); for (i=0; i<10; i++) { write(1, "No hago caso a ^C\n", 18); for (j = 0; j < 10000000; j++); } signal(SIGINT, SIG_DFL); for (i=0; i<10; i++) { write(1, "Ya hago caso a ^C\n", 18); for (j = 0; j < 10000000; j++); } signal(SIGINT, confirmar); for (i=0; i<10; i++) { write(1, "Ahora lo que digas\n", 19); for (j = 0; j < 10000000; j++); } exit(0); } ______________________________________________________________________ y ejecútelo, intentando abortarlo en varios momentos con CONTROL-C. Ejecútelo con strace y observe que signal no es una llamada al sistema, sino una rutina de biblioteca que llama a sigaction. 2.3.2.2. Sincronización con señales Ejecute el programa esignal.c para observar que las señales transmiten información, aunque sea de forma rudimentaria. ______________________________________________________________________ /* esignal.c */ #include #include #include void productor(int pid) { int i; for (i=1; i<=5; i++) { sleep(5); kill(pid, i); } sleep(5); kill(pid, SIGKILL); exit(0); } void atiende(int sig) { printf("Recibida %d\n", sig); } void consumidor(void) { int i; for (i=1; i<=5; i++) signal(i, atiende); for (;;) { write(1, ".", 1); sleep(1); } } int main(void) { int pid; if ((pid= fork())==0) consumidor(); else productor(pid); } ______________________________________________________________________ 2.3.2.3. Comunicación con tuberías Ejecute el programa pipe.c, para observar como el productor y el consumidor se intercambian información: ______________________________________________________________________ /* pipe.c */ #include #include void productor(int esc) { int i; for (i=1; i<=10; i++) { write(esc, &i, sizeof i); sleep(1); } exit(0); } void consumidor(int lec) { int leidos, i; while ((leidos = read(lec, &i, sizeof i)) > 0) { printf("Recibido %d\n", i); } exit(0); } int main(void) { int pid, tubo[2]; pipe(tubo); if ((pid= fork())==0) { close(tubo[1]); consumidor(tubo[0]); } else { close(tubo[0]); productor(tubo[1]); } } ______________________________________________________________________ 2.3.2.4. Programa de copia de ficheros con control de avance Modifique el programa de copia de ficheros realizado por un proceso hijo para que el padre saque un punto por la salida estándar por cada segundo que pase. Úsese sleep o alarm para controlar el tiempo. El programa principal terminará al recibir la señal SIGCHLD, cuando el hijo termine. 2.3.2.5. Programa de copia de ficheros concurrente Modifique el programa de copia de ficheros para que la lectura la haga un proceso y la escritura otro, comúnicándose a través de un pipe. 2.4. Recubrimientos y redirecciones 2.4.1. Objetivos Entender los mecanismos básicos de unix para ejecutar y combinar programas. 2.4.2. Especificación Estudie y pruebe el programa siguiente, que combina dos programa externos por medio de una tubería, cuyos extremos están redirigidos convenientemente a la salida estándar de uno y a la salida estándar del otro. ______________________________________________________________________ /* pipeline.c */ #include #include void productor(void) { execlp("ps", "ps", "-ax", NULL); perror("execlp"); exit(1); } void consumidor(void) { execlp("sort", "sort", "-n", NULL); perror("execlp"); exit(1); } int main(void) { int pid, tubo[2]; pipe(tubo); if ((pid= fork())==0) { close(tubo[0]); dup2(tubo[1], 1); close(tubo[1]); productor(); } else { close(tubo[1]); dup2(tubo[0], 0); close(tubo[0]); consumidor(); } } ______________________________________________________________________ 2.5. Memoria virtual 2.5.1. Objetivos Entender el espacio de direcciones virtual de los procesos. 2.5.2. Especificación 2.5.2.1. Determinación de direcciones virtuales El siguiente programa, imprime en hexadecimal la dirección virtual de diversas partes de un programa. Ejecútelo e intente explicar los resultados. Además mire el tamaño de los distintos segmentos del programa con size. ______________________________________________________________________ #include #include char e1[1000], e2[1000]; char *m = "Hola amigo"; int main(void) { char *p1 = malloc(10000); char *p2 = malloc(10000); printf("main %10x (%10u)\n", main, main); printf("e1 %10x (%10u)\n", &e1, &e1); printf("e2 %10x (%10u)\n", &e2, &e2); printf("m %10x (%10u)\n", &m, &m); printf("*m %10x (%10u)\n", m, m); printf("p1 %10x (%10u)\n", &p1, &p1); printf("p2 %10x (%10u)\n", &p2, &p2); printf("*p1 %10x (%10u)\n", p1, p1); printf("*p2 %10x (%10u)\n", p2, p2); exit(0); } ______________________________________________________________________ 2.5.2.2. Exploración del espacio de direcciones virtuales El siguiente programa, explora el espacio virtual de las direcciones que un proceso puede leer. Ejecútelo e interprete los resultados. ______________________________________________________________________ #include #include #include #include typedef enum {si, no, no_sabe, me_da_igual} accesibilidad; char *dir, *dir0; accesibilidad acc0; int tpagina; jmp_buf estado; void imprime_rango(char *d1, char* d2, accesibilidad acc) { printf("%8x-%8x (%10u-%10u) ", d1, d2, d1, d2); if (acc == si) printf("accesible\n"); else printf("inaccesible\n"); } void informa(accesibilidad acc) { /* Sólo imprime rangos */ if (acc0 == no_sabe) acc0 = acc; else if (acc != acc0) { imprime_rango(dir0, dir-1, acc0); dir0 = dir; acc0 = acc; } dir = dir + tpagina; if (dir == NULL) { imprime_rango(dir0, dir-1, acc); exit(0); } } void viola(int s) { /* Procesa violaciones de memoria */ informa(no); signal(SIGSEGV, viola); longjmp(estado, 1); } int main(void) { char dato; tpagina = getpagesize(); acc0 = no_sabe; dir = NULL; dir0 = dir; signal(SIGSEGV, viola); /* Etiqueta para volver de violación */ setjmp(estado); for (;;) { dato = *dir; /* Si es inaccesible, no se ejecuta */ informa(si); } } ______________________________________________________________________