[Red informática] Interpretación de la API de socket de interfaz de programación de red (2)

 Socket es una API expuesta por la pila de protocolos de red a los programadores. En comparación con los complejos protocolos de red informática, la API abstrae operaciones clave y datos de configuración, lo que simplifica la programación del programa.

        El contenido del socket descrito en este artículo proviene de la herramienta man en la distribución de Linux centos 9, y habrá algunas diferencias entre otras plataformas (como os-x y diferentes versiones). Este artículo presenta principalmente cada API en detalle para comprender mejor la programación de sockets.


seleccionar

Cumplir con POSIX.1-2008

1.Biblioteca

标准 c 库,libc, -lc

2.Archivo de encabezado

<sys/select.h>

3.Definición de interfaz

int select(int nfds, fd_set *_Nullable restrict readfds,
                  fd_set *_Nullable restrict writefds,
                  fd_set *_Nullable restrict exceptfds,
                  struct timeval *_Nullable restrict timeout);

4.Descripción de la interfaz

        En primer lugar, debemos tener en cuenta que select solo puede escuchar menos de FD_SETSIZE (1024) descriptores de archivos, lo que parece muy irrazonable ahora. Si desea evitar esta restricción, debe usar poll o epool.

        select puede monitorear varios descriptores de archivos al mismo tiempo y regresará siempre que un descriptor de archivo requiera operación. Los requisitos de operación del descriptor de archivo significan que las operaciones de E/S relacionadas se pueden realizar inmediatamente, como lectura o una pequeña cantidad de operaciones de escritura.

    fd_set

        Una estructura que representa un conjunto de descriptores de archivos. Según los requisitos de POSIX, el número máximo de descriptores de archivos en la estructura es FD_SETSIZE.

    Conjunto de descriptores de archivo

        Los parámetros importantes de la interfaz select() son 3 conjuntos de descriptores de archivos (declarados con el tipo fd_set), lo que permite a la persona que llama esperar 3 tipos de eventos en el conjunto de descriptores de archivos especificado. Cada parámetro fd_set puede ser NULL, siempre que ningún conjunto de descriptores de archivos necesite escuchar el evento correspondiente.

        Vale la pena señalar que una vez que regresa la interfaz, cada conjunto de descriptores de archivos se actualiza para indicar qué descriptores de archivos están listos. Por lo tanto, si se usa select() en un bucle, la colección debe reinicializarse antes de cada llamada.

        El contenido de un conjunto de descriptores de archivos se puede manipular utilizando las siguientes macros:

        FD_ZERO ()

        Esta macro se utiliza para borrar todos los descriptores de archivos del conjunto y es el primer paso para inicializar el conjunto de descriptores de archivos.

        FD_SET ()

        Esta macro se utiliza para agregar un descriptor de archivo a la colección. Si el descriptor de archivo ya existe, no se informará ningún error, pero no se realizará ninguna operación.

        FD_CLR ()

        Esta macro se utiliza para eliminar el descriptor de archivo especificado de la colección; si el descriptor de archivo no existe, no haga nada.

        FD_ISSET ()

        select() actualiza el contenido de la colección de acuerdo con las siguientes reglas: Después de completar la llamada select(), la macro FD_ISSET() se usa para detectar si el descriptor de archivo especificado todavía está en la colección. Si existe, devuelve un Valor -0, de lo contrario devuelve 0.

5. Parámetros

(1) lecturas

        Los descriptores de archivos de este conjunto se utilizan para controlar si están listos para su lectura. Una descripción de archivo lista para lectura significa que la operación de lectura no se bloqueará. En particular, EOF también se considera lista para lectura.

        Después de que regrese la función select(), solo los descriptores de archivos listos para lectura se conservarán en readfds y los demás se eliminarán.

(2) escrituras

        Los descriptores de archivos de este conjunto se utilizan para controlar si están listos para escribir. Una descripción de archivo lista para escribir significa que las operaciones de escritura no se bloquearán. Sin embargo, incluso si un descriptor de archivo está listo para escribir, es posible que se bloqueen operaciones de escritura de gran tamaño.

        Después de que regrese la función select(), solo los descriptores de archivos listos para escritura se conservarán en writefds y los demás se eliminarán.

