Provided by:
manpages-es_1.55-10_all 
NOMBRE
select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - multiplexacion de
E/S sincrona
SINOPSIS
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set
*exceptfds, struct timeval *utimeout);
int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set
*exceptfds, const struct timespec *ntimeout, sigset_t *sigmask);
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
DESCRIPCI'ON
select (o pselect) es la funcion eje de la mayor parte de programas en
C que manejan mas de un descriptor de fichero (o manejador de conector)
simultaneamente de manera eficiente. Sus principales argumentos son
tres arrays de descriptores de fichero: readfds, writefds, y exceptfds.
La forma de utilizar habitualmente select es bloquearse mientras se
espera un "cambio de estado" en uno o mas de los descriptores de
fichero. Un "cambio de estado" se produce cuando se vuelven
disponibles mas caracteres del descriptor de fichero, o cuando se
dispone de espacio en los buffers internos del nucleo para escribir mas
caracteres en el descriptor de fichero, o cuando un descriptor de
fichero provoca un error (en el caso de un conector o tuberia se da
cuando se cierra el otro extremo de la conexion).
En resumen, select tan solo vigila varios descriptores de fichero, y es
la llamada estandar en Unix para hacerlo.
Los arrays de descriptores de fichero son llamados conjuntos de
descriptores de fichero. Cada conjunto es declarado con el tipo
fd_set, y su contenido puede ser alterado con las macros FD_CLR,
FD_ISSET, FD_SET y FD_ZERO. FD_ZERO es normalmente la primera funcion
que se utiliza sobre un conjunto recien declarado. A partir de aqui,
aquellos descriptores de fichero individuales en los que este
interesado pueden ser anadidos uno por uno con FD_SET. select modifica
el contenido de los conjuntos de acuerdo a las reglas descritas abajo;
despues de invocar a select puede comprobar si su descriptor de fichero
esta aun presente en el conjunto con la macro FD_ISSET. FD_ISSET
devuelve un valor distinto de cero si el descriptor esta presente y
cero si no lo esta. FD_CLR elimina un descriptor de fichero del
conjunto, aunque yo no veo el uso que puede tener en un programa
correcto.
ARGUMENTOS
readfds
Este conjunto es observado para ver si hay datos disponibles
para leer en cualquiera de sus descriptores de fichero. Despues
de que select regrese, borrara de readfds todos los descriptores
de fichero salvo aquellos sobre los que pueda ejecutarse
inmediatamente una operacion de lectura con una llamada a recv()
(para conectores) o read() (para tuberias, ficheros y
conectores).
writefds
Este conjunto es observado para ver si hay espacio para escribir
datos en cualquiera de sus descriptores de fichero. Despues de
que select regrese, borrara de writefds todos los descriptores
de fichero salvo aquellos sobre los que se pueda ejecutar
inmediatamente una operacion de escritura con una llamada a
send() (para conectores) o write() (para tuberias, ficheros y
conectores).
exceptfds
Este conjunto es observado para las excepciones o errores sobre
cualquiera de sus descriptores de fichero. Sin embargo,
realmente es solo un rumor. Para lo que en verdad usa exceptfds
es para observar datos "fuera de orden" (OOB, out-of-band). Los
datos OOB son datos enviados por un conector usando la bandera
MSG_OOB, y por tanto exceptfds solo se aplica realmente a
conectores. Vea el contenido de recv(2) y send(2) sobre este
tema. Despues de que select regrese, borrara de exceptfds todos
los descriptores de fichero salvo aquellos sobre los que se
puede leer datos OOB. Solo puede leer un byte de datos OOB de
todas maneras (con la operacion recv()), y se pueden escribir
datos OOB en cualquier momento sin bloquearse. Por tanto no hay
necesidad de un cuarto conjunto para comprobar si en un conector
hay disponibles datos OOB para escribir.
nfds Es un entero que indica uno mas del maximo de cualquier
descriptor de fichero en cualquiera de los conjuntos. En otras
palabras, mientras esta atareado anadiendo descriptores de
fichero a sus conjuntos, debe calcular el maximo valor entero de
todos ellos, incrementar este valor en uno, y pasarlo como nfds
a select.
utimeout
Es el maximo valor de tiempo que select debe esperar antes de
regresar, incluso si nada interesante ocurrio. Si este valor se
pasa como NULL, select se bloqueara indefinidamente esperando un
evento. utimeout puede ser puesto a cero segundos, lo que
provoca que select regrese inmediatamente. La estructura struct
timeval esta definida como,
struct timeval {
time_t tv_sec; /* segundos */
long tv_usec; /* microsegundos */
};
ntimeout
Este argumento tiene el mismo significado que utimeout pero
struct timespec tiene precision de nanosegundos como sigue,
struct timespec {
long tv_sec; /* segundos */
long tv_nsec; /* nanosegundos */
};
sigmask
Este argumento contiene un conjunto de senales permitidas
mientras se realiza una llamada a pselect (vea sigaddset(3) y
sigprocmask(2)). Puede valer NULL, en cuyo caso no modifica el
conjunto de senales permitidas en la entrada y la salida de la
funcion. Se comportara igual que select.
COMBINANDO SE~NALES Y EVENTOS DE DATOS
pselect debe ser usada si esta esperando una senal asi como datos de un
descriptor de fichero. Los programas que reciben senales como eventos
normalmente utilizan el manejador de senales para activar una bandera
global. La bandera global indicara que el evento debe ser procesado en
el bucle principal del programa. Una senal provocara que la llamada a
select (o pselect) regrese tomando la variable errno el valor EINTR.
Este comportamiento es esencial para que las senales puedan ser
procesadas en el bucle principal del programa, de otra manera select se
bloquearia indefinidamente. Ahora, en algun lugar del bucle principal
habra una condicion para comprobar la bandera global. Asi que debemos
preguntarnos: cque ocurre si una senal llega despues de la condicion,
pero antes de la llamada a select? La respuesta es que select se
bloquearia indefinidamente, incluso aun si hay un evento pendiente.
Esta condicion de carrera se soluciona con la llamada pselect. Esta
llamada puede utilizarse para enmascarar senales que no van a ser
recibidas salvo dentro de la llamada pselect. Por ejemplo, digamos que
el evento en cuestion fue la salida de un proceso hijo. Antes del
comienzo del bucle principal, bloqueariamos SIGCHLD usando sigprocmask.
Nuestra llamada pselect podria habilitar SIGCHLD usando la mascara de
senal virgen. Nuestro programa se podria parecer a esto:
int child_events = 0;
void child_sig_handler (int x) {
child_events++;
signal (SIGCHLD, child_sig_handler);
}
int main (int argc, char **argv) {
sigset_t sigmask, orig_sigmask;
sigemptyset (&sigmask);
sigaddset (&sigmask, SIGCHLD);
sigprocmask (SIG_BLOCK, &sigmask,
&orig_sigmask);
signal (SIGCHLD, child_sig_handler);
for (;;) { /* bucle principal */
for (; child_events > 0; child_events--) {
/* procesar el evento aqui */
}
r = pselect (nfds, &rd, &wr, &er, 0, &orig_sigmask);
/* cuerpo principal del programa */
}
}
Observe que la llamada pselect de arriba puede ser reemplazada con:
sigprocmask (SIG_BLOCK, &orig_sigmask, 0);
r = select (nfds, &rd, &wr, &er, 0);
sigprocmask (SIG_BLOCK, &sigmask, 0);
pero todavia queda la posibilidad de que una senal pueda llegar despues
del primer sigprocmask y antes de select. Si hace esto, es prudente que
ponga al menos un tiempo de espera finito para que el proceso no se
bloquee. Es probable que glibc funcione actualmente de esta manera. El
nucleo de Linux no tiene todavia una llamada al sistema pselect nativa
por lo que probablemente todo esto sea nada mas que hablar por hablar.
PR'ACTICA
Por lo tanto, ccual es el proposito de select? cNo puedo simplemente
leer y escribir en mis descriptores siempre que quiera? El significado
de select es observar varios descriptores al mismo tiempo y poner a
dormir adecuadamente a los procesos si no hay ninguna actividad. Esto
lo hace mientras le permite manejar varias tuberias y conectores de
manera simultanea. Los programadores de Unix a menudo se encuentran en
la situacion de manejar la E/S de mas de un descriptor de fichero donde
el flujo de datos puede ser intermitente. Si tan solo creara una
secuencia de llamadas read y write, podria encontrarse con que una de
sus llamadas puede bloquearse esperando datos de/a un descriptor de
fichero, mientras que otro descriptor de fichero esta siendo
inutilizado aunque haya datos disponibles. select maneja
eficientemente esta situacion.
Un ejemplo tipico de select lo podemos encontrar en la pagina de manual
de select:
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int
main(void) {
fd_set rfds;
struct timeval tv;
int retval;
/* Observar stdin (descriptor 0) para ver cuando hay
entrada disponible. */
FD_ZERO(&rfds);
FD_SET(0, &rfds);
/* Esperar hasta cinco segundos. */
tv.tv_sec = 5;
tv.tv_usec = 0;
retval = select(1, &rfds, NULL, NULL, &tv);
/* No confie en el valor de tv por ahora! */
if (retval)
printf("Los datos ya estan disponibles.\n");
/* FD_ISSET(0, &rfds) sera verdadero. */
else
printf("No ha habido datos en cinco segundos.\n");
exit(0);
}
EJEMPLO DE REDIRECCI'ON DE PUERTOS
Aqui viene un ejemplo que ilustra mejor la verdadera utilidad de
select. El listado de abajo es un programa de reenvio TCP que redirige
de un puerto TCP a otro.
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <string.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
static int forward_port;
#undef max
#define max(x,y) ((x) > (y) ? (x) : (y))
static int listen_socket (int listen_port) {
struct sockaddr_in a;
int s;
int yes;
if ((s = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
perror ("socket");
return -1;
}
yes = 1;
if (setsockopt
(s, SOL_SOCKET, SO_REUSEADDR,
(char *) &yes, sizeof (yes)) < 0) {
perror ("setsockopt");
close (s);
return -1;
}
memset (&a, 0, sizeof (a));
a.sin_port = htons (listen_port);
a.sin_family = AF_INET;
if (bind
(s, (struct sockaddr *) &a, sizeof (a)) < 0) {
perror ("bind");
close (s);
return -1;
}
printf ("aceptando conexiones en el puerto %d\n",
(int) listen_port);
listen (s, 10);
return s;
}
static int connect_socket (int connect_port,
char *address) {
struct sockaddr_in a;
int s;
if ((s = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
perror ("socket");
close (s);
return -1;
}
memset (&a, 0, sizeof (a));
a.sin_port = htons (connect_port);
a.sin_family = AF_INET;
if (!inet_aton
(address,
(struct in_addr *) &a.sin_addr.s_addr)) {
perror ("formato de direccion IP incorrecto");
close (s);
return -1;
}
if (connect
(s, (struct sockaddr *) &a,
sizeof (a)) < 0) {
perror ("connect()");
shutdown (s, SHUT_RDWR);
close (s);
return -1;
}
return s;
}
#define SHUT_FD1 { \
if (fd1 >= 0) { \
shutdown (fd1, SHUT_RDWR); \
close (fd1); \
fd1 = -1; \
} \
}
#define SHUT_FD2 { \
if (fd2 >= 0) { \
shutdown (fd2, SHUT_RDWR); \
close (fd2); \
fd2 = -1; \
} \
}
#define BUF_SIZE 1024
int main (int argc, char **argv) {
int h;
int fd1 = -1, fd2 = -1;
char buf1[BUF_SIZE], buf2[BUF_SIZE];
int buf1_avail, buf1_written;
int buf2_avail, buf2_written;
if (argc != 4) {
fprintf (stderr,
"Uso\n\tfwd <puerto-escucha> \
<redirigir-a-puerto> <redirigir-a-direccion-ip>\n");
exit (1);
}
signal (SIGPIPE, SIG_IGN);
forward_port = atoi (argv[2]);
h = listen_socket (atoi (argv[1]));
if (h < 0)
exit (1);
for (;;) {
int r, nfds = 0;
fd_set rd, wr, er;
FD_ZERO (&rd);
FD_ZERO (&wr);
FD_ZERO (&er);
FD_SET (h, &rd);
nfds = max (nfds, h);
if (fd1 > 0 && buf1_avail < BUF_SIZE) {
FD_SET (fd1, &rd);
nfds = max (nfds, fd1);
}
if (fd2 > 0 && buf2_avail < BUF_SIZE) {
FD_SET (fd2, &rd);
nfds = max (nfds, fd2);
}
if (fd1 > 0
&& buf2_avail - buf2_written > 0) {
FD_SET (fd1, &wr);
nfds = max (nfds, fd1);
}
if (fd2 > 0
&& buf1_avail - buf1_written > 0) {
FD_SET (fd2, &wr);
nfds = max (nfds, fd2);
}
if (fd1 > 0) {
FD_SET (fd1, &er);
nfds = max (nfds, fd1);
}
if (fd2 > 0) {
FD_SET (fd2, &er);
nfds = max (nfds, fd2);
}
r = select (nfds + 1, &rd, &wr, &er, NULL);
if (r == -1 && errno == EINTR)
continue;
if (r < 0) {
perror ("select()");
exit (1);
}
if (FD_ISSET (h, &rd)) {
unsigned int l;
struct sockaddr_in client_address;
memset (&client_address, 0, l =
sizeof (client_address));
r = accept (h, (struct sockaddr *)
&client_address, &l);
if (r < 0) {
perror ("accept()");
} else {
SHUT_FD1;
SHUT_FD2;
buf1_avail = buf1_written = 0;
buf2_avail = buf2_written = 0;
fd1 = r;
fd2 =
connect_socket (forward_port,
argv[3]);
if (fd2 < 0) {
SHUT_FD1;
} else
printf ("conexion desde %s\n",
inet_ntoa
(client_address.sin_addr));
}
}
/* NB: lee datos OOB antes de las lecturas normales */
if (fd1 > 0)
if (FD_ISSET (fd1, &er)) {
char c;
errno = 0;
r = recv (fd1, &c, 1, MSG_OOB);
if (r < 1) {
SHUT_FD1;
} else
send (fd2, &c, 1, MSG_OOB);
}
if (fd2 > 0)
if (FD_ISSET (fd2, &er)) {
char c;
errno = 0;
r = recv (fd2, &c, 1, MSG_OOB);
if (r < 1) {
SHUT_FD1;
} else
send (fd1, &c, 1, MSG_OOB);
}
if (fd1 > 0)
if (FD_ISSET (fd1, &rd)) {
r =
read (fd1, buf1 + buf1_avail,
BUF_SIZE - buf1_avail);
if (r < 1) {
SHUT_FD1;
} else
buf1_avail += r;
}
if (fd2 > 0)
if (FD_ISSET (fd2, &rd)) {
r =
read (fd2, buf2 + buf2_avail,
BUF_SIZE - buf2_avail);
if (r < 1) {
SHUT_FD2;
} else
buf2_avail += r;
}
if (fd1 > 0)
if (FD_ISSET (fd1, &wr)) {
r =
write (fd1,
buf2 + buf2_written,
buf2_avail -
buf2_written);
if (r < 1) {
SHUT_FD1;
} else
buf2_written += r;
}
if (fd2 > 0)
if (FD_ISSET (fd2, &wr)) {
r =
write (fd2,
buf1 + buf1_written,
buf1_avail -
buf1_written);
if (r < 1) {
SHUT_FD2;
} else
buf1_written += r;
}
/* comprueba si se han escrito tantos datos como se han leido */
if (buf1_written == buf1_avail)
buf1_written = buf1_avail = 0;
if (buf2_written == buf2_avail)
buf2_written = buf2_avail = 0;
/* si un extremo ha cerrado la conexion, continua escribiendo al otro
extremo hasta que no queden datos */
if (fd1 < 0
&& buf1_avail - buf1_written == 0) {
SHUT_FD2;
}
if (fd2 < 0
&& buf2_avail - buf2_written == 0) {
SHUT_FD1;
}
}
return 0;
}
El programa anterior reenvia correctamente la mayoria de los tipos de
conexiones TCP, incluyendo los datos OOB de senal transmitidos por los
servidores telnet. Tambien es capaz de manejar el dificil problema de
tener flujos de datos en ambas direcciones a la vez. Podria pensar que
es mas eficiente hacer una llamada fork() y dedicar un hilo a cada
flujo. Esto es mas complicado de lo que podria pensar. Otra idea es
activar E/S no bloqueante haciendo una llamada ioctl(). Esto tambien
tiene sus problemas ya que acaba teniendo que utilizar plazos de tiempo
(timeouts) ineficientes.
El programa no maneja mas de una conexion simultanea a la vez, aunque
podria extenderse facilmente para hacer esto con una lista ligada de
buffers - uno para cada conexion. Por ahora, una nueva conexion hace
que la conexion actual se caiga.
REGLAS DE SELECT
Muchas personas que intentan usar select se encuentran con un
comportamiento que es dificil de comprender y que produce resultados no
transportables o dudosos. Por ejemplo, el programa anterior se ha
escrito cuidadosamente para que no se bloquee en ningun punto, aunque
para nada ha establecido el modo no bloqueante en sus descriptores de
fichero (vea ioctl(2)). Es facil introducir errores sutiles que hagan
desaparecer la ventaja de usar select, por lo que voy a presentar una
lista de los aspectos esenciales a tener en cuenta cuando se use la
llamada select.
1. Siempre debe de intentar usar select sin un plazo de tiempo. Su
programa no debe tener que hacer nada si no hay datos
disponibles. El codigo que depende de los plazos de tiempo no es
normalmente portable y es dificil de depurar.
2. Para un resultado eficiente, el valor de nfds se debe calcular
correctamente de la forma que se explica mas abajo.
3. No debe anadir a ningun conjunto un descriptor de fichero para
el que no tenga intencion de comprobar su resultado (y responder
adecuadamente) tras una llamada a select. Vea la siguiente
regla.
4. Cuando select regrese, se deben comprobar todos los descriptores
de fichero de todos los conjuntos. Se debe escribir en cualquier
descriptor de fichero que este listo para ello, se debe leer de
cualquier descriptor de fichero que este listo para ello, etc.
5. Las funciones read(), recv(), write() y send() no leen/escriben
necesariamente todos los datos que haya solicitado. Si
leen/escriben todos los datos es porque tiene poco trafico y un
flujo muy rapido. Ese no va a ser siempre el caso. Debe hacer
frente al caso en el que sus funciones solo logren enviar o
recibir un unico byte.
6. Nunca lea/escriba byte a byte a menos que este realmente seguro
de que tiene que procesar una pequena cantidad de datos. Es
extremadamente ineficiente no leer/escribir cada vez tantos
datos como pueda almacenar. Los buffers del ejemplo anterior son
de 1024 bytes aunque podrian facilmente hacerse tan grandes como
el maximo tamano de paquete posible en su red local.
7. Ademas de la llamada select(), las funciones read(), recv(),
write() y send() pueden devolver -1 con un errno EINTR o EAGAIN
(EWOULDBLOCK) que no son errores. Estos resultados deben
tratarse adecuadamente (lo que no se ha hecho en el ejemplo
anterior). Si su programa no va a recibir ninguna senal,
entonces es muy poco probable que obtenga EINTR. Si su programa
no activa E/S no bloqueante, no obtendra EAGAIN. Sin embargo,
todavia debe hacer frente a estos errores por completitud.
8. Nunca llame a read(), recv(), write() o send() con una longitud
de buffer de cero.
9. Excepto como se indica en 7., las funciones read(), recv(),
write() y send() nunca devuelven un valor menor que 1 salvo
cuando se produce un error. Por ejemplo, un read() sobre una
tuberia donde el otro extremo ha muerto devuelve cero (al igual
que un error de fin de fichero), pero devuelve cero solo una vez
(un lectura o escritura posterior devolvera -1). Cuando
cualquiera de estas funciones devuelva 0 o -1, no debe pasar el
descriptor correspondiente a select nunca mas. En el ejemplo
anterior, cierro el descriptor inmediatamente y le asigno -1
para evitar que se vuelva a incluir en un conjunto.
10. El valor del plazo de tiempo debe inicializarse con cada nueva
llamada a select, ya que algunos sistemas operativos modifican
la estructura. pselect, sin embargo, no modifica su estructura
de plazo de tiempo.
11. He oido que la capa de conectores de Windows no sabe tratar
adecuadamente los datos OOB. Tampoco sabe tratar llamadas select
cuando ningun descriptor de fichero se ha incluido en ningun
conjunto. No tener ningun descriptor de fichero activo es una
forma util de domir a un proceso con una precision de menos de
un segundo usando el plazo de tiempo. (Mire mas abajo.)
EMULACI'ON DE USLEEP
En sistemas que no tienen una funcion usleep, puede llamar a select con
un plazo de espera finito y sin descriptores de fichero de la siguiente
manera:
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 200000; /* 0.2 segundos */
select (0, NULL, NULL, NULL, &tv);
Sin embargo, solo se garantiza que funcionara en sistemas Unix.
VALOR DEVUELTO
En caso de exito, select devuelve el numero total de descriptores que
estan presentes todavia en los conjuntos de descriptores de fichero.
Si se cumple el plazo de espera para select, los conjuntos de
descriptores de fichero deberian estar vacios (pero en algunos sistemas
puede que no sea asi). Sin embargo el valor devuelto sera
definitivamente cero.
Un valor devuelto de -1 indica un error, y la variable errno sera
modificada apropiadamente. En caso de error, el contenido de los
conjuntos devueltos y la estructura timeout es indefinido y no deberia
ser usado. pselect, sin embargo, no modifica nunca ntimeout.
ERRORES
EBADF Un conjunto contiene un descriptor de fichero no valido. Este
error ocurre a menudo cuando anade a un conjunto un descriptor
de fichero sobre el que ya se ha ejecutado la operacion close, o
cuando ese descriptor de fichero ya ha experimentado alguna
clase de error. Por tanto deberia dejar de anadir a los
conjuntos cualquier descriptor de fichero que devuelva un error
de lectura o escritura.
EINTR Una senal de interrupcion fue capturada, como SIGINT o SIGCHLD
etc. En este caso deberia reconstruir sus conjuntos de
descriptores de fichero y volverlo a intentar.
EINVAL Ocurre si nfds es negativo o si se especifica un valor
incorrecto para utimeout o ntimeout.
ENOMEM Fallo interno de reserva de memoria.
OBSERVACIONES
Generalmente hablando, todos los sistemas operativos que soportan
conectores, tambien soportan select. Algunas personas consideran que
select es una funcion esoterica y raramente usada. De hecho, muchos
tipos de programas se vuelven extremadamente complicados sin ella.
select puede utilizarse para solucionar muchos problemas de manera
eficiente y portable. Problemas que los programadores ingenuos tratan
de resolver usando hilos, procesos hijos, IPCs, senales, memoria
compartida y otros oscuros metodos. pselect es una funcion mas reciente
que es menos comunmente usada.
La llamada al sistema poll(2) tiene la misma funcionalidad que select,
pero con un comportamiento menos sutil. Es menos portable que select.
CONFORME A
4.4BSD (la funcion select aparecio por primera vez en 4.2BSD).
Generalmente portable a/desde sistemas no-BSD que soporten clones de la
capa de conector BSD (incluyendo variantes de System V). Sin embargo,
observe que la variante de System V establece normalmente la variable
timeout antes de salir, mientras que la variante de BSD no lo hace.
La funcion pselect esta definida en IEEE Std 1003.1g-2000 (POSIX.1g).
Se encuentra en glibc2.1 en adelante. Glibc2.0 tiene una funcion con el
mismo nombre, que sin embargo no acepta un parametro sigmask.
V'EASE TAMBI'EN
accept(2), connect(2), ioctl(2), poll(2), read(2), recv(2), select(2),
send(2), sigaddset(3), sigdelset(3), sigemptyset(3), sigfillset(3),
sigismember(3), sigprocmask(2), write(2)
AUTORES
Esta pagina de manual fue escrita por Paul Sheer.