Linux之select

658 阅读8分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

前言

接触这几个概念是在整理BIO和NIO的时候,开始只知道epoll和select都是I/O多路复用的技术,都可以实现同时监听多个I/O事件的状态。只知道select是轮询模式效率较低,epoll是事件监听模式效率更高,底层基于linux,其实也是云里雾里,特此出此系列文章作为自己进一步学习和整理。(笔者技术栈有限,linux底层不会涉及太深只是对概念/机制基本的了解。)

确定学习目标

  • select的本质是啥

select

我们这里看下man手册对应章节,并结合大佬的文章

名称

select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous
       I/O multiplexing

概要

 #include <sys/select.h>

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

       void FD_CLR(int fd , fd_set * set );
       int  FD_ISSET(int fd , fd_set * set );
       void FD_SET(int fd , fd_set * set );
       void FD_ZERO(fd_set * set );

       int pselect(int nfds , fd_set *restrict readfds ,
                  fd_set *restrict writefds , fd_set *restrict exceptfds ,
                  const struct timespec *restrict timeout ,
                  const sigset_t *restrict sigmask );

   Feature Test Macro Requirements for glibc (see
   feature_test_macros(7)):

       pselect():
           _POSIX_C_SOURCE >= 200112L

描述

警告:select()只能监控文件描述符编号小于FD_SETSIZE(1024)-一个不合理的下限对于许多现代应用-并且这种限制不会改变。所有现代应用程序都应该使用poll(2)或epoll(7),不受此限制。

select()允许程序监控多个文件描述符,等待一个或者多个文件描述符“就绪“用于某种类型的I/O操作(例如,可能的输入)。如果可以在没有阻塞的情况下执行相应的I/O操作(例如read(2)或足够小的write(2)),则认为文件描述符已准备就绪。

File descriptor sets

select()的主要参数是三个文件描述符"set"(以fd_set类型声明),它允许调用者在指定单位文件描述符set上等到三类事件。如果没有文件描述符要监听相应的事件类型,可以将fd_set参数传NULL。

注意: 在返回时,每个文件描述符集都被修改以指示哪些文件描述符当前“就绪”。 因此,如果在循环中使用 select(),则必须在每次调用之前重新初始化集合。

文件描述符集的内容可以使用以下宏进行操作:
FD_ZERO():此宏用于清除(从中删除所有文件描述符)set。它应该被调用作为初始化文件描述符set的第一步。

FD_SET():此宏将文件描述符fd添加到set中。添加一个已经存在于set中的集合时无操作的,不会产生错误。

FD_CLR():此宏将文件从set中删除描述符fd。删除一个不存在于set中的集合时无操作的,不会产生错误。

FD_ISSET():select()根据以下描述的规则对sets的内容进行修改。在调用select()之后,可以使用 FD_ISSET() 宏来测试文件描述符是否仍然存在于集合中。 如果文件描述符 fd 存在于集合中,则 FD_ISSET() 返回非零值,否则返回零。

参数

select()的参数如下:

readfds:此set中的文件描述符是被检测是否已经读取就绪。一个文件描述符如果读操作不会阻塞,则表示已经读取就绪;特别是,文件描述符在文件末尾也就绪好。

select()返回后,readfds将清除所有文件描述符,读取就绪的文件描述符除外。

writefds:此set中的文件描述符是被检测是否已经写就绪。一个文件描述符如果写操作不会阻塞,则表示已经写就绪;但是,即使文件描述符指示为可写,大写仍可能会阻塞。

select()返回后,writefds将清除所有文件描述符,写就绪的文件描述符除外。

exceptfds:此set中的文件描述符是被检测“异常情况”。 有关一些异常情况的示例,请参阅 poll(2) 中对 POLLPRI 的讨论。

select() 返回后,exceptfds 将清除所有文件描述符,但发生异常情况的文件描述符除外。

nfds:此参数应设置为三个set中任何一个中编号最高的文件描述符,加 1。表明每一个set中的文件描述符是被检测,直至此限制(但请参阅 BUGS)。

timeout:timeout 参数是一个 timeval 结构(如下所示),它指定 select() 应该阻塞等待文件描述符准备好的时间间隔。 调用将阻塞,直到:

  • 文件描述符准备就绪;
  • 调用被信号处理程序中断;
  • 超时到期。