(3) eceptfds

        Los descriptores de archivos de este conjunto se utilizan para monitorear excepciones. Algunos ejemplos de excepciones se analizan en POLLPRI para poll().

        Después de que select() regresa, solo los descriptores de archivo donde ocurrieron excepciones se retienen en exceptfds.

(4) nfd

       Este parámetro debe establecerse en el número máximo de descriptores de archivos en los 3 conjuntos más 1.

(5) tiempo de espera

        timeout es una estructura de valor de tiempo que especifica el tiempo que select() espera hasta que el descriptor de archivo esté listo. Esta interfaz se bloqueará hasta que ocurran los siguientes eventos:

  • descriptor de archivo listo
  • La llamada fue interrumpida por manejo de señal.
  • tiempo de espera

        Vale la pena señalar que el valor del tiempo de espera se redondeará a la granularidad del reloj del sistema. Además, debido a retrasos en la programación del sistema, el intervalo de bloqueo puede ser ligeramente mayor que el tiempo de espera.

        Si ambos miembros del tiempo de espera son 0, la selección regresa inmediatamente (generalmente se usa para sondear).

        Si el tiempo de espera es NULL, la selección esperará indefinidamente hasta que un descriptor de archivo esté listo.

6.pseleccionar()

        La llamada al sistema pselect() permite a las aplicaciones esperar de forma segura a que un descriptor de archivo esté listo o que se produzca una señal.

        Es lo mismo que select(), excepto en los siguientes lugares:

  • select() usa el tiempo de espera de la estructura timeval, mientras que pselect() usa el tiempo de espera de la estructura timespec.
  • select() puede actualizar el parámetro de tiempo de espera para indicar cuánto tiempo queda, pero pselect() no lo hará.
  • select() no tiene señal para enmascarar el parámetro sigmask, lo que equivale a que el parámetro sigmask de pselect sea NULL

        sigmask es un puntero a la máscara de señal. Si no está vacío, pselect() primero lo usará para reemplazar la máscara de señal actual, luego seleccionará() y finalmente restaurará la máscara de señal original. Si es NULL, la llamada a pselect() no cambiará el valor de la máscara de señal.

        Excepto por la diferencia en la precisión del tiempo, los siguientes códigos en ambos extremos son equivalentes:

  ready = pselect(nfds, &readfds, &writefds, &exceptfds,
                           timeout, &sigmask);

        

sigset_t origmask;

pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
ready = select(nfds, &readfds, &writefds, &exceptfds, timeout);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);

        La razón por la que se diseñó pselect() es que si desea esperar a que se produzca una señal o que un descriptor de archivo esté listo, necesita una prueba atómica para resolver el problema de la carrera de datos. Por ejemplo, si una función de procesamiento de señales establece una bandera y la devuelve, si la señal llega cerca de la prueba y provoca competencia de datos, la prueba de esta bandera después de select() puede quedar bloqueada indefinidamente. Y pselect() permite bloquear señales primero, procesar las señales que han ocurrido y luego usar la máscara sigma especificada para llamar a pselect(), evitando la competencia de datos.

        se acabó el tiempo

        La estructura de tiempo de espera de select() se define de la siguiente manera:

           struct timeval {
               time_t      tv_sec;         /* seconds */
               suseconds_t tv_usec;        /* microseconds */
           };

        La estructura correspondiente a pselect() es timespec.

Select() en sistemas Linux modificará el valor del tiempo de espera para reflejar el tiempo sin suspensión, pero otras implementaciones no hacen esto. POSIX.1 considera legal cualquier comportamiento. Esto causará problemas de portabilidad entre sistemas Linux y otros sistemas, por lo que deberíamos pensar que el tiempo de espera es un valor desconocido después de select().

