Informática
Unix
1. ENTORNO
Nos adentramos en el mundo de las características multitarea de UNIX. Este capítulo trata las técnicas de las llamadas a programas y procesos por medio de las llamadas exec, fork y wait.
El objetivo será la implementación de un intérprete de órdenes o shell.
Cuando un programa UNIX entra en ejecución, recibe dos colecciones de datos desde el proceso que lo llamó:
- los argumentos,
- el entorno.
Un programa C recibe ambos en forma de array de punteros a caracteres. Todos los punteros disponen de una cadena seguida de un último carácter '\0', salvo el último puntero, que es NULL.
El programa también recibe un acumulador con el número de argumentos.
Todo programa C deberá comenzar así:
extern char** environ;
main( int argc, char** argv, char** entorno )
Las variables environ y entorno contienen los mismos valores, por lo que normalmente entorno se omite.
De manera convencional, las variables de entorno tienen el formato:
variable=valor
con un elemento nulo tras el valor.
Las variables de entorno suelen ser activadas por medio de una orden shell de asignación, como por ejemplo:
$PATH=/bin:/usr/bin:/usr/pepe/bin::
Un proceso puede capturar el valor de una variable de entorno a través de la función estándar getenv. Del mismo modo, un proceso puede acceder directamente al entorno a través del puntero environ, como puede verse en el siguiente ejemplo:
extern char** environ;
main( int argc, char** argv ) {
int i;
for( i=0; environ[ i ] != NULL; i++ )
printf( "%s\n", environ[ i ] );
exit( 0 );
}
La salida sería del tipo:
VARIABLE1=valor1
VARIABLE2=valor2
...
...
...
La actualización del entorno, sin embargo, no es tan sencilla. El almacenamiento interno es propiedad del proceso, y puede modificarse libremente. Sin embargo, no hay más espacio para dar cabida a nuevas variables o a valores más largos. De ahí que se opte por la actualización trivial o por la creación de un nuevo entorno.
Si la actualización es de grandes dimensiones, quizá fuera necesario remodelar el entorno y aportar mayor flexibilidad, siendo así posible añadir variables y añadir valores más largos. Por lo tanto, antes de llamar a un programa nuevo siempre puede construirse una versión en formato encapsulado y cargarse el entorno adecuado. Esta forma de actuar es la de la orden sh.
Puesto que sh es un lenguaje de programación, así como un intérprete de órdenes, almacena en su tabla de símbolos una serie de variables que son de interés exclusivo del programa shell que se ejecuta. Son ejemplos los contadores de bucles y las variables que guardan nombres de archivos. El paso de estas variables locales a otros programas sería costoso y de difícil manipulación.
Por ello, para distinguir entre las variables locales y locales que se introducen en el entorno, sh permite que una variable sea exportada:
export VARIABLE
Al construir un entorno a partir de la tabla de símbolos, se incluye el entorno original más los nuevos valores de las variables exportadas. Los cambios sobre las variables no exportadas se ignoran.
La exportación sólo funciona en una dirección: desde un proceso hacia cualquier programa al que invoque.
Cada programa hereda una copia del entorno ( en su propio espacio de direcciones ), por lo que un proceso no puede devolver información a su proceso padre a través de esta copia.
En nuestro programa asumiremos la siguiente tabla de símbolos:
#define VALORMAX 25
static struct tablaDeSímbolos {
char* nombre; /* Nombre de la variable */
char* valor; /* Valor de la variable */
BOOLEANO exportado; /* ¿ Va a exportarse ? */
} símbolos[ VALORMAX ];
Hay espacio para 25 variables, suficiente para nuestros programas.
Un zócalo vacío se representa con un valor NULL de nombre.
Las variables externas son inicializadas con el valor cero, por lo que inicialmente, todos los zócalos están vacíos.
A continuación se presenta un programa que busca un zócalo determinado o uno vacío ( el primero vacío que encuentre ):
static struct tablaDeSímbolos* buscar( char* nombre ) {
int i;
struct tablaDeSímbolos* v;
v = NULL;
for( i=0; i< VALORMAX; i++ )
if( símbolos[ i ].nombre == NULL ) {
if( v == NULL ) /* es el primer zócalo vacío que encuentra */
v = &símbolos[ i ];
}
else if( strcmp( símbolos[ i ].nombre, nombre)
== 0 ) {
v = &símbolos[ i ];
break;
}
return ( v );
}
La función buscar( ) devuelve un puntero a un zócalo, o un valor NULL, si la tabla de símbolos está llena.
El equivalente de la orden de asignación del shell lo realiza la función llenarTS. que admite dos argumentos de tipo cadena: un nombre de variable y su valor asignado.
La función llenarTS llama a la función buscar para localizar un zócalo vacío, y luego utiliza la función asignar para llevar a cabo la labor de copia:
BOOLEANO llenarTS( char* nombre, char* valor ){
/* añade un nombre y un valor al entorno */
struct tablaDeSímbolos* v;
if( ( v = buscar( nombre ) ) == NULL )
return( FALSO );
return( asignar( &v -> nombre, nombre ) &&
asignar( &v -> valor, valor ) );
}
Cuando buscar devuelve NULL, el nombre no está en la tabla de símbolos, y no hay ningún zócalo vacante. Por lo tanto, llenarTS falla.
A continuación se desarrolla la función asignar. Su primer argumento es un puntero a un miembro de tipo nombre o valor, y el segundo argumento es el nuevo nombre o cadena de valor. Si el miembro vale NULL, entonces malloc entra en funcionamiento para asignar espacio a una cadena. En caso contrario, hay espacio, y realloc nos asegura que este espacio es lo suficientemente amplio.
La cadena se copiará en el espacio reservado.
void* malloc( ), * realloc( );
static BOOLEANO asignar( char** p, char* s ) {
int dimension;
dimension = strlen( s ) + 1;
if( *p == NULL ) {
if( ( *p = ( char * ) malloc( dimension ) )
== NULL )
return( FALSO );
}
else if( ( *p = (char *) realloc( *p, dimension ) ) == NULL )
return( FALSO );
strcpy( *p, s );
return( CIERTO );
}
Para poner la marca de exportado, es necesario llamar a la función exportarTS.
La única dificultad que puede surgir es que la variable que se desea exportar no exista todavía, por lo que se asignará un valor nulo para reservar un zócalo para ella:
BOOLEANO exportarTS( char* nombre ) {
struct tablaDeSímbolos* v;
if( ( v = buscar( nombre ) ) == NULL )
return( FALSO );
if( v -> nombre == NULL )
if( !asignar( &v -> nombre, nombre ) ||
!asignar( &v -> valor, "") )
return( FALSO );
v -> exportado = CIERTO;
return( CIERTO );
}
Para tomar un valor de la tabla de símbolos haremos uso de la función leerZócaloTS.:
char* leerZócaloTS( char* nombre ) {
struct tablaDeSímbolos* v;
if( ( v = buscar( nombre ) ) == NULL ||
v -> nombre == NULL )
return( NULL );
return( v -> valor );
}
Normalmente, un programa querrá inicializar la tabla de símbolos del entorno que hereda. Por definición, el programa sólo recibe variables exportadas, por lo que todas las variables heredadas deberían marcarse como tales en la tabla de símbolos.
A continuación se presenta la función crearNuevoEntorno a tal efecto.
Esta función debería llamarse antes que cualquier otra que manipulase la tabla de símbolos:
BOOLEANO crearTS( ) { /* inicia la tabla de símbolos a partir de los valores del entorno */
int i, lonNombre;
char nombre[ 20 ];
for( i=0; environ[ i] != NULL; i++ ) {
lonNombre = strcspn( environ[ i ], "=");
strncpy( nombre, environ[ i ], lonNombre );
nombre[ lonNombre ] = '\0';
if( !llenarTS( nombre,
&environ[ i ][ lonNombre + 1 ] ) ||
!exportarTS( nombre ) )
return( FALSO );
}
return( CIERTO );
}
La función estándar strcspn devuelve el número de caracteres que preceden al primer signo "=" en la cadena de entorno.
A continuación se procederá a extraer todas las variables exportadas de la tabla de símbolos, y se construirá un nuevo entorno en formato encapsulado para pasárselo a otros programas. No se utilizará el almacenamiento ocupado por el entorno heredado, incluso perteneciéndonos, porque seguramente no será lo suficientemente amplio.
Por lo tanto, la primera vez que construyamos un nuevo entorno a partir de la tabla de símbolos, asignaremos espacio para el array de punteros y para las cadenas a las que apuntan.
Se utilizará el indicador actualizado para no asignar un nuevo espacio la siguiente vez que haya que construir un entorno. Tan sólo habrá que ejecutar una operación realloc para asegurarnos de que los espacios asignados a las cadenas son lo suficientemente amplios.
BOOLEANO crearNuevoEntorno( ) { /* Crea un entorno a partir de la tabla de símbolos */
int i, entor, longnv;
struct tablaDeSímbolos* v;
static BOOLEANO actualizado = FALSO;
if( !actualizado )
if( environ = ( char** )malloc((VALORMAX + 1 ) *
sizeof( char* ) ) ) == NULL )
return( FALSO );
entor = 0;
for( i = 0;i < VALORMAX; i++ ) {
v = &símbolos[ i ];
if( v -> nombre == NULL || !v -> exportado )
continue;
longnv = strlen( v -> nombre ) +
strlen( v -> valor ) + 2;
if( !actualizado ){
if( ( environ[ entor ] =
( char* ) malloc( longnv ) ) == NULL )
return( FALSO );
}
else if( ( environ[ entor ] =
( char* ) realloc( environ[ entor ], longnv ) ) == NULL )
return( FALSO );
sprintf( environ[ entor ], "%s=%s",
v -> nombre, v -> valor );
entor++;
} /* fin de for */
environ[ entor ] = NULL;
actualizado = CIERTO;
return( CIERTO );
}
Para imprimir la tabla de símbolos hará falta una nueva función:
void imprimirTS( ) { /* impresión del entorno */
int i;
for( i = 0; i < VALORMAX; i++ )
if( símbolos[ i ].nombre != NULL )
printf( "%3s %s=%s\n",
símbolos[ i ].exportado ? "[ E ]" : "",
símbolos[ i ].nombre,
símbolos[ i ].valor );
}
Las variables exportadas van precedidas de una 'E', entre corchetes.
Para mostrar el comportamiento de las funciones sobre las tablas de símbolos, se presenta un pequeño programa que añade dos variables a la tabla de símbolos y la imprime:
int main( ) {
if( !crearTS( ) )
fatal( "No se puede iniciar el entorno" );
printf( "Antes de actualizarlo: \n");
imprimirTS( );
if( !llenarTS( "cuenta", "0" ) )
fatal( "llenarTS" );
if( !llenarTS( "LIBRO"; "/usr/pepe/libro" ) ||
!exportarTS( "LIBRO" ) )
fatal( "llenarTS o exportarTS ");
printf( "\nTras actualizarlo: \n");
imprimirTS( );
exit( 0 );
}
Descargar
Enviado por: | Javier Camuñas |
Idioma: | castellano |
País: | España |