请注意,超时间隔将四舍五入到系统时钟粒度,内核调度延迟意味着阻塞间隔可能会超出少量。

如果 timeval 结构的两个字段都为零,则 select() 立即返回。 (这对于轮询很有用。)

如果 timeout 被指定为 NULL,select() 会无限期地阻塞等待文件描述符准备好。

pselect()

pselect() 系统调用允许应用程序安全地等待,直到文件描述符准备好或直到捕获到信号。

select() 和 pselect() 的操作是相同的,除了这三个不同之处:

  • select() 使用的超时是 struct timeval(秒和微秒),而 pselect() 使用 struct timespec(秒和纳秒)。

  • select() 可能会更新 timeout 参数以指示还剩多少时间。 pselect() 不会更改此参数。

  • select() 没有 sigmask 参数,其行为与使用 NULL sigmask 调用的 pselect() 一样。

sigmask 是指向信号掩码的指针(参见 sigprocmask(2)); 如果不为 NULL,则 pselect() 首先将当前信号掩码替换为 sigmask 指向的掩码,然后执行“select”功能,然后恢复原始信号掩码。 (如果 sigmask 为 NULL,则在 pselect() 调用期间不会修改信号掩码。)

除了 timeout 参数的精度不同之外,以下 pselect() 调用:

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);

需要 pselect() 的原因是,如果想要等待信号或文件描述符准备好,则需要进行原子测试以防止竞争条件。(假设信号处理程序设置了一个全局标志并返回 . 然后,如果信号在测试之后但在调用之前到达,则对该全局标志的测试随后调用 select() 可能会无限期挂起。相比之下,pselect() 允许一个人首先阻塞信号,处理那些已经进入,然后使用所需的 sigmask 调用 pselect(),避免竞争。)

The timeout

select() 的 timeout 参数是以下类型的结构:

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

pselect() 的相应参数具有以下类型:

The corresponding argument for pselect() has the following type:

           struct timespec {
               time_t      tv_sec;         /* seconds */
               long        tv_nsec;        /* nanoseconds */
           };

在 Linux 上, select() 修改超时以反映未睡觉的时间量; 大多数其他实现不这样做。 (POSIX.1 允许任何一种行为。)这在Linux 代码读取timeout传输到其他操作系统时以及将代码移植到在循环中为多个 select() 重用 struct timeval 而不重新初始化它的 Linux 时都会导致问题。 考虑在 select() 返回后未定义超时。

返回值

成功时,select()和pselect()返回三个返回的文件描述符set包含的文件描述符的数量(即readfsd、writefds、exceptfds总数)。如果在任何文件描述符准备好之前超时过期,则返回值可能为零。

出错时返回-1,并设置errno 以指示错误;文件描述符集未修改,超时变为未定义。

错误

EBADF:在其中一组中给出了无效的文件描述符。(可能是已经关闭的文件描述符,或者发生了错误的文件描述符。)但是,请参阅 BUGS。

EINTR:一个信号被捕获;见信号(7)

EINVAL:nfds为负数或超过RLIMIT_NOFILE资源限制(参见getrlimit(2))。

EINVAL:timeout 中包含的值无效。

ENOMEM:无法为内部表分配内存。

版本

pselect () 在内核 2.6.16 中被添加到 Linux 中。在此之前, pselect () 是在 glibc 中模拟的(但请参阅 BUGS)

示例

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

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

           /* 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);
       }

小结

  • select()只能监控FD编号小于FD_SETSIZE(32个整数大小,应该是用bitmap表示fd的编号,32位机就是3232=1024,64位机就是6432=2048,大小可以修改/proc/sys/fs/file-max重新编译内核),许多现代应用使用poll(2)或epoll(7),不受此限制。
  • select()可以监控多个I/O事件的状态。
  • select()会阻塞调用直到:FD准备就绪、调用被信号处理程序中断、超时到期。
  • select()返回三个FD set就绪总数,没有就绪直接超时返回0,出错时返回-1。
  • pselect()相当于select()的升级:对超时单位进行了修改、不会更新timeout、增加sigmask参数避免死锁。

select本质

select使用轮询超时模式,无差别的轮询所有流,找出准备就绪的流进行操作。返回就绪好的流总数却不知道是那个流。效率很低。同步IO多路复用体现在select可以监控多个I/O事件的状态。