7.Valor de retorno

        En caso de éxito, select() y pselect() devuelven el número total de descriptores de archivos en los tres conjuntos de descriptores de archivos devueltos (es decir, redfds, writefds y exceptfds se establecen en 1 dígito). El valor de retorno puede ser 0, lo que indica que el tiempo de espera expira antes de que un descriptor de archivo esté listo.

        Cuando ocurre un error, se devuelve -1 y se configura errno para indicar el tipo de error. El conjunto de descriptores de archivos no se modifica y el valor de tiempo de espera no está definido.

        El valor del error se define de la siguiente manera:

EBADF Hay descriptores de archivos ilegales en la colección, como descriptores de archivos cerrados o descriptores de archivos con errores). Para obtener más información, consulte ERRORES.
EINTR Se captura una señal; consulte la señal (7) para obtener más detalles.
ELECCIÓN ÚNICA nfds es un valor negativo o excede el límite de recursos RLIMIT_NOFILE. Para obtener más información, consulte getrlimit(2)
ELECCIÓN ÚNICA El valor del tiempo de espera es ilegal.
enomema No hay suficiente memoria para asignar la tabla interna

En otros sistemas UNIX, select() puede devolver un error EAGAIN en lugar de ENOMEM si el sistema no puede asignar recursos del kernel. POSIX define este error para poll(), pero no para select(). Teniendo en cuenta la portabilidad del programa, EGAIN debe comprobarse y volver a llamarse, al igual que EINTR.

8.Atención

        <sys/time.h> también proporciona la definición de fd_set. fd_set es un búfer de tamaño fijo. Ejecutar FD_CLR y FD_SET y pasar un valor negativo o fd mayor que FD_SETSIZE conducirá a resultados impredecibles. Además, POSIX requiere que fd sea un descriptor de archivo válido.

        Las operaciones select() y pselect() no se ven afectadas por el indicador O_NONBLOCK.

        puntas de auto-tubo

        En sistemas sin una implementación de pselect(), se puede lograr una captura de señal confiable (y más portátil) mediante el truco del self-pipe. Esta técnica escribe 1 byte en una tubería en la función de manejo de señales, y select() escucha el otro extremo de la tubería. Para evitar el bloqueo de escritura total y el bloqueo de lectura vacía, la canalización debe leerse y escribirse en modo de E/S sin bloqueo.

        Simular nuestro sueño

        Antes de dormir, algún código usaba select() para implementar un retraso de precisión portátil de menos de un segundo, estableciendo todas las colecciones en vacío, nfds en 0 y valores de tiempo de espera no vacíos.

        Mapeo de notificaciones entre select() y poll()

        En el árbol de código de Linux, podemos encontrar la conexión entre la lectura, escritura, notificación de excepción de select() y notificación de eventos poll()/epoll():

           #define POLLIN_SET  (EPOLLRDNORM | EPOLLRDBAND | EPOLLIN |
                                EPOLLHUP | EPOLLERR)
                              /* Ready for reading */
           #define POLLOUT_SET (EPOLLWRBAND | EPOLLWRNORM | EPOLLOUT |
                                EPOLLERR)
                              /* Ready for writing */
           #define POLLEX_SET  (EPOLLPRI)
                              /* Exceptional condition */

        Aplicación multiproceso

        Si otro contexto cierra un descriptor de archivo que un hilo escucha a través de select(), el resultado es desconocido. En algunos sistemas UNIX, select() dejará de bloquearse y regresará, indicando al descriptor de archivo que está listo (las operaciones posteriores generarán un error a menos que otro hilo abra nuevamente el descriptor de archivo y esté listo). En Linux y otros sistemas, cerrar el descriptor de archivo mediante otros subprocesos no tiene ningún efecto en select(). En resumen, si la aplicación se basa en estos comportamientos específicos, provocará errores.

        Diferencias entre la biblioteca C y el kernel

        El kernel de Linux permite que el conjunto de descriptores de archivos sea de cualquier tamaño, y el tamaño específico está determinado por el valor de nfds. Y glibc establece el tipo fs_set en un valor fijo. Consulte ERRORES.

        La interfaz pselect() de la que estamos hablando aquí está implementada por glibc. El nombre de la llamada al sistema subyacente es pselect6(). El comportamiento de la llamada al sistema es ligeramente diferente al de pselect().

        La llamada al sistema pselect6() de Linux modifica el parámetro de tiempo de espera; sin embargo, glibc oculta este comportamiento almacenando en caché localmente el valor del tiempo de espera. Por lo tanto, glibc pselect6() no modifica el parámetro de tiempo de espera, que también cumple con los requisitos POSIX.1-2001.

        El último parámetro de la llamada al sistema pselect6() no es del tipo de puntero sigset_t *, sino que tiene el siguiente formato:

           struct {
               const kernel_sigset_t *ss;   /* Pointer to signal set */
               size_t ss_len;               /* Size (in bytes) of object
                                               pointed to by 'ss' */
           };

        Esto permite que la llamada al sistema obtenga el puntero establecido del semáforo y su tamaño, y tiene en cuenta el hecho de que la mayoría de los sistemas admiten un máximo de 6 argumentos de llamada al sistema. Para conocer las diferencias en el procesamiento de señales, consulte la discusión sobre sigprocmask.

        detalles históricos glibc

        gblic 2.0 proporciona una versión incorrecta de pselect(), que no tiene un parámetro sigmask.

        glibc 2.1 a 2.2.1, para obtener la declaración pselect() en <sys/select.h>, se debe definir la macro _GNU_SOURCE.

