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].
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 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.
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:
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:
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 |
É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.
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)
...
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.
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.
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.
Literalmente, "… en caso contrario…" Esta sección es opcional
en la senencia if()
.
Leer un entero desde la entrada estándar, asignándolo a la variable entera “n”.
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
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.
El operador '%' significa "residuo" y permite verificar la paridad rápidamente [14] .
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]
.
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 $
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
$
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:
Implemente para el programa de biblioteca las siguientes funciones (accesibles desde un menú):
-
Ingreso de un nuevo libro
-
Búsqueda de libros por título
-
Búsqueda de libros por autor
-
Eliminación de un libro
-
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)
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".
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 |
---|---|
|
Compilar generando archivo objeto pero no enlazar |
|
Nombre del archivo ( |
|
Enlaza con la librería dinámica |
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.
Opción | Significado |
---|---|
|
Define un macro |
|
Cancela la definición de macro |
|
Especifica ruta para buscar archivos de encabezado |
|
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
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%.)
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.
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] .
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
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.
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.
|
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.
¿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.
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.
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".
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 |
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.)
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 |
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 |
---|---|
|
Reemplazar las ocurrencias de un texto por otro dentro de un string |
|
Eliminar espacios en blanco al inicio y al final de un string |
|
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:
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:
-
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)
-
Como consecuencia de lo anterior, cuando estos ejecutables se cargan en memoria, ésta se llena ineficientemente con código repetido
-
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
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:
/*
* a.c
*/
void fa()
{
}
/*
* b.c: Llama a fa()
*/
extern void fa(void);
void fb()
{
fa();
}
/*
* prueba.c
*/
extern void fb(void);
int main(void)
{
fb();
return 0;
}
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
:
/*
* 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
$ ./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:
-
rutina1_informixe() → libinformixe.so
-
rutina2_oracler() → liboracler.so
-
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
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.
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?
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:
-
sbrk
(ybrk
) no están especificadas en POSIX y su \ comportamiento es muy variado por lo que son poco portables -
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 -
La liberación de memoria con
sbrk
simplemente no es posible \ en algunos sistemas. Evidentementemalloc
tampoco podrá llevar a \ cabo la liberación, pero nos evita tener que considerar este caso por \ separado -
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 -
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 -
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 -
La librería estándar proporciona las rutinas complementarias
calloc
\ yrealloc
que resultarían muy tediosas de implementarse a cada momento \ si se usasbrk
.
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
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?
my_shutdown
para que soliciteuna confirmación antes de "bajar" el sistema. Si tiene tiempo, busque y analice el código fuente del comando halt de Linux.
my_shutdown
paraque 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):
-
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".
-
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.
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] .
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
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.
reloj_block.c
definiendo unnuevo 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:
-
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 sistemafork(2)
. -
Este "shell hijo" tiene como única finalidad el "convertirse" en otro programa (en nuestro caso,
ls
ocp
.) Esto se hace con un grupo de llamadas al sistema conocidas comoexec()
.
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:
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:
-
Agotamiento de memoria
-
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 sá
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:
-
Podrá recibir comandos con sus argumentos separados por espacios, los cuales serán ejecutados como subprocesos
-
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 “\>”. -
Podrá interpretar el amperstand (
&
) al final del comando (separado con un espacio) como ejecución en "background"; en caso contrario será en "foreground" [101]. -
Podrá reconocer el comando interno "exit" para terminar su ejecución.
-
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.
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 |
|
El dispositivo según el caso |
|
La estructura |
|
El ioctl |
|
Ver los resultados |
|
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
llamadod_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:
-
El nombre del archivo de base de datos (con cualquier extensión)
-
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)
-
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. -
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.
-
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:
-
Se añade el header
sys/file.h
paraflock()
y elunistd.h
parasleep()
-
Se abre la base con
GDBM_NOLOCK
-
Se ejecuta
flock(fd,LOCK_EX)
antes de la escritura yflock(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 funcionesaddch()
ymvaddch()
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 comoprintf()
-
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 comofgets()
-
Para imprimir un texto con un formato similar al de
printf()
, se puede usar las rutinasprintw()
ymvprintw()
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 nuestrogetnstr()
.) 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:
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:
Constante | Subsistema |
---|---|
|
Temporizadores |
|
Sonido |
|
Video |
|
CDROM |
|
Joystick |
|
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:
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:
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;
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.
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.)
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.)
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:
-
Inicializar el "toolkit GUI"
-
Definir la ventana inicial y sus widgets
-
Definir qué ocurre cuando el usuario interactua con los widgets
-
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:
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);
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.
#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);
}
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.
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;
}
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);
}
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()
.
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.
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
.
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
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í:
-
Inicializar gtk con
gtk_init(&argc,&argv)
-
Crear la ventana principal con
gtk_window_new()
-
Crear widgets con rutinas especializadas (por ejemplo,
gtk_label_new()
,gtk_button_new_with_label()
, etc.) -
Definir los Signal Handlers para los widgets que lo necesiten (típicamente con
gtk_signal_connect()
-
Añadir los widgets a sus contenedores y/o a la ventana principal (por ejemplo,
gtk_container_add()
,gtk_box_pack_start()
, etc.) -
Mostrar los widgets y la ventana principal (
gtk_widget_show()
, etc.) -
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:
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.
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 sí 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.
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.
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
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 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.)
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.
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()”.
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.
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
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.
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]
13.6. Ejercicios
Implemente (en el toolkit de su elección) algunas mejoras para el editor:
-
Una barra de menú para reemplazar los botones de "abrir" y "guardar"
-
Diálogos de alerta cuando no se puede abrir o guardar el archivo
-
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);
}
-
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 partirpipe()
y otras llamadas al sistema comodup()
,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:
-
P1 reserva S1 (tiene éxito y no se bloquea)
-
P2 reserva S2 (tiene éxito y no se bloquea)
-
P2 reserva S1 (se bloquea por estar ya reservado por P1)
-
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:
-
Inicializar una estructura "sockaddr_in" (que aquí llamamos "addr") la que contiene el puerto al que escuchará el servidor
-
Crear el socket de escucha (llamado "s") con
socket(2)
-
Asociar la estructura "addr" al socket con
bind(2)
(esto falla si ya hay otro proceso "escuchando" en el puerto) -
Preparar una "cola de recepción" de conexiones para el socket, usando
listen(2)
-
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") -
Utilizar "sc" para operaciones de lectura/escritura
-
Cerrar "sc" con
close(2)
-
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:
-
El descriptor de la conexión entrante (
fd
) -
La dirección IP del computador cliente (
host
) -
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:
-
Se prepara un "socket de escucha" llamado "s", asociado al puerto 5678 mediante "net_listen_port()"
-
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"
-
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()
-
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"
-
Se envía al cliente los estados de todos los clientes activos, en paquetes de 256 bytes
-
Con
net_close()
se cierra la conexión con el cliente y se libera la estructura asociada al punterocliente
Cada vez que un cliente se conecta,
sdm_servidor
invoca a status_update()
que realiza las
siguientes acciones:
-
Elimina los estados que tienen más de 60 segundos de antigüedad. Estos se "liberan" inscribiendo cero en el índice "status_time"
-
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:
-
Busca un espacio libre en la tabla "status_time" y lo inscribe; ni no queda espacio imprime un mensaje de error y retorna
-
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:
-
Los clientes pueden hacer reintentos de conexión cada minuto, con la esperanza de que la interrupción del servidor haya sido momentánea
-
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
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:
-
El socket que se ha conectado a la espera de mensajes del servidor
-
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);
}
}
}
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 paraselect(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;
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);
}
}
}
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:
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:
Modificador | Significado |
---|---|
|
Utilizar la sintaxis de e.r. extendidas. En caso contrario es la sintaxis básica |
|
La expresión regular no diferencia mayúsculas de minúsculas |
|
Desactivar soporte para averiguación de "subexpresiones" |
|
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 apar_clean()
-
La función
par_read_pattern()
emplea apar_test_pattern()
y se limita a avanzarpmatch[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] .
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.
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:
-
Una barra vertical (
|
) significa "duplicar la acción especificada en la siguiente regla" -
La macro
ECHO
instruye alex
a copiar el token hacia la salida estándar -
La macro
REJECT
solicita alex
ejecutar la acción correspondiente a la "siguiente mejor" de las reglas. -
Una invocación a
yymore()
solicita que el siguiente token hallado sea agregado al valor actual deyytext
-
La invocación a
yyless(int n)
retorna todos excepto los primeros 'n' caracters deyytext
al flujo de entrada -
La función
unputc(c)
coloca el caracter 'c' en el flujo de entrada, el cual será el siguiente caracter procesado -
La función
input()
extrae el siguiente caracter del flujo de entrada, siendo opuesta aunputc()
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:
-
Escribirla nosotros mismos
-
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
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…
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.
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".)
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;
}
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.
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.
Comando | Función |
---|---|
LIST |
Lista el programa |
RUN |
Ejecuta el programa |
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 |
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 comoA$=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:
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 |
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 lacondicion
es verdadera, las instrucciones que siguen aTHEN
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:
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:
-
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
-
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
-
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
-
Asimismo, en Gdbm, la búsqueda de un elemento no existente toma un tiempo similar a la búsqueda de uno existente
-
Para
N=100000
Gdbm en promedio es unas9
veces más veloz que una búsqueda lineal, y esto sigue mejorando conforme se incrementa N. En una prueba conN=1000000
, el factor fue alrededor de90
-
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
/*
* 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);
}
/*
* 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
/*
* 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;
}