Provided by:
manpages-es_1.55-9_all 
NOMBRE
select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - multiplexación de
E/S síncrona
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ÓN
select (o pselect) es la función eje de la mayor parte de programas en
C que manejan más de un descriptor de fichero (o manejador de conector)
simultáneamente 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 más de los descriptores de
fichero. Un "cambio de estado" se produce cuando se vuelven
disponibles más carácteres del descriptor de fichero, o cuando se
dispone de espacio en los buffers internos del núcleo para escribir más
carácteres en el descriptor de fichero, o cuando un descriptor de
fichero provoca un error (en el caso de un conector o tubería se da
cuando se cierra el otro extremo de la conexión).
En resumen, select tan sólo vigila varios descriptores de fichero, y es
la llamada estándar 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 función
que se utiliza sobre un conjunto recién declarado. A partir de aquí,
aquellos descriptores de fichero individuales en los que esté
interesado pueden ser añadidos uno por uno con FD_SET. select modifica
el contenido de los conjuntos de acuerdo a las reglas descritas abajo;
después de invocar a select puede comprobar si su descriptor de fichero
está aún presente en el conjunto con la macro FD_ISSET. FD_ISSET
devuelve un valor distinto de cero si el descriptor está presente y
cero si no lo está. 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. Después
de que select regrese, borrará de readfds todos los descriptores
de fichero salvo aquellos sobre los que pueda ejecutarse
inmediatamente una operación de lectura con una llamada a recv()
(para conectores) o read() (para tuberías, ficheros y
conectores).
writefds
Este conjunto es observado para ver si hay espacio para escribir
datos en cualquiera de sus descriptores de fichero. Después de
que select regrese, borrará de writefds todos los descriptores
de fichero salvo aquellos sobre los que se pueda ejecutar
inmediatamente una operación de escritura con una llamada a
send() (para conectores) o write() (para tuberías, ficheros y
conectores).
exceptfds
Este conjunto es observado para las excepciones o errores sobre
cualquiera de sus descriptores de fichero. Sin embargo,
realmente es sólo 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 sólo se aplica realmente a
conectores. Vea el contenido de recv(2) y send(2) sobre este
tema. Después de que select regrese, borrará de exceptfds todos
los descriptores de fichero salvo aquellos sobre los que se
puede leer datos OOB. Sólo puede leer un byte de datos OOB de
todas maneras (con la operación 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 más del máximo de cualquier
descriptor de fichero en cualquiera de los conjuntos. En otras
palabras, mientras está atareado añadiendo descriptores de
fichero a sus conjuntos, debe calcular el máximo valor entero de
todos ellos, incrementar este valor en uno, y pasarlo como nfds
a select.
utimeout
Es el máximo valor de tiempo que select debe esperar antes de
regresar, incluso si nada interesante ocurrió. Si este valor se
pasa como NULL, select se bloqueará indefinidamente esperando un
evento. utimeout puede ser puesto a cero segundos, lo que
provoca que select regrese inmediatamente. La estructura struct
timeval está 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 precisión de nanosegundos como sigue,
struct timespec {
long tv_sec; /* segundos */
long tv_nsec; /* nanosegundos */
};
sigmask
Este argumento contiene un conjunto de señales 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 señales permitidas en la entrada y la salida de la
función. Se comportará igual que select.
COMBINANDO SEÑALES Y EVENTOS DE DATOS
pselect debe ser usada si está esperando una señal así como datos de un
descriptor de fichero. Los programas que reciben señales como eventos
normalmente utilizan el manejador de señales para activar una bandera
global. La bandera global indicará que el evento debe ser procesado en
el bucle principal del programa. Una señal provocará que la llamada a
select (o pselect) regrese tomando la variable errno el valor EINTR.
Este comportamiento es esencial para que las señales puedan ser
procesadas en el bucle principal del programa, de otra manera select se
bloquearía indefinidamente. Ahora, en algún lugar del bucle principal
habrá una condición para comprobar la bandera global. Así que debemos
preguntarnos: ¿qué ocurre si una señal llega después de la condición,
pero antes de la llamada a select? La respuesta es que select se
bloquearía indefinidamente, incluso aún si hay un evento pendiente.
Esta condición de carrera se soluciona con la llamada pselect. Esta
llamada puede utilizarse para enmascarar señales que no van a ser
recibidas salvo dentro de la llamada pselect. Por ejemplo, digamos que
el evento en cuestión fue la salida de un proceso hijo. Antes del
comienzo del bucle principal, bloquearíamos SIGCHLD usando sigprocmask.
Nuestra llamada pselect podría habilitar SIGCHLD usando la máscara de
señal virgen. Nuestro programa se podría parecer a ésto:
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 aquí */
}
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 todavía queda la posibilidad de que una señal pueda llegar después
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
núcleo de Linux no tiene todavía una llamada al sistema pselect nativa
por lo que probablemente todo esto sea nada más que hablar por hablar.
PRÁCTICA
Por lo tanto, ¿cuál es el propósito de select? ¿No 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 tuberías y conectores de
manera simultánea. Los programadores de Unix a menudo se encuentran en
la situación de manejar la E/S de más de un descriptor de fichero donde
el flujo de datos puede ser intermitente. Si tan sólo creara una
secuencia de llamadas read y write, podría encontrarse con que una de
sus llamadas puede bloquearse esperando datos de/a un descriptor de
fichero, mientras que otro descriptor de fichero está siendo
inutilizado aunque haya datos disponibles. select maneja
eficientemente esta situación.
Un ejemplo típico de select lo podemos encontrar en la página 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 confíe en el valor de tv por ahora! */
if (retval)
printf("Los datos ya están disponibles.\n");
/* FD_ISSET(0, &rfds) será verdadero. */
else
printf("No ha habido datos en cinco segundos.\n");
exit(0);
}
EJEMPLO DE REDIRECCIÓN DE PUERTOS
Aquí viene un ejemplo que ilustra mejor la verdadera utilidad de
select. El listado de abajo es un programa de reenvío 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 dirección 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-dirección-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 ("conexión 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 leído */
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 conexión, continúa 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 reenvía correctamente la mayoría de los tipos de
conexiones TCP, incluyendo los datos OOB de señal transmitidos por los
servidores telnet. También es capaz de manejar el difícil problema de
tener flujos de datos en ambas direcciones a la vez. Podría pensar que
es más eficiente hacer una llamada fork() y dedicar un hilo a cada
flujo. Esto es más complicado de lo que podría pensar. Otra idea es
activar E/S no bloqueante haciendo una llamada ioctl(). Esto también
tiene sus problemas ya que acaba teniendo que utilizar plazos de tiempo
(timeouts) ineficientes.
El programa no maneja más de una conexión simultánea a la vez, aunque
podría extenderse fácilmente para hacer esto con una lista ligada de
buffers - uno para cada conexión. Por ahora, una nueva conexión hace
que la conexión actual se caiga.
REGLAS DE SELECT
Muchas personas que intentan usar select se encuentran con un
comportamiento que es difícil 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 ningún punto, aunque
para nada ha establecido el modo no bloqueante en sus descriptores de
fichero (vea ioctl(2)). Es fácil 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 código que depende de los plazos de tiempo no es
normalmente portable y es difícil de depurar.
2. Para un resultado eficiente, el valor de nfds se debe calcular
correctamente de la forma que se explica más abajo.
3. No debe añadir a ningún conjunto un descriptor de fichero para
el que no tenga intención 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 esté listo para ello, se debe leer de
cualquier descriptor de fichero que esté 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 tráfico y un
flujo muy rápido. Ese no va a ser siempre el caso. Debe hacer
frente al caso en el que sus funciones sólo logren enviar o
recibir un único byte.
6. Nunca lea/escriba byte a byte a menos que esté realmente seguro
de que tiene que procesar una pequeña 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 podrían fácilmente hacerse tan grandes como
el máximo tamaño de paquete posible en su red local.
7. Además 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 señal,
entonces es muy poco probable que obtenga EINTR. Si su programa
no activa E/S no bloqueante, no obtendrá EAGAIN. Sin embargo,
todavía 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
tubería donde el otro extremo ha muerto devuelve cero (al igual
que un error de fin de fichero), pero devuelve cero sólo una vez
(un lectura o escritura posterior devolverá -1). Cuando
cualquiera de estas funciones devuelva 0 o -1, no debe pasar el
descriptor correspondiente a select nunca más. 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 oído que la capa de conectores de Windows no sabe tratar
adecuadamente los datos OOB. Tampoco sabe tratar llamadas select
cuando ningún descriptor de fichero se ha incluido en ningún
conjunto. No tener ningún descriptor de fichero activo es una
forma útil de domir a un proceso con una precisión de menos de
un segundo usando el plazo de tiempo. (Mire más abajo.)
EMULACIÓN DE USLEEP
En sistemas que no tienen una función 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, sólo se garantiza que funcionará en sistemas Unix.
VALOR DEVUELTO
En caso de éxito, select devuelve el número total de descriptores que
están presentes todavía en los conjuntos de descriptores de fichero.
Si se cumple el plazo de espera para select, los conjuntos de
descriptores de fichero deberían estar vacíos (pero en algunos sistemas
puede que no sea así). Sin embargo el valor devuelto será
definitivamente cero.
Un valor devuelto de -1 indica un error, y la variable errno será
modificada apropiadamente. En caso de error, el contenido de los
conjuntos devueltos y la estructura timeout es indefinido y no debería
ser usado. pselect, sin embargo, no modifica nunca ntimeout.
ERRORES
EBADF Un conjunto contiene un descriptor de fichero no válido. Este
error ocurre a menudo cuando añade a un conjunto un descriptor
de fichero sobre el que ya se ha ejecutado la operación close, o
cuando ese descriptor de fichero ya ha experimentado alguna
clase de error. Por tanto debería dejar de añadir a los
conjuntos cualquier descriptor de fichero que devuelva un error
de lectura o escritura.
EINTR Una señal de interrupción fue capturada, como SIGINT o SIGCHLD
etc. En este caso debería 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, también soportan select. Algunas personas consideran que
select es una función esotérica 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, señales, memoria
compartida y otros oscuros métodos. pselect es una función más reciente
que es menos comúnmente 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 función select apareció 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 función pselect está definida en IEEE Std 1003.1g-2000 (POSIX.1g).
Se encuentra en glibc2.1 en adelante. Glibc2.0 tiene una función con el
mismo nombre, que sin embargo no acepta un parámetro sigmask.
VÉASE TAMBIÉN
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 página de manual fue escrita por Paul Sheer.