9.ERRORES

        POSIX permite que las implementaciones definan el límite superior de descriptores de archivos en el descriptor de archivo establecido a través de FD_SETSIZE. El kernel de Linux no tiene límite, pero la implementación de glibc establece fd_set en una longitud fija y establece FD_SETSIZE en 1024. La macro FD_*() opera de acuerdo a este límite. Para monitorear más de 1023 descriptores de archivos, use poll() o epoll.

        Los atributos de entrada y salida del parámetro fd_set tienen un diseño incorrecto, que se ha corregido en poll() y epoll().

        De acuerdo con los requisitos de POSIX, select() debe verificar que los descriptores de archivos en todos los conjuntos no puedan exceder nfds - 1; sin embargo, la implementación actual ignora aquellos descriptores de archivos con valores mayores que el valor máximo del descriptor de archivo abierto por el proceso actual. Según los requisitos de POSIX, estos descriptores de archivos pueden provocar errores de EBADF.

        A partir de glibc 2.1, glibc usa sigprocmask() y select() para implementar la simulación pselect(). Esta implementación deja atrás el problema de carrera de datos resuelto por pselect(). Las versiones modernas de glibc suelen utilizar la llamada al sistema pselect() proporcionada por el kernel, que es inmune a las carreras de datos.

        En Linux, select() puede informar que el descriptor del archivo socket está listo para ser leído, pero las lecturas posteriores se bloquearán. Esto ocurre a menudo cuando se han alcanzado los datos pero la suma de comprobación de los datos es incorrecta y los datos se descartan. Por supuesto, también podría tratarse de un falso positivo. Por lo tanto, es más seguro utilizar sockets O_NONBLOCK.

        select() en Linux actualiza el valor del tiempo de espera cuando es interrumpido por una señal, lo que POSIX.1 no permite. pselect() de Linux tiene el mismo comportamiento, pero glibc oculta este comportamiento. 

10.Ejemplos de código

       #include <stdio.h>
       #include <stdlib.h>
       #include <sys/select.h>

       int
       main(void)
       {
           int             retval;
           fd_set          rfds;
           struct timeval  tv;

           /* Watch stdin (fd 0) to see when it has input. */

           FD_ZERO(&rfds);
           FD_SET(0, &rfds);

           /* Wait up to five seconds. */

           tv.tv_sec = 5;
           tv.tv_usec = 0;

           retval = select(1, &rfds, NULL, NULL, &tv);
           /* Don't rely on the value of tv now! */

           if (retval == -1)
               perror("select()");
           else if (retval)
               printf("Data is available now.\n");
               /* FD_ISSET(0, &rfds) will be true. */
           else
               printf("No data within five seconds.\n");

           exit(EXIT_SUCCESS);
       }

Supongo que te gusta

Origin blog.csdn.net/BillyThe/article/details/132751807
Recomendado
Clasificación