Prefacio

Las presentes notas fueron confeccionadas en su mayor parte el año 2003, pero nunca se culminaron. Se presentan aquí puesto que el contenido puede ser útil a los interesados.

El texto ha sido convertido a asciidoc en Julio de 2023, pero requiere una revisión detallada (la conversión se ha hecho en forma semi-automática a partir de un formato propietario denominado QDK.)

Asimismo, está pendiente una revisión de diversas afirmaciones que a la fecha no son válidas como suele ocurrir con el paso de los años. Espero hacer esto gradualmente en la medida que el tiempo lo permita.

Del mismo modo, ciertas secciones están desactualizadas. Por ejemplo, las versiones 1.x de SDL actualmente son obsoletas, ergo los ejemplos correspondientes serán difíciles de compilar.

Finalmente, es necesario actualizar las referencias. El ejemplo más relevante en mi opinión es la aparición en 2010 de "The Linux Programming Interface" [1]; en esa misma línea, en 2013 se lanzó la tercera edición del clásico APUE [2].

Sobre estas notas…​

Este texto surgió por la necesidad de ordenar algunas ideas y tener un nuevo pretexto para revisar mucha documentación interesante. Mi pretención es proporcionar una visión general de algunas de las muchas facilidades que tiene a su disposición un programador de C en ambientes Linux y Unix, aunque también puede ser de valor para quien proviene de entornos Windows.

Téngase en cuenta que no pretendo propocionar información de tipo "referencial" sobre ningún tema en particular. Dada la diversidad de éstos, se requeriría muchos libros adicionales. En ese sentido, el lector que desea programar "en serio" deberá obligatoriamente consultar las guías, manuales y referencias correspondientes.

Los listados del texto algunas veces son programas completos; sin embargo, siempre se intenta reutilizar sus subrutinas constituyentes, las cuales ya no se repiten en los listados posteriores. De otro lado, todos los programas de ejemplo que presento han sido probados usando los sistemas Linux RedHat 8.0 [3] y Linux Debian 3.1.

Lima, Febrero de 2006

INTRODUCCION AL AMBIENTE DE PROGRAMACION

1. Revisión breve del lenguaje C

1.1. Introducción

Uno de los lenguajes más importantes e imprescindibles [4] en Linux/Unix es el C. C permite lograr la máxima interacción con el sistema operativo y con el hardware (después del assembler) por lo que se emplea típicamente para la elaboración de controladores de dispositivos (device drivers) y rutinas críticas en programas de propósito general. Con esto no queremos dar a entender que C es el mejor lenguaje para toda situación [5] .

Este texto no pretende enseñar el lenguaje C, y por el contrario, asumiremos que el lector ya lo utiliza con cierta fluidez. Nuestro objetivo central será describir algunas de las muchas maneras en que se puede utilizar en el sistema operativo Linux. Es importante resaltar que casi todo este material puede aplicarse con muy pocas modificaciones a las diferentes variantes de Unix.

Si bien no explicaremos el lenguaje C, es conveniente hacer una recapitulación rápida a fin de que el lector tenga una idea de la base que requiere para proseguir. Asumo que Ud. sabe (o tiene la idea de) cómo compilar programas en C, aunque quizá no en Linux. En el capítulo 2 se trata con detalle el tema de la compilación en Linux/Unix.

1.2. Conceptos Básicos

C es un lenguaje con una larga tradición. Como cualquier lenguaje, ha sufrido cierta evolución a través de los años, y sigue evolucionando aunque a un ritmo más "ordenado".

Los primeros "dialectos" usados en los años 70 y principios de los 80 se conocen como "K&R C" (por Brian Kernigham y Dennis Ritchie, sus creadores) y -aunque suene a falta de respeto- es preferible evitarlos en la medida de lo posible. Su uso puede ser inevitable en un sistemas muy antiguos que no lograron actualizarse a los nuevos tiempos.

A mediados de los 80 se aprecian intentos serios por lograr la estandarización del lenguaje cuyo resultado fue el dialecto "oficial" conocido como "ANSI C". ANSI es la institución de estándares de USA, y su primer estándar oficial para C fue el "X3.159-1989", que data de 1989. En general, los estándares (norteamericanos) de ANSI se han subsumido en la actualidad en los estándares internacionales ISO. Así el mencionado "ANSI C" fue ratificado como el estándar "ISO/IEC 9899:1990". El lenguaje C descrito en estos estándares se conoce informalmente como C89 o C90.

En 1995 se publicó un nuevo estándar (ISO/IEC 9899:1994) que involucraba algunos cambios poco significativos en el lenguaje, centrándose por el contrario en la librería estándar. A esto se le denomina C94 o C95. Finalmente, en 1999 se aparece el estándar ISO/IEC 9899:1999 (conocido como C99) que presenta un número considerable de novedades. Sin embargo, son pocos los compiladores que en la actualidad soportan completamente este estándar [6] .

El lenguaje vs la librería estándar

El lenguaje C es bastante reducido. Consiste de algunas palabras clave (keywords), operadores e identificadores que permiten efectuar ciertas tareas de modo muy eficiente. Sin embargo, muchos de los constituyentes de los programas comunes requieren efectuar tareas para las que el lenguaje no está preparado. Por ejemplo, "trazar una línea de color verde en una ventana" es algo para lo cual no existe ninguna palabra clave.

Esto se ha diseñado así a propósito. La idea es que todas aquellas tareas que van más allá del procesamiento de información "simple" (caracteres, números, conjuntos), deben implementarse mediante llamadas "externas" al lenguaje. En otras palabras, alguien escribirá "rutinas" adicionales que permiten al programador de C poder efectuar todas las tareas que requiere. Estas rutinas adicionales se suelen denominar "librerías" [7] . El programador utiliza las librerías sin requerir conocer cómo trabajan internamente, necesitando tan solo aplicar la documentación referente a cómo se invocan (lo que se conoce como "interfaz de programación" o API.) [8]

Existen muchas librerías para diversos propósitos; por ejemplo, librerías matemáticas para efectuar cálculo matricial, librerías multimedia para procesar archivos de audio y video, librerías para procesar documentos XML, etc.

En particular, es imprescindible conocer el papel de la librería estándar de C, la cual contiene una gran cantidad de rutinas que permiten llevar a cabo tareas que son requeridas en casi todos los programas. Por ejemplo, el lenguaje C no tiene ninguna palabra clave para mostrar o solicitar información al usuario vía la pantalla y el teclado. Sin embargo, la "librería estándar de C" contiene las rutinas necesarias para llevar a cabo estas acciones (por ejemplo, las famosas printf() y fgets(), respectivamente.)

En virtualmente todas las implementaciones de lenguaje C para computadores personales y superiores está garantizada la presencia de la "librería estándar" [9].

Existen casos especiales donde no es conveniente soportar la totalidad de la librería estándar (por ejemplo, dispositivos personales portátiles.)] , lo que conlleva a que todos los cursos y libros que enseñan lenguaje C dediquen gran parte del contenido a describir las rutinas de dicha librería, por lo que la gente muchas veces la considere (incorrectamente) como parte del lenguaje.

Como es de esperarse, esto no ocurre con otras librerías. Esto quiere decir que si el programador está interesado, por ejemplo, en procesar archivos en formato JPEG, deberá buscar (y quizá, comprar) una librería gráfica que le ayude (o tal vez, con mucha más dificultad, escribir su propia librería.) Una vez instalada la librería, deberá ser capaz de indicar al compilador que se desea "enlazar" el programa con dicha librería.

Un programa de prueba

El programa que mostramos a continuación permite indagar acerca de la paridad de un número entero introducido desde la línea de comando o desde el teclado. Sólo en este listado (por ser el primero) numeraremos las líneas:

     1	#include <stdio.h>
     2	#include <stdlib.h>

     3	void par(int);

     4	int main(int argc,char **argv)
     5	{
     6	int n;

     7	if(argc==2)
     8		par(atoi(argv[1]));
     9	else
    10		{
    11		scanf("%d",&n);
    12		par(n);
    13		}
    14	return 0;
    15	}

    16	void par(int x)
    17	{
    18	if(x%2)
    19		printf("El numero es impar\n");
    20	else
    21		printf("El numero es par\n");
    22	}

A continuación los pasos a seguir para la compilación y ejecución en ambiente Linux/Unix. Esto puede variar dependiendo del compilador del que se dispone.

$ cc -o paridad paridad.c
$ ./paridad
3
El numero es impar
$ ./paridad
2
El numero es par
$ ./paridad 7
El numero es impar
$ ./paridad 8
El numero es par

Analicemos las líneas relevantes del programa:

Líneas 1 y 2

Estas directivas #include insertan código C proveniente de otro archivo. Típicamente (como en nuestro caso) se emplean para declarar ciertas funciones de librerías que se van a emplear. Para esto, el estándar de C requiere que se proporcione ciertos archivos auxiliares "de encabezado" (headers) como los que hemos empleado [10] . En particular, stdio.h proporciona las declaraciones de las funciones scanf() [11] y printf(), mientras que stdlib.h proporciona la de atoi(). La siguiente tabla presenta algunos archivos de encabezado:

Table 1. Algunos archivos de encabezado estándar
Archivo Finalidad Algunas rutinas

stdio.h

Entrada/Salida

printf, scanf, getchar, putchar, fgets, fwrite, etc

string.h

Manejo de strings

strcpy, strcat, strcmp, etc

stdlib.h

Conversiones, asignación dinámica de memoria, números aleatorios, entorno, etc

atoi, atof, calloc, free, rand, srand, putenv, exit, system, etc

math.h

Operaciones matemáticas

log, exp, sin, cos, etc

time.h

Control del tiempo y manipulación de fechas

time, ctime, localtime, etc

Línea 3

Ésta corresponde a una declaración de función (prototipo.) Toda función que va a ser usada en un programa, debería [12] siempre declararse tal como lo hemos hecho [13] . De no hacerlo, el compilador genera una declaración por omisión que puede no ser correcta y tendremos errores difíciles de rastrear.

Línea 4

Todo programa se inicia en la función main la cual retorna un entero al sistema operativo (valor de retorno) y puede aceptar un conjunto de argumentos provenientes del mismo. Es también válido descartar los argumentos con una definición tal como:

int main(void)
...
Línea 6

Declaramos y definimos un entero "n". Los enteros (como el resto de tipos básicos de C) no tienen un rango pre-establecido y dependen de la implementación. En computadores con microprocesadores de 16 bits, los enteros int suelen estar en el rango [-2 ^ 15 ; 2 ^ 15 - 1] aunque son posibles otros valores. En computadores con microprocesadores de 32 bits, por el contrario, el rango típico es [-2 ^ 31 ; 2 ^ 31 - 1]. Esto también puede variar dependiendo del compilador empleado.

En aplicaciones que requieren un rango determinado para sus operaciones, es frecuente hacer uso de typedef para crear un nuevo tipo:

typedef int ENTERO;

Nuestro programa podría emplear ahora el tipo ENTERO para todos sus cálculos. Pero si portamos el programa a un computador cuyos enteros int son demasiado pequeños, entonces podríamos fácilmente cambiar la sentencia anterior por algo como:

typedef long ENTERO;

Así nuestro tipo ENTERO tiene ahora un rango mayor.

Esta solución de todas maneras es inconveniente, desde que nos hemos visto obligados a modificar el código y recompilarlo. El estándar C99 proporciona un nuevo conjunto de enteros con rangos definidos; por ejemplo, enteros de 8, 16, 32 y 64 bits representados en los tipos int8_t, int16_t, int32_t e int64_t respectivamente.

Línea 7

main recibe el número de argumentos y un array con los argumentos en las variables argc y argv. El primer argumento (argv[0]) es el propio nombre del programa que se ha ejecutado (./paridad) y los otros (argv[1], argv[2]…​) corresponden al resto de la línea de comandos. Nuestro programa usará argv[1] sólo si éste se proporciona, para lo cual debe consultar el primer argumento (argc.) Nótese que los nombres argc y argv son sólo convencionales.

Línea 8

La invocación a la función par() con el argumento argv[1] convertido a entero, cuando el test de la línea 7 haya sido satisfactorio.

Línea 9

Literalmente, "…​ en caso contrario…​" Esta sección es opcional en la senencia if().

Línea 11

Leer un entero desde la entrada estándar, asignándolo a la variable entera “n”.

Línea 14

Este programa siempre retorna cero al sistema operativo. Convencionalmente, el cero significa "éxito" y cualquier otro valor significa "error". El shell Linux/Unix puede usar una construcción como esta, gracias al valor de retorno:

$ ./paridad 7 && echo Programa exitoso || echo Programa fallido
Línea 16

La definición de la función par() que a diferencia de su prototipo ahora sí tiene el nombre del argumento (x.) En los prototipos esto no es necesario.

Línea 18

El operador '%' significa "residuo" y permite verificar la paridad rápidamente [14] .

Línea 19

printf() permite imprimir "con formato". En nuestro caso, el formato es un texto sencillo y realmente no se aprovecha. Podríamos haber empleado exitosamente la función puts().

Si Ud. no comprendió este programa…​ búsque un buen libro de C y retorne aquí cuando lo haya completado.

1.3. Punteros

Gran parte del poder del lenguaje C se deriva de la posibilidad de hacer uso de los "punteros"…​ y viceversa, gran parte de los problemas del lenguaje C se derivan de la posibilidad de hacer uso de los "punteros".

El siguiente programa solicita al usuario un número entero “n”. Luego construye una matriz de n x n enteros y los rellena con valores aleatorios de -10 a +10. Luego imprime la matriz e invoca a la función "suma()" que imprime la suma de todos los elementos.

/*
 * matrix.c: crea una matriz dinamicamente
 * con valores aleatorios y suma los elementos
 */
#include <stdio.h>
#include <stdlib.h>

void suma(int **m,int q);

int main(void)
{
int n,z,j;
int **matriz;

printf("n? ");
scanf("%d",&n);

matriz=calloc(n,sizeof(int*));
for(z=0;z<n;z++)
	{
	matriz[z]=calloc(n,sizeof(int));
	for(j=0;j<n;j++) matriz[z][j]=rand()%21-10;
	}

for(z=0;z<n;z++)
	{
	for(j=0;j<n;j++) printf("%3d",matriz[z][j]);
	printf("\n");
	}
suma(matriz,n);
return 0;
}

void suma(int **m,int n)
{
int z,j,s=0;

for(z=0;z<n;z++)
	for(j=0;j<n;j++) s+=m[z][j];
printf("Suma=%d\n",s);
}

A continuación un ejemplo de ejecución:

$ ./matrix
n? 10
 -9 -6 -1  9 -2  0  0 -1  5  0
 -8  9 10 -6 10 -3 -7  5  6  6
  7  4  2 -1 -8 -5 -5  3 -9  9
 -5-10 -7  2  7 -1 -9 -3  6  6
  5  8  2  4 10  0 10 -8 -8  5
  7  9 -3 -2 -2 -1  1  1 -9  0
 -1 -4  1  2  7 -5  9  8  0  4
  2 -7 -1 -7  7 -1  1  4 -1  4
 -3 -7  2  4  1  8-10 -8 -2 -9
  2  8 -5-10 -3 -9 -6 -4  8  4
Suma=12

Obsérvese que hemos hecho uso extensivo de la sintaxis de acceso a elementos de arrays (x[][]), pero en ninguna parte del programa se han definido variables de este tipo. Esto es sólo una conveniencia pues como se sabe:

X[n]      es igual que     *(X+n)
X[n][m]   es igual que     *(*(X+n)+m)

Supongo que habrá observado (o que ya sabía) que los valores de la matriz son siempre los mismos (ejecución tras ejecución) para un mismo “n”. Esto es así debido a que los números generados son pseudoaleatorios y por tanto, no verdaderos aleatorios. A fin de lograr una mejor "sensación" de aleatoriedad, se suele hacer uso de:

#include <time.h>
...
srand(time(NULL));

srand() inicializa la semilla de números aleatorios. Obviamente, se debería invocar antes de generar la matriz [15] .

La rutina calloc() sirve para reservar un bloque de memoria (inicializándolo a cero.) La hemos usado para generar un conjunto de n "punteros a int", y luego cada uno de éstos se ha hecho apuntar a nuevas áreas (también asignadas con calloc()) usadas para almacenar "conjuntos de ‘n’ valores int".

No hemos verificado -por abreviar- que calloc() NO haya retornado NULL, lo que hubiera significado (típicamente) que el sistema ya no tiene la memoria suficiente o que el usuario está limitado en su consumo [16] .

Punteros y Estructuras

Iniciaremos la elaboración de un programa que almacene los títulos de los libros de una biblioteca en la memoria del computador y que finalmente los grabará en disco. La siguiente estructura nos puede será útil:

struct LIBRO
	{
	char titulo[40];
	char autor[30];
	int edicion;
	int a_publicacion;
	};

Y la biblioteca entera puede ser un array de estas estructuras:

struct LIBRO bib[800];
int n_bib=0;

Por comodidad se suele definir el tipo “LIBRO” como la estructura en sí:

typedef struct
	{
	char titulo[40];
	char autor[30];
	int edicion;
	int a_publicacion;
	} LIBRO;

La biblioteca será ahora:

LIBRO bib[800];
int n_bib=0;

Si nuestra biblioteca sólo tiene 10 libros, entonces se está desperdiciando 790 estructuras en la valiosa memoria RAM. A fin de no desperdiciar tanto, podríamos sólo definir un puntero hacia un área de memoria que se incrementará conforme se requiera:

LIBRO *bib;
int n_bib=0;

A continuación un programa para llenar la biblioteca en memoria y guardarla en un archivo del disco:

/*
 * bib1.c: Lee un conjunto de libros y los graba
 * en disco
 */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef struct
        {
	char titulo[40];
	char autor[30];
	int edicion;
	int a_publicacion;
	} LIBRO;

LIBRO *bib=NULL;
int n_bib=0;
char tmp_line[80];

void getline(char *t)
{
printf("%s",t);
if(fgets(tmp_line,80,stdin)==NULL)
	{
	tmp_line[0]=0;
	return;
	}
tmp_line[strlen(tmp_line)-1]=0;
}

int main()
{
FILE *fp;

for(;;)
	{
	getline("Titulo? ([Enter] para terminar) ? ");
	if(tmp_line[0]==0) break;
	bib=realloc(bib,(n_bib+1)*sizeof(LIBRO));
	strcpy(bib[n_bib].titulo,tmp_line);

	getline("Autor? ");
	strcpy(bib[n_bib].autor,tmp_line);

	getline("Edicion? ");
	bib[n_bib].edicion=atoi(tmp_line);

	getline("Anho publicacion? ");
	bib[n_bib].a_publicacion=atoi(tmp_line);

	n_bib++;
	}
fp=fopen("libros.dat","w");
fwrite(bib,sizeof(LIBRO),n_bib,fp);
fclose(fp);
return 0;
}

Ahora lo vemos en ejecución. Hemos hecho además un volcado del contenido del archivo de disco [17] , el cual muestra una gran cantidad de espacio desperdiciado por los arrays componentes de la estructura.

$ ./bib
Titulo? ([Enter] para terminar) ? La muerte en Lima
Autor? Suicida anonimo
Edicion? 5
Anho publicacion? 2004
Titulo? ([Enter] para terminar) ? La vida en Azpitia
Autor? Danfy
Edicion? 6
Anho publicacion? 1997
Titulo? ([Enter] para terminar) ?
$ od -c libros.dat
0000000   L   a       m   u   e   r   t   e       e   n       L   i   m
0000020   a  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000040  \0  \0  \0  \0  \0  \0  \0  \0   S   u   i   c   i   d   a
0000060   a   n   o   n   i   m   o  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000100  \0  \0  \0  \0  \0  \0  \0  \0 005  \0  \0  \0 324  \a  \0  \0
0000120   L   a       v   i   d   a       e   n       A   z   p   i   t
0000140   i   a  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000160  \0  \0  \0  \0  \0  \0  \0  \0   D   a   n   f   y  \0  \0  \0
0000200  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000220  \0  \0  \0  \0  \0  \0  \0  \0 006  \0  \0  \0 315  \a  \0  \0
0000240
$
Lista enlazada

Una vez que hemos leído todos los datos de los libros desde el teclado, ¿cómo haríamos para añadir o eliminar un registro intermedio (digamos, el registro "k-esimo") de nuestra biblioteca?

Pensemos sólo en eliminar un libro. Primeramente habría que desplazar los libros un registro, cubriendo el "k-esimo":

	for(z=k;z<n_bib-1;z++)
		memcpy(&bib[z+1],&bib[z],sizeof(LIBRO))

o en forma más compacta:

	memcpy(bib+k,bib+k+1,(n_bib-k-1)*sizeof(LIBRO))

Y a fin de liberar la memoria anteriormente asignada al registro, tendríamos que reasignar el área con “realloc()” a un tamaño igual a n_bib-1:

	bib=realloc(bib,(n_bib-1)*sizeof(LIBRO));

Todo este desplazamiento es muy ineficiente [18] (pues por un sólo registro podríamos tener que desplazar físicamente a todos los otros) y en ciertas aplicaciones resulta inaceptable por el tiempo que puede conllevar. Una solución muy común a este tipo de problema consiste en utilizar las clásicas "listas enlazadas". Esto demanda que redefinamos la estructura del siguiente modo:

typedef struct LIBRO {
	char titulo[40];
	...
	struct LIBRO * siguiente;
	} LIBRO;

Nótese que requerimos especificar el tipo de la estructura (struct LIBRO) para que el puntero “*siguiente” haga referencia a la misma, y luego hacemos que el tipo struct LIBRO sea equivalente a LIBRO gracias al typedef. Una forma exactamente igual a lo anterior sería:

struct LIBRO {
	char titulo[40];
	...
	struct LIBRO * siguiente;
	};
typedef struct LIBRO LIBRO;

El programa que viene ahora es similar al anterior, con la diferencia que tras introducir los libros se solicita al usuario el índice (empezando de uno) de uno de los libros a ser eliminado de la lista enlazada:

/*
 * bib2.c: Leer un conjunto de libros, guardarlos en una
 * lista enlazada, y eliminar un elemento intermedio
 */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef struct LIBRO
        {
	char titulo[40];
	char autor[30];
	int edicion;
	int a_publicacion;
	struct LIBRO * siguiente;
	} LIBRO;

LIBRO *primero=NULL,*ultimo;
char tmp_line[80];

void getline(char *t)
{
printf("%s",t);
if(fgets(tmp_line,80,stdin)==NULL)
	{
	tmp_line[0]=0;
	return;
	}
tmp_line[strlen(tmp_line)-1]=0;
}

int main()
{
LIBRO *k,*k_anterior;
int z,eliminable;

for(;;)
	{
	getline("Titulo? ([Enter] para terminar) ? ");
	if(tmp_line[0]==0) break;
	if(primero==NULL)
		{
		primero=calloc(1,sizeof(LIBRO));
		ultimo=primero;
		}
	else
		{
		ultimo->siguiente=calloc(1,sizeof(LIBRO));
		ultimo=ultimo->siguiente;
		}
	ultimo->siguiente=NULL;
	
	strcpy(ultimo->titulo,tmp_line);
	getline("Autor? "); strcpy(ultimo->autor,tmp_line);
	getline("Edicion? "); ultimo->edicion=atoi(tmp_line);
	getline("Anho public.? "); ultimo->a_publicacion=atoi(tmp_line);
	}

getline("\nQue libro eliminara? ");
eliminable=atoi(tmp_line);
if(eliminable==1)
	abort(); /* Falta corregir esto */

k=primero; z=0;
while(k)
	{
	z++;
	if(z==eliminable)
		{
		k_anterior->siguiente=k->siguiente;
		free(k);
		break;
		}
	k_anterior=k;
	k=k->siguiente;
	}
printf("Nuevo listado de libros:\n");
k=primero;
while(k)
	{
	printf("%s\n",k->titulo);
	k=k->siguiente;
	}

return 0;
}

Como se ve, se mantiene un puntero llamado “primero” al primer elemento de la lista enlazada, así como el puntero “ultimo” al último. El puntero al último elemento sirve para añadir nuevos elementos a la lista con facilidad:

ultimo->siguiente=calloc(1,sizeof(LIBRO));

Por otro lado, obsérvese que la inicialización del primer elemento es un caso especial pues allí no tiene sentido lo anterior (dado que "primero" y "ultimo" están en NULL.)

La eliminación del elemento “eliminable” consiste en una "reconexión" de la lista, y la liberación aislada del elemento. Nótese que cuando se escoge el primer elemento el programa aborta (abort(3)). Ud. deberá (como ejercicio) completar el código necesario para eliminar el primer elemento pues es un caso especial en el que no hay un “k_anteiror” apuntando a algo.

$ ./bib2
Titulo? ([Enter] para terminar) ? Libro 1
Autor? xxx
Edicion? 1
Anho public.? 2000
Titulo? ([Enter] para terminar) ? Libro 2
Autor? xxx
Edicion? 2
Anho publicacion? 2001
Titulo? ([Enter] para terminar) ? Libro 3
Autor? xxx
Edicion? 2002
Anho public.? 2002
Titulo? ([Enter] para terminar) ? Libro 4
Autor? jj benites
Edicion? 29
Anho public.? 2070
Titulo? ([Enter] para terminar) ?

Que libro eliminara? 3
Nuevo listado de libros:
Libro 1
Libro 2
Libro 4
$
Graficando funciones

El siguiente programa es tremendamente sencillo, pero nos permite ilustrar los punteros a funciones. Lo implementaremos en varios archivos.

La rutina más importante se guarda en el archivo graficador.c:

#include <stdio.h>

#define MAX_X 60
#define MAX_Y 20

void graficador(double x1, double y1, double x2, double y2,
		double (*f)(double))
{
int x,y,valor[MAX_X];

for(x=0;x<MAX_X;x++)
	valor[x]=MAX_Y*((*f)( x1+x*(x2-x1)/MAX_X )-y1)/(y2-y1);

putchar('+');
for(x=0;x<MAX_X;x++) putchar('-');
putchar('+');
putchar('\n');

for(y=MAX_Y;y>=0;y--)
	{
	putchar('|');
	for(x=0;x<MAX_X;x++)
		{
		if(valor[x]==y)
			putchar('*');
		else
			putchar(' ');
		}
	putchar('|');
	putchar('\n');
	}

putchar('+');
for(x=0;x<MAX_X;x++) putchar('-');
putchar('+');
putchar('\n');
}

Esta rutina grafica en la pantalla [19] la región (x1,y1)-(x2,y2) y obtiene los valores de una función real que se le pasa como argumento. Estos valores se convierten al rango de enteros [0,MAX_Y] para ser dibujados en la pantalla con facilidad.

La función main, junto con un par de ejemplos que aparecen en la electrónica básica [20] , se muestran a continuación (archivo prueba.c):

#include "graficador.h"
#include <math.h>

double rect_completa(double x)
{
return fabs(sin(x));
}

double rect_media(double x)
{
if(sin(x)<0)
	return 0;
return sin(x);
}

int main(void)
{
graficador(0,-1.4,2*3.1416,1.4,sin);
graficador(0,-2,2*3.1416,2,tan);
graficador(0,-0.8,5*3.1416,1.8,rect_media);
graficador(0,-0.8,5*3.1416,1.8,rect_completa);
return 0;
}

Por comodidad, hemos creado el archivo de encabezado graficador.h cuya única finalidad es declarar la rutina graficador():

void graficador(double , double , double , double ,
		double (*)(double));

Compile todo esto con:

$ gcc -o prueba prueba.c graficador.c
$ ./prueba

El resultado es el siguiente:

Sinusoide, Tangente, Rectificadores de Onda

Ejercicios

Implemente para el programa de biblioteca las siguientes funciones (accesibles desde un menú):

  1. Ingreso de un nuevo libro

  2. Búsqueda de libros por título

  3. Búsqueda de libros por autor

  4. Eliminación de un libro

  5. Guardar la base de datos

El programa cargará la base de datos desde disco tan pronto como se inicia.

2. Usando el compilador Gcc y Make

Casi la totalidad de instalaciones Linux emplean el compilador "GCC" (Gnu Compiler Collection) para compilar programas en C [21] . GCC ha sido portado a diversas arquitecturas y está disponible prácticamente en todas las variantes Unix. Sin embargo, cada Unix suele proporcionar su propio compilador, aunque en ciertos casos es un producto "opcional" (es decir, hay que comprarlo.)

La compilación de programas C en Linux se lleva a cabo mediante el comando gcc [22] . En los sistemas Unix frecuentemente se usa el comando cc, o algunas variantes como "c89" que deben estar documentadas en los manuales correspondientes; en muchos de los ejemplos que se muestran a continuación, "gcc" puede ser reemplazado por "cc" sin mayor dificultad.

2.1. Usando el comando Gcc (o cc)

Compilar programas de un sólo archivo fuente

El siguiente listado presenta un programa completo (prog1.c)

/*
 * prog1.c: Saludo trivial
 */
#include <stdio.h>

int main(void)
{
printf("Hola mundo\n");
return 0;
}

La compilación de un programa contenido totalmente en un único archivo (como el ejemplo de arriba) es muy simple. Usamos comando gcc (o cc), especificando el nombre del ejecutable resultante con la opción -o, y el archivo fuente:

$ gcc -o prog1 prog1.c
$ ./prog1
Hola mundo

Como Ud. debe saber bien, la compilación de un programa corresponde a la traducción del código fuente (C en nuestro caso) a código máquina (específico del microprocesador.) Este código máquina se guarda (si se desea) en "archivos objeto" (también llamados de "código objeto" o simplemente "objetos") con extensión ".o". Nótese que en ejemplo anterior no se generó ningún archivo objeto [23] , cosa que sí haremos a continuación:

$ rm prog1
$ gcc -c prog1.c
$ ls
prog1.c  prog1.o

El siguiente paso es el llamado "enlace" (link), el cual genera el ejecutable final a partir de uno o más "archivos objeto". Para el ejemplo anterior, ejecutaríamos lo siguiente:

$ gcc -o prog1 prog1.o
$ ls
prog1  prog1.c  prog1.o

Como se ve, en estos dos pasos hemos generado tanto el "archivo objeto" como el "ejecutable".

Enlazando la librería matemática

En muchos sistemas las funciones matemáticas/científicas no se enlazan automáticamente. Por ejemplo, considere el programa (prog2.c.)

/*
 * prog2.c: Calculo matematico
 */
#include <stdio.h>
#include <math.h>

#define PI 3.1416

int main(void)
{
printf("Seno de PI/4 es %lf\n",sin(PI/4));

return 0;
}

A pesar de que no tiene error alguno, su compilación falla:

$ gcc -o prog2 prog2.c
/tmp/ccUAjaIU.o: In function `main':
/tmp/ccUAjaIU.o(.text+0x21): undefined reference to `sin'
collect2: ld devolvió el estado de salida 1
$

Obsérvese que la compilación propiamente dicha no es la que falla, sino la etapa de "enlace". Esto se aprecia en la última línea donde se menciona el programa “ld” (link editor, el enlazador [24] .)

La penúltima línea del mensaje indica que hay una "referencia indefinida a sin ". Es decir, no se puede ubicar el código de la función sin(). La solución es muy sencilla. Basta con añadir el enlace de la librería matemática:

$ gcc -o prog2 prog2.c -lm
$ ./prog2
Seno de PI/4 es 0.707108

La explicación en detalle de la opción -lXXX se proporciona en el capítulo 3. Por ahora sólo recuerde que en ciertos programas deberá añadir -lm al comando de compilación.

2.2. Opciones del compilador

He de empezar advirtiendo que cada compilador tiene sus propias opciones aunque hay cierto acuerdo en las más sencillas. Por ejemplo, en las secciones anteriores hemos apreciado el uso de las siguientes opciones: .Algunas opciones "estándar"

Opcion Significado

-c

Compilar generando archivo objeto pero no enlazar

-o outfile

Nombre del archivo (outfile) generado como resultado

-lXXX

Enlaza con la librería dinámica libXXX.so

En esta sección veremos más opciones de compilación; sin embargo, mi referencia es el compilador GNU C (gcc.) Si Ud. emplea un compilador distinto tendrá que contrastar este texto con los manuales respectivos.

Table 2. Opciones de precompilación
Opción Significado

-D NOMBRE[=VALOR]

Define un macro

-U NOMBRE

Cancela la definición de macro

-I RUTA

Especifica ruta para buscar archivos de encabezado

-E

Sólo invoca al precompilador sin compilar

Para el siguiente programa:

/*
 * test_precompilador.c
 */
#include <stdio.h>

#ifdef NUMEROS_GRANDES
typedef long long ENTERO;
#define FORMATO "%lld"
#else
typedef int ENTERO;
#define FORMATO "%d"
#endif

#define expande(t) tostr(t)
#define tostr(s) #s

#ifdef TITULO
char *tit= expande(TITULO) "\n";
#else
char *tit="Prueba de preprocesador\n";
#endif

int main(void)
{
ENTERO a;
printf("%s",tit);

printf("Escriba un numero? ");scanf(FORMATO,&a);
printf("Su mitad es " FORMATO "\n",a/2);
return 0;
}

Obtenemos la siguiente salida dependiendo de las macros establecidas:

$ gcc -o test_precompilador test_precompilador.c
$ ./test_precompilador
Prueba de preprocesador
Escriba un numero? 30000
Su mitad es 15000
$ ./test_precompilador
Prueba de preprocesador
Escriba un numero? 40000000000
Su mitad es 1073741823

El último resultado claramente es erroneo, lo que se debe a que el valor introducido no es soportado en el tipo de dato entero empleado (por omisión, int.) Si recompilamos con los flags adecuados, forzaremos a emplear un tipo de dato más grande (aquí, long long) que soportará el valor. Adicionalmente hemos modificado "título" con que se inicia el programa [25] .

$ gcc -DTITULO="Manzana Verde" -DNUMEROS_GRANDES \
        -o test_precompilador test_precompilador.c
$ ./test_precompilador
Manzana Verde
Escriba un numero? 40000000000
Su mitad es 20000000000
Optimización

La opción “-O” ("O" mayúscula, posiblemente acompañada de un número) suele usarse para solicitar al compilador que optimice el programa a fin de lograr mejor performance [26] .

El siguiente programa de prueba permite hacer un extenso cálculo de enteros. Requiere que le proporcionemos en la línea de comandos un valor numérico inicial totalmente arbitrario [27] y un segundo argumento que corresponde al número de iteraciones a calcular.

/*
 * test_optimizacion.c: Opciones de optimizacion
 */
#include <stdio.h>
#include <stdlib.h>

long s1,s2,s3,s4;

void x1(void) { s1=(10*s4+s3+31)%79+3; }
void x2(void) { s2=(7*s4+s3*8+9*s4+19)%25+4; }
void x3(void) { s3=(s1*3+11*s2+13*s4)%191+5; }
void x4(void) { s4=(s1*9+s2*5+s3*17+37)%421+6; }

int main(int argc,char **argv)
{
long z,n;

s1=atol(argv[1]); s2=17; s3=90; s4=13;
n=atol(argv[2]);

for(z=0;z<n;z++)
	{
	x1(); x2(); x3(); x4();
	}
printf("%ld %ld %ld %ld\n",s1,s2,s3,s4);
return 0;
}

Haciendo pruebas en mi sistema (un Athlon XP 2000), 25'000,000 resulta ser un valor que proporciona resultados del orden de pocos segundos [28] :

$ gcc  -o test_optmizacion test_optmizacion.c
$ time ./test_optmizacion 9 25000000
20 9 107 403

real    0m5.419s
user    0m5.330s
sys     0m0.002s
$ gcc -O  -o test_optmizacion test_optmizacion.c
$ time ./test_optmizacion 9 25000000
20 9 107 403

real    0m5.046s
user    0m4.939s
sys     0m0.004s
$ gcc -O2  -o test_optmizacion test_optmizacion.c
$ time ./test_optmizacion 9 25000000
20 9 107 403

real    0m4.914s
user    0m4.898s
sys     0m0.004s
$ gcc -O3  -o test_optmizacion test_optmizacion.c
$ time ./test_optmizacion 9 25000000
20 9 107 403

real    0m4.074s
user    0m4.061s
sys     0m0.000s

Como se aprecia, la reducción de tiempo (real) para la optmización "simple" (-O) fue de aproximadamente 6.9%, para la optimización de nivel 2 (-O2) resulta aproximadamente 10% y para la de nivel 3 (-O3) alcanza un 26%. Estos niveles de mejora son tremendamente variables dependiendo del contenido del programa (hay programas que con todas las opciones no mejoran ni 1%.)

Arquitectura y tipo de CPU

Los compiladores de C típicamente generan código ejecutable por un conjunto muy amplio de procesadores (de una misma familia) a fin de que el programa pueda ser fácilmente distribuido entre computadores similares. Sin embargo, esto obliga generalmente a desaprovechar la potencia de los computadores más avanzados (de dicha familia.) El compilador gcc dispone de la opción -march para especificar la "arquitectura" del procesador que ejecutará el programa (otros compiladores deben proporcionar opciones análogas.)

En el caso del computador que estoy usando en estos momentos, (un Athlon XP) la opción correspondiente es -march=athlon-xp. Los resultados son espectaculares:

$ gcc  -o test_optmizacion test_optmizacion.c
$ time ./test_optmizacion 9 25000000
20 9 107 403

real    0m5.419s
user    0m5.330s
sys     0m0.002s
$ gcc -march=athlon-xp -o test_optmizacion test_optmizacion.c
$ time ./test_optmizacion 9 25000000
20 9 107 403

real    0m2.846s
user    0m2.832s
sys     0m0.002s
$ gcc -O3 -march=athlon-xp -o test_optmizacion test_optmizacion.c
$ time ./test_optmizacion 9 25000000
20 9 107 403

real    0m1.479s
user    0m1.467s
sys     0m0.000s

Ahora el tiempo de ejecución se ha reducido en 47% por sólo aprovechar la arquitectura del CPU, y en 73% si además se aprovecha la optimización de nivel 3.

Evidentemente, el precio a pagar es alto: el ejecutable puede no ser utilizable en computadores que emplean la familia de microprocesadores Intel, o incluso modelos anteriores de AMD.

Alertas

El compilador normalmente es capaz de alertar acerca de código fuente válido pero "riesgoso", lo que muchas veces significa que hemos cometido un error involuntario.

Por ejemplo, considere el siguiente programa "sintácticamente" correcto:

/*
 * /* Comentario anidado?
 */

#include <stdio.h>

main()
{
float f;
int t;
printf("Un numero como %d\n",f);
}

Si lo compilamos con gcc (al menos con las opciones preinstaladas en mi sistema) no obtengo niguna alerta:

$ gcc -o test_alertas test_alertas.c

A continuación pediremos al compilador sea más quisquilloso con nuestro programa. Las opciones que se muestran a continuación son específicas de gcc y Ud. deberá consultar la documentación respectiva si emplea otro compilador. Para empezar, solicitemos al compilador que nos alerte acerca de posibles "comentarios anidados" como el que tiene nuestro programa:

gcc -Wcomment -o test_alertas test_alertas.c
test_alertas.c:2:4: aviso: "/*" dentro de un comentario

Más útiles son los avisos con respecto a incoherencias en los formatos de printf() y similares. En nuestro caso el formato "%d" no podrá procesar un "float":

$ gcc -Wformat -o test_alertas test_alertas.c
test_alertas.c: En la función `main':
test_alertas.c:11: aviso: formato int, argumento double (argumento 2)

Otro aspecto cuestionable es que no hemos especificado el valor de retorno de main() (el cual es int por omisión) y pudo ser un descuido nuestro. Hagamos que el compilador nos alerte en estos casos:

$ gcc -Wimplicit-int -o test_alertas test_alertas.c
test_alertas.c:8: aviso: el tipo de devolución por omisión es `int'

De acuerdo a lo anterior, la función main() debe retornar un entero; sin embargo, en ningún momento hemos retornado nada. Esto también puede ser alertado por el compilador:

$ gcc -Wreturn-type -o test_alertas test_alertas.c
test_alertas.c:8: aviso: el tipo de devolución por omisión es `int'
test_alertas.c: En la función `main':
test_alertas.c:12: aviso: el control alcanza el final de una función
que no es void

La variable 't' no se usa:

gcc -Wunused-variable -o test_alertas test_alertas.c
test_alertas.c: En la función `main':
test_alertas.c:10: aviso: variable `t' sin uso

Bien, suficiente diversión. Hay muchas otras alertas que se podrían configurar de modo similar, sin embargo la opción -Wall nos puede ahorrar todo el trabajo:

$ gcc -Wall -o test_alertas test_alertas.c
test_alertas.c:2:4: aviso: "/*" dentro de un comentario
test_alertas.c:8: aviso: el tipo de devolución por omisión es `int'
test_alertas.c: En la función `main':
test_alertas.c:11: aviso: formato int, argumento double (argumento 2)
test_alertas.c:10: aviso: variable `t' sin uso
test_alertas.c:12: aviso: el control alcanza el final de una función
que no es void

Esta opción implica la máxima rigurosidad en alertas, y la recomiendo siempre que sea posible. Trate de "pulir" sus programas hasta que no aparezca ninguna [29] .

Estándares

Considere el siguiente programa que emplea enteros extra-largos (long long int.) Estos enteros no están soportados en el estándar C89 (ver capítulo 1) pero sí son parte del C99:

/*
 * test_estandar.c: C89 vs C99
 */
#include <stdio.h>

int main(void)
{
long long int x=6LL;

printf("Superentero: %lld\n",x);

return 0;
}

Gcc normalmente utiliza el estándar C89 así como extensiones específicas de GNU. Si deseamos especificar un cumplimiento estrícto del estándar "c89" (sin extensiones) debemos usar la opción -std [30] junto con la opción -pedantic. En nuestro programa, éstas producen el siguiente resultado:

$ gcc -Wall -pedantic -std=c89 -o test_estandar test_estandar.c
test_estandar.c: En la función `main':
test_estandar.c:5: aviso: ISO C89 no da soporte a `long long'
test_estandar.c:5: aviso: ISO C89 prohibe constantes enteras long long
test_estandar.c:7: aviso: ISO C89 no tiene soporte para el modificador
de longitud ll `printf'

El mismo programa, sujeto al estándar C99 no tiene estos inconvenientes:

$ gcc -Wall -pedantic -std=c99 -o test_estandar test_estandar.c
$

2.3. Automatización de la compilación con Make

Programas de varios archivos de código fuente

En los programas grandes es frecuente separar el código en diversos archivos para que todo sea más manejable. El siguiente archivo (modulo1.c) efectua un cálculo matemático a partir de dos números enteros:

/*
 * modulo1.c
 */
int mcd(int a,int b)
{
int r;
while(b)
	{
	r=a%b;
	a=b;
	b=r;
	}
return a;
}

Espero que el lector haya descubierto que esta función simplemente calcula el máximo común divisor con el algoritmo de Euclides. Ahora viene la segunda parte (modulo2.c):

/*
 * modulo2.c
 */
#include <stdio.h>

int mcd(int,int);

int main(void)
{
int x=510, y=340;

printf("MCD de %d y %d es %d\n",x,y,mcd(x,y));
return 0;
}

Método 1

Para compilar todo en un solo paso (sin generar "archivos objeto"), podríamos hacer lo siguiente:

$ gcc -o mcd modulo1.c modulo2.c

Esto compila ambos archivos fuente y genera un único ejecutable (que he llamado mcd.)

Método 2

La otra manera de compilar esto (y que es más importante para lo que veremos) es hacerlo todo paso a paso:

$ gcc -c modulo1.c
$ gcc -c modulo2.c
$ gcc -o mcd modulo1.o modulo2.o

La diferencia es más trascendente de lo que puede parecer en principio. Obsérvese que las dos primeras operaciones son "compilaciones" y la última sólo es un "enlace". ¿Qué ocurriría si hacemos una modificación en - por ejemplo - modulo1.c? Si usamos el primer método, el compilador estaría recompilando inutilmente a modulo2.c. El segundo método permite evitar esto:

$ gcc -c modulo1.c
$ gcc -o mcd modulo1.o modulo2.o

Por tanto es más eficiente que el primero.

Esta eficiencia es importantísima en proyectos grandes; por ejemplo, con 100 o más archivos de código fuente. La automatización de todas estas cuestiones se las podemos dejar a make.

Make es una herramienta diseñada para compilar proyectos relativamente grandes y con "dependencias" complicadas.

Dependencias y el Makefile

Un concepto fundamental en “make” [31] son las relaciones de dependencia. En el ejemplo anterior observamos la siguiente secuencia de operaciones:

   modulo1.c  --> modulo1.o ---+
                                +--> mcd
   modulo2.c  --> modulo2.o ---+

Si - por ejemplo - modulo1.c es modificado por el programador, entonces es evidente que debemos generar un nuevo archivo objeto modulo1.o para dar lugar a un nuevo ejecutable mcd. En otras palabras, mcd depende de modulo1.o y de modulo2.o, mientras que modulo1.o depende de modulo1.c, y por último modulo2.o depende de modulo2.c.

Nótese que todo archivo (excepto los de código fuente) debe ser construido a partir de sus "dependencias". Todo esto lo especificaremos en un archivo llamado convencionalmente "Makefile" que es usado por el comando “make” para describir lo que tiene que hacer. Para nuestro ejemplo sería algo así:

# Primera version de Makefile

mcd: modulo1.o modulo2.o
	gcc -o mcd modulo1.o modulo2.o

modulo1.o: modulo1.c
	gcc -c modulo1.c

modulo2.o: modulo2.c
	gcc -c modulo2.c

Por lo que podemos apreciar existen tres "reglas", que son las que indican qué archivos dependen de cuales, y cómo generarlos cuando hace falta. En la primera regla, el "target" u "objetivo" a construir es el ejecutable “mcd”. Éste se debe reconstruir cuando está desactualizado con respecto a sus dependencias modulo1.o o modulo2.o. Luego de esta especificación de dependencias se debe colocar los comandos a ejecutarse para satisfacer tales dependencias. Como sabemos, gcc permite generar el ejecutable a partir de los archivos objeto.

Análogamente, tanto modulo1.o como modulo2.o tienen dependencias respecto a los archivos fuente modulo1.c y modulo2.c respectivamente, cosa que se especifica mediante dos reglas adicionales.

Note
Estos comandos especificados en cada regla deben iniciarse siempre con un tabulador al principio de la línea. De no ser así, make asume que se trata de una nueva regla. No se puede usar espacios, necesariamente es un caracter de tabulación.
Invocación de make

El "Makefile" anteriormente presentado puede guardarse en cualquier archivo de texto, aunque por comodidad se suele utilizar el nombre Makefile o makefile.

Pruebe a compilar el programa a partir de los módulos. Empiece borrando el ejecutable y los módulos objeto para ver el proceso completo:

$ rm -f mcd modulo1.o modulo2.o
$ ls
Makefile  modulo1.c  modulo2.c

Ahora invoque a make. A continuación muestro lo que obtuve en mi computador [32] :

$ make
gcc -c modulo1.c
gcc -c modulo2.c
gcc -o mcd modulo1.o modulo2.o

Si volvemos a invocar a make Ud. apreciará que éste se niega a hacer algo debido a que todo está ahora actualizado.

Supogamos ahora que modificamos a modulo1.c. Podríamos hacer un cambio editando el archivo, pero para engañar a make simplemente basta con que actualicemos su fecha de modificación (a fin de que sea más reciente que los archivos que dependen de éste.) El comando touch puede actualizar esa fecha:

$ make
make: `mcd' está actualizado.
$ touch modulo1.c
$ make
gcc -c modulo1.c
gcc -o mcd modulo1.o modulo2.o

Observe con atención que make sólo ha compilado lo necesario (no ha recompilado modulo2.c.) Cómo logra hacerlo es lo que veremos a continuación.

Procesamiento de dependencias

¿En qué casos se considera que un archivo "target" está "desactualizado" con respecto a sus dependencias? Antes de responder a esto, make hace un análisis de cada una de las "dependencias" y verifica si éstas son "target’s" en otras reglas. Todas estas reglas deben ser resueltas antes de proseguir. En nuestro caso, las dependencias modulo1.o y modulo2.o son los "target" de la segunda y tercera regla respectivamente. Analicemos la segunda regla (la tercera es análoga.)

La segunda regla (con target modulo1.o) presenta como dependencia única a modulo1.c. Como ya no hay ninguna otra regla que tenga a modulo1.c como target, entonces Make analiza las fechas de modificación del target modulo1.o y su dependencia modulo1.c. Si modulo1.c es más reciente que modulo1.o, entonces significa que el programador ha hecho cambios en el código fuente; por lo tanto, make ejecuta el comando indicado en la regla, el cual regenera a modulo1.o. Esto trae como consecuencia que éste último ahora sea más reciente que modulo1.c.

Tras analizar a reglas segunda y tercera, make retorna a analizar la primera. Ahora verificará las fechas de modificación para conocer si las dependencias (modulo1.o y modulo2.o) son más recientes que el target mcd. De ser así se ejecutará los comandos asociados a dicha regla; es decir, invocará a gcc para volver a hacer el enlace de los archivos objeto generando un nuevo ejecutable.

Es importante advertir que en el momento en que make regresa a la primera regla, las fechas de modificación de los archivos involucrados pueden haber cambiado (debido a los comandos de las otras reglas.) De este modo, un simple cambio en un archivo de código fuente se propaga adecuadamente al archivo objeto correspondiente y da lugar a un nuevo ejecutable.

También debemos anotar que la primera regla normalmente es la que inicia la cadena recurrente de dependencias. Si Ud. hubiera empezado el archivo con la siguiente regla:

modulo1.o: modulo1.c
	gcc -c modulo1.c
...

Entonces make sólo se preocuparía de actualizar el archivo objeto "modulo1.o" cuando sea necesario (cuando cambie "modulo1.c") pero no hará absolutamente nada más debido a que "modulo1.c" no es target en ninguna otra regla.

A riesgo de sonar repetitivo…​ recuerde una vez más que los comandos asociados a las reglas deben iniciarse con un tabulador como primer caracter de la línea.

Reglas implícitas

El Makefile que presentamos arriba se puede abreviar considerablemente. Por ejemplo, la siguiente versión será totalmente equivalente y más poderosa:

# Segunda version de Makefile

mcd: modulo1.o modulo2.o
	gcc -o mcd modulo1.o modulo2.o  # usar tabulador

clean:
	rm -f *.o mcd  # usar tabulador

Empecemos por el final. Hemos añadido una regla con target "clean", la cual no participa normalmente del proceso de compilación (clean no aparece como dependencia de nada.) Simplemente nos sirve para "limpiar" el directorio del programa de todos los archivos generados (objetos y el ejecutable) a fin de forzar una compilación total.

Como mencioné, esto no ocurrirá a no ser que lo solicitemos explícitamente con el siguiente comando:

$ make clean

Si bien no es obligatorio tener una regla con target "clean", muchos proyectos la tienen, y mucha gente espera encontrarla.

Dejando de lado el target "clean", y volviendo a lo anterior, ¿qué ocurrió con las otras dos reglas que estaban en la versión anterior del Makefile? La respuesta es que son redundantes.

Make normalmente viene preconfigurado con lo que se conoce como "reglas implícitas", que simplemente son acciones a tomarse con ciertas relaciones de dependencia cuando no se proporciona una acción explícitamente.

Como vemos, en la primera regla la acción para construir el ejecutable está completamente determinada de manera explícita. Como sabemos, make además buscará otras reglas donde modulo[12].o sean "targets". Al no haber tales reglas, entonces intentará buscar una regla implícita que sea adecuada.

Una regla implícita "adecuada" (que no necesitamos añadir al Makefile pues ya está pre-configurada) tendría aproximadamente el siguiente aspecto:

%.o:%.c
	gcc -c $<

Esta regla es aplicada por make a los archivos modulo[12].c para generar los target’s modulo[12].o [33] . De este modo, esta nueva versión del Makefile es completamente equivalente a la anterior.

Recuerde que ésto último no requiere añadirse al Makefile; sólo se presenta para que la explicación sea más completa.

Variables

Continuando con nuestra reducción del Makefile, presentamos una tercera versión:

# Tercera version de Makefile

OBJETOS= modulo1.o modulo2.o

mcd: $(OBJETOS)
	gcc -o $@ $(OBJETOS)

clean:
	rm -f *.o mcd

Como se aprecia, las variables se pueden definir con la notación NOMBRE=VALOR. Éstas se expanden con la notación $(NOMBRE) y pueden proporcionan un gran poder de abstracción en Makefiles complicados.

Algunas variables tienen un significado particular dependiendo de su contexto. Por ejemplo, la variable "@" (que se expande con "$@") significa el "target" de una regla y su valor (en nuestro caso, reemplaza a “mcd”.) A estas variables se les denomina a veces "automáticas".

Table 3. Algunas variables automáticas en GNU Make
Variable Significado

@

El target de la regla

<

El nombre de la primera dependencia

?

El nombre de todas las dependencias más nuevas que el target

^

El nombre de todas las dependencias sin repeticiones

+

El nombre de todas las dependencias, posiblemente repetidas

Variables en reglas implícitas

Hace un momento presenté una probable regla implícita para construir un archivo objeto (*.o) a partir de un archivo fuente C (*.c). A continuación presento una versión más acorde con la realidad:

%.o:%.c
	$(CC) -c $(CFLAGS) $<

Esto quiere decir que es posible usar un compilador distinto a gcc con facilidad (al menos desde el punto de vista de make) simplemente definiendo la variable CC al inicio del Makefile. Asimismo, la variable CFLAGS permite introducir algunas opciones al proceso de compilación (ver más adelante.)

Table 4. Algunas variables de GNU Make que invocan programas con opciones
Nombre del programa Opciones Significado Valor por omisión

AR

ARFLAGS

Programa de "archivamiento" (capítulo 3)

ar

AS

ASFLAGS

Programa ensamblador

as

CC

CFLAGS

Compilador de C

cc

CXX

CXXFLAGS

Compilador de C++

g++

FC

FFLAGS

Compilador fortran

f77

LEX

LFLAGS

Analizador lexicográfico (capítulo 20)

lex

PC

PFLAGS

Compilador de pascal

pc

YACC

YFLAGS

Analizador sintáctico (capítulo 20)

yacc

RM

Programa que elimina archivos

rm -f

Comentarios finales sobre Make

Make es un programa tremendamente sofisticado. Aquí sólo hemos presentado los aspectos más elementales, aunque ya estamos en capacidad de compilar proyectos pequeños. En Linux la versión de Make distribuida es GNU Make, la cual presenta numerosas extensiones y algunas incopatibilidades con respecto a las versiones "oficiales" de los Unix’es. Para más información sobre GNU Make, consúltese la documentación "info" (info make.)

3. Crear y Utilizar Librerías

Las librerías son rutinas (en general, cualquier código) usado para proporcionar funcionalidad a los programas. Por lo general, las librerías contienen una colección de funciones que llevan a cabo un conjunto de tareas orientadas a cierto propósito. Por ejemplo, la librería "gdbm" proporciona un conjunto de rutinas para interactuar con bases de archivos planos (capítulo 10), la librería "zlib" proporciona un conjunto de rutinas para comprimir y descomprimir cualquier conjunto de datos, la librería SDL permite programar aplicaciones multimedia/juegos (capítulo 12), etc.

En este capítulo mostraremos cómo crear y utilizar librerías, lo cual requiere que el lector conozca el material del capítulo 2.

3.1. Un ejemplo: mejoras para el procesamiento de strings

A fin de ilustrar el proceso completo de creación de una librería, utilizaremos (inventaremos) un ejemplo práctico (stringplus.) En realidad, no es muy relevante qué es lo que hace esta librería, sino la forma como está siendo construida.

3.1.1. Diseño

Aunque la librería estándar ya proporciona un conjunto de funciones para procesamiento de cadenas de caracteres (strcpy, strcat, strchr, etc.) es recurrente la necesidad de diversas funciones adicionales tales como:

Función Descripción

replace

Reemplazar las ocurrencias de un texto por otro dentro de un string

trim

Eliminar espacios en blanco al inicio y al final de un string

substr

Proporcionar una sección de una cadena en un nuevo string

A esta pequeña librería le denominaremos stringplus.

3.1.2. Implementación

A fin de hacer la explicación más interesante y general, implementaremos cada función en un archivo aparte:

Reemplazo de textos

Dada una cadena de texto, buscar todas las ocurrencias de cierto patrón (otra cadena) y reemplazarlo por otra cadena de texto. La cadena original no se altera, y los reemplazos dan lugar a una nueva, asignada dinámicamente (se debe eliminar posteriormente con free(3).) Nótese que esta implementación asume que los strings a crearse no pasan de 1024 bytes. Esto es un fallo que Ud. debería corregir a modo de ejercicio:

/*
 * replace.c: remplazar en cadena todas las ocurrencias
 * de "tal" por "cual", retornando un nuevo string
 */
#include "stringplus.h"

char *replace(const char *cadena,const char *tal,const char *cual)
{
if(cadena==NULL || tal==NULL || cual==NULL || strlen(tal)==0)
	return NULL;

/* Implementacion poco inteligente: reservamos 1K 
 * y no verificamos si no alcanza */
char *res=(char *)calloc(1024,sizeof(char));
const char *p1=cadena;
const char *p=cadena;

for(;;)
	{
	p=strstr(p,tal);
	while(*p1 && p1!=p)
		{
		int j=strlen(res);
		res[j]=*p1;
		res[j+1]=0;
		p1++;
		}
	if(p1==p)
		{
		strcat(res,cual);
		p1+=strlen(tal);
		p+=strlen(tal);
		}
	if(!*p1)
		break;
	}
return res;
}

3.1.3. Sub-Cadenas de Texto

La función substring recibe como argumento una cadena (que no se altera), un entero no negativo que indica la posición a partir de la cual se extraerá una nueva cadena, y otro entero (no negativo) que indica cuántos caracteres se extraerán como máximo. Si los enteros proporcionados son negativos, se retorna NULL. Si los enteros intentan obtener caracteres más allá de los que contiene la cadena, se retorna un string vacío.

/*
 * substr.c: Retornar un nuevo string a partir
 * de una seccion de un string inicial
 */
#include "stringplus.h"

char *substr(const char *cadena,int n,int m)
{
if(cadena==NULL || n<0 || m<0)
	return NULL;
if(n>=strlen(cadena))
	return strdup("");
if(n+m <= strlen(cadena))
	{
	char *p=(char*)malloc(m+1);
	p[m]=0;
	strncpy(p,cadena+n,m);
	return p;
	}
return strdup(cadena+n);
}

3.1.4. Recorte de blanks

Es frecuente la necesidad de eliminar espacios en blanco (blanks) en cadenas de texto. En general, consideramos "blanks" a los espacios, tabuladores y saltos de línea. La función trim retorna un nuevo string recortado a partir de una cadena pasada como argumento (que no se modifica.)

/*
 * trim.c: eliminar espacios en blanco
 * al inicio y al final de un string
 */
#include "stringplus.h"

char *trim(const char *cadena)
{
if(cadena==NULL)
	return NULL;
if(*cadena == 0)
	return strdup("");
char *p=strdup(cadena);
char *ptr=p;
while(*p)
	{
	if(*p !=' ' && *p!='\t' && *p!='\n')
		break;
	p++;
	}
char *t=p+strlen(p)-1;
while(*t)
	{
	if(t==p)
		break;
	if(*t !=' ' && *t!='\t' && *t!='\n')
		break;
	t--;
	}
*(t+1)=0;
char *tmp=strdup(p);
free(ptr);
return tmp;
}

3.1.5. Header de la librería

Es típico que las librerías tengan uno o más headers. Para nuestro caso bastará con uno:

/*
 * stringplus.h: Header de libreria de strings
 */
#ifndef STRINGPLUS_H
#define STRINGPLUS_H
#include <stdio.h>
#include <string.h>

char *replace(const char *cadena,const char *tal,const char *cual);
char *substr(const char *cadena,int n,int m);
char *trim(const char *cadena);

#endif

3.2. Prueba

Ahora crearemos un pequeño programa para probar todo el código anterior (prueba.c):

#include "stringplus.h"

int main(int argc,char **argv)
{
char *to_replace="Erre con erre guitarra";
char *from_replace=replace(to_replace,"rr","abc");
if(from_replace==NULL)
	fprintf(stderr,"Error en replace\n"),exit(1);
printf("to_replace:[%s]\n",to_replace);
printf("from_replace(rr,abc):[%s]\n\n",from_replace);
free(from_replace);

char *to_substr="La tierra es guera";
char *from_substr=substr(to_substr,3,6);
if(from_substr==NULL)
	fprintf(stderr,"Error en substr\n"),exit(1);
printf("to_substr:[%s]\n",to_substr);
printf("from_substr(3,6):[%s]\n\n",from_substr);
free(from_substr);


char *to_trim="   There goes the Challenger    ";
char *from_trim=trim(to_trim);
if(from_trim==NULL)
	fprintf(stderr,"Error en trim\n"),exit(1);
printf("to_trim:[%s]\n",to_trim);
printf("from_trim:[%s]\n\n",from_trim);
free(from_trim);


return 0;
}

3.2.1. Compilación y ejecución

El siguiente Makefile permite compilar todos nuestros módulos, generando un ejecutable prueba:

OBJECTS = prueba.o replace.o trim.o substr.o

prueba: $(OBJECTS)
	gcc -o $@ $(OBJECTS)

clean:
	rm -f prueba *.o

La salida es la siguiente:

$ ./prueba
to_replace:[Erre con erre guitarra]
from_replace(rr,abc):[Eabce con eabce guitaabca]

to_substr:[La tierra es guera]
from_substr(3,6):[tierra]

to_trim:[   There goes the Challenger    ]
from_trim:[There goes the Challenger]

3.2.2. Recapitulación

Revisemos en lo que hemos conseguido hasta este momento. Gracias al código de los archivos:

  • replace.c

  • substr.c

  • trim.c

  • stringplus.h

El programa prueba.c ha podido ejecutarse haciendo uso de nuestras nuevas funciones extendidas de strings, lo cual era el objetivo de todo esto. Eso significa que los cuatro archivos listados arriba constituyen para todo propósito lo que se conoce conceptualmente por "librería".

De otro lado, otro programador que hace uso de nuestra librería no necesita estrictamente tener acceso al código fuente de la misma. Por tal motivo nuestra librería podría constar sólamente de los archivos compilados:

  • replace.o

  • substr.o

  • trim.o

  • stringplus.h

Nótese que el archivo cabecera stringplus.h no tiene equivalente compilado por lo que se debe proporcionar. El distribuír sólo código compilado es una práctica comercial común, pero que lamentablemente limita las posibilidades de introducir mejoras y correcciones [34] .

Ahora bien, desde el punto de vista del sistema operativo Unix/Linux todavía no disponemos de una "librería" como tal, puesto que éstas se suelen distribuír en un solo archivo de formato especial (y no como un conjunto de archivos objeto *.o) como veremos a continuación.

3.3. Construcción de archivos de librería

3.3.1. Librerías estáticas

Las librerías estáticas consisten simplemente en empaquetar los archivos de código objeto *.o en un solo gran archivo (con extensión *.a) usando el clásico comando ar [35] . Este archivo se puede enlazar con el compilador como si fuese cualquier otro archivo objeto.

Dados los archivos del ejemplo anterior, el siguiente Makefile genera el archivo de librería estática "stringplus.a", y lo enlaza con el código de prueba (prueba.c):

# Crear un archivo de libreria estatica
# 
OBJECTS = replace.o trim.o substr.o

prueba: prueba.c stringplus.a
	gcc -o $@ prueba.c stringplus.a

stringplus.a: $(OBJECTS)
	ar rcs $@ $(OBJECTS)

clean:
	rm -f prueba *.o stringplus.a

La ejecución de make se muestra a continuación [36] :

$ make
cc    -c -o replace.o replace.c
cc    -c -o trim.o trim.c
cc    -c -o substr.o substr.c
ar rcs stringplus.a replace.o trim.o substr.o
gcc -o prueba prueba.c stringplus.a

Con esto, la distribución de la librería se limita a los archivos:

  • stringplus.h

  • stringplus.a

Así como una buena explicación de cómo se usa.

A esta clase de librería se le conoce como "librería estática". Los ejecutables enlazados con librerías estáticas pueden ser llevados a otros computadores sin necesidad de llevar también la librería, pues ésta ya está contenida en aquellos.

3.3.2. Librerías compartidas (shared libraries)

El esquema de enlace con librerías estáticas funciona bien, pero tiene algunas características que pueden ser muy indeseables:

  1. Si dos o más programas se enlazan a la librería estática, cada ejecutable tendrá añadido el mismo código repetido (desperdicio de espacio de disco)

  2. Como consecuencia de lo anterior, cuando estos ejecutables se cargan en memoria, ésta se llena ineficientemente con código repetido

  3. Si descubrimos un problema en nuestra librería y logramos corregirlo, entonces debemos volver a enlazar todos los programas que utilizaban la versión defectuosa. En la práctica, esto significa generar y reinstalar todos los ejecutables.

Las librerías compartidas (conocidas como shared libraries o dynamic libraries) resuelven todos estos problemas de un modo bastante completo, a costa de cierta complejidad añadida.

Lamentablemente el procedimiento necesario para hacer esto no es totalmente estandarizado entre Unix’es, y lo que haremos aquí (Linux/gcc) puede requerir adaptaciones para otros ambientes.

El siguiente Makefile permite obtener la librería "libstringplus.so" y enlazarla con "prueba.c":

# Crear un archivo de libreria dinamica
# 
OBJECTS = replace.o trim.o substr.o
CFLAGS = -fpic

prueba: prueba.c libstringplus.so
	gcc -o $@ prueba.c libstringplus.so
#	gcc -L. -o $@ prueba.c -lstringplus

libstringplus.so: $(OBJECTS)
	gcc -shared -o $@ $(OBJECTS)

clean:
	rm -f prueba *.o libstringplus.so

Convencionalmente, las librerías dinámicas tienen un nombre de archivo de la forma libXXX.so.v.m.r [37] , donde la extensión "*.so" indica que se trata de un "shared object"; "v", "m" y "r" son enteros positivos: "v" corresponde a la "versión" de la librería, "m" es el "número menor", y "r" el "release". Diferentes valores para el número "v" significan diversas versiones incompatibles de la librería [38] ; por el contrario, distintos valores de "m" corresponden a versiones compatibles de la misma [39] , y finalmente "r" (que es opcional) complementa a "m" permitiendo indicarle mayor granularidad. En conjunto, estos números permiten la coexistencia de distintas versiones de una librería.

Por simplicidad, en lo que sigue no emplearemos los números "v", "m" y "r", aunque cumplen una función fundamental para el correcto manejo de "dependencias" de las aplicaciones, por lo que se recomienda leer el "Programming Library HOWTO" http://www.dwheeler.com/program-library .

Usando gcc, la opción -shared genera librerías compartidas mientras que la opción -fpic [40] genera código máquina con "direcciones independientes de la posición", lo cual es indicado para todos los módulos constituyentes de las librerías compartidas (mas no es necesario para los programas que las invocan. [41] )

Ahora bien, esta "shared library" por definición NO está contenida en el archivo ejecutable con quien está enlazada. Por ejemplo, nuestro ejecutable (prueba) sólo contiene una referencia a “libstringplus.so” [42] , la que deberá ser cargada a memoria recién en el momento de la ejecución del programa.

Esto significa que si queremos llevar nuestro ejecutable “prueba” a otro computador, también tendremos que llevar por separado “libstringplus.so” [43] .

Para apreciar a qué shared library’s hace referencia nuestro ejecutable prueba, podemos usar el comando ldd:

$ ldd prueba
libstringplus.so => not found
libc.so.6 => /lib/libc.so.6 (0x40029000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
$

Como se aprecia, nuestra librería está referenciada aunque no está siendo encontrada (ver mensaje "not found".) Esto se debe a que las shared library’s tienen que colocarse en ciertos directorios especiales en el sistema previamente a su uso.

Si tratamos de ejecutar el programa, éste fallará:

$ ./prueba
./prueba: error while loading shared libraries: libstringplus.so:
cannot open shared object file: No such file or directory

Una manera temporal de solucionar esto consiste en especificar la variable de entorno “LD_LIBRARY_PATH” con el directorio en el que se encuentra “libstringplus.so”. En nuestro caso, es el directorio actual ("."):

$ LD_LIBRARY_PATH=. ./prueba
to_replace:[Erre con erre guitarra]
from_replace(rr,abc):[Eabce con eabce guitaabca]

to_substr:[La tierra es guera]
from_substr(3,6):[tierra]

to_trim:[   There goes the Challenger    ]
from_trim:[There goes the Challenger]

3.3.3. Enlace en tiempo de ejecución

La solución de largo plazo consiste en "instalar" nuestra librería en el sistema. Para esto podemos hacer lo siguiente:

$ su
Password:
# cp libstringplus.so /usr/lib
# exit
$ ldd prueba
libstringplus.so => /usr/lib/libstringplus.so (0x40029000)
libc.so.6 => /lib/libc.so.6 (0x4002b000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
$ ./prueba
to_replace:[Erre con erre guitarra]
...

Parece que todo se resuelve mágicamente llevando la librería al directorio /usr/lib. Lo que ocurre es que éste directorio (al igual que /lib y otros más) es uno de los lugares preconfigurados donde el "enlazador de tiempo de ejecución" busca las shared libraries. Por ejemplo, en el sistema (Linux) que estoy utilizando, las rutas son [44] :

$ cat /etc/ld.so.conf
/usr/kerberos/lib
/usr/X11R6/lib
/usr/lib/qt-3.0.5/lib
/usr/lib/sane
/usr/lib/mysql
/usr/lib/qt2/lib
/usr/lib/wine

A éstas se les añade automáticamente /lib y /usr/lib. Esta información se puede hallar en el man page de “ld.so”.

Para resumir esta sección, cuando distribuimos (hacia usuarios finales) un programa enlazado dinámicamente con la librería "stringplus" (como por ejemplo, prueba), entonces también debemos proporcionarle la librería libstringplus.so, o al menos indicarle que ésta es requisito para que el programa funcione.

Por otro lado, cuando querramos distribuir nuestra librería a otro desarrollador, proporcionaremos los archivos [45] :

  • stringplus.h

  • libstringplus.so

(y la explicación correspondiente.)

3.3.4. La opción "-l" del compilador

Si se observa con atención el último Makefile, observará que para la compilación de prueba (tras haber generado la shared library) hemos indicado el enlace a la libstringplus.so simplemente especificando el nombre completo de tal archivo:

	gcc -o $@ prueba.c libstringplus.so

Esto funciona bien puesto que la librería está en nuestro directorio actual.

Sin embargo, muchas veces enlazamos nuestros programas con librerías que no hemos construido nosotros y que están instaladas en diversos directorios del sistema. En ese caso, el comando hipotético podría ser algo como:

	gcc -o $@ prueba.c /usr/lib/libstringplus.so

Sin embargo, el directorio donde se ubican las librerías es variable, y sería engorroso tener que buscarlas para cada caso.

A tal efecto, la opción "-l" del compilador [46] permite que especifiquemos tan solo el nombre de la librería (con -lXXX) y automáticamente se buscará en todos los directorios adecuados el archivo “libXXX.so”. Nótese que para que esto funcione, es necesario que el archivo tenga un nombre con este formato (iniciado con "lib" y terminado en ".so".)

Lamentablemente, esto crea un nuevo inconveniente: el enlazador ld no considera al directorio actual (.) como posible lugar de búsqueda. A fin de añadirlo a la lista de directorios, empleamos la opción "-L" del compilador [47] . Finalmente, el nuevo comando para generar el ejecutable (en el Makefile) es:

	gcc -L. -o $@ prueba.c -lstringplus

3.4. Ejecución dinámica con dlopen

La interfaz dlopen proporciona un mecanismo para ejecutar código compilado en demanda, lo que proporciona un grado de flexibilidad extremo. Proporcionaremos un nuevo ejemplo para hacer la respectiva demostración [48] .

3.4.1. Ejemplo: Factoriales Reimplementados

A continuación tres implementaciones que nos permiten obtener el factorial de un número entero (retornando un entero largo.)

3.4.2. Con loop simple

/*
 * Calculo de un factorial con un loop repetido
 */

long factorial(int n)
{
long f=1;

do
	f*=n;
while(--n);
return f;
}

3.4.3. Con recursividad

/*
 * Calculo de un factorial usando recursividad
 */

long factorial(int n)
{
if(n>1) return n*factorial(n-1);
else return 1L;
}

3.4.4. Con fórmula aproximada

/*
 * Calculo de un factorial usando formula
 * de Stirling
 */

#include <math.h>

#define PI 3.14159265358979323846

long factorial(int n)
{
double f;

return exp(-n)*pow(n,n+0.5)*sqrt(2.0*PI)*
	(1.0+1.0/(12.0*n)+1.0/(288.0*n*n));
}

Aunque no era necesario, me parece más didáctico haber creado las tres implementaciones mediante una función que tiene el mismo nombre (factorial.) Como se sabe, sería imposible crear un programa que acceda a estas tres funciones a la vez (¿cómo las podría distinguir?.)

Pero con dlopen (y compañía) sí podemos. El siguiente programa hace uso de éstas implementaciones de factorial dependiendo de lo que el usuario escoge:

#include <stdio.h>
#include <dlfcn.h>
#include <errno.h>

//long factorial(int);

main(int argc,char **argv)
{
int n,m;
void *obj;
long (*fact)(int);

printf("Escriba el numero > ");
scanf("%d",&n);
printf("Que metodo usamos? (1-loop 2-recursividad 3-Stirling) > ");
scanf("%d",&m);

switch(m)
	{
	case 1: obj=dlopen("./fact1.so",RTLD_NOW); break;
	case 2: obj=dlopen("./fact2.so",RTLD_NOW); break;
	case 3: obj=dlopen("./fact3.so",RTLD_NOW); break;
	default:
	fprintf(stderr,"Metodo invalido!\n");exit(1);
	}
if(obj==NULL)
	{ fprintf(stderr,"dlopen: %s\n",dlerror()); exit(1); }

fact=dlsym(obj,"factorial");
if(fact==NULL)
	{ fprintf(stderr,"dlsym: %s\n",dlerror()); exit(1); }

printf("%ld\n",(*fact)(n));
}

Nótese que nuestro programa accede a shared libraries (.so) aunque -al menos en Linux- también puede acceder a archivos objeto (.o). A tal efecto, el siguiente Makefile permite compilar todo:

LIBS = fact1.so fact2.so fact3.so
CFLAGS = -fpic

all: $(LIBS) prueba

%.so:%.o
	gcc -shared -o $@ $<

prueba: prueba.c
	gcc -o prueba prueba.c -ldl

clean:
	rm -f *.o *.so prueba

Obsérvese que "prueba" no se enlaza con ninguna de las implementaciones de factoriales, pero sí debe hacerlo con la librería "libdl.so" (opción -ldl) que proporciona estas capacidades. Note también la inclusión del header "dlfcn.h".

A continuación una muestra de su ejecución:

$ ./prueba
Escriba el numero > 5
Que metodo usamos? (1-loop 2-recursividad 3-Stirling) > 1
120
$ ./prueba
Escriba el numero > 5
Que metodo usamos? (1-loop 2-recursividad 3-Stirling) > 2
120
$ ./prueba
Escriba el numero > 5
Que metodo usamos? (1-loop 2-recursividad 3-Stirling) > 3
dlopen: ./fact3.so: undefined symbol: sqrt

Casi listo, pero en el caso de “fact3” se requería enlazar con la librería matemática (que define la función sqrt().) Esto es exclusivo de fact3, por lo que lo enlazamos en el momento de su creación:

LIBS = fact1.so fact2.so fact3.so
CFLAGS = -fpic

all: $(LIBS) prueba

%.so:%.o
	gcc -shared -o $@ $<

fact3.so: fact3.o
	gcc -shared -o $@ $< -lm

prueba: prueba.c
	gcc -o prueba prueba.c -ldl

clean:
	rm -f *.o *.so prueba

Como se ve, hemos añadido una regla explícita para el caso de fact3.so que incluye a -lm [49]. Esta vez, la ejecución no falla.

3.4.5. Handlers y símbolos

Como se aprecia, nuestro programa central accede a las "shared libraries" mediante la función dlopen(). Ésta retorna un puntero tipo (void *) que sirve para hacer referencia al contenido de la misma (o NULL en caso de error.) Obsérve que requiere especificar la ruta completa (en este caso con “./”) pues en caso contrario buscará la librería sólo en los lugares preconfigurados del sistema (ver discusión sobre esto más arriba en la sección de creación de shared libraries [50]. ) Pruebe a usar "fact1.so" en lugar de "./fact1.so" para que lo compruebe.

Teniendose el "handler" a la shared library, es posible obtener un puntero a cualquier objeto de la misma (una función o una variable.) Para esto se emplea la función dlsym() que recibe como argumento el nombre de este objeto (lo que en las librerías se conoce como un símbolo.) Estos punteros deben ser del tipo adecuado al objeto al que se está referenciando (en nuestro caso, un puntero a función que recibe "int" y retorna "long".)

Cuando el programa ya no requiere acceder a estos objetos, el "handler" obtenido con dlopen() debe cerrarse con dlclose() (nuestro programa de prueba finaliza de inmediato, por lo que no vale la pena la molestia.)

3.5. Problemas Frecuentes

Referencias no resueltas/circulares

A continuación proporcionamos los archivos constituyentes de dos librerías triviales, y un programa que hace uso de éstas. Esto nos permitirá mostrar un error que aparece con relativa frecuencia, generado por referencias no resueltas:

Código fuente de la librería "a.a"
/*
 * a.c
 */

void fa()
{
}
Código fuente de la librería "b.a"
/*
 * b.c: Llama a fa()
 */
extern void fa(void);

void fb()
{
fa();
}
Código fuente de programa de prueba
/*
 * prueba.c
 */
extern void fb(void);

int main(void)
{
fb();
return 0;
}
Compilación y explicación

A continuación el Makefile originalmente diseñado para compilar esto:

all: prueba

prueba: prueba.c a.a b.a
	gcc -o $@ prueba.c a.a b.a
a.a: a.o
	ar rcs $@ a.o
b.a: b.o
	ar rcs $@ b.o
clean:
	rm -f a.o b.o a.a b.a prueba

Y la salida cuando se utiliza:

cc    -c -o a.o a.c
ar rcs a.a a.o
cc    -c -o b.o b.c
ar rcs b.a b.o
gcc -o prueba prueba.c a.a b.a
b.a(b.o)(.text+0x7): In function `fb':
: undefined reference to `fa'
collect2: ld returned 1 exit status
make: *** [prueba] Error 1

Para comprender la falla, debemos entender cómo opera el enlazador sobre los archivos que se le proporcionan. Si vemos los comandos ejecutados, apreciaremos que la última invocación al compilador gcc compila el archivo “prueba.c” (lo que genera el módulo objeto prueba.o) e invoca al enlazador con los siguientes argumentos (en el orden indicado):

  • prueba.o

  • a.a

  • b.a

El enlazador analiza el primer archivo. En tanto prueba.o sólo hace referencia a la función fb(), el enlazador toma nota de que ésta debe estar definida en algún módulo subsiguiente. Es decir, hasta este momento lo único que "hace falta" es la implementación de fb().

Luego se procesa a.a. En tanto esta librería no contiene fb(), el enlazador asume que su contenido no se necesita, y se ignora completamente. Esto parece un descuido, pero es la manera normal en que se comporta el enlazador [51] .

Finalmente se procesa b.a. Dado que contiene a fb(), su código se toma en consideración para el proceso. Sin embargo, fb() hace referencia a una cierta función fa(). El enlazador toma nota de esto, y asume que fa() está implementada en un módulo todavía no analizado. Como b.a es el último módulo, el enlazador no puede resolver la referencia pendiente fa() y falla.

Una manera rápida (y correcta) de resolver esto, consiste simplemente en intercambiar el orden en que se proporcionan los archivos al enlazador:

gcc -o $@ prueba.c b.a a.a

Sin embargo, existen casos más sofisticados (con más archivos participantes) en los cuales no es suficiente con jugar con el orden de los mismos. En tales casos se puede hablar de referencias circulares. Para resolver esto, a veces basta con repetir los archivos a enlazar (por ejemplo, gcc -o $@ prueba.c a.a b.a a.a) o cambiar el comportamiento del enlazador para que "regrese" a buscar referencias no resueltas en los módulos especificados con anterioridad. Para obtener este comportamiento, se puede utilizar la siguiente (extraña) sintaxis [52] :

gcc -o $@ prueba.c -Wl,--start-group -Wl,a.a -Wl,b.a -Wl,--end-group

La opción -Wl, de gcc, permite pasar opciones directamente al enlazador ld. En el enlazador, las opciones --start-group y --end-group permiten encerrar un conjunto de librerías sobre las cuales ld tendrá el comportamiento señalado arriba.

3.5.1. Forzar exportación de símbolos (dlopen)

En el ejemplo que mostramos a continuación el programa principal (prueba.c) invoca mediante dlopen() a la librería “checksum” que contiene la función del mismo nombre. Esta función requiere tener acceso al string llamado “compartido”, el cual está definido en el programa prueba.c:

Archivos de ejemplo:
/*
 * prueba.c: Invocar a checksum()
 */
#include <dlfcn.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>

char *compartido="Habia una vez, un andinista";

int main(int argc,char **argv)
{
void *obj;
void (*cksum)(void);

obj=dlopen("./checksum.so",RTLD_NOW);
if(obj==NULL)
	{ fprintf(stderr,"dlopen: %s\n",dlerror()); exit(1); }

cksum=dlsym(obj,"checksum");
if(cksum==NULL)
	{ fprintf(stderr,"dlsym: %s\n",dlerror()); exit(1); }

(*cksum)();
return 0;
}
/*
 * checksum.c: Hallar checksum a string "compartido"
 */
#include <stdio.h>
#include <string.h>

extern char *compartido;

void checksum(void)
{
int z;
unsigned short int s=0;
for(z=0;z<strlen(compartido);z++)
	s+=(unsigned char)compartido[z];
printf("Checksum=%hd\n",s);
}
# 
# Makefile para mostrar fallo de export-dynamic
# 
LIBS = checksum.so
CFLAGS = -fpic -Wall

all: $(LIBS) prueba

%.so:%.o
	gcc -shared -o $@ $<

prueba: prueba.c
	gcc -Wall -o prueba prueba.c -ldl

clean:
	rm -f *.o *.so prueba
Ejecución y explicación
$ ./prueba
dlopen: ./checksum.so: undefined symbol: compartido

El programa falla cuando se intenta cargar la librería compartida debido a que no se ha podido resolver el símbolo "compartido". Los símbolos accesibles dinámicamente tienen que estar registrados en la llamada "tabla de símbolos dinámicos", la cual normalmente sólo incluye aquellos que son referenciados por otro módulo en el momento del enlace.

En el caso de prueba.c, el enlazador no tiene forma de saber que “compartido” está referenciado por checksum.c (pues se enlazan separadamente) y por tanto no se incluye en la tabla.

La forma más simple de evitar este problema es forzar la exportación de todos los símbolos hacia la tabla mencionada, para lo cual bastará con añadir la opción --export-dynamic del enlazador; por tanto, desde el compilador, usaremos -Wl,--export-dynamic [53] :

$ gcc -Wall -Wl,--export-dynamic -o prueba prueba.c -ldl
$ ./prueba
Checksum=2488

El caso de “fact3.so” es interesante y nos permite ilustrar el segundo argumento de dlopen (actualmente RTLD_NOW.) “fact3.so” es un caso especial debido a que hace referencia a "símbolos externos", es decir, rutinas o funciones que están contenidas fuera de ella. Por ejemplo, se invoca a las rutinas “exp()” y “sqrt()” que están implementadas en la librería matemática estándar (libm.so [54] .)

Como vimos, “fact3.so” tiene a libm.so como una dependencia:

	fact3.so -> libm.so

Compilemos nuevamente a “prueba” pero sin la librería matemática libm.so:

$ gcc -o prueba prueba.c -ldl
$ ./prueba
Escriba el numero > 5
Que metodo usamos? (1-loop 2-recursividad 3-Stirling) > 1
120
$ ./prueba
Escriba el numero > 3
Que metodo usamos? (1-loop 2-recursividad 3-Stirling) > 3
dlopen: ./fact3.so: undefined symbol: sqrt

Como se aprecia, en el tercer caso la ejecución de dlopen falla debido a que no puede resolverse (al menos) el símbolo “sqrt()” que es usado por la fórmula de Stirling. Nótese que todavía no se ha invocado a dlsym() y menos a la función factorial().

Ahora cambiaremos el modo de trabajo de dlopen. Modifique esta línea:

case 3: obj=dlopen("./fact3.so",RTLD_NOW); break;

Por esta:

case 3: obj=dlopen("./fact3.so",RTLD_LAZY); break;

Volvemos a compilar sin libm.so y probamos:

$ gcc -o prueba prueba.c -ldl
$ ./prueba
Escriba el numero > 4
Que metodo usamos? (1-loop 2-recursividad 3-Stirling) > 3
./prueba: relocation error: ./fact3.so: undefined symbol: exp

Como se aprecia, ni dlopen ni dlsym fallaron, y el programa cayó (aparatosamente) durante su intento de invocar a la rutina “exp()”. En otras palabras, la resolución de los símbolos se ha postergado hasta el momento de su uso.

Recapitulando, el segundo argumento para dlopen() puede ser RTLD_NOW o RTLD_LAZY. En el primer caso, todos los símbolos externos referenciados por la shared library deben ser resueltos tan pronto se carga la misma (de lo contrario la carga falla); en el segundo estos son resueltos conforme se utilizan.

¿Qué método debemos usar? El primer método parece ser el más seguro pues el programador podría tomar una acción correctiva cuando dlopen() falla desde el inicio. Sin embargo, el segundo método también tiene sus ventajas. Imaginemos por un momento una shared library que tiene la capacidad de interactuar con tres tipos distintos de bases de datos: Informixe, Oracler y Sybases. Esta librería implementa las siguientes funciones, las cuales tienen sus correspondientes dependencias:

  1. rutina1_informixe() → libinformixe.so

  2. rutina2_oracler() → liboracler.so

  3. rutina2_sybases() → libsybases.so

Lo más probable es que el usuario sólo tenga instalada una sola de las bases de datos, por lo que sólo dispone en su sistema de, por ejemplo, libsybases.so.

Si un programa utilizara dicha librería con el método RTLD_NOW, entonces forzaría al usuario a tener instaladas las 3 shared libraries que actúan como dependencias. Esto es absurdo y casi imposible en la práctica.

3.6. Ejercicios

1 La librería de textos

Añada a nuestra librería de textos algunos las siguientes mejoras: .. En replace() elimine el tamaño prefijado de 1K para el string resultado .. En substr(), permita argumentos numéricos negativos tal como ocurre en \ diversos lenguajes como PHP, Pyhton, Java .. Añada una función split() que permita "partir" una cadena de texto \ en un array de varias cadenas constituyentes mediante un delimitador \ pasado como argumento

3.6.1. Números complejos

El programa que dibuja un fractal de Mandelbrot del capítulo 12 hace uso de un conjunto de rutinas que implementan operaciones de números complejos. Pruebe a adaptar y completar este código separándolo en una shared library. Adapte el programa de dibujo del fractal para que la emplee.

Invocar a la librería matemática

Mediante dlopen acceda a la librería matemática libm.so [55] . Solicite al usuario el nombre de una función y un valor numérico real. Estas funciones deben corresponder a aquellas definidas en la librería matemática y serán invocadas con el valor introducido. Por ejemplo:

Nombre de funcion? sin
Argumento? 0.1
Resultado: .09983341664682815230
Nombre de funcion? klimb
Error: 'klimb' no se encuentra!
Nombre de funcion?
Símbolos dinámicos del programa

En el ejemplo de cálculo de factoriales con dlopen(), añada una nueva implementación al archivo prueba.c:

long factorial(int x)
{
return exp(lgamma(x+1.0));
}

Asegúrese de incluir el header math.h. Añada una opción adicional (método '4') para invocar a esta implementación. A fin de acceder a ésta, utilice dlopen() pero especificando NULL en el argumento que especifica la librería compartida.

¿Se requiere de --export-dynamic? Posiblemente el método '2' (recursividad) empiece a arrojar valores erróneos. Encuentre la explicación.

INTERFAZ CON EL SISTEMA OPERATIVO

4. Llamadas al Sistema vs Librería Estándar

Las llamadas al sistema se consideran la interfaz de más bajo nivel para acceder al sistema operativo Unix/Linux, sólo por encima de la programación del mismo kernel. Muchas de éstas son accesibles indirectamente mediante la librería estándar. En este capítulo intentaremos explicar la relación entre ambos métodos mediante ejemplos ilustrativos.

4.1. Conceptos

Como se sabe, el sistema operativo Linux (y todos los Unix’es) consisten, entre otros componentes, de un programa principal denominado "núcleo" o "kernel" el cual administra la ejecución de los procesos así como los recursos de hardware. Cuando un proceso en ejecución desea hacer uso de los recursos del computador (por ejemplo, reservar un área de memoria), debe hacer una solicitud al kernel. Éste es quien finalmente proporciona (o deniega) el recurso al proceso.

Estas "solicitudes" se conocen como llamadas al sistema (system calls), las cuales, desde el punto de vista del programador, lucen como una interfaz de programación (API) para interactuar con los recursos de hardware y otros procesos. Se puede considerar una librería, pero cuya implementación subyace en el kernel, lo que la hace muy especial.

Un caso interesante corresponde a la interacción con los archivos de disco. Según veníamos diciendo, el acceso a los recursos del computador (como los discos) se debe hacer mediante solicitudes al kernel (llamadas al sistema.) Si leemos la documentación adecuada, encontraremos que la llamada al sistema que permite escribir información en un archivo se llama write(2). Sin embargo, la mayoría de programadores de C aprenden que una forma típica de hacer lo mismo consiste en invocar a la rutina fprintf de la librería estándar. Surge la pregunta, ¿Por qué hay dos métodos para lograr lo mismo? Es decir, ¿Para qué disponemos de la librería estándar si el kernel ya nos proporciona una llamada al sistema?

Podemos responder de las siguientes maneras: . La librería estándar puede permitirnos una ejecución más eficiente \ mediante la asignación de buffers de lectura/escritura . La librería estándar presenta más flexibilidad al \ programador que las llamadas al sistema. Es decir, es más fácil \ interactuar con las rutinas de I/O de la libería estándar que con \ las correspondientes llamadas al sistema [56]. . En un sistema operativo distinto, las llamadas al sistema pueden \ ser completamente distintas (pues el kernel será distinto), pero \ si sólo usamos la librería estándar (que, aunque suena \ repetitivo, está estandarizada) entonces nuestro programa podrá \ seguir siendo válido en aquel otro sistema operativo.

A estas alturas el lector debe estarse preguntando: Si hay tantas ventajas con la librería estándar, ¿para qué complicarnos con las llamadas al sistema?

Ocurre que la librería estándar, en tanto tiene que ser "implementable" en muchos sistemas operativos, tiene que hacer un compromiso y recoger lo más común, frecuente y/o representativo de éstos. En otras palabras, termina implementando el "mínimo común denominador" de todos ellos, por lo que se abandona la funcionalidad que precisamente distingue a cada sistema operativo. A veces esta funcionalidad distintiva es justamente lo que necesitamos.

En otras palabras, las llamadas al sistema permiten potencialmente hacer cosas que con la librería estándar no se puede.

El lector podría pensar que usar las llamadas al sistema significa destruir la portabilidad. Esto no es necesariamente cierto; más bien, se llega a un compromiso distinto: si bien la "librería estándar" es casi universalmente portable, las "llamadas al sistema" estandarizadas son muy portables entre diversas variantes de Linux y Unix [57] . Sin embargo, subyace el hecho de que para muchos programas las llamadas al sistema no son imprescindibles; en tales casos NO se deben utilizar a fin de no perder ni un ápice de portabilidad footnote:[ Las "llamadas al sistema" mencionadas (como read(), getuid(), etc) al menos en Linux no son realmente "llamadas al sistema" sino subrutinas pertenecientes al gran conjunto de librerías "GNU libc". El kernel proporciona una interfaz única (implementada con una interrupción) que recibe un código numérico que identifica la funcionalidad que se desea (por ejemplo, la funcionalidad de read(), getuid(), etc.) y los parámetros necesarios.

En algunos sistemas (por ejemplo RedHat 8.0, Debian 3.1), estos "códigos de llamada al sistema" se pueden ver en el archivo /usr/include/asm/unistd.h. Tenga en cuenta que esto no es portable. También vea el manual de syscall(2) y syscalls(2) si tiene curiosidad. ] .

4.1.1. Manejo de errores

Convencionalmente, la mayoría de llamadas al sistema retornan "-1" en caso de error; sin embargo, esto no nos indica nada acerca de la naturaleza del error. Para obtener dicha información se puede consultar la variable global errno (asumiendo que se incluye el header <errno.h>, la cual corresponde a un número entero positivo que tiene significados pre-establecidos de diversos errores. Dicha variable es empleada como mecanismo para detallar errores tanto de por las llamadas al sistema como por la librería estándar.

La función perror(3) es parte de la librería estándar, y tiene como objetivo imprimir una descripción acerca del valor actual de errno. Otra forma de obtener esta información (sin necesidad de programar) consiste en navegar dentro de algunos archivos header tales como /usr/include/errno.h o /usr/include/asm/errno.h [58] .

4.1.2. Extensiones a la librería estándar

La librería estándar de lenguaje C (especificada en los estándares ANSI/ISO) está diseñada para ser independiente del sistema operativo. Sin embargo, existen muchos casos en los que es conveniente disponer de rutinas que, sin ser llamadas al sistema, desempeñen tareas que tienen relación directa con el sistema operativo en uso.

Con este fin, diversas versiones de Unix han estandarizado un conjunto de extensiones a la "librería estándar". Estas extensiones comprenden, por ejemplo, rutinas para leer archivos de configuración, navegar en directorios, encriptar passwords, etc. Dependiendo del sistema operativo en uso, algunas de estas rutinas se documentan como llamadas al sistema y viceversa.

4.2. Ejemplos Ilustrativos

En lo que sigue de este capítulo presentaremos algunos ejemplos en los que un mismo programa se implementa tanto con llamadas al sistema como mediante la librería estándar. En los capítulos subsiguientes se describen programas que dependen exclusivamente de llamadas al sistema.

4.2.1. Asignación Dinámica de Memoria

Los sistemas Unix proporcionan la llamada al sistema sbrk(2) [59] , encargada de extender o recortar el área de datos disponible para que un proceso manipule información; en otras palabras, permite reservar más memoria para el proceso, o liberarla. A continuación se presenta un ejemplo sencillo (probado en Linux) en donde el segmento de datos se expande en 20,000 bytes, los cuales son modificados y luego leídos:

/*
 * sbrk_block: reserva de bloque con sbrk
 */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#define BS 20000
int main()
{
char *ptr;
ptr=sbrk(BS);
if(ptr==(char*)-1) exit(1);
int z;
for(z=0;z<BS;z++)
	ptr[z]=z%128;
long t=0;
for(z=0;z<BS;z++)
	t+=ptr[z];
printf("Total=%ld\n",t);
return 0;
}

Su equivalente mediante la librería estándar es muy similar:

/*
 * malloc_block: Reserva de bloque con malloc
 */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#define BS 20000
int main()
{
char *ptr;
ptr=malloc(BS);
if(ptr==NULL) exit(1);
int z;
for(z=0;z<BS;z++)
	ptr[z]=z%128;
long t=0;
for(z=0;z<BS;z++)
	t+=ptr[z];
printf("Total=%ld\n",t);
return 0;
}

Hasta aquí todo parece indicar que ambos métodos son similares y se podrían emplear indistintamente, pero esto NO es cierto. El lector NUNCA debería emplear sbrk en sus programas (si dispone de malloc) debido a:

  1. sbrk (y brk) no están especificadas en POSIX y su \ comportamiento es muy variado por lo que son poco portables

  2. La liberación de memoria con sbrk requiere reducir el segmento \ de datos pasándole un valor negativo; esto podría eliminar la \ información que se encuentra "al final" del segmento

  3. La liberación de memoria con sbrk simplemente no es posible \ en algunos sistemas. Evidentemente malloc tampoco podrá llevar a \ cabo la liberación, pero nos evita tener que considerar este caso por \ separado

  4. sbrk en ciertos sistemas no agranda el segmento de datos de \ manera contínua, sino dejando "huecos" que deben administrarse \ cuidadosamente. Malloc nos evita tener que controlar esto

  5. Para múltiples asignaciones de bloques pequeños, es más eficiente hacer \ una única invocación a sbrk, y administrar manualmente las posiciones \ de los pequeños bloques. Malloc se encarga de esto automáticamente

  6. En ciertos sistemas existen alternativas mejores que sbrk (para \ ciertas circunstancias.) Malloc se encarga de elegir la mejor estrategia \ para realizar la asignación de manera óptima

  7. La librería estándar proporciona las rutinas complementarias calloc \ y realloc que resultarían muy tediosas de implementarse a cada momento \ si se usa sbrk.

Como se aprecia, para este caso está muy claro que los programas siempre deberán preferir la librería estándar en lugar de las llamadas al sistema.

4.2.2. Borrar archivos y directorios

Los programas que presentamos a continuación eliminan el archivo o directorio especificado como primer argumento en la línea de comando. En el caso de las llamadas al sistema, se debe emplear unlink(2) o rmdir(2) respectivamente, mientras que la librería estándar posee la rutina remove(3) para ambos casos.

Con llamadas al sistema:

/*
 * unlink_rmdir: borra archivo o directorio en
 * primer argumento
 */
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int main(int argc,char **argv)
{
struct stat s;
if(argc!=2)
	return 1;
if(stat(argv[1],&s)==-1)
	return 2;
if(S_ISDIR(s.st_mode))
	{
	if(rmdir(argv[1])==-1)
		return 3;
	return 0;
	}
if(unlink(argv[1])==-1)
	return 4;
return 0;
}

Nótese el uso de la llamada al sistema stat(2) que se emplea para obtener información acerca de un archivo. A fin de discernir si estamos ante un archivo regular o un directorio, se puede emplear el macro S_ISDIR() sobre el miembro st_mode de la estructura de tipo struct stat [60] . El manual de stat(2) proporciona la explicación de los miembros de dicha estructura.

Con librería estándar el listado (remove.c) es más corto:

/*
 * remove: borra archivo o directorio en
 * primer argumento
 */
#include <stdio.h>

int main(int argc,char **argv)
{
if(argc!=2)
	return 1;
if(remove(argv[1])==-1)
	return 2;
return 0;
}

En conclusión, para este caso la librería estándar resulta mucho más cómoda, aunque las llamadas al sistema permiten acceder a más información que puede ser útil para casos más sofisticados.

4.2.3. I/O a Disco

El siguiente programa (file_write) escribe un millón de bytes hacia un archivo mediante la llamada al sistema write(2):

/*
 * file_write: Escribir un millon de bytes
 * con write(2)
 */
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
int fd=open("test.dat",O_WRONLY|O_CREAT,0777);
if(fd==-1)
	return 1;
int z;
char x;
for(z=0;z<1000000;z++)
	{
	x=z%127;
	write(fd,&x,1);
	}
close(fd);
return 0;
}

La versión que hace uso de la librería estándar es muy similar:

/*
 * file_fputc: Escribir un millon de bytes
 * con fputc(3)
 */
#include <stdio.h>

int main()
{
FILE *fp=fopen("test.dat","w");
if(fp==0)
	return 1;
int z;

for(z=0;z<1000000;z++)
	fputc(z%127,fp);
fclose(fp);
return 0;
}

Sin embargo, los tiempos logrados por la librería estándar corresponden a la 40-ava parte de la implementación con llamadas al sistema [61] :

$ time ./file_write

real    0m4.409s
user    0m0.130s
sys     0m4.250s
$ time ./file_fputc

real    0m0.107s
user    0m0.080s
sys     0m0.010s

La librería estándar a final de cuentas tiene que invocar a las llamadas al sistema que escriben en disco (como write); sin embargo, aquella emplea un buffer intermedio que es llevado al disco (mediante write) sólo cuando se llena (o cuando se cierra el archivo.) Esto reduce el número de invocaciones a write, y permite ilustrar el hecho de que la sola invocación a una llamada al sistema es -en términos de tiempo- mucho más costosa que la invocación de rutinas de cualquier librería.

En el capítulo 8 se trata las llamadas al sistema para I/O.

4.2.4. Temporizadores

A continuación se presenta un programa que imprime asteriscos a intervalos de un segundo. El retardo se programa mediante la rutina sleep(3) que es parte del estándar POSIX y se encuentra en prácticamente cualquier sistema operativo. sleep tiene una granularidad de 1 segundo, lo que significa que no es posible especificar retardos de una décima o una milésima de segundo.

/*
 * star_sleeep: Imprimir estrellas cada segundo
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
int z,k;

for(;;)
	{
	k=rand()%80;
	for(z=0;z<k;z++)
		putchar(' '); /* Un espacio */
	printf("*\n");
	sleep(1);
	}
return 0;
}

Para mostrar cómo se pueden hacer estos retardos más granulares, el listado 2 presenta el mismo programa pero usando la rutina usleep(3) que es parte del estándar del Unix BSD, y que también se encuentra en Linux. Esta rutina tiene una granularidad de una millonésima de segundo [62] .

/*
 * star_usleep: Imprimir estrellas cada decima
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
int z,k;

for(;;)
	{
	k=rand()%80;
	for(z=0;z<k;z++)
		putchar(' '); /* Un espacio */
	printf("*\n");
	usleep(100000); /* Cien mil */
	}
return 0;
}

Lamentablemente, la función usleep no es parte de los estándares actuales de Unix y se considera obsoleta. Normalmente se implementa en términos de la llamada al sistema nanosleep(2) o select(2).

El mismo programa, pero con la llamada al sistema nanosleep(2) (star_nanosleep.c):

/*
 * star_nanosleep: Imprimir estrellas cada decima
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>

void retardo(void)
{
struct timespec ts;
ts.tv_sec = 0; 
ts.tv_nsec = 100000000;  /* 0.1 segundos */
nanosleep (&ts,NULL);
}

int main()
{
int z,k;

for(;;)
	{
	k=rand()%80;
	for(z=0;z<k;z++)
		putchar(' '); /* Un espacio */
	printf("*\n");
	retardo();
	}
return 0;
}

Finalmente, la versión con select(2) (star_select.c) [63] :

/*
 * star_select: Imprimir estrellas cada decima
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>

void retardo(void)
{
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 100000;  /* 0.1 segundos */
select (0, NULL, NULL, NULL, &tv);
}

int main()
{
int z,k;

for(;;)
	{
	k=rand()%80;
	for(z=0;z<k;z++)
		putchar(' '); /* Un espacio */
	printf("*\n");
	retardo();
	}
return 0;
}

En este caso parece ser que las llamadas al sistema (como nanosleep) llevan la ventaja.

4.2.5. Lista de Grupos

El programa que se presenta a continuación muestra cómo las llamadas al sistema y las rutinas de la librería estándar (o de POSIX) se complementan. Se trata de obtener la lista de los grupos a que pertenece el usuario que lanzó el proceso en ejecución.

Empezamos obteniendo el UID del proceso, lo cual es inmediato gracias a la llamada al sistema getuid(2).

Luego, dado el UID es sencillo buscar la entrada correspondiente en el archivo /etc/passwd y obtener el nombre del usuario así como el GID del grupo principal. Con estos valores se recorre el archivo /etc/group y se puede conocer tanto el nombre que corresponde al GID hallado, así como los grupos suplementarios pues contienen en su lista de "usuarios" los nombres de sus miembros [64] :

/*
 * Encontrar los grupos a los que pertenezco
 */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <grp.h>

int main()
{
/* Encontrar el UID */
uid_t u=getuid();
/* Encontrar entrada en passwd */
struct passwd *p=getpwuid(u);
/* Recorremos /etc/group */

struct group *g;
while((g=getgrent()))
	{
	/* Estamos en el grupo principal? */
	if(g->gr_gid==p->pw_gid)
		printf("G. Prin.: %s\n",g->gr_name);
	/* Estamos en un grupo suplementario? */
	char **ptr=g->gr_mem;
	while(*ptr)
		{
		if(strcmp(*ptr,p->pw_name)==0)
			printf("G. Sup.: %s\n",g->gr_name);
		ptr++;
		}
	}
return 0;
}

Estos recorridos en los archivos señalados se podrían hacer "manualmente" mediante funciones de I/O; sin embargo, POSIX y otros estándares de Unix proporcionan rutinas auxiliares para recorrerlos de un modo más sencillo. De éstas, hemos empleado getpwuid(3) y getgrent(3).

4.2.6. Shutdown Inmediato

El programa del listado 3 permite hacer shutdown inmediato [65] a un sistema Linux. Este programa debe ser ejecutado por el administrador para tener éxito [66] . Nótese que hemos empleado tres llamadas al sistema: getuid, sync y reboot. Consulte ahora mismo el manual de estas llamadas al sistema con el comando man. Posiblemente deba especificar la sección correspondiente (con man 2 …​) Las dos primeras son totalmente portables entre sistemas Linux y Unix, mientras que la tercera está disponible en Linux mas no necesariamente en otros Unix [67] .

La ejecución del programa se inicia averiguando el UID (User ID) de sí mismo (para ver quién lo ha ejecutado.) En caso de ser distinto de cero - lo que significa que el ejecutor NO es root [68] imprime un mensaje y finaliza sin hacer nada (con exit.) En caso contrario, hace un "flush" de los buffers de memoria a disco (a fin de reducir las pérdidas de información) con la llamada al sistema sync y finalmente detiene el computador con reboot.

/*
 * my_shutdown: Bajar el sistema de inmediato
 */
#include <unistd.h>
#include <sys/types.h>
#include <sys/reboot.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
int user;

user=getuid();
if(user!=0)
        {
        fprintf(stderr,"Usted no es el administrador!\n");
        exit(1);
        }

sync();
sleep(3);
reboot(RB_POWER_OFF);
return 1;
}

Con la librería estándar no hay un equivalente directo. Podemos emplear indirectamente al comando /sbin/halt que se emplea normalmente para detener el sistema, pero el comportamiento no es idéntico. Podría decirce que la librería estándar ya llegó a su límite.

/*
 * via_halt: Lanzar el comando halt
 */
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
int user;

user=getuid();
if(user!=0)
        {
        fprintf(stderr,"Usted no es el administrador!\n");
        exit(1);
        }

system("/sbin/halt");
return 1;
}

4.3. Ejercicios

1 El buffer óptimo

A En el programa file_write, la grabación se ha realizado byte a byte, mientras que en file_fputc, tal como se explicó, la librería estándar astútamente usa un buffer a fin de hacer la grabación en bloques más grandes. Se pide modificar file_write para hacer la grabación usando un buffer de tamaño configurable. Además, se deberá probar cuánto toma realizar la misma grabación (un millón de bytes) con distintos tamaños de buffer. En general, si el buffer es más grande, el tiempo se reduce. Sin embargo, esta reducción no es ilimitada, puesto que a partir de cierto tamaño, el agrandamiento del buffer tiene un efecto despreciable. Analice y encuentre un tamaño "óptimo" de buffer.

B ¿Este tamaño óptimo es igual en otros sistemas? ¿en distintos tipos de sistema de archivo?

2 Modifique el programa my_shutdown para que solicite

una confirmación antes de "bajar" el sistema. Si tiene tiempo, busque y analice el código fuente del comando halt de Linux.

3 Investigue cómo se podría adaptar el programa my_shutdown para

que pueda operar en otros sistemas Unix (¿y en sistemas Windows?).

5. Programación con señales

Las señales proporcionan un mecanismo de notificación entre procesos y el sistema operativo cuyo uso es frecuente en el desarrollo de aplicaciones de mediana y alta complejidad. En este capítulo expondremos los mecanismos necesarios para escribir programas usando señales mediante ejemplos ilustrativos.

5.1. Conceptos

Las señales se pueden pensar como "avisos" enviados a los procesos Unix o Linux, los cuales están identificados por ciertas constantes numéricas. Por ejemplo, al interrumpir la ejecución de un proceso con el teclado (frecuentemente con la combinación CTRL-C [69] ), el proceso en cuestión recibe la señal "SIGINT", que tiene por valor 2. Otro caso familiar ocurre al ejecutar el comando kill inmediatamente seguido del Process ID (PID), usado para terminar un proceso. En este caso, el proceso recibe la señal SIGTERM (que tiene valor 15 [70] .) En la tabla que se presenta más abajo se indican algunas señales de uso frecuente.

¿Qué ocurre cuando un proceso recibe una señal? Generalmente se presentan dos casos (aunque hay excepciones):

  1. Si el proceso se ha "preparado" para "capturar" esa señal, entonces el proceso es "notificado". Normalmente esto hace que el proceso ejecute cierta función predeterminada (llamada signal handler), aunque como un caso particular, la acción puede ser simplemente "ignorar la señal".

  2. Si el proceso no se ha "preparado" para capturar esa señal, entonces el proceso recibe cierto tratamiento preestablecido (default action.) En la mayoría de los casos esto corresponde a la finalización inmediata del proceso [71]. Entre las excepciones más destacables se encuentra la señal SIGKILL (número 9) la cual no puede ser "capturada" por ningún proceso, lo cual significa que inexorablemente termina o "mata" al proceso. Es por esto que muchos usuarios emplean el comando kill -9 pid para terminar incondicionalmente un proceso.

Como es de esperarse, un usuario (o sus procesos) no pueden enviar señales a procesos de otro usuario, mientras que el administrador no tiene esta limitación.

Table 5. Algunas señales
Señal Valor Acción Normal Explicación

SIGINT

2

Terminar Proceso

Emitida normalmente al presionar CTRL-C

SIGQUIT

3

Terminar Proceso

Emitida normalmente al presionar CTRL-Backslash

SIGFPE

8

Terminar Proceso

Excepción de operación de punto flotante

SIGKILL

9

Terminar Proceso

Terminación incondicional. No "capturable"

SIGSEGV

11

Terminar Proceso

Intento de acceso inválido a memoria

SIGALRM

14

Terminar Proceso

Usada frecuentemente como temporizador

SIGTERM

15

Terminar Proceso

Usada para terminar procesos

SIGCHLD

17

Ignorada

Un proceso hijo ha terminado

SIGUSR1

22

Terminar Proceso

Para uso del programador

La información de los procesos en ejecución puede ser hallada rápidamente mediante el comando:

ps -ef

o mediante:

ps ax

Esto permite identificar rápidamente el PID de los mismos, especialmente con ayuda de grep:

$ ps ax|grep temporizador
 1315 pts/1    S+     0:00 ./temporizador

Así, para terminar el proceso mostrado arriba usaríamos:

$ kill 1315

Esto en realidad envía la señal SIGTERM. Para enviar otra señal (como por ejemplo, SIGQUIT):

$ kill -SIGQUIT 1315

Recomiendo consultar cualquier libro de Unix o Linux para más detalles sobre las señales y el comando kill. A partir de ahora el enfoque se dirigirá hacia la programación.

5.2. Interfaz de programación

Lamentablemente la interfaz de programación ha tenido diversas modificaciones e incompatibilidades históricas según los "sabores" de Unix. En términos muy generales se reconoce una antigua implementación (años 70 y parte de los 80) de señales "inseguras" [72] , propensas a "race conditions" y otros problemas difíciles de identificar. La llamada al sistema signal(2) se asocia a este período. A partir de las recomendaciones POSIX.1, la programación de señales se hizo más robusta, extendiéndose notablemente con POSIX 1b. Aunque "signal" se mantiene por compatibilidad en prácticamente todos los sistemas Unix (y Linux), se recomienda evitar su uso en aplicaciones complejas que requieren ser portables.

5.3. Método obsoleto: signal

Al margen de ser poco recomendada, signal(2) es la interfaz más sencilla en lo que refiere a programar señales y la emplearé aquí con fines didácticos y porque el lector de seguro se la encontrará en muchas aplicaciones existentes.

A modo de ejemplo, considérese el siguiente programa (temporal.c) que crea un archivo (que asumiremos debe ser sólo temporal), duerme durante algunos segundos, borra el archivo temporal y termina. Esto no tiene nada de especial; sin embargo, supóngase ahora que el usuario decide terminar el proceso prematuramente presionando CONTROL-C o ejecutando el clásico comando “kill PID”; esto casi de seguro ocurrirá durante la "pausa" de sleep(). Así, el proceso será terminado, pero el archivo "temporal" permanecerá.

/* temporal.c */

#include <unistd.h>
#include <stdio.h>

int main()
{
FILE *fp;

fp=fopen("/tmp/temporal","w");
fprintf(fp,"La Hormiga Roja\n");
fclose(fp);

sleep(100);

unlink("/tmp/temporal");
return 0;
}

Como se sabe, no es correcto que el sistema se llene de archivos temporales sin utilidad, por lo que intentaremos corregir el programa mediante la captura de señales.

El listado que se presenta a continuación (tempo_signal.c) presenta una solución trivial usando signal(2). Allí se ha declarado y definido una función llamada “borra_y_sale()” (nuestro signal handler) que sencillamente termina el programa pero asegurándose de eliminar el archivo temporal. Nótese que esta función no debe retornar nada (tipo void) pero sí debe aceptar un parámetro numérico entero.

/* temp_signal.c */

#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

void borra_y_sale(int);

int main()
{
FILE *fp;

signal(SIGINT,borra_y_sale);
signal(SIGTERM,borra_y_sale);

fp=fopen("/tmp/temporal","w");
fprintf(fp,"La Hormiga Roja\n");
fclose(fp);

sleep(100);

unlink("/tmp/temporal");
return 0;
}

void borra_y_sale(int s)
{
unlink("/tmp/temporal");
exit(1);
}

Las señales más frecuentes (pero no las únicas) que se emplean para terminar un proceso, son SIGINT (como dijimos, CTRL+C) y SIGTERM (con kill pid.) Por esto las capturaremos con "signal". La ocurrencia de estas señales ocasionará que se ejecute el código de la función “borra_y_sale”, con lo que conseguiremos el efecto deseado.

En el código correspondiente a borra_y_sale, la variable "s" puede ser utilizada para discernir cuál fue la señal recibida. De este modo podríamos tomar acciones distintas de ser necesario. Otra manera de proceder corresponde a crear distintos signal handlers, de modo tal que cada señal desencadene acciones distintas.

Un caso particular que se presenta con frecuencia corresponde a "ignorar" las señales. Esto se puede hacer en tempo_signal.c simplemente definiendo el signal handler como una función vacía (que no hace nada.) Sin embargo, una forma más simple (con algunas diferencias sutiles) consiste en utilizar el identificador SIG_IGN. En efecto, en lugar de escribir:

signal(SIGINT,funcion_vacia);

Escribiríamos:

signal(SIGINT,SIG_IGN);

Entonces esta señal sería completamente ignorada y no se requeriría del signal handler [73] .

5.4. Método recomendado: sigaction

La llamada al sistema sigaction(2) corresponde al sucesor estandarizado de signal. Tiene mayor flexibilidad y un mejor comportamiento en diversas situaciones (que no expondremos aquí [74] .) Lamentablemente, la flexibilidad suele estar acompañada de una mayor complejidad, y ésta no es la excepción.

Sigaction requiere de tres argumentos, a saber, el número de la señal, y hasta dos punteros a estructuras de tipo sigaction (creadas por el usuario.) A continuación se presenta una nueva versión del programa, donde hemos reemplazado signal por sigaction. Obsérvese que aquí el tercer argumento pasado a sigaction es NULL.

/* tempo_sigaction.c */
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

void borra_y_sale(int);

int main()
{
FILE *fp;
struct sigaction s;

s.sa_handler=borra_y_sale;
sigemptyset(&s.sa_mask);
s.sa_flags=0;
sigaction(SIGINT,&s,NULL);
sigaction(SIGTERM,&s,NULL);

fp=fopen("/tmp/temporal","w");
fprintf(fp,"La Hormiga Roja\n");
fclose(fp);
sleep(100);
unlink("/tmp/temporal");
return 0;
}

void borra_y_sale(int s)
{
unlink("/tmp/temporal");
exit(1);
}

La estructura sigaction tiene (al menos) los siguientes miembros:

  • sa_handler: El signal handler asociado a la señal. Similar al caso de signal.

  • sa_mask: Un "signal set" (conjunto de señales) correspondiente a las señales que serán bloqueadas durante la ejecución del signal handler (se explica más adelante.) Por ahora baste indicar que debemos inicializarlo a "cero" mediante la rutina auxiliar sigemptyset(3) y un puntero al mismo.

  • sa_flags: Permite modificar el comportamiento de las señales en algunos aspectos definidos por el vendedor. Estas modificaciones son interesantes pero lamentablemente no muy portables.

5.4.1. Secciones Críticas

El siguiente listado (reloj.c) simula un cronómetro mediante las variables "h" y "m" (hora y minuto.) Tal como en un cronómetro normal, los valores de "h" están en el rango de 0 a 23 (nunca alcanza 24), mientras que "m" está en el rango de 0 a 59 (nunca alcanza 60.)

Los valores actuales de (h,m) se obtienen lanzando externamente la señal SIGUSR1 al proceso (mediante el comando kill -SIGUSR1 pid.)

/*
 * Simulacion de reloj
 */
#include <signal.h>
#include <stdio.h>

void imprime_hora(int);

int h=0,m=0;

int main()
{
struct sigaction s;

s.sa_handler=imprime_hora;
sigemptyset(&s.sa_mask);
s.sa_flags=0;
sigaction(SIGUSR1,&s,NULL);

for(;;)
	{
	m++;
	if(m==60)
		{
		m=0;
		h++;
		if(h==24)
			h=0;
		}
	}
return 0;
}

void imprime_hora(int s)
{
fprintf(stderr,"Hora: %02d:%02d\n",h,m);
}

Si Ud. analiza el programa paso a paso, notará que en ciertos momentos los valores de las variables "h" y "m" sí alcanzan los valores extremos 24 y 60 respectivamente, los cuales se emplean para resetearlas a cero. Lamentablemente, esto significa que la señal podría llegar en un "mal momento" y generar una valor incorrecto (por ejemplo, 06:60 o 24:00), lo cual efectivamente ocurre si se hace un buen número de pruebas [75] .

El problema radica en que existe una sección crítica en el programa (el incremento de los contadores, su análisis y posible reseteo) durante la cual no es correcto que la señal sea capturada. Lo deseable es que durante esta sección crítica, si se generase una señal, ésta sea mantenida "en suspenso" hasta que se salga de aquella (obviamente, con un valor coherente de "h" y "m".) A esto se le conoce como el "bloqueo" de señales, y se implementa mediante la llamada al sistema sigprocmask(2):

/*
 * Simulacion de reloj con bloqueo de senhal
 */
#include <signal.h>
#include <stdio.h>

void imprime_hora(int);

int h=0,m=0;

int main()
{
struct sigaction s;

s.sa_handler=imprime_hora;
sigemptyset(&s.sa_mask);
s.sa_flags=0;
sigaction(SIGUSR1,&s,NULL);

sigset_t usr1_mask;
sigemptyset(&usr1_mask);
sigaddset(&usr1_mask,SIGUSR1);

for(;;)
	{
	sigprocmask(SIG_BLOCK,&usr1_mask,NULL);
	m++;
	if(m==60)
		{
		m=0;
		h++;
		if(h==24)
			h=0;
		}
	sigprocmask(SIG_UNBLOCK,&usr1_mask,NULL);
	}
return 0;
}

void imprime_hora(int s)
{
fprintf(stderr,"Hora: %02d:%02d\n",h,m);
}

Con respecto al listado anterior, la única diferencia consiste en el uso de sigprocmask al inicio y al final de la "sección crítica", lo cual significa que con cada iteración del loop principal, la señal bloqueada (SIGUSR1) tiene la oportunidad de ser procesada (ejecutar el signal handler.)

Todo proceso tiene una "máscara de bloqueo" que no es otra cosa que la lista de señales bloqueadas. En el ejemplo anterior, hemos creado el "conjunto de señales" usr1_mask (de tipo sigset_t) que contiene únicamente la señal SIGUSR1 (añadida mediante sigaddset().) La primera invocación a sigprocmask bloquea las señales especificadas en el conjunto pasado como argumento (por eso se especifica la constante SIG_BLOCK) mientras que la segunda "desbloquea" las señales del mismo conjunto (constante SIG_UNBLOCK.)

Haga las pruebas necesarias y verifique que esto resuelve los inconvenientes señalados para la versión anterior [76] .

Señal de Alarma

La llamada al sistema alarm(2) permite programar un temporizador que enviará al proceso la señal SIGALRM al cumplirse el tiempo especificado. Esto normalmente significa que se deberá capturar dicha señal a fin de hacer algo útil en ese precíso momento.

El listado que se muestra a continuación es una ligera variación con respecto al anterior. Ahora el programa no sólo genera los valores de "h" y "m" al llegar SIGUSR1 (a voluntad del usuario), sino también en cada segundo de manera automática. Con este fin hemos hecho dos invocaciones a sigaction(), una para cada señal.

La alarma se programa antes de entrar al loop principal con un argumento igual a 1 segundo. Esto significa que al transcurrir este tiempo se generará la señal SIGALRM por primera vez, y se ejecutará el handler imprime_hora(). Esta función analiza la variable "s" (que puede contener SIGALRM o SIGUSR1) y para primer caso reinicializa el temporizador invocando nuevamente a alarm() [77] .

Otra modificación consiste en guardar los valores de "h" y "m" en un archivo (no enviarlos al error estándar como antes.) Supondremos que un requisito adicional del programa radica en que al finalizar, debe dejar el archivo hora.txt con la última "hora" registrada. Si se analiza con cuidado, se observará que aquí puede presentarse un problema si el programa es terminado justo en el momento en que el archivo se ha abierto (pues se borra toda información del mismo), pero antes de terminarse la nueva grabación. Esto es altamente improbable (pues el lapso considerado es muy breve) pero no es imposible. Dicho en otras palabras, tenemos una nueva (pequeña) sección crítica.

Puesto que el programa no tiene forma de terminar por sí mismo, es de esperarse que sea terminado externamente mediante alguna señal (como SIGTERM.) Esto quiere decir que durante nuestra nueva sección crítica, las señales deben ser bloqueadas.

Podría pensarse nuevamente en sigprocmask, pero hay una forma más sencilla. Como se recordará, la estructura sigaction que se pasa a sigaction() contiene un miembro (sa_mask) correspondiente a un "conjunto de señales" que precísamente son bloqueadas durante la ejecución del signal handler. Para este caso hemos "llenado" totalmente (con todas las señales existentes) este conjunto empleando la rutina sigfillset() a diferencia de lo que hacíamos anteriormente (sigemptyset()) que lo mantenía "vacío" (es decir, no bloqueaba señal alguna.)

/*
 * Simulacion de reloj con alarm
 */
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void imprime_hora(int);

int h=0,m=0;

int main()
{
struct sigaction s;

s.sa_handler=imprime_hora;
sigfillset(&s.sa_mask);
s.sa_flags=0;
sigaction(SIGUSR1,&s,NULL);
sigaction(SIGALRM,&s,NULL);

sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask,SIGUSR1);
sigaddset(&mask,SIGALRM);

alarm(1);

for(;;)
	{
	sigprocmask(SIG_BLOCK,&mask,NULL);
	m++;
	if(m==60)
		{
		m=0;
		h++;
		if(h==24)
			h=0;
		}
	sigprocmask(SIG_UNBLOCK,&mask,NULL);
	}
return 0;
}

void imprime_hora(int s)
{
FILE *fp=fopen("hora.txt","w");
if(fp==NULL)
	exit(1);
fprintf(fp,"Hora: %02d:%02d\n",h,m);
fclose(fp);
if(s==SIGALRM)
	alarm(1);
}

No huelga recordar que SIGKILL [78] es una excepción a la regla al no poder capturarse o bloquearse, lo que significa que todos estos programas pueden fallar si se emplea. Esta es la razón por la que su uso se recomienda sólo como último recurso (el famoso kill -9.)

5.5. Envío de señales

La llamada al sistema kill(2) es empleada para enviar señales desde un proceso a otro (o hacia sí mismo.) Como se indicó más arriba, esto sólo funciona entre procesos que tienen el mismo dueño, o cuando se trata del administrador.

Esta llamada al sistema requiere especificar el PID del proceso destinatario así como la señal a enviarse. El programa que se lista a continuación consiste en un loop infinito al inicio del cual, el proceso se envía la señal SIGUSR1 a sí mismo (obtiene su PID mediante la llamada al sistema getpid(2)); esto desencadenará la ejecución alternada de los handlers “dia()” y “noche()”, los cuales reprograman el comportamiento de dicha señal.

Luego de esto, el programa invoca a la función “la_pausa()”, que como indica su nombre, hace una breve pausa. Sin embargo, añadiremos el siguiente requisito: durante este lapso (que asumiremos es una especie de "sección crítica"), la señal SIGUSR1 deberá desactivar su comportamiento normal (no intercambiará día-noche); por el contrario, ésta sólo imprimirá un mensaje alertando que no está "operativa". Esto lo debe verificar el lector lanzando la señal manualmente con el comando kill mientras el proceso está en ejecución.

/* killbill.c */
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void dia(int);
void noche(int);
void la_pausa(void);
void h_pausa(int);

int main()
{
struct sigaction s;
s.sa_handler=dia; sigemptyset(&s.sa_mask); s.sa_flags=0;
sigaction(SIGUSR1,&s,NULL);
for(;;)
	{
	kill(getpid(),SIGUSR1);
	la_pausa();
	}
return 0;
}

void la_pausa(void)
{
struct sigaction s_c,s_estado;
s_c.sa_handler=h_pausa;
sigemptyset(&s_c.sa_mask);
s_c.sa_flags=0;
sigaction(SIGUSR1,&s_c,&s_estado);
/* Se inicia la pausa */
sleep(1);
/* Termina la pausa */
sigaction(SIGUSR1,&s_estado,NULL);
}

void dia(int sig)
{
struct sigaction s;
s.sa_handler=noche; sigemptyset(&s.sa_mask); s.sa_flags=0;
sigaction(SIGUSR1,&s,NULL);
printf("Dia\n");
}

void noche(int sig)
{
struct sigaction s;
s.sa_handler=dia; sigemptyset(&s.sa_mask); s.sa_flags=0;
sigaction(SIGUSR1,&s,NULL);
printf("Noche\n");
}

void h_pausa(int sig)
{
printf("Estamos en la pausa! no interrumpir!\n");
}

La función la_pausa() permite ilustrar el uso del tercer argumento de sigaction; si éste no es NULL, entonces se asume que se trata de una estructura (de tipo struct sigaction) en la que se almacena la configuración actual de la señal. Ésta es utilizada al final de la_pausa() para restablecer aquella a su valor original, sin necesidad de conocer si estamos "de día" o "de noche".

En los capítulos 6 y 7 veremos otras características de sigaction y de kill [79] .

5.6. Ejercicios

1 El siguiente listado presenta una implementación alternativa de la

función sleep(3) que se emplea para que un programa "duerma" un determinado número de segundos. Esta implementación se hace mediante las llamadas al sistema alarm(2) - antes explicada, así como de la llamada al sistema pause(2). Ésta última simplemente detiene la ejecución del programa hasta que arriba alguna señal (en nuestro caso, SIGALRM.) [80]

/*
 * sleep.c: Implementacion de sleep con alarm
 */
#include <unistd.h>
#include <signal.h>

void my_sleep(int tiempo);

int main()
{
	my_sleep(5);
	return 0;
}

static void dumb_handler(int s)
{
return;
}

void my_sleep(int tiempo)
{
struct sigaction s;

/* s.sa_handler=SIG_IGN; */
s.sa_handler=dumb_handler;
sigemptyset(&s.sa_mask);
s.sa_flags=0;
sigaction(SIGALRM,&s,NULL);

alarm(tiempo);
pause();
return;
}

Esta implementación presenta varios problemas cuya resolución se sugieren al lector:

A) Se ha empleado un handler "tonto" denominado "dumb_handler", el cual aparentemente no realiza ninguna función. Se podría pensar que nos podemos ahorrar este handler programando la señal para que sea "ignorada" con la siguiente línea:

s.sa_handler=SIG_IGN;

Sin embargo, esto no funcionará. Pruebe esto y lea el manual de pause(2). Intente comprender este comportamiento.

B) Si antes de invocar a "my_sleep" ya se hubiera programado una alarma, ésta se perdería pues "my_sleep" la "reprograma" . Más correcto sería que "my_sleep" respete la alarma inicial antes programada y retorne al cumplirse ésta (descartando el argumento inicial "tiempo".) Para esto, Ud. deberá averiguar primero si ya hay una alarma programada y cuántos segundos le faltan para cumplirse. Esto se puede hacer invocando a la llamada al sistema alarm con argumento cero, lo que retorna cuántos segundos faltan para que llege el evento, o cero si no hay evento pendiente. Lamentablemente esto tiene el efecto secundario de cancelar la alarma que está pendiente, por lo que posiblemente se deba volver a invocar por segunda vez.

C) Si SIGALRM ya hubiese tenido un signal handler previo a la invocación de "my_sleep", éste se perdería. Modifique "my_sleep" para que restaure el signal handler original.

2 Modifique el programa reloj_block.c definiendo un

nuevo conjunto de señales “orig_mask” que será empleado del siguiente modo:

	sigprocmask(SIG_BLOCK, &usr1_mask, &orig_mask);
	... Seccion critica ...
	sigprocmask(SIG_SETMASK, &orig_mask, NULL);

Revise las páginas de manual de sigprocmask para comprender por qué esto es equivalente a la versión original.

6. Nacimiento y Muerte de Procesos

La creación y finalización de procesos "hijos" es parte fundamental de muchas aplicaciones en Linux/Unix. En este capítulo haremos una primera revisión a este extenso tema, centrado en las llamadas al sistema fork(2) y waitpid(2).

6.1. Algunos Conceptos

Normalmente todo proceso Linux/Unix puede crear otro proceso a partir de sí mismo. Este proceso generado se suele denoninar "proceso hijo" (child process) mientras que el generador se convierte en el "proceso padre" (parent process.) El proceso hijo es una réplica exacta del proceso padre, aunque suele tener un "comportamiento" distinto.

En muchas ocasiones el "hijo" interactúa con el "padre" colaborando para desempeñar una tarea de modo cooperativo, aunque en otras, el "hijo" puede desempeñar funciones totalmente ajenas al progenitor. Un ejemplo muy didáctico lo constituye la ejecución de un comando en el shell (intérprete de comandos), tal como ls o cp. Esto se implementa necesariamente en dos etapas:

  1. El shell (por ejemplo bash, sh, ksh, etc.) genera un proceso hijo que es una copia exacta de sí mismo (es decir, otro shell.) Esto se hace mediante la llamada al sistema fork(2).

  2. Este "shell hijo" tiene como única finalidad el "convertirse" en otro programa (en nuestro caso, ls o cp.) Esto se hace con un grupo de llamadas al sistema conocidas como exec().

Aquí nos interesa sólo la primera parte de este proceso, a saber, la creación del proceso "hijo" a partir del "padre" mediante fork(2). En el capítulo 7 desarrollaremos exec().

6.2. Demostración de Fork

Note
No podemos dejar de mencionar el popular artículo "A fork() in the road" discutido en https://news.ycombinator.com/item?id=29709802 .

El listado que presentamos a más abajo (fork_trivial.c) es una ilustración básica de la creación de un proceso hijo mediante fork(2). Esta llamada al sistema, al ser invocada, crea instantáneamente el proceso "hijo" con las mismas características que el padre, y ejecutándose en el mismo punto del programa [81] . La única diferencia (y lo que permite distinguirlos) radica en el valor de retorno de fork:

Table 6. Valores retornados por fork

0

Es el proceso hijo

> 0

Es el proceso padre que es informado acerca del pid de su proceso hijo recién creado

-1

Es el proceso padre y no pudo crear hijo

Algunas causas por las que fork podría fallar corresponden a:

  1. Agotamiento de memoria

  2. Alcance del límite asignado al usuario para creación de procesos

/* fork_trivial.c */
#include <sys/types.h>
#include <sys/unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
int p;

printf("Lanzo fork...\n");
p=fork();
if(p==-1)
	{
	printf("Fork fallo.\n");
	exit(1);
	}
if(p==0)
	printf("Soy el proceso hijo. Mi PID es: %d\n",getpid());
else
	printf("Soy el proceso padre. El PID de mi hijo es: %d\n",p);

return 0;
}

Nótese que se ha empleado la llamada al sistema getpid(2) que proporciona el PID del proceso en ejecución a fin de imprimir información de interés.

En el siguiente ejemplo (fork_descendencia.c) se muestra una forma de crear un proceso "hijo", luego un "nieto", un "bisnieto" y un "tataranieto" [82] . Cada uno de ellos tiene un tiempo de vida de aproximadamente 10 segundos. Obsérvese también que se ha hecho uso de la llamada al sistema getppid(2) que proporciona el PID del padre del proceso. Contraste la ejecución con el comando ps(1) [83] .

Si se elimina el sleep(10) la salida puede ser diferente. Sucede que si los procesos "padre" mueren antes que los "hijos", estos últimos son "adoptados" por el proceso especial llamado init (cuyo PID=1), y eso es lo que podría retornar getppid(2) [84] .

/*
 * fork_descendencia.c:
 * Crear hijo, nieto, bisnieto, tataranieto
 */
#include <sys/types.h>
#include <sys/unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
int p,z;

printf("Se inicia el padre con PID %d\n",getpid());

for(z=1;z<=4;z++)
	{
	p=fork();
	if(p==0)
		printf("Soy la generacion %d. Mi PID es: %d. Mi PPID es %d\n",z,getpid(),getppid());
	else
		{
		sleep(10);
		exit(0);
		}
	}
return 0;
}

Finalmente, el siguiente listado (fork_hermanos.c) muestra cómo crear un conjunto de procesos "hijos" del mismo padre; es decir, un grupo de "hermanos". De igual modo, el lector debe observar el efecto que puede tener la eliminación del sleep(10) en cuanto al valor retornado por getppid().

/*
 * fork_hermanos:
 * Crear cinco procesos "hermanos"
 */
#include <sys/types.h>
#include <sys/unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
int p,z;

printf("Se inicia el padre con PID %d\n",getpid());

for(z=1;z<=5;z++)
	{
	p=fork();
	if(p==0)
		{
		printf("Soy el hijo %d. Mi PID es \
%d. Mi PPID es %d\n",z,getpid(),getppid());
		exit(0);
		}
	}
sleep(10);
return 0;
}

6.3. Paralelizar una aplicación

Con frecuencia las aplicaciones crean procesos hijos con el fin de realizar tareas disímiles (pero complementarias.) En cambio aquí - a fin de hacer la exposición más sencilla - se presenta un programa que realiza un gran cálculo matemático valiéndose de tres procesos hijos auxiliares cuyo comportamiento es casi idéntico.

En los libros de cálculo se demuestra fácilmente que la constante matemática PI se puede hallar mediante la suma infinita:

PI/4 = 1 - 1/3 + 1/5 - 1/7 + 1/9 - 1/11 ...

Nosotros sumaremos hasta el término "1/n", intentando dividir el proceso en tres sumas parciales. Tal como indica la fórmula, al final multiplicaremos por cuatro:

4*{
1        - 1/3      + 1/5      - 1/7      .. - 1/(K-1)
+
1/(K+1)  - 1/(K+3)  + 1/(K+5)  - 1/(K+7)  .. - 1/(2K-1)
+
1/(2K+1) - 1/(2K+3) + 1/(2K+5) - 1/(2K+7) .. - 1/(3K-1)
}

Asimismo, compararemos el resultado obtenido con una aproximación más "exacta" de PI recogida de los libros a fin de aquilatar el error.

Admito que este esquema de tres sumas parciales es un tanto "caprichoso". Sin embargo, además de servirnos para ilustrar fork(), también podría ser muy beneficioso en un computador que tenga tres unidades de procesamiento (CPU’s) pues el sistema operativo normalmente intenta balancear entre estos a las aplicaciones. En nuestro caso, significaría que el programa obtiene sus resultados de forma paralela (y por tanto, más rápida) aprovechando el hardware disponible. A esto se le conoce como "paralelizar" la aplicación [85] .

#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>

#define FILE_TMP "pi.tmp"
void sumador(double);

double N;
int fd;

int main()
{
double nterm;
printf("Ultimo termino? ");
scanf("%lf",&nterm);
N=ceil(nterm/12);

unlink(FILE_TMP);
fd=open(FILE_TMP,O_WRONLY|O_CREAT,0777);
if(fd==-1)
	{
	perror("open fallo");
	exit(1);
	}
if(fork()==0)
	sumador(0.0);
if(fork()==0)
	sumador(4.0*N);
if(fork()==0)
	sumador(8.0*N);
while(wait(NULL)!=-1)
	;
close(fd);
double p=0;
FILE *fp;
char str[256];
fp=fopen(FILE_TMP,"r");
while(fgets(str,256,fp))
	p+=atof(str);
fclose(fp);
double delta=fabs(3.1415926535897932384626433832795-p);
printf("PI ~ %.15f D=%.1E\n",p,delta);
return 0;
}

void sumador(double inicio)
{
double contador;
double p=0;
double fin=4*N+inicio;
char str[256];
for(contador=1.0+inicio;contador<fin;contador+=4.0)
	{
	p=p+1.0/contador;
	p=p-1.0/(contador+2.0);
	/* printf("+1/%d-1/%d\n",
	(int)contador,(int)contador+2); */
	}
sprintf(str,"%.15f\n",4*p);
write(fd,str,strlen(str));
exit(0);
}

Este programa requiere algunos comentarios adicionales. En primer lugar, se ha empleado la estructura de "procesos hermanos" de fork_hermanos.c para crear tres subprocesos (hijos.)

Cada proceso hijo hace un cálculo (independientemente) del resto. Sin embargo, al final, el proceso padre debe recoger estos resultados parciales y sacar un valor total. Esto requiere algún mecanismo de "comunicación entre procesos", para lo cual Linux/Unix proporciona diversas facilidades que se discuten en otros capítulos. A fin de mantener sencillo el ejemplo, se ha utilizado un archivo temporal de disco llamado "pi.tmp" donde cada proceso escribirá una línea con el subtotal que ha calculado. El padre, al terminar todos los hijos, lee los valores calculados desde ese archivo y los totaliza. Nótese con especial cuidado que el archivo es abierto por el padre (en modo escritura) una sola vez; todos los hijos automáticamente heredan el "file handler" (en general, todos los archivos abiertos) por lo que todos los hijos también pueden grabar en el mismo. Asimismo, cada hijo añade una linea al archivo en vez de sobreescribir las existentes. Esto ocurre porque el "puntero" de grabación se comparte entre procesos heredados [86] .

6.4. Esperar el final de los hijos

Otro aspecto muy importante del programa anterior radica en cómo puede determinar el padre que todos sus hijos han terminado. Para esto hemos empleado la llamada al sistema wait(2), que en su forma más simple (como en nuestro programa) simplemente "espera" a que algún hijo termine. En nuestro programa, la hemos invocado repetidamente hasta que nos retornó "-1", que corresponde - entre otros - al caso en que ya no hay hijos cuya muerte se debe "esperar" [87] . También se pudo haber invocado exactamente tres veces, a fin de asegurarnos de que los tres procesos hijos hayan terminado.

wait(2) es la función más simple - y menos potente - para esperar procesos hijos. El listado que se muestra a continuación (espera.c) es una demostración de la función waitpid(2). A diferencia de wait(), waitpid() permite esperar por un hijo en particular (conociendo el PID), así como recibir notificaciones acerca de procesos hijos que son "detenidos" (pero no terminados.) Si se desea esto último, se debe añadir la constante WUNTRACED en el tercer argumento.

Al igual que en wait(2), se puede suministrar un "puntero a entero" a fin de recabar información concerniente a la forma en que terminó el proceso [88] .

/*
 * espera.c
 * Demostracion de waitpid
 */
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>

void menu_hijo(void);

int main()
{
int pid_child,status;

pid_child=fork();
if(pid_child>0)
	{
	for(;;)
		{
		waitpid(pid_child,&status,WUNTRACED);
		printf("\nSoy el proceso padre\n");
		if(WIFEXITED(status))
			{
			printf("El hijo termino normalmente.\n");
			printf("Valor de retorno: %d\n",
					WEXITSTATUS(status));
			break;
			}
		if(WIFSIGNALED(status))
			{
			printf("El hijo termino por senhal.\n");
			printf("La senhal fue: %d\n",
					WTERMSIG(status));
			break;
			}
		if(WIFSTOPPED(status))
			{
			printf("El hijo ha sido detenido.\n");
			printf("La senhal fue: %d\n",
					WSTOPSIG(status));
			printf("Se le continuara esperando...\n");
			}
		}
	}
else
	menu_hijo();

return 0;
}

void menu_hijo(void)
{
int i,r;

for(;;)
	{
	printf("Soy el proceso hijo. Escriba un numero de senhal\n"
	"o cero para terminar: ");
	scanf("%d",&i);
	if(i==0)
		{
		printf("Escriba el valor de retorno: ");
		scanf("%d",&r);
		exit(r);
		}
	kill(getpid(),i);
	}
}

El programa crea un proceso hijo, el cual solicita al usuario un valor numérico que será usado como señal a aplicarse a sí mismo. Aquí se puede apreciar el uso de la llamada al sistema kill(2) que permite enviar una señal a cualquier proceso, incluido a sí mismo [89] .

Si el usuario introduce un numero cero, el proceso hijo terminará con exit(3). Como se aprecia en el listado, la variable entera status es usada para averiguar la manera en que terminó el proceso hijo. Esta variable requiere de las macros WIFEXITED, WIFSIGNALED y WIFSTOPPED para indagar, respectivamente, si el proceso terminó en forma normal (por ejemplo con exit(3)), por una señal, o ha sido detenido. Las macros complementarias WEXITSTATUS, WTERMSIG y WSTOPSIG permiten obtener información más detallada.

A continuación mostramos una sesión de ejecución de este programa en un sistema Linux.

$ ./espera
Soy el proceso hijo. Escriba un numero de senhal
o cero para terminar: 2

Soy el proceso padre
El hijo termino por senhal.
La senhal fue: 2

$ ./espera
Soy el proceso hijo. Escriba un numero de senhal
o cero para terminar: 0
Escriba el valor de retorno: 17

Soy el proceso padre
El hijo termino normalmente.
Valor de retorno: 17

$ ./espera
Soy el proceso hijo. Escriba un numero de senhal
o cero para terminar: 19

Soy el proceso padre
El hijo ha sido detenido.
La senhal fue: 19
Se le continuara esperando...

***

Soy el proceso hijo. Escriba un numero de senhal
o cero para terminar:

Los dos primeros casos son sencillos. En el tercer caso, la señal 19 (SIGSTOP) detiene el proceso (no lo termina.) Esto es reportado correctamente por el proceso padre, el cual nuevamente invoca a waitpid (se indica con asteriscos.) A fin de que el proceso hijo continue, se debe abrir otra sesión y ejecutar un comando tal como kill -SIGCONT pid, donde "pid" es el PID del proceso hijo. Para averiguar el PID del proceso hijo detenido, se puede usar el comando ps, visualizando el campo de STATUS con el identificador "T":

$ ps ax | grep espera
  PID TTY      STAT   TIME COMMAND
...
15071 pts/1    S      0:00 ./espera
15072 pts/1    T      0:00 ./espera
15092 pts/2    S      0:00 grep espera
...

En este ejemplo, es claro que el PID del hijo es 15072.

6.5. Procesos zombies

La mayoría de los procesos tiene una duración finita. Hemos visto anteriormente que cuando un proceso padre muere, el proceso hijo es "adoptado" por el proceso de PID=1 (llamado init.) Sin embargo, una situación muy interesante ocurre cuando el hijo muere antes que el "padre". En este caso, no obstante que el proceso hijo ya no está en ejecución, el sistema guarda la "información de estado" acerca de éste, a la espera de que el padre en algún momento la reclame.

Si el proceso padre no hace nada con respecto a su "hijo" muerto, el sistema reportará en la tabla de procesos un "zombie" (proceso que no puede morir) por tiempo indefinido (hasta que el padre reclame la "información de estado" o muera.)

Las llamadas wait y waitpid se usan por los procesos padre para "esperar" a la muerte de sus procesos hijos, y de paso obtener la "información de estado" que evita la formación de zombies. Es una mala práctica permitir la existencia de zombies por no tomar las precauciones del caso.

Por ejemplo, los listados fork_descendencia.c y fork_hermanos.c pueden generar zombies durante el tiempo en que el padre "duerme" con sleep(), debido a que no los ha "esperado" como sí ocurre en los ejemplos posteriores. El comando ps deberá ser capaz de mostrar estos "zombies" más o menos del modo siguiente:

 2473 pts/1    Z      0:00 [fork_descend <defunct>]
 2474 pts/1    Z      0:00 [fork_descend <defunct>]
 2475 pts/1    Z      0:00 [fork_descend <defunct>]
 2476 pts/1    Z      0:00 [fork_descend <defunct>]
 2477 pts/1    Z      0:00 [fork_descend <defunct>]

En ambos casos, los difuntos desaparecen cuando el padre termina (pues, como se dijo, los hijos huérfanos son adoptados y "esperados" por init, lo que les permite "morir en paz" [90] .)

6.6. Grupos de procesos

Los procesos hijos de un mismo antecesor mantienen un rasgo que los identifica como pertenecientes a la misma "familia". Este es el "process group id" (PGID.) El siguiente listado (fork_grupo.c) presenta un programa que crea una jerarquía de cuatro procesos: padre, hijo, nieto y bisnieto. Sin embargo, a diferencia de todos los programas anteriores, el nieto y el bisnieto tienen un PGID distinto al de sus progenitores, el cual ha ocurrido gracias a la llamada al sistema setpgrp(2) [91] .

Hemos definido además un "signal handler" para la señal SIGINT en el padre, lo cual se heredará a toda la descendencia. Cuando los cuatro procesos están en ejecución (en este caso, durmiendo) se requiere que el usuario presione la tecla de interrupción (CTRL+C) a fin de detenerlos. Como observará el lector, esto sólo afecta al padre y al hijo pues están en el "grupo de procesos en foreground" [92] .

/* fork_grupo.c */
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

void handler(int s)
{
printf("Proceso %d recibio senhal %d\n",getpid(),s);
}

int main()
{
int z;

signal(SIGINT,handler);

z=fork();
if(z>0)
	{
	printf("Proceso padre pid=%d pgid=%d\n",getpid(),getpgrp());
	sleep(60);
	exit(0);
	}
/* El hijo */
z=fork();
if(z>0)
	{
	printf("Proceso hijo pid=%d pgid=%d\n",getpid(),getpgrp());
	sleep(60);
	exit(0);
	}

setpgrp();

/* El nieto */
z=fork();
if(z>0)
	{
	printf("Proceso nieto pid=%d pgid=%d\n",getpid(),getpgrp());
	sleep(60);
	exit(0);
	}

/* El bisnieto */
printf("Proceso bisnieto pid=%d pgid=%d\n",getpid(),getpgrp());
sleep(60);
return 0;
}

6.7. Demonios

El concepto informal de demonio se refiere a un proceso que no interactúa con el usuario mediante terminal alguno. Aunque no existe una defición precisa (y por tanto, no existe una manera única de programarlo), existe un conjunto de recomendaciones establecidas que pasaremos a explicar. El siguiente programa implementa un demonio que se limita a escribir aleatoriamente algunas frases absurdas en el archivo /tmp/infierno.txt:

/* demonio.c */
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>

void inicializa(void);
void trabaja(void);
void cierra_archivos(void);

int main(void)
{
inicializa();
trabaja();
return 0;
}

void inicializa(void)
{
int p;
if((p=fork())==-1) {
	fprintf(stderr,"fork error (%d)\n",errno);
	exit(1);
	}
if(p!=0) exit(0);
setsid();
chdir("/");
umask(022);
cierra_archivos();
}

void cierra_archivos(void)
{
int openmax,z;
openmax=sysconf(_SC_OPEN_MAX);
if(openmax==-1) openmax=20;
for(z=0;z<openmax;z++)
	close(z);
}

static char *frase[]={
	"A quien madruga, le duelen los parpados",
	"La paciencia es la madre de todos los males",
	"Hay que hacer un MBA",
	"Este programa es absurdo"};
void trabaja(void)
{
FILE *fp;
int l=sizeof(frase)/sizeof(char*);

for(;;) {
	fp=fopen("/tmp/infierno.txt","w");
	fprintf(fp,"%s\n",frase[rand()%l]);
	fclose(fp);
	sleep(5);
	}
}

La función main() invoca a la inicialización del demonio (que es lo que nos interesa) y luego a una rutina que hace el "trabajo real" (trabaja().) Aquí sólo explicaremos la inicialización del demonio [93] .

Como se aprecia, el programa se inicia haciendo una copia de sí mismo con fork(2), y descartando al padre (que termina con exit(3).) Esto tiene doble propósito; en primer lugar, el shell que ejecuta al programa recibe la notificación de que éste ha terminado; de este modo, el shell puede proseguir con otras tareas. En segundo motivo es mucho más sutil, pero en términos informales la idea es "desconectar" al demonio de todos los procesos asociados al terminal (especialmente del shell), lo que equivale a "desasociarlo" al terminal de control en uso. Esto evita, por ejemplo, que el shell pueda colocar (incorrectamente) al demonio en "foreground", o que le envíe señales en caso de desconexión, etc. Todo esto se logra mediante la invocación a setsid(2), la cual crea una nueva "sesión" que no está asociada a ningún terminal; sin embargo, para que setsid(2) funcione es necesario que el proceso que la invoca no sea un "proceso líder de grupo [94] ", lo que se puede conseguir fácilmente con fork(2).

Es recomendable además cambiar el directorio actual del proceso a la raíz (/) o a un directorio específico de la aplicación (chdir(2).) Por ejemplo, si el proceso tiene un fallo irrecuperable los archivos core se generan en el directorio actual del proceso. En este punto también se aconseja invocar a umask(2) con un argumento conveniente a fin de evitar que los archivos que se crean tengan permisos independientes de la máscara prefijada del usuario heredada del shell.

El último paso consiste en cerrar los descriptores abiertos heredados del shell pues ya no se utilizarán. Lamentablemente no hay una forma portable de conocer qué descriptores han sido abiertos por lo que se procede a intentar cerrar todos los descriptores admisibles. Sin embargo, tampoco hay una manera infalible de conocer el máximo número de descriptores admisibles. El estándar POSIX.1 proporciona la rutina sysconf(3) para obtener el valor de diversos límites como el mencionado; a tal fin se debe invocar con el argumento _SC_OPEN_MAX. Si ésta retorna -1, utilizamos un valor arbitrario de 20 [95] .

6.8. Ejercicios

1 En el listado fork_descendencia.c, si se añade una llamada sleep(5) justo antes de cerrar el loop for, provocaremos que los descendientes tomen más tiempo en crearse. Esto ocasionará que sus padres mueran primero. Así podrá observar claramente la adopción de procesos huérfanos por parte de init:

		sleep(10);
		exit(0);
		}
	sleep(5);
	}

Tras comprobar esto elimine el sleep(10) (dejando el sleep(5)) y modifique el programa para que los padres "esperen" correctamente a los hijos sin que estos sean adoptados por init.

2 Modifique el programa de cálculo de PI para que el número de subprocesos sea un argumento solicitado al usuario.

3 Investigue la llamada al sistema wait4(2) y úsela en el programa del espera.c para averiguar el consumo de cpu del proceso hijo al terminar éste.

4 Cuando un proceso invoca a setpgrp(2), y ésta tiene éxito, su nuevo PGID coincide con el PID. Como recordará, en el programa fork_grupo.c el "nieto" y el "bisnieto" comparten el mismo PGID. El hijo tiene acceso al PID del nieto, y por tanto al PGID del nieto y del bisnieto. Modifique el programa para que el "hijo", pasados 30 segundos, termine al nieto y al bisnieto usando el PGID de éstos. Para esto, deberá leer el manual de kill(2) donde se indica que el uso de un PID negativo equivale a enviar la señal a todos los procesos con PGID igual al valor absoluto de tal "pid".

7. Las funciones exec y el mini-shell

Cuando un proceso requiere iniciar la ejecución de otro programa, utiliza un conjunto de llamadas al sistema conocidas como "funciones exec". Un ejemplo típico lo constituyen los shell’s de los sistemas Linux/Unix, los cuales suelen ejecutar diversas aplicaciones dependiendo de lo que el usuario solicite en la línea de comando.

A tal efecto, en este capítulo desarrollaremos paulatinamente un pequeño programa que intenta emular las funciones fundamentales de un shell; este programa requiere la aplicación de conceptos que se presentan (en orden de importancia) en los capítulos 6, 5 y 8.

7.1. Conceptos Básicos

Linux/Unix permiten la creación de nuevos procesos mediante la llamada al sistema fork(2). Sin embargo, estos procesos son prácticamente idénticos en cuanto a que ejecutan exactamente el mismo programa (el mismo código.) A fin de lanzar un nuevo programa (es decir, cargar y ejecutar un archivo de disco) utilizaremos las "funciones exec" que reemplazarán el código del proceso en ejecución por el del nuevo programa. Téngase en cuenta que esto no crea un nuevo proceso (el PID no varía tras el exec), simplemente reemplaza al que está en ejecución e invocó a exec.

En lo que sigue, intentaremos construir un "shell básico" que ejecutará comandos dependiendo de lo que el usuario le proporcione como entrada.

7.2. Un shell prototipo

A fin de mostrar el uso de las llamadas al sistema "exec" [96] , a continuación se presenta un prototipo para un "shell trivial" que permite ejecutar tres comandos dependiendo de lo que el usuario seleccione mediante un menú. El texto que le sigue muestra una breve sesión con este programa.

/*
 * shell_trivial.c: Ejecutar uno de tres 
 * comandos elegidos por el usuario.
 */
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>

int main()
{
int opcion,i;
char *cmd;

for(;;)
	{
	printf("Seleccione: 1-date 2-ls 3-cal 0=finalizar ? ");
	scanf("%d",&opcion);
	switch(opcion)
		{
		case 0:
			return 0;
		case 1:
			cmd="date"; break;
		case 2:
			cmd="ls"; break;
		case 3:
			cmd="cal"; break;
		default:
			fprintf(stderr,"Error: comando no comprendido\n");
		}
	i=fork();
	if(i==0)
		{
		execlp(cmd,cmd,NULL);
		perror("Exec fallo");
		return 1;
		}
	else
		waitpid(i,NULL,0);
	}
}

Ejemplo de sesión con este programa:

$ ./shell_trivial
Seleccione: 1-date 2-ls 3-cal 0=finalizar ? 1
dom ago  3 12:21:52 PET 2003
Seleccione: 1-date 2-ls 3-cal 0=finalizar ? 2
exec.sxw  ikaro  out  prog1  prog1.c  t5.txt  t8.txt  tx1.txt  xprog6  xprog6.c
Seleccione: 1-date 2-ls 3-cal 0=finalizar ? 3
   agosto de 2003
do lu ma mi ju vi 
                1  2
 3  4  5  6  7  8  9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Seleccione: 1-date 2-ls 3-cal 0=finalizar ? 0
$

En el programa, el usuario selecciona un valor numérico lo que provoca que se asigne una determinada cadena de texto al puntero "cmd". Luego se crea un proceso hijo, y el hijo inmediatamente invoca a “execlp()” para ejecutar un nuevo programa (que viene en "cmd".) El padre, por su parte, sólo espera a que este programa termine para volver a presentar el menú al usuario. Esto es a grandes rasgos lo que hace un shell.

¿Qué hubiera ocurrido si sólo se hubiese ejecutado el exec, sin el fork? - en ese caso, el "comando" se hubiera ejecutado, pero nunca volveríamos al menú inicial pues este programa ya no estaría en ejecución.

Finalmente, debemos observar con cuidado la sintaxis usada en el execlp. Esta función requiere como primer parámetro el nombre de un programa ejecutable, en nuestro caso, el puntero "cmd". El resto de parámetros corresponden a la lista de argumentos "argv[ ]" a ser pasados al comando, empezando por "argv[0]", que convencionalmente corresponde al nombre del programa (por eso hemos empleado nuevamente a "cmd".) La lista de argumentos se debe terminar con un puntero NULL [97] .

Por ejemplo, si se hubiera deseado ejecutar el comando "ls -l", la función execlp correspondiente sería:

execlp("ls","ls","-l",NULL);

Por otro lado, surge la pregunta natural: ¿cómo encuentra el sistema la ruta donde está el programa "ls"? La respuesta es la famosa variable de entorno [98] "PATH", que contiene la lista de directorios con "ejecutables" donde se busca los programas [99] . Existen otras formas de "exec" que no consultan esta variable de entorno y asumen la presencia de un "pathname" [100] .

Las funciones exec no deben retornar nunca. En nuestro programa, si exec retorna se ejecuta la rutina "perror" y el proceso hijo termina. Típicamente esto ocurre cuando el sistema no puede hallar el programa o no se tiene permiso de ejecución.

7.3. Un shell más inteligente

El ejemplo anterior ilustró el punto central de las funciones exec. Ahora intentaremos elaborar un "shell" totalmente nuevo con las siguientes características:

  1. Podrá recibir comandos con sus argumentos separados por espacios, los cuales serán ejecutados como subprocesos

  2. Interpretará la sintaxis > archivo como redirección de la salida estándar del comando hacia el archivo especificado. Para facilitar el "parsing" obligaremos a introducir espacios antes y después del “\>”.

  3. Podrá interpretar el amperstand (&) al final del comando (separado con un espacio) como ejecución en "background"; en caso contrario será en "foreground" [101].

  4. Podrá reconocer el comando interno "exit" para terminar su ejecución.

  5. Desactivará la señal SIGQUIT durante la introducción de comandos; la señal SIGINT servirá para cancelar el comando introducido hasta el momento. Estas señales se reactivarán para los comandos ejecutados en foreground, manteniéndose anuladas para procesos en background.

Mostraremos y explicaremos el programa por partes. Asumiremos que el programa está constituido por un solo archivo (para facilitar la exposición.) El lector puede fácilmente adaptar el programa para organizarlo en varios archivos.

7.3.1. Inicialización y loop principal

El listado 1 tiene como función princial reprogramar las señales SIGINT, SIGQUIT y SIGCHLD, así como recibir el comando del usuario.

/*
 * Listado 1
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>

#define LINE_WIDTH 256
#define N_ARGS 30

int read_a_line(void);
int parse(void);
int process(void);
void cleanup(void);
void intr_handler(int);
void child_handler(int);
void desactivar_senhales(void);
void redireccion(void);

char LINE[LINE_WIDTH];
char DST_FILE[LINE_WIDTH];
char *COMMAND[N_ARGS];
char flag_cancel;
char flag_bg;
char flag_redirect;

int main(int argc,char **argv)
{
struct sigaction sa;

sa.sa_handler=intr_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags=0;
sigaction(SIGINT,&sa,NULL);

sa.sa_handler=SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags=0;
sigaction(SIGQUIT,&sa,NULL);

sa.sa_handler=child_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags=SA_RESTART;
sigaction(SIGCHLD,&sa,NULL);

while(1)
	{
	if(read_a_line()==-1)
		continue;
	if(parse()==-1)
		continue;
	if(process()==-1)
		return 0;
	cleanup();
	}
}

Las funciones cuyos prototipos se muestran en el listado 1 se irán explicando poco a poco. El programa asigna un handler a SIGINT (intr_handler()), ignora SIGQUIT, y asigna un handler a SIGCHLD (child_handler().) El handler de SIGINT no debe hacer nada, excepto interrumpir la lectura del texto (ver función read_a_line().) Como se sabe, para esto es necesario que el handler exista (aunque esté vacío) puesto que si la señal simplemente se ignora, entonces la lectura no se interrumpiría [102] . La señal SIGCHLD será enviada por el sistema a nuestro proceso cada vez que uno de sus hijos muera. Esto nos ayudará a evitar los "procesos zombies" [103] . Esto tiene el efecto indeseable de interrumpir la lectura (tal como en el caso de SIGINT) por lo que hemos utilizado el "flag" SA_RESTART [104] , que reinicia la lectura en forma instantánea al terminar la ejecución del handler, con lo que evitamos su interrupción [105] .

Finalmente, las funciones read_a_line() y parse() retornan -1 en caso de error (por lo que se debe volver a leer inmediatamente), mientras que process() retorna -1 cuando se da el comando de salida “exit”, por lo que termina el programa. En caso contrario, se invoca a la rutina cleanup() que limpia la memoria asignada dinámicamente por parse().

7.3.2. Signal handlers y lectura de comandos

/*
 * Listado 2
 */
void intr_handler(int s)
{
}

void child_handler(int s)
{
int r;

do
	r=waitpid(-1,NULL,WNOHANG);
while(r!=0 && r!=-1);
}

int read_a_line(void)
{
int z;

write(STDOUT_FILENO,"$$$ ",4);
z=read(STDIN_FILENO,LINE,LINE_WIDTH-1);

if(z==-1 && errno==EINTR)
	{
	write(STDOUT_FILENO,"\n",1);
	return -1;
	}
if(z==0)
	{
	LINE[0]='\0';
	return 0;
	}
LINE[z-1]='\0';
return 0;
}

El listado 4 presenta los handlers y la función read_a_line(). El handler de SIGCHLD presenta el uso de waitpid(2) con la opción WNOHANG, el cual hace que la función retorne inmediatamente así los procesos hijos no hayan terminado todavía. Como "PID" se ha especificado "-1", lo que significa "esperar a cualquier proceso hijo". De acuerdo a la documentación, waitpid retornará "-1" si ya no hay más hijos por esperar, o "0" si ningún hijo ha terminado todavía [106] . Por este motivo, la invocamos repetidamente (tratando de "esperar" a todos los hijos muertos en hasta el momento), hasta que retorne cualquiera de estos valores. Esto se hace así debido a que podría tenerse varios procesos en background que mueren en forma simultánea.

La lectura del comando se hace mediante read(2). Esta llamada al sistema puede retornar antes de tiempo por una interrupción de señal (en nuestro caso, SIGINT), lo cual se comprueba verificando que el valor de retorno sea "-1" y que “errno” contenga "EINTR" [107] .

Verificamos también si se ha retornado cero caracteres, lo que significa que el usuario presionó sólo [CTRL]+[D] (End Of File), y en caso contario, se asume que se presionó [Enter] para finalizar la línea y éste "fin de línea" es eliminado.

7.3.3. Parsing

/*
 * Listado 3
 */
int parse(void)
{
char *n=LINE;
int z=0;
char *err_msg1="Se requiere archivo de destino\n";
char *err_msg2="Demasiados argumentos\n";

flag_bg=0;
flag_redirect=0;

n=strtok(n," ");
if(n==NULL)
	{
	COMMAND[0]=NULL;
	return 0;
	}
do
	{
	if(strcmp(n,"&")==0)
		{
		COMMAND[z]=NULL;
		flag_bg=1;
		break;
		}
	if(strcmp(n,">")==0)
		{
		COMMAND[z]=NULL;
		n=strtok(NULL," ");
		if(n==NULL)
			{
			cleanup();
			write(STDERR_FILENO,err_msg1,strlen(err_msg1));
			return -1;
			}
		strcpy(DST_FILE,n);
		flag_redirect=1;
		continue;
		}
	if(z==N_ARGS)
		{
		COMMAND[z-1]=NULL;
		cleanup();
		write(STDERR_FILENO,err_msg2,strlen(err_msg2));
		return -1;
		}
	COMMAND[z]=strdup(n);
	z++;
	}
	while((n=strtok(NULL," ")));

COMMAND[z]=NULL;

return 0;
}

El listado 3 sólo realiza el "parsing" de la línea ingresada. Los "tokens" o palabras separadas por espacios se guardan consecutivamente en el array de punteros "COMMAND[ ]". En caso de errores, se retorna -1 liberando la memoria usada con cleanup(). Nótese que los punteros se asignan mediante la rutina strdup(3), por lo que deberán liberarse con free(3). En caso de redirección (\>) o de ejecución en background (\&), se activan las variables de estado “flag_redirect” y “flag_bg”.

7.3.4. Ejecutar los comandos

/*
 * Listado 4
 */
int process(void)
{
int p;

if(COMMAND[0]==NULL)
	return 0;

if(strcmp(COMMAND[0],"exit")==0)
	return -1;

p=fork();

if(p==0)
	{
	if(flag_redirect)
		redireccion();
	if(flag_bg==1)
		desactivar_senhales();
	execvp(COMMAND[0],COMMAND);
	exit(errno);
	}
else
	if(flag_bg==0)
		waitpid(p,NULL,0);
return 0;
}

La función process() utiliza el array "COMMAND[ ]" y ejecuta el programa especificado mediante fork y exec. Antes de esto se verifica si el comando (primer token) es “exit” o si simplemente no hay comando (no se hace nada si el usuario simplemente presiona [ENTER].) De haberse solicitado redirección, se invoca a la función redireccion(). Si se ha solicitado ejecución en background, se desactivan las señales en la función desactivar_senhales(). Nótese que para el caso foreground las señales no se necesitan reactivar. Esto se debe a que "exec" resetea los signal handlers a su valor por omisión si es que éstos están asociados a un signal handler.

La función exec usada en esta ocasión es “execvp”, la cual requiere el nombre del programa como primer parámetro, y un array de punteros para especificar los argumentos como segundo parámetro. Este array de punteros debe terminar en NULL, cosa que ya se ha hecho en parse(). Como sabemos, exec no debe retornar (salvo error.) En ese caso, el proceso hijo no tiene nada más que hacer salvo retornarlo con exit() a fin de que el padre lo analice (cosa que no hacemos aquí.)

Finalmente, si el proceso se lanza en foreground, debemos esperar a que éste termine. Para esto usamos waitpid(2) en su forma más simple (sin el flag WNOHANG.)

7.3.5. Limpieza de memoria

/* listado 5 */
void cleanup(void)
{
char **p=COMMAND;

while(*p)
	free(*p++);
}

La función cleanup() del listado 5 libera la memoria asignada con strdup() en el array COMMAND. De no haberse implementado, el programa continuaría trabajando bien, pero silenciosamente crecería su uso de memoria de modo ilimitado [108] .

7.3.6. Desactivación de señales

/*
 * Listado 6
 */
void desactivar_senhales(void)
{
struct sigaction sa;

sa.sa_handler=SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags=0;

sigaction(SIGINT,&sa,NULL);
}

El listado 6 presenta la función “desactivar_senhales()” usada cuando se lanza un proceso en background. Como se aprecia, sólo hemos desactivado SIGINT debido a que estaba asignada al signal handler intr_handler(). SIGQUIT no ha requerido de este tratamiento pues ya estaba ignorada desde el inicio.

SIGCHLD no ha requerido modificación pues al estar asignada al handler child_handler(), es reseteada automáticamente por exec a su "default".

7.3.7. Redireccion de salida estándar

/*
 * Listado 8
 */
void redireccion(void)
{
int fd;
int e;
char *msg_err="Error abriendo el archivo de redireccion\n";

close(STDOUT_FILENO);
fd=open(DST_FILE,O_WRONLY|O_CREAT,0777);
if(fd!=STDOUT_FILENO)
	{
	e=errno;
	write(STDERR_FILENO,msg_err,strlen(msg_err));
	exit(e);
	}
}

El listado 7 presnta el código usado para implementar la redirección. Se ha cerrado el descriptor STDOUT_FILENO (o sea, el número 1) y se ha invocado a open(2) inmediatamente. La llamada al sistema open(2) siempre retorna el descriptor más pequeño que no está abierto. En este caso, sólo podía ser el 1 (puesto que el cero, STDIN_FILENO, ya estaba abierto para leer del terminal.) Esto significa que open() está reabriendo la salida estándar hacia el archivo “DST_FILE”, lo cual será empleado por el programa ejecutado con exec. En caso de error debemos escribir en el "error estándar" (STDERR_FILENO) pues ya no hay salida estándar.

Como se indicó al inicio, todos estos listados conforman un solo gran archivo. Concaténelos y compile el archivo total:

$ cat listado[1234567].c > minishell.c
$ gcc -o minishell minishell.c

Ahora presentamos una sesión (recortada) con nuestro mini-shell:

$ ./minishell
$$$ ls /etc
CORBA            gnome            nethack
Crack            gnome-vfs-2.0    network
Muttrc           gnome-vfs-mime   nsswitch.conf
Muttrc.user-es   gpm.conf         oaf
X11              grep-dctrl.rc    openoffice
...
$$$ ls / > prueba
$$$ cat prueba
bin
boot
...
usr
var
win1
$$$ sleep 10 &
$$$ pwd
/root/prog_unix/cap5
$$$ ps
  PID TTY          TIME CMD
 1086 pts/1    00:00:01 bash
13586 pts/1    00:00:00 prog3
13591 pts/1    00:00:00 sleep
13593 pts/1    00:00:00 ps
$$$ ps
  PID TTY          TIME CMD
 1086 pts/1    00:00:01 bash
13586 pts/1    00:00:00 prog3
13594 pts/1    00:00:00 ps
$$$ exit
$

7.4. Ejercicios

1 Nuestro shell no reporta errores en la ejecución de los procesos hijo. Ni siquiera nos avisa cuando no puede ejecutar el programa solicitado. Para esto el proceso padre deberá averiguar las condiciones en que sus hijos terminan mediante las facilidades proporcionadas en waitpid(2) [109] . Inspírese en el comportamiento de los shell’s de Linux/Unix e intente implementar esto.

2 Cada vez que se ejecute un proceso en background, regístre el comando y el PID en una tabla a fin de que esta pueda ser consultada. Implemente el comando interno “jobs” que lista esta tabla. Verifique que la terminación de los procesos en background limpien paulatinamente esta tabla a fin de mantenerla actualizada. Compare ésto con el comando jobs del shell bash o ksh.

3 Si se elimina el flag SA_RESTART en el handling de SIGCHLD, además del problema con la "interrupción de la lectura" que se describió antes, surgirá otro problema un tanto más sutil. Considérese el caso en que se ejecuta el comando “sleep 20 \&” (en background) e inmediatamente se lanza un “sleep 60” (en foreground.) El primer sleep terminará tras 20 segundos y se lanzará la señal SIGCHLD al "shell", lo que invocará al signal handler (child_handler()), pero tendrá el efecto indeseado de interrumpir el waitpid al proceso en foreground. Compruébese esto en la práctica.

Elimine el flag SA_RESTART (a fin de hacer el programa más portable) y solucione los problemas mencionados. El siguiente código puede resolver el último de éstos inconvenientes.

	if(flag_bg==0)
		{
		while(waitpid(p,NULL,0)==-1 && errno==EINTR)
			;
		}

La "interrupción de la lectura" se puede solucionar activando una variable auxiliar en el hanlder de SIGCHLD que puede usarse para forzar una relectura inmediata. A continuación se muestra parte del código de read_a_line() donde se hace uso de la variable "flag_hijo_murio".


sigo_leyendo:
flag_hijo_murio=0;
z=read(STDIN_FILENO,LINE,LINE_WIDTH-1);

/* Interrupcion por SIGINT o SIGCHLD */
if(z==-1 && errno==EINTR)
        {
        if(flag_hijo_murio)
                goto sigo_leyendo;
        write(STDOUT_FILENO,"\n",1);
        return -1;
        }

Verificar que esto resuelve los inconvenientes.

8. Entrada/Salida a bajo nivel

Cualquier programa que pretenda acceder o conservar información contenida en dispositivos de almacenamiento requiere efectuar I/O (entrada/salida) desde y hacia los archivos correspondientes. Aquí mostraremos cómo se lleva a cabo este proceso en los sistemas Linux/Unix mediante llamadas al sistema.

8.1. Conceptos Básicos

Linux/Unix considera cualquier información de dispositivos de almacenamiento como contenida en "archivos", los cuales deben "abrirse" para efectuar el acceso correspondiente. La "apertura" (open) del archivo, se efectúa haciendo referencia al "nombre del archivo", y si tal "apertura" es exitosa, el sistema asigna un número entero a la nueva conexión programa-archivo. Este número se conoce como "file descriptor" o "descriptor de archivo". La llamada al sistema que permite abrir archivos es open(2).

A partir de ese momento, todas las operaciones que hagan relación a tal conexión se harán con referencia al "file descriptor", y no al nombre del archivo.

Probablemente el lector ya conoce la forma típica de interactuar con los archivos del sistema, a saber, mediante la "librería estándar" (recuérdese, por ejemplo, las funciones fopen, fgets, fprintf, etc.) Esta librería estándar, a fin de cuentas, invoca a las llamadas al sistema que veremos.

8.2. Escribiendo en disco

/*
 * write_trivial: Crear un archivo de prueba con
 * llamadas al sistema
 */
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>

int main()
{
int fd;

fd=open("/tmp/trivial.txt",O_WRONLY|O_CREAT,0666);
if(fd==-1)
	perror("open fallo");

write(fd,"xyz\n",4);

close(fd);
return 0;
}

El descriptor obtenido con open(2) será empleado únicamente para escribir información en el archivo, por lo que se usa la constante de "tipo de acceso" O_WRONLY.

Table 7. Especificaciones de tipo de acceso
Constante Significado

O_WRONLY

Sólo lectura

O_RDONLY

Sólo escritura

O_RDWR

Lectura y escritura

Además, si el archivo especificado no existe, deseamos que sea creado, para lo cual se añade el modificador O_CREAT. La presencia de este flag obliga a especificar el modo o "permiso" de creación del archivo como tercer argumento de open(2). Hemos usado el modo 0777 [110] , lo cual constituye los "permisos iniciales" que se reducirán con la "máscara de permisos del usuario" (umask.) [111] Si el archivo ya existiera, este tercer argumento es obviado [112] .

Al igual que la mayoría de llamadas al sistema, si open(2) falla, retorna "-1" y se puede consultar errno para más detalles.

Luego, se escribe información en el archivo mediante write(2), la cual requiere especificar el descriptor, un puntero a la información a escribir, y cuántos bytes se escribirán. Finalmente el archivo se cierra con close(2).

8.3. Los buffers son "volátiles"

El programa write_kill.c muestra un programa que crea dos archivos y graba cierta información en ellos, usando llamadas al sistema y la librería estándar respectivamente. Antes de cerrarse los archivos (con close() y fclose() respectivamente) el proceso muere con SIGKILL [113] . El texto subsiguiente muestra los resultados del programa.

/*
 * write_kill: Crear archivos de prueba con
 * llamadas al sistema y libreria estandar
 */
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <signal.h>

int main()
{
int fd;
FILE *fp;

fd=open("/tmp/wk1",O_WRONLY|O_CREAT,0666);
write(fd,"Mitsubishi\n",11);

fp=fopen("/tmp/wk2","w");
fputs("Mitsubishi\n",fp);

raise(SIGKILL);

return 0;
}

Algunos resultados:

$ ./write_kill
Terminado (killed)
$ cat /tmp/wk1
Mitsubishi
$ cat /tmp/wk2
$

Lo que se puede apreciar es que la llamada al sistema write(2) sí cumplió su cometido, mientras que fputs(3) no lo hizo. Esto último ocurrió porque fputs(3), siendo parte de la librería estándar, intenta escribir en sus buffers hasta llenarlos, antes de llevar a cabo la grabación real (con write(2).) En este caso la señal dió muerte al proceso y la información del buffer se perdió [114] .

Write(2), en cambio, no emplea este tipo de buffer, y logra escribir en el archivo. Sin embargo, esto no significa necesariamente que la información recién grabada resida físicamente en el disco duro, pues la información escrita por write(2) normalmente también va a un buffer administrado por el kernel (buffer caché), que no está afecto a la terminación del proceso como el buffer de la librería estándar. Pero si el sistema sufriera una "interrupción eléctica" apenas terminado el programa, la información "grabada" por write(2) (que reside en el buffer caché) también se podría perder.

El sistema operativo efectúa la verdadera grabación del "buffer caché" cada cierto tiempo en forma automática y transparente. De acuerdo con esto, si la interrupción eléctica ocurriera "algún tiempo" después de la ejecución del programa, los datos sí estarían físicamente en el disco y no se perderían. Con el fin de evitar que esto ocurra, existen maneras de forzar escrituras "reales" en el disco con cada write() [115] .

8.4. I/O síncrona

La llamada al sistema open(2) permite especificar una serie de "flags" que modifican el comportamiento de las operaciones de I/O. Uno de ellos es el flag “O_SYNC” que genera "escrituras reales" en el disco. El uso de esta opción, por tanto, hará más lentas las escrituras.

/*
 * write_sync.c
 * write en modo sincrono
 */
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>

#define SIZ 10000

int main()
{
long z;
int fd;

fd=open("/tmp/write_sync.txt",O_WRONLY|O_CREAT|O_SYNC,0666);
if(fd==-1)
	perror("open fallo");

for(z=0;z<SIZ;z++)
	write(fd,"x",1);

close(fd);
return 0;
}

En mi sistema el tiempo requerido para el proceso fue de 8.5 segundos aproximadamente; en cambio, al eliminar la opción O_SYNC, este tiempo se redujo a 0.13 segundos. Es evidente que la versión "con buffer caché" es mucho más veloz.

8.5. Los descriptores estándar

De modo convencional, el shell y casi todas las aplicaciones Linux/Unix reservan los archivos cuyos descriptores son "0", "1" y "2" para la "entrada estándar", "salida estándar" y "error estándar", respectivamente. Normalmente, estos tres archivos corresponden al "terminal del control" donde el usuario ejecuta los procesos, y así, leer el descriptor "0" equivale a leer desde el teclado; escribir en los descriptores "1" o "2", equivalen a escribir en la pantalla. Aunque es bastante seguro usar estos números, el estándar POSIX.1 recomienda usar en su lugar las constantes STDIN_FILENO, STDOUT_FILENO, y STDERR_FILENO, respectivamente. El siguiente ejemplo (descriptores.c) muestra un programa trivial de interacción con el usuario que hace uso de estos descriptores "estándar". Nótese que estos no requieren ser abiertos con open() pues el shell ya los ha abierto por nosotros. Este ejemplo hace uso de la llamada al sistema read(2) que lee información de un archivo especificado por su descriptor [116]). ] .

/*
 * descripores.c: Descriptores estandar
 */
#include <unistd.h>
#include <string.h>

int main()
{
char *quest="Escriba algo: ";
char L[256];
char R[512]="Ud. escribio: ";
int i;

write(STDOUT_FILENO,quest,strlen(quest));
i=read(STDIN_FILENO,L,256);
L[i]='\0';

strcat(R,L);
write(STDOUT_FILENO,R,strlen(R));

return 0;
}

En el texto siguiente se aprecia claramente cómo se comporta este programa en su interacción con el terminal y cuando la salida estándar es redirigida a un archivo llamado “ikaro”:

$ ./descriptores
Escriba algo: Jojolete
Ud. escribio: Jojolete
$ ./descriptores > ikaro
Ninfas de la tierra
$ cat ikaro
Escriba algo: Ud. escribio: Ninfas de la tierra

8.6. Alterando archivos abiertos

La llamada al sistema open() permite especificar un conjunto de opciones relacionadas a la apertura del archivo. A modo de ejemplo, la opción O_APPEND especifica que toda escritura con write() siempre añadirá los datos al final del archivo:

fd=open("nombre archivo",O_WRONLY|O_CREAT|O_APPEND,0666);

En ciertos casos se desea añadir (o eliminar) estas opciones luego de que el archivo ha sido abierto (después del open()), lo cual se puede conseguir mediante la llamada al sistema fcntl(2).

El programa append.c abre un mismo archivo en dos oportunidades. Luego, usando uno de los descriptores de archivo (fd1), escribe un texto en aquél. En este momento el segundo descriptor (fd2) todavía "apunta" al inicio del archivo, por lo que si se emplea para escribir, destruiría lo que se hizo vía fd1.

Aquí entra en acción el flag O_APPEND: obsérvese que fcntl(2) es usada en dos ocasiones, la primera para obtener la información actual acerca de las "opciones" del archivo abierto (F_GETFL), lo que proporciona un valor entero status al que se le aplica el flag O_APPEND con una operación "or". Luego, este nuevo estado se "graba" con F_SETFL [117] .

/*
 * append.c: Demostracion de fcntl con el flag
 * O_APPEND
 */
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>

int main()
{
int fd1,fd2,status;

fd1=open("/tmp/append.txt",O_WRONLY|O_CREAT,0666);
fd2=open("/tmp/append.txt",O_WRONLY|O_CREAT,0666);

write(fd1,"xyz\n",4);

/* Setear modo append para fd2 */
status=fcntl(fd2,F_GETFL,0);
status|=O_APPEND;
fcntl(fd2,F_SETFL,status);

write(fd2,"abc\n",4);

close(fd1); close(fd2);
return 0;
}

El resultado del programa:

$ cat /tmp/append.txt
xyz
abc

Fcntl(2) permite modificar otros flags del archivo abierto, pero no los discutiremos aquí.

8.7. Archivos especiales

Como se sabe, el sistema Linux/Unix suele interactuar con los dispositivos de hardware mediante "archivos especiales". Por ejemplo, un disco duro puede ser accesado como /dev/hda, un diskette como /dev/fd0, etc. La mayoría de los dispositivos de hardware transmiten o reciben cierta información, para lo cual el programador normalmente empleará las conocidas llamadas al sistema read(2) y write(2) para leer y escribir, desde y hacia el dispositivo, respectivamente.

Sin embargo, en muchos casos no basta con leer y escribir puesto que los dispositios de hardware suelen ser capaces de realizar otras operaciones. Un ejemplo típico se presenta en el siguiente programa (expulsion.c), el cual solicita la expulsión de un CDROM a la unidad correspondiente. Antes de ejecutarlo, asegúrese de que un CDROM esté dentro de la unidad, y que no esté en uso ni montado como filesystem [118] .

/*
 * expulsion.c: expulsion de CDROM
 */
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

/* Especifica de linux */
#include <linux/cdrom.h>

/* Modificar como convenga */
#define DEVICE "/dev/cdrom"

int main()
{
int fd,i;
fd=open(DEVICE,O_RDONLY);
if(fd==-1)
	{
	perror("No se pudo abrir " DEVICE);
	return 1;
	}
i=ioctl(fd,CDROMEJECT);
if(i==-1)
	{
	perror("No se pudo aplicar ioctl");
	return 1;
	}
return 0;
}

La llamada al sistema ioctl(2) sirve para dar cabida a cualquier operación "no convencional" que va más allá de la lectura/escritura de información. Su primer argumento es el decriptor del archivo abierto (al dispositivo de hardware) mientras que el segundo corresponde a un "comando" aplicado sobre el dispositivo. Dependiendo del "comando", puede requerirse argumentos adicionales en el mismo ioctl. Lamentablemente muchos de estos comandos no están estandarizados y son incompatibles entre diversos sistemas Unix, o Linux’es con kernels muy desfasados. Este ejemplo usa el "comando" CDROMEJECT definido en el header <linux/cdrom.h> [119] . Esto, evidentemente, sólo es aplicable en Linux, debiendo el lector buscar métodos análogos para otros Unix’es.

8.8. Ejercicios

1 Open y permisos

A Investigue la documentación de open(2). Esta llamada al sistema permite crear un nuevo archivo con determinados "permisos" gracias a su tercer argumento. Modifique el write_trivial.c a fin de que el archivo sea creado con permiso "600".

B Es poco probable que sólo con open(2) Ud. pueda generar un archivo con un permiso tan permisivo como 0777. Hay dos maneras de conseguirlo, a saber, 1) modificando previamente al open(2) la "mascara de creación" con la llamada al sistema umask(2), y 2) modificando posteriormente al open(2) los permisos de archivo con la llamada al sistema chmod(2). Investigue estas llamadas al sistema e intente implementar ambos métodos en write_trivial.c.

C A fin de crear un archivo nuevo se ha empleado open(2) con el flag O_CREAT. Investigue la llamada al sistema creat(2) y úsela en write_trivial.c para reemplazar a open.

2 Añadido y eliminación de flags con fcntl(2)

A Modifique write_sync.c a fin de añadir la opción O_APPEND como parte de los flags de open(2). Esto provocará que la información grabada se añada al final del archivo “/tmp/write_sync.txt”. Verifíquelo observando los tamaños del archivo antes y luego de la ejecución.

B Luego vuelva a modificar este mismo programa para mediante fcntl(2) después del open(2), se "desactive" el flag O_APPEND. Verifíquelo observando los tamaños del archivo resultante, antes y luego de la ejecución.

C Investigue la llamada al sistema lseek(2) y úsela para lograr un efecto similar al flag O_APPEND de la parte "A" de este ejercicio.

D De modo análogo a la parte "B" de este ejercicio, intente eliminar el flag O_SYNC. En las versiones actuales de Linux esto no funciona, siendo una limitación. Verifíquelo tomando tiempos a la ejecución. Pruébelo en otros sistemas Unix.

4 Geometría del disco duro

Adapte el programa expulsion.c para leer la geometría de un disco duro (como /dev/hda o lo que convenga según el caso.) Para esto, deberá usar el ioctl: HDIO_GETGEO, el cual requiere como argumento adicional un puntero a una estructura de tipo “hd_geometry”, lo cual está definido en el archivo header <linux/hdreg.h>. Las modificaciones a hacer son:

Archivo header

#include <linux/hdreg.h>

El dispositivo según el caso

#define DEVICE "/dev/hda"

La estructura

struct hd_geometry geom;

El ioctl

i=ioctl(fd,HDIO_GETGEO,&geom);

Ver los resultados

printf("Heads: %u Sectors: %u Cyls: %u\\n", geom.heads, geom.sectors, geom.cylinders);

Compare el resultado de este programa con la salida de fdisk(8):

[root@edithpiaf]# fdisk -l /dev/hda

Disco /dev/hda: 255 cabezas, 63 sectores, 7297 cilindros
Unidades = cilindros de 16065 * 512 bytes
...
[root@edithpiaf]# ./leer_geometria
Heads: 255 Sectors: 63 Cyls: 7297

9. Directorios y Subdirectorios

Los directorios son archivos especiales usados para contener el nombre de otros archivos. Mediante unas pocas funciones sencillas de utilizar, es posible lograr su manipulación.

En este capítulo desarrollaremos dos programas principales, a saber, un "clone" del popular comando DIR del sistema DOS, y un prototipo de "file-manager" (administrador de archivos), ambos con la finalidad de ilustrar la programación con directorios. Este último ejemplo requiere conceptos de la librería curses (capítulo {curses_num}.)

9.1. DIR ala DOS

El siguiente listado presenta una imitación del comando DIR del sistema DOS (al que denominamos xdir.) Esto demuestra el uso típico de las rutinas opendir(), readdir() y _closedir() [120] .

Cuando se abre un directorio con la rutina opendir(), ésta devuelve un puntero a una variable de tipo DIR (definida en dirent.h) que representa un "handler" al directorio abierto.

Una vez abierto el directorio, se procede a leer su contenido. Para esto usamos la función readdir() que recibe el handler obtenido con opendir() y devuelve un puntero a una estructura de tipo dirent (también definida en dirent.h.) En realidad es muy sencillo de usar no obstante la mezcla de los dos punteros.

/*
 * xdir.c: Lee el directorio actual
 */
#include <sys/types.h>
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
DIR *dh;
struct dirent *de;

dh=opendir(".");
if(dh==NULL)
	fprintf(stderr,"No pude abrir directorio actual (%d)\n",errno),exit(1);
while((de=readdir(dh)))
	printf("%s\n",de->d_name);
closedir(dh);
return 0;
}
  • opendir requiere como argumento el nombre del directorio a abrirse

  • Tras la lectura del directorio, éste se debe cerrar con closedir() a fin de no desperdiciar recursos

  • El miembro de la estructura dirent llamado d_name contiene el nombre del archivo actual. En mi sistema, por ejemplo, es de tipo char[256]

9.1.1. La estructura dirent

Con esta estructura se puede obtener información adicional al nombre de cada archivo. En mi sistema, la orden man 3 readdir muestra:

struct dirent {
	long            d_ino;        /* num inodo*/
	off_t           d_off;        /* despl. al siguiente dirent */
	unsigned short  d_reclen;     /* long. de este registro */
	unsigned char   d_type;       /* tipo de fichero */
	char            d_name[256];  /* nombre del fichero */
};

9.2. Dirclone: xdir mejorado

Ahora, basandonos en xdir.c, haremos algunos añadidos. Trataremos de generar un comportamiento un poco más parecido al del original DOS, soportando algunas opciones y argumentos. El programa resultante (dirclone.c) constará de un solo archivo, pero para la explicación lo dividiremos en varios listados más pequeños. El lector deberá simplemente concatenarlos:

cat listado[12345].c > dirclone.c

La primera parte (listado1.c)recogerá las opciones y los argumentos. Leerá los directorios solicitados y almacenará los contenidos:

/*
 * listado1.c (dirclone)
 */
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>

int getdir(int n);
int impresion(int n);
int check_line(int x);
int compara(char **,char**);
void parse_opciones(void);
void sort_dir(void);
char *mayusculas(char *x);

char *directorio[100]; int ndirectorio=0;
char **archivo; int narchivo;
char *opcion[5]; int nopcion=0;

char opt_wide=0, opt_pause=0, opt_sort=0, opt_may=1;

int main(int argc,char **argv)
{
int n=1;

for(;n<argc;n++)
	{
	if(argv[n][0]=='/')
		{
		if(nopcion==5)
			fprintf(stderr,"Demasiadas opciones\n"),exit(1);
		else
			opcion[nopcion++]=strdup(argv[n]);
		}
	else
		{
		if(nopcion==100)
			fprintf(stderr,"Muchos directorios\n"),exit(1);
		else
			directorio[ndirectorio++]=strdup(argv[n]);
		}
	}
parse_opciones();
if(ndirectorio==0)
	directorio[ndirectorio++]=strdup(".");
		
for(n=0;n<ndirectorio;n++)
	{
	printf("\nDirectorio de %s:\n",directorio[n]);
	getdir(n);
	if(opt_sort)
		sort_dir();
	impresion(n);
	}
return 0;
}

Los arreglos de punteros “directorio” y “archivo” contienen respectivamente, la lista de directorios seleccionados por el usuario, y la lista de archivo en el directorio que se está procesando en un momento dado.

Este programa admite las opciones:

  • /W Listado horizontal breve

  • /P Pausa para visualizar por páginas

  • /L No convertir a mayúsculas

  • /O Ordenar en orden alfabético por nombre

Las rutas se deben especificar con backslash (tal como en DOS.) Así por ejemplo, es válido ejecutar:

$ dirclone \\etc\\sysconfig  /W

Nótese que se requiere doble backslash debido al significado especial que tiene este símbolo en el shell.

Finalmente, nuestro dirclone.c aceptará más de un directorio como argumento (cosa que no era posible en el antiguo DIR.)

9.2.1. Procesamiento de opciones

/*
 * listado2.c (dirclone)
 */
void parse_opciones(void)
{
int z,j;

for(z=0;z<nopcion;z++)
	{
	for(j=0;j<strlen(opcion[z]);j++)
		switch(tolower(opcion[z][j]))
			{
			case 'w': opt_wide=1; break;
			case 'p': opt_pause=1; break;
			case 'o': opt_sort=1; break;
			case 'l': opt_may=0; break;
			/* Otras? */
			}
	}
}

Este código es sencillo. Dependiendo de las opciones del usuario se activarán las variables auxiliares opt_*. Nótese que las opciones pueden fundirse (/W /O = /W/O = /WO) y pueden usarse en mayúsculas o minúsculas.

9.2.2. Cargar el directorio

/*
 * listado3.c (dirclone)
 */
int getdir(int nd)
{
int z;
DIR *dh;
struct dirent *de;
char *x=directorio[nd];

for(z=0;z<strlen(x);z++)
	if(x[z]=='\\')
		x[z]='/';
dh=opendir(x);
if(dh==NULL)
	{
	perror("DIRCLONE");
	return -1;
	}
archivo=NULL;
narchivo=0;
while((de=readdir(dh)))
	{
	archivo=realloc(archivo,(narchivo+1)*sizeof(char*));
	archivo[narchivo]=strdup(de->d_name);
	narchivo++;
	}
closedir(dh);
return 0;
}

Aquí leemos las entradas del directorio "nd" (indice al array directorio.) Cada vez que se lee un directorio la variable archivo se inicializa a NULL para que realloc pueda funcionar adecuadamente. El realloc hace crecer al área apuntada por archivo conforme se lee el directorio. Cada entrada se genera haciendo una copia con strdup.

9.2.3. Listado breve

La función de impresión del directorio es algo extensa por lo que la explicaremos en dos partes. La primera corresponde a la impresión abreviada horizontal (opción /W) y la segunda a la impresión lineal normal.

/*
 * listado4.c (dirclone)
 */

#define MORE_LINE l++;if(check_line(l))l=0
int impresion(int nd)
{
int z,na=0,nnd=0;
char path[1024];
struct stat s;
unsigned long total=0;
static int l=0;

MORE_LINE; MORE_LINE;
if(opt_wide)
	{
	int k=0;
	for(z=0;z<narchivo;z++)
		{
		sprintf(path,"%s/%s",directorio[nd],archivo[z]);
		stat(path,&s);
		if(S_ISDIR(s.st_mode))
			{
			char *p=malloc(3+strlen(archivo[z]));
			sprintf(p,"[%.13s]",archivo[z]);
			free(archivo[z]); archivo[z]=p;
			nnd++;
			}
		else
			{
			na++;
			total+=s.st_size; MORE_LINE;
			}
		printf("%-15.15s ",mayusculas(archivo[z]));
		if(k++==4)
			{ k=0; putchar('\n'); MORE_LINE; }
		}
	if(k)
		{ putchar('\n'); MORE_LINE; }
	}
else

Nótese que usamos la llamada al sistema stat(2) para obtener información adicional del archivo, como saber si se trata de un subdirectorio o su tamaño. Para esto, stat recibe un puntero a una estructura tipo stat (asignada por el usuario) que hemos llamado "s". En mi sistema, cuando consulto man 2 stat obtengo información sobre esta estructura:

struct stat
  {
      dev_t         st_dev;      /* dispositivo */
      ino_t         st_ino;      /* inodo */
      mode_t        st_mode;     /* protección */
      nlink_t       st_nlink;    /* número de enlaces físicos */
      uid_t         st_uid;      /* ID del usuario propietario */
      gid_t         st_gid;      /* ID del grupo propietario */
      dev_t         st_rdev;     /* tipo dispositivo (si es
                                    dispositivo inodo) */
      off_t         st_size;     /* tamaño total, en bytes */
      unsigned long st_blksize;  /* tamaño de bloque para el
                                    sistema de ficheros de E/S */
      unsigned long st_blocks;   /* número de bloques asignados */
      time_t        st_atime;    /* hora último acceso */
      time_t        st_mtime;    /* hora última modificación */
      time_t        st_ctime;    /* hora último cambio */
  };

Este manual detalla también el uso de la macro S_ISDIR que permite saber si el archivo es un directorio.

Note también que hemos truncado los nombres de archivo a 15 caracteres (y los [directorios] a 13) mediante la "precisión" del especificador de formato %s de printf, sólo para efectos de visualización.

9.2.4. Listado largo

Aquí también truncamos los nombres de archivo, en este caso, a 20 caracteres.

	{
	
	for(z=0;z<narchivo;z++)
		{
		time_t t;
		struct tm *dt;

		sprintf(path,"%s/%s",directorio[nd],archivo[z]);
		stat(path,&s); t=s.st_mtime;
		dt=localtime(&t);
		if(S_ISDIR(s.st_mode))
			{
			printf("%-20.20s <DIR>      "
			"%.2d/%.2d/%.2d %.2d:%.2d\n",
			mayusculas(archivo[z]),
			dt->tm_mday,dt->tm_mon+1,dt->tm_year-100,
			dt->tm_hour,dt->tm_min);
			MORE_LINE;
			nnd++;
			}
		else
			{
			printf("%-20.20s   %8ld "
			"%.2d/%.2d/%.2d %.2d:%.2d\n",
			mayusculas(archivo[z]),s.st_size,
			dt->tm_mday,dt->tm_mon+1,dt->tm_year-100,
			dt->tm_hour,dt->tm_min);
			total+=s.st_size; MORE_LINE;
			na++;
			}
		}
	};
printf("      %d archivos     %ld bytes\n",na,total);
MORE_LINE;
printf("      %d directorios\n",nnd);
MORE_LINE;
return 0;
}

Obsérvese al final la conversión a la hora en formato dd/mm/yy hh:mm usando la estructura tm y la rutina localtime.

En estos listados se aprecia el uso de nuestra macro llamado MORE_LINE usada para verificar, cada vez que se imprime una línea, si necesitamos hacer una pausa (opción /P.) Nótese que este análisis lo realiza la función check_line (ver abajo) gracias a la variable estática "l" que guarda el número de líneas impresas.

9.2.5. Verificación de líneas impresas

La función check_line es autoexplicativa.

/*
 * listado6.c (dirclone)
 */
int check_line(int x)
{
if(opt_pause==0)
	return 0;
if(x<24)
	return 0;
printf("Presione Enter para continuar . . .");
getchar();
return 1;
}

9.2.6. Ordenación

Hemos empleado la función qsort que requiere de una función auxiliar para hacer las comparaciones (función compara.)

/*
 * listado7.c (dirclone)
 */

int compara(char **x,char**y)
{
return strcmp(*x,*y);
}

void sort_dir(void)
{
qsort(archivo,narchivo,sizeof(char*),
  (int (*)(const void *,const void *))compara);
}

9.2.7. Mayúsculas o minúsculas

Este código evita la conversión a mayúsculas si la opción /L está activada; en caso contrario, convierte cada caracter a mayúsculas con toupper().

/*
 * listado8.c (dirclone)
 */

char *mayusculas(char *x)
{
static char M[1024];
int z;

if(!opt_may)
	return x;
for(z=0;z<strlen(x);z++)
	M[z]=toupper(x[z]);
M[z]='\0';
return M;
}

Esto completa nuestro dirclone.c. Vea los ejercicios para más ideas.

9.3. Prototipo de File-Manager

Este programa es más ambicioso, aunque no muy complejo. Intenta crear la base para un "administrador de archivos" usando la pantalla completa usando Curses/Ncurses. Nuestra versión sólo sirve para navegar entre directorios, pero es relativamente sencillo adaptarla para hacer otras cosas más útiles.

Al igual que en el caso anterior, el programa consta de un solo archivo, cuyas partes se mostrarán consecutivamente.

El primer listado corresponde a la inicialización. Se obtiene el directorio inicial (el actual o el primer argumento de línea de comando) y se inicia el loop sin final de la función run() que es la que hace el trabajo real.

/*
 * listado1.c (mini file manager)
 */
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ncurses.h>
#include <pwd.h>
#include <grp.h>

int run(void);
char *gr(int g);
char *us(int u);

char ruta[1024];

int main(int argc,char **argv)
{
struct stat s;

if(argc>1)
	strcpy(ruta,argv[1]);
else
	strcpy(ruta,".");
if(stat(ruta,&s)==0 && S_ISDIR(s.st_mode))
	{
	int e;
	
	initscr();
	keypad(stdscr,TRUE);
	for(;;)
		if((e=run()))
			break;
	endwin();
	if(e==-1)
		{ perror("filemanager"); exit(1); }
	return 0;
	}
else
	fprintf(stderr,"Especifique una ruta valida.\n"),exit(1);
}

9.3.1. Almacenamiento del directorio

La función run efectúa la apertura y almacenamiento dinámico del contenido del directorio (en el puntero archivo) y luego interactúa con el usuario. Aquí mostramos sólo la primera parte.

Obsérvese que hemos cambiado el directorio actual del proceso (chdir) y luego hallamos la ruta absoluta usando la ruitina getcwd. Esto nos evita tener que analizar rutas relativas y casos especiales (como el "..".)

/*
 * listado2.c (mini file manager)
 */
int run(void)
{
DIR *dh;
struct dirent *de;
int narchivo=0,pos=0,offset=0,z,k;
char **archivo=NULL;
struct stat s;

if(chdir(ruta)==-1)
	return -1;
if(getcwd(ruta,1024)==NULL)
	return -1;

dh=opendir(ruta);
if(dh==NULL)
	return -1;
while((de=readdir(dh)))
	{
	archivo=realloc(archivo,(narchivo+1)*sizeof(char*));
	archivo[narchivo]=strdup(de->d_name);
	narchivo++;
	}
closedir(dh);

9.3.2. Interacción con el usuario

Esta interacción se da en un loop infinito que muestra el directorio (o parte de éste), recepciona los comandos del usuario por el teclado, y los procesa.

Dejamos el procesamiento para el siguiente listado.

/*
 * listado3.c (mini file manager)
 */

for(;;)
	{
	static char FDIR[257];
	
	clear();
	printw("DIRECTORIO:%s\n\n",ruta);
	for(z=0;z<21;z++)
		{
		if(offset+z>=narchivo)
			break;
		if(offset+z==pos) attron(A_REVERSE);
		stat(archivo[offset+z],&s);
		if(S_ISDIR(s.st_mode))
			{
			attron(A_BOLD);
			sprintf(FDIR,"%s/",archivo[offset+z]);
			printw(" %-30.30s",FDIR); attroff(A_BOLD);
			}
		else
			printw(" %-30.30s",archivo[offset+z]);
		if(offset+z==pos) attroff(A_REVERSE);
		printw("   %4o  %-8s  %-8s\n",
		s.st_mode&0777,us(s.st_uid),gr(s.st_gid));
		}	
	move(pos-offset+2,31);
	k=getch();

Nótese que hacemos uso de las características de video invertido y video brillante del terminal para el archivo actual y para los directorios, respectivamente. Además, la variable FDIR se usa para guardar el nombre de los directorios con un slash añadido (/) al final. Por otro lado, mostramos los permisos en formato octal mientras que los usuarios y grupos propietarios se obtienen en funciones aparte (us y gr.)

9.3.3. Respondiendo al usuario

Esto termina el contenido de la función run():

/*
 * listado4.c (mini file manager)
 */

	switch(k)
		{
		case KEY_DOWN:
			if(pos<narchivo-1) pos++;
			if(pos-offset>20) offset++;
			break;
		case KEY_UP:
			if(pos>0) pos--;
			if(offset>pos) offset--;
			break;
		case '\n':
			stat(archivo[pos],&s);
			if(S_ISDIR(s.st_mode))
				{
				sprintf(ruta,"%s/%s",ruta,archivo[pos]);
				for(z=0;z<narchivo;z++)
					free(archivo[z]);
				free(archivo);
				return 0;
				}
			break;
		case 033:
			return 1;
		}
	}
return 0;
}

El único caso interesante se da cuando el usuario presiona [ENTER] sobre un directorio. Esto crea un nuevo PATH al cual se reingresa con chdir, para lo cual se retorna a main con valor cero. Antes de esto se limpia la memoria asociada a archivo.

La tecla [ESCAPE] (código 033) permite terminar la aplicación.

9.3.4. Obtener nombres de usuarios y grupos

Las funciones us y gr ilustran el uso de las rutinas getpwuid y getgrgid que requieren de punteros a estructuras especiales definidas en pwd.h y grp.h.

/*
 * listado5.c (mini file manager)
 */
char *us(int u)
{
struct passwd *usp;
usp=getpwuid(u);
return usp->pw_name;
}

char *gr(int g)
{
struct group *grp;
grp=getgrgid(g);
return grp->gr_name;
}

9.4. Ejercicios

1 Correcciones a DIRCLONE

A) DIRCLONE tiene una falla de memoria que debe ser corregida: cada vez que se procesa un directorio, se asigna memoria para el puntero índice archivo y para cada uno de los strings (nombres de archivo) asociados a los elementos de éste. Cuando se especifica más de un directorio a listarse, esta memoria se reasigna para los nuevos elementos, pero nunca se borra la anteriormente utilizada.

B) Cuando DIRCLONE recibe un archivo como argumento (y no un directorio), inocentemente intenta listarlo provocando un error. Observe el comportamiento del comando DIR del DOS para este caso e impleméntelo.

C) Cuando DIRCLONE recibe una opción inválida, no emite ningún mensaje. Analice el comportamiento del DIR original en este caso. Si desea, puede implementar la opción de ayuda /?.

D) El original DIR en el listado largo, separa el nombre de la extensión en columnas separadas. Impleméntelo.

2 Listado recursivo

Extienda DIRCLONE para que acepte una nueva opción "/S" que permita listar recursivamente todos los subdirectorios contenidos a partir del especificado.

3 Ordenamiento mejorado

Si revisa la ayuda del DIR original, notará que es posible ordenar el listado de diversos modos (opciones /ON, /OE, /OS, etc.)

4 File manager útil

El mini file manager sólo permite navegar por los directorios del sistema. Implemente los siguientes comandos extendiendo el switch…​case…​ que procesa las teclas recibidas:

A) La tecla [ENTER] puede servir para ejecutar un archivo si éste es ejecutable (consultar el manual de stat(2) para más información.) En caso contrario se puede invocar al editor "vi" para editarlo.

B) La tecla [F5] puede usarse para "refrescar" el listado que se está visualizando (por ejemplo, si se sospecha que algunos archivos han sido creados/borrados/alterados.)

C) La tecla [DELETE] puede usarse para borrar un archivo. El programa debería pedir confirmación.

5 Extras para el file manager

A) Imprima una barra de ayuda donde se describan los comandos disponibles.

B) Añada la fecha de modificación al listado para cada archivo. Puede usarse un formato compacto como en el caso de DIRCLONE.

C) Genere una caja que encierre toda la aplicación a fin de darle mejor aspecto.

D) En el ejercicio anterior, parte "A", en lugar de editar un archivo con "vi" por el hecho de no ser ejecutable, conviene saber si realmente es de texto (caso contrario "vi" no ayudará mucho.) Una forma rápida de hacer esto es invocando a las utilidades file y grep y usar el valor de retorno. Por ejemplo:

file /etc/passwd | grep text > /dev/null
echo $?
0
file /bin/cp | grep text > /dev/null
echo $?
1

La línea de comandos "file %s | grep text > /dev/null" se puede usar como entrada para la rutina system(), que la ejecutará con ayuda del shell. Consulte el manual de esta rutina para indagar por el valor de retorno.

6 Ordenar archivos por fechas [121]

El listado obtenido mediante ls permite ordenar los archivos de uno o más directorios por fechas de modificación (opción -t.) Sin embargo, cuando se desea buscar los últimos archivos modificados en una estructura de directorios esto no sirve. Incluso con la opción -R (listdo recursivo) el ordenamiento sólo se refiere a los archivos dentro de cada directorio.

Se pide crear un programa que reciba como argumentos el nombre de uno o más directorios, y que ordene por fecha a todos los archivos contenidos en éstos (incluso en sus subdirectorios.) Se puede usar qsort() para efectuar el ordenamiento. El programa arrojará un listado ordenado con las fechas de modificación de los archivo, y sus respectivos nombres.

6 Construya un programa utilitario llamado "weirdfiles", el cual analiza los archivos con nombres extraños de un directorio y permite al usuario renombrarlos interactivamente. Los nombres extraños son aquellos que contienen caracteres no imprimibles, comodines del shell, espacios y un guión (dash) como primer caracter. Añada una opción (-a) para que el renombramiento sea automático (no interactivo.)

$ ./weirdfiles -a .
Se renombro 20 archivos con nombres problematicos.
$ ./weirdfiles directx
Archivo Klima\0x98kito*5 tiene nombre problematico.
Nuevo nombre? Klimaquito_5
$

10. Bases de datos en archivos indexados

10.1. Introducción

Es frecuente la necesidad de archivar información de manera eficiente, cómoda y segura. Los sistemas de bases de datos intentan satisfacer estos requerimientos de distintas formas y bajo diversas estrategias. Por ejemplo, los llamados DBMS (database management systems) suelen ser complejos programas (muchas veces con servidores dedicados) que proporcionan un increible espectro de funciones para usuarios y aplicaciones.

Sin embargo, también es común que los programas requieran almacenar un conjunto de datos sin depender de un sistema auxiliar tipo DBMS. Por ejemplo, un pequeño programa que guarde el catálogo de discos de mi abuelo no debería requerir la adquisición, configuración y mantenimiento de un (gigante) sistema DBMS. En esos casos los programadores pueden recurrir a la creación de archivos "planos" cuyo formato específico es totalmente arbitrario.

Sin embargo, cuando el volumen de información crece, esto deja de ser eficiente y seguro.

A tal efecto existen librerías especializadas que ayudan a los programadores a crear pequeñas bases de datos (que se suelen denominar de "archivos indexados") que son un punto intermedio entre los dos escenarios que hemos descrito.

Estas bases de datos no requieren de la instalación de programas adicionales (como es el caso de un sistema DBMS) y reducen los problemas mencionados para los archivos "arbitrarios".

Note
Una estrategia adicional consiste en utilizar una base de datos "embedded", la cual combina gran parte de la funcionalidad de un DBMS con la simplicidad de los archivos indexados. El caso más conocido en la actualidad es SQLite https://www.sqlite.org/index.html .

Es digno de resaltar que la "velocidad" del almacenamiento "de archivo indexado" es en muchos casos superior a la que se alcanza con un sistema DBMS. Por otro lado, emular esta performance programáticamente es francamente una pérdida de tiempo, y una puerta abierta a la introducción de bugs.

10.1.1. dbm/ndbm

En los sistemas Unix comerciales existe una librería estándar para crear bases de datos de "archivo indexados" llamada dbm o ndbm. Esta librería es relativamente antigua y si bien existen opciones superiores, todavía es posible encontrar programas que la emplean.

10.1.2. GNU dbm (Gdbm)

Esta librería es un reemplazo bastante similar a dbm/ndbm, pero "abierto". Está distribuida bajo la licencia GPL (versión 2.)

Posee funciones de compatibilidad que permiten operar sobre archivos dbm/ndbm.

10.1.3. Berkeley DB

Esta librería posee un diseño más sofisticado, así como API’s para lenguaje C, C++ y Java. Se distribuye mediante una licencia bastante liberal por Sleepycat Software. Tiene un modo de compatibilidad con dbm aunque no puede usar archivos ya creados con esa librería.

En lo que sigue usaremos Gdbm, pero los conceptos son similares para los otros casos.

Note
En Ubuntu 22.04 hemos empleado el comando sudo apt-get install libgdbm6 libgdbm-dev para instalar la librería Gdbm.

10.2. Un diccionario electrónico

El programa que presentamos a continuación simplemente almacena la definición de las palabras que se le proporcionan.

/*
 * Listado 1: Diccionario electronico basico
 */
#include <gdbm.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void consulta(void); void nuevo(void);

GDBM_FILE f;
char L[80];

int main(int argc,char **argv)
{
f=gdbm_open("diccionario.data",0,GDBM_WRCREAT,0600,NULL);
if(f==NULL) fprintf(stderr,"Fallo gdbm_open\n"),exit(1);

while(1)
	{
	printf("1- consulta, 2- ingresar nuevo, 3- salir > ");
	fgets(L,80,stdin);
	if(L[0]=='1') consulta();
	if(L[0]=='2') nuevo();
	if(L[0]=='3') break;
	}
gdbm_close(f);
return 0;
}

void consulta(void)
{
datum palabra,descripcion;

printf("Palabra a consultar? ");
fgets(L,80,stdin); L[strlen(L)-1]=0;
palabra.dptr=strdup(L);
palabra.dsize=strlen(L);
descripcion=gdbm_fetch(f,palabra);
if(descripcion.dptr==NULL)
	printf("No se encontró esa palabra\n");
else
	{
	strncpy(L,descripcion.dptr,descripcion.dsize);
	L[descripcion.dsize]=0;
	printf("Descripcion: %s\n",L);
	free(descripcion.dptr);
	}
}

void nuevo(void)
{
datum palabra,descripcion;

printf("Palabra a ingresar? ");
fgets(L,80,stdin); L[strlen(L)-1]=0;
palabra.dsize=strlen(L); palabra.dptr=strdup(L);
printf("Definicion? ");
fgets(L,80,stdin); L[strlen(L)-1]=0;
descripcion.dsize=strlen(L); descripcion.dptr=strdup(L);
if(gdbm_store(f,palabra,descripcion,GDBM_REPLACE)!=0)
	printf("Error en gdbm_store\n");
free(palabra.dptr);
free(descripcion.dptr);
}

La compilación requiere la inclusión de la librería Gdbm (-lgdbm), por lo que mi Makefile luce así:

CFLAGS:=-Wall
LDFLAGS:=-lgdbm

all: diccionario

clean:
	rm -f *.o diccionario

Un ejemplo de uso:

$ ./diccionario
1- consulta, 2- ingresar nuevo, 3- salir > 2
Palabra a ingresar? jirafa
Definicion? animal de cuello largo
1- consulta, 2- ingresar nuevo, 3- salir > 2
Palabra a ingresar? tucan
Definicion? animal politico de colores
1- consulta, 2- ingresar nuevo, 3- salir > 1
Palabra a consultar? jirafa
Descripcion: animal de cuello largo
1- consulta, 2- ingresar nuevo, 3- salir > 3
$

El programa es muy fácil de seguir. Algunos aspectos interesantes:

10.2.1. Conexión a la base de datos

La "conexión" a la base de datos se hace con gdbm_open(). Esta requiere:

  1. El nombre del archivo de base de datos (con cualquier extensión)

  2. un valor numérico relacionado al buffer interno de la librería (en nuestro caso, "cero" hace que la librería asigne automáticamente uno conveniente)

  3. Un identificador que señala el tipo de operaciones que haremos con la base de datos: GDBM_WRCREAT permite crear la base de datos (si no existe), leer y escribir [122]; GDBM_READER sólo permite lecturas. Lamentablemente no es posible que dos procesos actúen como escritores en la misma base de datos a la vez.

  4. El modo (permisos) con que se crea el archivo de base de datos, si éste todavía no existe. En nuestro caso hemos dado lectura y escritura sólo al propietario.

  5. Un puntero a una función que se ejecutará en caso de un problema grave (fatal.) Si se usa NULL, Gdbm la proporciona.

El valor retornado por esta rutina es de tipo GDBM_FILE, y deberá ser usado a lo largo del programa para las operaciones con la base de datos.

10.2.2. Los datos en Gdbm

Todo lo que se graba en la base de datos está indexado por una "clave". Por ejemplo, en un diccionario, todo el contenido está indexado por las palabras definidas; en una avenida, los solares están indexados por un número asociado a las viviendas; en un supermercado, todos los productos suelen tener un código numérico (especialmente si usan lectoras de códigos de barras.)

La idea principal de todo esto es que si se quiere acceder a un dato, hay que hacerlo mediante la clave.

En GDBM, tanto la clave como los datos referenciados por ella, pueden ser cualquier tipo de información y pueden tener cualquier tamaño.

El tipo de datos datum es una estructura que se usa para almacenar tanto las claves como los datos referenciados. datum es una esctructura con dos miembros:

  • dptr, un puntero a char, que referencia al dato

  • dsize, un entero que señala el tamaño del dato almacenado en dptr

En nuestro programa, estamos interesados en almacenar sólo textos (sin el caracter nulo del fin de cadena). Nuestra clave es una palabra leída en el array “L”, así que la "cargamos" en un datum llamado "palabra" así:

palabra.dptr=strdup(L);
palabra.dsize=strlen(L);

Nótese que esto sólo sirve para hacer la búsqueda, y luego nosotros debemos liberar la memoria asignada con strdup() manualmente (con free()) o el programa nunca parará de elevar su consumo de memoria.

Como se aprecia en la funcion consulta(), la rutina gdbm_fetch() se usa para recibir un datum que contiene la respuesta (que he llamado llamado "descripcion".)

El miembro "dptr" de "descripción" contiene la información buscada, o NULL en caso de que no exista aquella. Por eso la copiamos (con strncpy()) a un array que podamos manipular (nuestro array auxiliar “L”) asegurándonos de terminarlo en NUL para poder imprimirlo sin sorpresas.

La información "dptr" también debe ser liberada manualmente por nosotros (la librería GDBM la asigna en memoria con malloc() pero nunca la libera.)

La escritura es sencilla si se ha comprendido lo anterior. Notar el último parámetro de gdbm_store() que corresponde al flag GDBM_REPLACE, que pudo ser también GDBM_INSERT. En el primer caso, si se trata de escribir con una clave ya existente, la librería simplemente sobreescribe el dato (sin avisar) mientras que en el segundo no hace nada y retorna 1.

En caso de error, esta rutina retorna -1.

10.3. Locks, cerrojos o candados

Un aspecto crítico en las bases de datos está referido a la consistencia de los datos que observan los lectores y escritores de la misma. Imaginemos el siguiente escenario para una compañía de reserva aerea:

  • El asiento 60-A del vuelo 900 Lima-Quincemil de hoy es el único que queda disponible. Esta situación se puede apreciar en todos los terminales de venta de la empresa aerea

  • Pedro K. paga el costo del pasaje y el operador solicita en el computador que el asiento 60-A quede asignado a este buen señor

  • Al mismo tiempo, Pablo J., que está enterado de la misma información en otro punto de venta, paga el costo del pasaje y el operador realiza la misma operación

  • El sistema central recibe la ambas ordenes, casi simultáneamente, y puesto que el asiento 60-A está libre, simultáneamente escribe el registro correspondiente (quizá con una mezcla confusa de los datos de Pedro K. y Pablo J.)

  • Pedro y Pablo se enfrentan a la tripulación de la nave

Si pensamos un poco, el problema radica en el paso cuatro, en el sentido de que es posible que dos procesos escriban información en el mismo lugar (el mismo registro de la base de datos correspondiente al asiento 60-A.) Debe haber un mecanismo que impida esto, y de hecho los "cerrojos" o "candados" (locks) tienen esta función. Con los cerrojos, sólo un proceso puede escribir (o leer) uno o más registros a la vez.

En el ejemplo anterior, los procesos antes de escribir deben solicitar un cerrojo sobre la base de datos. Aunque las reservas han llegado casi al mismo tiempo, el sistema operativo siempre asignará secuencialmente los cerrojos a los procesos a fin de que nunca haya simultaneidad.

Siguiendo con el ejemplo, supondremos que el sistema operativo asigna el primer cerrojo a la solicitud de Pedro K. (que llegó un milisegundo antes que la de Pablo J.) Entonces, asignado el cerrojo, el sistema debera volver a verificar que el registro está libre, y en este caso, ecribirá la información. Acto seguido, liberará el cerrojo.

Por otro lado, la solicitud de Pablo J. no recibe el cerrojo (pues ya fue asignado a Pedro K.) y el sistema procede a indicarle que lamentablemente el asiento acaba de ser vendido [123] .

10.3.1. Locks en Gdbm

Gdbm no presenta muchas ventajas en cuanto a los cerrojos. Normalmente, Gdbm sólo permite que un proceso que accede a la base datos escriba en ella (es decir, que abra la base de datos con GDBM_WRCREAT):

Gdbm crea un cerrojo sobre todo el archivo cuando éste se abre con GDBM_WRCREAT. Un único "cerrojo de escritor".

Por ejemplo, nuestro diccionario no puede ser ejecutado por más de un usuario a la vez sobre la misma base de datos. Pruébelo Ud. mismo usando dos terminales.

Una solución a esto corresponde a hacer una versión del programa distinta, apta sólo para "lectores" del diccionario. En ese caso, basta con cambiar el flag GDBM_WRCREAT por GDBM_READER en la apertura de la base de datos:

f=gdbm_open("diccionario.data",0,GDBM_READER,0600,NULL);

Los procesos que abren la base de datos con GDBM_READER no pueden usar las funciones que modifican la base de datos (éstas simplemente no hacen nada.)

Otra forma algo mejor, consiste en preguntar a los usuarios si desean actuar como escritores (además de lectores.) Según su respuesta, se reemplazará el flag GDBM_WRCREAT por GDBM_READER en la apertura. Quizá podría pedirse un password a los escritores. Creo que esto no es difícil de implementar y se deja como ejercicio.

10.3.2. Locks manuales

Supongamos que de todos modos deseamos que todos los usuarios escriban en el diccionario. Si bien Gdbm no nos ayuda en esto, al menos nos da algunas salidas.

Una forma forzada pero ilustrativa de lograr esto, consiste en abrir la base de datos en modo escritura (gdbm_open()) sólo en el momento de hacer una modificación, y cerrar la base de datos inmediatamente. Como estas operaciones suelen ser veloces, es baja la probabilidad de que dos procesos pretendan escribir en simultáneo en la base de datos (una "colisión"), con el consiguiente error. Pero si obtenemos un error, podríamos simplemente esperar unos momentos (hacer un retardo), y volver a intentar abrir la base de datos hasta obtener el lock de escritor.

El problema de esto es que si tenemos muchos usuarios escritores, y una base de datos grande (más lenta), la probabilidad de "colisión" aumenta. Para evitar esto, nos veríamos obligados a reducir el "retardo" a fin de que los procesos intenten con más frecuencia obtener el lock de escritor; pero esto puede conllevar a una sobrecarga innecesaria y perjudicial en la performance sistema. Con todo, puede ser una solución de baja escalabilidad pero muy interesante, por lo que también se deja como ejercicio.

Veamos ahora otra solución más satisfactoria. Aquí haremos una sola apertura a la base de datos con el flag GDBM_WRCREAT para poder tener acceso a las funciones de escritura, pero además especificaremos el flag GDBM_NOLOCK, que evita que Gdbm ponga el cerrojo de escritor a la base de datos.

Si bien nuestro programa de diccionario no se beneficia mucho con los cerrojos, de todos modos trataremos de hacer las escrituras con una "reserva" previa. Es decir, ahora nosotros debemos poner nuestros propios cerrojos.

Ahora, antes de hacer cada escritura podríamos poner un cerrojo en modo "bloqueo" para:

  • Reservar la base de datos a fin de que sólo el proceso puede escribir

  • Si otro proceso ha hecho la reserva, nuestro proceso espera (se bloquea) a que se libere el otro cerrojo

La ventaja aquí es que el sistema operativo automáticamente detiene el proceso que quiere escribir sólo por el tiempo necesario.

Esto constrasta con el comportamiento de gdbm_open() que en caso de colisión no se bloquea sino que retorna inmediatamente con un código de error [124].

La siguiente versión del programa permite la escritura "segura" y simultánea por varios usuarios en el diccionario. Nótese que son muy pocas las modificaciones con respecto a la versión anterior:

  1. Se añade el header sys/file.h para flock() y el unistd.h para sleep()

  2. Se abre la base con GDBM_NOLOCK

  3. Se ejecuta flock(fd,LOCK_EX) antes de la escritura y flock(fd,LOCK_UN) después de.

/*
 * Listado 2: Diccionario con multiples escritores
 */
#include <gdbm.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/file.h>

void consulta(void); void nuevo(void);

GDBM_FILE f;
char L[80];

int main(int argc,char **argv)
{
f=gdbm_open("diccionario.data",0,GDBM_WRCREAT|GDBM_NOLOCK,0600,NULL);

... EL RESTO DE main() NO CAMBIA
}

void consulta(void)
{
... consulta() NO CAMBIA EN LO ABSOLUTO
}

void nuevo(void)
{
datum palabra,descripcion;
int fd;

printf("Palabra a ingresar? ");
fgets(L,80,stdin); L[strlen(L)-1]=0;
palabra.dsize=strlen(L); palabra.dptr=strdup(L);
printf("Definicion? ");
fgets(L,80,stdin); L[strlen(L)-1]=0;
descripcion.dsize=strlen(L); descripcion.dptr=strdup(L);
fd=gdbm_fdesc(f);
printf("A punto de aplicar cerrojo...\n");
flock(fd,LOCK_EX);
if(gdbm_store(f,palabra,descripcion,GDBM.REPLACE)!=0)
	printf("Error en gdbm_store\n");
printf("Hecho! ahora duermo 10s...\n"); sleep(10);
flock(fd,LOCK_UN);
free(palabra.dptr);
free(descripcion.dptr);
}

Adicionalmente se ha añadido un retardo de 10 segundos para que el archivo permanezca más tiempo bloqueado. Esto sólo se ha hecho por fines didácticos: el usuario deberá ahora tratar de iniciar el programa en dos terminales distintos y debe intentar registrar una nueva palabra casi al mismo tiempo. Si observa con cuidado los mensajes, notará que en uno de los casos el programa hace la escritura y duerme los 10 segundos antes de liberar el cerrojo, mientras que el otro tiene que esperar a que todo esto termine para recién aplicar su cerrojo, escribir, y volver a esperar otros 10 segundos.

Sólo cuando haya apreciado todo esto, elimine los mensajes innecesarios y el retardo.

10.4. Búsquedas en la base de datos

Gdbm no presenta facilidades para efectuar búsquedas. Lo único que podemos obtener es un listado con todo el contenido de la base de datos (sin ningún orden en particular) con el cual implementaremos esta funcionalidad.

Este listado total lo obtenemos con las rutinas gdbm_firstkey() y gdbm_nextkey() con un loop como lo que sigue:

datum key,nextkey,data;
	...
	key=gdbm_firstkey(f);
	while (key.dptr)
		{
		data=gdbm_fetch(f,key);

		// Hacer algo con (key,data)

		free(data.dptr);
		nextkey=gdbm_nextkey(f,key);
		free(key.dptr);
		key=nextkey;
		}

En realidad, las rutinas mencionadas permiten obtener un listado total de todas las claves de la base de datos; he añadido el datum "data" que recoge la información referenciada por estas claves.

Como se aprecia, el datum retornado por las funciones mencionadas puede apuntar a NULL para indicar que no hay registros que leer; además, en estas funciones Gdbm usa malloc() para la información retornada en *.dptr, por lo que se debe liberar manualmente con free().

Ahora trataremos de aprovechar esto para hacer una búsqueda en el diccionario. El usuario escribirá un texto y el programa buscará las entradas en don de ese texto esté contenido ya sea en la palabra-clave o en la definición. En realidad es muy fácil:

1- consulta, 2- nuevo, 3- busqueda 4- salir > 3
Texto a buscar? rafa
jirafa: animal de cuello largo
1- consulta, 2- nuevo, 3- busqueda 4- salir >

El programa:

/*
 * Listado 3: Diccionario con busquedas
 */
#include <gdbm.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/file.h>

void consulta(void); void nuevo(void); void busqueda(void);

GDBM_FILE f;
char L[80];

int main(int argc,char **argv)
{
f=gdbm_open("diccionario.data",0,GDBM_WRCREAT|GDBM_NOLOCK,0600,NULL);
if(f==NULL) fprintf(stderr,"Fallo gdbm_open\n"),exit(1);
while(1)
	{
	printf("1- consulta, 2- nuevo, 3- busqueda 4- salir > ");
	fgets(L,80,stdin);
	if(L[0]=='1') consulta();
	if(L[0]=='2') nuevo();
	if(L[0]=='3') busqueda();
	if(L[0]=='4') break;
	}
gdbm_close(f);
return 0;
}

void consulta(void)
{ ... NO CAMBIA ... }

void nuevo(void)
{ ... NO CAMBIA ... }

void busqueda(void)
{
datum key,nextkey,data;
char t_palabra[80],t_descripcion[80];

printf("Texto a buscar? ");
fgets(L,80,stdin); L[strlen(L)-1]=0;

key=gdbm_firstkey(f);
while (key.dptr)
	{
	data=gdbm_fetch(f,key);

	strncpy(t_palabra,key.dptr,key.dsize);
	t_palabra[key.dsize]=0;
	strncpy(t_descripcion,data.dptr,data.dsize);
	t_descripcion[data.dsize]=0;
	if(strstr(t_palabra,L)!=NULL ||
		strstr(t_descripcion,L)!=NULL)
		printf("%s: %s\n",t_palabra,t_descripcion);

	free(data.dptr);
	nextkey=gdbm_nextkey(f,key);
	free(key.dptr);
	key=nextkey;
	}
}

Los datos recogidos en (key,data) son llevados a los strings t_palabra y t_descripcion a fin de que sean terminados en NUL; y como ya debe saber el lector, buscar si un texto está dentro de otro es bastante sencillo con la conocida rutina strstr() que no explicaremos aquí.

10.5. Ejercicios

1 Implemente el diccionario de modo tal que todos los usuarios puedan escribir, pero sin hacer uso de flock(2). Para esto, el programa abrirá la base de datos en modo GDBM_READER (para procesar las lecturas), y justo en el momento de la escritura, la reabrirá en modo GDBM_WRCREAT (no se necesita cerrar la apertura inicial.) En caso de fallar esta apertura, el programa debe reintentar abrirla tras un "prudente" retardo. Una vez culminada la escritura, debe cerrarla para que otros procesos tengan la misma oportunidad.

Intente simular que la escritura toma mucho tiempo (digamos, 10s) añadiendo un sleep(10) justo antes de cerrar la base de datos, de modo tal que se pueda apreciar el efecto de bloqueo-reintento usando dos terminales en los que el programa intenta escribir al mismo tiempo.

2 En lugar de emplear el descriptor de la base de datos, implemente el bloqueo con un archivo auxiliar. Para esto, simplemente cree un archivo (no interesa el contenido) llamado por ejemplo, diccionario.lock, y abra un descriptor al mismo con open(2). Si todos los programas crean el cerrojo hacia el mismo archivo, entonces el efecto es el mismo.

Esta técnica es muy usada para sincronizar procesos que interactuan, independientemente de que se trabaje con bases de datos.

3 Mejore el diccionario añadiendo un campo adicional a la descripción del texto: haga que en cada escritura se guarde además la fecha y hora en que se realiza. Una forma muy simple de hacer esto es definir un delimitador que separe la descripción de cada palabra de su fecha y hora de registro. Por ejemplo, en lugar de guardar:

clave dato

jirafa

animal de gran altura

Se puede guardar algo como:

clave dato

jirafa

animal de gran altura%01/01/04 12:33

Ud. debe utilizar un caracter poco empleado (como el signo de porcentaje) para este fin. Nótese que al nivel de Gdbm no habría absolutamente ningún cambio que realizar.

4 Adapte el programa diccionario para que el computador genere N > 1000 palabras aleatorias con sus correspodientes definiciones (usando las letras aleatoriamente) y las almacene con Gdbm. Con esto obtendremos una base de datos relativamente grande.

Note
De preferencia N debe ser un argumento de línea de comandos a fin de permitir parametrizar valores más grandes que permitan "exigir" al computador.

Paralelamente, este programa deberá escribir un archivo de texto con las palabras y las definiciones, todas delimitadas por el fin de línea. Este archivo tiene la ventaja de ser fácilmente legible y editable por las personas:

ajsfdluwe
nvkl lkwluc ocnwiquoiw
jnsalkdas
kjdf wduiut hcajsd qwuiqwehqwu

Luego haga un pequeño programa que simplemente busque una palabra existente en el diccionario, y tómele tiempo (T_DBM_1.)

Y luego, que busque una palabra NO existente en el diccionario; tómele tiempo (T_DBM_2.)

Luego haga otro pequeño programa (que no tiene nada que ver con Gdbm) que busque secuencialmente alguna palabra existente en el archivo de texto, y tómele tiempo (T_TXT_1.)

Y por último, que busque una palabra NO existente en el archivo de texto (T_TXT_2.)

Compare todos los tiempos para diferentes valores de N.

PROGRAMACION INTERACTIVA

11. Programando el terminal con Curses

Cuando se utilizan terminales o consolas en modo texto (o emuladores de terminal) y se necesita acceder a todas las zonas de la pantalla, es frecuente el empleo de la librería "curses" o su clone "ncurses", disponibles en los sistemas Unix y Linux.

11.1. Algunas Nociones

La interacción con los terminales en modo texto suele hacerse línea a línea de modo secuencial. Sin embargo, es frecuente que las aplicaciones requieran utilizar áreas arbitrarias de la pantalla para lograr un efecto visual más apropiado. A tal efecto, los terminales reconocen e interpretan ciertas combinaciones de caracteres especiales como "comandos". En otras palabras, estos caracteres en lugar de imprimirse en la pantalla, ordenan al terminal que adquiera cierto comportamiento inusual, tal como cambiar el color del texto, hacer saltar el cursor, etc.

Estas combinaciones de caracteres especiales se conocen como "secuencias de escape", y varían con cada tipo de terminal utilizado, por lo que no es recomendable su utilización en forma directa. Precísamente, la librería "Curses" se desarrolló con el fin de abstraer esta complejidad y proporcionar al programador una interface estandarizada para enviar comandos a cualquier tipo de terminal sin preocuparse por las secuencias de escape.

La librería "Curses" no es de libre distribución, por lo que Linux no la proporciona. En su lugar se provee la librería "Ncurses" [125] , la cual emula la funcionalidad de "Curses" y la supera en algunos aspectos. Aquí usaremos la palabra "Curses" para designar ambas.

En lo que sigue presentaremos algunos ejemplos típicos; luego elaboraremos un "juego interactivo", y terminaremos con la implementación de una rutina genérica de menú.

11.2. Iniciando Curses

El listado "basico.c" presenta un programa bastante sencillo que solicita un texto al usuario y luego lo imprime en otra posición de la pantalla.

/*
 * basico.c: Programa basico
 */
#include <ncurses.h>
#include <unistd.h>

char buf[81];

int main()
{
    initscr();
    mvaddstr(5, 10, "Escribe tu nombre: ");
    getnstr(buf, 80);
    mvprintw(10, 4, "Tu nombre es: %s...", buf);
    refresh();
    sleep(3);
    endwin();
    return 0;
}

A continuación una serie de consideraciones relevantes:

  • Los programas que usan Curses deben incluir el header <curses.h>. En el caso de Linux, si usamos Ncurses, incluiremos <ncurses.h> aunque suele proporcionarse un enlace simbólico llamado <curses.h> con lo que resulta indistinto

  • La primera rutina Curses a utilizarse deberá ser initscr(), la cual inicializa Curses y borra la pantalla

  • Para imprimir texto hemos usado la rutina mvaddstr(). Esta rutina permite posicionar el cursor para la impresión. El primer argumento es la posición vertical (desde el margen superior) y el segundo la posición horizontal (desde el margen izquierdo.) Ambos se inician en cero

  • Una rutina relacionada es addstr(), que sirve para imprimir texto en la posición actual del cursor. Análogamente, existen las funciones addch() y mvaddch() para imprimir un caracter a la vez. Para imprimir sólo debemos usar rutinas Curses y no debemos combinarlas con rutinas de la librería estándar como printf()

  • Para leer una línea de texto hemos usado la rutina getnstr(). No se debe combinar con otras rutinas de lectura de la librería estándar como fgets()

  • Para imprimir un texto con un formato similar al de printf(), se puede usar las rutinas printw() y mvprintw() como demuestra el ejemplo

  • Normalmente, nada de lo que se imprime aparece en la pantalla hasta que se ejecuta la rutina refresh() o hasta que se invoca a cualquier rutina Curses de lectura de texto (como nuestro getnstr().) El lector deberá probar la ejecución del programa sin la invocación a "refresh" para notar la diferencia

  • Todos los programas antes de terminar deberían ejecutar la rutina endwin() que desactiva la configuración Curses y reestablece el terminal para el shell

Para compilar un programa Curses, se deberá especificar el enlace a esta librería. Por ejemplo:

cc -o prog1 prog1.c -lcurses

En sistemas que no usan "curses" sino "ncurses", se deberá usar “-lncurses”.

Un requisito fundamental para ejecutar correctamente una aplicación que usa Curses es que la variable de entorno “TERM” esté correctamente configurada. Esto debería ser consultado al administrador del sistema.

11.3. Documentación Curses

En la mayoría de sistemas Unix/Linux, la documentación de curses se encuentra en los "man pages" correspondientes. Por ejemplo, en Linux el comando “man ncurses” proporciona un listado de otros "man pages" para ciertas funciones. A fin de dar una idea de esto, mostramos parte de este manual:

curses Routine Name     Manual Page Name
--------------------------------------------
...
addch                   addch(3NCURSES)
addchnstr               addchstr(3NCURSES)
addchstr                addchstr(3NCURSES)
addnstr                 addstr(3NCURSES)
addnwstr                addwstr(3NCURSES)
addstr                  addstr(3NCURSES)
addwstr                 addwstr(3NCURSES)
assume_default_colors   default_colors(3NCURSES)*
attr_get                attr(3NCURSES)
...

Supóngase que se desea información sobre la rutina "addch"; entonces la documentación se consigue en ciertos sistemas simplemente mediante “man addch” y en otros mediante el comando: “man curs_addch” (agregando el prefijo “curs”.)

Un concepto recurrente en la documentación corresponde a las "ventanas". "Curses" permite definir zonas rectangulares (partes de la pantalla) y darles un tratamiento independiente (por ejemplo, cada ventana tendrá diferentes ejes de coordenadas.) La pantalla entera se considera también una ventana especial y existe una variable global de Curses para hacer referencia a ella: “stdscr”. En nuestros ejemplos no crearemos ventanas aunque sí emplearemos “stdscr”.

Algunas implementaciones de Curses proporcionan librerías adicionales para crear "paneles", "menús" y "formularios". Los paneles son similares a las ventanas con la diferencia que pueden superponerse. Los menús proporcionan facilidades para selección de opciones, y los formularios permiten introducir diversos tipos de información de un modo semiautomático. En mi opinión, la interfaz de programación de los paneles, menús y formularios es muy poco intuitiva e impráctica. No será explicada aquí [126] .

11.4. Caja sin eco

Normalmente cuando se solicita información al usuario vía teclado, es conveniente que el usuario pueda observar lo que está tipeando. Para esto, el terminal debe tener el "eco" activado, lo que significa que a cada tecla presionada se le hará "eco"; es decir, en la pantalla aparecerá el caracter correspondiente a la tecla presionada (este es el caso normal cuando el usuario interactúa con el shell.)

Sin embargo, en algunos casos esto no es deseable (por ejemplo, cuando se introduce una contraseña.) El programa que mostramos a continuación (caja_sin_eco.c) dibuja una cajita [127] y espera a que el usuario presione una tecla. Sin embargo, esta tecla no aparecerá en el lugar del cursor debido a que el eco se desactivó con la rutina noecho(). En otro lugar de la cajita se mostrará qué tecla fue la que se presionó. Pruebe el mismo programa eliminando la invocación a "noecho" y observe la diferencia.

/*
 * caja_sin_eco.c
 */
#include <ncurses.h>
#include <unistd.h>

int main()
{
    int z, j, c;
    initscr();
    noecho();
    cbreak();

    for (j = 3; j <= 20; j++) {
	mvaddch(2, j, '-');
	mvaddch(10, j, '-');
    }
    for (z = 3; z <= 9; z++) {
	mvaddch(z, 2, '|');
	mvaddch(z, 21, '|');
    }
    mvaddch(2, 2, '+');
    mvaddch(2, 21, '+');
    mvaddch(10, 2, '+');
    mvaddch(10, 21, '+');

    mvaddstr(4, 5, "??? ");
    c = getch();
    mvprintw(6, 5, "Tecla: %c", c);
    refresh();
    sleep(2);
    endwin();
    return 0;
}

La rutina cbreak() instruye al terminal para que cada tecla presionada por el usuario esté inmediatamente disponible en el programa. Lo opuesto (modo nocbreak) significa que el usuario deberá terminar una línea de texto con [Enter] para que el programa recién empiece a recibir las teclas que se presionaron [128] .

11.4.1. Caja Mejorada

El listado anterior presentó una caja construida en base a caracteres estándar del teclado. Ciertos terminales son capaces de mucho más. En el listado siguiente (caja_mejorada.c) se presenta un programa que dibuja la misma caja pero usando caracteres especiales proporcionados por Curses. Nótese que la apariencia final puede veriar dependiendo del tipo de terminal [129] .

/*
 * caja_mejorada.c
 */
#include <ncurses.h>
#include <unistd.h>

int main()
{
    int z, j, c;
    initscr();
    noecho();

    for (j = 3; j <= 20; j++) {
	mvaddch(2, j, ACS_HLINE);
	mvaddch(10, j, ACS_HLINE);
    }
    for (z = 3; z <= 9; z++) {
	mvaddch(z, 2, ACS_VLINE);
	mvaddch(z, 21, ACS_VLINE);
    }
    mvaddch(2, 2, ACS_ULCORNER);
    mvaddch(2, 21, ACS_URCORNER);
    mvaddch(10, 2, ACS_LLCORNER);
    mvaddch(10, 21, ACS_LRCORNER);

    mvaddstr(4, 5, "??? ");
    c = getch();
    mvprintw(6, 5, "Tecla: %c", c);
    refresh();
    sleep(2);
    endwin();
    return 0;
}

Los identificadores de estos caracteres especiales (como ACS_VLINE) se pueden hallar en el manual de addch().

11.5. Animación: Carrera de obstáculos

A continuación desarrollaremos un sencillo juego consistente en esquivar obstáculos que aparecerán en la pantalla. Supondremos que conducimos un auto (al cual controlaremos horizontalmente) y el objetivo serásobrevivir el mayor tiempo posible sin colisionar con los obstáculos.

Los obstáculos aparecerán a lo largo de la pantalla y "descenderán" hacia el auto a velocidad creciente. Esto significa que el contenido de toda la pantalla (excepto el auto) deberá desplazarse hacia abajo, cosa que se conoce como "scroll". Si bien Curses proporciona una rutina para hacer scroll, nosotros no la usaremos a fin de tener un control más completo de la pantalla, lo que nos obliga a escribir nuestra propia rutina de scroll.

Por otro lado, intentaremos que la pantalla sea "pintada" en momentos estratégicos (cuando todos los objetos están en su lugar) por lo que usaremos una "pantalla buffer" en la que se harán todas las modificaciones correspondientes al juego, y luego este buffer será volcado de una sola vez al terminal vía Curses.

Finalmente, el programa no asumirá dimensiones fijas de la pantalla, sino que las averiguará al iniciarse. Esto permitirá jugar en terminales gráficos tipo Xterm redimensionados por el usuario a cualquier tamaño.

El programa ha sido escrito en un solo archivo fuente, sin embargo, lo presentaré por partes para facilitar la explicación. El lector deberá unir estas piezas en un mismo archivo para compilarlo.

/*
 * carrera.c: Demostracion de ncurses
 */
#include <ncurses.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>

void nuevo_obstaculo(void);
void scroll_down(void);
void clear_screen(void);
void draw_screen(void);
void set(int y, int x, chtype c);
void pinta_carrito(void);
void borra_carrito(void);
void carrera(void);
void pset(int y, int x, chtype c);

// Altura de zona oculta para crear obstaculos
#define GAP 5

chtype **pantalla;
int max_y, max_x;
int car_y, car_x;
int crash = 0;

Nuestra "pantalla buffer" será "GAP" lineas más alta que la pantalla real del terminal. Por lo tanto, sólo una parte de la "pantalla buffer" se visualizará mientras que otra parte permanecerá oculta. Esto se ha hecho así a fin que los obstáculos grandes (de varias líneas de altura) puedan ser dibujados en la "zona oculta" y el scroll los haga aparecer gradualmente en la "zona visible" con lo que conseguiremos un efecto más natural.

La memoria para "pantalla buffer" se asignará dinámicamente, por lo que hemos definido la variable "pantalla" como un puntero a puntero al tipo "chtype", que es el tipo de los caracteres en curses (vease el manual de addch().) Finalmente, max_y, max_x, car_y, car_x corresponden respectivamente a las dimensiones de la pantalla del terminal y a la posición del carrito. "crash" es un flag usado para alertar cuando el carrito colisiona.

int main()
{
    int z;
    time_t inicio, final;

    srand(time(NULL));
    initscr();
    cbreak();
    nodelay(stdscr, TRUE);
    keypad(stdscr, TRUE);
    noecho();

/* Encontrar dimensiones de pantalla */
    getmaxyx(stdscr, max_y, max_x);

/* Nuestra pantalla sera de max_y+GAP */
    max_y += GAP;
    pantalla = calloc(max_y, sizeof(chtype *));
    for (z = 0; z < max_y; z++)
	pantalla[z] = calloc(max_x, sizeof(chtype));

// Pantalla limpia y carrito al medio
    clear_screen();
    car_x = max_x / 2;
    car_y = max_y - 4;
// La carrera empieza
    time(&inicio);
    carrera();
// Fin de la carrera
    draw_screen();
    refresh();
    time(&final);
    sleep(1);
    endwin();
// Score:
    printf("Duracion de la carrera: %ld seg\n", (long) (final - inicio));
    return 0;
}

Como se sabe, srand() se usa para inicializar la generación de números aleatorios, cosa que no detallaremos aquí.

La rutina nodelay() configura el terminal para que la lectura de teclas no se "bloquee", es decir, para que cuando se lea una tecla con getch() el programa continue inmediatamente si el usuario no presiona nada [130] . Como se vio en programas anteriores, getch normalmente detiene la ejecución del programa hasta que el usuario responde (ejecución "bloqueada".) Nótese que nodelay() requiere como argumentos la "ventana" a configurar y el flag "TRUE" o "FALSE". Como deseamos configurar la pantalla total, usaremos la variable especial stdscr. Asimismo, deseamos controlar el carrito con las teclas de movimiento de cursor. Estas "teclas especiales" requieren que "Curses" se configure mediante "keypad" a fin de que sean reconocidas [131] . Los argumentos son análogos a nodelay(). Las rutinas cbreak() y noecho() se explicaron anteriormente [132] .

Para dimensionar nuestra "pantalla buffer" requerimos conocer el tamaño de la "pantalla real", lo que se consigue con la macro “getmaxyx” de Curses. Nótese que el alto de la pantalla se incrementa en "GAP", y que mediante “calloc()” se le asigna un espacio de (max_x*[GAP+max_y]) caracteres de tipo “chtype”.

void carrera(void)
{
    int c;
    long delay = 30000;
    int aux = 0;

    for (;;) {
	nuevo_obstaculo();
	if (aux == 0)
	    scroll_down();
	if (crash)
	    return;
	aux = (aux + 1) % 8;
	draw_screen();
	refresh();
	c = getch();
	borra_carrito();
	switch (c) {
	case KEY_LEFT:
	    if (car_x > 2)
		car_x--;
	    break;
	case KEY_RIGHT:
	    if (car_x < max_x - 3)
		car_x++;
	    break;
	case 'q':
	    return;
	}
	pinta_carrito();
	if (crash)
	    return;
	usleep(delay);
	delay -= 10;
	if (delay < 10000)
	    delay = 10000;
    }
}

A fin de sincronizar la velocidad de procesamiento del teclado con el scroll, se ha configurado el scroll de pantalla cada 8 lecturas getch(), lo que se consigue con la variable "aux" que va de cero a (8-1=7) [133] . Estas lecturas getch() tienen además un retardo de "delay" microsegundos (sino el juego es demasiado veloz.) Nótese que la función scroll_down() retorna "verdadero" en caso de choque del carrito, y el juego termina.

En cada iteración se invoca a la rutina “nuevo_obstaculo()”, la cual posiblemente creará un nuevo obstáculo en la "zona oculta".

La tecla presionada por el usuario (si la hay) se analiza y compara con "KEY_LEFT" y "KEY_RIGHT" (teclas de movimiento de cursor.) Asimismo, la tecla "q" finaliza el juego.

Durante el procesamiento de estas teclas, el carrito es borrado puesto que puede reaparecer en una nueva posición. Nótese que esto no borra el carrito de la "pantalla real", sino sólo de la "pantalla buffer" por lo que no observaremos molestos "parpadeos" del mismo.

A fin de incrementar la dificultad conforme avanza el juego, el "delay" se va reduciendo en cada iteración. Finalmente, cada iteración redibuja la pantalla real (nuestra rutina draw_screen()) y la refresca con la rutina Curses refresh().

void clear_screen(void)
{
    int z, j;

    for (z = 0; z < max_y; z++)
	for (j = 0; j < max_x; j++)
	    pantalla[z][j] = ' ';
}

void draw_screen(void)
{
    int z, j;

    for (z = GAP; z < max_y - 1; z++)
	for (j = 0; j < max_x; j++)
	    mvaddch(z - GAP, j, pantalla[z][j]);
}

void scroll_down(void)
{
    int z, j;

    borra_carrito();
    for (z = max_y - 1; z >= 1; z--)
	for (j = 0; j < max_x; j++)
	    pantalla[z][j] = pantalla[z - 1][j];
    for (j = 0; j < max_x; j++)
	pantalla[0][j] = ' ';
    pinta_carrito();
}

Estas rutinas son bastante autoexplicativas. La rutina de "scroll" merece un comentario. El scroll es precedido por el borrado del carrito (pues éste no debe descender) y tras el "scroll", el carrito se repinta mediante “pinta_carrito()”. Esta última rutina setea "crash" en "1" en caso de detectar colisiones.

void nuevo_obstaculo(void)
{
    int x = rand() % max_x;

    if (rand() % 18 == 0)
	switch (rand() % 10) {
	case 0:
	case 1:
	case 2:
	case 3:
	case 4:
	case 5:
	case 6:
	case 7:
	    //   *
	    pset(0, x, '*');
	    break;
	case 8:
	    //    + 
	    //   OOO
	    pset(0, x, '+');
	    pset(1, x, 'O');
	    pset(1, x - 1, 'O');
	    pset(1, x + 1, 'O');
	    break;
	case 9:
	    //  .   .
	    pset(0, x + 2, '.');
	    pset(0, x - 2, '.');
	    break;
	}
}

Esta rutina dibuja un obstáculo si la función “rand()” retorna un múltiplo de 18 [134] . En otras palabras, en promedio, una de cada 18 veces se generará un obstáculo. En caso de generarse, el obstáculo se colocará en la "zona oculta" de la "pantalla buffer" en la posición aleatoria "x".

Como se observa, se toma un número aleatorio de 0 a 9, y en la mayoría de casos se genera un "bache simple" (un solitario asterisco) en la pantalla (casos 0-7.) En el caso 8 se genera una "iglesia" y en el caso 9 un "tunel" compuesto por dos puntitos. Evidentemente no se ha hecho un gran aprovechamiento de la "zona oculta" con estos obstáculos tan pequeños.

Por seguridad, no hemos modificado directamente la variable "pantalla", sino que hemos preferido usar una función auxiliar “pset()” que verifica que las coordenadas estén dentro del rango aceptable. Esto permite crear obstáculos entrecortados en los bordes de la pantalla sin temor a hacer caer el programa (por ejemplo, con un acceso a pantalla[0][-1].)

void borra_carrito(void)
{
    pset(car_y, car_x, ' ');
    pset(car_y + 1, car_x, ' ');
    pset(car_y + 1, car_x + 1, ' ');
    pset(car_y + 1, car_x - 1, ' ');
    pset(car_y + 2, car_x, ' ');
    pset(car_y + 2, car_x + 1, ' ');
    pset(car_y + 2, car_x - 1, ' ');
}

void pinta_carrito(void)
{
    set(car_y, car_x, '^');
    set(car_y + 1, car_x, '#');
    set(car_y + 1, car_x + 1, 'H');
    set(car_y + 1, car_x - 1, 'H');
    set(car_y + 2, car_x, '#');
    set(car_y + 2, car_x + 1, 'H');
    set(car_y + 2, car_x - 1, 'H');
}

Estas rutinas son autoexplicativas. En el caso de “pinta_carrito()”, la rutina “set()” es similar a “pset()” con la diferencia de que activa la variable "crash" en caso de detectar colisiones. El carrito luce así:

      ^
     H#H
     H#H

Por último, las rutinas que faltaban:

void pset(int y, int x, chtype c)
{
    if (x >= 0 && x < max_x && y >= 0 && y < max_y)
	pantalla[y][x] = c;
}

void set(int y, int x, chtype c)
{
    if (x < 0 || x >= max_x || y < 0 || y >= max_y)
	return;
    if (pantalla[y][x] != ' ') {
	pset(y, x, '*');
	crash = 1;
    } else
	pantalla[y][x] = c;
}

11.6. Menú Personalizado

A continuación crearemos una función para implementar un menú usando Curses. Este menú utilizará las teclas de cursor para seleccionar una opción. La "opción activa" se visualiza con vídeo invertido. Empecemos por el programa que hace uso de esta función:

/*
 * pruebamenu.c: Usando x_menu()
 */
#include <ncurses.h>
#include <stdio.h>

int x_menu(char *, ...);

int main()
{
    int z;

    initscr();

    z = x_menu("Elija lo que mas le agrade",
	       "Auto de lujo", "Casa de campo",
	       "Viaje a Paris", "Pareja con dinero", NULL);

    printw("Ud. eligio la opcion %d. Presione Enter...", z);
    getch();
    endwin();
    return 0;
}

Como se ve, la función del menú (x_menu()) requiere un título seguido por la lista de alternativas terminada en NULL. Se retorna la alternativa seleccionada o -1 en caso de error. Obsérvese que main() debe inicializar el sistema Curses con “initscr()”.

/*
 * x_menu.c: Menu curses
 */
#include <ncurses.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>

int x_menu(char *t, ...)
{
    va_list ap;
    char *p;
    int n = 0, z, x = 0, flag = 0;
    char *opts[100];

    keypad(stdscr, TRUE);
    va_start(ap, t);
    for (;;) {
	p = va_arg(ap, char *);
	if (p == NULL)
	    break;
	opts[n] = strdup(p);
	n++;
    }
    va_end(ap);
    if (n == 0)
	return -1;

    for (;;) {
	clear();
	printw("%s\n\n", t);
	for (z = 0; z < n; z++) {
	    if (z == x)
		attron(A_REVERSE);
	    printw("%s", opts[z]);
	    if (z == x)
		attroff(A_REVERSE);
	    printw("\n");
	}
	switch (getch()) {
	case KEY_DOWN:
	    if (x < n - 1)
		x++;
	    break;
	case KEY_UP:
	    if (x > 0)
		x--;
	    break;
	case ' ':
	case '\n':
	    flag = 1;
	    break;
	}
	if (flag == 1)
	    break;
    }
    for (z = 0; z < n; z++)
	free(opts[z]);
    clear();
    return x;
}

Como se aprecia, la función x_menu (de tipo "variadic") almacena todos los argumentos en el array de punteros "opts" en forma dinámica. Luego ingresa a un loop donde se imprimen las opciones, cuidando de que la opción activa ("x") sea impresa en vídeo invertido (con attron()/attroff() y el argumento A_REVERSE.) Véase el manual de estas funciones para más información. Finalmente, la memoria asignada con strdup() se libera con free() y se retorna la opción seleccionada.

Nótese que x_menu() borra toda la pantalla con clear(). Esto puede no ser deseable. Véase el ejercicio 5.

11.7. Ejercicios

1 Modifique el juego para que los obstáculos sean más interesantes (intente usar todo el "GAP") Amplíe los tipos de obstáculos.

2 Modifique el juego para que haga uso de colores. Los colores se deben inicializar con la rutina start_color. Luego se deben definir las "parejas de colores" mediante init_pair(). Finalmente, la parejas de colores (COLOR_PAIR) pueden ser usadas al imprimir.

La figura muestra un screenshot de una versión del juego que responde a las preguntas anteriores:

cf2r

3 El programa genera obstáculos a un ritmo constante independientemente del tamaño de la pantalla. Esto hace que en una pantalla pequeña sea muy difícil de jugar (gran densidad de obstáculos) y lo contrario en una pantalla grande. Pruebe esto con un terminal gráfico tipo Xterm. Modifique el programa para que la "densidad de obstáculos" se genere en función del ancho de la pantalla (max_x). [135]

4 Modifique la función x_menu() para que genere una caja alrededor de las opciones. Esto requiere encontrar la opción de mayor longitud para dimensionar la caja.

5 Modifique la función x_menu() para que acepte dos argumentos correspondientes a las coordenadas donde se inicia el menú en la pantalla. Calculado el tamaño de la "caja", bórrese sólo esta área rectangular evitando borrar la pantalla entera con clear().

12. Programación Gráfica con SDL

SDL es una librería que proporciona acceso de "bajo nivel" para video, audio, teclado, mouse, joystick y temporizadores de manera portable para múltiples plataformas. Entre otras cosas, esta librería se ha utilizado exitosamente para portar diversos juegos comerciales de Wind*ws a Linux, aunque el camino inverso también es posible [136] .

En esta revisión sólo veremos aspectos básicos de programación gráfica 2D. Consúlte la documentación para más información acerca de la programación acelerada 3D y en particular el enlace con las librerías OpenGL. Para más imformación y software adicional, visite el https://www.libsdl.org Sitio oficial de SDL.

12.1. Inicializar Subsistemas SDL

SDL comprende diversos ámbitos a los que denomina "subsistemas". Un programa puede utilizar uno o más subsistemas, los cuales deben ser inicializados antes de emplearse:

Table 8. Subsistemas SDL
Constante Subsistema

SDL_INIT_TIMER

Temporizadores

SDL_INIT_AUDIO

Sonido

SDL_INIT_VIDEO

Video

SDL_INIT_CDROM

CDROM

SDL_INIT_JOYSTICK

Joystick

SDL_INIT_EVERYTHING

Todo lo anterior

Para inicializar los subsistemas, se debe llamar a la función SDL_Init() con las constantes indicadas en la tabla haciendo una operación OR binaria. Por ejemplo, si nuestro programa usará Video, Sonido y Joystick de SDL, escribiremos:

if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO|SDL_INIT_JOYSTICK)==-1)
	fprintf(stderr,"Error inicializando.\n"),exit(1);

Como se aprecia, esta función retorna -1 en caso de error.

Nota Redundante: Es imprescindible inicializar los subsistemas SDL antes de hacer uso de ellos.

Tan pronto SDL_Init() retorna con éxito, es conveniente definir una "función de salida" con atexit(3) de modo tal que invoque a SDL_Quit(). Ésta última restaurará el modo original de la pantalla y del teclado a fin de no perder el control en el caso de una aplicación fallida:

atexit(SDL_Quit);

12.2. Inicializar el Video

Una vez inicializado el subsistema de video SDL con SDL_Init(), es necesario configurar el modo de video deseado. Entre los aspectos involucrados tenemos principalmente la resolución y el número máximo de colores simultáneos (pixel depth.) En este texto se utilizará una resolución de 800 x 600 pixels y una profundidad de 16 bits por pixel (16 bpp), lo que da un total de 2 ^ 16=65536 colores simultáneos como máximo. Por supuesto, es posible emplear 8 bpp, 24 bpp y 32 bpp y muchas otras resoluciones.

SDL_Surface *pantalla;

if(SDL_Init(SDL_INIT_VIDEO)==-1)
	exit(1);
pantalla=SDL_SetVideoMode(800,600,16,SDL_SWSURFACE);

La constante SDL_SWSURFACE solicita a SDL la creación una "superficie" por software (normalmente asociada a una "ventana" del ambiente GUI.) Esta superficie es accesible mediante el puntero de tipo SDL_Surface llamado "pantalla".

También podríamos haber escrito:

SDL_SWSURFACE|SDL_FULLSCREEN

Para forzar a que nuestra superficie ocupe toda la pantalla y no sólo una ventana.

SDL_SetVideoMode() retorna NULL en caso de error.

12.3. Algunas rutinas auxiliares

Para los siguientes programas emplearemos las siguientes rutinas auxiliares (archivo aux.c):

/*
 * Rutinas auxiliares para SDL
 */
#include "sdl_aux.h"
#include <stdlib.h>
#include <stdio.h>

Uint32 getpixel(SDL_Surface * s, int x, int y)
{
    Uint32 r;
    Uint8 *p = (Uint8 *) s->pixels + y * s->pitch + x * 2;
    r = *(Uint16 *) p;
    return r;
}

void putpixel(SDL_Surface * s, int x, int y, Uint32 pixel)
{
    Uint8 *p = (Uint8 *) s->pixels + y * s->pitch + x * 2;
    *(Uint16 *) p = pixel;
}

SDL_Surface *inicializa(void)
{
    SDL_Surface *s;
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) < 0)
	fprintf(stderr, "SDL_Init() error\n"), exit(1);
    atexit(SDL_Quit);
    s = SDL_SetVideoMode(800, 600, 16, SDL_SWSURFACE);
/* Para habilitar pantalla completa:
s=SDL_SetVideoMode(800,600,16,SDL_SWSURFACE|SDL_FULLSCREEN); */
    if (s == NULL) {
	fprintf(stderr, "Error en SDL_SetVideoMode\n");
	exit(1);
    }
    return s;
}

/* Los colores referenciales para el espectro */
static SDL_Color ref_color[10] = {
    { 255, 0, 0, -1 },
    { 255, 255 / 3, 0, -1 },
    { 255, 2 * 255 / 3, 0, -1 },
    { 255, 255, 0, -1 },
    { 255 / 2, 255, 0, -1 },
    { 0, 255, 255, -1 },
    { 0, 2 * 255 / 3, 255, -1 },
    { 0, 0, 255, -1 },
    { 2 * 255 / 3, 0, 255, -1 },
    { 255, 0, 255, -1 }
};

Uint32 getColorSpectrum(SDL_Surface * s, double c)
{
    if (c < 0.0 || c > 1.0)
	c = 0.0;
    double particion = 1.0 / 9;
    int i_part = c * 8.999999;
    double inicio = particion * i_part;
    double fin = particion * (i_part + 1);
    double factor = (c - inicio) / (fin - inicio);
    SDL_Color rpta;
    rpta.r =
	ref_color[i_part].r + (ref_color[i_part + 1].r -
			       ref_color[i_part].r) * factor;
    rpta.g =
	ref_color[i_part].g + (ref_color[i_part + 1].g -
			       ref_color[i_part].g) * factor;
    rpta.b =
	ref_color[i_part].b + (ref_color[i_part + 1].b -
			       ref_color[i_part].b) * factor;
    return SDL_MapRGB(s->format, rpta.r, rpta.g, rpta.b);
}

En primer lugar, hemos separado la inicialización del video SDL en la función inicializa(), la cual será invocada al inicio de nuestros ejemplos. Asimismo, inicializaremos el subsistema de temporización pues nuestros programas lo requerirán (invocarán a SDL_Delay() para hacer pausas.)

Sorprendentemente, SDL no proporciona funciones para modificar pixels directamente en las superficies, pero permite hacer cambios directamente en la memoria de las mismas. Conociendo la representación de los pixels se puede hacer los cambios necesarios, tal como se muestra en las funciones getpixel() y putpixel() [137] . Téngase en cuenta que estas rutinas sólo pueden operar con una profundidad de 16 bpp. Consúltese la documentación para los equivalentes en otras profundidades de color, o para funciones más generales que operen en cualquier profundidad.

Hemos agregado una rutina auxiliar que permite obtener los colores del espectro (en la representación adecuada a la superficie gráfica en uso) a partir de un valor real en el rango [0,1]. La usaremos en lo sucesivo para dar más "vida" a algunos gráficos.

Como se ve, los colores se representan con un entero de 32 bits (tipo Uint32); sin embargo, la codificación de bits de los mismos depende de diversos factores [138] . Es por esto que los "colores" del programa se deben "calcular" primero con la rutina SDL_MapRGB(), la cual requiere información acerca del formato de la superficie donde se va a pintar (el miembro →format.)

12.4. Trazos simples y Colores

El siguiente programa traza un rectángulo con el conjunto de colores del espectro:

/* grafica_espectro: Grafica muchos colores en un rectangulo */

#include "sdl_aux.h"

int main()
{
    SDL_Surface *screen;
    Uint32 blanco;
    int z, j;

    screen = inicializa();
    blanco = SDL_MapRGB(screen->format, 0xff, 0xff, 0xff);

    SDL_FillRect(screen, NULL, blanco);
    if (SDL_MUSTLOCK(screen))
	SDL_LockSurface(screen);
    for (z = 100; z < 500; z++)
	for (j = 0; j < 200; j++)
	    putpixel(screen, 100 + j, z,
		     getColorSpectrum(screen, (z - 100) / 400.0));
    if (SDL_MUSTLOCK(screen))
	SDL_UnlockSurface(screen);
    SDL_UpdateRect(screen, 0, 0, 0, 0);
    SDL_Delay(5000);		// 5 segundos para admirar la pantalla
    return 0;
}

El color "blanco" se ha utilizado para hacer un "llenado" en toda la superficie con la función SDL_FillRect(). El segundo argumento (NULL) significa "relleno total", sin embargo, se podría rellenar sólo una región.

Cuando se accede directamente a la "memoria de pixels" de una superficie (como lo hacen getpixel() y putpixel()) posiblemente sea necesario "bloquear" dicha superficie a fin de evitar ciertas des-sincronizaciones con otras operaciones de SDL. Por este motivo se emplea (en forma condicional) las rutinas SDL_LockSurface() y SDL_UnlockSurface() durante el acceso a la memoria de pixels, previa consulta mediante la macro SDL_MUSTLOCK() [139] . Mientras una superficie está "bloqueada" no se debe hacer llamadas al sistema ni a otras de SDL; sólo debe acceder a los pixels.

Finalmente, tras modificar la pantalla, debemos asegurarnos de que estas modificaciones surtan efecto, pues SDL no necesariamente hace los cambios en el momento que se lo solicitan a fin de hacer optimizaciones; [140] a esto se le denomina "actualización". Con este fin se emplea la rutina SDL_UpdateRect(), la cual permite especificar (en sus últimos cuatro parámetros) qué región de la pantalla se requiere actualizar. En nuestro caso, todos los parámetros en "cero" corresponden a un "atajo" que significa actualizar la pantalla completa.

12.4.1. Compilación de programas SDL

Para obtener las opciones de compilación adecuadas, SDL proporciona el comando auxiliar sdl-config. Las opciones "de compilación" se obtienen con sdl-config --cflags, mientras que las opciones "de enlace" se obtienen con sdl-config --libs. Nuestro programa podría por lo tanto compilarse mediante:

cc $(sdl-config --cflags) -o grafica_espectro
	grafica_espectro.c sdl_aux.c $(sdl-config --libs)

Como siempre, es mejor emplear un Makefile como el que se muestra al final del capítulo.

12.5. Gráficos y Matemáticas

Las fórmulas matemáticas se emplean con frecuencia para producir diversas clases de gráficos. Entre los más sorprendentes destacan los famosos fractales, que tienen la característica de generar figuras con mucha "variación y contenido" a partir de ecuaciones tremendamente simples.

12.5.1. Fractales de Mandelbrot

El programa listado a continuación produjo el siguiente resultado:

Un fractal de Mandelbrot

Este programa se inicia con la creación de una pequeña e incompleta librería de rutinas para operar con números complejos (pues así lo requieren estos fractales.) A fin de cuentas, lo único que deseamos es aplicar reiterativamente la fórmula:

Z = Z^2+c

donde Z es un punto cualquiera del plano complejo y c es una constante (también compleja.) Si la fórmula anterior es aplicada reiterativamente (en nuestro caso, 50 iteraciones) y siempre se genera un número relativamente pequeño [141] , entonces el punto que representa el Z inicial, es dibujado; caso contrario, se descarta.

Como se observa, el valor “c” que usa el programa (rutina operacion()) es:

c = -0.53 + 0.53 i

Lo cual se puede modificar para obtener otra clase de figuras.

El programa grafica el área delimitada por las coordenadas del plano complejo (x1=0, x2=1.7, y1=-1, y2=1), pero el lector podrá variar estos argumentos con mucha facilidad (al inicio de la función main().)

Asimismo, es muy fácil definir la granuralidad de los puntos del gráfico con la variable "n" (en nuestro programa, 1000.) Con valores menores, el programa prueba menos puntos del área propuesta, pero se ejecuta más aprisa. Pruebe la ejecución con valores distintos de "n".

/*
 * mandelbrot.c: Fractales de Mandelbrot
 */
#include "sdl_aux.h"
#include <math.h>

typedef struct {
    double x, y;
} COMPLEJO;

COMPLEJO suma(COMPLEJO a, COMPLEJO b)
{
    COMPLEJO r;
    r.x = a.x + b.x;
    r.y = a.y + b.y;
    return r;
}

COMPLEJO multiplica(COMPLEJO a, COMPLEJO b)
{
    COMPLEJO r;
    r.x = a.x * b.x - a.y * b.y;
    r.y = a.x * b.y + a.y * b.x;
    return r;
}

double norma(COMPLEJO a)
{
    return sqrt(a.x * a.x);
}

COMPLEJO operacion(COMPLEJO a)
{
    COMPLEJO c = { -0.53, 0.53 };
    return suma(multiplica(a, a), c);
}

int pintable(double x, double y)
{
    COMPLEJO a = { x, y };
    double norma_maxima = norma(a) * 50;

    int z;
    for (z = 0; z < 50; z++) {
	a = operacion(a);
	if (norma(a) > norma_maxima)
	    return 0;
    }
    return 1;
}

int main(int argc, char *argv[])
{
/* Modificar estas variables para nuevas figuras */
    double x1 = 0, x2 = 1.7, y1 = -1, y2 = 1;
    int n = 1000;

/* Variables auxiliares */
    double delta = (x2 - x1) / n;
    int m = (y2 - y1) / delta;
    double x, y;
    int z, j, px, py;

/* SDL */
    SDL_Surface *s;
    Uint32 negro, blanco;

    s = inicializa();
    negro = SDL_MapRGB(s->format, 0x0, 0x0, 0x0);
    blanco = SDL_MapRGB(s->format, 0xff, 0xff, 0xff);
    SDL_FillRect(s, NULL, blanco);

    for (z = 0; z < m; z++) {
	if (SDL_MUSTLOCK(s))
	    SDL_LockSurface(s);
	for (j = 0; j < n; j++) {
	    x = x1 + j * delta;
	    y = y1 + z * delta;
	    if (pintable(x, y)) {
		px = 800 * j / n;
		py = 600 * z / m;
		putpixel(s, px, py, negro);
	    }
	}
	if (SDL_MUSTLOCK(s))
	    SDL_UnlockSurface(s);
	if (z % 10 == 0)
	    SDL_UpdateRect(s, 0, 0, 0, 0);
    }
    SDL_UpdateRect(s, 0, 0, 0, 0);
    SDL_Delay(5000);		/* 5 segundos para admirar pantalla */
    return 0;
}

12.5.2. Gráfica de Funciones Arbitrarias

El siguiente programa permite graficar una función de dos variables (la cual se puede cambiar con facilidad al inicio), generándose un efecto tridimensional:

Gráfica de función de dos variables

El programa considera los puntos (x,y) del primer cuadrante tales que x+y < extension`base_, lo cual corresponde a una región triangular. La variable `factor controla la granularidad de los puntos a colorearse, y foff indica cada cuántos puntos se traza una línea del "enrejado" negro cobertor. La función convierte3d2d() es el corazón del programa, en tanto proporciona las coordenadas de pantalla para cualquier punto en el espacio, para lo cual asume una perspectiva arbitraria. El resto del programa es relativamente sencillo de seguir, teniendo en consideración conceptos básicos de vectores.

/* grafica_funcion: Grafica de funcion 3d */

#include "sdl_aux.h"
#include <math.h>

double extension_base = 4.0;
double factor = 0.006;
int foff = 20;

double funcion(double x, double y)
{
    return sin(3 * x) + sin(3 * y);
}

void convierte3d2d(double x_3d, double y_3d, double z_3d,
		   double *x_2d, double *y_2d)
{
    double origen_x = 400.0, origen_y = 300.0;
    double factor_x = 130.0, factor_y = 130.0, factor_z = 90.0;

    *x_2d = origen_x + y_3d * factor_y * 0.75 - x_3d * factor_x * 0.75;
    *y_2d =
	origen_y - z_3d * factor_z + x_3d * factor_x * 0.25 +
	y_3d * factor_y * 0.25;
}

void putpixel3d(SDL_Surface * screen, double x, double y, double z,
		Uint32 pixel)
{
    double x2, y2;
    convierte3d2d(x, y, z, &x2, &y2);
    putpixel(screen, x2, y2, pixel);
}

void drawline3d(SDL_Surface * screen, double x1, double y1, double z1,
		double x2, double y2, double z2, Uint32 pixel)
{
/* Hallar los extremos de la linea y la distancia en pixels */
    double inicio_x, inicio_y, fin_x, fin_y;
    convierte3d2d(x1, y1, z1, &inicio_x, &inicio_y);
    convierte3d2d(x2, y2, z2, &fin_x, &fin_y);
    double d_pixels = sqrt((inicio_x - fin_x) * (inicio_x - fin_x) +
			   (inicio_y - fin_y) * (inicio_y - fin_y));

/* Recorrer vector de la linea */
    double vx = x2 - x1, vy = y2 - y1, vz = z2 - z1, f;
    for (f = 0.0; f <= 1.0; f += 1.0 / d_pixels)
	putpixel3d(screen, x1 + vx * f, y1 + vy * f, z1 + vz * f, pixel);
}

double minimo = 1e10, maximo = -1e10;

void draw_function(SDL_Surface * screen, double x, double y)
{
    double f = funcion(x, y);
    double color = (f - minimo) / (maximo - minimo);
    putpixel3d(screen, x, y, f, getColorSpectrum(screen, color));
}

int main()
{
    SDL_Surface *screen;
    Uint32 blanco, negro;

    screen = inicializa();
    blanco = SDL_MapRGB(screen->format, 0xff, 0xff, 0xff);
    negro = SDL_MapRGB(screen->format, 0, 0, 0);

    SDL_FillRect(screen, NULL, blanco);
    if (SDL_MUSTLOCK(screen))
	SDL_LockSurface(screen);

// Trazar ejes de coordenadas
    drawline3d(screen, 0, 0, 0, 3, 0, 0, negro);
    drawline3d(screen, 0, 0, 0, 0, 3, 0, negro);
    drawline3d(screen, 0, 0, 0, 0, 0, 3, negro);

// Hallar extremos de la funcion
    double x, y, s, f;
    for (s = 0.0; s < extension_base; s += factor) {
	for (x = 0; x <= s; x += factor) {
	    y = s - x;
	    f = funcion(x, y);
	    if (f < minimo)
		minimo = f;
	    if (f > maximo)
		maximo = f;
	}
    }

// Trazar la funcion
    int offset_x = 0, offset_y;
    for (s = 0.0; s < extension_base; s += factor) {
	offset_y = 0;
	for (x = s; x >= 0; x -= factor) {
	    y = s - x;
	    draw_function(screen, x, y);
	    if (offset_x == 0 && offset_y == 0) {
		if (x > factor * foff)
		    drawline3d(screen, x, y, funcion(x, y),
			       x - factor * foff, y,
			       funcion(x - factor * foff, y), negro);
		if (s < extension_base - factor * foff)
		    drawline3d(screen, x, y, funcion(x, y), x,
			       y + factor * foff, funcion(x,
							  y +
							  factor * foff),
			       negro);
	    }
	    offset_y++;
	    if (offset_y == foff)
		offset_y = 0;
	}
	offset_x++;
	if (offset_x == foff)
	    offset_x = 0;
    }
    if (SDL_MUSTLOCK(screen))
	SDL_UnlockSurface(screen);
    SDL_UpdateRect(screen, 0, 0, 0, 0);
    SDL_Delay(5000);		// 5 segundos para admirar la pantalla
    return 0;
}

12.6. Algunos elementos de programación de Juegos

El programa que se lista a continuación (nave.c) corresponde a un prototipo de un juego (Disparos a Nave Espacial) que ilustrará diversos aspectos típicos de esta clase de programación:

#include "sdl_aux.h"
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <unistd.h>

SDL_Rect rec_screen;
int fire = 0;

static void process_events(void)
{
    SDL_Event event;

    while (SDL_PollEvent(&event)) {
	switch (event.type) {
	case SDL_KEYDOWN:
	    if (event.key.keysym.sym == SDLK_ESCAPE)
		exit(0);
	    break;
	case SDL_QUIT:
	    exit(0);
	    break;
	case SDL_MOUSEBUTTONDOWN:
	    if (event.button.button == SDL_BUTTON_LEFT) {
		int x = event.button.x;
		int y = event.button.y;
		fire++;
		if (x > rec_screen.x &&
		    x < rec_screen.x + rec_screen.w &&
		    y > rec_screen.y && y < rec_screen.y + rec_screen.h) {
		    printf("Acierto en %d disparos\n", fire);
		    exit(0);
		}
	    }
	}
    }
}

#define NSTAR 20
#define PI 3.141592653

int main()
{
    SDL_Surface *screen, *nave, *s_nave;
    SDL_Rect rec_screen2 = { 0, 0, 0, 0 };
    Uint32 negro, blanco, transparente;
    int z;
    double angulo = 0;

    screen = inicializa();

    negro = SDL_MapRGB(screen->format, 0x00, 0x00, 0x00);
    blanco = SDL_MapRGB(screen->format, 0xff, 0xff, 0xff);

    if ((nave = SDL_LoadBMP("nave.bmp")) == NULL)
	fprintf(stderr, "No pude leer nave.bmp\n"), exit(1);
    nave = SDL_DisplayFormat(nave);
    s_nave = SDL_DisplayFormat(nave);
    transparente = getpixel(nave, 0, 0);
    SDL_SetColorKey(nave, SDL_SRCCOLORKEY, transparente);

    rec_screen.w = nave->w;
    rec_screen.h = nave->h;
    SDL_FillRect(screen, NULL, negro);

    if (SDL_MUSTLOCK(screen))
	SDL_LockSurface(screen);
    for (z = 0; z < NSTAR; z++)
	putpixel(screen, rand() % 800, rand() % 600, blanco);
    if (SDL_MUSTLOCK(screen))
	SDL_UnlockSurface(screen);
    SDL_UpdateRect(screen, 0, 0, 0, 0);

    for (;;) {
	process_events();
	angulo += 0.01;
	if (angulo > 2 * PI)
	    angulo = 0;
	rec_screen.x = 400 + 200 * cos(angulo);
	rec_screen.y = 300 + 200 * sin(angulo);
	rec_screen.w = nave->w;
	rec_screen.h = nave->h;
	SDL_BlitSurface(screen, &rec_screen, s_nave, NULL);
	SDL_BlitSurface(nave, NULL, screen, &rec_screen);
	SDL_UpdateRect(screen,
		       rec_screen.x, rec_screen.y, rec_screen.w,
		       rec_screen.h);
	SDL_UpdateRect(screen, rec_screen2.x, rec_screen2.y, rec_screen2.w,
		       rec_screen2.h);
	SDL_BlitSurface(s_nave, NULL, screen, &rec_screen);
	SDL_Delay(5);
	rec_screen2 = rec_screen;
    }
    return 0;
}

12.6.1. Archivos externos con Imágenes

El listado nos muestra una forma de trabajar con imágenes cargadas desde archivos externos. El lector puede emplear cualquier imagen en lugar de la que hemos utilizado (nave.bmp.) Lo importante es que la imagen DEBE estar en un archivo de formato BMP no comprimido [142] . Como se ve, la carga de la imágen BMP hacia una superficie SDL se hace con la función SDL_LoadBMP(). image::sdl/lanave.jpg["Mapa de bits de nave espacial"]

12.6.2. Imágen optimizada para Animación

Dado que la superficie de la "nave" recién cargada será desplazada sobre la superficie de la pantalla, es conveniente emplear la rutina SDL_DisplayFormat() a fin de obtener una superficie optimizada para realizar copias sobre aquella [143] . Con esto obtenemos una nueva versión de la superficie correspondiente a la nave.

La "nave espacial" se desplaza dando círculos (para el cálculo de la posición se emplea las funciones cos() y sin().) Este desplazamiento consiste en copiar la superficie de la nave hacia una determinada posición de la superficie de la pantalla.

El espacio de fondo es de color negro y se ha dibujado NSTAR "estrellas" (simples puntos blancos) cuya posición se define aleatoriamente.

12.6.3. Animación y Copia de superficies

Una de las cosas más importantes que demuestra este programa es el uso de la función SDL_BlitSurface(), que permite copiar (blitting) una superficie (en nuestro caso, la nave) sobre otra (aquí, la pantalla de fondo.) El poder de SDL en las animaciones se deriva en gran parte de su capacidad de hacer estas copias a muy alta velocidad, aprovechándose en muchos casos el hardware de video acelerado.

Como se aprecia, la función recibe los punteros a las superficies y además dos punteros a estructuras de tipo SDL_Rect que se utilizan para especificar "rectángulos", los cuales denotan la posición y el tamaño de la copia. Por ejemplo, la zona de la pantalla que se salva (antes de pintar la nave) se define mediante el siguiente rectángulo:

rec_screen.x=400+200*cos(angulo); rec_screen.y=300+200*sin(angulo);
        rec_screen.w=nave->w;rec_screen.h=nave->h;
Nave espacial en vuelo

En esta animación, sólo se actualiza el rectángulo correspondiente a la posición del bitmap de la nave (no se repinta la pantalla completa con sus estrellas), lo que obliga a que además "borremos" la posición antigua de la nave con el fondo original (se repinta el fondo), por lo que previamente al pintado de la nave, su respectivo "fondo de pantalla" se guarda en la superficie “s_nave” que tiene las mismas dimensiones que la nave.

Asimismo, dado que la nave se pinta en su nueva posición y el espacio se "repinta" en la antigua posición, es necesario "actualizar" ambas regiones de la pantalla.

12.6.4. Transparencia

La superficie correspondiente a la nave tiene fondo negro. Este fondo negro al copiarse sobre el fondo espacial plagado de estrellas puede crear una apariencia desagradable pues los bordes (negros) de la nave "ocultarían" a aquellas.

Por suerte es posible configurar un color de una superficie para que signifique "transparente". A tal efecto, se cogió el pixel de posición superior izquierda de la nave (que no es parte de su fuselaje) y le proporcionamos el significado de "transparente" mediante:

SDL_SetColorKey(nave,SDL_SRCCOLORKEY,transparente);

Con esto, el color del "fondo" de la superficie de la nave ya no tiene importancia. Pruebe a cambiar el color del espacio y verifique que la nave se sigue dibujando bien.

12.6.5. Eventos

El otro gran tema introducido en el programa corresponde a los eventos, que son los que permiten saber si el usuario ha presionado el teclado, ha movido el mouse o la palanca del joystick. Los eventos deben ser leídos constantemente durante el programa con la función SDL_PollEvent(), la que recibe un puntero a una estructura de tipo SDL_Event.

Si hay eventos pendientes de ser leídos (en la cola de eventos de SDL) estos podrán ser recogidos en un loop como el que sigue:

while(SDL_PollEvent(&event)) {
	switch(event.type)
		{
		...
		}

Como se aprecia, la estructura SDL_Event tiene un miembro type que especifica el tipo de evento que se está recibiendo, lo que permite descartar los que nos son irrelevantes.

En nuestro caso, hemos procesado tres tipos de evento: SDL_KEYDOWN (presión de una tecla), SDL_QUIT (solicitud de fin de aplicación, por ejemplo, cerrar la ventana, Ctrl+C, etc.) y SDL_MOUSEBUTTONDOWN (presión de botón del mouse.)

En el caso de la presión de las teclas, la estructura SDL_Event contendrá un valor significativo en su miembro “key”, que es otra estructura [144] . Esta última posee el miembro “keysym”, el cual guarda información acerca de cuál es la tecla presionada.

Sin embargo, keysym (estructura de tipo SDL_keysym) especifica la tecla de varias maneras, aunque la más portable corresponde a su miembro “sym” [145] , con lo que en definiva llegamos a event.key.keysym.sym, que puede compararse con las constantes estándar SDL para diferentes teclas. En nuestro caso, lo hemos comparado contra la constante para la tecla de "escape" (SDLK_ESCAPE.)

Como indicamos, la rutina de procesamiento de eventos también verifica el caso en que uno de los botones del mouse haya sido presionado (en particular, el botón izquierdo.) Esto que incrementa “fire” (el numero de "disparos") y se verifica si la posición del puntero está al interior del rectángulo usado para dibujar la nave (rec_screen) lo que significa un "disparo" acertado, con lo que el programa termina.

Se ha añadido una pausa de retardo a fin de que el movimiento de la nave tenga una velocidad adecuada, para lo cual se empleó la rutina SDL_Delay() con cinco milisegundos.

Makefile del capítulo
CFLAGS:=$(shell sdl-config --cflags) -Wall -g
LDFLAGS:=$(shell sdl-config --libs)
PROGRAMAS = grafica_espectro grafica_funcion mandelbrot solucion_2 nave

all: sdl_aux.o $(PROGRAMAS)

grafica_espectro: grafica_espectro.c
	cc -Wall -o $@ $< sdl_aux.o $(LDFLAGS)
	
grafica_funcion: grafica_funcion.c
	cc -Wall -O2 -o $@ $< sdl_aux.o $(LDFLAGS)
	
mandelbrot: mandelbrot.c
	cc -Wall -O2 -o $@ $< sdl_aux.o $(LDFLAGS)
	
nave: nave.c
	cc -Wall -O2 -o $@ $< sdl_aux.o $(LDFLAGS)
	
solucion_2: solucion_2.c
	cc -Wall -O2 -o $@ $< sdl_aux.o $(LDFLAGS)
	
clean:
	rm -f *.o $(PROGRAMAS)

12.7. Ejercicios

1 Mejorando el juego espacial

Dote de un movimiento aleatorio a la nave espacial de nave.c. Use vectores de velocidad y aceleracion cambiante.

Diseñe una imagen alternativa de la nave espacial en explosión como producto de disparos exitosos.

2 Colores para el fractal

El siguiente gráfico fue producido por el programa de la última sección, haciéndole el añadido de generar un color para cada pixel en función del valor de la "última norma" calculada para el mismo; en otras palabras, para cada valor (escalar) de la norma obtenida, se generó una triada (r,g,b) que permitió en cada iteración reemplazar el color negro (que ahora será el color de fondo.)

Fractal de Mandelbrot con muchos colores

Utilizando la función de colores del espectro, reproduzca este resultado.

13. Toolkits para Interfaz Gráfica de Usuario

En este capítulo pretendo mostrar muy superficialmente algunos de los "toolkits GUI" más utilizados y/o interesantes empleados para desarrollar aplicaciones en entornos de escritorio.

13.1. Introducción y Terminología

Los entornos de escritorio modernos (también llamados ambientes orientados a ventanas) representan el modo más popular mediante el cual los usuarios interactúan con los computadores en la actualidad, presentando una apariencia y un comportamiento relativamente estandarizado. Otro término que se suele utilizar para el mismo concepto es la abreviación "GUI" (graphical user interface, o interfaz gráfica de usuario.)

Entornos de Escritorio

En estos ambientes GUI las aplicaciones generan "ventanas" en la pantalla, en las que se vuelca la información correspondiente, y desde las que se espera las órdenes del usuario. Estas ventanas suelen estar compuestas de elementos típicos como botones, etiquetas de texto, zonas de edición, íconos, combos, menús, etc. Todos estos elementos (a veces incluida la ventana misma) suelen denominarse "widgets", y son realmente los que se encargan de la interacción del usuario con las aplicaciones. Esta interacción corresponde tanto a la información que se presenta visualmente hacia el usuario, como a los "eventos" generados por este último hacia la aplicación (típicamente con el uso de teclado y mouse.)

Todas estas aplicaciones y sus ventanas, son administradas por un "sistema de ventanas" que puede variar con el sistema operativo en uso. Por ejemplo, los sistemas Unix y Linux utilizan un sistema de ventanas llamado X Window (también conocido como X11 o simplemente X.) El "sistema de ventanas" es el encargado de lidiar físicamente con el hardware correspondiente a fin de que las ventanas se visualicen y reciban los eventos.

El desarrollo de aplicaciones GUI consiste esencialmente en solicitar qué widgets se mostrarán en las ventanas, y qué hacer cuando el usuario interactúa con cada widget. Los widgets y los mecanismos para su acceso se proporcionan mediante librerías conocidas como "toolkits GUI", que son precísamente lo que veremos a lo largo del capítulo.

Es necesario advertir que el estudio de cualquier toolkit GUI requiere normalmente de una significativa dedicación de tiempo debido a la cantidad de detalles que suelen estar involucrados (libros enteros están dedicados a explicar un único toolkit.) Debido a esto, nosotros sólo los analizaremos muy superficialmente.

A diferencia de las aplicaciones que se ejecutan en la consola, en el ambiente GUI el programador no determina el momento y la secuencia en que el usuario "responde" a los requerimientos de la aplicación. Por el contrario, es la aplicación la que ahora "responde" a diversas clases de requerimientos del usuario (eventos) en el momento que éste lo desea. En consecuencia, el flujo de los programas GUI es más complicado que en los programas de consola, y consiste aproximadamente de los siguientes pasos:

  1. Inicializar el "toolkit GUI"

  2. Definir la ventana inicial y sus widgets

  3. Definir qué ocurre cuando el usuario interactua con los widgets

  4. Esperar a que el usuario utilice los widgets, y responder acorde

En el último paso, cuando la aplicación "responde" a los eventos del usuario, es cuando se realiza el trabajo real. Por ejemplo, iniciar algún proceso cuando el usuario presiona un botón de "OK".

En este capítulo presento dos toolkits diseñados para programar en lenguaje C (Motif y Gtk+), así como otros dos toolkits que requieren el uso de C. Como de seguro advertirá el lector, C (y otros lenguajes orientados a objeto) se prestan de forma natural para el desarrollo de toolkits y sus aplicaciones, por lo que cada vez es menos usual el uso del lenguaje C en este contexto [146] .

13.2. Motif

Motif es el toolkit estándar en la mayoría de sistemas Unix comerciales. Su uso está restringido al ambiente X Window, y la programación se lleva a cabo mediante lenguaje C. De los toolkits que veremos en este capítulo, Motif es el más antiguo por lo que ya es relativamente obsoleto. La única razón para su inclusión aquí es su amplia difusión en la familia tradicional Unix.

En mi opinión, de los tookits que veremos aquí, Motif se puede considerar el más difícil de aprender debido a diversos factores, tales como la organización del API, la cantidad de constantes y tipos de datos requeridos, el necesario uso de "Xt", y la extensión de los listados de código fuente.

Hasta hace unos años, Motif era un producto cuya licencia prohibía la apertura de su código fuente por lo que no estaba disponible gratuítamente en ambientes Linux (pero se podía comprar.) Esto dio lugar a proyectos como Lesstiff, un clone de Motif de aceptable calidad.

Actualmente Motif está disponible en sistemas Linux y similares mediante una licencia Open Source. A esa distribución se le conoce como OpenMotif.

Para desarrollar con Motif/OpenMotif es menester tener claros algunos conceptos:

13.2.1. Ambiente de Desarrollo X Window

El sistema X Window consiste de un servidor que interactúa directamente con el hardware (visualización e ingreso de datos), así como de "programas cliente" que realizan solicitudes al servidor (para mostar información y capturar eventos.) Esta comunicación cliente-servidor se realiza mediante un conjunto de mensajes conocidos como el "X protocol", el cual utiliza (entre otras) a las redes TCP/IP como medio de transporte. Por lo tanto, las aplicaciones que pretenden ser "clientes" de X Window deben ser capaces de armar los mencionados mensajes del "X protocol" para enviarlos por la red.

A fin de abstraer estas operaciones de bajo nivel, el ambiente de desarrollo X Window proporciona la librería "Xlib", la cual permite al desarrollador olvidarse del X protocol y solicitar acciones de más alto nivel, tales como crear ventanas, volcar gráficos y textos, manipular recursos, colores, etc.

Asimismo el ambiente de desarrollo X Window proporciona la librería "Xt" (X Toolkit Intrinsics), la cual proporciona un marco de trabajo para crear nuevos widgets con una estructura jerárquica y orientada a objetos. En sí, la librería Xt no incluye los widgets necesarios para crear aplicaciones útiles; por el contrario, los vendedores deberían aprovechar las facilidades de Xt para llevar a cabo esta tarea [147] .

Motif corresponde a un conjunto de widgets de apariencia y comportamiento estandarizado que trabajan en el marco de Xt. Desde el punto de vista del desarrollador, corresponde a una librería (toolkit) que permite acceder a dichos widgets. Si bien Motif utiliza internamente a Xt, no lo "oculta" completamente; esto significa que el desarrollador deberá invocar en algunos casos directamente a las rutinas de Xt. Análogamente, Xt está implementado a partir de Xlib, pero en ciertos casos el desarrollador también requerirá invocar directamente a esta última [148] .

13.2.2. Estándares GUI

Motif es el resultado del trabajo de un grupo de vendedores de Unix agrupados (en 1988) en la organización denominada "Open Software Foundation" cuya función fue la promulgación de estándares Unix. Ésta organización fue posteriormente fusionada con su similar (competidora) llamada "X/Open" en 1996 para crear la que ahora se conoce como "The Open Group". Actualmente esta organización se encarga de mantener y mejorar el estándar Motif.

Hasta hace unos años era frecuente hallar sistemas Unix comerciales utilizando el GUI conocido como Open Look, que es un toolkit anterior a Motif. No lo veremos aquí.

Es importante anotar que algunos vendedores importantes de Unix han manifestado su intención de abandonar Motif en favor de Gnome/Gtk+, aunque en la práctica esto todavía no se ha dado.

13.2.3. Ejemplo Básico: Hello World

El programa que veremos a continuación simplemente muestra una ventana conteniendo un texto y un botón. Este botón se encarga de culminar la aplicación al ser presionado:

Hello World con Motif

Saliéndome de la tradición, no presentaré este (sencillo) programa en un único archivo. Por el contrario, se presentarán primero algunas rutinas auxiliares que facilitarán la exposición y simplificarán el listado principal más adelante.

Todas estas rutinas auxiliares serán declaradas en un único archivo aux_lib.h el cual presento a continuación. Como veremos, el código fuente que hace uso de tipos y funciones de Motif debe declarar siempre el header <Xm/Xm.h> y posiblemente otros más dependiendo de los widgets que se utilicen:

#include <Xm/Xm.h>

Widget create_button(Widget parent,char *texto);
Widget create_label(Widget parent,char *texto);
void update_text(Widget w,char *texto);
Widget create_rowcolumn(Widget parent);
Widget create_rowcolumn_h(Widget parent);
Widget create_editor(Widget parent);
Widget create_fileselection(Widget parent,char *titulo);
char *get_selection(XtPointer ptr);
void set_editor_data(Widget editor, char *data);
char *get_editor_data(Widget editor);
char *get_data_from_file(const char *filename);
int save_data_to_file(const char *filename,const char *data);
Creación de Widgets

Como se dijo anteriormente, las aplicaciones GUI crean widgets para la interacción con el usuario. Estos widgets se presentan al interior de una ventana, y por tanto se dice que la ventana es el "widget padre" de aquéllos.

En el ejemplo que estamos confeccionando, requerimos presentar dos widgets (un texto o "Label" y un botón o "PushButton"), uno bajo el otro. Cuando se requiere presentar varios widgets a la vez, normalmente es necesario utilizar un "widget contenedor" que especifique la posición relativa de éstos. En nuestro caso, utilizaremos un widget llamado "RowColumn" con este propósito.

Por tanto, tendremos un Label y un PushButton que tendrán a un RowColumn como padre. A su vez, el RowColumn tendrá como padre a la ventana.

Las rutinas auxiliares create_label() y create_button() permiten crear widgets de tipo Label y PushButton, respectivamente:

#include "Xm/Xm.h"
#include "Xm/Label.h"
#include "aux_lib.h"

Widget create_label(Widget parent,char *texto)
{
XmString texto_motif;
Widget label;
Arg arg[1];

texto_motif=XmStringCreateSimple(texto);
XtSetArg(arg[0],XmNlabelString,texto_motif);
label=XmCreateLabel(parent,"label",arg,1);
XmStringFree(texto_motif);
return label;
}
#include "Xm/Xm.h"
#include "Xm/PushB.h"
#include "aux_lib.h"

Widget create_button(Widget parent,char *texto)
{
XmString texto_motif;
Widget button;
Arg arg[1];

texto_motif=XmStringCreateSimple(texto);
XtSetArg(arg[0],XmNlabelString,texto_motif);
button=XmCreatePushButton(parent,"boton",arg,1);
XmStringFree(texto_motif);
return button;
}

Como se aprecia, hay bastante código para un fin tan sencillo. En realidad es posible reducirlo un poco, pero de esta forma nos será más útil para la exposición.

En primer lugar, los widgets de Motif tienen (sin excepción) el tipo "Widget" (en realidad es un tipo definido en Xt.) En tanto nuestras rutinas crean widgets, retornarán objetos de este tipo.

Ambas rutinas requieren conocer el widget "padre", el cual es necesario para la creación de nuevos widgets. Con este mismo fin, Motif proporciona un conjunto de rutinas cuyo prefijo es XmCreate*. Su primer argumento es el widget padre; el segundo es el "nombre del widget" (que para nosotros será poco relevante); el tercero es un "array de recursos" y el cuarto es la longitud del mencionado array.

Los "recursos" corresponden a los "atributos" o "propiedades" del widget. Cuando se crea un nuevo widget con las funciones XmCreate* es posible especificar los recursos iniciales del mismo, precísamente mediante un "array de recursos". En las funciones anteriores el array contuvo exactamente un recurso, correspondiente en ambos casos al "texto" del widget (recurso XmNlabelString.)

Los recursos se inscriben en el "array de recursos" mediante la función XtSetArg(), la cual recibe como argumentos: Un elemento del array de recursos, el tipo de recurso que se está inscribiendo, y el valor del recurso. Suponiendo por un momento que deseamos crear un widget estableciendo dos de sus recursos, tedríamos que crear un array de recursos de (al menos) dos elementos e invocaríamos dos veces a XtSetArg():

Arg recursos[2];

XtSetArg(recursos[0],XmNwidth,318);
XtSetArg(recursos[1],XmNlabelString,un_texto);

XmCreate*(parent,"nombre",recursos,2);

Los widgets de Motif tienen una abundante cantidad de recursos, los cuales deben consultarse en el libro "Motif Programmer’s Reference" que publica The Open Group.

Sólo falta explicar la rutina XmStringCreateSimple(). Motif utiliza un tipo de dato llamado XmString para almacenar cadenas de texto. Estas cadenas tienen la capacidad de contener información complementaria al texto como puede ser el juego de caracteres, el tipo y tamaño de letra y colores. La rutina XmStringCreateSimple() permite crear un XmString a partir de una cadena de lenguaje C (char *.)

El XmString es utilizado durante la creación del widget y luego debe ser liberado de la memoria (con XmStringFree()) pues el widget hace una copia de aquél durante su creación.

Creación de Widget contenedor
#include "Xm/Xm.h"
#include "Xm/RowColumn.h"
#include "aux_lib.h"

Widget create_rowcolumn(Widget parent)
{
return XmCreateRowColumn(parent,"rowcolumn",NULL,0);
}

La creación del widget RowColumn es más sencilla que en los casos anteriores debido a que no especificaremos ningún recurso. El widget RowColumn por omisión presenta a sus widgets "hijos" uno bajo el otro a no ser que se especifique el recurso XmNorientation con el valor XmHORIZONTAL como en la siguiente función auxiliar:

#include "Xm/Xm.h"
#include "Xm/RowColumn.h"
#include "aux_lib.h"

Widget create_rowcolumn_h(Widget parent)
{
Arg arg[1];

XtSetArg(arg[0],XmNorientation,XmHORIZONTAL);
return XmCreateRowColumn(parent,"rowcolumn",arg,1);
}
Programa principal HelloWorld

Ahora sí estamos preparados para presentar el programa HelloWorld.c que hace uso de las anteriores rutinas:

#include "Xm/Xm.h"
#include <stdio.h>
#include <stdlib.h>
#include "aux_lib.h"

void pushed(Widget widget, XtPointer client_data, XtPointer call_data)
{
printf("Adios!\n");
exit(0);
}

int main(int argc,char **argv)
{
Widget shell,boton,label,rowcolumn;
XtAppContext app;

shell=XtAppInitialize(&app,"Applicacion",NULL,0,&argc,argv,
	NULL,NULL,0);
rowcolumn=create_rowcolumn(shell);
label=create_label(rowcolumn,"Hola Mundo!");
boton=create_button(rowcolumn,"Salir");

XtAddCallback(boton,XmNactivateCallback,pushed,NULL);

XtManageChild(boton);
XtManageChild(label);
XtManageChild(rowcolumn);

XtRealizeWidget(shell);
XtAppMainLoop(app);

return 0;
}

El listado no contiene código relacionado a ningún widget en particular, por lo que basta con incluir el header Xm/Xm.h. Explicaremos la función pushed() un poco más abajo.

El programa inicia su ejecución con una invocación a la rutina XtAppInitialize(), la cual recibe un gran número de argumentos, casi todos nulos. Esta rutina se encarga de inicializar el toolkit Xt y debería ser llamada al principio de cualquier programa Motif [149] . Su primer argumento es un puntero a una variable de tipo "Application Context" que es una estructura utilizada para almacenar diversos parámetros que se aplican durante las operaciones gráficas. Nosotros no lo utilizaremos directamente, pero Xt nos obliga a "obtenerlo" durante la inicialización, y a proporcionárselo durante la llamada a XtAppMainLoop() que veremos más adelante.

El segundo argumento es el "nombre de la aplicación", el cual también será poco relevante para nosotros. El resto de argumentos será normalmente cero o NULL, a excepción de &argc y argv, los cuales se utilizan para procesar ciertas opciones de la línea de comandos que son estándares en las aplicaciones de X [150] .

Como valor de retorno obtenemos un "shell widget" (una ventana) que se puede utilizar para proceder a la creación de los widgets. En primer lugar creamos el RowColumn (especificando a la ventana como padre) y a continuación el Label y el PushButton (especificando al RowColumn como padre.)

La invocación a XtAddCallback() permite establecer una función que será invocada cuando el usuario interactúe con los widgets (la famosa función "callback".) En nuestro caso, cuando el usuario presione el widget “boton”, se ejecutará la "función callback" pushed(). En los widgets sencillos normalmente se especificará XmNactivateCallback como segundo argumento de XtAddCallback(). El cuarto argumento puede contener un puntero a alguna información que se desee hacer accesible en la función callback, o NULL en caso contrario.

La rutina XtManageChild() ("gerenciar widget hijo") comunica a Xt que deseamos que el widget indicado sea mostrado en el momento en que se muestre la ventana principal, y es necesario invocarla para todos los widgets "hijos".

Finalmente, la rutina XtRealizeWidget() muestra la ventana principal (y todos sus widgets hijos gerenciados por Xt.)

Una vez que se ha concluido la construcción y el lanzamiento de los widgets, los programas normalmente "esperan" a que ocurran eventos significativos y así disparar los callbacks correspondientes. La rutina XtAppMainLoop() permite ingresar en este estado de "espera", y normalmente es la última rutina de main().

Terminamos esta sección comentando la función callback pushed(). En Motif (o mejor, en Xt) las funciones callback tienen como prototipo:

void nombre_funcion(Widget widget,
	XtPointer client_data,
	XtPointer call_data);

Evidentemente el “widget” corresponde a aquél en el cual ocurrió un evento que desencadenó la invocación al callback. El puntero client_data se puede utilizar para pasar cualquier información útil al callback, y se programa con el cuarto argumento de XtAddCallback(). Esto es utilizado con frecuencia cuando el mismo callback es utilizado para varios eventos.

Por último, call_data normalmente apuntará a una estructura cuyo tipo dependerá del widget y del evento asociados. La información almacenada en estas estructuras proporciona ciertos detalles útiles acerca del evento ocurrido. Por ejemplo, cuando se selecciona cualquier opción de un menú se generará un evento y la llamada correspondiente a la función callback; sin embargo, es la estructura apuntada por call_data la que indicará exactamente cuál de las opciones del menú fue la que el usuario eligió. Un ejemplo de estas estructuras lo veremos en el siguiente programa.

La compilación de los programas Motif normalmente requieren del enlace con la "librería Motif" (libXm) por lo que el código anterior se podría compilar con un comando similar a:

cc -o HelloWorld HelloWorld.c aux_button.c
	aux_label.c aux_rowcolumn.c -lXm

Sin embargo, siempre será preferible emplear un Makefile como el que se muestra al final de esta sección. En ocasiones también es necesario especificar otras librerías relacionadas (por ejemplo, con -lX11 y -lXt), pero lo mejor es consultar la documentación respectiva.

13.2.4. Ejemplo: Editor de Textos Motif

El programa que presentamos ahora consiste en un editor de textos que permite seleccionar y cargar el contenido de un archivo (de texto) al área de edición, y posteriormente guardarla.

Editor de textos con Motif
Rutinas auxiliares de manejo de archivos

Estas rutinas no tienen nada que ver con Motif; su función es leer y escribir buffers de caracteres desde y hacia archivos. No requiren explicación.

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

char *get_data_from_file(const char *filename)
{
struct stat stat_struct;
FILE *fp=fopen(filename,"r");
if(fp==NULL)
	return NULL;
if(stat(filename,&stat_struct)==-1)
	return NULL;
char *ptr=(char *)calloc(stat_struct.st_size+1,sizeof(char));
if(ptr==NULL)
	return NULL;
fread(ptr,1,stat_struct.st_size,fp);
fclose(fp);
return ptr;
}

int save_data_to_file(const char *filename,const char *data)
{
FILE *fp=fopen(filename,"w");
if(!fp)
	return 0;
fwrite(data,1,strlen(data),fp);
fclose(fp);
return 1;
}
Rutinas auxiliares de widget de edición

El widget "Text" permite definir una zona de edición de textos con mucha facilidad. La primera rutina crea uno de estos widgets con algunos recursos convenientes (edición multilínea, 80 columnas y 25 filas.) Las otras dos simplemente copian un buffer de memoria al área de edición y viceversa.

#include "Xm/Xm.h"
#include "Xm/Text.h"
#include "aux_lib.h"

Widget create_editor(Widget parent)
{
Arg arg[3];

XtSetArg(arg[0],XmNeditMode,XmMULTI_LINE_EDIT);
XtSetArg(arg[1],XmNcolumns,80);
XtSetArg(arg[2],XmNrows,25);

return XmCreateScrolledText(parent,"Editor",arg,3);
}

void set_editor_data(Widget editor, char *data)
{
XmTextSetString(editor,data);
}

char *get_editor_data(Widget editor)
{
return XmTextGetString(editor);
}
Modificación del texto de un Label

Esta sencilla rutina modificará el recurso XmNlabelString de un Label o un PushButton.

#include "Xm/Xm.h"
//#include "Xm/Label.h"
#include "aux_lib.h"

void update_text(Widget w,char *texto)
{
Arg arg[1];
XmString texto_motif;

texto_motif=XmStringCreateSimple(texto);
XtSetArg(arg[0],XmNlabelString,texto_motif);
XtSetValues(w, arg, 1);
XmStringFree(texto_motif);
}

Este código es muy similar al de las rutinas de creación del Label y del PushButton. Como se aprecia, es necesario recurrir a la rutina (de Xt) XtSetValues().

Rutinas de Diálogo de Selección de Archivo

Las rutinas que se presentan a continuación permiten crear un nuevo widget de tipo FileSelectionDialog, y obtener el nombre del archivo seleccionado a partir del “call_data” de una función callback:

#include "Xm/Xm.h"
#include "Xm/FileSB.h"
#include "aux_lib.h"

Widget create_fileselection(Widget parent,char *titulo)
{
Arg arg[2];

XtSetArg(arg[0],XmNtitle,titulo);
XtSetArg(arg[1],XmNdialogStyle,XmDIALOG_FULL_APPLICATION_MODAL);
return XmCreateFileSelectionDialog(parent,"FileSelection",arg,2);
}

char *get_selection(XtPointer ptr)
{
char *rpta;

XmFileSelectionBoxCallbackStruct *callBackStruct=
	(XmFileSelectionBoxCallbackStruct *)ptr;
XmStringGetLtoR(callBackStruct->value,XmSTRING_DEFAULT_CHARSET,&rpta);
return rpta;
}

Hemos forzado a que este diálogo sea "modal", es decir, que no permita la interacción con otras ventanas de la aplicación a fin de mantener la coherencia de la misma (por ejemplo, no tendría sentido tener al mismo tiempo abiertos los diálogos de "abrir" y "guardar" archivos.) Para esto hemos hecho uso del recurso XmNdialogStyle.

En cuanto a la obtención del nombre de archivo, la documentación del evento correspondiente a los botones de un FileSelectionDialog indica que se proporciona un puntero a estructura de tipo XmFileSelectionBoxCallbackStruct (la cual se obtiene con un cast) y esta a su vez contiene un miembro llamado “value” que corresponde a un XmString. Finalmente se obtiene una cadena “char *” mediante una función auxiliar.

Cuerpo del Editor

Aquí va:

#include "Xm/Xm.h"
#include <stdio.h>
#include <stdlib.h>
#include "aux_lib.h"

#define UNNAMED "Sin Nombre"

Widget shell, fileSelectorAbrir, fileSelectorGuardar, label_file, editor;

void guardar(Widget widget, XtPointer client_data, XtPointer call_data)
{
XtManageChild(fileSelectorGuardar);
}

void guardar_ok(Widget widget, XtPointer client_data, XtPointer call_data)
{
char *filename=get_selection(call_data);
char *data=get_editor_data(editor);
if(save_data_to_file(filename,data))
	update_text(label_file,filename);
else
	fprintf(stderr,"No se pudo grabar %s\n",filename);
XtFree(data);
XtFree(filename);
XtUnmanageChild(widget);
}

void abrir(Widget widget, XtPointer client_data, XtPointer call_data)
{
XtManageChild(fileSelectorAbrir);
}

void abrir_ok(Widget widget, XtPointer client_data, XtPointer call_data)
{
char *filename=get_selection(call_data);
char *ptr=get_data_from_file(filename);
if(ptr)
	{
	set_editor_data(editor,ptr);
	free(ptr);
	update_text(label_file,filename);
	}
XtFree(filename);
XtUnmanageChild(widget);
}

void fs_cancel(Widget widget, XtPointer client_data, XtPointer call_data)
{
XtUnmanageChild(widget);
}

int main(int argc,char **argv)
{
Widget boton_load,boton_save,rowcolumn,rowh;
XtAppContext app;

shell=XtAppInitialize(&app,"Applicacion",NULL,0,&argc,argv,
	NULL,NULL,0);

rowcolumn=create_rowcolumn(shell);
rowh=create_rowcolumn_h(rowcolumn);

boton_load=create_button(rowh,"Abrir");
XtAddCallback(boton_load,XmNactivateCallback,abrir,NULL);

boton_save=create_button(rowh,"Guardar");
XtAddCallback(boton_save,XmNactivateCallback,guardar,NULL);

editor=create_editor(rowcolumn);
label_file=create_label(rowcolumn,UNNAMED);

fileSelectorAbrir=create_fileselection(shell,"Elija Archivo a Abrir");
XtAddCallback(fileSelectorAbrir,XmNokCallback,abrir_ok,NULL);
XtAddCallback(fileSelectorAbrir,XmNcancelCallback,fs_cancel,NULL);

fileSelectorGuardar=create_fileselection(shell,"Guardar como...");
XtAddCallback(fileSelectorGuardar,XmNokCallback,guardar_ok,NULL);
XtAddCallback(fileSelectorGuardar,XmNcancelCallback,fs_cancel,NULL);

XtManageChild(boton_load);
XtManageChild(boton_save);
XtManageChild(label_file);
XtManageChild(editor);
XtManageChild(rowh);
XtManageChild(rowcolumn);
XtRealizeWidget(shell);
XtAppMainLoop(app);

return 0;
}

Como se aprecia, algunos widgets se crean fuera de las funciones debido a que serán accesados desde los callbacks. Los callbacks más sencillos son abrir() y guardar() que respectivamente muestran los diálogos de "abrir archivo" y "guardar archivo", los cuales han sido anteriormente creados (pero no mostrados) en main().

Los callbacks abrir_ok() y guardar_ok() son fáciles de comprender a partir de las rutinas explicadas anteriormente. Ambas culminan con una llamada a XtUnmanageChild() a fin de ocultar el diálogo correspondiente. Evidentemente, estos se ejecutan cuando el usuario presiona el botón "OK" en los diálogos de abrir y cerrar archivo, respectivamente.

El callback fs_cancel() se ejecuta cuando el usuario presiona el botón "CANCEL" de cualquiera de los diálogos de selección de archivo (en ambos casos sencillamente cierra el diálogo.)

En cuanto a main(), es interesante apreciar que la ventana se constituye de un RowColumn vertical, el cual en su primera fila contiene a otro RowColumn, pero esta vez horizontal, usado para contener los dos botones. El área de texto se establece en la segunda fila, y en la tercera se muestra un texto conteniendo el nombre de archivo actual.

Finalmente, los widgets de selección de archivo tienen cada uno dos callbacks correspondientes a los botones OK y CANCEL respectivamente, los cuales son especificados mediante las constantes XmNokCallback y XmNcancelCallback.

Makefile de los ejemplos

Los ejemplos de esta sección se pueden construir con el siguiente Makefile:

CFLAGS = -Wall

OBJETOS = aux_label.o aux_button.o aux_rowcolumn.o \
	aux_rowcolumn_h.o aux_editor.o aux_fileselection.o \
	aux_update_text.o aux_file.o

all: HelloWorld Editor

HelloWorld: HelloWorld.o aux_lib.a
	cc -o $@ $< aux_lib.a -lXm

Editor: Editor.o aux_lib.a
	cc -o $@ $< aux_lib.a -lXm

aux_lib.a: $(OBJETOS)
	ar cr $@ $(OBJETOS)

clean:
	rm -f *.o HelloWorld Editor aux_lib.a
UIL y MRM

Motif proporciona un lenguaje auxiliar de especificación de widgets llamado User Interface Language (UIL). Mediante este lenguaje, el desarrollador crea un archivo (de texto) en el que se especifica el diseño gráfico de las ventanas. A partir de este archivo se genera una versión binaria optimizada, y en tiempo de ejecución la aplicación accede al mismo mediante un conjunto de rutinas conocidas como "Motif Resource Manager" (MRM), las cuales crean los widgets especificados. La aplicación posteriormente enlaza los callbacks y se desarrolla de manera normal.

La ventaja evidente de este procedimiento es que se separa el diseño gráfico del código fuente (la presentación de la aplicación.) Por ejemplo, una modificación gráfica requerirá la modificación del archivo UIL, mas no la recompilación de todo el código fuente de la aplicación.

13.3. GTK+

Resumiendo algunas líneas de la documentación oficial:

Gtk+ [151] , el "Gimp ToolKit", es una librería que sirve para crear interfaces gráficas de usuario (GUI) en ciertas plataformas, especialmente Linux/Unix con X Window [152] . Está diseñada para ser pequeña y eficiente, y desarrollar aplicaciones pequeñas y grandes, ofreciendo un completo conjunto de Widgets.

Esta librería está escrita en lenguaje C, pero su diseño tiene una fuerte orientación a objetos.

El desarrollo de programas se puede hacer con diversos lenguajes, empezando por C, pues existen diversos proyectos que han enlazado las rutinas de Gtk+ para usarse desde otros lenguajes (el sitio web de Gtk+ menciona C++, Guile, Perl, Python, TOM, Ada95, Objective C, Free Pascal, Eiffel, Java y C#.)

Su página principal (desde donde se puede descargar) es http://www.gtk.org/ .

13.3.1. Estructura de programas Gtk+

En Gtk+ la estructura de los programas es más o menos así:

  1. Inicializar gtk con gtk_init(&argc,&argv)

  2. Crear la ventana principal con gtk_window_new()

  3. Crear widgets con rutinas especializadas (por ejemplo, gtk_label_new(), gtk_button_new_with_label(), etc.)

  4. Definir los Signal Handlers para los widgets que lo necesiten (típicamente con gtk_signal_connect()

  5. Añadir los widgets a sus contenedores y/o a la ventana principal (por ejemplo, gtk_container_add(), gtk_box_pack_start(), etc.)

  6. Mostrar los widgets y la ventana principal (gtk_widget_show(), etc.)

  7. Invocar a gtk_main()

13.3.2. Señales y eventos

Como la mayoría de los entornos de programación GUI, Gtk+ se basa en la generación de "señales" para que los programas cumplan su cometido. Nótese que estas "señales" NO son las señales del sistema operativo Unix.

Existe un conjunto de "señales" definidas por Gtk+ para determinados tipos de widgets. Éstas se generan cuando ocurren ciertas condiciones o acciones del usuario (por ejemplo, al presionar un botón se genera una señal llamada "clicked".)

Sin embargo, Gtk+ también procesa otras condiciones generadas por el "sistema operativo gráfico" que normalmente son de nivel más bajo; a ésto le denomina "eventos".

El tratamiento de las señales y eventos es muy similar, y en muchos casos uno puede asumir que se trata de lo mismo: Cuando una señal (o un evento) ocurre, Gtk+ invoca normalmente a una función [153] definida por el usuario. A esta función se le denomina "signal handler" , "event handler", o "callback". Lamentablemente, el prototipo de estas funciones (handlers) puede variar dependiendo de la señal o evento considerado. Por ejemplo, una barra de scrolling que se desplaza a cierta posición normalmente incluye información adicional relacionada con la nueva posición. Esto no tiene sentido en otros widgets (por ejemplo, en un botón.)

13.3.3. Ejemplo Básico: Hello World

El programa mostrado a continuación [154] (gtk-helloworld.c) genera una ventana conteniendo un texto y un botón. Cuando este botón es presionado, un mensaje aparece impreso en el terminal desde donde se ejecuta el programa (se trata de nuestro signal handler hello().)

#include <gtk/gtk.h>
#include <stdlib.h>

void hello(GtkWidget *widget, gpointer data)
{
g_print("Adios!\n");
exit(0);
}

gint evento_delete(GtkWidget *widget,
	GdkEvent *event, gpointer data)
{
gtk_main_quit();
return TRUE;
}

int main(int argc,char **argv)
{
GtkWidget *ventana, *caja, *texto, *boton;
gtk_init(&argc,&argv);

ventana=gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(ventana),"Hola mundo!");
gtk_container_set_border_width(GTK_CONTAINER(ventana),10);

caja=gtk_vbox_new(FALSE,5);
texto=gtk_label_new("Hola Mundo con GTK+");
boton=gtk_button_new_with_label("Presiona el Boton");

gtk_container_add(GTK_CONTAINER(ventana),caja);
gtk_box_pack_start(GTK_BOX(caja),texto,TRUE,TRUE,0);
gtk_box_pack_start(GTK_BOX(caja),boton,TRUE,TRUE,0);

g_signal_connect(GTK_OBJECT(boton),"clicked",
		G_CALLBACK(hello),NULL);
g_signal_connect(G_OBJECT(ventana),"delete_event",
		G_CALLBACK(evento_delete),NULL);

gtk_widget_show(texto);
gtk_widget_show(boton);
gtk_widget_show(caja);
gtk_widget_show(ventana);

gtk_main();
return 0;
}

Una muestra del resultado:

Hello World con Gtk+

Como se aprecia al inicio de main(), los widgets creados son:

  • ventana: La ventana principal

  • caja: Un contenedor "vertical" que permite posicionar al "texto" y al "botón" (no se visualiza)

  • texto: Un texto simple

  • boton: Un botón simple

Intente identificar éstos en el gráfico que se presentó arriba.

El primer widget ("ventana") corresponde a la ventana principal del programa. Ésta es creada mediante gtk_window_new() y en casi todos los casos recibe como argumento el "tipo de ventana" GTK_WINDOW_TOPLEVEL [155] . A esta "ventana" se le proporciona un título mediante gtk_window_set_title() y se le especifica un "ancho desde el borde" de 10 pixels mediante gtk_container_set_border_width() [156] . Como se aprecia en estas rutinas, Gtk+ hace un abundante uso de macros de verificación y conversión, usados para cambiar el tipo de los punteros enviados como argumento a las funciones Gtk+. Por ejemplo, para definir el título de la ventana utilizamos

gtk_window_set_title(GTK`WINDOW(ventana),"Hola mundo!");

lo cual convierte el puntero "ventana" (de tipo GtkWidget*) a un puntero más especializado de tipo GtkWindow* capaz de recibir un título. Esto evita construcciones tales como:

gtk_window_set_title((GtkWindow*)ventana,"Hola mundo!");

y además realiza algunas verificaciones adicionales.

Cajas contenedoras

En Gtk+ (al igual que en muchos otros toolkits) es necesario crear "cajas contenedoras" cuando se desea desplegar varios widgets en sentido horizontal o vertical (como en nuestro caso, con el "texto" y el "botón" uno debajo del otro.) En nuestro programa, este widget "caja" se ha creado mediante gtk_vbox_new() (también se pudo hacer una caja horizontal mediante gtk_hbox_new(), pruébelo!) Su primer argumento (FALSE) significa que los widgets a ser contenidos no tienen el mismo tamaño (no son elementos homogeneos) y el segundo (5) corresponde al espaciamiento entre los widgets contenidos.

Las rutinas de creación de widgets gtk_label_new() y gtk_button_new_with_label() tienen propósitos evidentes.

Luego se añade la caja contenedora vertical a la ventana con gtk_container_add(). Nótese que la "ventana" también es una clase de contenedor (que contiene a la caja) por lo que se emplea el macro GTK_CONTAINER(). Finalmente el "texto" y el "botón" se añaden a la caja vertical con gtk_box_pack_start(), la cual se emplea tanto para cajas verticales como horizontales [157] . El tercer argumento (en nuestro caso, TRUE) significa que el espacio ocupado por un widget contenido se expandirá lo necesario para cubrir el espacio que quede libre en el contenedor; el cuarto argumento (también TRUE) sólo tiene sentido si el anterior es TRUE, e indica que el área expandida del widget será ocupada totalmente por el mismo widget o por "relleno de fondo". El último argumento (en nuestro caso, cero) corresponde al espacio entre el borde del widget y el exterior.

Conexión de Señales

Como indicamos, nuestro signal handler hello() debe ser ejecutado cada vez que se presiona el botón. Esto se consigue con la rutina g_signal_connect() la cual recibe como primer argumento el widget emisor de la señal (pero con conversión a G_OBJECT()); el segundo argumento es el nombre de la señal (hay que consultar la referencia de cada widget para conocer qué señales emite); el tercero es el signal handler (con conversión a G_CALLBACK()) y el último es un puntero a un dato opcional que puede emplearse en el signal handler.

Por otro lado, se ha asociado otro signal handler para el evento "delete_event". Nótese que su prototipo es distinto al del handler anterior [158] . Este "evento" lo envía el sistema gráfico cuando el usuario solicita la eliminación de la ventana (generalmente con un click de mouse en una esquina de la misma.) Nuestro handler en ese caso invoca a gtk_main_quit() que termina la aplicación [159] .

Finalmente, cada widget que se pretende que aparezca en pantalla debe ser "mostrado" con gtk_widget_show(). Pruebe a eliminar algunas de estas llamadas y vea la diferencia.

Compilando Hello World!

Los programas Gtk+ requieren de una serie de opciones pasadas al compilador para poder compilarse. Estas opciones son extensas y poco portables.

A fin de facilitar la compilación, Gtk+ utiliza el script pkg-config (desarrollado por programadores relacionados al proyecto Gnome) que está disponible en .URL www.freedesktop.org . Los sistemas Linux actuales normalmente ya proporcionan pkg-config cuando se instalan los paquetes de desarrollo Gtk+.

A continuación la salida que obtuve con pkg-config para mi sistema:

$ pkg-config --cflags gtk+-2.0
-DXTHREADS -I/usr/include/gtk-2.0 -I/usr/lib/gtk-2.0/inclu
de -I/usr/X11R6/include -I/usr/include/atk-1.0 -I/usr/incl
ude/pango-1.0 -I/usr/include/freetype2 -I/usr/include/glib
-2.0 -I/usr/lib/glib-2.0/include
$ pkg-config --libs gtk+-2.0
-Wl,--export-dynamic -lgtk-x11-2.0 -lgdk-x11-2.0 -latk-1.0
 -lgdk_pixbuf-2.0 -lm -lpangoxft-1.0 -lpangox-1.0 -lpango-
1.0 -lgobject-2.0 -lgmodule-2.0 -ldl -lglib-2.0
$

A fin de no tener que tipear manualmente todo esto cada vez que invocamos al compilador, el siguiente comando lo puede hacer automáticamente:

gcc -Wall -g -o gtk-helloworld gtk-helloworld.c $(pkg-config
       --cflags --libs gtk+-2.0)

--cflags son opciones de compilación y --libs son opciones del enlazador.

13.3.4. Ejemplo: Editor de Textos

Editor de textos con Gtk+

Este programa ilustra el uso de más widgets. En breve, se trata de un editor de textos que permite abrir un archivo para leer su contenido, y guardarlo (posiblemente con otro nombre.)

#include <gtk/gtk.h>
#include <stdio.h>

#define UNNAMED "Sin Nombre"

char *get_data_from_file(const char *filename);
int save_data_to_file(const char *filename,const char *data);

void abrir_archivo(GtkWidget *widget, gpointer dat);
void guardar_archivo(GtkWidget *widget, gpointer dat);
gint evento_delete(GtkWidget *widget,
	GdkEvent *event, gpointer data);
void abrir_ok(GtkWidget *w,GtkFileSelection *fs);
void close_dialog(GtkWidget *w,GtkFileSelection *fs);

GtkWidget *data, *ventana, *cajav, *cajah, *texto,
	*boton_guardar, *boton_abrir, *file_open, *file_guardar;
GtkTextBuffer *buffer;

int main(int argc,char **argv)
{
gtk_init(&argc,&argv);

ventana=gtk_window_new(GTK_WINDOW_TOPLEVEL);
cajav=gtk_vbox_new(FALSE,5);
cajah=gtk_hbox_new(FALSE,5);
texto=gtk_label_new(UNNAMED);
boton_abrir=gtk_button_new_with_label("Abrir");
boton_guardar=gtk_button_new_with_label("Guardar");
data=gtk_text_view_new();

gtk_window_set_title(GTK_WINDOW(ventana),"Editor con GTK+");
gtk_container_set_border_width(GTK_CONTAINER(ventana),5);

gtk_widget_set_size_request(data,600,300);
buffer=gtk_text_view_get_buffer(GTK_TEXT_VIEW(data));

gtk_container_add(GTK_CONTAINER(ventana),cajav);
gtk_box_pack_start(GTK_BOX(cajav),cajah,TRUE,TRUE,0);
gtk_box_pack_start(GTK_BOX(cajav),data,TRUE,TRUE,0);
gtk_box_pack_start(GTK_BOX(cajav),texto,FALSE,TRUE,0);
gtk_box_pack_start(GTK_BOX(cajah),boton_abrir,FALSE,TRUE,0);
gtk_box_pack_start(GTK_BOX(cajah),boton_guardar,FALSE,TRUE,0);

gtk_signal_connect(GTK_OBJECT(boton_abrir),"clicked",
		GTK_SIGNAL_FUNC(abrir_archivo),NULL);
gtk_signal_connect(GTK_OBJECT(boton_guardar),"clicked",
		GTK_SIGNAL_FUNC(guardar_archivo),NULL);
gtk_signal_connect(GTK_OBJECT(ventana),"delete_event",
		GTK_SIGNAL_FUNC(evento_delete),NULL);

gtk_widget_show(texto);
gtk_widget_show(boton_abrir); gtk_widget_show(data);
gtk_widget_show(boton_guardar); gtk_widget_show(cajah);
gtk_widget_show(cajav); gtk_widget_show(ventana);
gtk_main();
return 0;
}

void guardar_ok(GtkWidget *w,GtkFileSelection *fs)
{
gchar *ptr;
GtkTextIter inicio,fin;
const char *archivo=gtk_file_selection_get_filename(
	GTK_FILE_SELECTION(fs));

gtk_text_buffer_get_bounds(buffer,&inicio,&fin);
ptr=gtk_text_buffer_get_text(buffer,&inicio,&fin,FALSE);
if(save_data_to_file(archivo,ptr))
	gtk_label_set_text(GTK_LABEL(texto),archivo);
else
	fprintf(stderr,"Error grabando archivo\n");
g_free(ptr);
gtk_widget_hide(GTK_WIDGET(fs));
}

void guardar_archivo(GtkWidget *widget, gpointer dat)
{
file_guardar=gtk_file_selection_new("Nombre para Guardar");
g_signal_connect(G_OBJECT(GTK_FILE_SELECTION(
	file_guardar)->ok_button),"clicked",
	G_CALLBACK(guardar_ok),G_OBJECT(file_guardar));
g_signal_connect(G_OBJECT( GTK_FILE_SELECTION(
	file_guardar)->cancel_button), "clicked",
	G_CALLBACK(close_dialog),G_OBJECT(file_guardar));
gtk_widget_show(file_guardar);
}

void abrir_archivo(GtkWidget *widget, gpointer dat)
{
file_open=gtk_file_selection_new("Seleccione archivo");
g_signal_connect(G_OBJECT(GTK_FILE_SELECTION(
	file_open)->ok_button),"clicked",
	G_CALLBACK(abrir_ok),G_OBJECT(file_open));
g_signal_connect(G_OBJECT( GTK_FILE_SELECTION(
	file_open)->cancel_button), "clicked",
	G_CALLBACK(close_dialog),G_OBJECT(file_open));
gtk_widget_show(file_open);
}

void close_dialog(GtkWidget *w,GtkFileSelection *fs)
{
gtk_widget_hide(GTK_WIDGET(fs));
}

void abrir_ok(GtkWidget *w,GtkFileSelection *fs)
{
const char *archivo=gtk_file_selection_get_filename(
	GTK_FILE_SELECTION(fs));
char *ptr=get_data_from_file(archivo);
if(ptr)
	{
	gtk_text_buffer_set_text(buffer,ptr,-1);
	gtk_label_set_text(GTK_LABEL(texto),archivo);
	}
else
	fprintf(stderr,"No se pudo abrir archivo %s\n",archivo);
gtk_widget_hide(GTK_WIDGET(fs));
}

gint evento_delete(GtkWidget *widget,
	GdkEvent *event, gpointer data)
{
gtk_main_quit();
return TRUE;
}

Este programa hace uso de dos rutinas auxiliares de lectura y escritura en archivos (que no tienen relación con el tema de GUI) y que se proporcionaron en el archivo aux_file.c en la sección anterior (Motif.)

Tal como se indicó, para compilar usaremos algo como lo que sigue:

gcc -Wall -g -o gtk-editor gtk-editor.c aux_file.c $(pkg-config
    --cflags --libs gtk+-2.0)

Algunos puntos dignos de comentario:

  • Se ha creado dos "cajas" contenedoras, una vertical y otra horizontal para emplazar adecuadamente los widgets participantes

  • El tamaño del widget de edición de textos se ha forzado a un tamaño predeterminado mediante gtk_widget_set_size_request()

  • El widget de edición (GtkTextView) se encarga de presentar el contenido de un "buffer de edición" (tipo GtkTextBuffer.) Por tanto, para modificar la información que se presenta en un GtkTextView, sólo se debe modificar el GtkTextBuffer

  • Para guardar el contenido del buffer de edición se emplean "iteradores", que son punteros a distintas posiciones del buffer (se obtienen iteradores que apuntan al inicio y al final del mismo mediante gtk_text_buffer_get_bounds())

  • En la función abrir_archivo() se definen los handlers de la señal "clicked" para los botones del diálogo de selección de archivo

Como se ha podido apreciar, Gtk+ requiere de numerosas y repetitivas llamadas a rutinas de nombres incómodos; sin embargo, mucho de este código es fácilmente aislable en pequeñas subrutinas auxiliares.

A continuación un Makefile que compila los programas de esta sección:

CFLAGS = $(shell pkg-config --cflags gtk+-2.0) -g -Wall
LDFLAGS = $(shell pkg-config --libs gtk+-2.0)

all: gtk-helloworld gtk-editor

gtk-helloworld: gtk-helloworld.o
	cc -o $@ $< $(LDFLAGS)

gtk-editor: gtk-editor.o aux_file.o
	cc -o $@ gtk-editor.o aux_file.o $(LDFLAGS)

clean:
	rm -f gtk-helloworld gtk-editor *.o
Glade

Glade es una herramienta cuya función es acelerar el diseño gráfico de aplicaciones Gtk+ y Gnome (lo que se conoce a veces como una herramienta RAD.) Glade genera un archivo de especificación de widgets en formato XML, el cual es posteriormente cargado por las aplicaciones mediante ciertas rutinas de una librería auxiliar (libglade.)

Desarrollo Gtk+ con Glade

13.4. QT

Resumiendo la explicación del sitio Web de Trolltech .URL http://www.trolltech.com/products/qt.html www.tolltech.com , Qt es un framework de aplicaciones GUI en C++. Está totalmente orientado a objetos y es fácilmente extensible.

Qt es la base en la que se desarrolla el escritorio KDE.

Al menos para la versión 3.0.5, está soportado en:

  • MS/Windows - 95, 98, NT 4.0, ME, and 2000

  • Unix/X11 - Linux, Sun Solaris, HP-UX, Compaq Tru64 UNIX, IBM AIX, SGI IRIX y otros

  • Macintosh - Mac OS X

  • Embedded - Plataformas Linux con soporte framebuffer

Qt es desarrollado y mantenido por la empresa Trolltech. Ésta la distribuye en una versión comercial (para desarrollos comerciales) y una versión "Free Edition", disponible para descarga vía Web, la cual puede usarse para desarrollar aplicaciones no comerciales [160] . Como indiqué, Qt requiere el uso de C (básico) para la escritura de los programas. Puesto que no es este el lugar indicado para explicar el lenguaje C, asumiremos en lo sucesivo que el lector ya lo conoce, al menos superficialmente.

13.4.1. Hello World, el proyecto

Trataremos de implementar el programa "Hello World" de modo similar a lo que hicimos en los toolkits anteriores.

Hola Mundo con Qt

En Qt las aplicaciones se encapsulan en "proyectos", y en nuestro caso, crearemos uno para "Hello World".

Qt proporciona una herramienta auxiliar usada para generar archivos de construcción de proyectos (Makefiles.) Esta herramienta se llama qmake y la describimos a continuación.

La forma más simple de iniciar el proyecto consiste en crear un nuevo directorio (por ejemplo "hw1") y en ese directorio crear el texto fuente del programa:

$ mkdir helloworld
$ cd helloworld
$ vi qt-helloworld.h
$ vi qt-helloworld.cpp

Como se aprecia, hemos creado dos archivos para el programa; uno de ellos (qt-helloworld.h) corresponde a la declaración de una subclase llamada HelloWorld, y el otro (qt-helloworld.cpp) es el programa en sí, que hace uso de dicha clase. Este programa (dada su simplicidad) se pudo hacer en un archivo único, aunque teniendo algunos cuidados.

A continuación el archivo header:

/* qt-helloworld.h */
#include <qvbox.h>

class HelloWorldDialog : public QVBox
{
Q_OBJECT
public slots:
	void imprimir(void);
};

Y ahora el programa principal:

/* qt-helloworld.cpp */
#include <stdio.h>
#include <stdlib.h>
#include <qpushbutton.h>
#include <qlabel.h>
#include <qvbox.h>
#include <qapplication.h>
#include <qlayout.h>
#include "qt-helloworld.h"

void HelloWorldDialog::imprimir(void)
{
printf("Adios!\n");
exit(0);
}

int main( int argc, char **argv )
{
QApplication a(argc,argv);
HelloWorldDialog caja;
caja.setCaption("Hola Mundo!");
caja.setMargin(4);
caja.setSpacing(4);
a.setMainWidget( &caja );
QLabel texto("Hola Mundo con QT",&caja);
QPushButton boton("Presiona el boton",&caja);
QObject::connect(&boton,SIGNAL(clicked()),&caja,SLOT(imprimir()));
caja.show();
return a.exec();
}

En este punto, Ud. deberá crear un archivo de "proyecto" (con extensión .pro) que contenga todas las características del programa. Una forma rápida de obtener este archivo consiste en ejecutar el comando “qmake -project”, el cual creará un archivo llamado “helloworld.pro” [161] . Éste contiene una descripción acerca de qué archivos de código fuente existen en el directorio (en nuestro caso, sólo qt-helloworld.cpp) así como una serie de opciones que facilitan la compilación en diversas plataformas con requerimientos diferenciados. En este texto no profundizaremos sobre estas facilidades que están explicadas en la excelente documentación de Qt.

$ qmake -project
$ ls
helloworld.pro  qt-helloworld.cpp  qt-helloworld.h

Revise el archivo helloworld.pro. Éste debe ser modificado cuando se agregan o eliminan archivos de código fuente.

El siguiente paso consiste en crear un “Makefile” para la compilación del proyecto a partir de los parámetros del archivo helloworld.pro, lo cual se hace con el mismo comando “qmake”, esta vez sin opciones:

$ qmake
$ ls
Makefile  helloworld.pro  qt-helloworld.cpp  qt-helloworld.h

Como ya tenemos el Makefile, ejecutamos “make” para proceder a la compilación. Nótese que el ejecutable resultante se llamará “helloworld” [162] :

$ make
g++ -c -pipe -Wall -W -O2  -DQT`NO`DEBUG -DQT_SHARED
-DQT`THREAD`SUPPORT -I/usr/share/qt3/mkspecs/default
-I. -I. -I/usr/include/qt3 -o qt-helloworld.o
qt-helloworld.cpp

/usr/share/qt3/bin/moc qt-helloworld.h -o moc_qt-helloworld.cpp

g++ -c -pipe -Wall -W -O2  -DQT`NO`DEBUG -DQT_SHARED
-DQT`THREAD`SUPPORT -I/usr/share/qt3/mkspecs/default
-I. -I. -I/usr/include/qt3 -o moc_qt-helloworld.o
moc_qt-helloworld.cpp

g++  -o helloworld qt-helloworld.o moc_qt-helloworld.o
-L/usr/share/qt3/lib -L/usr/X11R6/lib -lqt-mt -lXext -lX11 -lm

$ ls
Makefile               helloworld           helloworld.pro
moc`qt-helloworld.cpp  moc`qt-helloworld.o  qt-helloworld.cpp
qt-helloworld.h        qt-helloworld.o

Todo programa que usa Qt deberá definir un objeto de tipo QApplication que recibe como argumentos la línea de comando a fin de inicializar el toolkit y procesar algunas opciones estándar. En nuestro caso, le hemos denominado "a".

Hemos definido tres widgets: caja (clase HelloWorld, que hereda de la clase de caja vertical, QVBox), texto (texto simple QLabel) y boton (un botón pulsador, clase QPushButton.) Obsérvese que texto y boton son creados especificando un puntero a la caja vertical (&caja), lo que significa que automáticamente están en su interior; dicho de otro modo, caja es el widget "padre" de texto y botón. Cuando más adelante se "muestra" la caja con “caja.show()”, los widgets interiores ("hijos") también se muestran automáticamente.

En la creación del widget caja no se especifica un widget "padre", lo que significia que será una ventana independiente. Por otro lado, siendo ésta nuestra única ventana, es lógico que el programa termine cuando ésta ventana se destruya. Tal comportamiento se consigue especificando que dicha caja sea la "ventana principal" de la aplicación, lo que se indica mediante la llamada a.setMainWidget(&caja).

Al final de main(), la aplicación entra al loop de espera de eventos usando “a.exec()”.

Señales y slots

Quizá la línea más interesante del programa sea:

QObject::connect(&boton,SIGNAL(clicked()),&caja,SLOT(imprimir()));

Ésta es una invocación al método "estático" connect() de la clase "QObject". Se utiliza para interconectar eventos ocurridos en los widgets (que se denominan "señales") con métodos de otros widgets, los que son "alertados" de este suceso. Estos métodos que "reciben la señal" se denominan "slots".

En nuestro caso, hemos asociado la señal “clicked()” del “boton”, con el slot “imprimir()” del widget “caja”. En otras palabras, al presionar el botón, se imprime un mensaje.

Cuando una aplicación define slots, estos deben definirse como métodos de una subclase de QObject o de una de sus subclases. En nuestro caso hemos definido la subclase HelloWorld como subclase de QVBox, que es un widget de Qt, y por tanto subclase de QObject.

Como se aprecia (en qt-helloworld.h), las funciones "slot" se deben colocar luego de la declaración “public slot:”. Sin embargo, esto no es parte del lenguaje C++ sino "extensión" de Qt, las cuales son activadas cuando se añade una línea con la declaración "Q_OBJECT" en la declaración de la clase (tal como en nuestro ejemplo.)

Cuando existen estas declaraciones, qmake se encarga de generar llamadas a un "precompilador" especializado de Qt que traduce las "extensiones" mencionadas a "C++ real", lo que motiva a que se generen algunos archivos auxiliares (lo cual es transparente para el desarrollador.)

13.4.2. Editor de texto

El siguiente listado presenta una implementación del editor de textos con Qt:

/* qt-editor.cpp */
#include <stdio.h>
#include <qpushbutton.h>
#include <qfiledialog.h>
#include <qlabel.h>
#include <qlineedit.h>
#include <qtextedit.h>
#include <qvbox.h>
#include <qapplication.h>
#include <qlayout.h>
#include "qt-editor.h"

#define UNNAMED "Sin Nombre"

char *get_data_from_file(const char *filename);
int save_data_to_file(const char *filename,const char *data);

QTextEdit *data;
QPushButton *boton_grabar;
QLabel *texto;

void Editor::guardar_archivo(void)
{
QString s = QFileDialog::getSaveFileName( ".",
"Todos (*)",
this,
"Guardar archivo",
"Escriba nombre de archivo");
if(s!=NULL)
	{
	if(save_data_to_file(s.latin1(),data->text()))
		texto->setText(s);
	else
		fprintf(stderr,"Error grabando archivo\n");
	}
}

void Editor::abrir_archivo(void)
{
QString s = QFileDialog::getOpenFileName( ".",
"Todos (*)",
this,
"Abrir archivo",
"Seleccione archivo a editar");
if(s!=NULL)
	{
	char *ptr=get_data_from_file(s.latin1());
	if(ptr)
		{
		data->setText(ptr);
		texto->setText(s);
		free(ptr);
		}
	else
		fprintf(stderr,"Error abriendo archivo\n");
	}
}

int main( int argc, char **argv )
{
QApplication a(argc,argv);
Editor cajav;
a.setMainWidget( &cajav );
cajav.setMargin(4);
cajav.setSpacing(4);
cajav.setCaption("Editor con Qt");

QHBox cajah(&cajav);
cajah.setSpacing(4);
data=new QTextEdit(&cajav);
texto=new QLabel(UNNAMED,&cajav);

QPushButton boton_abrir("Abrir",&cajah);
boton_grabar=new QPushButton("Grabar",&cajah);

QObject::connect(&boton_abrir,SIGNAL(clicked()),
		&cajav,SLOT(abrir_archivo()));
QObject::connect(boton_grabar,SIGNAL(clicked()),
		&cajav,SLOT(guardar_archivo()));

data->setFixedSize(600,400);
cajav.show();
return a.exec();
}

El archivo cabecera qt-editor.h:

/* qt-editor.h */
#include <qvbox.h>

class Editor : public QVBox
{
Q_OBJECT
public slots:
	void abrir_archivo(void);
	void guardar_archivo(void);
};

Obsérvese que Qt almacena las cadenas de texto mediante el tipo QString, el cual permite contener caracteres Unicode. Cuando se requere generar cadenas de caracteres simples terminadas en NUL, se emplea el método latin1() de esta misma clase.

Editor de textos con Qt
Qt-Designer

Qt-Designer es una herrmienta RAD que permite diseñar gráficamente la apariencia de las ventanas Qt. Asimismo, posee capacidades de edición (algo limitadas) para introducir directamente todo el código fuente necesario, constituyéndose en un entorno de desarrollo integral.

Los diseños de las ventanas se almacenan en archivos XML (con extensión .ui) y son transformados en clases mediante la generación de archivos de código fuente (.cpp y .h) mediante la herramienta “uic” (User Interface Compiler.) Todo esto ocurre en forma casi transparente para el desarrollador.

13.5. FLTK

Informalmente conocido como el "Fast Light Toolkit", este toolkit proporciona un completo conjunto de widgets para desarrollar aplicaciones en C++. Su característica más mencionada corresponde a lo "livianos" que resultan los ejecutables puesto que Fltk ha sido diseñado para ser compilado estáticamente. Asimismo, proporciona facilidades para acceder a regiones vía OpenGL y funciones para dibujo 2-D. En general, los listados de programas escritos en Fltk son muy reducidos.

13.5.1. Ejemplo Básico: Hello World

Hello World con Fltk

El siguiente código hace todo el trabajo. Todas las aplicaciones que utilizan Fltk deben incluir el header <FL/Fl.H> (la 'H' final está en mayúsculas), y otros headers dependiendo de los widgets utilizados.

#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <stdio.h>
#include <stdlib.h>

void hello(Fl_Widget *w, void *data)
{
printf("Adios!\n");
exit(0);
}

int main(int argc, char **argv)
{
Fl_Window *window = new Fl_Window(150,62);
Fl_Box *box = new Fl_Box(FL_NO_BOX,4,4,142,25,"Hola Mundo!");
Fl_Button *button = new Fl_Button(4,33,142,25,"OK");
button->callback(hello,NULL);
window->end();
window->show(argc, argv);
return Fl::run();
}

La ventana corresponde a un Fl_Window, en la cual se ha insertado un texto (que aquí se insertan como 'cajas' mediante el widget Fl_Box), así como un botón (Fl_Button.) Nótese que los widgets creados se insertan por omisión en la ventana, hasta que se invoca al método end() de esta última.

El botón tiene asociada una función callback denominada hello(), cuyo prototipo es siempre el que se muestra. En nuestro caso sólo termina el programa. Para asociar el callback se emplea el método “callback()” del botón, pasándole la función callback y un puntero opcional de datos adicionales.

Al terminar de agregar los widgets, la ventana es mostrada con su método show(), al cual hemos pasado los argumentos de línea de comando.

El "loop principal" de recepción de eventos corresponde al método estático “run()” de la clase “Fl”.

Quizá el detalle más desconcertante corresponde a la especificación de las posiciones y dimensiones en los widgets en el momento de su creación. Los cuatro valores enteros corresponden respectivamente a:

  • Distancia horizontal desde el extremo izquierdo de la ventana

  • Distancia vertical desde el extremo superior de la ventana

  • Ancho del widget

  • Alto del widget

Por último, el texto "Hola Mundo!" se ha introducido en una "caja", la cual se creó con el tipo “FL_NO_BOX”, lo que significa que no se trazarán los bordes de la misma. Existen muchos otros tipos de caja, los que se pueden hallar en el archivo header <Enumerations.H>.

13.5.2. Editor de texto

El editor de texto nuevamente hará referencia a las funciones auxiliares de manejo de archivos definidas en el archivo aux_file.c muy anteriormente explicado.

Editor de textos con Fltk

El código es sorprendentemente breve:

#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Text_Editor.H>
#include <FL/Fl_Text_Buffer.H>
#include <FL/Fl_File_Chooser.H>
#include <stdio.h>
#include <stdlib.h>

#define UNNAMED "Sin Nombre"

char *get_data_from_file(const char *filename);
int save_data_to_file(const char *filename,const char *data);

Fl_Text_Editor *editor;
Fl_Box *filename;

void abrir(Fl_Widget *w, void *data)
{
char *newfile = fl_file_chooser("Seleccione Archivo", "*", NULL);
if (newfile != NULL)
	{
	char *ptr=get_data_from_file(newfile);
	if(ptr)
		{
		editor->buffer()->text(ptr);
		free(ptr);
		filename->label(newfile);
		}
	else
		fprintf(stderr,"No se pudo abrir %s\n",newfile);
	}
}

void grabar(Fl_Widget *w, void *data)
{
char *newfile = fl_file_chooser("Escriba Nombre de Archivo", "*", NULL);
if (newfile != NULL)
	{
	char *ptr=editor->buffer()->text();
	if(save_data_to_file(newfile,ptr))
		filename->label(newfile);
	else
		fprintf(stderr,"No se pudo grabar %s\n",newfile);
	free(ptr);
	}
}

int main(int argc, char **argv)
{
Fl_Window *window = new Fl_Window(408,366);
Fl_Button *boton_abrir = new Fl_Button(4,4,100,25,"Abrir");
Fl_Button *boton_grabar = new Fl_Button(108,4,100,25,"Grabar");
editor = new Fl_Text_Editor(4,33,400,300);
Fl_Text_Buffer *buffer = new Fl_Text_Buffer();
editor->buffer(buffer);
filename = new Fl_Box(FL_NO_BOX,4,337,400,25,UNNAMED);
boton_abrir->callback(abrir,NULL);
boton_grabar->callback(grabar,NULL);
window->end();
window->show(argc, argv);
return Fl::run();
}

En la función main() se ha utilizado un widget de tipo Fl_Text_Editor para la región de edición. Este widget tiene diversas características convenientes, tales como scroll automático y resaltados.

El texto que despliega un Fl_Text_Editor se almacena en un tipo de dato especial llamado Fl_Text_Buffer, el cual es "conectado" al anterior mediante el método buffer():

editor->buffer(buffer);

Las funciones callback hacen uso de la función auxiliar fl_file_chooser(), la cual despliega un diálogo de selección de archivo. El primer argumento corresponde al título del diálogo, el segundo es el patrón de filtrado de los archivos, y el último (si está presente) es el nombre de archivo "inicial" sugerido.

En caso de que el usuario seleccione o escriba el nombre de un archivo, la función retorna un puntero a este nombre (o NULL en caso contrario.) Este nombre de archivo se sobreescribe automáticamente en las subsiguientes invocaciones a fl_file_chooser() y no debe ser liberado (con free().) [163]

Desarrollo Fltk con Fluid

13.6. Ejercicios

Implemente (en el toolkit de su elección) algunas mejoras para el editor:

  1. Una barra de menú para reemplazar los botones de "abrir" y "guardar"

  2. Diálogos de alerta cuando no se puede abrir o guardar el archivo

  3. Una confirmación si se va a salir sin grabar el texto editado

COMUNICACION ENTRE PROCESOS

14. Tuberías o Pipes

En este capítulo describiremos algunos mecanismos que permiten transferir información de un proceso hacia otro, asumiendo que ambos se ejecutan en el mismo computador.

14.1. Pipes en la Librería Estándar

Quizá el modo más sencillo de comunicar información entre dos procesos se da cuando uno de éstos ejecuta al otro como un proceso "hijo". Para esta situación la librería estándar de los sistemas POSIX, proporciona la función popen(), la cual permite a un proceso lanzar un nuevo proceso y establecer comunicación unidireccional entre ambos. Por ejemplo, el siguiente programa ejecuta el comando ‘ls’ como un nuevo proceso, y lee la salida de este último. Finalmente muestra cuántas líneas fueron leídas:

#include <stdio.h>

int main()
{
    FILE *fp;
    int c, lineas = 0;

    fp = popen("ls", "r");
    if (fp == NULL) {
	fprintf(stderr, "Error en popen\n");
	return 1;
    }
    while ((c = fgetc(fp)) != EOF)
	if (c == '\n')
	    lineas++;
    pclose(fp);
    printf("Se obtuvieron %d lineas\n", lineas);
    return 0;
}

Como se aprecia, el ejecutable a lanzar se especifica en el primer argumento mientras que el segundo corresponde a la dirección del flujo de información. En nuestro caso, la "r" (read) significa que el proceso padre leerá desde el handler “fp” lo que escribe el proceso hijo en su salida estándar. Asimismo, una "w" (write) significa que el proceso padre escribirá en el hanlder “fp” para que el proceso hijo la lea desde su entrada estándar.

Es importante saber que el programa que se especifica como primer argumento a popen() corresponde en realidad a una línea de comandos que es procesada por el shell /bin/sh, por lo que se puede especificar cualquier opción de shell. Por ejemplo, el siguiente programa instruye al shell a listar el "home directory" del usuario actual, y se captura la salida del último comando (wc):

#include <stdio.h>

int main()
{
    FILE *fp;
    int c;

    fp = popen("ls $HOME | wc", "r");
    if (fp == NULL) {
	fprintf(stderr, "Error en popen\n");
	return 1;
    }
    while ((c = fgetc(fp)) != EOF)
	putchar(c);
    pclose(fp);
    return 0;
}

Por último, la rutina pclose() además de cerrar los flujos de comunicación asociados, espera a que el proceso creado termine.

14.2. La llamada al sistema pipe

Esta llamada al sistema permite establecer un flujo unidireccional [164] entre procesos que tienen un ancestro común (por ejemplo, padre-hijo, hijo-hijo, padre-nieto, etc.) Para esto, el proceso padre deberá invocar a pipe() proporcionando un array con capacidad para dos enteros. La llamada al sistema retornará dos descriptores de archivo (de lectura y escritura, respectivamente) en este array. Posteriormente, los subprocesos generados con fork() heredan estos descriptores y pueden utilizarlos como mecanismo de comunicación.

En el siguiente programa se demuestra la comunicación "hijo-hijo". El proceso inicial obtiene los descriptores de pipe(), crea dos procesos hijos y cierra los recién obtenidos descriptores pues no participará de la comunicación. El primer proceso hijo (hermano1()) escribe un mensaje en el descriptor fd[1] (de escritura) mientras que el segundo proceso hijo (hermano2()) lo lee desde el descriptor fd[0] (de lectura.)

#include <stdio.h>
#include <unistd.h>
#include <string.h>

void hermano1(void);
void hermano2(void);

int fd[2];

int main()
{
    int i;

    if (pipe(fd) == -1) {
	fprintf(stderr, "Error en pipe()\n");
	return 1;
    }
    i = fork();
    if (i == 0) {
	hermano1();
	return 0;
    }
    i = fork();
    if (i == 0) {
	hermano2();
	return 0;
    }
/* El padre no participara de la comunicacion */
    close(fd[0]);
    close(fd[1]);
    return 0;
}

void hermano1(void)
{
    const char *msg = "Mensaje desde hermano1";
/* Cierra descriptor para leer */
    close(fd[0]);
/* Escribe en descriptor para escribir */
    write(fd[1], msg, strlen(msg));
    close(fd[1]);
}

void hermano2(void)
{
    char buffer[100];
    int n;

/* Cierra descriptor para escribir */
    close(fd[1]);
/* Lee desde descriptor para leer */
    n = read(fd[0], buffer, 100);
    if (n == 0) {
	fprintf(stderr, "Se cerro descriptor de lectura\n");
	return;
    }
    if (n == -1) {
	fprintf(stderr, "Error leyendo desde descriptor de lectura\n");
	return;
    }
    close(fd[0]);
    printf("hermano2 recibio mensaje: %.*s\n", n, buffer);
}
Notas
  • Si todos los procesos participantes han cerrado su descriptor de lectura, cualquier intento de escritura genera la señal SIGPIPE y falla

  • Cuando el último de los procesos cierra su descriptor de escritura, la lectura retorna cero bytes (fin de archivo)

  • La rutina popen() normalmente se implementa a partir pipe() y otras llamadas al sistema como dup(), fork(), exec(), etc.

14.3. Named Pipes o "Tuberías con Nombre"

Los "named pipes" consisten en un archivo especial que es utilizado por dos (o más) procesos para transferir información. Este archivo debe ser creado previamente a la comunicación. Tras su creación, un proceso deberá abrir (con open(2)) e intentar leer (con read(2)) el "contenido" del archivo, lo que normalmente ocasiona su bloqueo hasta que haya información disponible. A continuación, uno o más procesos también abren el archivo y realizan escrituras (con write(2)) sobre el mismo, las cuales serán recibidas por el mencionado proceso lector. Este esquema requiere que todos los procesos participantes conozcan la ruta y el nombre del archivo "named pipe".

La creación del archivo "named pipe" se realiza desde el shell mediante el comando “mkfifo”, el cual a su vez utiliza una función o llamada al sistema del mismo nombre. El siguiente programa crea un "named pipe" llamado “tuberia” en el directorio actual con permisos de lectura y escritura para el usuario actual:

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

int main()
{
    if (mkfifo("tuberia", 0600) == -1) {
	fprintf(stderr, "Error creando named pipe\n");
	return 1;
    }
    printf("Creacion de named pipe exitosa\n");
    return 0;
}

A fin de verificar el funcionamiento podríamos crear dos programas que respectivamente lean y escriban en el archivo “tuberia”; sin embargo, éstos no aportarían nada a nuestra exposición (pues en el capítulo 8 ya se discutió la lectura y escritura de archivos) por lo que nos limitaremos a una demostración con algunas herramientas Linux/Unix. En primer lugar, creamos el "named pipe" y lanzamos el programa "lector":

$ ./crea`named`pipe
Creacion de named pipe exitosa
$ ls -l tuberia
prw-------  1 diego users 0 Feb 18 15:16 tuberia
$ cat tuberia

Nótese el tipo de archivo “p” en la salida de “ls -l”. Nuestro lector “cat”, simplemente se bloquea esperando a que llegue la información.

A continuación, desde otra ventana/terminal lancemos un proceso que escriba algunos mensajes:

$ while true ; do echo "Mensaje"; sleep 1; done > tuberia

Este proceso sencillamente escribe la palabra “Mensaje” en el "named pipe" indefinidamente cada segundo. A partir de este momento Ud. debería apreciar el “Mensaje” capturado en la otra ventana/terminal por cat:

$ ./crea`named`pipe
Creacion de named pipe exitosa
$ ls -l tuberia
prw-------  1 diego users 0 Feb 18 15:16 tuberia
$ cat tuberia
Mensaje
Mensaje
Mensaje
Mensaje

Nótese que esto termina inmediatamente si se cancela el proceso escritor debido a que ya no hay más escritores por lo que cat recibe un "fin de archivo".

14.4. Ejercicios

1 La llamada al sistema dup2(int oldfd,int newfd) permite obtener un nuevo descriptor de archivo (newfd) equivalente a otro (oldfd), es decir, ambos permiten leer/escribir en relación al mismo archivo. Si newfd estuviera abierto, es una buena práctica cerrarlo antes de utilizarlo con dup2().

Se pide hacer un programa que mediante dos "pipes" (dos llamadas a pipe()) permita ejecutar el utilitario Unix/Linux “wc”, enviándole vía su entrada estándar una cadena de caracteres introducida por el usuario (vía un pipe.) Asimismo, la salida estándar de “wc” deberá ser capturada por nuestro programa (con el otro pipe) para ser finalmente mostrada en pantalla.

Puesto que “wc” (al igual que la mayoría de utilitarios del sistema) siempre lee y escribe, respectivamente, desde y hacia los descriptores 0 y 1, es necesario que nuestro programa genere un proceso hijo el cual haga "apuntar" estos descriptores hacia los respectivos "pipes"; es decir, se necesita realizar las operaciones:

...
dup2(pipe1_fd,0);
dup2(pipe2_fd,1);
...
exec(...) /* wc */

2 Repita la demostración del "named pipe" pero ahora con más de una ventana/terminal que escribe sobre el mismo. Verifique que el proceso lector captura todas las escrituras. Verifique también que la terminación de un escritor no detiene al lector, salvo cuando se trata del último de aquéllos.

15. Semáforos

Una de las facilidades más interesantes proporcionadas por el subsistema IPC corresponde a los semáforos, los cuales permiten la sincronización mutua de un conjunto de procesos que acceden a recursos comunes.

15.1. Conceptos

Esta sección la iniciaremos con un ejemplo relativamente simple que ilustre el concepto y la interfaz de los semáforos System V. En el capítulo 16 se podrá apreciar más ejemplos del uso de semáforos.

Los semáforos System V pueden considerarse sencillamente como un número entero no negativo, cuyo valor los procesos intentan disminuir e incrementar. Cuando un proceso ha conseguido disminuir el semáforo hasta cero, cualquier otro proceso que intente una nueva disminución quedará automáticamente suspendido por el kernel, y sólo se reactivará [165] cuando el proceso inicial vuelva a incrementar el semáforo, efectívamente permitiendo al segundo concretar la disminuición que intentaba. Lo que hace especiales a los semáforos es que es imposible que dos procesos a la vez disminuyan el mismo semáforo para llevarlo a cero; en otras palabras, aunque dos procesos intenten hacer la disminución al mismo tiempo, el kernel siempre elegirá a uno de ellos.

Desde el punto de vista del flujo de los programas, cuando un proceso logra disminuir a un semáforo hasta cero (mientras los otros procesos también lo están intentando) es como si el primer proceso hubiera alcanzado el privilegio para hacer algo que los otros no pueden. Esto es típicamente aprovechado cuando un proceso debe operar sobre un recurso de información compartido pero sin la momentánea intromisión de los otros.

15.1.1. Interfaz de programación

A continuación analizaremos la interfaz de programación mediante dos rutinas auxiliares que emplearemos en los ejemplos que siguen. La explicación resulta un tanto tediosa de leer, por lo que puede pasarla por alto en una primera lectura y pasar directamente a los ejemplos.

/*
 * x_sem.c: libreria para semaforos
 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include "x_sem.h"

#define PERMISOS 0600

int x_sem(key_t llave)
{
    int id_sem;
    union semun {
	int val;
	struct semid_ds *buf;
	unsigned short int *array;
    } s;

    id_sem = semget(llave, 0, 0);
    if (id_sem != -1)
	return id_sem;
    if (errno != ENOENT) {
	perror("x_sem:1: semget");
	exit(-1);
    }
    id_sem = semget(llave, 1, IPC_CREAT | IPC_EXCL | PERMISOS);
    if (id_sem == -1) {
	perror("x_sem:2: semget");
	exit(-1);
    }
    s.val = 1;
    if (semctl(id_sem, 0, SETVAL, s) == -1) {
	perror("x_sem: semctl");
	exit(-1);
    }
    return id_sem;
}

void x_semop(int id_sem, int op)
{
    struct sembuf sops[1];
    sops[0].sem_num = 0;
    sops[0].sem_op = op;
    sops[0].sem_flg = SEM_UNDO;
    if (semop(id_sem, sops, 1) == -1) {
	perror("x_semop: semop");
	exit(-1);
    }
}

Un aspecto importante de las llamadas al sistema de semáforos radica en que hacen referencia a "arrays de semáforos", y no a semáforos independientes. Sin embargo, en la mayoría de programas (así como en nuestros ejemplos) el manejo de "arrays" puede resultar engorroso. A tal efecto, nuestras rutinas crean "arrays" compuestos por un único semáforo, y permiten con mucha comodidad aplicar las operaciones más frecuentes sobre éstos.

La rutina x_sem() hace uso de las llamadas al sistema semget() y semctl(). La primera se emplea para acceder a un semáforo (o mejor dicho, a un array de semáforos), así como para la creación de los mismos. La segunda permite -entre otras cosas- configurar el valor inicial de éstos.

A nivel de todo el sistema operativo, los semáforos son identificados mediante una "llave de semáforo", la que debe ser conocida por todos los procesos que manipulan aquél. Esta llave es empleada en la primera invocación a semget() para "acceder" a tal semáforo. En este caso, semget() no posee en su tercer argumento la opción IPC_CREAT, lo que significa que el segmento ya debe existir para que se retorne con éxito. En caso contrario, semget() debe retornar error (-1) y la variable errno debería contener ENOENT, que significa jústamente que el semáforo no existe.

En tal caso volvemos a emplear semget() pero con la opción IPC_CREAT que efectivamente crea un array de semáforos, el cual para nuestro caso siempre tendrá un único elemento (el "uno" del segundo argumento.) Asimismo, hemos especificado la opción IPC_EXCL; esta opción hace que semget() retorne error si el array de semáforos ya existiera. El motivo de esta precaución se debe a que este mismo momento (justo después de nuestro primer semget() pero antes del segundo) otro programa podría haber creado el semáforo, lo que significa una duplicidad de "inicalizaciones" que a su vez puede generar algunos problemas [166] .

Como indicamos anteriormente, en esta rutina también utilizamos la llamada al sistema semctl() para configurar el valor numérico inicial de los semáforos. En nuestros ejemplos los semáforos sólo requieren tener dos estados (libre u ocupado), por lo que sus valores serán uno y cero, respectivamente; y por lo tanto, el valor numérico inicial será precísamente uno. Lamentablemente, la sintaxis de semctl no es muy sencilla debido a que permite efectuar diversas tareas disímiles. Su primer argumento obviamente es el identificador obtenido con semget; el segundo es el número del semáforo en el conjunto sobre el que efectuaremos la operación (en nuestro caso, el cero, que corresponde al primer y único semáforo del conjunto); el tercer argumento es el tipo de operación que vamos a realizar (alterar su valor numérico, operación SETVAL) y el cuarto es un dato que depende del tipo de operación elegido, el cual se proporciona mediante una unión de tipo semun.

Para complicar un poco más las cosas, la unión semun no siempre está definida en los archivos cabecera del sistema, por lo que la tenemos que especificar nosotros. Para el caso de la operación SETVAL sólo debemos usar su miembro “val” especificando el valor inicial del semáforo. Obsérvese que la unión se pasa por valor y no por referencia.

La rutina x_semop() emplea la llamda al sistema semop(). El primer argumento de ésta es el identificador de "array de semáforos" (obtenido con semget()); el segundo es un array con un conjunto de estructuras que determinan qué se hace con los semáforos, y el tercero es el tamaño de este array. Como sólo tenemos una operación que realizar en cada caso, el array tiene tamaño uno.

Las mencionadas estructuras son de tipo sembuf y tienen los siguientes campos:

  • sem_num: Número de semáforo sobre el que se efectúa la operación, en nuestro caso, siempre es el semáforo número cero

  • sem_op: Incremento/decremento sobre el semáforo. Para "reservar" el semáforo intentamos disminuirlo (-1) y para "liberarlo" lo incrementamos (+1)

  • sem_flg: Modificadores opcionales. Hemos usado SEM_UNDO que libera el semáforo cada vez que el proceso termina

El valor del semáforo sólo se puede disminuir hasta cero (recuerde que inicialmente era uno.) Si un proceso disminuye el semáforo (con sem_op=-1) éste ahora será cero. Si en ese momento otro proceso pretende hacer lo mismo, el kernel lo suspende hasta que el primero libere el semáforo (con sem_op=+1.) De este modo aseguramos que sólo un proceso a la vez se "reserve" el semáforo. Nótese que nuestra rutina x_semop() requiere sólo del identificador de semáforo (obtenido con semget() o con x_sem()) y el "incremento/decremento" (+1 o -1.)

Finalmente, los programas que hacen uso de estas rutinas deben incluir el archivo cabecera x_sem.h:

/* x_sem.h */
#ifndef X_SEM_H
#define X_SEM_H
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int x_sem(key_t llave);
void x_semop(int id_sem,int op);
#endif

15.2. Ejemplo de Sincronización

El siguiente programa demuestra dramáticamente los inconvenientes que surgen cuando no se sincronizan los procesos que acceden a los mismos recursos. Aquí los recursos corresponden a dos archivos (arch1 y arch2) que contienen cada uno una línea de texto con un valor numérico. Un proceso intentará disminuir (en una unidad) el valor almacenado en arch1 y a continuación incrementará (también en una unidad) el valor almacenado en arch2. Esto se puede imaginar como una transferencia monetaria de una cuenta hacia otra.

Simultáneamente, otro proceso realizará la misma operación pero en orden inverso: desde arch2 hacia arch1.

Puesto que cada proceso hace siempre una resta de una unidad y una suma de una unidad a cada archivo, es de esperarse que el total almacenado en ambos archivos se mantenga constante, aunque los valores absolutos de cada archivo fluctuen aleatoriamente. A continuación presentamos un programa (sin semáforos) que crea dos subprocesos con este propósito, y cuyo proceso principal se dedica a reportar los valores actuales:

15.2.1. Transferencia sin Sincronización

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

void transfiere(const char *origen, const char *destino);
static char L[64];

int main()
{
    FILE *fp;
    int p1, p2, v1, v2;

    fp = fopen("arch1", "w");
    fprintf(fp, "5000");
    fclose(fp);

    fp = fopen("arch2", "w");
    fprintf(fp, "5000");
    fclose(fp);

    p1 = fork();
    if (p1 == 0)
	transfiere("arch1", "arch2");
    p2 = fork();
    if (p2 == 0)
	transfiere("arch2", "arch1");
    for (;;) {
	fp = fopen("arch1", "r");
	fgets(L, 64, fp);
	fclose(fp);
	v1 = atoi(L);
	fp = fopen("arch2", "r");
	fgets(L, 64, fp);
	fclose(fp);
	v2 = atoi(L);
	printf("%d + %d -> %d\n", v1, v2, v1 + v2);
	sleep(1);
    }
}

void transfiere(const char *origen, const char *destino)
{
    FILE *fp;
    int v;

    for (;;) {
	fp = fopen(origen, "r");
	fgets(L, 64, fp);
	fclose(fp);
	v = atoi(L);
	fp = fopen(origen, "w");
	fprintf(fp, "%d\n", v - 1);
	fclose(fp);

	fp = fopen(destino, "r");
	fgets(L, 64, fp);
	fclose(fp);
	v = atoi(L);
	fp = fopen(destino, "w");
	fprintf(fp, "%d\n", v + 1);
	fclose(fp);
    }
}

El resultado de su ejecucion luce totalmente incompatible con lo que pretendíamos:

$ ./sem
4985 + 4985 -> 9970
4985 + 4099 -> 9084
6127 + 6504 -> 12631
6504 + 1202 -> 7706
758 + 758 -> 1516
758 + 1606 -> 2364
6238 + 6238 -> 12476
2616 + 2616 -> 5232
194 + 194 -> 388
194 + 1629 -> 1823
1629 + 3105 -> 4734
3105 + -1206 -> 1899

Este programa tiene diversos problemas que son insalvables sin un mecanismo de sincronización. Por ejemplo, los valores reportados en pantalla siempre son erróneos debido a que tras la lectura del primer archivo es muy probable que el segundo ya haya sido alterado con lo que el total es inválido.

Del mismo modo en la función transfiere(), cada archivo es leído y luego reescrito con un nuevo valor. Entre estas dos operaciones es posible que el otro proceso haga su respectiva escritura de un nuevo valor, el cual se perderá puesto que ya no será leído por el primer proceso.

El caso más evidente ocurre cuando uno de los procesos está efectuando su respectiva escritura, la cual consta de tres pasos: un fopen() (en modo "w"), un fprintf() y un fclose(). Como sabemos, tras el fopen() el archivo queda listo para ser escrito, pero al mismo tiempo su anterior contenido es destruido. Justamente en este momento puede ocurrir la respectiva lectura del otro proceso, el cual no obtendrá ningún número!

15.2.2. Transferencia con Sincronización

Todo lo anterior es fácilmente corregible si sincronizamos los (tres) procesos para que no accedan a los archivos al mismo tiempo. Para esto basta con reservar un semáforo antes del acceso a aquellos, y liberarlo cuando se termina:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include "x_sem.h"

#define LLAVE_SEMAFORO 0x003333

void transfiere(const char *origen, const char *destino);
static char L[64];
static int sem;

int main()
{
    FILE *fp;
    int p1, p2, v1, v2;

    fp = fopen("arch1", "w");
    fprintf(fp, "5000");
    fclose(fp);

    fp = fopen("arch2", "w");
    fprintf(fp, "5000");
    fclose(fp);

    sem = x_sem(LLAVE_SEMAFORO);
    p1 = fork();
    if (p1 == 0)
	transfiere("arch1", "arch2");
    p2 = fork();
    if (p2 == 0)
	transfiere("arch2", "arch1");
    for (;;) {
	x_semop(sem, -1);

	fp = fopen("arch1", "r");
	fgets(L, 64, fp);
	fclose(fp);
	v1 = atoi(L);
	fp = fopen("arch2", "r");
	fgets(L, 64, fp);
	fclose(fp);
	v2 = atoi(L);
	printf("%d + %d -> %d\n", v1, v2, v1 + v2);
	x_semop(sem, +1);
	sleep(1);
    }
}

void transfiere(const char *origen, const char *destino)
{
    FILE *fp;
    int v;

    for (;;) {
	x_semop(sem, -1);

	fp = fopen(origen, "r");
	fgets(L, 64, fp);
	fclose(fp);
	v = atoi(L);
	fp = fopen(origen, "w");
	fprintf(fp, "%d\n", v - 1);
	fclose(fp);

	fp = fopen(destino, "r");
	fgets(L, 64, fp);
	fclose(fp);
	v = atoi(L);
	fp = fopen(destino, "w");
	fprintf(fp, "%d\n", v + 1);
	fclose(fp);

	x_semop(sem, +1);
    }
}

El resultado ahora es perfecto:

$ ./sem2
4693 + 5307 -> 10000
4056 + 5944 -> 10000
3147 + 6853 -> 10000
3228 + 6772 -> 10000
3261 + 6739 -> 10000
3057 + 6943 -> 10000
2290 + 7710 -> 10000
1677 + 8323 -> 10000

Como indicamos, la "reserva del semáforo" consiste en invocar a nuestra rutina x_semop con “-1” (disminución a cero) y la "liberación" se consigue con “+1” (retorna a uno.)

15.2.3. Deadlocks

En aplicaciones sofisticadas es usual el uso simultáneo de varios semáforos. En tal caso existe un problema potencial comunmente conocido como "deadlock" que consiste en la "paralización" de un conjunto de procesos que mutuamente esperan la liberación de sus semáforos. Imaginemos dos procesos "P1" y "P2" que utilizan dos semáforos "S1" y "S2" en la siguiente secuencia:

  1. P1 reserva S1 (tiene éxito y no se bloquea)

  2. P2 reserva S2 (tiene éxito y no se bloquea)

  3. P2 reserva S1 (se bloquea por estar ya reservado por P1)

  4. P1 reserva S2 (se bloquea por estar ya reservado por P2)

En esta situación ambos procesos quedan (para siempre) bloqueados esperándose mutuamente puesto que ninguno tiene la opción de continuar su ejecución y liberar sus semáforos reservados. El sistema operativo no detecta estas situaciones, las cuales son responsabilidad exclusiva del programador.

En general, los deadlocks se pueden prevenir teniendo cuidado de respetar un mismo orden de reserva en todos los procesos cuando se utiliza más de un semáforo. Así, el problema surge en el ejemplo anterior porque los procesos siguen distintas secuencias de reserva S1-S2 y S2-S1.

15.3. Semáforos vs. Locks

Para muchos programas el soporte de semáforos proporcionado por las facilidades de IPC System V es demasiado sofisticado y algo engorroso (especialmente si no se construyen rutinas auxiliares como x_sem().) Es por esta razón que algunos programadores emplean (exitosamente) locks o "cerrojos de archivo" en lugar de semáforos. Stevens (1993:463) señala que poseen una performance relativamente similar y recomienda su empleo. Consúltese el capítulo 10 para más información acerca de cómo implementar cerrojos en archivos.

15.4. Ejercicios

1 La reserva de los semáforos debe realizarse en zonas críticas y de ejecución breve de los programas a riesgo de afectar negativamente la performance. En el ejemplo de transferencia de valores el semáforo fue reservado durante el acceso a ambos archivos; sin embargo, puede ser más conveniente utilizar dos semáforos, cada uno para controlar los accesos a cada uno de los archivos. Esto permite que mientras un proceso ha reservado un semáforo-archivo, el otro puede ir operando sobre el otro semáforo-archivo con lo que se reducen los tiempos perdidos de bloqueo.

De otro lado, la rutina que reporta en pantalla, deberá reservar simultáneamente ambos semáforos a fin de obtener un total consistente. Implemente la versión de dos semáforos.

2 En la versión de dos semáforos propuesta en el ejercicio 1, surge el inconveniente de que el reporte se realice justamente en mitad de una transferencia (tras liberarse el semáforo-archivo de origen pero antes de reservarse el de destino) con lo que el total puede quedar mermado en una o dos unidades (que están en mitad de su transferencia.) Con ayuda de más semáforos intente resolver este inconveniente. Tenga cuidado con los "deadlocks".

3 Consulte el capítulo 10 y convierta el programa de Transferencia de Valores para que emplee cerrojos de archivos en lugar de semáforos.

16. Memoria Compartida

La Memoria Compartida (Shared Memory) permite a un conjunto de procesos acceder simultáneamente a una misma zona de memoria. Es una de las facilidades más importantes del subsistema IPC, y se emplea en muchas aplicaciones de gran envergadura.

16.1. Memoria compartida

Normalmente dos procesos cualesquiera que se ejecuatan bajo un mismo sistema Linux/Unix mantienen muy poca información en común. A tal efecto, la memoria compartida permite que dos o más procesos accedan a una misma área de memoria de un modo controlado. El uso de la memoria compartida se considera un mecanismo adecuado cuando A) se desea que la información se comparta de forma prácticamente instantánea, B) se pretende que un número elevado de procesos accedan a la misma información y C) Todos los procesos se ejectutan en el mismo computador.

16.1.1. Interfaz de programación

La memoria compartida se programa mediante las llamadas al sistema shmget() y shmat(). Por fines prácticos, éstas serán aisladas en un archivo independiente a modo de librería (x_shm.c):

/*
 * x_shm.c: libreria para memoria compartida
 */
#include <stdlib.h>
#include <stdio.h>
#include "x_shm.h"

#define PERMISOS 0600

void *x_shm(key_t llave, int len)
{
    int id;
    void *valor;

    if (len == 0)
	id = shmget(llave, 0, 0);
    else
	id = shmget(llave, len, IPC_CREAT | PERMISOS);
    if (id == -1) {
	perror("sensor shmget");
	exit(-1);
    }
    valor = shmat(id, 0, 0);
    if (valor == (void *) -1) {
	perror("sensor shmat");
	exit(-1);
    }
    return valor;
}

La llamada al sistema shmget permite "abrir" un segmento de memoria compartida, análogamente a la apertura de un archivo. Cuando se emplea IPC_CREAT en el tercer argumento, el segmento se creará si no existiera previamente. El proceso creador del segmento debe especificar además los "permisos" de acceso al segmento. En nuestro caso, hemos usado PERMISOS=0600, lo que implica que sólo los procesos del usuario actual pueden leer y escribir en el segmento. De otro lado, el segundo argumento especifica el tamaño del segmento (en bytes) a ser creado; si el segmento ya ha sido creado este argumento es irrelevante. Nuestra rutina x_shm sigue la convención de intentar crear el segmento (IPC_CREAT) solo si el tamaño especificado para el mismo es distinto de cero.

El primer argumento es el más interesante; corresponde a lo que se denomina "llave del segmento", que es un valor asignado arbitrariamente por el programador. La asunción (algo cuestionable) es que ningún otro segmento de memoria compartida (de otras aplicaciones) usará la misma llave en su creación, mientras que todos nuestros procesos sí la comparten a manera de "secreto" [167] . A consecuencia de esto, si dos programadores "inventan" el mismo número para la llave, sus programas tendrán un conflicto si se intentan ejecutar en simultáneo en el mismo computador [168] .

Finalmente, shmget() retorna un identificador numérico (entero) usado para realizar operaciones con el segmento, análogo a un "descriptor de archivo". Nótese que todavía no podemos acceder a la memoria del segmento, puesto que tenemos que realizar la operación de "asociación".

La llamada al sistema shmat() permite "asociar" un segmento de memoria compartida con un puntero a la misma, a fin de que podamos leer y/o escribir en ella. Ésta llamada requiere como primer argumento el identificador del segmento de memoria que deseamos utilizar y retorna un puntero a la memoria del mismo. Obsérvese que los otros dos argumentos normalmente son cero, salvo situaciones poco frecuentes [169] .

Los ejemplos que hagan uso de x_shm() deberán incluir el archivo cabecera x_shm.h:

/* x_shm.h */
#ifndef X_SHM_H
#define X_SHM_H
#include <sys/ipc.h>
#include <sys/shm.h>
void *x_shm(key_t llave, int len);
#endif

16.2. Ejemplo: Sensor del medio ambiente

El siguiente programa (sensor.c) genera tres números aleatorios cada segundo, los cuales simularán ser la temperatura, humedad relativa y velocidad del viento. Estos valores se inscriben en un área de memoria compartida para que cualquier otro proceso (que sea del mismo usuario dado el permiso 0600) los pueda leer en cualquier momento:

/* sensor.c */
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include "x_shm.h"

#define SENSOR_KEY 0x12345

int main(void)
{
    double *valor;
    valor = (double *) x_shm(SENSOR_KEY, 0x1000);
    for (;;) {
	// Temperatura celsius
	valor[0] = 15.0 + (rand() % 100) / 10.0;
	// Humerdad relativa
	valor[1] = 80.0 + (rand() % 100) / 5.0;
	// Velocidad de viento
	valor[2] = 1.0 + (rand() % 100) / 20.0;

	sleep(1);
    }
    return 0;
}

El puntero devuelto por x_shm() es de tipo void *, al que forzamos con un cast al tipo double *. Mediante este último puntero escribimos los tres valores del "medio ambiente", cada segundo.

Al ser ejecutado, el programa trabaja silenciosamente sin emitir mensaje alguno. La única señal de su existencia la obtenemos de la salida del comando informativo ipcs: (en su sistema puede obtener un resultado con otras líneas adicionales)

ipcs -m
------ Shared Memory Segments --------
key        shmid   owner  perms   bytes   nattch  status
0x00012345 98306   diego  600     4096    1

Nótese que aquí el único "segmento de memoria compartida" tiene la llave 0x12345 que definimos en sensor.c mediante SENSOR_KEY.

Ahora presentaremos un programa que lee estos valores (lee_sensor.c.) Como se ve, es muy parecido al anterior, aunque un poco más fácil:

/* lee_sensor.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "x_shm.h"

#define SENSOR_KEY 0x12345

int main(void)
{
    double *valor;

    valor = (double *) x_shm(SENSOR_KEY, 0);
    for (;;) {
	system("clear");
	printf("Temperatura: %g grados celsius\n", valor[0]);
	printf("Humedad:     %g %%\n", valor[1]);
	printf("Viento:      %g nudos\n", valor[2]);
	sleep(1);
    }
    return 0;
}

Hemos especificado cero en el "tamaño" del segmento pues asumiremos que ésta ya ha sido creado por sensor.c.

El puntero double *valor ahora se emplea para leer lo que encuentra en la memoria compartida. La salida es algo como esto:

Temperatura: 17.6 grados celsius
Humedad:     88 %
Viento:      2.3 nudos

16.2.1. Eliminación de recursos IPC

Quizá haya notado que el segmento de memoria compartida creado por el programa “sensor.c” no se borra del sistema aunque ya no haya ningún proceso que haga uso del mismo (obsérvelo con el comando ipcs.) Esta es una ventaja y a la vez una desventaja de la IPC System V. Si bien puede resultar útil crear un segmento de memoria compartida y que éste permanezca intacto hasta que lo necesitemos, también puede verse como un potencial desperdicio de memoria muy fácil de olvidar. A no ser que el sistema sea reiniciado, los segmentos de memoria compartida (así como los semáforos y colas) permacerán allí para siempre, a no ser que la aplicación [170] o el usuario los elimine explícitamente. El comando Unix/Linux usado para eliminar recursos IPC System V es ipcrm. Por ejemplo, dada la salida de ipcs que vimos anteriormente, para eliminar el segmento de memoria compartida que hemos creado (cuya llave es 0x00012345 y su shmid es 98306, usaremos:

$ ipcrm -m 98306

Algunos programas pueden fallar cuando tratan de crear un recurso IPC que ya existe, por lo que puede ser necesario eliminarlos previamente.

16.3. Ejemplo: Simulación de bolas rodantes

El siguiente programa crea una "ciudad bidimensional" en la que se desplazarán algunas bolas siguiendo los caminos. Esta ciudad (y las bolas) se registran en un segmento de memoria compartida. Nótese que el programa creador (pelotas_creador.c) crea el segmento, inicializa el mapa de la ciudad, y termina inmediatamente. No es necesario que se mantenga en ejecución para que el segmento conserve la información:

/* pelotas_creador.c */
#include <stdio.h>
#include <string.h>
#include "x_shm.h"

#define ANCHO 70
#define ALTO 20
#define PELOTAS_KEY 0x666999

char campo[ALTO][ANCHO] = {
    "####################",
    "#   ##   #   #   ###",
    "# # ## # # # # # ###",
    "#                ###",
    "### ## # # # # #####",
    "###    #   #   #####",
    "####################"
};

int main(void)
{
    char *valor;

    valor = x_shm(PELOTAS_KEY, ALTO * ANCHO);
    memcpy(valor, campo, ANCHO * ALTO);
    printf("Campo para pelotas ha sido creado exitosamente.\n");
    return 0;
}

Ahora mostraremos el programa que utiliza este mapa para hacer rodar una bola (pelotas_jugador.c.) Este programa debe ser ejecutado varias veces en simultáneo (en distintos terminales o ventanas) a fin de simular el rodamiento de varias bolas. Tenga en cuenta que si uno de estos programas se detiene, no borrará la bola que le corresponde, la que quedará como un obstáculo para las que siguen en movimiento.

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include "x_shm.h"

#define ANCHO 70
#define ALTO 20
#define PELOTAS_KEY 0x666999

extern void mueve(void);
char *campo;
int x, y, v;

char getxy(int x, int y)
{
    return campo[x + ANCHO * y];
}

void set(int x, int y, char p)
{
    campo[x + ANCHO * y] = p;
}

int puede(int x, int y)
{
    if (getxy(x, y) == '#')
	return 0;
    while (getxy(x, y) == '*')
	usleep(1e4);
    return 1;
}

void pinta(void)
{
    int z, j;

    system("clear");
    for (z = 0; z < ALTO; z++) {
	for (j = 0; j < ANCHO; j++)
	    printf("%c", getxy(j, z));
	printf("\n");
    }
}

int main(void)
{
    int factor;

    campo = x_shm(PELOTAS_KEY, 0);
    x = 1, y = 1, v = 2;
    if (getxy(x, y) == '#') {
	printf("Problema en posicion de inicio\n");
	return 1;
    }
    srand(time(NULL));
    factor = 3 + rand() % 10;
    for (;;) {
	int xp, yp;
	xp = x;
	yp = y;
	mueve();
	set(xp, yp, ' ');
	set(x, y, '*');
	pinta();
	usleep(factor * 1e4);
    }
    return 0;
}

Cada proceso de bola mantiene una variable “v” que indica la dirección del movimiento de la misma: 1=hacia arriba, 2=hacia la derecha, 3=hacia abajo, 4=hacia la izquierda. La bola intenta siempre avanzar; si se encuentra con una "pared", trata de cambiar de rumbo 90 grados, y como última posibilidad retorna por donde vino (función mueve()):

/* pelotas_mueve.c */
extern int x, y, v;
extern int puede(int x, int y);

void mueve(void)
{
    switch (v) {
    case 1:
	if (puede(x, y - 1)) {
	    y -= 1;
	    break;
	}
	if (puede(x + 1, y)) {
	    x += 1;
	    v = 2;
	    break;
	}
	if (puede(x - 1, y)) {
	    x -= 1;
	    v = 4;
	    break;
	}
	v = 3;
	break;
    case 2:
	if (puede(x + 1, y)) {
	    x += 1;
	    break;
	}
	if (puede(x, y + 1)) {
	    y += 1;
	    v = 3;
	    break;
	}
	if (puede(x, y - 1)) {
	    y -= 1;
	    v = 1;
	    break;
	}
	v = 4;
	break;
    case 3:
	if (puede(x, y + 1)) {
	    y += 1;
	    break;
	}
	if (puede(x + 1, y)) {
	    x += 1;
	    v = 2;
	    break;
	}
	if (puede(x - 1, y)) {
	    x -= 1;
	    v = 4;
	    break;
	}
	v = 1;
	break;
    case 4:
	if (puede(x - 1, y)) {
	    x -= 1;
	    break;
	}
	if (puede(x, y + 1)) {
	    y += 1;
	    v = 3;
	    break;
	}
	if (puede(x, y - 1)) {
	    y -= 1;
	    v = 1;
	    break;
	}
	v = 2;
	break;
    }
}

He aquí como se ve la salida del programa en la 3ra ventana terminal (con tres bolas):

####################
#   ##   #   #   ###
# # ## # # # # # ###
#        *     * ###
### ## # # # # #####
###*   #   #   #####
####################

La velocidad de movimiento (en realidad, el factor de pausa) de las bolas se determina al inicio aleatoriamente usando rand(), por lo que las bolas más rápidas alcanzarán eventualmente a las bolas más lentas. En una ciudad real con pistas de un solo carril, cuando los autos se desplazan por una misma ruta terminan formando una "cola" con el auto más lento en la delantera. Este comportamiento ha sido simulado en la función puede(), la cual "retarda" la bola cada vez que encuentra con otra más lenta en su camino hasta que ésta se mueva.

16.3.1. Errores difíciles de reproducir

El tránsito de nuestra ciudad luce bastante ordenado aunque algo aburrido. Pensemos ahora en un caso poco probable: dos autos llegan exactamente al mismo tiempo a una intersección, desde direcciones distintas. ¿Qué ocurre?

En nuestro programa, las bolas verifican la no existencia de otra de éstas mediante:

while(getxy(x,y)=='*')
	usleep(1e4);
... avanzar ...

Si la posición (x,y) fuera justamente una intersección, existe una pequeña (pero no nula) posibilidad de que dos procesos encuentren simultáneamente la intersección libre y ambos decidan avanzar en simultáneo (colisión). En la vida real, para evitar esto las intersecciones tienen "semáforos", y como sabemos, los sistemas Linux/Unix también [171] .

Este "pequeño" error de nuestro programa es del tipo más difícil de combatir. Puesto que la probabilidad de que ocurra es tremendamente pequeña, el error no aparecerá normalmente en el escritorio del programador; pero cuando el programa se distribuya a cientos de usuarios que lo emplean los 365 días de año, algún día habrá alguien que tendrá "mala suerte".

En la siguiente sección resolveremos este problema mediante el uso de un semáforo (ver capítulo {sem_num} para más información.) La combinación de Memoria Compartida con un mecanismo de sincronización (como los semáforos) es muy usual en las aplicaciones.

16.3.2. Bolas Rodantes con Semáforos

Detallemos un poco más el problema de las coliciones en las intersecciones. Cuando dos bolas "analizan" la siguiente posición para su desplazamiento y ésta resulta libre, ambas pueden (a la vez) "moverse" hacia ésta, lo cual es una "colisión". A fin de evitar esto, podríamos forzar a que sólo una bola a la vez tenga derecho de "analizar" y "moverse", puesto que así, la siguiente bola que "analiza", ya puede hallar a la otra en su nueva posición (pues ésta ya se ha "movido".)

Una forma de implementar esto es mediante un "semáforo" que funcionará como una especie de "autorización" que va pasando de bola en bola (de proceso en proceso), y que les permite su avance (es decir, el análisis y el movimiento.) A continuación, la nueva versión del programa de movimiento de bolas (pelotas_jugador_sem_crash.c) que hace uso un semáforo [172] :

/* pelotas_jugador_sem_crash */
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include "x_shm.h"
#include "x_sem.h"

#define ANCHO 70
#define ALTO 20
#define PELOTAS_KEY 0x666999

extern void mueve(void);
char *campo;
int x, y, v;
int plantar;

char getxy(int x, int y)
{
    char c = campo[x + ANCHO * y];
    if (c == 0)
	return ' ';
    return c;
}

void set(int x, int y, char p)
{
    campo[x + ANCHO * y] = p;
}

int puede(int x, int y)
{
    if (getxy(x, y) == '#')
	return 0;
    if (getxy(x, y) == '*')
	plantar = 1;
    return 1;
}

void pinta(void)
{
    int z, j;

    system("clear");
    for (z = 0; z < ALTO; z++) {
	for (j = 0; j < ANCHO; j++)
	    printf("%c", getxy(j, z));
	printf("\n");
    }
}

int main(void)
{
    int id_sem, factor;

    campo = (char *) x_shm(PELOTAS_KEY, 0);
    id_sem = x_sem(PELOTAS_KEY);

    x = 1, y = 1, v = 2;
    if (getxy(x, y) == '#')
	return 1;
    srand(time(NULL));
    factor = 3 + rand() % 10;
    for (;;) {
	int xp, yp;

	xp = x;
	yp = y;
	x_semop(id_sem, -1);
	plantar = 0;
	mueve();
	if (!plantar) {
	    set(xp, yp, ' ');
	    set(x, y, '*');
	} else {
	    x = xp;
	    y = yp;
	}
	pinta();
	x_semop(id_sem, +1);
	usleep(factor * 1e4);
    }

    return 0;
}

Además del tema de los semáforos hay otras pequeñas modificaciones en esta versión. Por ejemplo, en la función puede() ya no usamos el loop while pues podría ocasionar una espera indefinida debido a que en ese punto del programa el proceso ya se ha adueñado del semáforo mientras el resto de procesos están suspendidos esperando que lo libere. En su lugar, si el proceso se encuentra con otra bola, simplemente no avanza (se activa la variable plantar, que en en realidad no detiene el avance, pero que genera un inmediato retroceso en main() [173] ) e inmediatamente libera su semáforo.

16.4. Ejercicios

1 Adapte el programa sensor del medio ambiente para que registre algunos datos reales del desempeño del computador. Por ejemplo, % de uso de cpu, % de uso de filesystems. Mejore la visualización de los "lectores" usando Curses (capítulo 11.)

2 Mejore la visualización del programa de movimiento de bolas usando Curses (capítulo 11) a fin de evitar el borrado con parpadeo de system("clear").

3 En el programa que genera la "ciudad" para el movimiento de bolas, altere el mapa indicando las "intersecciones" (cruce de dos vías) con una "s". Estas 's' deben dar lugar ahora a semáforos individuales (los que se pueden almacenar en un conjunto de semáforos.) Cada vez que la bola de un proceso llegue a una 's', el semáforo respectivo deberá ser "disminuido" en uno, y liberado tras el siguiente movimiento. Esto es análogo a ciertos semáforos inteligentes instalados en algunas ciudades reales, los cuales permiten el paso dependiendo de la dirección de los vehículos que llegan a la intersección con lo que se reducen las esperas innecesarias.

4 El programa lee_sensor.c nunca escribirá en el segmento (sólo lee), por lo que es conveniente (aunque no imprescindible) que la llamada shmat() utilice la opción SHM_RDONLY (sólo lectura) en su tercer argumento, como una medida extra de seguridad.

Asimismo, cuando x_shm() falla, el programa termina de inmediato (con exit()) sin dar posibilidad a un mejor manejo de errores.

El ejercicio consiste en mejorar x_shm() a fin de superar estas limitaciones.

17. Sockets y TCP/IP

La interfaz de programación de sockets es un mecanismo general de comunicación entre procesos, y es la más empleada para programar aplicaciones que utilizan los protocolos TCP/IP [174] . En este capítulo presentamos algunos conceptos básicos y ejemplos "típicos" de conectividad TCP, para dos o más procesos. A tal efecto confeccionaremos la base de una pequeña "librería" de rutinas de comunicación, que emplearemos en los ejemplos.

17.1. Conceptos muy básicos de TCP/IP

Sin el más remoto ánimo de describir los protocolos TCP/IP, baste indicar que para conectar dos computadores, ambos deben poseer al menos una dirección de red (dirección IP) válida [175] , así como un medio físico asociado a las mismas. Por ejemplo, los computadores de las redes de oficina generalmente tienen un hardware de red Ethernet mediante el cual se interconectan por medio de cables, hubs, switches y routers. Cada interfaz Ethernet tiene normalmente una dirección IP asociada [176] .

17.1.1. Puertos

En la mayoría de casos lo que se desea no es que los computadores interactúen, sino sus aplicaciones (las cuales muchas veces está utilizando un usuario.) Esto hace necesario un mecanismo de identificación para cada aplicación que participa, al que (en las redes TCP/IP) se le denomina puerto y es un número entero positivo.

Resumiento, en una comunicación entre dos procesos de red TCP/IP, los parámetros principales son las direcciones IP de las interfaces de red de los computadores participantes, así como el número de puerto que identifica a cada proceso participante dentro de cada computador.

Muchos puertos tienen propósitos estandarizados. Por ejemplo, el puerto 80 está reservado para que los servidores Web "escuchen" solicitudes de los programas de tipo "Web Browser". Estos últimos por el contrario, reciben algún puerto libre en el sistema, normalmente superior a 1024 [177] . Un listado de puertos reservados se puede apreciar en el archivo /etc/services presente en los sistemas Unix/Linux.

17.1.2. Clientes y Servidores

Las comunicaciones TCP/IP son bidireccionales. Se denomina clientes a los procesos que inician una conexión TCP/IP con otro proceso (que posiblemente se ejecuta en otro computador.) Aquel otro proceso, puesto que debe estar esperando a recibir conexiones (se dice que está "escuchando" en un puerto) es denominado servidor. Una vez establecida la conexión, la diferencia entre cliente y servidor desaparece.

17.2. Averiguar la hora de un computador

Mediante este ejemplo pretendemos explicar la programación de un sencillo "cliente" TCP. Aprovecharemos que los sistemas Linux/Unix ya proveen el "servidor" correspondiente (aunque puede estar desactivado.) Más adelante analizaremos la programación de los servidores.

El servicio "daytime" se suele usar como una herramienta de diagnóstico. Este servicio, cuando está habilitado, recibe conexiones de tipo TCP o UDP (en realidad, son dos servicios) y responde a la conexión con un texto indicando la fecha y hora actual del servidor [178] .

En la mayoría de computadores este servicio es innecesario y suele estar desactivado. Ud. deberá activarlo (o pedir al administrador que lo active) antes de proseguir.

17.2.1. Activación del servicio daytime

Esta sección no es de programación, sino más bien de "administración" de redes Linux/Unix. Para activar el servicio "daytime tcp" se puede usar los siguientes métodos (se requiere acceso a root):

En algunos Linux (como RedHat) se puede usar el comando ntsysv, activar la casilla correspondiente a "daytime", grabar, y luego ejecutar:

# service xinetd reload

Si no se dispone de ntsysv, se puede editar los archivos de configuración de xinetd y activar este servicio manualmente.

En otros sistemas Linux/Unix (por ejemplo, Debian 3.1), la configuración suele estar dada en el archivo /etc/inetd.conf. Configure la línea correspondiente a "daytime" y envíe la señal 1 (SIGHUP) a inetd para que recargue su configuración:

# vi /etc/inetd.conf
...
daytime         stream  tcp     nowait  root    internal
...
# ps ax | grep inetd
  413 ?        Ss     0:00 /usr/sbin/inetd
# kill -1 413

Ud. puede verificar si daytime está activado con el siguiente comando en Linux [179] :

# netstat -a --inet -n | grep 13
tcp 0  0   0.0.0.0:13    0.0.0.0:*    LISTEN

Notar el 0.0.0.0:13, que significa "escucha" (LISTEN) en el puerto 13 con protocolo TCP. Si revisa el archivo /etc/services, notará que el 13 corresponde al servicio "daytime".

17.2.2. Librería Cliente TCP

A continuación presentamos dos funciones que serán invocadas por los ejemplos que veremos.

/*
 * cliente.c: rutinas de cliente tcp
 */
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>

int connect_by_port(const char *hostname, int port)
{
    struct hostent *host;
    struct sockaddr_in dir;
    int s;

    s = socket(AF_INET, SOCK_STREAM, 0);
    if (s == -1) {
	fprintf(stderr, "socket() failure (%d)\n", errno);
	return -1;
    }
    host = gethostbyname(hostname);
    if (host == NULL) {
	fprintf(stderr, "gethostbyname() failure (%d)\n", errno);
	return -1;
    }
    memset(&dir, 0, sizeof(dir));
    dir.sin_family = AF_INET;
    dir.sin_port = htons(port);
    memcpy(&dir.sin_addr, host->h_addr, host->h_length);

    if (connect(s, (struct sockaddr *) &dir, sizeof(dir)) == -1) {
	fprintf(stderr, "connect() failure (%d)\n", errno);
	return -1;
    }
    return s;
}

int connect_by_service(const char *hostname, const char *service)
{
    struct servent *se;
    se = getservbyname(service, "tcp");
    if (se == NULL) {
	fprintf(stderr, "getservbyname() failure (%d)\n", errno);
	return -1;
    }
    return connect_by_port(hostname, ntohs(se->s_port));
}

La función connect_by_port() permite establecer una conexión TCP hacia un computador y puerto especificados como argumentos. Este "computador" puede ser un nombre (tal como xx.dominio.com) o una dirección IP (tal como 34.12.44.22.) La rutina gethostbyname() convierte este dato en una estructura de tipo hostent que contiene la dirección IP adecuadamente codificada para las funciones de sockets (en el campo "h_addr") [180] . Por otro lado, la llamada al sistema connect() requiere una estructura de tipo sockaddr_in que contiene tanto la dirección (campo "h_addr") como el puerto. Esto se almacena en sus campos sin_addr y sin_port [181] , [182] .

Es conveniente comentar los argumentos de llamada al sistema socket(). El primero de ellos (en nuestro caso PF_INET) corresponde a la "familia de protocolos" a emplearse. En nuestro caso, PF_INET corresponde a los protocolos Internet (TCP/IP.) En Linux, por ejemplo, es posible usar el identificador PF_X25 para acceder a protocolos X.25 (asumiendo que se dispone del software/hardware correspondiente), PF_IPX para acceder a protocolos IPX de Novell, etc.

El segundo parámetro corresponde al tipo de conexión que se desea. En nuestro caso ha sido SOCK_STREAM que corresponde a un flujo bidireccional confiable y ordenado. Para la famila TCP/IP esto corresponderá al protocolo TCP, que es lo que deseamos.

El último parámetro sirve específicamente para señalar qué protocolo deseamos para el tipo de conexión elegida cuando hay varias opciones disponibles. En tanto SOCK_STREAM normalmente es satisfecho con TCP, no hay necesidad de especificar nada aquí.

A continuación el programa cliente de "daytime" (daytime.c):

/*
 * daytime.c: Cliente daytime
 */
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

extern int connect_by_service(const char *, const char *);

int main(int argc, char **argv)
{
    int s, n;
    char R[256];
    char server[256];

    if (argc == 1)
	strcpy(server, "127.0.0.1");
    else
	strcpy(server, argv[1]);

    s = connect_by_service(server, "daytime");
    if (s == -1)
	exit(1);

    memset(R, 0, 256);
    n = read(s, R, 256);
    if (n == -1)
	fprintf(stderr, "read %d\n", errno), exit(1);
    close(s);
    printf("Server %s responde: %s", server, R);
    return 0;
}

Como se aprecia, hemos invocado a una nuestra rutina connect_by_service() (listada anteriormente) para que establezca una conexión al host deseado (por omisión, 127.0.0.1) y al puerto correspondiente al servicio "daytime" [183] .

En caso de error, esta función retorna -1; en caso contrario, retona un "descriptor del socket" (similar a un archivo abierto con open(2)) usado para las operaciones de I/O. En nuestro caso, nos hemos limitado a leer de la red con read(2) potencialmente hasta 256 caracteres, e imprimirlos en el terminal. Esta forma de "leer" no es del todo correcta como se verá más adelante.

17.3. Sistema de monitoreo de varios computadores

A continuación desarrollaremos un pequeño sistema distribuido de monitoreo (SDM), cuyo objetivo apunta a lograr que todos los computadores de una red estén informados acerca del estado del resto de ellos.

Para conseguir esto, uno de los computadores ejecutará un proceso especial que actuará como "repositorio general", el cual mantendrá centralizadamente la información de todos los demás. Llamaremos sdm_servidor.c a este programa.

El resto de computadores informará a sdm_servidor acerca de su estado, y al mismo tiempo obtendrá el estado de todos los otros. En nuestra versión, el "estado" consistirá simplemente en una línea de texto conteniendo el nombre del computador (hostname) y la carga promedio. Este "estado" será actualizado cada 10 segundos por cada computador participante mediante el programa sdm_cliente.c. Si un computador participante pasa mas de un minuto sin informar a sdm_servidor, éste último considerará que el participante ha dejado de serlo y lo eliminará de la lista.

Como es de esperarse, todo esto requiere que sdm_servidor sea un "servidor tcp/ip" en el sentido explicado anteriormente, a fin de que recepcione múltiples conexiones entrantes (clientes.)

17.3.1. Implementación de servidores TCP

En general, los programas servidores TCP tienen la siguiente estructura:

/*
 * Esqueleto servidor TCP
 */
struct sockaddr_in addr;
int puerto=...;

memset(&addr,0,sizeof(addr));
addr.sin_family=AF.INET;
addr.sin.addr.s.addr=INADDR_ANY;
addr.sin_port=htons(puerto);

s=socket(...);
bind(s,&addr,sizeof(addr));
listen(s,#);

for(;;)
	{
	sc=accept(s,&addr,&tmp);
	... usar descriptor "sc" ...
	close(sc);
	}

En resumen, lo que hace esto es:

  1. Inicializar una estructura "sockaddr_in" (que aquí llamamos "addr") la que contiene el puerto al que escuchará el servidor

  2. Crear el socket de escucha (llamado "s") con socket(2)

  3. Asociar la estructura "addr" al socket con bind(2) (esto falla si ya hay otro proceso "escuchando" en el puerto)

  4. Preparar una "cola de recepción" de conexiones para el socket, usando listen(2)

  5. En un loop posiblemente sin fin, recibir las conexiones entrantes con accept(2), la cual produce un nuevo descriptor de la conexión entrante (aquí llamado "sc")

  6. Utilizar "sc" para operaciones de lectura/escritura

  7. Cerrar "sc" con close(2)

  8. Volver a invocar a accept(2) a la espera de nuevas conexiones

Una variante frecuente de este esquema, consiste en crear un subproceso hijo que se encargue de operar con "sc", mientras el proceso padre de inmediato retorna a la espera de nuevas conexiones con accept().

17.3.2. Librería servidor TCP

Con el fin de hacer más reutilizable y sencillo el código del servidor, craremos una estructura auxiliar (a la que denominaremos CONEXION) que evitará tener que lidiar con estructuras de tipo "sockaddr_in" y una serie de detalles poco relevantes.

Esta estructura tiene por miembros:

  1. El descriptor de la conexión entrante (fd)

  2. La dirección IP del computador cliente (host)

  3. El puerto que utiliza el cliente (port)

Dicha estructura la definimos en un archivo cabecera a ser incluido por cualquier programa que invoque nuestras rutinas (tcpaux.h):

/* tcpaux.h: rutinas de comunicacion TCP */
#ifndef TCPAUX_H
#define TCPAUX_H

#define LEN 256

typedef struct
	{
	int fd;
	char host[LEN];
	int port;
	} CONEXION;

int net_listen_port(int p);
void net_close(CONEXION *c);
CONEXION *net_accept(int s);

int connect_by_port(const char *host,int port);
int connect_by_service(const char *host,const char *service);

int net_read(int fd, char* buf, int count);
int net_write(int fd,const char* buf, int count);
#endif

A continuación el código que implementa rutinas de "servidor" TCP.

/*
 * servidor.c: rutinas de servidor tcp
 */
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "tcpaux.h"

CONEXION *net_accept(int s)
{
    struct sockaddr_in addr;
    int sc, len;
    CONEXION *c;

    len = sizeof(addr);
    memset(&addr, 0, len);
    sc = accept(s, (struct sockaddr *) &addr, &len);
    if (sc == -1)
	return NULL;

    c = calloc(1, sizeof(CONEXION));
    c->fd = sc;
    strncpy(c->host, inet_ntoa(addr.sin_addr), LEN);
    c->port = ntohs(addr.sin_port);
    return c;
}

int net_listen_port(int p)
{
    int s;
    struct sockaddr_in addr;

    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(p);

    if ((s = socket(PF_INET, SOCK_STREAM, 0)) == -1) {
	fprintf(stderr, "socket (%d)\n", errno);
	return -1;
    }
    if (bind(s, (struct sockaddr *) &addr, sizeof(addr)) == -1) {
	fprintf(stderr, "bind (%d)\n", errno);
	return -1;
    }
    if (listen(s, 5) == -1) {
	fprintf(stderr, "listen (%d)\n", errno);
	return -1;
    }
    return s;
}

void net_close(CONEXION * c)
{
    if (c == NULL)
	return;
    close(c->fd);
    free(c);
}

Obsérvese el uso de la rutina inet_ntoa() (en net_accept()) empleada para construir un texto con la dirección IP a partir de la estructura sockaddr_in.

Un programa "servidor" que hace uso de estas rutinas tiene aproximadamente la siguiente estructura [184] :

s=net_listen_port(numero de puerto);
CONEXION* cliente=net_accept(s);
...
read(cliente->fd,buffer,LEN)
write(cliente->fd,buffer,LEN)
...
net_close(cliente);

17.3.3. El servidor del sistema de monitoreo

A continuación el programa sdm_servidor:

/*
 * sdm_servidor.c: Servidor sistema de estados
 */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include "tcpaux.h"

#define MAX_STATUS 10
static void status_update(CONEXION *, char *);

time_t status_time[MAX_STATUS];
char status_host[MAX_STATUS][256];
char status_data[MAX_STATUS][256];

int main()
{
    int s, z;
    char clientdata[256];
    CONEXION *cliente;
    char n;

    s = net_listen_port(5678);
    for (;;) {
	cliente = net_accept(s);
	if (read(cliente->fd, clientdata, 256) == 256)
	    status_update(cliente, clientdata);

	n = 0;
	for (z = 0; z < MAX_STATUS; z++)
	    if (status_time[z])
		n++;
	write(cliente->fd, &n, 1);
	for (z = 0; z < MAX_STATUS; z++)
	    if (status_time[z])
		write(cliente->fd, status_data[z], 256);
	net_close(cliente);
    }
}

static void status_update(CONEXION * c, char *d)
{
    int z;
    time_t t;

    t = time(NULL);
    for (z = 0; z < MAX_STATUS; z++)
	if (status_time[z] < t - 60)
	    status_time[z] = 0;
    for (z = 0; z < MAX_STATUS; z++)
	if (strcmp(status_host[z], c->host) == 0)
	    break;
    if (z == MAX_STATUS)
	for (z = 0; z < MAX_STATUS; z++)
	    if (status_time[z] == 0)
		break;
    if (z == MAX_STATUS) {
	fprintf(stderr,
		"Tabla llena. No puedo actualizar host %s\n", c->host);
	return;
    }
    status_time[z] = t;
    strcpy(status_host[z], c->host);
    sprintf(status_data[z], "%s: %s", c->host, d);
}

A continuación una breve explicación de este listado:

  1. Se prepara un "socket de escucha" llamado "s", asociado al puerto 5678 mediante "net_listen_port()"

  2. Inicia un loop sin fin. En cada iteración acepta una conexión entrante por el "socket de escucha" (función "net_accept()"). Esta conexión se describe con la variable puntero "cliente"

  3. Usando el descriptor de la nueva conexión (que se puede hallar en "cliente→fd") se lee 256 bytes. Para nuestro programa, esto corresponde a la "información de estado" del cliente, la cual se procesa en la función status_update()

  4. Se contabiliza (en "n") cuantos "estados" o clientes válidos tenemos hasta el momento y se envía al cliente un byte que contiene el valor "n"

  5. Se envía al cliente los estados de todos los clientes activos, en paquetes de 256 bytes

  6. Con net_close() se cierra la conexión con el cliente y se libera la estructura asociada al puntero cliente

Actualización de estados

Cada vez que un cliente se conecta, sdm_servidor invoca a status_update() que realiza las siguientes acciones:

  1. Elimina los estados que tienen más de 60 segundos de antigüedad. Estos se "liberan" inscribiendo cero en el índice "status_time"

  2. Busca en la tabla "status_host" a ver si el cliente actual (el que se acaba de conectar) ya tiene una entrada; en caso contrario:

  3. Busca un espacio libre en la tabla "status_time" y lo inscribe; ni no queda espacio imprime un mensaje de error y retorna

  4. Actualiza en la tabla el estado recién enviado por el cliente, la hora y el host

17.3.4. El cliente del sistema de monitoreo

Este programa (sdm_cliente.c) cada 10 segundos intenta conectarse al programa sdm_servidor (explicado arriba.) Siendo cliente TCP, hace uso de las rutinas connect_by_port() y connect_by_service() que se expusieron anteriormente.

Este programa además ilustra el uso de la llamada al sistema uname(2) que permite obtener información acerca del "nombre del host" [185] .

Posteriormente, y en forma repetitiva, obtenemos la salida del comando uptime (mediante popen(3)), el cual proporciona la "carga promedio" del sistema (vea su documentación si no lo conoce.)

Todo esto es combinado en el string status y se envía a sdm_servidor tan pronto como éste acepta nuestra conexión. Posteriormente, recibimos exactamente un byte, y según el valor de éste, leeremos cierta cantidad de bloques de 256 bytes (los estados de todos los computadores participantes), los cuales se imprimen en la salida estándar, previa limpieza de pantalla con system("clear").

Tras esta lectura, el socket se cierra y se espera 10 segundos. Luego todo empieza de nuevo.

/*
 * sdm_cliente.c: Cliente de sistema de estados
 */
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/utsname.h>
#include "tcpaux.h"

#define MAXSTR 256

int main(int argc, char **argv)
{
    int s, z;
    char hostname[MAXSTR], uptime[MAXSTR], server[MAXSTR],
	status[MAXSTR], *p, n;
    FILE *fp;
    struct utsname un;

    if (argc != 2)
	fprintf(stderr, "Especifique host servidor.\n"), exit(1);
    strcpy(server, argv[1]);

    uname(&un);
    strcpy(hostname, un.nodename);

    for (;;) {
	fp = popen("uptime", "r");
	if (fp == NULL)
	    fprintf(stderr, "Fallo popen uptime.\n"), exit(1);
	fgets(uptime, 256, fp);
	pclose(fp);
	uptime[strlen(uptime) - 1] = '\0';
	p = strstr(uptime, "average");
	if (p == NULL)
	    fprintf(stderr, "Error formato de uptime.\n"), exit(1);

	sprintf(status, "%-20s %s", hostname, p + 8);
	/*printf("[%s]\n",status); */

	s = connect_by_port(server, 5678);
	if (s == -1)
	    fprintf(stderr, "No pude conectarme.\n"), exit(1);

	write(s, status, 256);
	if (read(s, &n, 1) == -1)
	    exit(1);
	system("clear");
	for (z = 0; z < n; z++)
	    if (read(s, status, 256) == 256)
		printf("%.256s\n", status);
	close(s);
	sleep(10);
    }
    return 0;
}

17.3.5. Problemas Potenciales

A continuación analicemos algunos escenarios de falla de nuestro rudimentario "Sistema de Monitoreo", y propongamos soluciones:

Si el proceso servidor cae, o si el computador que lo ejecuta cae, o si su conexión de red cae, entonces, todo el sistema se viene abajo puesto que los clientes no podrán conectarse y terminarán automáticamente. En general, la caída de los servidores es algo que se suele evitar a toda costa (por lo que suelen ser computadores más costosos y de mejor calidad.) Pero, ¿cómo podríamos arreglárnoslas si de todos modos el servidor cae? Algunas ideas:

  1. Los clientes pueden hacer reintentos de conexión cada minuto, con la esperanza de que la interrupción del servidor haya sido momentánea

  2. Se puede definir un segundo servidor desde el inicio, de modo tal que los clientes siempre estén actualizando a ambos, en la medida que éstos respondan

Como el servidor no mantiene conexiones estables con los clientes, la caída de cualquiera de éstos últimos normalmente no generará ningún efecto negativo. Su estado será borrado por antigüedad y todo seguirá como de costumbre.

Sin embargo, considérese el caso de un cliente cuya conexión se pierde DURANTE la comunicación con el servidor (lo cual es poco probable puesto que ésta es muy breve, pero no es imposible.) En ese caso, el servidor se "congelará" hasta que descubra que el cliente ha fallado. Es decir, todo el sistema lucirá "congelado" por algunos momentos (que puede ser del orden de minutos.)

Típicamente, esto se evita haciendo que el servidor atienda a los clientes mediante procesos auxiliares, de modo tal que cualquier "congelamiento" sólo afecte al proceso auxiliar mientras el padre sigue sirviendo al resto de clientes. Existen otras maneras, pero van más allá de nuestros objetivos.

En la siguiente sección se detalla y resuelve otro problema potencial.

17.4. Lecturas y Escrituras TCP

Los ejemplos anteriores han venido empleando directamente las llamadas al sistema read(2) y write(2) para hacer las lecturas y escrituras con los sockets. Éstas deben funcionar bien en condiciones favorables (paquetes pequeños, redes LAN descongestionadas, interconexión sin routers intermedios, etc.), pero pueden tener problemas en situaciones extremas como veremos.

El protocolo TCP tiene como características asociadas tanto la garantía de no corrupción de la información, así como el orden en que ésta se transfiere (en otros protocolos es posible recibir paquetes con datos corrompidos, o en un orden distinto al que se transmitieron.)

Sin embargo, algo que NO garantiza TCP es el mantenimiento de los límites de los paquetes transmitidos. Esto quiere decir, por ejemplo, que si enviamos 10000 bytes por la red mediante una sola escritura write(), es posible que quien reciba estos datos tenga que hacer 10 lecturas read() de 1000 bytes cada una para leerlo todo. O al revés, tras enviar diez pequeños paquetes de 15 bytes cada uno, el lector los recibe todos en una sola gran lectura de 150 bytes.

Esto obliga a que las aplicaciones definan mecanismos adicionales a fin de enviar y recibir la cantidad esperada de información.

Nótese que estos problemas pueden ocurrir también durante las escrituras de red. Por ejemplo, podemos intentar escribir 10000 bytes, pero nuestro sistema puede aceptar sólo 1500 (eso lo comprobamos con el valor retornado por write(2)), lo que nos obliga a intentar volver a enviar los otros 8500 bytes faltantes en sucesivos intentos [186] .

En conclusión, nuestros anteriores programas que emplean directamente read(2) y write(2) de seguro funcionarán bien en una red LAN no sobrecargada, pero en otras condiciones surgirán problemas.

La solución no es compleja, y se presta a ampliar nuestra librería de rutinas TCP/IP. A continuación presentamos dos funciones que intentan respectivamente leer y escribir exactamente N bytes haciendo los reintentos necesarios [187] .

/*
 * readwrite.c: lectura/escritura con reintentos
 */
#include <unistd.h>

int net_read(int fd, char *buf, int count)
{
    char *pts = buf;
    int status = 0, n;

    while (status != count) {
	n = read(fd, pts + status, count - status);
	if (n == -1)
	    return -1;
	if (n == 0)
	    return status;
	status += n;
    }
    return count;
}

int net_write(int fd, const char *buf, int count)
{
    const char *pts = buf;
    int status = 0, n;

    while (status != count) {
	n = write(fd, pts + status, count - status);
	if (n == -1)
	    return -1;
	status += n;
    }
    return count;
}

17.5. Multiplexión

En esta sección veremos una de las técnicas más importantes que permite interactuar simultáneamente con varias conexiones de red.

Cuando tenemos un conjunto de conexiones establecidas, es frecuente que deseemos leer o escribir datos provenientes de, o hacia las mismas. Lamentablemente, tanto read(2) como write(2) sólo pueden operar con un descriptor a la vez [188] .

Una solución a la que a veces se recurre consiste en crear N procesos para que cada uno de ellos haga read()/write() respectivamente, sobre las N conexiones. Por lo general esto tiene sentido sólo cuando se trata de una lectura y una escritura.

Otra posibilidad consiste en iterar sobre todos los descriptores de un modo similar a esto: (con write() sería análogo.)

for(z=0;z<MAXFD;z++)
	if(read(z,buf,sizeof(buf)>0)
		... llego data por descriptor z ...

Para que esto funcione, es necesario que read(2) no se "bloquee" esperando a que llegue información en los descriptores, a fin de que todos tengan la oportunidad de ser analizados a cada momento. Esto último se puede conseguir configurando el flag O_NONBLOCK a cada descriptor con fcntl(2).

El problema de esta estrategia (polling) es que desperdicia ingentes recursos de procesador puesto que se trata de un loop que nunca se detiene hasta que llegue la información.

La solución más conveniente se proporciona mediante la llamada al sistema select(2), la cual se usa para "esperar" a que los descriptores estén listos para ser leídos (con información recién llegada) o para ser escritos (con nueva información de nuestro proceso.)

17.5.1. Select en detalle

Esta llamada admite el siguiente prototipo:

int select(int n, fd`set *rds, fd`set *wds, fd_set *eds,
       struct timeval *timeout);

rds, wds y eds son, respectivamente, el conjunto de descriptores desde donde se desea leer, a donde se desea escribir, y en donde pueden ocurrir "excepciones". Cuando no estamos interesados en todos estos casos, se puede especificar NULL.

timeout es una estructura que permite indicar un tiempo de "espera" máximo a select(2), pasado el cual retorna aunque no haya ningún descriptor preparado. Si se especifica NULL entonces select(2) espera para siempre (se bloquea.)

“n” es el descriptor con el número más alto en cualquiera de los tres conjuntos, más 1.

select(2) retorna el número de descriptores que están "listos" para alguno de los casos señalados, cero si se vence el tiempo especificado, y -1 en caso de error. Los conjuntos de descriptores se manipulan mediante las macros:

  • FD_CLR(int fd, fd_set *set) Elimina un descriptor del conjunto

  • FD_ISSET(int fd, fd_set *set) Indaga por un descriptor al conjunto

  • FD_SET(int fd, fd_set *set) Añade un descriptor al conjunto

  • FD_ZERO(fd_set *set) Elimina todos los descriptores del conjunto

17.5.2. Ejemplo: Sistema de concurso escolar

A continuación elaboraremos un pequeño sistema interactivo de preguntas y respuestas que puede ser usado para hacer competir a un grupo de colegiales. El "servidor" será un programa que "inventa" un problema matemático. Cada "cliente" que se conecta al servidor recibe el problema, y puede intentar responderlo.

Cada vez que alguien se conecta, el servidor informa a los ya conectados acerca del nuevo concursante. De igual modo el servidor informa cada vez que alguien se desconecta (por ejemplo, presionando CTRL+C en su terminal.) El servidor verifica que las respuestas dadas sean correctas o incorrectas y va informando a todos los participantes acerca de las respuestas que recibe. Cuando la respuesta es correcta, lo notifica y crea un nuevo problema matemático.

A continuación un ejemplo de una sesión típica:

$ ./prog6 10.1.1.6 Diego
Nuevo concursante (4): Diego
33+36
56
Palos para Diego!
Su torpe respuesta fue 56
33+36
Nuevo concursante (5): Sebastian
Palos para Sebastian!
Su torpe respuesta fue 90
33+36
69
Felicitaciones para Diego!
Su brillante respuesta fue 69
27+15
Cliente de sistema de concurso

Como es usual, este programa hace uso de las funciones que hemos venido programando “connect_by_port()”, “net_read()” y “net_write()”. Lo interesante radica en que emplea a select(2) para escuchar simultáneamente en dos descriptores:

  1. El socket que se ha conectado a la espera de mensajes del servidor

  2. El descriptor cero, entrada estándar, a la espera de texto introducido por teclado

Se emplea dos "conjuntos de descriptores". El primero, ifds, contiene los dos descriptores antes mencionados; el segundo, testds, antes de llamar a select() es una copia de ifds, y luego es alterado por la llamada al sistema para indicar qué descriptores son los que están listos para ser escuchados. Esto último se verifica con la macro FD_ISSET. Si se trata del descriptor cero (entrada por teclado), el texto se lee con fgets(), y se envía al servidor. Si se trata del socket de red, se lee con “net_read()” y simplemente se imprime en pantalla.

Aprecie con cuidado la inicialización de ifds con las macros:

  • FD_ZERO(&ifds);

  • FD_SET(0, &ifds);

  • FD_SET(s, &ifds);

/*
 * concurso_cliente.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include "tcpaux.h"

#define LEN 256

char pregunta[LEN];
int respuesta;
fd_set ifds, testds;
int s;

int main(int argc, char **argv)
{
    int n, z;
    char txt[LEN];
    fd_set testds, ifds;

    if (argc != 3) {
	fprintf(stderr, "Especifique server y vuestro nombre\n");
	exit(1);
    }
    if ((s = connect_by_port(argv[1], 5678)) == -1)
	exit(1);
    sprintf(txt, "%s", argv[2]);
    net_write(s, txt, LEN);

    FD_ZERO(&ifds);
    FD_SET(0, &ifds);
    FD_SET(s, &ifds);

    for (;;) {
	testds = ifds;
	n = select(s + 1, &testds, NULL, NULL, NULL);
	if (n < 1)
	    exit(1);
	if (FD_ISSET(0, &testds)) {
	    fgets(txt, LEN, stdin);
	    net_write(s, txt, LEN);
	}
	if (FD_ISSET(s, &testds)) {
	    z = net_read(s, txt, LEN);
	    if (z < 1) {
		printf("El Servidor ha terminado\n");
		exit(0);
	    }
	    printf("%s", txt);
	}
    }
}
Servidor del sistema de concurso

El principio del listado presenta las variables principales que se emplearán, a saber:

  • s: Socket de escucha de nuevas conexiones

  • maxfd: El socket de más alto valor para select(2)

  • concursante: Un array (que limita el numero de participantes) donde se registra el nombre de éstos

  • problema: Un texto conteniendo la pregunta matemática

  • respuesta: El valor de la respuesta correcta

  • buffer: Un buffer usado en las transferencias de red

  • ifds, testds: Similar al programa anterior

/*
 * concurso_servidor.c: parte 1
 */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include "tcpaux.h"

#define MAX_FD 100
#define LEN 256

static void crea_pregunta(void);
static void procesa_respuesta(int fd);
static void broadcast(char *msg);

int s, maxfd;
char *concursante[MAX_FD];
char problema[LEN];
char buffer[LEN];
int respuesta;
fd_set ifds, testds;
Loop principal del servidor de concursos

A continuación, la rutina main() del servidor del sistema de concursos. Obsérvese que iniciamos el programa creando un socket de escucha con net_listen_port; ese socket se asocia al conjunto de descriptores ifds en el que se "escucha".

Cuando select(2) retorna, analizamos todos los descriptores listos para ser "leídos" (con FD_ISSET) y si el descriptor coincide con el socket de escucha, significa que se trata de una nueva conexión, la cual se procesa como se explicó antes con nuestra rutina net_accept.

En la medida que sólo nos interesa el nuevo descriptor (f=cliente→fd), liberamos la memoria asociada a la estructura "cliente" con free(3). Este descriptor se añade a la lista de descriptores escuchados con FD_SET.

Leemos el nombre del concursante (de la red) y lo enviamos a todos los participantes mediante nuestra función broadcast(). Finalmente, le enviamos la pregunta actual al nuevo concursante.

Si el descriptor actual no coincide con el socket de escucha, entonces tiene que tratarse de una conexión ya establecida en la que está llegando infomación. Esto se procesa en nuestra función procesa_respuesta().

/*
 * concurso_servidor.c: parte 2
 */

int main(int argc, char **argv)
{
    int n, z;
    CONEXION *cliente;

    s = net_listen_port(5678);
    maxfd = s;
    FD_ZERO(&ifds);
    FD_SET(s, &ifds);

    crea_pregunta();

    for (;;) {
	testds = ifds;
	n = select(maxfd + 1, &testds, NULL, NULL, NULL);
	if (n == -1)
	    continue;
	for (z = 0; z <= maxfd; z++)
	    if (FD_ISSET(z, &testds)) {
		if (z == s) {
		    int f;

		    cliente = net_accept(s);
		    f = cliente->fd;
		    FD_SET(f, &ifds);
		    if (f > maxfd)
			maxfd = f;
		    free(cliente);
		    net_read(f, buffer, LEN);
		    concursante[f] = strdup(buffer);
		    sprintf(buffer,
			    "Nuevo concursante (%d): %s\n",
			    f, concursante[f]);
		    broadcast(buffer);
		    net_write(f, problema, LEN);
		} else
		    procesa_respuesta(z);
	    }
    }
}
Rutinas auxiliares del servidor

La rutina crea_pregunta es trivial y no requiere mayor comentario.

La rutina procesa_respuesta recibe el descriptor "listo a ser leído" como argumento, y lee. Si el valor retornado por net_read() es cero, entonces se trata de una desconexión del cliente. Se elimina el descriptor del conjunto de escucha ifds y se cierra con close(2). Finalmente se envía un mensaje a todos los participantes que quedan avisando de tal desconexión.

En caso contrario, asumimos que se trata de un intento de resolución del problema. Por seguridad ponemos el byte 10 de la respuesta a cero (para evitar posibles desbordes) y obtenemos el valor numérico con atoi(3); si este valor es correcto, se genera un nuevo problema. En cualquier caso, se genera un mensaje para todos los participantes explicando las incidencias.

Por último, la función broadcast() envía el texto especificado a todos los descriptores del conjunto (exceptuando al socket de escucha de nuevas conexiones.)

/*
 * concurso_servidor.c: parte 3
 */
static void crea_pregunta(void)
{
    int a = rand() % 50;
    int b = rand() % 50;

    sprintf(problema, "%d+%d\n", a, b);
    respuesta = a + b;
    printf("Nuevo Problema:%s", problema);
}

static void procesa_respuesta(int fd)
{
    char ans[LEN];
    int z;

    z = net_read(fd, ans, LEN);
    if (z == 0) {
	sprintf(buffer,
		"Se desconecta concursante %s (%d)\n",
		concursante[fd], fd);
	free(concursante[fd]);
	FD_CLR(fd, &ifds);
	close(fd);
	broadcast(buffer);
	return;
    }
    ans[10] = '\0';
    if (atoi(ans) == respuesta) {
	crea_pregunta();
	sprintf(buffer, "Felicitaciones para %s!\n"
		"Su brillante respuesta fue %d\n%s",
		concursante[fd], atoi(ans), problema);
    } else
	sprintf(buffer, "Palos para %s!\n"
		"Su torpe respuesta fue %d\n%s",
		concursante[fd], atoi(ans), problema);
    broadcast(buffer);
}

static void broadcast(char *msg)
{
    int z;

    for (z = 0; z <= maxfd; z++)
	if (FD_ISSET(z, &ifds) && z != s)
	    net_write(z, msg, LEN);
}

17.6. Ejercicios

1 Extensiones al sistema de monitoreo

Los clientes actualmente envían un texto que comprende los tres valores numéricos de la carga del sistema. Intente mejorar esto, incluyendo otra información relevante. Por ejemplo:

  • Porcentajes de utilización de los filesystems (comando df)

  • Memoria libre/paginación (comandos free, vmstat)

  • Uso de Swap (comando swapon -s)

  • Porcentaje de utilización de CPU (comando vmstat)

2 Redundancia en el sistema de monitoreo

Implemente las recomendaciones indicadas en la sección de "Problemas Potenciales". La primera es muy sencilla. En la segunda Ud. deberá mantener dos o más sockets abiertos (uno por cada servidor), e imprimir la información enviada por sólo uno de ellos (se supone que todos comparten la misma información.)

3 Mejoras al sistema de concursos

A) Los problemas presentados se reducen a una suma de dos números de cero a 49. Esto se puede hacer más interesante con muy poco esfuerzo: más operaciones aritméticas, rangos configurables, etc.

B) Es posible que dos concursantes ingresen con un mismo nombre. Evítelo.

C) Cuando un concursante está escribiendo su respuesta, en medio de su texto puede surgir cualquier mensaje proveniente del servidor. Esto resulta confuso para el usuario.

Una forma de resolverlo consiste en usar Curses/Ncurses para que la pantalla tenga tres áreas independientes:

  • Planteamiento del problema

  • Mensajes variados

  • Edición de la respuesta del participante

Implemente esta característica.

4 Chat

A) El sistema de concursos puede ser muy fácilmente adaptado para obtener un sistema de conversación en el que todos vean lo que escriben todos los concursantes.

B) Cuando alguien se conecta, es conveniente que obtenga la lista de todos los usuarios conectados.

C) Adapte el cliente de chat para hacer uso de Curses/Ncurses (vea el capítulo 11.)

ANALISIS DE TEXTOS Y LENGUAJES

18. Procesamiento de Expresiones Regulares

Las Expresiones Regulares constituyen un poderoso lenguaje de patrones empleado en diversas herramientas y lenguajes de programación para procesar textos.

18.1. Introducción a las Expresiones Regulares

El lenguaje de Expresiones Regulares se compone de un conjunto de caracteres especiales que son empleados para encontrar patrones de diversa clase en cadenas de textos o archivos de texto. Por ejemplo, si disponemos del siguiente texto:

"Erase una vez, el hombre. Nadie sabe exactamente de
donde vino, y menos hacia donde va"

Entonces con las expresiones regulares podemos averiguar cosas como:

  • ¿El texto contiene la palabra 'donde'?

  • ¿Qué letras siguen a la palabra 'donde'?

  • ¿Qué líneas del texto empiezan con la palabra 'donde'?

  • ¿Existen dígitos numéricos en las líneas?

Para esto se emplea lo que se conoce como un "patrón de búsqueda", que no es otra cosa que una cadena de texto en la que ciertos caracteres tienen significado especial. Se puede considerar que estos caracteres especiales constituyen un "lenguaje de búsquedas", a saber, el lenguaje de las expresiones regulares.

Aunque las expresiones regulares están estandarizadas (por ejemplo, en POSIX), esto no evita que existan diversas variantes. Históricamente han existido dos sintaxis principales de expresiones regulares, a saber, las "expresiones regulares básicas" y las "expresiones regulares extendidas", las cuales presentan diferencias en sus características más avanzadas. De otro lado, algunos lenguajes de programación (notablemente Perl) han implementado sus propias extensiones a las expresiones regulares al punto de considerase como variantes independientes.

En este texto no pretendemos abordar el tema del lenguaje de expresiones regulares como tal, para lo cual hay ya abundante información impresa y online. Nuestro propósito corresponderá a la elaboración de programas que hacen uso de aquéllas.

En lo que sigue presentaremos la interfaz de programación POSIX, y posteriormente elaboraremos una pequeña librería que simplifica el uso de esta interfaz.

18.1.1. Referencia del lenguaje de expresiones regulares

A continuación un resumen de los principales constituyentes de las expresiones regulares extendidas estandarizadas en POSIX:

Table 9. Algunos constituyentes de expresiones regulares
Patrón Significado

c

Exactamente el mismo caracter

.

Exactamente un caracter cualquiera

^

El principio de una línea

$

El final de una línea

[c…​]

Exactamente un caracter de entre los especificados

[^c…​]

Exactamente un caracter distinto de los especificados

[a-b]

Exactamente un caracter en el rango [a…​b] (ambos inclusive)

*

Cero o más repeticiones de la expresión anterior

+

Una o más repeticiones de la expresión anterior

?

Cero o una repetición de la expresión anterior

{n}

Exactamente 'n' repeticiones de la expresión anterior

{n,}

'n' o más repeticiones de la expresión anterior

{n,m}

Entre 'n' y 'm' repeticiones (ambas inclusive) de la e. a.

x|y

La expresión 'x', o la expresión 'y'

(…​)

Una subexpresión que se puede extraer en forma independiente

\\

El siguiente caracter tiene signficado literal

18.2. Interfaz de Programación

La interfaz de programación POSIX para expresiones regulares se compone de las funciones: regcomp(), regexec(), regerror(), y regfree(), las cuales se declaran mediante los headers sys/types.h y regex.h.

18.2.1. Compilación de una Expresión Regular

En la interfaz POSIX, antes de poder hacer búsquedas de textos es menester "compilar" las expresiones regulares en una representación interna. Esta representación interna se almacena en una estructura de tipo “regex_t”, cuya dirección proporcionaremos a regcomp() como primer argumento.

El segundo argumento corresponde a nuestra expresión regular (un simple const char *) y el último argumento son modificadores opcionales para la expresión entre los que tenemos:

Table 10. Modificadores de opciones para regcomp()
Modificador Significado

REG_EXTENDED

Utilizar la sintaxis de e.r. extendidas. En caso contrario es la sintaxis básica

REG_ICASE

La expresión regular no diferencia mayúsculas de minúsculas

REG_NOSUB

Desactivar soporte para averiguación de "subexpresiones"

REG_NEWLINE

El salto de línea no se considera un caracter cualquiera

En caso de exito, regcomp() retorna cero. Por ejemplo:

regex_t preg;
int rc=regcomp(&preg,"en (19[0-9][0-9])",REG_ICASE|REG_EXTENDED);
if(rc!=0)
	printf("Error en regcomp()\n");

Este ejemplo intenta compilar una expresión regular que permite buscar el patrón “en 19XY” donde “XY” son dígitos numéricos. Puesto que el “19XY” está entre paréntesis, de hallarse en el texto será posible extraer este valor (subcadena) de manera independiente como veremos; de lo contrario sólo sabríamos si el patrón está o no en el texto.

18.2.2. Búsqueda de patrones en un texto

Disponiendo de una expresión regular compilada, la rutina regexec() permite averiguar si el patrón correspondiente se encuentra (o no) en cualquier texto que se le proporcione. Para el ejemplo anterior, continuaríamos con:

regmatch_t pmatch[15];
int re=regexec(&preg, cualquier_texto, 15, pmatch, 0);
if(re!=0)
	printf("No se encuentra el patron\n");

Si el patrón no se encuentra en el texto, entonces regexec() retorna un valor distinto de cero. En caso de éxito, las subexpresiones especificadas entre paréntesis (si las hubiera) podrán ser recogidas gracias al array pmatch [189] . Puesto que nuestro array fue dimensionado a 15 elementos, esto significa que regexec() sólo tendrá capacidad para encontrar la posición de hasta 15-1 subexpresiones (el primer elemento se reserva para la expresión original total.) Nótese que es necesario informar a regexec() acerca del tamaño del array mediante el tercer argumento. El cuarto argumento contiene precísamente el array en cuestión, y el quinto es una posible combinación de modificadores de opciones cuyo uso es muy poco frecuente por lo que no las explicaremos aquí.

El array pmatch contiene estructuras de tipo regmatch_t, las cuales tienen por definición:

typedef struct
	{
	regoff_t rm_so;
	regoff_t rm_eo;
	} regmatch_t;

Los miembros rm_so y rm_eo contienen, respectivamente, la posición inicial y final de cada subexpresión en el texto original, exceptuando el primer elemento del array (pmatch[0]) que corresponde a las posiciones de la expresión regular total. Por ejemplo, si tenemos el texto:

const char *cualquier_texto="Fue en 1974 cuando ocurrio";

Entonces la invocación a regexec() de más arriba retornará la estructura (pmatch[1]) con datos significativos:

pmatch[1].rm_so  -> 7
pmatch[1].rm_eo  -> 10

que corresponden a las posiciones del '1' y del '4' en el texto, contando desde cero (recordar que el patrón de búsqueda fue “en (19[0-9][0-9])”.)

El resto de estructuras (pmatch[2…​]) no proporcionan información relevante, lo que es indicado con el valor -1 en el miembro rm_so.

18.2.3. Eliminación de la Expresión Regular Compilada

Cuando ya no se requiere la expresión regular compilada (porque no se invocará más a regexec()), es necesario eliminar la memoria asociada a aquélla. Para esto se emplea la función regfree(), la cual recibe la dirección de memoria donde está almacenada la expresión regular compilada. Para nuestro ejemplo:

regfree(&preg);

18.3. Creación de una Librería de Expresiones Regulares

Como habrá podido notar, la interfaz de programación es potente pero relativamente tediosa de utilizar, especialmente si estamos interesados en hallar subexpresiones regulares. La librería que se desarrolla a continuación es un intento por simplificar la interfaz sin perder lo principal de la potencia de las funciones originales.

18.3.1. Ejemplo de uso de la Librería

A continuación un programa que hace uso de la librería que se desarrollará. Como se aprecia, ya no tendremos que preocuparnos por las etapas de "compilación-ejecución-liberación", y las subexpresiones estarán disponibles automáticamente en un arreglo de cadenas de texto llamado par_text[]:

#include <stdio.h>
#include "par_lib.h"

int main()
{
    const char *texto = "Como la carga electrica no esta distribuida "
	"uniformemente en el volumen nuclear, sino que esta concentrada "
	"en los protones, debemos escribir Z(Z-1) en vez de Z^2 porque "
	"cada uno de los Z protones interactua solamente con los restantes "
	"Z-1.";

    par_start(texto);
    if (par_test_pattern("Z"))
	printf("El texto contiene 'Z'\n");
    if (par_read_pattern("uniforme")) {
	printf("El texto contiene 'uniforme'\n");
	if (par_test_pattern("[^ ]*"))
	    printf("Tras 'uniforme' sigue '%s'\n", par_text[0]);
    }
    if (par_read_pattern("([^ ]*),[^0-9]*([0-9][^0-9]*[0-9])")) {
	printf("Antes de una coma: '%s'\n", par_text[1]);
	printf("Entre dos digitos: '%s'\n", par_text[2]);
    }
    if (par_read_pattern("Z(..........)"))
	printf("Diez caracteres tras 'Z': '%s'\n", par_text[1]);
    if (par_test_pattern("a(.*)1"))
	printf("Entre 'a' y '1': '%s'\n", par_text[1]);

    par_clean();		/* no imprescindible */
    return 0;
}

El análisis se inicia (y se puede reiniciar en cualquier momento) con una llamada a par_start() con el texto en el que se pretende hacer la búsqueda. Si deseamos saber si un patrón está contenido en el texto, basta con invocar a par_text_pattern() con el patrón deseado; en caso positivo retorna 1 y cero si no lo encuentra.

En ciertos casos es conveniente hacer búsquedas "con avance", es decir, que tras hallar un patrón en el texto, la siguiente búsqueda se continue tras ese patrón hallado (a esto se le suele denominar 'parsing'.) Para esto se dispone de la rutina par_read_pattern(), y su efecto se aprecia claramente cuando par_test_pattern() retorna lo que sigue al patrón 'uniforme'.

En cualquier caso, el patrón hallado (para la expresión regular completa) se puede obtener en la cadena par_text[0], y si se solicitó subexpresiones (expresiones regulares entre paréntesis), éstas se pueden obtener en las cadenas par_text[1], par_text[2], etc. La variable entera par_size (no utilizada en el ejemplo) indica cuántas subcadenas fueron halladas.

Los valores de par_text[] y par_size son liberados y reasignados automáticamente con las sucesivas llamadas a la librería, por lo que se deberán copiar a otras variables si se pretenden conservar.

Cuando se ha terminado de utilizar la librería, es posible (pero no obligatorio) liberar la memoria dinámica asignada mediante la función par_clean().

18.3.2. Archivo cabecera de la librería

No presenta mayor novedad:

#ifndef PAR_LIB_H
#define PAR_LIB_H

/* Subexpresiones halladas en la ultima busqueda. Son 
   sobreescritas con cada busqueda */
extern char **par_text;

/* Cantidad de subexpresiones halladas. Es sobreescrita
   con cada busqueda */
extern int par_size;

/* Inicializa la busqueda con el texto proporicionado */
int par_start(const char *text);

/* Busca el patron a partir de la posicion actual. Retorna 1 en
   caso de exito y cero ni no lo encuentra. La posicion actual
   no se altera */
int par_test_pattern(const char *pattern);

/* Igual que par_test_pattern pero desplaza la posicion actual
   justo despues del patron hallado */
int par_read_pattern(const char *pattern);

/* Libera la memoria asociada por la ultima busqueda. No es
   obligatoria su invocacion pero es mas elegante liberar
   cualquier memoria que ya no se usa */
void par_clean(void);

#endif

Este archivo deberá ser incluido por cualquier programa que hace uso de la librería.

18.3.3. Implementación de la librería

Aquí va:

#include "par_lib.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <regex.h>

#define NMATCH 20

char **par_text = NULL;
int par_size = 0;

static const char *ptr;
static int cflags = REG_EXTENDED;
static int eflags = 0;
static regmatch_t pmatch[NMATCH];

int par_start(const char *text)
{
    if (text == NULL)
	return 0;
    par_clean();
    ptr = text;
    return 1;
}

int par_test_pattern(const char *pattern)
{
    if (ptr == NULL || pattern == NULL || *pattern == 0)
	return 0;
    regex_t preg;
    int rc = regcomp(&preg, pattern, cflags);
    if (rc != 0)
	return 0;
    int re = regexec(&preg, ptr, NMATCH, pmatch, eflags);
    regfree(&preg);
    if (re != 0)
	return 0;
    par_clean();
    int z;
    for (z = 0; z < NMATCH; z++) {
	if (pmatch[z].rm_so != -1)
	    par_size++;
    }
    par_text = calloc(par_size, sizeof(char *));
    int j = 0;
    for (z = 0; z < NMATCH; z++) {
	int a = pmatch[z].rm_so;
	int b = pmatch[z].rm_eo - 1;
	if (pmatch[z].rm_so != -1) {
	    par_text[j] = calloc(b - a + 2, sizeof(char));
	    strncpy(par_text[j], ptr + pmatch[z].rm_so, b - a + 1);
	    j++;
	}
    }
    return 1;
}

int par_read_pattern(const char *pattern)
{
    if (!par_test_pattern(pattern))
	return 0;
    ptr += pmatch[0].rm_eo;
    return 1;
}

void par_clean(void)
{
    if (par_text == NULL)
	return;
    int z;
    for (z = 0; z < par_size; z++)
	if (par_text[z] != NULL)
	    free(par_text[z]);
    free(par_text);
    par_text = NULL;
    par_size = 0;
}

Algunos comentarios:

  • La constante NMATCH determina que como máximo se podrá indagar el valor de veinte-1=diecinueve subexpresiones

  • Hemos especificado la sintaxis de expresiones regulares extendidas

  • Cada subexpresión regular genera una asignación dinámica de memoria con calloc(), donde se copia el texto correspondiente. Estas áreas dinámicas son liberadas en cada invocación a par_clean()

  • La función par_read_pattern() emplea a par_test_pattern() y se limita a avanzar pmatch[0].rm_eo posiciones; esta estructura normalmente contiene las posiciones extremas de la expresión regular total hallada

18.4. Ejercicios

1 Extienda la librería de patrones para incluir funciones tales como par_test_word(), par_test_number(), y sus correspondientes par_read*(), las cuales, respectivamente, buscarán una palabra y un número entero.

2 Una potencial penalidad en la performance de la librería radica en que cada invocación necesariamente conlleva una compilación de la expresión regular pasada como argumento. Esto se puede paliar fácilmente para el caso (relativamente frecuente) en que repetidamente se solicite la misma expresión regular, guardando en una variable adicional el texto de la misma para compararlo en las sucesivas invocaciones. Implemente esta optimización.

19. Analisis Léxicográfico con Lex

Muchos programas requieren procesar textos con diversos propósitos. Una de las formas de realizar este procesamiento consiste en considerar dichos textos como compuestos por "palabras" o "tokens" (cuya definición exacta dependerá de la clase de procesamiento a efectuarse), y a continuación realizar la "extracción" de dichos tokens, desde el inicio hasta el final del texto. Este proceso se denomina "análisis lexicográfico" y el programa encargado del mismo se suele denominar "scanner" [190] .

La librería estándar de C proporciona una facilidad primitiva de extracción de tokens mediante la rutina strtok(); para casos más sofisticados será necesario escribir una rutina personalizada o mejor, recurrir a una herramienta especializada en este tipo de tarea como Lex o Flex, que es lo que veremos a continuación.

19.1. Introducción a Lex/Flex

Lex es una herramienta Unix cuya función es generar código fuente de analizadores lexicográficos ("scanners") de propósito general. En los ambientes Open Source (como Linux) está muy difundido el programa Flex que brinda la funcionalidad equivalente [191] . La especificación para el "scanner" se proporciona mediante un conjunto de expresiones regulares [192] desde un archivo de texto el cual tiene una sintaxis especial.

Los "scanners" generados por lex/flex procesan el texto proveniente del "file handler" global yyin, el cual por omisión apunta a stdin. Es posible alterar este comportamiento simplemente haciendo que yyin apunte hacia otro archivo:

yyin=fopen("otro_archivo","r");

Análogamente, la salida del "scanner" por omisión es enviada al "file handler" global yyout, que equivale a stdout a no se que se redirija explícitamente.

El uso de lex/flex consiste de los siguientes pasos: . Escribir un archivo de especificación del scanner . Generar el scanner mediante la invocación a lex/flex . Compilar el scanner, enlazando con la "librería de lex/flex"

Con el fin de ilustrar este procedimiento, implementaremos un sencillo programa que extrae palabras delimitadas por espacios a partir de una línea de texto leída desde la entrada estándar. A continuación listamos el archivo de especificación del scanner, el cual contiene también el código fuente necesario para su uso (función main().)

%%
\n        return 0;
[^ \n]+   printf("Palabra: '%s'\n",yytext);
.         ;
%%
int main()
{
yylex();
return 0;
}

Como puede intuir el lector, la rutina de análisis lexicográfico generada por lex se denomina ‘yylex()’, y en nuestro ejemplo ésta es invocada por la rutina main() incluida al final del archivo de especificación.

Con el fin de generar el scanner escribiremos:

$ lex -t palabras.l > palabras.c

Por último, la siguiente línea realiza la compilación y el enlace con la librería lex [193] :

$ cc -o palabras palabras.c -ll

Al probar el resultado obtenemos:

$ ./palabras
El pobre pollo enamorado
Palabra: 'El'
Palabra: 'pobre'
Palabra: 'pollo'
Palabra: 'enamorado'

19.2. Archivo de Especificación de Lex

A continuación explicaremos la sintaxis del archivo de especificación de lex. En principio, este archivo está compuesto de tres secciones:

definiciones
%%
reglas
%%
codigo de usuario

19.2.1. Sección de Definiciones

La sección de definiciones puede contener "definiciones de expresion" y código fuente global. Las "definiciones de expresion" se emplean a manera de abreviación para simplificar las reglas de la segunda sección; por ejemplo:

DIGITO        [0-9]

Esta definición permite posteriormente referirnos a la expresión regular “[0-9]” mediante el identificador "DIGITO" (utilizando la sintaxis “{DIGITO}”.)

El código fuente global se debe especificar entre dos líneas delimitadoras con la sintaxis:

%{
...
%}

Este código fuente es colocado sin alteración al principio del scanner generado, por lo que suele contener directivas de preprocesador, declaraciones, etc. Por ubicarse fuera de yylex() (y de cualquier otra función) su ámbito es global.

19.2.2. Sección de Reglas

Consiste de líneas que tienen el formato:

patron       accion

Los "patrones" son expresiones regulares que representan los "tokens", los cuales son buscados en el texto secuencialmente; la "acción" es código fuente en lenguaje C que se ejecutará cuando el respectivo patrón en hallado [194] .

Expresiones Regulares de Lex

Las expresiones regulares soportadas por lex se basan en las tradicionales "expresiones regulares extendidas", pero cuentan con algunos añadidos y modificaciones entre las que tenemos:

1 Para buscar literalmente el texto '[abc]' (sin las comillas simples) se puede emplear '"[abc]"'. Naturalmente, de no utilizarse las comillas dobles el patrón correspondería a una expresión regular que encuentra un único caracter en el conjunto {a,b,c}

2 Los patrones \\a, \\b, \\f, \\n, \\r, \\t, y \\v tienen la misma interpretación que las constantes de caracteres del lenguaje C. En nuestro ejemplo por eso utilizamos \\n para hacer referencia a un caracter de fin-de-línea

3 El punto (.) corresponde a cualquier caracter, excepto fin-de-línea

4 Los patrones de la forma \\123 y \\x6c corresponden respectivamente, a los códigos octal y hexadecimal de un caracter

5 El patrón de la forma patron1/patron2 encuentra la concatenación de ambos patrones pero el segundo no se considera como parte del "avance" en el texto

6 Las expresiones de la forma {identificador} corresponden a la respectiva definición dada en la primera sección.

El analizador lexicográfico generado analiza su entrada a la búsqueda de textos que coincidan con las expresiones regulares de las reglas. Cuando hay varias reglas que coinciden, se selecciona aquella que representa la cadena de texto más larga. Si aún hay varias reglas satisfactorias, se selecciona aquella que está más al inicio en la especificación. Es decir que para un mismo texto puede existir un conjunto de reglas satisfactorias de las cuales solo una es "la mejor".

Una vez que se ha hallado el patrón, el texto correspondiene (el token) es accesible mediante la cadena de caracteres (global) yytext, y su longitud mediante el entero yyleng. A continuación se ejecuta la acción asociada a esta regla y el proceso continúa con el resto del texto de entrada.

Cuando el texto no encaja con ningún patrón, el primer caracter de la posición actual es copiado a la salida (yyout) y el proceso continúa a partir del siguiente.

Acciones

Las acciones pueden extenderse a diversas líneas mediante bloques de código encerrados entre llaves. Asimismo, si la regla no especificó ninguna acción, entonces el texto coincidente será simplemente descartado.

Existen algunas abreviaciones y facilidades disponibles para las acciones. Por ejemplo:

  1. Una barra vertical (|) significa "duplicar la acción especificada en la siguiente regla"

  2. La macro ECHO instruye a lex a copiar el token hacia la salida estándar

  3. La macro REJECT solicita a lex ejecutar la acción correspondiente a la "siguiente mejor" de las reglas.

  4. Una invocación a yymore() solicita que el siguiente token hallado sea agregado al valor actual de yytext

  5. La invocación a yyless(int n) retorna todos excepto los primeros 'n' caracters de yytext al flujo de entrada

  6. La función unputc(c) coloca el caracter 'c' en el flujo de entrada, el cual será el siguiente caracter procesado

  7. La función input() extrae el siguiente caracter del flujo de entrada, siendo opuesta a unputc()

Código Fuente Local

En la sección de reglas también es posible agregar código fuente si éste se encierra entre dos líneas de la forma conocida %{ y %}. Este código fuente es local a la rutina yylex(), por lo que se puede emplear para declarar variables locales a la misma.

19.2.3. Sección de "Código de Usuario"

Esta sección es opcional; todo el texto de esta sección se agrega sin modificaciones al código fuente generado. Se emplea para agregar rutinas auxiliares, y posiblemente main() como en nuestro anterior ejemplo.

19.3. Uso del Analizador Generado

Por omisión, el código fuente generado consiste de la función yylex() con la siguiente definición:

int yylex(void)
{
...
}

Los programas más sencillos utilizan a yylex() a fin de transformar uno a uno los tokens provenientes de la entrada estándar de diversas maneras (de acuerdo a las acciones especificadas) y el resultado se vuelca de inmediato en la salida estándar; a esta clase de programa se le suele denominar "filtro". Para esto normalmente main() invocará una única vez a yylex() y el programa terminará cuando yylex() retorne por primera y última vez. Esto es precísamente lo que hizo nuestro ejemplo “palabras.l”.

En otra clase de programas (por lo general, más sofisticados) el análisis lexicográfico es tan solo la primera etapa de un proceso más grande [195] . En esos casos yylex() se invoca repetidamente a fin de que capture y retorne los tokens de la entrada a la rutina del proceso principal (sin enviar nada a yyout.) Sólo al concluir la extracción de tokens, el proceso principal continúa.

19.3.1. Ejemplo: Iniciales de palabras

El siguiente ejemplo también está contenido totalmente en un único archivo. Su función consiste en leer las palabras (tokens) provenientes de la entrada estándar o un archivo especificado en la línea de comandos, y retorna sucesivamente la primera letra de cada una de estas palabras (en mayúsculas.) Con esta información (que viene a ser el "código del token"), la rutina main() contabiliza la cantidad de palabras asociadas para cada letra inicial y al final imprime un reporte. Nótese que el final del archivo se detecta cuando yylex() retorna cero, lo cual es el comportamiento normal del scanner. Asimismo, hemos empleado una variable local "c" para facilitar la escritura de una de las acciones. Por último, en la sección de definiciones se incluyó el archivo cabecera ctype.h (para toupper()), una constante y la variable global de contadores.

Otro aspecto interesante radica en la expresión regular usada en la segunda regla, la cual captura "cualquier caracter excepto fin-de-línea" (el punto), o al "fin-de-línea" explícitamente indicado.

%{
#include <ctype.h>
#define LETRAS ('Z'-'A'+1)

int contadores[LETRAS];
%}
%%
%{
char c;
%}
[A-Za-z]+      { c=yytext[0]; return toupper(c); }
.|\n           ;
%%
int main(int argc,char **argv)
{
int indice,v,z;
if(argc>1)
	{
	if((yyin=fopen(argv[1],"r"))==NULL)
		{
		fprintf(stderr,"Error abriendo archivo %s\n",argv[1]);
		return 1;
		}
	}
for(;;)
	{
	v=yylex();
	if(v==0)
		break;
	indice=v-'A'; /* Indice va de cero a LETRAS-1 */
	contadores[indice]++;
	}
for(z=0;z<LETRAS;z++)
	if(contadores[z])
		printf("%c -> %d\n",'A'+z,contadores[z]);
return 0;
}

19.4. Ejemplo: Sumadora Básica

Terminamos el capítulo con un ejemplo una pizca más sofisticado, una sumadora simple. A continuación un ejemplo de uso:

$ ./sumadora
jjskfurjvervj
Error! Use numero+numero+... o 'fin' para terminar
13+53 + 12
78
fin
$

A continuación el listado respectivo (nuevamente totalmente contenido en un sólo archivo.)

%{
#define NUMERO 1
#define TOTAL  2
#define ERROR  3

%}
NUM    [0-9]+
%%
{NUM}        return NUMERO;
[ ]*"+"[ ]*$ return ERROR;
[ ]*"+"[ ]*  ;
^\n          ;
\n           return TOTAL;
fin\n        return 0;
.            return ERROR;
%%

int main()
{
int v,c;
double suma=0.0;
for(;;)
	{
	switch(yylex())
		{
		case 0:
		return 0;

		case ERROR:
		printf("Error! Use numero+...+numero "
			"o 'fin' para terminar\n");
		for(;;)
			{
			c=input();
			if(c==EOF)
				return 0;
			if(c=='\n')
				break;
			}
		suma=0.0;
		break;

		case NUMERO:
		suma=suma+atof(yytext);
		break;

		case TOTAL:
		printf("%g\n",suma);
		suma=0.0;
		break;
		}
	}
return 0;
}

19.5. Ejercicios

1 Actualmente el ejemplo de la sumadora sólo procesa números enteros, no obstante que la variable “suma” es real. Adapte el ejemplo para que procese números reales.

2 Si se pretende hacer el análisis lexicográfico de un texto en memoria, la forma más portable corresponde a guardarlo primero en un archivo temporal, y "scanear" este último (redirigiendo yyin.) Implemente una subrutina que permita llevara a cabo esta operación.

20. Analizadores sintácticos: Yacc / Bison

20.1. Introducción

Diversas aplicaciones tienen la necesidad de analizar textos que cumplen con cierta sintaxis. El ejemplo más conocido posiblemente corresponde a los compiladores, los cuales reciben los textos de código fuente, y deben validar el cumplimiento de la sintaxis del lenguaje de programación en uso. Otras aplicaciones sofisticadas también permiten al usuario introducir "programas" en alguna clase de "lenguaje"; algunos ejemplos comunes son las macros de las hojas de cálculo comerciales, los "store procedures" de las DBMS, los scripts del shell Unix/Linux, etc.

Todas estas aplicaciones tienen en común la necesidad de validar el cumplimiento de ciertas reglas de sintaxis (o gramática) de algún lenguaje. Asimismo, requieren efectuar diversas acciones en función de las construcciones sintácticas que se encuentran en el texto.

Esta clase de análisis suele denominarse "parsing", y es un trabajo que puede tornarse muy engorroso cuando la gramática del mencionado lenguaje es compleja. A fin de automatizar (o evitar) la escritura de código de "parsing", fue creada la herramienta yacc [196] .

Yacc recibe una "especificación de la "gramática" [197] del lenguaje que queremos definir, y produce como resultado una subrutina (en lenguaje C) que implementa el "parser" correspondiente.

En el mundo Open Source se dispone de byacc (un clone de yacc de dominio público) así como de bison, que posee las mismas características de yacc y otras más. En este texto sólo haremos uso de las características estándar de yacc, las que pueden ser usadas en cualquiera de las versiones Open Source.

20.2. Gramática en Yacc

A fin de especificar la gramática de nuestro lenguaje, se debe especificar un conjunto de "agrupamientos sintácticos" (cualquier expresión significativa en el lenguaje) junto con sus correspondientes "reglas" para construirlos desde sus partes.

Quizá la única forma de comprender esto es mediante ejemplos. Imaginemos una "sumadora" y consideremos la sintaxis de lo que introducimos en ella. En principio, las expresiones aceptabes son algo similar a:

12+54+124+77+44+665+2+44

Si a todo este texto le denominamos "expr" (por "expresión matemática"), podríamos plantear la siguiente regla recursiva:

expr = expr '+' expr

Es decir, una expresión matemática se puede formar mediante dos expresiones matemáticas separadas por un símbolo de adición (+); para el ejemplo anterior se podrían plantear las siguientes expresiones matemáticas:

12+54+124
y
77+44+665+2+44

Las cuales, unidas por un símbolo de adición dan lugar a la expresión matemática original.

Sin embargo, esto no funciona indefinidamente, pues si consideramos simplemente:

12+54

Entonces tendremos que ambos miembros ya no son las mencionadas "expresiones matemáticas", sino números simples. Debido a esto, es necesario agregar la siguiente regla:

expr = NUMERO

Donde "NUMERO" corresponde al conjunto de dígitos de cero a nueve que todos conocemos (volveremos a esto más adelante.) Con esto, las reglas de sintaxis (la gramática) del lenguaje de nuestra sumadora serán:

expr = NUMERO
expr = expr + expr

Como indicamos, las reglas pueden ser recursivas; sin embargo, deben haber algunas que terminen la recursión debido a que hacen referencia a elementos que ya no se pueden definir recursivamente (como "NUMERO" en nuestro ejemplo.)

A los componentes mínimos de las reglas (los que no se pueden construir a partir de otros) les denominaremos "token’s" (también se les llama símbolos terminales), mientras que a los que sí se se construyen a partir de otros, les llamaremos "agrupamientos" (o símbolos no terminales.)

20.2.1. Especificación de la Gramática

El conjunto de reglas de sintaxis (gramática) debe ser proporcionada a Yacc en un archivo de texto (convencionalmente con extensión .y). Cada "agrupamiento" debe tener un nombre (similar a un identificador de lenguaje C), convencionalmente escrito en minúsculas. Para el ejemplo de la sumadora, utilizamos “expr” para el agrupamiento que representa a las "expresiones matemáticas".

Por otro lado, cada "token" también debe tener una identificación, la cual convencionalmente se escribe en mayúsculas. En nuestro ejemplo tenemos el token "NUMERO", y el símbolo de adición, al cual no hemos asignado ningún nombre. Usualmente, los tokens de un único caracter constante suelen ser especificados con su mismo caracter entrecomillado (por ejemplo, con '+'). Así, nuestra gramática queda reducida a:

expr =	NUMERO
	|
	expr '+' expr
	;

El separador '|' permite especificar diversas reglas para encontrar la el mismo tipo de agrupamiento. Los "puntos y coma" (;) sólo son separadores de las reglas.

20.2.2. Valor semántico

Lo anterior basta para reconocer la entrada de números y el signo '+' (validación de la sintaxis.) Sin embargo, esto todavía no produce nada como resultado.

A fin de que nuestro programa realmente realice una suma cuando se encuentra ante la segunda expresión, requerimos especificar una "acción" como lo siguiente:

expr =	NUMERO
	|
	expr '+' expr { $$ = $1 + $3 ; }
		;

Estas acciones se escriben en lenguaje C; dentro de éstas se puede emplear las "macros Yacc" para hacer referencia a los constituyentes de la regla. Así, la macro $$ simboliza la expresión del lado izquierdo (el total) mientras que $1, $2, $3,…​ corresponden a los elementos constituyentes.

Nótese que en la primera regla correspondía escribir:

expr =	NUMERO { $$ = $1 }

Sin embargo, este ya es el comportamiento pre-establecido cuando no se especifica ninguna acción, por lo que ya no escribimos nada.

20.2.3. Tipos de datos de lenguaje C

Los valores representados por los "agrupamientos" así como los tokens deben corresponder a variables de lenguaje C (del tipo conveniente.) En nuestro caso es bastante sencillo, pues las expresiones matemáticas "expr" y los números constituyentes (NUMERO) corresponderán sencillamente a números enteros (tipo int de lenguaje C.)

Este hecho se debe declarar usando la macro YYSTYPE al inicio del archivo de gramática del siguiente modo:

%{
#define YYSTYPE int
%}

Los identificadores ‘%{’ y ‘%}’ son separadores de Yacc.

Asimismo, todos los tokens deben ser declarados a continuación, mediante la directiva ‘%token’. Para nuestro ejemplo:

%{
#define YYSTYPE int
%}
%token NUMERO
%%
 ... reglas de la gramatica ...

Los tokens de un solo caracter (como '+') no se deben declarar aquí. Nótese el separador "%%" que marca el fin de las declaraciones y da inicio a las reglas de la gramática.

20.2.4. Analisis Lexicográfico

Recapitulando, hasta aquí tenemos una gramática que permitirá a nuestra calculadora encontrar el resultado a partir de expresiones numéricas con símbolos de suma (‘+’.) Almacenaremos nuestra gramática en el archivo "sumadora.y" (pues .y es la extensión convencional para yacc.)

Yacc generará (a partir de esa gramática) el código fuente de una subrutina llamada yyparse() en un archivo (normalmente) llamado y.tab.c, mientras que bison lo almacenará en un archivo de nombre sumadora.tab.c.

$ yacc -b sumadora sumadora.y
yacc: 1 shift/reduce conflict
$ ls
sumadora.tab.c  sumadora.y

Nótese que apareció un mensaje referente a cierto "conflicto". Esto se corregirá después.

Volviendo a la gramática, los únicos tokens de nuestra calculadora son el "NUMERO" y el '+'. Estos tokens conforman el texto que el usuario ingresará (por ejemplo, desde el teclado.) Sin embargo, yacc NO se encarga de obtener los tokens, sino que nosotros debemos proporcionarselos.

La extracción de los tokens de un texto arbitrario se conoce como "análisis lexicográfico" y como indicamos, no es función de yacc. Por el contrario, yacc asume que (de alguna manera) estará disponible una función auxiliar llamada “yylex()” que le proporcione los tokens uno a uno. Nuestra responsabilidad es por lo tanto implementar la función “yylex()”, para lo cual hay normalmente dos caminos:

  1. Escribirla nosotros mismos

  2. Utilizar el utilitario Lex (que se vió en el capítulo 19.)

Para nuestro ejemplo, la función yylex() deberá ser capaz de leer la entrada estándar (el teclado) y retornar:

  • La constante NUMERO cuando encuentra un número entero

  • La constante '+' cuando encuentra este símbolo

  • Cero, cuando ya no hay nada que procesar

Asimismo, cuando encuentra un número entero yylex() deberá almacenar su valor en la variable global (de yacc) llamada yyval, justo antes de retornar la constante NUMERO. En los otros casos el valor de yyval es irrelevante.

La variable yyval tendrá el tipo declarado arriba con ‘YYSTYPE’.

20.2.5. Código fuente en el archivo de gramática

Como hemos ido adelantando, los archivos de gramática tienen la siguiente estructura:

%{
Directivas de preprocesamiento C, declaraciones, etc.
%}
Declaraciones de Yacc (como %token)
%%
Reglas de gramática
%%
Código en lenguaje C opcional

En nuestro caso, incluiremos el programa completo (incluso la función main()) en un único archivo sumadora.y. Para esto aprovecharemos la última sección en la que se puede introducir cualquier código C adicional.

20.2.6. Ejemplos Básicos

Sumadora de una Linea

El siguiente archivo de gramática contiene el programa completo para una sumadora que procesa una única línea de texto:

%{
#define YYSTYPE int
%}
%token NUMERO
%%
line:	expr '\n' { printf("%d\n",$1); exit(0); }
	;
expr:	NUMERO
	|
	expr '+' expr { $$ = $1 + $3 ; }
	;
%%
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

int yylex(void)
{
int c;

c=getchar();
if(isdigit(c))
	{
	ungetc(c,stdin);
	scanf("%d",&yylval);
	return NUMERO;
	}
return c;
}

int yyerror(const char *s)
{
fprintf(stderr,"sumadora: %s\n", s);
return 0;
}

int main(int argc,char *argv[])
{
yyparse();
return 0;
}

Parece mucho trabajo para una sumadora…​ y todavía falta más!. Sin embargo, casi todo será repetitivo en adelante, y cuando terminemos tendremos una calculadora muy fácilmente reprogramable. No se desanime…​

Invocación a Yacc

Para generar el ejecutable lo más recomendable es aprovechar las reglas implícitas de make, las cuales invocarán a yacc así como al compilador en forma automática. Por ejemplo, el siguiente Makefile permite fácilmente hacer el trabajo [198] :

sumadora1: sumadora1.o
	cc -o $@ $<

El resultado es interesante:

$ make
yacc  sumadora1.y
conflicts: 1 shift/reduce
mv -f y.tab.c sumadora1.c
cc    -c -o sumadora1.o sumadora1.c
cc -o sumadora1 sumadora1.o
rm sumadora1.c

Obsérvese que aparece un mensaje que reza “conflicts: 1 shift/reduce”. Esta es una alerta que corregiremos más adelante.

Explicación complementaria

Hemos añadido la función main() que simplemente llama a yyparse(); también hemos añadido una función auxiliar invocada por yyparse() en caso de errores, llamada yyerror(). Finalmente, hemos añadido la ya mencionada función yylex() que recibe la expresión desde la entrada estándar, retornando un número entero cada vez que detecta un dígito numérico; en caso contrario, retorna el caracter recibido.

Asociado a esto último está el agrupamiento "line" de la gramática. Este agrupamiento se utiliza para imprimir una "expresión completa". El problema es ¿cuándo se considera que hemos procesado sufientes tokens como para imprimir el resultado? Aquí se ha definido arbitrariamente que cuando una expresión sea continuada de un fin de línea ('\n') entonces se considera que la expresión está lista para ser impresa y el programa termina.

Es importante saber que yacc considera al primer agrupamiento de la gramática (en nuestro caso, "line") como "la expresión completa" (a veces llamada "símbolo inicial".)

Sumadora Multilínea

La sumadora que presentamos a continuación permite al usuario obtener muchas sumas presionando la tecla [Enter] (a diferencia del progrma anterior que terminaba con el primer resultado.)

En esta versión, cuando el usuario desee realmente terminar, deberá presionará la combinación [Ctrl]+[D] (fin de archivo.) Todo esto lo podemos conseguir con unos pequeños cambios en la gramática:

%{
#define YYSTYPE int
%}
%token NUMERO
%left '+'
%%
todo:	line
	|
	todo line
	;
line:	expr '\n' { printf("%d\n>>> ",$1); }
	;
expr:	NUMERO
	|
	expr '+' expr { $$ = $1 + $3 ; }
	;
%%
#include <stdio.h>
#include <ctype.h>

int yylex(void)
{
int c;

c=getchar();
if(c==EOF) return 0;
if(isdigit(c))
	{
	ungetc(c,stdin);
	scanf("%d",&yylval);
	return NUMERO;
	}
return c;
}

int yyerror(const char *s)
{
fprintf(stderr,"sumadora: %s\n", s);
return 0;
}

int main(int argc,char *argv[])
{
printf(">>> ");
yyparse();
return 0;
}
Asociatividad

Todos sabemos que la suma cumple la ley asociativa, es decir, si tenemos "a+b+c" da lo mismo empezar sumando "a+b" y luego "c", que "b+c" y luego "a". En otras palabras, "(a+b)c=a(b+c)".

En la gramática de nuestras sumas, las reglas fácilemente permiten expresiones del tipo "a+b+c", y puesto que no hemos informado nada acerca de la asociatividad, yacc decidirá por sí mismo pero emitirá un mensaje de alerta tal como:

conflicts: 1 shift/reduce

Esto se puede evitar fácilmente indicando a yacc el orden de asociatividad para el operador de adición; ya sea a la izquierda: "(a+b)c", o a la derecha: "a(b+c)". En nuestro caso hemos indicado asociación a la izquierda con la declaración %left '+', aunque como sabemos también se pudo utilizar la derecha.

Entrada Completa

Hemos añadido dos reglas para un nuevo agrupamiento llamado "todo". Este agrupamiento representará a todo el texto que procesa yyparse(), el cual ahora consta de muchas líneas. El "todo" se declara recursivamente como una única línea, o la combinación de "todo" con una nueva línea.

De este modo conseguimos fácilmente que el "parser" entre a un ciclo sin fin, intentando obtener todos los tokens necesarios para completar el agrupamiento "todo".

20.3. Calculadora Multibases

A la gramática del último ejemplo se le puede añadir otras operaciones con relativa facilidad, con lo cual crearemos una calculadora totalmente funcional en esta sección.

Adicionalmente, incluiremos la posibilidad de especificar la base de numeración de los números que se ingresan. En el siguiente ejemplo, a -21 se le añade el cuadrado del número 101 de la base 2 (o sea, 5); es decir, -21+25:

$ ./operabases
-21+101[2]^2
4

El archivo completo (operabases.y) se muestra a continuación:

%{
#define YYSTYPE int
%}
%token ENTERO
%left '+' '-'
%left '*' '/'
%left NEGATIVO
%right '^'
%%
todo:	line
	|
	todo line
	;
line:	expr '\n' { printf("%d\n",$1); }
	;
expr:	numero
	|
	expr '+' expr { $$ = $1 + $3 ; }
	|
	expr '-' expr { $$ = $1 - $3 ; }
	|
	expr '*' expr { $$ = $1 * $3 ; }
	|
	expr '/' expr { $$ = $1 / $3 ; }
	|
	expr '^' expr {
			int z;
			$$ = $1;
			for(z=1;z< $3 ;z++)
				$$ *= $1;
			}
	|
	'(' expr ')' { $$ = $2 ; }
	|
	'-' expr %prec NEGATIVO { $$ = - $2 ; }
	;
numero: ENTERO
	|
	ENTERO '[' ENTERO ']' {
			int resto;
			int factor = 1;
			int numero = $1;
			$$ = 0;
			while(numero)
				{
				resto = numero % 10;
				numero -= resto;
				numero /= 10;
				$$ += resto * factor;
				factor *= $3;
				}
			}
	;
%%
#include <stdio.h>
#include <ctype.h>

int yylex(void)
{
int c;

c=getchar();
if(c==EOF) return 0;
if(isdigit(c))
	{
	ungetc(c,stdin);
	scanf("%d",&yylval);
	return ENTERO;
	}
return c;
}

int yyerror(const char *s)
{
fprintf (stderr,"operabases: %s\n", s);
return 0;
}

int main(int argc,char *argv[])
{
yyparse();
return 0;
}

Un "número" es ahora una expresión que corresponde ya sea a un entero "sin base" (base 10) o a un entero acompañado de una base (entre corchetes.) En ese sentido, el analizador lexicográfico ya no retorna "números", sino "enteros".

20.3.1. Precedencia de Operadores

Como sabemos, existe lo que se conoce como "precedencia de operadores" para decidir el órden en que se realizan las operaciones aritméticas. Así, 2 + 3 ^ 2 es 2 + 9 y no 5 ^ 2 puesto que la potenciación tiene mayor precedencia que el resto de las operaciones.

Esta precedencia de operadores se especifica mediante el orden en el que se colocan las declaraciones de los tokens correspondientes a los operadores. Los operadores de mayor precedencia deben ir después que los de menor precedencia. Por ejemplo:

%left '+' '-'
%left '*' '/'

Significa que la multiplicación y la división tienen precedencia sobre la suma y la resta.

Como se indicó anteriormente, %left señala la asociatividad del operador hacia la izquierda y %right hace lo contrario, lo cual es correcto en el caso de la potenciación:

          4      4
         3     (3 )
2*3*4 = 2   = 2

Un caso que rompe el esquema corresponde al operador negativo unario (que simplemente cambia el signo de la expresión a su derecha.) Aunque usa el mismo signo, su precedencia no es la misma que la de la resta. Por ejemplo:

2 * -3 = (2) * (-3)

En cambio la resta tiene menos precedencia que la multiplicación, con la que se obtendría el absurdo:

2 * -3 = (2*) - (3)

Puesto que se no se trata de un nuevo símbolo, será necesario especificar con un identificador la precedencia de la regla correspondiente al negativo unario. Con ese fin hemos utilizado la palabra “NEGATIVO”, la cual se especifica en la sección de declaraciones después de la multiplicación y la división, pero antes de la potenciación. Asimismo, la regla que define el número negativo se acompaña de “%prec NEGATIVO” para asociarle la correspondiente precedencia.

Un pequeño ejercicio: la expresión +1+2 falla. Corríjalo.

20.4. Calculadora de Números Complejos

El siguiente ejemplo es una extensión natural de la calculadora y tiene como novedad la capacidad de ejecutar ciertas operaciones en números complejos:

(1+2i)^5
41-38i

La novedad de este programa con respecto a todos los que hemos visto anteriormente radica en que ya NO es posible utilizar un único tipo de dato tanto para los "ENTEROS" como para los agrupamientos "expr" (con la declaración “YYSTYPE int”), puesto que estos últimos ahora son números complejos por lo que deberán contener tanto una parte real como una imaginaria; es decir, los agrupamientos "expr" deberán ser estructuras de un tipo apropiado.

Por comodidad definiremos esta estructura en un archivo cabecera llamado complex.h:

typedef struct
	{
	int r;
	int i;
	} Complex;

Complex producto(Complex a, Complex b);

La gramática y las rutinas auxiliares vienen a continuación en operacomplex.y:

%{
#include "complex.h"
%}
%union {
	int val;
	Complex complejo;
	}

%token <val> ENTERO
%type <complejo> expr

%token ENTERO
%left '+' '-'
%left '*'
%left NEGATIVO
%right '^'
%%
todo:	line
	|
	todo line
	;
line:	expr '\n' { printf("%d%+di\n",$1.r,$1.i); }
	;
expr:	ENTERO 'i'   { $$.i = $1 ; $$.r = 0; }
	|
	'i'          { $$.i = 1  ; $$.r = 0; }
	|
	ENTERO       { $$.r = $1 ; $$.i = 0; }
	|
	expr '+' expr { $$.r = $1.r + $3.r ; $$.i = $1.i + $3.i ; }
	|
	expr '-' expr { $$.r = $1.r - $3.r ; $$.i = $1.i - $3.i ; }
	|
	expr '*' expr { $$ = producto($1,$3); }
	|
	expr '^' ENTERO {
			int z;
			$$ = $1;
			for(z=1;z< $3 ;z++)
				$$ = producto($$,$1);
			}
	|
	'(' expr ')' { $$ = $2 ; }
	|
	'-' expr %prec NEGATIVO { $$.r = - $2.r ; $$.i = - $2.i ; }
	;
%%
#include <stdio.h>
#include <ctype.h>

Complex producto(Complex a, Complex b)
{
Complex r;
r.r=a.r*b.r-a.i*b.i;
r.i=a.r*b.i+a.i*b.r;
return r;
}

int yylex(void)
{
int c;

c=getchar();
if(c==EOF) return 0;
if(isdigit(c))
	{
	ungetc(c,stdin);
	scanf("%d",&yylval.val);
	return ENTERO;
	}
return c;
}

int yyerror(const char *s)
{
fprintf(stderr,"operacomplex: %s\n", s);
return 1;
}

int main(int argc,char *argv[])
{
yyparse();
return 0;
}

20.4.1. Especificación de Múltiples Tipos

El archivo se inicia incluyendo al archivo auxiliar complex.h a fin de que el tipo Complex esté disponible en lo sucesivo. A continuación viene lo más importante del programa, la directiva %union, la cual reemplaza a YYSTYPE puesto que ya no nos basta con un único tipo de dato para representar a todos los elementos de la gramática. Precisando, ahora requerimos tanto enteros simples (int) para los "tokens" que retorna yylex(), así como números complejos (Complex.) Estos tipos se declaran de un modo muy similar a una unión de lenguaje C, es decir, especificando (entre llaves) tantos "miembros de la unión" como tipos se pretenda utilizar. Puesto que aquí requerimos dos tipos, hemos declarado la %union conteniendo dos miembros que representan a dichos tipos. Como en toda unión, los miembros deberán tener algún nombre, para lo cual tenemos total libertad de elección (aquí les hemos colocado val y complejo.)

A continuación, los tokens se declaran con la directiva %token, ahora especificando el miembro de la unión asociado entre signos < y >. Para nuestro caso, el token ENTERO se almacenará en un int, el cual en la unión está representado por el miembro val, por lo que escribimos:

%token <val> ENTERO

Los agrupamientos también se declaran con la directiva %type. Para nuestro caso, el agrupamiento expr contendrá un número complejo, por lo que escribimos:

%type <complejo> expr

En cuanto a los agrupamientos ‘line’ y ‘todo’, estos nunca reciben un valor como tal (en sus reglas nunca aparece el $$) por lo que no requieren que su tipo sea declarado mediante la %union. Dicho en otras palabras, sólo el token ENTERO y el agrupamiento expr tienen "significado semántico".

La gramática es muy similar a la calculadora del ejemplo anterior. Hemos eliminado la división a fin de reducir un poco el listado, y la multiplicación se ha extraído en una función auxiliar a fin de que la gramática se mantenga clara.

La última modificación importante corresponde al paso del entero desde yylex(). Como se aprecia, esto se hace mediante yylval.val; es decir, la variable yylval ahora corresponde a una unión tal como se declaró en %union.

20.5. B.A.S.I.C.

Si Ud. tuvo "infancia informática", lo siguiente le debe ser familiar:

10 print "La Edad"
20 print
40 input "Cual es tu nombre?",N$
50 input "Cual es tu edad? ",E
70 print
80 print N$;" tiene ";E;" anhos"
90 gosub 500
100 end
500 print
510 print "El abuelo de ";N$;" tiene ";E+40;" anhos"
520 return
run
La Edad

Cual es tu nombre? Oscar
Cual es tu edad?  40

Oscar tiene 40 anhos

El abuelo de Oscar tiene 80 anhos

LISTO

Se trata de una sesión con un computador ficticio que interpreta el antiguo, venerable y anatemizado lenguaje Basic, con listados orientados a números de líneas.

El siguiente programa permite implementar lo que se ha visto arriba, y algunas cosas más.

20.5.1. Especificación del lenguaje

Las siguientes tablas resumen los comandos e instrucciones a implementar. Los comandos corresponden a órdenes que se ejecutan directamente, sin conformar parte del programa. Las instrucciones por el contrario, se asocian a números de línea, y constituyen el cuerpo del programa.

Table 11. Comandos Externos
Comando Función

LIST

Lista el programa

RUN

Ejecuta el programa

Table 12. Instrucciones Basic
Instrucción Significado

CLS

Borra la pantalla

END

Termina el programa

GOSUB

Salta a una subrutina

GOTO

Salta a una línea

INPUT

Recibe un texto o un número por teclado y lo asigna a una variable

PRINT

Imprime textos y el resultado de expresiones numéricas

RETURN

Retorna de una subrutina

El programa además permite la definición y evaluación de variables de tipo numérico (identificadores simples) así como variables de cadenas de caracteres (un identificador terminado en signo dólar '$'.) A fin de demostrar la flexibilidad de yacc, incluiremos algunas sutilezas propias de ciertos dialectos de Basic, como por ejemplo:

  • La instrucción PRINT con una expresión terminada en punto y coma, no genera salto de línea

  • La instrucción PRINT con una expresión terminada en coma, no genera salto de línea pero sí 10 espacios

  • Las expresiones que imprime PRINT se pueden concatenar si se emplea el punto y coma como separador; si se emplea la coma, entonces se genera un espacio intermedio

  • La instrucción INPUT puede (opcionalmente) aceptar una cadena de caracteres separada de la variable por una coma

  • La variable que lee INPUT puede ser numérica o de cadena de caracteres

  • Las cadenas de caracteres se pueden concatenar con el operador +, especialmente en asignaciones como A$=B$+C$

Este programa (con gramática incluida) contiene más de 400 líneas, por lo que se ha dividido en varios archivos a fin de facilitar el mantenimiento, y de paso nos permite ilustrar algunas características adicionales.

20.5.2. Rutinas iniciales

Empecemos por el principio; el archivo basic_main.c contiene la rutina main() así como otras rutinas auxiliares:

#include "basic.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "y.tab.h"

#define READY printf("\nLISTO\n")

char *ptr_line[MAX_LINE];
const char *ptr_lex;
int current_line;
Variable variable[MAX_VARS];
S_Variable s_variable[MAX_VARS];
char status_flag;
int line_stack[MAX_STACK];
int line_stack_index;

static char line[MAX_LINE_CHARS];

void parse_input_line(void)
{
int nl=yylval.val;
const char *ptr_aux=ptr_lex;
if(yylex()==0)
	{
	if(ptr_line[nl])
		free(ptr_line[nl]);
	ptr_line[nl]=NULL;
	return;
	}
while(*ptr_aux==' ' || *ptr_aux=='\t')
	ptr_aux++;
if(ptr_line[nl])
	free(ptr_line[nl]);
ptr_line[nl]=strdup(ptr_aux);
}

void list(FILE * fp)
{
int z;
for(z=0;z<MAX_LINE;z++)
	if(ptr_line[z])
		fprintf(fp,"%d %s\n",z,ptr_line[z]);
}

int set_line_next(int next)
{
while(next<MAX_LINE-1)
	{
	next++;
	if(ptr_line[next])
		{
		current_line=next;
		ptr_lex=ptr_line[current_line];
		return 0;
		}
	}
return -1;
}

int main(int argc,char *argv[])
{
READY;
while(fgets(line,MAX_LINE_CHARS,stdin))
	{
	line[strlen(line)-1]='\0';
	ptr_lex=line;
	switch(yylex())
		{
		case NUMERO:
			parse_input_line();
			continue;
		case RUN:
			if(set_line_next(-1)==-1)
				{
				printf("\nError: No hay lineas\n");
				continue;
				}
			status_flag=0; line_stack_index=0;
			for(;;)
				{
				yyparse();
				if(set_line_next(current_line)==-1) break;
				if(status_flag) break;
				}
			READY;
			continue;
		case LIST:
			list(stdout);
			READY;
			continue;
		case 0:
		continue;
		}
	printf("\nERROR: Comando no reconocido\n");
	}
return 0;
}

Las líneas del programa se almacenan en el array de punteros char “ptr_line[]”, el cual tiene capacidad para MAX_LINE líneas. El número de línea corresponde precísamente al índice de este array, y cuando una línea no está presente, el elemento correspondiente contiene NULL. Esta no es la manera más eficiente de almacenar las cadenas de texto, pero es suficiente para nuestros propósitos [199] . La rutina main() recibe líneas desde la entrada estándar, y solicita a la rutina yylex() que las analice. Esto se hace mediante el puntero global ptr_lex, el cual se inicializa al principio de la línea cada vez que se lee.

El primer token de la línea permite indagar si se trata de un número (de línea) o en caso contrario debe ser una instrucción de tipo "comando externo". En el caso de los números se invoca a la rutina parse_input_line() que explicaremos luego. Como veremos, cada instrucción y comando externo corresponde a un token distinto, por lo que es muy sencillo discernirlos.

A continuación se encuentra el código que "ejecuta" el programa (comando externo "run", correspondiente al token “RUN”.) Este código invoca repetidamente a yyparse() y sólo se detiene cuando ya no hay más líneas por procesar (función set_line_next()) o si la variable status_flag se ha activado (lo cual ocurre por errores o por la instrucción END.)

Finalmente, el comando externo "list" (token “LIST”) permite obtener un listado total del programa, y se implementa en la función list(). Dicha función recibe un puntero a FILE (normalmente la salida estándar), lo cual será conveniente si en el futuro pretendemos enviar el listado hacia un archivo de disco.

La función set_line_next() se encarga de hacer avanzar la "línea actual" (variable current_line) y reinicializa el puntero global ptr_lex al principio del texto de la nueva línea. Esto permite que la función yylex() procese a partir de ese punto.

En este punto es necesario hacer una observación muy importante: En nuestra rutina main() hemos utilizado los "tokens" retornados por yylex(), los cuales corresponden a diversas constantes auxiliares que son definidas por yacc; puesto que ahora main() se encuentra fuera del archivo de gramática, es necesario que nosotros le proporcionemos las constantes de yacc. Con este fin, es posible instruir a yacc para que genere un archivo auxiliar (header) llamado y.tab.h, el cual deberá ser incluido por nuestro basic_main.c, a fin de que acceda a las referidas constantes.

Para que yacc genere este archivo simplemente se le debe pasar la opción -d en la línea de comandos, lo cual nosotros haremos indirectamente desde nuestro Makefile.

Puesto que Make ya tiene conocimientos acerca de yacc, bastará con agregar al Makefile la siguiente línea:

YFLAGS = -d

para que yacc sea invocado con la mencionada opción.

20.5.3. Scanner para el lenguaje

A continuación listamos el archivo basic_lex.c, el cual contiene a la rutina de análisis lexicográfico yylex(). Aquí ocurre la misma situación que en basic_main.c: la rutina yylex() está fuera del archivo de gramática por lo que también se deberá incluir el archivo header auxiliar y.tab.h:

#include "basic.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "y.tab.h"

struct 
	{
	const char *instr;
	int token;
	} ins_aux []={
{ "print",	PRINT },
{ "goto",	GOTO },
{ "cls",	CLS },
{ "input",	INPUT },
{ "return",	RETURN },
{ "gosub",	GOSUB },
{ "end",	END },
{ "list",	LIST },
{ "run",	RUN },
{NULL,0}
};

int compare_first(const char *data, const char *what)
{
int r=0;
while(*what)
	{
	if(! *data) return 0;
	if(toupper(*data++)!=toupper(*what++))
		return 0;
	r++;
	}
if(isalpha(*data)) return 0;
return r;
}

int search_char(const char *data,char c)
{
int z=0;
while(*data)
	{
	if(*data++ == c) return z;
	z++;
	}
return -1;
}

int yylex(void)
{
int aux;
if(! *ptr_lex)
	return 0;
while(*ptr_lex == ' ' || *ptr_lex == '\t')
	ptr_lex++;
if(*ptr_lex == '\"')
	{
	if((aux=search_char(ptr_lex+1,'\"'))!=-1)
		{
		yylval.cadena=calloc(aux+1,sizeof(char));
		strncpy(yylval.cadena,ptr_lex+1,aux);
		ptr_lex+=(aux+2);
		return STRING;
		}
	}
if(isdigit(*ptr_lex) || *ptr_lex=='.')
	{
	sscanf(ptr_lex,"%lf%n",&yylval.val,&aux);
	ptr_lex+=aux;
	return NUMERO;
	}
int z=0;
while(ins_aux[z].instr)
	{
	if((aux=compare_first(ptr_lex,ins_aux[z].instr)))
		{
		ptr_lex+=aux;
		return ins_aux[z].token;
		}
	z++;
	}
aux=0;
while(isalnum(ptr_lex[aux]))
	aux++;
if(aux)
	{
	yylval.cadena=calloc(aux+1,sizeof(char));
	strncpy(yylval.cadena,ptr_lex,aux);
	ptr_lex+=aux;
	return IDENTIF;
	}
return *ptr_lex++;
}

La rutina yylex() gira en función del puntero ptr_lex, el cual apunta al código fuente en proceso. El trabajo que realiza es en breve:

  • Retornar cero cuando se llega al fin de la línea (yyparse lo interpreta como fin del texto)

  • Retornar cadenas de caracteres (STRING) si se encuentra la forma "…​"

  • Retornar números (NUMERO) cuando se encuentran dígitos

  • Retornar el token correspondiente a las instrucciones del lenguaje

  • Retornar cadenas de caracteres posiblemente correspondiendo a nombres de variables (usando el token IDENTIF)

La rutina search_char() es similar a strchr() pero retorna la posición donde se encuentra el caracter que se busca (no un puntero) lo cual es conveniente y evita realizar una "resta de punteros" que no es muy portable.

Asimismo, la rutina compare_first() permite indagar si un texto se inicia con una determinada "palabra" (ignorando mayúsculas de minúsculas.) Aquí "palabra" se refiere sólo a conjuntos de letras. Por ejemplo, su penúltima línea evita que un texto que empieza con "printer" se reconozca como conteniendo "print".

La rutina compare_first() se usa para buscar instrucciones y comandos externos, los cuales por comodidad se han agrupado en el array de estructuras ins_aux[], lo cual evita tener que escribir un gran número de sentencias if(compare_first())…​.

20.5.4. Rutinas auxiliares

A continuación presentamos un conjunto de rutinas auxiliares que no son tan interesantes:

#include "basic.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

void ptr_set_number(char **x,double n)
{
*x=malloc(64);
sprintf(*x,"%g",n);
}

void ptr_add_text(char **x,char *text)
{
*x=realloc(*x,strlen(*x)+strlen(text)+1);
sprintf((*x)+strlen(*x),"%s",text);
free(text);
}

void variable_set(char *n, double v)
{
int z,j=-1;
for(z=0;z<MAX_VARS;z++)
	{
	if(j==-1 && variable[z].name==NULL)
		{ j=z; continue; }
	if(variable[z].name && strcmp(variable[z].name,n)==0)
		{ variable[z].value=v; free(n); return; }
	}
variable[j].name=n;
variable[j].value=v;
}

double variable_get(char *n)
{
int z;
for(z=0;z<MAX_VARS;z++)
	if(variable[z].name && strcmp(variable[z].name,n)==0)
		{
		free(n); return variable[z].value;
		}
free(n); return 0.0;
}

void s_variable_set(char *n, char *v)
{
int z,j=-1;
for(z=0;z<MAX_VARS;z++)
	{
	if(j==-1 && s_variable[z].name==NULL)
		{ j=z; continue; }
	if(s_variable[z].name && strcmp(s_variable[z].name,n)==0)
		{ s_variable[z].value=v; free(n); return; }
	}
s_variable[j].name=n;
s_variable[j].value=v;
}

char *s_variable_get(char *n)
{
int z;
for(z=0;z<MAX_VARS;z++)
	if(s_variable[z].name && strcmp(s_variable[z].name,n)==0)
		{
		free(n); return s_variable[z].value;
		}
free(n); return "";
}

void read_set(char *n)
{
char input_line[MAX_LINE_CHARS];
fgets(input_line,MAX_LINE_CHARS,stdin);
variable_set(n,atof(input_line));
}

void s_read_set(char *n)
{
char *input_line=malloc(MAX_LINE_CHARS);
fgets(input_line,MAX_LINE_CHARS,stdin);
input_line[strlen(input_line)-1]='\0';
s_variable_set(n,input_line);
}

Las primera rutina permite construir un texto a partir de un número double, mientras que la segunda concatena la segunda cadena a la primera (la cual se redimensiona con realloc().) Nótese que la segunda cadena se libera puesto que en ningún caso se volverá a utilizar (esto se verá en la gramática.)

Las cuatro rutinas que siguen simplemente llenan y consultan dos arrays de estructuras que almacenan las variables, tanto numéricas como de cadenas de caracteres (arreglos variable[] y s_variable[].) Nuevamente, las cadenas de texto que corresponden a los identificadores de las variables, son liberadas de memoria cuando se trata de variables ya registradas, puesto que en ningún caso de la gramática el identificador es reutilizado.

Finalmente, las últimas dos rutinas permiten asignar variables a partir de una línea de texto ingresada por el usuario (se utilizan con la instrucción INPUT.)

Este es un buen momento para mostrar el archivo cabecera del proyecto, basic.h:

#define MAX_LINE 1000
#define MAX_VARS 100
#define MAX_STACK 10
#define MAX_LINE_CHARS 1024

extern char *ptr_line[MAX_LINE];
extern int current_line;
extern const char *ptr_lex;
extern char status_flag;
extern int line_stack[MAX_STACK];
extern int line_stack_index;

typedef struct
	{
	char *name;
	double value;
	} Variable;
extern Variable variable[MAX_VARS];

typedef struct
	{
	char *name;
	char *value;
	} S_Variable;
extern S_Variable s_variable[MAX_VARS];

void ptr_set_number(char **,double n);
void ptr_set_text(char **,char *text);
void ptr_add_number(char **,double n);
void ptr_add_text(char **,char *text);
void variable_set(char *n, double v);
double variable_get(char *n);
void s_variable_set(char *n, char *v);
char *s_variable_get(char *n);
void read_set(char *n);
void s_read_set(char *n);

20.5.5. La gramática del lenguaje

Ahora pasaremos al archivo de gramática:

%{
#include "basic.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
%}
%union {
	double val;
	char *cadena;
	}

%token <val> NUMERO
%token <val> PRINT GOTO CLS INPUT GOSUB RETURN END LIST RUN
%token <cadena> STRING IDENTIF
%type <cadena> p_expr s_expr str_item
%type <val> expr

%left ',' ';'
%left '+' '-'
%left '*' '/'
%left NEGATIVO
%%
todo:	instr
	|
	todo ':' instr
	;
instr:  PRINT		{ printf("\n"); }
	|
	PRINT p_expr	{ printf("%s\n",$2); free($2); }
	|
	PRINT p_expr ',' { printf("%s          ",$2); free($2); }
	|
	PRINT p_expr ';' { printf("%s",$2); free($2); }
	|
	RETURN		{ current_line=line_stack[--line_stack_index];
			  return 1;  }
	|
	GOTO NUMERO	{ current_line = $2-1; return 1; }
	|
	GOSUB NUMERO	{ line_stack[line_stack_index++]=current_line;
			  current_line = $2-1; return 1; }
	|
	END		{ status_flag=1; return 1; }
	|
	CLS		{ system("clear"); }
	|
	INPUT STRING ',' IDENTIF { printf("%s ",$2); free($2);
				read_set($4); }
	|
	INPUT IDENTIF	{ printf("? "); read_set($2); }
	|
	INPUT STRING ',' IDENTIF '$' { printf("%s ",$2); free($2);
				s_read_set($4); }
	|
	INPUT IDENTIF '$'	{ printf("? "); s_read_set($2); }
	|
	IDENTIF '=' expr  { variable_set($1,$3); }
	|
	IDENTIF '$' '=' s_expr  { s_variable_set($1,$4);  }
	;
p_expr: expr		{ ptr_set_number(&$$,$1); }
	|
	s_expr
	|
	p_expr ',' p_expr { ptr_add_text(&$$,strdup(" "));
			    ptr_add_text(&$$,$3); }
	|
	p_expr ';' p_expr { ptr_add_text(&$$,$3); }
	;
s_expr: str_item
	|
	s_expr '+' str_item { ptr_add_text(&$$,$3); }
	;
str_item: STRING
	|
	IDENTIF '$'	{ $$ = strdup(s_variable_get($1)); }
	;
expr:	NUMERO        { $$ = $1 ; }
	|
	IDENTIF       { $$ = variable_get($1); }
	|
	expr '+' expr { $$ = $1 + $3 ; }
	|
	expr '-' expr { $$ = $1 - $3 ; }
	|
	expr '*' expr { $$ = $1 * $3 ; }
	|
	expr '/' expr { $$ = $1 / $3 ; }
	|
	'(' expr ')' { $$ = $2 ; }
	|
	'-' expr %prec NEGATIVO { $$ = - $2 ; }
	;
%%

int yyerror(const char *s)
{
printf("Error de linaxis en linea %d\n",current_line);
status_flag=1;
return 1;
}

La %union contiene los miembros val y cadena, utilizados para contener valores double y punteros a char. Los números (reales) serán tokens identificados por “NUMERO”, y como sabemos, sus valores se almacenarán en el miembro val de la unión yylval. En el caso de las instrucciones (como PRINT, GOTO, etc.) hemos definido un token para cada una de ellas; si bien han sido asignadas al miembro val de la unión, éste no será utilizado. Otra estrategia (menos conveniente) consistiría en definir un único token genérico para todas las instrucciones (por ejemplo, “INSTRUCCION”) y emplear el valor de yylval.val para diferenciar qué instrucción en particular es la que se recibe.

Tanto las cadenas literales de texto (de la forma "…​") como los nombres de las variables, generarán punteros a char que apuntan a memoria asignada dinámicamente (ver yylex()) por lo que el tipo correspondiente al miembro “cadena” es el indicado.

Finalmente, tenemos agrupamientos que almacenan números (expr) y cadenas de texto:

  • str_item: Una texto que proviene de una cadena literal o de una variable de la forma X$

  • s_expr: Una expresión que se arma contatenando texto con el operador '+'. Usado por ejemplo en A$=B$+C$

  • p_expr: Una expresión (de cadena de texto) válida en el contexto de la instrucción PRINT, conteniendo las anteriores expresiones así como expresiones numéricas converitidas a texto, y las combinaciones de sí misma unidas por comas y puntos y comas

La gramática de las instrucciones es sencilla. Por ejemplo, PRINT simplemente imprime su expresión y la libera de la memoria (puesto que todas las expresiones de cadena son asignadas dinámicamente.) GOTO utiliza la conocida variable current_line para programar la siguiente línea de texto (una antes que la deseada) puesto que set_line_next() siempre avanza al menos una línea. Nótese que GOTO retorna a main() para que ocurra realmente el salto de línea.

GOSUB/RETURN usan la misma lógica que GOTO pero con el añadido de almacenar los números de línea en una "pila" a fin de "recordar" la línea de llamada.

Las rutinas de asignación y consulta de variables invocan a las rutinas auxiliares creadas con este propósito. Como sabemos, éstas también se encargan de liberar de la memoria el texto del identificador cuando la variable ya ha sido registrada.

En el resto de agrupamientos sólo p_expr merece un comentario adicional puesto que se define recursivamente en función de sí misma a ambos lados, por ejemplo:

p_expr ';' p_expr { ptr_add_text(&$$,$3); }

Esto es similar a las definiciones de expr y tal vez Ud. recuerde (de los anteriores ejemplos) que existe una ambigüedad en el orden de evaluación. Para evitarla hemos definido la asociatividad de los operadores ';' y ',' en la sección inicial:

%left ',' ';'

20.5.6. Compilación y construcción del ejecutable

Finalmente, presentamos el Makefile del proyecto, conteniendo la opción YFLAGS = -d que explicaramos anteriormente.

CFLAGS = -Wall
YFLAGS = -d

basic: basic.o basic_aux.o basic_main.o basic_lex.o
	cc -o $@ $^

clean:
	rm -f *.o basic y.tab.h

Al utilizarlo, apreciamos que el trabajo que nos ahorra es invaluable:

$ make clean
$ make
yacc -d basic.y
mv -f y.tab.c basic.c
cc    -c -o basic.o basic.c
cc    -c -o basic`aux.o basic`aux.c
cc    -c -o basic`main.o basic`main.c
cc    -c -o basic`lex.o basic`lex.c
cc -o basic basic.o basic`aux.o basic`main.o basic_lex.o
rm basic.c

20.6. Ejercicios

1 Tomando como base la calculadora, construya un programa que permita realizar operaciones con polinomios:

$ ./polinomios
>> (x-1)^2
+1x^2-2x+1
>> (-x+2)^3
-1x*3+6x*2-12x+8
>> (x+x*2-1)*3
+1x*6+3x*5-5x^3+3x-1
>> x-2
+1x-2

2 Implemente algunos comandos e instrucciones típicas (incluyendo funciones) al intérprete de lenguaje basic:

Table 13. Comandos Externos
Nombre de Comando Propósito

NEW

Elimina todas las líneas de la memoria

SAVE "archivo"

Graba un archivo en disco

LOAD "archivo"

Carga un archivo desde disco

SYSTEM

Termina el intérprete basic y sale al sistema operativo

SHELL "comando"

Ejecuta un "comando" de shell. De no estar presente el "comando", inicia una sesión con el shell.

FILES

Lista el directorio actual

Table 14. Nuevas Funciones
Nombre de Función Propósito

STR$(X)

Obtiene una cadena de texto a partir de un valor numérico

VAL(X$)

Obtiene el valor numérico a partir de una cadena de texto

CHR$(X)

Obtiene un caracter a partir de su código ASCII numérico

ASC(X$)

Obtiene el valor numérico ASCII del primer caracter de una cadena

LEFT$(X$,n)

Obtiene los primeros 'n' caracteres de X$ tomados desde la izquierda

RIGHT$(X$,n)

Obtiene los primeros 'n' caracteres de X$ tomados desde la derecha

MID$(X$,n,s)

A partir de la posición 'n' de X$, obtiene (a lo más) 's' caracteres

SIN(X)

Obtener el seno del ángulo X

COS(X)

Obtener el coseno del ángulo X

TAN(X)

Obtener la tangente del ángulo X

SQRT(X)

Obtener la raíz cuadrada de X

Adicionalmente se requiere:

  • Agregar la operación de potenciación (^) para los números, utilizando la función matemática pow()

  • El comando “run” debería ser capaz de recibir una cadena de texto con un programa a ser cargado y ejecutado (previa eliminación del programa actualmente en memoria)

  • Si se proporciona un argumento en la línea de comandos, este debería corresponder al nombre de un archivo de programa Basic a ser ejecutado de inmediato

  • Implemente la instrucción IF condicion THEN instruccion…​. Para esto deberá definir un nuevo agrupamiento (de tipo entero) que contenga un "estado lógico" (verdadero o falso) y permita comparar expresiones numéricas mediante los operadores "=", "<", ">", "⇐", ">=", "<>". Cuando la condicion es verdadera, las instrucciones que siguen a THEN se ejecutan. En caso contrario, se salta a la siguiente línea

  • Implemente los operadores binarios AND, OR, NOT para crear agrupamientos lógicos más complejos

3 Gráficos en Basic

Implemente las siguientes instrucciones gráficas en Basic utilizando, por ejemplo, SDL:

Table 15. Instrucciones Gráficas
Instrucción Propósito

GRAPHIC arg[,r,g,b]

Con arg=1, crea una ventana para dibujos con fondo de color [r,g,b] (por omisión blanco.) Con arg=0, la ventana se cierra

COLOR r,g,b

Define el color de dibujo a [r,g,b]

DRAW xa,ya [TO xb,yb]

Traza un punto en (xa,ya) o una línea desde (xa,ya) hasta (xb,yb)

Appendix A: Respuestas para algunos Ejercicios

A.1. Capítulo 5

A.1.1. Ejercicio 1, parte A

Pause(2) requiere que el proceso en curso reciba una señal. Con SIG_IGN el proceso ignora (no recibe) las señal.

A.1.2. Ejercicio 1, partes B y C

/*
 * Solucion a sleep.c
 */
#include <unistd.h>
#include <signal.h>

void my_sleep(int tiempo);

int main()
{
	alarm(7);
	my_sleep(5);
	sleep(10);
	return 0;
}

static void dumb_handler(int s)
{
return;
}

void my_sleep(int tiempo)
{
struct sigaction s,u;
int ap;

ap=alarm(0);
if(ap>0 && tiempo>ap)
	{
	/* Reprogramar la alarma */
	alarm(ap);
	pause();
	return;
	}

s.sa_handler=dumb_handler;
sigemptyset(&s.sa_mask);
s.sa_flags=0;
sigaction(SIGALRM,&s,&u);

alarm(tiempo);
pause();

/* Resetear signal handler a valor original */
sigaction(SIGALRM,&u,NULL);

/* Reprogramar la alarma para lo que le
 * quedaba pendiente */
if(ap>0)
	alarm(ap-tiempo);

return;
}

A.1.3. Ejercicio 2

El tercer argumento de sigprocmask (si no es NULL) permite obtener la máscara actual de señales del proceso. Por otro lado, el identificador SIG_SETMASK reconfigura la máscara exactamente al valor señalado en el segundo argumento. Puesto que el único cambio aplicado a la máscara consistió en bloquear SIGUSR1, el restablecimiento de la antigua máscara equivale a desbloquear dicha señal.

A.2. Capítulo 6

A.2.1. Ejercicio 1

La solución consiste en usar waitpid en lugar de sleep(10):

6a7
> #include <sys/wait.h>
23c24
<               sleep(10);
---
>               waitpid(p,NULL,0);
25a27
>       sleep(5);

A.2.2. Ejercicio 3

Lo nuevo aquí es el uso de la estructura tipo rusage (variable "uso") cuyos campos ru_utime y ru_stime proporcionan el tiempo consumido por el hijo.

Para ver los campos proporcionaods por rusage se puede consultar el manual de la función getrusage(2). Como se aprecia allí, los campos ru_utime y ru_stime son de tipo struct timeval.

Esta estructura se documenta en el manual de la función gettimeofday así:

	struct timeval {
		long    tv_sec;         /* segundos */
		long    tv_usec;        /* microsegundos */
 	};

El nuevo listado:

/*
 * wait4
 */
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <stdio.h>
#include <stdlib.h>

void menu_hijo(void);

int main()
{
int pid_child,status;
struct rusage uso;

pid_child=fork();
if(pid_child>0)
	{
	for(;;)
		{
		wait4(pid_child,&status,WUNTRACED,&uso);
		printf("\nSoy el proceso padre\n");
		if(WIFEXITED(status))
			{
			printf("El hijo termino normalmente.\n");
			printf("Valor de retorno: %d\n",
					WEXITSTATUS(status));
			break;
			}
		if(WIFSIGNALED(status))
			{
			printf("El hijo termino por senhal.\n");
			printf("La senhal fue: %d\n",
					WTERMSIG(status));
			break;
			}
		if(WIFSTOPPED(status))
			{
			printf("El hijo ha sido detenido.\n");
			printf("La senhal fue: %d\n",
					WSTOPSIG(status));
			printf("Se le continuara esperando...\n");
			}
		}
	// Recursos
	printf("User:%ld System:%ld\n",	uso.ru_utime.tv_sec*1000000+
					uso.ru_utime.tv_usec
					,
					uso.ru_stime.tv_sec*1000000+
					uso.ru_stime.tv_usec);
	}
else
	menu_hijo();

return 0;
}

void menu_hijo(void)
{
volatile int i,r=0;

/* Hacer algunos calculos para gastar CPU modo usuario */
for(i=0;i<1000000;i++)
	r=(r+43)%123+7*(r+12)%13;
/* Hacer algunas llamadas al sistema para gastar CPU system */
for(i=0;i<1000000;i++)
	r=getpid();

for(;;)
	{
	printf("Soy el proceso hijo. Escriba un numero de senhal\n"
	"o cero para terminar: ");
	scanf("%d",&i);
	if(i==0)
		{
		printf("Escriba el valor de retorno: ");
		scanf("%d",&r);
		exit(r);
		}
	kill(getpid(),i);
	}
}

A.2.3. Ejercicio 4

La llamada al sistema kill(2) permite enviar una señal a todo un grupo de procesos simplemente especificando el PGID con valor negativo.

En el fork_grupo.c simplemente cambiaremos la siguiente sección:

/* El hijo */
z=fork();
if(z>0)
        {
        printf("Proceso hijo pid=%d pgid=%d\n",getpid(),getpgrp());
        sleep(60);
        exit(0);
        }

Por lo siguiente:

/* El hijo */
z=fork();
if(z>0)
        {
        printf("Proceso hijo pid=%d pgid=%d\n",getpid(),getpgrp());
        sleep(30);
        printf("Termino el grupo del nieto y bisnieto\n");
        kill(-z,SIGTERM);
        waitpid(z,NULL,0);
        sleep(30);
        exit(0);
        }

A.3. Capítulo 7

A.3.1. Ejercicios 1, 2 y 3

/*
 * Solucion minishell
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>

#define LINE_WIDTH 256
#define N_ARGS 30
#define N_P_BG 100

int read_a_line(void);
int parse(void);
int process(void);
void cleanup(void);
void intr_handler(int);
void child_handler(int);
void desactivar_senhales(void);
void redireccion(void);
void interpreta_status(int,char *);
void registrar_job(int);
void listar_jobs(void);
void eliminar_job(int);

char LINE[LINE_WIDTH];
char LINE_COPY[LINE_WIDTH];
char DST_FILE[LINE_WIDTH];
char *COMMAND[N_ARGS];
char flag_hijo_murio;
char flag_bg;
char flag_redirect;

typedef struct
	{
	int pid;
	char cmd[256];
	} BGJOB;

BGJOB bgjob[N_P_BG]; /* Hasta N_P_BG procesos en background */

int main(int argc,char **argv)
{
struct sigaction sa;

sa.sa_handler=intr_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags=0;
sigaction(SIGINT,&sa,NULL);

sa.sa_handler=SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags=0;
sigaction(SIGQUIT,&sa,NULL);

sa.sa_handler=child_handler;
sigemptyset(&sa.sa_mask);
/* Ya no se usa SA_RESTART */
sa.sa_flags=0;
sigaction(SIGCHLD,&sa,NULL);

while(1)
	{
	if(read_a_line()==-1)
		continue;
	if(parse()==-1)
		continue;
	if(process()==-1)
		return 0;
	cleanup();
	}
}

void intr_handler(int s)
{
}

void child_handler(int s)
{
int r;

flag_hijo_murio=1;

do
	{
	r=waitpid(-1,NULL,WNOHANG);
	if(r>0)
		eliminar_job(r);
	}
while(r!=0 && r!=-1);
}

int read_a_line(void)
{
int z;

write(STDOUT_FILENO,"$$$ ",4);

sigo_leyendo:
flag_hijo_murio=0;
z=read(STDIN_FILENO,LINE,LINE_WIDTH-1);

/* Interrupcion por SIGINT o SIGCHLD */
if(z==-1 && errno==EINTR)
	{
	if(flag_hijo_murio)
		goto sigo_leyendo;
	write(STDOUT_FILENO,"\n",1);
	return -1;
	}
if(z==0)
	{
	LINE[0]='\0';
	return 0;
	}
LINE[z-1]='\0';

sprintf(LINE_COPY,"%-.255s",LINE);

return 0;
}

int parse(void)
{
char *n=LINE;
int z=0;
char *err_msg1="Se requiere archivo de destino\n";
char *err_msg2="Demasiados argumentos\n";

flag_bg=0;
flag_redirect=0;

n=strtok(n," ");
if(n==NULL)
	{
	COMMAND[0]=NULL;
	return 0;
	}
do
	{
	if(strcmp(n,"&")==0)
		{
		COMMAND[z]=NULL;
		flag_bg=1;
		break;
		}
	if(strcmp(n,">")==0)
		{
		COMMAND[z]=NULL;
		n=strtok(NULL," ");
		if(n==NULL)
			{
			cleanup();
			write(STDERR_FILENO,err_msg1,strlen(err_msg1));
			return -1;
			}
		strcpy(DST_FILE,n);
		flag_redirect=1;
		continue;
		}
	if(z==N_ARGS)
		{
		COMMAND[z-1]=NULL;
		cleanup();
		write(STDERR_FILENO,err_msg2,strlen(err_msg2));
		return -1;
		}
	COMMAND[z]=strdup(n);
	z++;
	}
	while((n=strtok(NULL," ")));

COMMAND[z]=NULL;

return 0;
}

int process(void)
{
int p;

if(COMMAND[0]==NULL)
	return 0;

if(strcmp(COMMAND[0],"exit")==0)
	return -1;

if(strcmp(COMMAND[0],"jobs")==0)
	{
	listar_jobs();
	return 0;
	}

p=fork();

if(p==0)
	{
	static char *not_found="No se encontro el programa\n";
	
	if(flag_redirect)
		redireccion();
	if(flag_bg==1)
		desactivar_senhales();
	execvp(COMMAND[0],COMMAND);
	write(STDERR_FILENO,not_found,strlen(not_found));
	exit(errno);
	}
else
	if(flag_bg==0)
		{
		static char end_msg[80];
		int status;

		while(waitpid(p,&status,0)==-1 && errno==EINTR)
			;
		interpreta_status(status,end_msg);
		write(STDOUT_FILENO,end_msg,strlen(end_msg));
		}
	else
		registrar_job(p);
return 0;
}

void cleanup(void)
{
char **p=COMMAND;

while(*p)
	free(*p++);
}

void desactivar_senhales(void)
{
struct sigaction sa;

sa.sa_handler=SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags=0;

sigaction(SIGINT,&sa,NULL);
}

void redireccion(void)
{
int fd;
int e;
char *msg_err="Error abriendo el archivo de redireccion\n";

close(STDOUT_FILENO);
fd=open(DST_FILE,O_WRONLY|O_CREAT,0777);
if(fd!=STDOUT_FILENO)
	{
	e=errno;
	write(STDERR_FILENO,msg_err,strlen(msg_err));
	exit(e);
	}
}

void interpreta_status(int status,char *END_STATUS)
{
if(WIFEXITED(status))
	{
	sprintf(END_STATUS,
		"Valor de retorno: %d\n",
		WEXITSTATUS(status));
	return;
	}
if(WIFSIGNALED(status))
	{
	sprintf(END_STATUS,
		"Termino por senhal %d\n",
		WTERMSIG(status));
	return;
	}
}

void registrar_job(int p)
{
int z;

for(z=0;z<N_P_BG;z++)
	if(bgjob[z].pid==0)
		{
		bgjob[z].pid=p;
		strcpy(bgjob[z].cmd,LINE_COPY);
		return;
		}
/* Si se llega aqui es porque no hay mas espacio
 * para tantos procesos background. Se deberia
 * hacer algo mejor. */
}

void listar_jobs(void)
{
int z;

for(z=0;z<N_P_BG;z++)
	if(bgjob[z].pid!=0)
		printf("%-5d %s\n",bgjob[z].pid,bgjob[z].cmd);
}

void eliminar_job(int p)
{
int z;

for(z=0;z<N_P_BG;z++)
	if(bgjob[z].pid==p)
		{
		bgjob[z].pid=0;
		return;
		}
}

A.4. Capítulo 8

A.4.1. Ejercicio 1, parte B, método 1

/*
 * Solucion 1-b: Crear un archivo con permiso 777
 * Cambio de mascara
 */
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>

int main()
{
int fd,oldmask;

oldmask=umask(0);
printf("Antigua mascara:%o\n",oldmask);

fd=open("/tmp/prueba1",O_WRONLY|O_CREAT,0666);
if(fd==-1)
	perror("open fallo");

write(fd,"x",1);

close(fd);
return 0;
}

A.4.2. Ejercicio 1, parte B, método 2

/*
 * solucion 1-b: Crear un archivo con permiso 777
 * Cambio de mascara
 */
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main()
{
int fd;

fd=open("/tmp/prueba1",O_WRONLY|O_CREAT,0666);
if(fd==-1)
	perror("open fallo");

if(chmod("/tmp/prueba1",0777)==-1)
	printf("Error en chmod\n"),exit(1);

write(fd,"x",1);

close(fd);
return 0;
}

A.4.3. Ejercicio 3, parte D

/*
 * solucion 3-d: Crear un archivo de prueba con
 * write en modo sincrono, e intentar eliminar el modo
 * sincrono. No funciona en Linux.
 */
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>

#define SIZ 10000

int main()
{
long z;
int fd;
int s;

fd=open("/tmp/write_sync.txt",O_WRONLY|O_CREAT|O_SYNC|O_APPEND,0666);
if(fd==-1)
	perror("open fallo");

s=fcntl(fd,F_GETFL);
s&= ~O_SYNC;
fcntl(fd,F_SETFL,s);

for(z=0;z<SIZ;z++)
	write(fd,"x",1);

close(fd);
return 0;
}

A.4.4. Ejercicio 4

/*
 * Obtener geometria de disco duro
 */

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

/* Especifica de linux */
#include <linux/hdreg.h>

/* Modificar como convenga */
#define DEVICE "/dev/hda"

int main()
{
int fd,i;
struct hd_geometry geom;

fd=open(DEVICE,O_RDONLY);
if(fd==-1)
	{
	perror("No se pudo abrir " DEVICE);
	return 1;
	}
i=ioctl(fd,HDIO_GETGEO,&geom);
if(i==-1)
	{
	perror("No se pudo aplicar ioctl");
	return 1;
	}

printf("Heads: %u Sectors: %u Cyls: %u\n",
	geom.heads, geom.sectors, geom.cylinders);
return 0;
}

A.5. Capítulo 9

A.5.1. Ejercicio 5

/*
 * datesort.c: ordenamiento de archivos por fechas
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <time.h>

typedef struct
	{
	char *filename;
	time_t t;
	} ARCHIVO;

ARCHIVO *archivo=NULL;
int n_archivo=0;

void getfiles(char *dirname);
void nuevo_archivo(char *f,struct stat *sp);
int compara(const void *a, const void *b);

int main(int argc,char **argv)
{
int z;
struct tm *st;

if(argc<2)
	getfiles(".");
else
	for(z=1;z<argc;z++)
		getfiles(argv[z]);

qsort(archivo,n_archivo,sizeof(ARCHIVO),compara);

for(z=0;z<n_archivo;z++)
	{
	st=localtime(&archivo[z].t);
	printf("%4d/%02d/%02d %02d:%02d %s\n",
		st->tm_year+1900,st->tm_mon+1,st->tm_mday,
		st->tm_hour,st->tm_min,archivo[z].filename);
	}
return 0;
}

void getfiles(char *dirname)
{
DIR *d;
struct dirent *de;
char path[1024];
struct stat s;

d=opendir(dirname);
if(d==NULL)
	{
	fprintf(stderr,"No pude abrir %s\n",dirname);
	return;
	}
while((de=readdir(d)))
	{
	if(strcmp(de->d_name,".")==0)
		continue;
	if(strcmp(de->d_name,"..")==0)
		continue;
	sprintf(path,"%s/%s",dirname,de->d_name);
	stat(path,&s);
	nuevo_archivo(path,&s);	
	if(S_ISDIR(s.st_mode))
		getfiles(path);
	}
closedir(d);
}

void nuevo_archivo(char *f,struct stat *sp)
{
/* printf("Registro %s\n",f); */
archivo=realloc(archivo,(n_archivo+1)*sizeof(ARCHIVO));
if(archivo==NULL)
	fprintf(stderr,"realloc error\n"),exit(1);
archivo[n_archivo].filename=strdup(f);
archivo[n_archivo].t=sp->st_mtime;
n_archivo++;
}


int compara(const void *a, const void *b)
{
ARCHIVO *m,*n;

m=(ARCHIVO*) a;n=(ARCHIVO *)b;
if(m->t > n->t)
	return 1;
if(m->t < n->t)
	return -1;
return 0;
}

A.6. Capítulo 10

A.6.1. Ejercicio 4

El siguiente listado permite generar la base de datos y el archivo de texto con palabras y definiciones aleatorias. Nótese que es una simple adaptación del listado 1:

#include <gdbm.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void nuevo(int);

GDBM_FILE f;
FILE *fp;
char L[80];

int main(int argc,char **argv)
{
int z,N;

if(argc!=2 || ((N=atoi(argv[1]))<1000))
	fprintf(stderr,"Especifique numero > 1000\n"),exit(1);
f=gdbm_open("aleatorio.data",0,GDBM_WRCREAT,0600,NULL);
if(f==NULL) fprintf(stderr,"Fallo gdbm_open\n"),exit(1);
fp=fopen("aleatorio.txt","w");
if(fp==NULL) fprintf(stderr,"Fallo fopen\n"),exit(1);

for(z=0;z<N;z++)
	if(z==N/2)
		nuevo(1);
	else
		nuevo(0);

gdbm_close(f);
fclose(fp);
return 0;
}

void nuevo(int p)
{
datum palabra,descripcion;
int n,z;

n=5+rand()%10;
for(z=0;z<n;z++) L[z]='a'+rand()%26;
L[z]=0; palabra.dsize=z; palabra.dptr=strdup(L);
fprintf(fp,"%s\n",L);
if(p) printf("%s\n",L);

n=5+rand()%50;
for(z=0;z<n;z++) L[z]='a'+rand()%26;
L[z]=0; descripcion.dsize=z; descripcion.dptr=strdup(L);
fprintf(fp,"%s\n",L);
if(p) printf("%s\n",L);

if(gdbm_store(f,palabra,descripcion,GDBM_REPLACE)!=0)
	printf("Error en gdbm_store\n");
free(palabra.dptr);
free(descripcion.dptr);
}

Como se aprecia, el programa muestra la palabra aleatoria "central" del archivo, lo cual será útil para las pruebas.

El programa mostrado a continuación también es adaptación del listado 1. Permite obtener un registro a partir de la línea de comando:

#include <gdbm.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void consulta(char *);

GDBM_FILE f;
char L[80];

int main(int argc,char **argv)
{
if(argc!=2)
	fprintf(stderr,"Especifique una palabra a buscar\n"),exit(1);
f=gdbm_open("aleatorio.data",0,GDBM_READER,0600,NULL);
if(f==NULL) fprintf(stderr,"Fallo gdbm_open\n"),exit(1);

consulta(argv[1]);
gdbm_close(f);
return 0;
}

void consulta(char *w)
{
datum palabra,descripcion;

palabra.dptr=strdup(w);
palabra.dsize=strlen(w);
descripcion=gdbm_fetch(f,palabra);
if(descripcion.dptr==NULL)
	printf("No se encontró esa palabra\n");
else
	{
	strncpy(L,descripcion.dptr,descripcion.dsize);
	L[descripcion.dsize]=0;
	printf("Descripcion: %s\n",L);
	free(descripcion.dptr);
	}
}

Por último, el programa mostrado a continuación es la versión en archivos planos:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void consulta(char *);

FILE *fp;

int main(int argc,char **argv)
{
if(argc!=2)
	fprintf(stderr,"Especifique una palabra a buscar\n"),exit(1);
fp=fopen("aleatorio.txt","r");
if(fp==NULL) fprintf(stderr,"Fallo fopen\n"),exit(1);

consulta(argv[1]);
fclose(fp);
return 0;
}

void consulta(char *w)
{
static char L[80],D[80];

while(fgets(L,80,fp))
	{
	fgets(D,80,fp);
	L[strlen(L)-1]=0;
	D[strlen(D)-1]=0;
	if(strcmp(L,w)==0)
		{
		printf("Descripcion: %s\n",D);
		return;
		}
	}
printf("No se encontró esa palabra\n");
return;
}

A continuación algunos resultados obtenidos (recortados) en mi computador personal [200] tras la generación de 100000 registros, y tras un reboot para liberar el caché. De los resultados podemos inferir:

  1. El caché mejora la performance significativamente. Esto se aprecia dramáticamente, pues en cada caso la segunda vez que se ejecuta el mismo comando, el tiempo se reduce considerablemente

  2. El tiempo de búsqueda crece linealmente en una búsqueda secuencial (lo que no es sorpresa) y el peor caso se da cuando se busca un elemento no existente

  3. El tiempo de búsqueda se mantiene casi constante en las bases Gdbm para una gran variación de "N", cosa que es tremendamente importante

  4. Asimismo, en Gdbm, la búsqueda de un elemento no existente toma un tiempo similar a la búsqueda de uno existente

  5. Para N=100000 Gdbm en promedio es unas 9 veces más veloz que una búsqueda lineal, y esto sigue mejorando conforme se incrementa N. En una prueba con N=1000000, el factor fue alrededor de 90

  6. La información en Gdm ocupa poco más del doble de espacio que la información en archivo de texto

Note que no se ha analizado aquí el tema del tiempo que toma la escritura, lo cual puede ser muy importante en muchas aplicaciones.

$ time ./solucion4-dbm dcpimlieyrxq
Descripcion: dberojbqwkfrykxq
real    0m0.057s
$ time ./solucion4-dbm dcpimlieyrxq
Descripcion: dberojbqwkfrykxq
real    0m0.003s
$ time ./solucion4-dbm dcpimlieyrx8
No se encontróa palabra
real    0m0.012s
$ time ./solucion4-dbm dcpimlieyrx8
No se encontróa palabra
real    0m0.003s

$ time ./solucion4-txt dcpimlieyrxq
Descripcion: dberojbqwkfrykxq
real    0m0.129s
$ time ./solucion4-txt dcpimlieyrxq
Descripcion: dberojbqwkfrykxq
real    0m0.036s
$ time ./solucion4-txt dcpimlieyrx9
No se encontróa palabra
real    0m0.113s
$ time ./solucion4-txt dcpimlieyrx9
No se encontróa palabra
real    0m0.071s
$ ls -l
total 12372
-rw-------  1 alfa  alfa  9109522 ene 11 17:29 aleatorio.data
-rw-r--r--  1 alfa  alfa  4101808 ene 11 17:29 aleatorio.txt
...

A.7. Capítulo 11

A.7.1. Ejercicios 1 y 2

#include <ncurses.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>

void nuevo_obstaculo(void);
void scroll_down(void);
void clear_screen(void);
void draw_screen(void);
void set(int y, int x, chtype c);
void pinta_carrito(void);
void borra_carrito(void);
void carrera(void);
void pset(int y, int x, chtype c);

// Zona oculta para crear obstaculos
#define GAP 5

chtype **pantalla;
int max_y, max_x;
int car_y, car_x;
int crash = 0;
int v1, v2, v3, v4, v5;

int main()
{
    int z;
    time_t inicio, final;

    srand(time(NULL));
    initscr();
    cbreak();
    nodelay(stdscr, TRUE);
    keypad(stdscr, TRUE);
    noecho();
    start_color();
    init_pair(1, COLOR_GREEN, COLOR_BLACK);
    init_pair(2, COLOR_YELLOW, COLOR_BLACK);
    init_pair(3, COLOR_BLUE, COLOR_BLACK);
    init_pair(4, COLOR_CYAN, COLOR_BLACK);
    init_pair(5, COLOR_RED, COLOR_BLACK);
    v1 = COLOR_PAIR(1);
    v2 = COLOR_PAIR(2);
    v3 = COLOR_PAIR(3);
    v4 = COLOR_PAIR(4);
    v5 = COLOR_PAIR(5);

/* Encontrar dimensiones de pantalla */
    getmaxyx(stdscr, max_y, max_x);

/* Nuestra pantalla sera de max_y+GAP */
    max_y += GAP;
    pantalla = calloc(max_y, sizeof(chtype *));
    for (z = 0; z < max_y; z++)
	pantalla[z] = calloc(max_x, sizeof(chtype));

// Pantalla limpia y carrito al medio
    clear_screen();
    car_x = max_x / 2;
    car_y = max_y - 4;
// La carrera empieza
    time(&inicio);
    carrera();
// Fin de la carrera
    draw_screen();
    refresh();
    time(&final);
    sleep(1);
    endwin();
// Score:
    printf("Duracion de la carrera: %ld seg\n", (long) (final - inicio));
    exit(0);
}

void carrera(void)
{
    int c;
    long delay = 30000;
    int aux = 0;

    for (;;) {
	nuevo_obstaculo();
	if (aux == 0)
	    scroll_down();
	if (crash)
	    return;
	aux = (aux + 1) % 8;
	draw_screen();
	refresh();
	c = getch();
	borra_carrito();
	switch (c) {
	case KEY_LEFT:
	    if (car_x > 2)
		car_x--;
	    break;
	case KEY_RIGHT:
	    if (car_x < max_x - 3)
		car_x++;
	    break;
	case 'q':
	    return;
	}
	pinta_carrito();
	if (crash)
	    return;
	usleep(delay);
	delay -= 10;
	if (delay < 10000)
	    delay = 10000;
    }
}

void clear_screen(void)
{
    int z, j;

    for (z = 0; z < max_y; z++)
	for (j = 0; j < max_x; j++)
	    pantalla[z][j] = ' ';
}

void draw_screen(void)
{
    int z, j;

    for (z = GAP; z < max_y - 1; z++)
	for (j = 0; j < max_x; j++)
	    mvaddch(z - GAP, j, pantalla[z][j]);
}

void scroll_down(void)
{
    int z, j;

    borra_carrito();
    for (z = max_y - 1; z >= 1; z--)
	for (j = 0; j < max_x; j++)
	    pantalla[z][j] = pantalla[z - 1][j];
    for (j = 0; j < max_x; j++)
	pantalla[0][j] = ' ';
    pinta_carrito();
}

void nuevo_obstaculo(void)
{
    int x = rand() % max_x;
    int j;

    if (rand() % 18 == 0)
	switch (rand() % 10) {
	case 0:
	case 1:
	case 2:
	case 3:
	case 4:
	case 5:
	    // Arbolito
	    //   *
	    //  *** 
	    // *****
	    //   |
	    pset(0, x, '*' | v1);
	    pset(1, x, '*' | v1);
	    pset(1, x - 1, '*' | v1);
	    pset(1, x + 1, '*' | v1);
	    pset(2, x, '*' | v1);
	    pset(2, x - 1, '*' | v1);
	    pset(2, x + 1, '*' | v1);
	    pset(2, x - 2, '*' | v1);
	    pset(2, x + 2, '*' | v1);
	    pset(3, x, '|' | v2);
	    break;
	case 6:
	    // 
	    for (j = x - 3; j <= x + 3; j++)
		pset(0, j, '#' | v4);
	    pset(0, x - 11, '#');
	    pset(0, x + 11, '#');
	    for (j = x - 2; j <= x + 2; j++)
		pset(1, j, '#' | v4);
	    pset(1, x - 10, '#');
	    pset(1, x + 10, '#');
	    for (j = x - 1; j <= x + 1; j++)
		pset(2, j, '#' | v4);
	    pset(2, x - 9, '#');
	    pset(2, x + 9, '#');
	    pset(3, x, 'Y' | v4);
	    pset(3, x - 8, '#');
	    pset(3, x + 8, '#');
	    pset(4, x - 7, 'O');
	    pset(4, x + 7, 'O');
	    pset(4, x - 8, '-');
	    pset(4, x + 8, '-');
	    pset(4, x - 9, '-');
	    pset(4, x + 9, '-');
	    break;
	case 7:
	    // Piedritas (.)
	    for (j = 0; j < rand() % 20; j++)
		pset(rand() % 4, x - 3 + rand() % 7, '.' | v2);
	    break;
	case 8:
	    // Una iglesia
	    //   +
	    //  OOO 
	    // OOOOOOO
	    // OO OOOO
	    pset(0, x, '+' | v2);
	    pset(1, x, 'O' | v4);
	    pset(1, x - 1, 'O' | v4);
	    pset(1, x + 1, 'O' | v4);
	    pset(2, x, 'O' | v4);
	    pset(2, x - 1, 'O' | v4);
	    pset(2, x + 1, 'O' | v4);
	    pset(2, x - 2, 'O' | v4);
	    pset(2, x + 2, 'O' | v4);
	    pset(2, x + 3, 'O' | v4);
	    pset(2, x + 4, 'O' | v4);
	    pset(3, x, ' ');
	    pset(3, x - 1, 'O' | v4);
	    pset(3, x + 1, 'O' | v4);
	    pset(3, x - 2, 'O' | v4);
	    pset(3, x + 2, 'O' | v4);
	    pset(3, x + 3, 'O' | v4);
	    pset(3, x + 4, 'O' | v4);
	    break;
	case 9:
	    // Un puente    |     |
	    // +++++++++++++|     |+++++++++++++
	    // +++++++++++++|     |+++++++++++++
	    //              |     |
	    for (j = x + 3; j < x + 12; j++) {
		pset(1, j, '_' | v3);
		pset(2, j, '_' | v3);
	    }
	    for (j = x - 3; j > x - 12; j--) {
		pset(1, j, '_' | v3);
		pset(2, j, '_' | v3);
	    }
	    pset(0, x + 2, '|' | v2);
	    pset(0, x - 2, '|' | v2);
	    pset(1, x + 2, '|' | v2);
	    pset(1, x - 2, '|' | v2);
	    pset(2, x + 2, '|' | v2);
	    pset(2, x - 2, '|' | v2);
	    pset(3, x + 2, '|' | v2);
	    pset(3, x - 2, '|' | v2);
	    pset(4, x + 2, '|' | v2);
	    pset(4, x - 2, '|' | v2);
	    break;
	}
}

void borra_carrito(void)
{
    pset(car_y, car_x, ' ');
    pset(car_y + 1, car_x, ' ');
    pset(car_y + 1, car_x + 1, ' ');
    pset(car_y + 1, car_x - 1, ' ');
    pset(car_y + 2, car_x, ' ');
    pset(car_y + 2, car_x + 1, ' ');
    pset(car_y + 2, car_x - 1, ' ');
}

void pinta_carrito(void)
{
    set(car_y, car_x, '^');
    set(car_y + 1, car_x, '#');
    set(car_y + 1, car_x + 1, 'H');
    set(car_y + 1, car_x - 1, 'H');
    set(car_y + 2, car_x, '#');
    set(car_y + 2, car_x + 1, 'H');
    set(car_y + 2, car_x - 1, 'H');
}

void pset(int y, int x, chtype c)
{
    if (x >= 0 && x < max_x && y >= 0 && y < max_y)
	pantalla[y][x] = c;
}

void set(int y, int x, chtype c)
{
    if (x < 0 || x >= max_x || y < 0 || y >= max_y)
	return;
    if (pantalla[y][x] != ' ') {
	pset(y, x, '*' | v2);
	crash = 1;
    } else
	pantalla[y][x] = c;
}

A.8. Capítulo 12

A.8.1. Ejercicio 2

El siguiente programa implementa el algoritmo de coloreado sugerido en el texto. El fondo de la pantalla se cambia a color negro.

/*
 * mandelbrot.c: Fractales de Mandelbrot con colores
 */
#include "sdl_aux.h"
#include <math.h>

typedef struct {
    double x, y;
} COMPLEJO;

COMPLEJO suma(COMPLEJO a, COMPLEJO b)
{
    COMPLEJO r;
    r.x = a.x + b.x;
    r.y = a.y + b.y;
    return r;
}

COMPLEJO multiplica(COMPLEJO a, COMPLEJO b)
{
    COMPLEJO r;
    r.x = a.x * b.x - a.y * b.y;
    r.y = a.x * b.y + a.y * b.x;
    return r;
}

double norma(COMPLEJO a)
{
    return sqrt(a.x * a.x);
}

COMPLEJO operacion(COMPLEJO a)
{
    COMPLEJO c = { -0.53, 0.53 };
    return suma(multiplica(a, a), c);
}

int pintable(double x, double y, double *ultima_norma)
{
    COMPLEJO a = { x, y };
    double norma_maxima = norma(a) * 50;

    int z;
    for (z = 0; z < 50; z++) {
	a = operacion(a);
	if (norma(a) > norma_maxima)
	    return 0;
    }
    *ultima_norma = norma(a);
    return 1;
}

int main(int argc, char *argv[])
{
/* Modificar estas variables para nuevas figuras */
    double x1 = 0, x2 = 1.7, y1 = -1, y2 = 1;
    int n = 1000;

/* Variables auxiliares */
    double delta = (x2 - x1) / n;
    int m = (y2 - y1) / delta;
    double x, y;
    int z, j, px, py;
    double ultima_norma;

/* SDL */
    SDL_Surface *s;
    Uint32 negro;

    s = inicializa();
    negro = SDL_MapRGB(s->format, 0x0, 0x0, 0x0);
    SDL_FillRect(s, NULL, negro);

    for (z = 0; z < m; z++) {
	if (SDL_MUSTLOCK(s))
	    SDL_LockSurface(s);
	for (j = 0; j < n; j++) {
	    x = x1 + j * delta;
	    y = y1 + z * delta;
	    if (pintable(x, y, &ultima_norma)) {
		px = 800 * j / n;
		py = 600 * z / m;
		putpixel(s, px, py, getColorSpectrum(s, ultima_norma));
	    }
	}
	if (SDL_MUSTLOCK(s))
	    SDL_UnlockSurface(s);
	if (z % 10 == 0)
	    SDL_UpdateRect(s, 0, 0, 0, 0);
    }
    SDL_UpdateRect(s, 0, 0, 0, 0);
    SDL_Delay(5000);		/* 5 segundos para admirar pantalla */
    return 0;
}

A.9. Capítulo 14

A.9.1. Ejercicio 1

Tal como se aprecia, la mayor parte corresponde a cerrar descriptores inútiles. Asimismo, hemos aprovechado al shell para lanzar "wc" con relativa comodidad y generalidad.

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

int main()
{
    int i, fd_envio[2], fd_recepcion[2];
    char TEXTO[256];
    char RESPUESTA[256];

    printf("Escriba un texto > ");
    fgets(TEXTO, 256, stdin);

    if (pipe(fd_envio) == -1 || pipe(fd_recepcion) == -1) {
	fprintf(stderr, "Error en pipe()\n");
	return 1;
    }
    i = fork();
    if (i > 0) {
	int n;
	close(fd_envio[0]);
	close(fd_recepcion[1]);
	write(fd_envio[1], TEXTO, strlen(TEXTO));
	close(fd_envio[1]);
	n = read(fd_recepcion[0], RESPUESTA, 256);
	if (n > 0)
	    printf("Respuesta:%.*s", n, RESPUESTA);
	else
	    printf("Error en lectura de respuesta\n");
	close(fd_recepcion[0]);
	return 0;
    } else {
	close(fd_envio[1]);
	close(fd_recepcion[0]);
	close(0);
	close(1);
	dup2(fd_envio[0], 0);
	dup2(fd_recepcion[1], 1);
	close(fd_envio[0]);
	close(fd_recepcion[1]);
	execl("/bin/sh", "sh", "-c", "wc", NULL);
	return 1;
    }
}

A.10. Capítulo 15

A.10.1. Ejercicio 1

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include "x_sem.h"

#define LLAVE_SEMAFORO_1 0x003333
#define LLAVE_SEMAFORO_2 0x003334

void transfiere(const char *origen, const char *destino,
		int sem_org, int sem_dst);
static char L[64];
static int sem1, sem2;

int main()
{
    FILE *fp;
    int p1, p2, v1, v2;

    fp = fopen("arch1", "w");
    fprintf(fp, "5000");
    fclose(fp);

    fp = fopen("arch2", "w");
    fprintf(fp, "5000");
    fclose(fp);

    sem1 = x_sem(LLAVE_SEMAFORO_1);
    sem2 = x_sem(LLAVE_SEMAFORO_2);
    p1 = fork();
    if (p1 == 0)
	transfiere("arch1", "arch2", sem1, sem2);
    p2 = fork();
    if (p2 == 0)
	transfiere("arch2", "arch1", sem2, sem1);
    for (;;) {
	x_semop(sem1, -1);
	x_semop(sem2, -1);

	fp = fopen("arch1", "r");
	fgets(L, 64, fp);
	fclose(fp);
	v1 = atoi(L);
	fp = fopen("arch2", "r");
	fgets(L, 64, fp);
	fclose(fp);
	v2 = atoi(L);
	printf("%d + %d -> %d\n", v1, v2, v1 + v2);
	x_semop(sem2, +1);
	x_semop(sem1, +1);
	sleep(1);
    }
}

void transfiere(const char *origen, const char *destino, int sema,
		int semb)
{
    FILE *fp;
    int v;

    for (;;) {
	x_semop(sema, -1);

	fp = fopen(origen, "r");
	fgets(L, 64, fp);
	fclose(fp);
	v = atoi(L);
	fp = fopen(origen, "w");
	fprintf(fp, "%d\n", v - 1);
	fclose(fp);

	x_semop(sema, +1);
	x_semop(semb, -1);

	fp = fopen(destino, "r");
	fgets(L, 64, fp);
	fclose(fp);
	v = atoi(L);
	fp = fopen(destino, "w");
	fprintf(fp, "%d\n", v + 1);
	fclose(fp);

	x_semop(semb, +1);
    }
}

A.10.2. Ejercicio 2

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include "x_sem.h"

#define LLAVE_SEMAFORO_1 0x003333
#define LLAVE_SEMAFORO_2 0x003334
#define LLAVE_SEMAFORO_3 0x003335
#define LLAVE_SEMAFORO_4 0x003336

void transfiere(const char *origen, const char *destino,
		int sem_org, int sem_dst, int sem_rep);
static char L[64];
static int sem1, sem2;

int main()
{
    FILE *fp;
    int p1, p2, v1, v2;

    fp = fopen("arch1", "w");
    fprintf(fp, "5000");
    fclose(fp);

    fp = fopen("arch2", "w");
    fprintf(fp, "5000");
    fclose(fp);

    sem1 = x_sem(LLAVE_SEMAFORO_1);
    sem2 = x_sem(LLAVE_SEMAFORO_2);
    int sem_rep_1 = x_sem(LLAVE_SEMAFORO_3);
    int sem_rep_2 = x_sem(LLAVE_SEMAFORO_4);

    p1 = fork();
    if (p1 == 0)
	transfiere("arch1", "arch2", sem1, sem2, sem_rep_1);
    p2 = fork();
    if (p2 == 0)
	transfiere("arch2", "arch1", sem2, sem1, sem_rep_2);
    for (;;) {
	x_semop(sem_rep_1, -1);
	x_semop(sem_rep_2, -1);
	x_semop(sem1, -1);
	x_semop(sem2, -1);

	fp = fopen("arch1", "r");
	fgets(L, 64, fp);
	fclose(fp);
	v1 = atoi(L);
	fp = fopen("arch2", "r");
	fgets(L, 64, fp);
	fclose(fp);
	v2 = atoi(L);
	printf("%d + %d -> %d\n", v1, v2, v1 + v2);
	x_semop(sem2, +1);
	x_semop(sem1, +1);
	x_semop(sem_rep_2, +1);
	x_semop(sem_rep_1, +1);
	sleep(1);
    }
}

void transfiere(const char *origen, const char *destino,
		int sema, int semb, int sem_rep)
{
    FILE *fp;
    int v;

    for (;;) {
	x_semop(sem_rep, -1);
	x_semop(sema, -1);

	fp = fopen(origen, "r");
	fgets(L, 64, fp);
	fclose(fp);
	v = atoi(L);
	fp = fopen(origen, "w");
	fprintf(fp, "%d\n", v - 1);
	fclose(fp);

	x_semop(sema, +1);
	x_semop(semb, -1);

	fp = fopen(destino, "r");
	fgets(L, 64, fp);
	fclose(fp);
	v = atoi(L);
	fp = fopen(destino, "w");
	fprintf(fp, "%d\n", v + 1);
	fclose(fp);

	x_semop(semb, +1);
	x_semop(sem_rep, +1);
    }
}

A.11. Capítulo 16

A.11.1. Ejercicio 3

Programa Creador:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "x_shm.h"

#define ANCHO 70
#define ALTO 20
#define PELOTAS_KEY 0x666999
#define SEMAFOROS_KEY 0x666998

#define PERMISOS (SHM_R|SHM_W)

char campo[ALTO][ANCHO] = {
    "####################",
    "#   ##   #   #   ###",
    "# # ## # # # # # ###",
    "#  s  s s s s s  ###",
    "### ## # # # # #####",
    "###    #   #   #####",
    "####################"
};

int main(void)
{
    int id_sem, z, j, n_sem = 0;
    char *ptr_campo, *ptr_semaforos;

    union semun {
	int val;
	struct semid_ds *buf;
	unsigned short int *array;
    } s;

    struct {
	int n_semaforos;
	int pos[100][2];
    } mem_sem;

    ptr_campo = x_shm(PELOTAS_KEY, ALTO * ANCHO);
/* Inicializar conjunto de semaforos */
    ptr_semaforos = x_shm(SEMAFOROS_KEY, sizeof(mem_sem));
/* Inicializar conjunto de semaforos */
    for (z = 0; z < ALTO; z++)
	for (j = 0; j < ANCHO; j++)
	    if (campo[z][j] == 's') {
		mem_sem.pos[n_sem][1] = j;
		mem_sem.pos[n_sem][0] = z;
		campo[z][j] = ' ';
		n_sem++;
	    }
    mem_sem.n_semaforos = n_sem;

/* Copiar a memoria compartida */
    memcpy(ptr_campo, campo, ANCHO * ALTO);
    memcpy(ptr_semaforos, &mem_sem, sizeof(mem_sem));

/* Inicializar conjunto de semaforos */
    id_sem = semget(PELOTAS_KEY, n_sem, IPC_CREAT | PERMISOS);
    if (id_sem == -1) {
	perror("pelotas semget");
	exit(-1);
    }
    for (z = 0; z < n_sem; z++) {
	s.val = 1;
	if (semctl(id_sem, z, SETVAL, s) == -1) {
	    perror("pelotas semctl");
	    exit(-1);
	}
    }
    return 0;
}

Programa Jugador:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include "x_shm.h"

#define ANCHO 70
#define ALTO 20
#define PELOTAS_KEY 0x666999

extern void mueve(void);
void libera_semaforo(void);
void obtiene_semaforo(void);

char *campo;
int x, y, v, n;
int *p;
int plantar;
int id_sem;
int tiene_semaforo = -1;

int cual_semaforo(void)
{
    int z;
    for (z = 0; z < n; z++)
	if (*(p + 1 + 2 * z) == x && *(p + 1 + 2 * z + 1) == y)
	    return z;
    return -1;
}

char getxy(int x, int y)
{
    char c = campo[x + ANCHO * y];
    if (c == 0)
	return ' ';
    return c;
}

void set(int x, int y, char p)
{
    campo[x + ANCHO * y] = p;
}

int puede(int x, int y)
{
    if (getxy(x, y) == '#')
	return 0;
    if (getxy(x, y) == '*')
	plantar = 1;
    if (tiene_semaforo != -1)
	libera_semaforo();
    if (getxy(x, y) == 's')
	obtiene_semaforo();
    return 1;
}

void obtiene_semaforo(void)
{
    tiene_semaforo = 0;
    struct sembuf sops[1];

    tiene_semaforo = cual_semaforo();
    sops[0].sem_num = tiene_semaforo;
    sops[0].sem_op = -1;
    sops[0].sem_flg = SEM_UNDO;
    semop(id_sem, sops, 1);
}


void libera_semaforo(void)
{
    struct sembuf sops[1];
    sops[0].sem_num = tiene_semaforo;
    sops[0].sem_op = +1;
    sops[0].sem_flg = SEM_UNDO;
    semop(id_sem, sops, 1);
    tiene_semaforo = -1;
}

void pinta(void)
{
    int z, j;

    for (z = 0; z < ALTO; z++) {
	for (j = 0; j < ANCHO; j++)
	    printf("%c", getxy(j, z));
	printf("\n");
    }
}

int main(void)
{
    int factor;

    campo = x_shm(PELOTAS_KEY, 0);

    id_sem = semget(PELOTAS_KEY, 0, 0);
    if (id_sem == -1) {
	perror("pelotas semget");
	exit(-1);
    }

    p = (int *) (campo + ALTO * ANCHO);
    n = *p;

    x = 1, y = 1, v = 2;
    if (getxy(x, y) == '#')
	return 1;
    srand(time(NULL));
    factor = 3 + rand() % 10;
    for (;;) {
	int xp, yp;

	xp = x;
	yp = y;
	plantar = 0;
	mueve();
	if (!plantar) {
	    set(xp, yp, ' ');
	    set(x, y, '*');
	} else {
	    x = xp;
	    y = yp;
	}
	system("clear");
	pinta();
	usleep(factor * 1e4);
    }

    return 0;
}

A.12. Capítulo 17

A.12.1. Ejercicio 4-A y 4-B

Servidor de Chat
/*
 * Solucion: servidor chat
 */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include "tcpaux.h"

#define MAX_FD 100

static void actualiza_listado(void);
static void procesa_respuesta(int fd);
static void broadcast(char *msg);

int s, maxfd;
char *concursante[MAX_FD];
char listado[LEN];
char buffer[LEN];
int respuesta;
fd_set ifds, testds;

int main(int argc, char **argv)
{
    int n, z;
    CONEXION *cliente;

    s = net_listen_port(5678);
    maxfd = s;
    FD_ZERO(&ifds);
    FD_SET(s, &ifds);

    actualiza_listado();

    for (;;) {
	testds = ifds;
	n = select(maxfd + 1, &testds, NULL, NULL, NULL);
	if (n == -1)
	    continue;
	for (z = 0; z <= maxfd; z++)
	    if (FD_ISSET(z, &testds)) {
		if (z == s) {
		    int f;

		    cliente = net_accept(s);
		    f = cliente->fd;
		    FD_SET(f, &ifds);
		    if (f > maxfd)
			maxfd = f;
		    free(cliente);
		    net_read(f, buffer, LEN);
		    concursante[f] = strdup(buffer);
		    sprintf(buffer,
			    "MNuevo participante: %s", concursante[f]);
		    broadcast(buffer);
		    actualiza_listado();
		} else
		    procesa_respuesta(z);
	    }
    }
}

static void actualiza_listado(void)
{
    int z;

    strcpy(listado, "L");
    for (z = 0; z <= maxfd; z++)
	if (FD_ISSET(z, &ifds) && z != s) {
	    strcat(listado, concursante[z]);
	    strcat(listado, " ");
	}
    broadcast(listado);
}

static void procesa_respuesta(int fd)
{
    char ans[LEN];
    int z;

    z = net_read(fd, ans, LEN);
    if (z == 0) {
	sprintf(buffer, "MSe desconecta %s", concursante[fd]);
	free(concursante[fd]);
	FD_CLR(fd, &ifds);
	close(fd);
	broadcast(buffer);
	actualiza_listado();
	return;
    }
    sprintf(buffer, "[%s]: %s", concursante[fd], ans);
    broadcast(buffer);
}

static void broadcast(char *msg)
{
    int z;

    for (z = 0; z <= maxfd; z++)
	if (FD_ISSET(z, &ifds) && z != s)
	    net_write(z, msg, LEN);
}
Cliente de Chat
/*
 * Solucion: Cliente chat
 */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/select.h>
#include "tcpaux.h"

fd_set ifds, testds;
int s;

int main(int argc, char **argv)
{
    int n, z;
    char txt[LEN];
    fd_set testds, ifds;

    if (argc != 3) {
	fprintf(stderr, "Especifique server y vuestro alias\n");
	exit(1);
    }
    if ((s = connect_by_port(argv[1], 5678)) == -1)
	exit(1);
    sprintf(txt, "%s", argv[2]);
    net_write(s, txt, LEN);

    FD_ZERO(&ifds);
    FD_SET(0, &ifds);
    FD_SET(s, &ifds);

    for (;;) {
	testds = ifds;
	n = select(s + 1, &testds, NULL, NULL, NULL);
	if (n < 1)
	    exit(1);
	if (FD_ISSET(0, &testds)) {
	    fgets(txt, LEN, stdin);
	    txt[strlen(txt) - 1] = '\0';
	    net_write(s, txt, LEN);
	}
	if (FD_ISSET(s, &testds)) {
	    z = net_read(s, txt, LEN);
	    if (z < 1) {
		printf("El Servidor ha terminado\n");
		exit(0);
	    }
	    switch (txt[0]) {
	    case 'M':
		printf("MENSAJE DEL SERVER: %s\n", txt + 1);
		break;
	    case 'L':
		printf("NUEVA LISTA DE USUARIOS: %s\n", txt + 1);
		break;
	    case '[':
		printf("CONVERSACION: %s\n", txt);
		break;
	    }
	}
    }
}

A.12.2. Ejercicio 4-C

Cliente de Chat mejorado con Curses
/*
 * Solucion: Cliente chat con curses
 */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/select.h>
#include <ncurses.h>

#define LEN 256
#define LIN 10

int connect_by_port(const char *hostname, int port);
int net_read(int fd, char *buf, int count);
int net_write(int fd, const char *buf, int count);

char listado[LEN];
char conversacion[LIN][LEN];
fd_set ifds, testds;
int s;

int main(int argc, char **argv)
{
    int n, z, c, offset = 0;
    char txt[LEN];
    char input[LEN] = "";
    fd_set testds, ifds;

    if (argc != 3) {
	fprintf(stderr, "Especifique server y vuestro alias\n");
	exit(1);
    }
    for (z = 0; z < LIN; z++)
	strcpy(conversacion[z], "");

    if ((s = connect_by_port(argv[1], 5678)) == -1)
	exit(1);
    sprintf(txt, "%s", argv[2]);
    net_write(s, txt, LEN);

    initscr();
    noecho();

    FD_ZERO(&ifds);
    FD_SET(0, &ifds);
    FD_SET(s, &ifds);

    for (;;) {
	clear();
	printw("USUARIOS CONECTADOS:\n%s\n\n", listado);
	printw("CONVERSACION:\n");
	for (z = 0; z < LIN; z++)
	    printw("  || %s\n", conversacion[z]);
	printw("\n>>> ");
	printw(input);

	testds = ifds;
	refresh();
	n = select(s + 1, &testds, NULL, NULL, NULL);
	if (n < 1) {
	    endwin();
	    exit(1);
	}
	if (FD_ISSET(0, &testds)) {
	    c = getch();
	    if (c == '\n' || c == KEY_ENTER) {
		net_write(s, input, LEN);
		offset = 0;
		input[offset] = '\0';
	    } else
		// La tecla backspace siempre dio problemas
	    if (c == '\b' || c == KEY_BACKSPACE || c == 127) {
		if (offset > 0)
		    input[--offset] = '\0';
	    } else {
		input[offset++] = c;
		input[offset] = '\0';
	    }
	}
	if (FD_ISSET(s, &testds)) {
	    z = net_read(s, txt, LEN);
	    if (z < 1) {
		endwin();
		printf("El Servidor ha terminado\n");
		exit(0);
	    }
	    switch (txt[0]) {
	    case 'L':
		strcpy(listado, txt + 1);
		break;
	    case 'M':
		txt[0] = '*';
	    case '[':
		for (z = 1; z < LIN; z++)
		    strcpy(conversacion[z - 1], conversacion[z]);
		strcpy(conversacion[LIN - 1], txt);
		break;
	    }
	}
    }
}

A.13. Capítulo 19

A.13.1. Ejercicio 1

Basta con modificar la definición de la expresión regular para NUM de:

NUM    [0-9]+

a:

NUM    [0-9]+|[0-9]*"."[0-9]+

A.14. Capítulo 20

A.14.1. Ejercicio 2

Sólo mostramos el archivo de gramática:

%{
#include "basic.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
%}
%union {
	int entero;
	double val;
	char *cadena;
	}

%token <val> NUMERO
%token <val> PRINT GOTO CLS INPUT GOSUB RETURN END IF THEN
%token <val> SAVE LIST RUN NEW LOAD SYSTEM SHELL FILES
%token <val> LEFT RIGHT MID CHR ASC STR VAL
%token <val> SIN COS TAN SQRT EXP
%token <val> AND OR NOT
%token <cadena> STRING IDENTIF
%type <cadena> p_expr s_expr str_item
%type <val> expr
%type <entero> l_expr

%right NOT
%left AND OR
%left ',' ';'
%left '+' '-'
%left '*' '/'
%left NEGATIVO
%right '^'
%%
todo:	instr
	|
	todo ':' instr
	|
	todo ':' if_ins instr
	|
	if_ins instr
	;
if_ins: IF l_expr THEN  { if(!$2) return 1; }
	;
instr:  PRINT		{ printf("\n"); }
	|
	PRINT p_expr	{ printf("%s\n",$2); free($2); }
	|
	PRINT p_expr ',' { printf("%s          ",$2); free($2); }
	|
	PRINT p_expr ';' { printf("%s",$2); free($2); }
	|
	RETURN		{ current_line=line_stack[--line_stack_index];
			  return 1;  }
	|
	GOTO NUMERO	{ current_line = $2-1; return 1; }
	|
	GOSUB NUMERO	{ line_stack[line_stack_index++]=current_line;
			  current_line = $2-1; return 1; }
	|
	END		{ status_flag=1; return 1; }
	|
	CLS		{ system("clear"); }
	|
	INPUT STRING ',' IDENTIF { printf("%s ",$2); free($2);
				read_set($4); }
	|
	INPUT IDENTIF	{ printf("? "); read_set($2); }
	|
	INPUT STRING ',' IDENTIF '$' { printf("%s ",$2); free($2);
				s_read_set($4); }
	|
	INPUT IDENTIF '$'	{ printf("? "); s_read_set($2); }
	|
	IDENTIF '=' expr  { variable_set($1,$3); }
	|
	IDENTIF '$' '=' s_expr  { s_variable_set($1,$4);  }
	;
p_expr: expr		{ ptr_set_number(&$$,$1); }
	|
	s_expr
	|
	l_expr		{ if($1) $$=strdup("-1"); else $$=strdup("0"); }
	|
	p_expr ',' p_expr { ptr_add_text(&$$,strdup(" "));
			    ptr_add_text(&$$,$3); }
	|
	p_expr ';' p_expr { ptr_add_text(&$$,$3); }
	;
l_expr: expr '='  expr { if(fabs($1-$3)<1e-6) $$=-1; else $$=0; }
	|
	expr '<' expr { if($1<$3) $$=-1; else $$=0; }
	|
	expr '<' '=' expr { if($1<$4) $$=-1; else $$=0; }
	|
	expr '>' expr { if($1>$3) $$=-1; else $$=0; }
	|
	expr '>' '=' expr { if($1>$4) $$=-1; else $$=0; }
	|
	expr '<' '>' expr { if(fabs($1-$4)>=1e-6) $$=-1; else $$=0; }
	|
	expr '>' '<' expr { if(fabs($1-$4)>=1e-6) $$=-1; else $$=0; }
	|
	'(' l_expr ')' { $$=$2; }
	|
	l_expr AND l_expr { $$=$1&$3; }
	|
	l_expr OR l_expr { $$=$1|$3; }
	|
	NOT l_expr { if($2) $$=0; else $$=-1; }
	;
s_expr: str_item
	|
	s_expr '+' str_item { ptr_add_text(&$$,$3); }
	;
str_item: STRING
	|
	IDENTIF '$'	{ $$ = strdup(s_variable_get($1)); }
	|
	STR '$' '(' expr ')' { ptr_set_number(&$$,$4); }
	|
	LEFT '$' '(' s_expr ',' expr ')' { $$ = calloc($6+1,sizeof(char));
					   strncpy($$,$4,$6);
					   free($4); }
	|
	RIGHT '$' '(' s_expr ',' expr ')' { int l=$6;
					   if(l>strlen($4)) l=strlen($4);
					   $$ = calloc(l+1,sizeof(char));
					   strncpy($$,$4+strlen($4)-l,l);
					   free($4); }
	|
	MID '$' '(' s_expr ',' expr ',' expr ')' { 
					$6--;
					$$ = calloc($8+1,sizeof(char));
					if($6<strlen($4))
						strncpy($$,$4+(int)$6,$8);
					free($4); }
	|
	CHR '$' '(' expr ')' { $$=calloc(2,sizeof(char)); $$[0]=$4; }
	;
expr:	NUMERO        { $$ = $1 ; }
	|
	IDENTIF       { $$ = variable_get($1); }
	|
	expr '+' expr { $$ = $1 + $3 ; }
	|
	expr '-' expr { $$ = $1 - $3 ; }
	|
	expr '*' expr { $$ = $1 * $3 ; }
	|
	expr '/' expr { $$ = $1 / $3 ; }
	|
	expr '^' expr { $$ = pow($1,$3) ; }
	|
	'(' expr ')' { $$ = $2 ; }
	|
	'-' expr %prec NEGATIVO { $$ = - $2 ; }
	|
	VAL '(' s_expr ')' { $$ = atof($3); free($3); }
	|
	ASC '(' s_expr ')' { $$ = $3[0]; free($3); }
	|
	SIN '(' expr ')' { $$ = sin($3); }
	|
	COS '(' expr ')' { $$ = cos($3); }
	|
	TAN '(' expr ')' { $$ = tan($3); }
	|
	SQRT '(' expr ')' { $$ = sqrt($3); }
	|
	EXP '(' expr ')' { $$ = exp($3); }
	;
%%

int yyerror(const char *s)
{
printf("Error de linaxis en linea %d\n",current_line);
status_flag=1;
return 1;
}

1. Kerrisk, M. (2010). The Linux programming interface: a Linux and UNIX system programming handbook. No Starch Press.
2. Stevens, W. R., & Rago, S. A. (2013). Advanced Programming in the UNIX Environment. Addison-Wesley.
3. No confundir con RedHat Enterprise, que es un producto posterior.
4. El kernel está programado casi exclusivamente en lenguaje C. Según un conocido estudio (More than a Gigabuck: Estimating GNU/Linux’s Size by David A. Wheeler, http://www.dwheeler.com/sloc) más del 70% del código fuente de todas las aplicaciones de un sistema Linux típico corresponde a Lenguaje C.
5. Sobre cuál es el "mejor lenguaje" ante una situación dada, existen al menos dos enfoques. El primero afirma que un buen programador (con un buen diseño) es capaz de hacer buenos programas para resolver cualquier tipo de problema con cualquier lenguaje, aunque en algunos casos pueden ocurrir diferencias significativas en el tiempo requerido para la codificación; por ejemplo, hay organizaciones que elaboran actualmente -con mucho éxito- portales Web usando Lisp, un lenguaje cuyo propósito teórico original fue la manipulación de fórmulas simbólicas y fue empleado fundamentalmente por investigadores de inteligencia artificial. El segundo enfoque señala que cada lenguaje debe ser usado sólo en el ámbito para el cual fue creado, al poseer bondades y limitaciones esenciales; por ejemplo, SQL no estaría indicado para programar un driver de comunicaciones. La verdad (como casi siempre) está en un punto intermedio de ambos enfoques, o mejor, en la adecuada sinergía de los mismos.
6. Esta información fue extraida de la documentación de gcc.
7. Este enfoque crea mucha incomodidad a los principiantes, pero es ventajoso en tanto se simplifica el lenguaje, se hace más modular y más independiente de la plataforma.
8. Este enfoque de "caja negra" muchas veces es insuficiente, por lo que muchas veces es necesario conocer conceptos, interdependencias y efectos colaterales de la implementación de la librería.
9. En los sistemas Linux, la librería estándar se implementa como parte de una librería más grande llamada GNU libc o glibc. No debe confundirse con otra gran librería llamada glib.
10. En los sistemas Linux/Unix la mayoría de estos "headers" se ubican en el directorio /usr/include, aunque algunos dependerán del compilador. Por ejemplo, en el sistema operativo que estoy usando en este momento, gcc emplea adicionalmente los headers ubicados en el directorio /usr/lib/gcc-lib/i386-redhat-linux/3.2/include. En programas "simples" el lector no debería tener necesidad de conocer la administración de estas rutas, pero sí puede ser necesario al emplear librerías adicionales.
11. scanf() es una función insegura cuyo uso puede dar lugar a problemas de seguridad. De hecho, este programa adolece de este inconveniente.
12. Cuando la definición de una función está antes que sus invocaciones en un mismo archivo, y no hay otras invocaciones a ella desde otros archivos, entonces la declaración se hace innecesaria y no hay ambigüedad. Sin embargo, aún en ese caso es conveniente escribir la declaración, pues con posteriores modificaciones del programa estas condiciones pueden dejar de ser ciertas.
13. Esta es sin duda la diferencia más significativa entre los dialectos K\&R C y ANSI C.
14. Nótese que el mecanismo de cálculo para números negativos no está bien definido en C89 pero sí en C99.
15. La generación de números aleatorios es un tema bastante sofisticado como para considerarlo aquí. En particular, las rutinas presentadas no se deben utilizar con fines criptográficos. Consúltese la bibliografía especializada para más información.
16. Tanto calloc() como rand() se declaran en stdlib.h.
17. Vea el capítulo 10 para una mejor manera (en términos de performance) de almacenar datos en disco. El "desperdicio" de espacio en el archivo mostrado se puede reducir con una librería de compresión de datos como zlib.
18. En realidad, para muchas aplicaciones esta solución es la más veloz dada su simplicidad (aunque se tenga que desplazar demasiados datos.) Especialmente debido a que memcpy() suele estar programada de un modo muy optimizado (muchas veces en assembler.) El realloc() de "reducción" normalmente no hace nada más que "recordar" que hay más espacio libre para nuevas asignaciones dinámicas: no devuelve realmente la memoria al kernel.
19. El lector puede sentirse decepcionado de usar el modo texto para este tipo de trabajo. Evidentemente lo hacemos porque es lo único que nos permite el C estándar. Vea los capítulos 12 y 13 para algunas ideas acerca de cómo usar modos de alta resolución y colores.
20. rec_completa es un rectificador de onda completa (el clásico puente de cuatro diodos) y rec_media es un rectificador de media onda (un solo diodo.)
21. La documentación menciona la capacidad de compilar programas en C, C++, Objective-C, Ada, Fortran, Java y treelang. Por otro lado, existen otros compiladores alternativos, entre los que destaca el de Intel (comercial) por su aparente superioridad en la performance del código generado.
22. El comando "cc" también se proporciona en el sistema Linux pero sólo invoca a gcc.
23. Posiblemente el compilador lo generó, pero lo eliminó automáticamente al no ser solicitado explícitamente en la línea de comando.
24. Otra forma de probar esta aseveración es compilar sin enlazar (opción -c.) Verá que no ocurre ningún error.
25. Las macros empleadas para el cambio de título realizan un "artificio" que evita tener que introducir explícitamente las comillas mediante -DTITULO="…​".
26. En ciertos casos la optimización puede orientarse a consumir menos memoria, a compilar más velozmente, a generar ejecutables pequeños, etc.
27. La razón de esto es evitar que el compilador "haga trampa". Si éste conociera el valor inicial de nuestros cálculos, podría él mismo realizar los cálculos programados y así generaría un programa (increiblemente optimizado) que contenga la respuesta final desde el principio.
28. Para ser más estrictos, deberíamos efectuar tests mucho más largos y en repetidas oportunidades a fin de promediarlos y eliminar casos excepcionales.
29. En muchas versiones de Unix, se proporciona el comando lint que realiza una función análoga a la opción "-Wall" pero sin compilar.
30. La opción -ansi es equivalente a -std=c89.
31. Algunos detalles sólo aplican a la versión GNU Make (se indica en cada caso), aunque los conceptos son aplicables a todas las versiones de Make.
32. Si Ud. hubiera usado un nombre distinto a "Makefile" o "makefile", tendría que invocar a make usando algo como make -f archivo.
33. Realmente se aplica esta regla porque make se da cuenta de que existen los archivos fuente modulo*.c para aplicarla. Si éstos no existieran entonces no se aplicaría dicha regla implícita. Por el contrario, si existieran archivos fuente llamados modulo1.f y modulo2.f (lenguaje Fortran) entonces la regla implícita correspondiente invocaría a un compilador Fortran.
34. Existen diversos argumentos para no distribuir el código fuente: secreto de fábrica, control del estándar, seguridad, etc; todos son cuestionables en ciertas circunstancias.
35. En el Makefile empleamos “ar” con las opciones "rcs". Considero que no se gana mucho explicándolas por lo que remito a leer el man page de ar si tiene curiosidad.
36. En algunos sistemas Unix es necesario ejecutar el comando ranlib tras el ar a fin de actualizar un componente especial llamado \\.SYMDEF que contiene un índice de todos los símbolos externos definidos en los otros componentes. Este índice permite ganar velocidad en el momento del enlace. La opción "s" de GNU ar (que usa Linux) implica automáticamente a ranlib por lo que no se requiere de este último.
37. A este nombre completo se le denomina a veces "nombre real" o "real name".
38. Por ejemplo, las funciones han cambiado su interfaz, o han desaparecido algunas que otrora se proporcionaban, o han cambiado en su comportamiento, etc. El nombre de la librería junto con el número "v" (libXXX.so.v) se conoce como el "soname" de la librería.
39. Por ejemplo, se han añadido nuevas funciones; se han corregido bugs; se ha optimizado la ejecución, etc.
40. En ciertas arquitecturas y en ciertos programas, se puede generar un "desbordamiento" de la tabla de relocalización de direcciones. Para evitar esto se indica utilizar -fPIC (en lugar de -fpic), lo cual hace crecer una pizca a los archivos objeto.
41. La documentación de gcc menciona la posibilidad de añadir código no "independiente de la posición" (opción -mimpure-text) pero esto ocasiona que el código se copie en el momento de la ejecución (no se comparte como debería.) Evite esto compilando siempre con -fpic.
42. Un detalle muy importante radica en que se está enlazando con el nombre "base" de la librería, sin considerar los valores "v", "m", "r" antes señalados. Esto normalmente obliga a que el nombre "base" sea un enlace simbólico hacia un "real name".
43. Evidentemente, la ganancia se da cuando se llevan muchos ejecutables que referencian a la misma librería compartida.
44. Si se copia la librería hacia cualquiera de estos directorios (a diferencia de /lib y /usr/lib), es necesario ejecutar además el comando ldconfig para refrescar el "caché" de búsqueda de librerías.
45. Típicamente se proporciona también la versión estática.
46. En realidad, este es trabajo del enlazador (ld), el cual es invocado por el el compilador.
47. Esto no significa que durante la ejecución se buscará la librería en el directorio actual. Para nuestro caso, seguiremos empleando LD_LIBRARY_PATH.
48. Es importante mencionar que la interfaz dlopen no es estándar entre Unix’es. Su origen es SunOS y está incluida en POSIX 1003.1-2003, pero algunas versiones de Unix tienen sus equivalentes propietarios.
49. Se podría añadir el enlace -lm al ejecutable prueba (en lugar de fact3.so.) Esto también funcionaría, pero no es muy correcto en la medida que crea una dependencia innecesaria en “prueba”. Supongase que (como usuarios finales) no dispusiéramos de la librería matemática libm.so, entonces con el método recomendado cuando menos podríamos usar los dos métodos fact1 y fact2. Por el contrario, si el ejecutable se enlaza de antemano, entonces éste fallaría en el momento de su sóla invocación.
50. Por ejemplo, tendríamos que usar LD_LIBRARY_PATH.
51. Si se analiza con detenimiento es bastante lógico: en una librería que contiene muchos módulos y muchas funciones, por lo general son sólo unas pocas las que realmente se necesitan. Sería muy costoso considerar a todas en el análisis.
52. La documentación del enlazador menciona las opciones -( y -) como equivalentes a start-group y end-group. Como los paréntesis requieren "quoting" para desactivar su comportamiento especial en el shell, he preferido exponer la forma "larga".
53. En Linux se menciona una opción equivalente -rdynamic. Es menos portable.
54. Lamentablemente hay sistemas en los que la librería libm.so se enlaza automáticamente por omisión. En ese caso los ejemplos que siguen no funcionarán.
55. El man page de dlopen(3) suele proporcionar un ejemplo de acceso a la librería matemática.
56. Las llamadas al sistema están diseñadas en función de la arquitectura del kernel y no de la comodidad del programador. En el diseño de las librerías, en cambio, la facilidad de uso del programador juega un rol crucial.
57. Entre variantes de Linux la portabilidad es prácticamente 99% por tratarse del mismo kernel. Pueden surgir problemas cuando se pretende usar el mismo programa entre kernel’s Linux de muy distinta versión. Yendo a otro tema, muchas llamadas al sistema para los sistemas Unix/Linux se han implementado mediante librerías en los sistemas DOS/Windows.
58. Las distribuciones de la base de datos Mysql (que suelen estar instaladas en los sistemas Linux) proporcionan un comando llamado perror que recibe el valor numérico de errno e invoca a la función perror(). Sugiero que consulte la documentación de perror(3) y haga su propia versión de dicho utilitario.
59. En algunos casos se proporciona una llamada equivalente llamada brk(2). En Linux, ambas están disponibles y sbrk se implementa como una función que invoca a brk.
60. Existe una macro complementaria para consultar por archivos regulares (S_ISREG().) Sin embargo, no es útil para nuestros propósitos pues unlink elmina también enlaces simbólicos, sockets, fifo’s, y archivos de dispositivo.
61. Evidentemente, todos estos tiempos pueden variar de sistema en sistema, pero la idea principal subyace.
62. En la práctica la granularidad de un sistema sin actividad es de milésimas. Recuérdese también que Linux/Unix no es un sistema de tiempo real (aunque hay excepciones) por lo que estrictamente es imposible hablar de granularidad asegurada.
63. Inconscientemente, siempre asocio a select con I/O de archivos y sockets, por lo que prefiero la implementación con nanosleep.
64. Existen comandos que permiten mostrar esta información: id(1) y groups(1). El lector puede consultar su código fuente y contrastarlo con el que se presenta aquí.
65. Es tan inmediato que no debe usarlo en sistemas que están realizando algún trabajo importante!
66. Aunque no lo explicaremos aquí, esta restricción se puede eliminar mediante el permiso Set-uid.
67. En realidad, la rutina reboot() que estamos empleando es una función auxiliar de glibc que a su vez invoca a una llamada al sistema específica de Linux también llamada reboot() que es más compleja de utilizar.
68. Como se sabe, todos los usuarios en el sistema se identifican internamente por un número conocido como UID. El administrador se define como el usuario con UID=0 (generalmente root.)
69. Más generalmente, la tecla "INTR", reprogramable con el comando stty.
70. Lamentablemente algunas señales tienen valores distintos dependiendo de la versión de Unix. Es preferible por tanto emplear los identificadores literales.
71. Por ejemplo, la mayoría de comandos Unix no captura la señal SIGINT para que el usuario pueda interrumpirlos interactivamente con CTRL-C.
72. También conocidas como "Unreliable signals"
73. Si en un momento posterior necesitamos que una señal tenga nuevamente el comportamiento "estándar" del sistema, podríamos usar el identificador SIG_DFL. Es decir, si usamos signal(SIGINT,SIG_DFL), la combinación CONTROL-C podrá volver a interrumpir el programa en forma inmediata.
74. Sin ánimo de detallar las ventajas de sigaction(2), baste indicar que signal(2) tiene un comportamiento bastante "aberrante" en los sistemas System-III y otros de la época. En aras de una mayor portabilidad, se debe preferir sigaction(2). Signal puede ser empleada en casos sencillos, que no demandan portabilidad y tras haberse asegurado de que su comportamiento no es "inseguro" según la documentación del vendedor.
75. Lance el comando con redirección del error estándar: ./reloj 2>x.txt. Luego, con el comando: while true; do kill -SIGUSR1 1188; done, es posible generar una larga lista de reportes del cronómetro en x.txt (1188 es el PID del proceso.)
76. Otro problema potencial del ejemplo radica en el uso de las funciones de librería estándar como fprintf en el signal handler. Esto no es una buena práctica pues la librería estándar no es "reentrante"; es decir, puede "des-sincronizarse" si se emplea simultáneamente en tanto en las funciones "normales" como en los signal handlers.
77. Evidentemente, esto se hace así porque las alarmas no se reprograman por sí solas de manera automática.
78. Y también SIGSTOP.
79. En el listado anterior se pudo reemplazar fácilmente kill() por raise(). Consulte el manual de esta rutina auxiliar.
80. Este ejercicio es una adaptación de un ejemplo del libro de R. Stevens "Advanced Programming in the Unix Environment". Addison Wesley. New York. 1993. pp. 286.
81. Es decir, la vida del "hijo" se inicia inmediatamente después del fork().
82. Esto no es terminología estándar. Se ha escrito así por ser fácil de comprender.
83. Con el comando ps axm he obtenido resultados adecuados en Linux. En otros sistemas se debe usar algo como ps axu o ps -ef.
84. En general, no hay garantía con respecto a cuál de los procesos se reanuda más aprisa (el padre o el hijo.) El sleep(10) intenta dar tiempo a los hijos para indagar el estado de sus padres.
85. Sin entrar en una discusión sobre el tema, para este ejemplo en particular probablemente es más conveniente utilizar multithreading.
86. Si cada proceso hijo hubiera abierto el archivo por separado, el "puntero" de grabación no se compartiría y la única manera de evitar sucesivas sobreescrituras es especificando en todos los casos una apertura en modo "añadir" (append), lo cual se conseguiría introduciendo la constante O_APPEND entre los flags de open().
87. Wait(2) retorna -1 en diversos casos de error. Para ser más estrictos, también deberíamos verificar que la variable errno contenga ECHILD.
88. waitpid(0,NULL,0) es equivalente a wait(NULL).
89. En este caso, la rutina raise(3) pudo ahorrarnos el uso de getpid().
90. Es decir, dejan de ser "almas en pena".
91. En Linux y otros sistemas, se puede apreciar el PGID con el comando ps axmj. En otros se puede usar ps -efj.
92. El shell es el encargado de poner los grupos de procesos en foreground (o en background mediante el símbolo &.) En general, el shell es el principal usuario del PGID.
93. Existe una rutina no muy portable llamada daemon(3) que puede realizar gran parte de la inicialización de los demonios. Esta rutina está presente en sistemas BSD 4.4 así como en sistemas Linux. No la utilizaremos aquí.
94. Un proceso líder de grupo es aquel cuyo PID es igual a su PGID. No desarrollaremos este concepto aquí, y aceptaremos que fork(2) permite conseguir un proceso que no es líder.
95. En sys/param.h se define una constante análoga llamada NOFILE. Ésta puede funcionar bien, pero se debe evitar por no ser estándar.
96. Típicamente, sólo una de las "funciones exec" es una llamada al sistema, y las otras corresponden a funciones de librería que invocan a la primera. Para nuestros propósitos, no es importante hacer esa distinción por lo que las denominamos con generalidad las "funciones exec".
97. En antiguos sistemas Unix con compilador pre-ANSI-C, la terminación de la lista se efectuaba con la construcción "(char *)0".
98. El entorno puede ser administrado por diversas funciones como getenv(), putenv(), setenv(), etc.
99. Si el comando proporcionado contuviera al menos un slash (/), entonces se asume que se trata de un "full pathname" y el sistema NO consultará la variable PATH.
100. En aplicaciones orientadas hacia la seguridad es inconveniente confiar en el PATH por lo que se usan las otras formas de "exec".
101. Las ideas de foreground y background aplicadas en su real magnitud, pertenecen al concepto de "job control" que no desarrollaremos aquí. No pretendemos crear un competidor para bash!
102. Ver el ejercicio 1 del capítulo 5.
103. Como sabemos por el capítulo 5, esta señal normalmente es ignorada.
104. SA_RESTART se proporciona en la mayoría de Unix’es derivados de BSD pero no es parte de POSIX. Su comportamiento no es muy estable por lo que sugiero evitar su uso. En su lugar, es preferible que la señal se interrumpa y se vuelva a leer. Si Ud. está usando un sistema Unix que no soporta esta característica, vea el tercer ejercicio de este capítulo para una solución alternativa.
105. A fin de observar esto, sugiero eliminar el SA_RESTART y ejecutar un proceso en background tal como "sleep 5 &" y esperar 5 segundos. Vea también la nota anterior.
106. Ver capítulo 6 para más detalles sobre waitpid().
107. Falta verificar el caso en que read retorna -1 y errno no es EINTR. Correspondería a algún error de lectura.
108. Este tipo de problema es típico de los programas grandes, donde la complejidad dificulta su solución. El "garbage collecting" es una popular solución que proporcionan otros lenguajes (aunque en ciertos casos disminuye la velocidad de ejecución.)
109. En el capítulo 6 hemos desarrollado un ejemplo acerca de cómo obtener la información del "estado de terminación" de un proceso con waitpid(2).
110. El "0" en "0777" es importante pues indica que el número está en base octal.
111. No explicaremos aquí el tema de la "máscara de permisos" (umask), cosa que se trata en cualquier libro básico de Unix. En un ejercicio se solicita esta investigación.
112. Por tanto, si no se usa O_CREAT, entonces no se debería especificar el tercer argumento de open(2).
113. La rutina raise(3) provoca que el proceso se envíe una señal a sí mismo.
114. Con el fin de que el buffer de la librería estándar sea grabado y no se pierda (antes del SIGKILL), disponemos de tres opciones: 1) Que el programa termine normalmente 2) Que se cierre el archivo con fclose(3) 3) Que se aplique la función de la librería estándar fflush(3)
115. Las motores de base de datos suelen requerir esta funcionalidad a fin de mantener la consistencia de los datos ante las caídas.
116. En el listado, la llamada al sistema read(2) lee hasta 255 bytes en el array "L"; el número exacto de bytes leídos es retornado y almacenado en "i". Como asumiremos que se trata de cadenas de texto, hemos completado el array "L" con un cero al final (L[i
117. Para "eliminar" una opción, también se debe leer el "status", aplicar la expresión “status \&= ~opcion”, y grabar.
118. El lector puede mejorar el programa intentando desmontar el CDROM de ser necesario.
119. En el sistema Linux que estoy usando en este momento, el comando man ioctl_list proporciona una larga lista de IOCTL’s (comandos.)
120. A fin de leer directorios normalmente no se emplean llamadas al sistema debido a la falta de estandarización y a que no redundan en ningún beneficio sobre las rutinas mencionadas.
121. Esta herramienta me ha ayudado, por ejemplo, a descubrir qué archivos son modificados "secretamente" por un programa mal documentado o sin código fuente.
122. En versiones anteriores de Gdbm la constante GDBM_WRITER tenía la misma función; actualmente no permite crear el archivo.
123. Existen muchos otros escenarios bastante sofisticados. Los sistemas DBMS suelen brindar un soporte "transaccional" que ayuda a los diseñadores a resolver estos problemas.
124. La llamada al sistema lock permite poner un cerrojo en un archivo de un modo muy sencillo. Lo único que requiere es disponer de un descriptor al archivo abierto. Si bien podríamos obtener un descriptor fácilmente abriendo el archivo de base de datos con open(2), podemos aprovechar el descriptor ya creado internamente por Gdbm. Para esto, se proporciona la rutina gdbm_fdesc() que retorna un descriptor de archivo hacia la base de datos.
125. Se sobreentiende que el administrador ha instalado los paquetes de desarrollo para curses/ncurses. En muchos sistemas esto no se instala a no ser que se solicite explícitamente.
126. El código fuente del paquete Ncurses proporciona buenos ejemplos de estas librerías auxiliares.
127. Existen funciones en Curses para el trazado de "cajas". No se describen aquí.
128. No se debe asumir si el terminal está en modo cbreak o nocbreak, por lo que es conveniente invocar explícitamente a una de estas funciones en programas interactivos.
129. Sugiero probar las consolas de texto así como los terminales gráficos xterm, kterm, etc.
130. Aunque no lo utilizamos aquí, si el usuario no introduce ninguna tecla getch() retornará la constante de Curses "ERR".
131. De no activarse el "keypad" sólo las teclas alfanuméricas son procesadas adecuadamente por Curses. Las teclas KEY_LEFT y KEY_RIGHT del listado siguiente no serían reconcidas.
132. Todas estas funciones se suelen usar juntas en este tipo de programa. En Linux, véase el manual de "inopts" o "curs_inopts" para más detalles. Revise además la función raw() como alternativa a cbreak().
133. Esto lo obtuve por prueba y error. Otros valores producen una distinta "sensación" de juego.
134. Este método puede no ser el más adecuado dependiendo de la implementación de rand(). Vea el manual correspondiente para más información.
135. Un programa más sofisticado podría detectar dinámicamente modificaciones en el tamaño de la pantalla y redimensionar sus estructuras para esto.
136. Diversos compiladores de C para Windows están soportados; yo he probado -exitosamente- a recompilar en Wind*ws algunos programas desarrollados originalmente en Linux, usando el compilador (Open Source) Mingw.
137. Estas rutinas han sido ligeramente adaptadas de los ejemplos que adjunta la documentación oficial de SDL.
138. Aún para una misma profundidad de colores, las tarjetas de video pueden emplear distintos métodos para almacenar los colores. Por ejemplo, con 16 bits se puede usar un esquema "5,6,5" (5 bits para el rojo, 6 para el verde, 5 para el azul) pero otros son también posibles.
139. La necesidad de bloquear la superficie varía según diversos factores por lo que siempre se debería hacer el test señalado.
140. Salvando las distancias, esto es semejante a refresh() de Curses (capítulo 11.)
141. Es decir, si su norma no diverge al infinito.
142. SDL puede hacer uso de otros formatos con mucha facilidad mediante la librería auxiliar SDL_Image, pero no lo expondremos aquí.
143. Por ejemplo, la codificación en bits del archivo BMP puede no corresponder a la codificación de la pantalla, por lo que en cada copia se requeriría una "traducción".
144. Esta estructura es de tipo SDL_KeyboardEvent. Además de keysym permite conocer si la tecla está siendo presionana o liberada.
145. Por ejemplo, contiene un miembro “scancode” que guarda el código de la tecla devuelto por el hardware. Esto no es portable. Asimismo, contiene un miembro “mod” de tipo SDLMod, que corresponde a la combinación con teclas especiales (como SHIFT, CTRL, ATL, etc.)
146. Por ejemplo, muchos programadores recomiendan desarrollar aplicaciones con Gtk+ utilizando lenguajes más convenientes como Python.
147. Utilizando la terminología de objetos, se puede decir que las clases de Xt son abstractas, y los vendedores deberían crear las subclases concretas.
148. Por ejemplo, ni Motif ni Xt proporcionan rutinas para trazar líneas en áreas de dibujo.
149. Las rutinas del toolkit Xt se identifican fácilmente porque su nombre se inicia con "Xt". Análogamente, las de Motif se inician con "Xm".
150. Por ejemplo, xclock -display remoto:0 -geometry 400x400+0+0 permite lanzar la aplicacion xclock (reloj) en el primer display del computador "remoto". El reloj tendrá 400x400 pixels y aparecerá en la esquina superior izquierda. Estas opciones son automáticamente procesadas cuando se pasan &argc y argv a XtAppInitialize().
151. En este texto nos basamos en la versión 2.0 de Gtk+; téngase presente que hay numerosas diferencias con respecto a las versiones 1.X, todavía muy difundidas.
152. Al menos, desde la versión 2 se soporta Win32 y en menor grado MacOS y otros. En las anteriores versiones sólo operaba con X Window.
153. Puede ser más de una. Las funciones asociadas se invocarán una tras otra.
154. A diferencia de la exposición de Motif, aquí presentamos los programas invocando directamente a las rutinas respectivas. Esto se debe a que los listados de Gtk+ son más intuitivos que en el otro toolkit, pero no necesariamente más breves.
155. La alternativa es GTK_WINDOW_POPUP, que son "ventanas" no administradas por el window manager y se emplean para implementar ciertos widgets.
156. Este "ancho de borde" en las ventanas (como en nuestro caso) corresponde al espacio libre que se guarda desde el borde de la misma hasta los widgets interiores; pruebe a eliminarlo para que note la diferencia. En widgets que no son "ventanas", corresponde al espacio libre que se guarda desde la periferia actual del widget hacia afuera.
157. De allí que se utilice la macro genérica GTK_BOX().
158. Personalmente, considero que los diseñadores debieron programar una capa de abstracción adicional para uniformizar los callbacks de las señales y eventos, evitándose "casos especiales".
159. De no haberse capturado este evento, su generación provocaría que la ventana desaparezca pero la aplicación seguiría (inútilmente) en ejecución. Pruébelo.
160. En algún momento esto desató una polémica respecto al caracter abierto de Qt, en especial si se iba a utilizar para el desarrollo de Kde. Véase los archivos de las discusiones en Internet.
161. El mismo nombre del directorio actual.
162. El nombre del ejecutable se puede variar en el archivo de proyecto mediante la directiva TARGET. Por omisión tiene el mismo nombre que el archivo de proyecto.
163. Fltk proporciona Flud, una herramienta RAD que permite diseñar las ventanas gráficamente; posteriormente genera el código fuente que implementa estos diseños.
164. En variantes System V el flujo es bidireccional. Dado que esto no es portable, no lo asumiremos aquí.
165. Esto no es tan cierto; existen otras condiciones por las que el proceso puede abandonar su estado "suspendido". Vea el manual de semop(2) para más detalles.
166. Nuestro programa tras inicializar un semáforo lo "configura" a un valor inicial. Esto podría generar un error en otro proceso que ya ha estado utilizando del semáforo.
167. No se tome en sentido de "oculto". La "llave" es visible por cualquier usuario y proceso tan pronto el segmento de memoria compartida es creado. Ver el comando ipcs.
168. Si se emplea la constante especial IPC_PRIVATE como primer argumento de shmget, el sistema asignará una llave que nadie está usando en ese momento. El problema es que luego tendríamos que "publicar" la llave de alguna otra forma (por ejemplo, escribiéndola en un archivo) para que nuestros otros procesos "encuentren" el segmento de memoria compartida.
169. El segundo argumento permite especificar la dirección de memoria donde se desea que el segmento sea creado físicamente. Normalmente se permite al kernel elegir esta posición dado que no es una característica portable. El tercer argumento puede contener la constante SHM_RND usado para "corregir" el segundo argumento (que debe ser no nulo) a un valor múltiplo de la constante interna SHMLBA. Asimismo, este tercer argumento puede contener la constante SHM_RDONLY para especificar que el proceso actual sólo leerá el segmento, mas no lo modificará.
170. Los procesos pueden eliminar un segmento de memoria compartida usando la llamada al sistema semctl() así: semctl(id,IPC_RMID,NULL).
171. En la mayoría de lugares, a falta de semáforo existe la convención que el auto relativamente "a la izquierda", cede el paso. Pruebe a programarlo!
172. Nótese que se ha reutilizado la llave de la memoria compartida para el semáforo. Esto no crea ningún conflicto.
173. Esto no es muy elegante, pero evita tener que hacer muchas modificaciones a la versión anterior.
174. Según el libro de Sidnie Feit "TCP/IP, architecture, protocols and implementation" (ISBN 0-07-020345-6), la interface de sockets fue introducida por primera vez en 1982 con la version 4.1c de BSD. Una alternativa muy difundida a la interfaz de sockets corresponde a la "Transport Layer Interface" (TLI) introducida en 1986 por AT\&T.
175. El concepto de dirección "válida" requeriría una explicación que va más allá del propósito de este texto. Consúltese los manuales de redes.
176. Aunque la mayoría de la gente asocia la dirección IP al computador, en realidad está asociada a la "interfaz de red". Un computador con varias interfaces de red en uso normalmente tiene varias direcciones IP asignadas.
177. En los sistemas Unix, sólo los procesos con usuario "root" pueden "escuchar" en puertos menores a 1024. Esto obliga a que ciertos servicios de red (es decir, procesos que "escuchan" solicitudes) sean iniciados exclusivamente por el administrador.
178. Quizá a algunos les ayude pensar en un “ping” que contesta con la "hora".
179. En otros Unix también se puede usar netstat pero con otras opciones.
180. Obviamente, si se proporciona un nombre de host, el sistema deberá resover este nombre en una dirección IP, para lo cual usará la configuración del "resolver" (por ejemplo, consultar a un servidor DNS.) Por suerte, todo esto es transparente para nosotros.
181. Nótese que el puerto se asigna mediante la rutina htons(). Esto es debido a que la estructura debe guardar una representación adecuada del número entero que representa al puerto, la cual debe ser estandar entre diversos computadores. En la medida que distintos computadores almacenan los enteros en diferentes esquemas de secuencias de bytes, se hace necesaria una función como htons() que uniformice la representación.
182. Si se ve la documentación de connect(2) (y otras llamadas al sistema relacionadas) se verá que el prototipo no especifica una estructura de tipo “sockaddr_in” sino de tipo “sockaddr”. La estructura “sockaddr” es una suerte de "plantilla" para acomodar cualquier protocolo, pero en la práctica se requiere de estructuras específicas para cada uno de ellos. Para el caso de conexiones TCP/IP se usa “sockaddr_in”.
183. Se podría haber usado directamente el número 13, pero en general es más adecuado especificar el "nombre" y permitir al administrador reconfigurar el servicio a otro puerto vía la modificación de /etc/services.
184. Se puede hacer una rutina que tenga el efecto combinado de net_listen_port() y net_accept(), pero es necesario tener la posibilidad de invocarlas por separado en programas que esperan más de una conexión de los clientes, pues sólo se debe "escuchar" en el puerto una sola vez (listen()/bind()) mientras que para cada conexión entrante se hará su respectiva aceptación (accept().)
185. Sin embargo, vea la documentación de la misma para tener una idea más completa de qué es realmente el "nombre del host".
186. Téngase en consideración que esto no ocurre así con los dispositivos orientados a bloque (como los discos) en los cuales se garantiza la lectura/escritura total (sin fragmentación.)
187. Nuestros ejemplos se caracterizan por enviar y recibir paquetes de tamaños pre-establecidos, cosa que se puede controlar con las rutinas que se presentan. Sin embargo, existen muchos programas donde el tamaño de los paquetes es muy variable, lo que requiere que la aplicación introduzca algún esquema adicional de control como por ejemplo, un "delimitador", o quizá una "cabecera" que indique el tamaño del paquete "lógico".
188. Recuérdese que en el ejemplo del Sistema de Monitoreo, un proceso servidor interactuaba con muchos otros, pero esta interacción era sólo con uno a la vez. Por otro lado, se sugiere al lector investigar las rutinas readv() y writev() (que no explicaremos aquí) pues no tienen la limitación mencionada para read() y write().
189. A no ser que se haya especificado REG_NOSUB a regcomp().
190. A veces también se le denomina "parser", aunque este último término se reserva con más precisión para los "analizadores sintácticos" como yacc.
191. En estos sistemas a veces lex es un enlace simbólico a flex.
192. Vea el capítulo 18 para una introducción a las expresiones regulares si todavía no las conoce.
193. En algunos sistemas que disponen de flex es necesario reemplazar el -ll por -lfl
194. Es conveniente encerrar el código fuente de "acción" entre llaves para mejorar la portabilidad, especialmente si aquélla se compone de varias sentencias de lenguaje C separadas por punto-y-coma (;).
195. Generalmente sigue un análisis sintáctico.
196. Su creador (hacia 1972), Steve Johnson le llamó así como abreviación a "Yet Another Compiler Compiler".
197. Estrictamente hablando, yacc sólo puede analizar las gramáticas de tipo "LALR". Consúltese la documentación correspondiente para más detalles.
198. Evidentemente, si no desea utilizar un Makefile Ud. puede escribir los comandos manualmente.
199. Incluso cuando no hay ninguna línea, este array ya ocupa MAX_LINE*sizeof(char*) bytes.
200. Estos resultados datan del año 2006 y deben considerarse "anecdóticos"; la medición de la performance de I/O es un tópico muy complejo por el elevado número de variables involucradas (notablemente, la optimización de memoria "caché" a cargo del sistema operativo y los controladores de los dispositivos de almacenamiento.)