【 Linux 网络编程 】Linux 下 select 的使用

一、相关函数

NAME
       select, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing 同步多路IO转接

SYNOPSIS
       /* According to POSIX.1-2001, POSIX.1-2008 */
       #include <sys/select.h>

       /* According to earlier standards */
       #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 *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);
       
struct timeval {
               long    tv_sec;         /* seconds */
               long    tv_usec;        /* microseconds */
           };
the two fields of a timeval structure are typed as long 

RETURN VALUE 返回值

On success, select() return the number of file descriptors contained in the three returned descriptor sets (that is, the total number of bits that are set in readfds, writefds, exceptfds) which may be zero if the timeout expires before anything interesting happens.
成功的情况下,select会返回所有文件描述符集合的总数和,(意思就是,会返回三个集合中的文件描述符总数。)如果是因为超时返回,则返回0。

On error, -1 is returned, and errno is set to indicate the error; the file descriptor sets are unmodified, and timeout becomes undefined.
在错误的情况下,会返回-1,并且errno会被设置来指示对应的错误,返回后的文件描述符不会有任何变化,但是timeout字段会变为未定义状态。

DESCRIPTION 描述

select() allow a program to monitor multiple file descriptors, waiting until one or more of the file descriptors become “ready” for some class of I/O operation
select函数允许程序监视多个文件描述符,直到一个或多个文件描述符满足某些IO操作,函数才返回。

A file descriptor is considered ready if it is possible to perform a corresponding I/O operation without blocking.
如果一个文件描述符可以无阻塞的进行某些IO操作,那么我们就认为这个文件描述符变为ready状态。

select() can monitor only file descriptors numbers that are less than FD_SETSIZE;
select仅仅只能监视小于FD_SETSIZE个文件描述符。

Three independent sets of file descriptors are watched.
在函数参数中有三个独立的被监视的文件描述符集合。

The file descriptors listed in readfds will be watched to see if characters become available for reading (more precisely, to see if a read will not block; in particular, a file descriptor is also ready on end-of-file).
readfds集合,用来监视这个集合中是否有文件描述符可以进行读操作,(更精确地说,这样会使read操作不会阻塞,【read操作也是会阻塞的】,特殊情况,如果遇到了文件末尾,仍然认为这个文件描述符读可用,只不过此时read返回的n为0,此操作用来判断对端是否关闭连接。)

The file descriptors in writefds will be watched to see if space is available for write (though a large write may still block).
writefds文件描述符集合,用来监视缓冲区是否有空间进行写操作,(虽然大块的写操作仍然会被阻塞)

The file descriptors in exceptfds will be watched for exceptional conditions. (For examples of some exceptional conditions)
exceptfds文件描述符集合用来监视一些例外情况。

On exit, each of the file descriptor sets is modified in place to indicate which file descriptors actually changed status. (Thus, if using select() within a loop, the sets must be reinitialized before each call.)
在函数返回的时候,对应的文件描述符集合会被改变,【也就是这些参数是传入-传出参数】。(虽然,我们需要在一个循环中调用select,但是每次调用之前,都需要再次初始化这些传入的文件描述符集合)

Each of the three file descriptor sets may be specified as NULL if no file descriptors are to be watched for the corresponding class of events.
如果不想监视对应的文件描述符集合,可以传入NULL参数。

为了对文件描述符集合进行操作,设定了一些宏,通过这些宏来操作文件描述符集合。

FD_ZERO() clears a set. 用来清空一个文件描述符集合

FD_SET() and FD_CLR() respectively add and remove a given file descriptor from a set.
FD_SET和FD_CLR对应的添加和移除一个文件描述符到对应的集合中

FD_ISSET() tests to see if a file descriptor is part of the set; this is useful after select() returns.
FD_ISSET函数用来判断一个文件描述符是否在文件描述符集合中,这个操作在select return之后很有用。

nfds should be set to the highest-numbered file descriptor in any of the three sets, plus 1. The indicated file descriptors in each set are checked, up to this limit (but see BUGS).
nfds应该被设定为三个集合中的最大描述符的值+1,这个被用来循环检查这些文件描述符。

The timeout argument specifies the interval that select() should block waiting for a file descriptor to become ready.
timeout参数用来指示select一次阻塞等待的超时时长。

The call will block until either:
select调用会一直阻塞,除非出现以下情况:

  • a file descriptor becomes ready; 有一个文件描述符变为就绪状态

  • the call is interrupted by a signal handler; 被信号处理函数中断

  • the timeout expires. 超时

Note that the timeout interval will be rounded up to the system clock granularity, and kernel scheduling delays mean that the blocking interval may overrun by a small amount.
注意,超时间隔将被舍入到系统时钟粒度,而内核调度延迟意味着阻塞间隔可能会超出一小部分。

If both fields of the timeval structure are zero, then select() returns immediately. (This is useful for polling.)
如果对于传入的timeval参数,两个属性均为0,则select进行一次收集之后,会立即返回,(这对于轮询来说非常管用)

If timeout is NULL (no timeout), select() can block indefinitely.
如果传入的timeout参数是NULL,则select会永远阻塞下去,直到有文件描述符满足对应的事件。

NOTES 注意

An fd_set is a fixed size buffer. Executing FD_CLR() or FD_SET() with a value of fd that is negative or is equal to or larger than FD_SETSIZE will result in undefined behavior.
一个fd_set文件描述符集合是一个固定长度的buffer,如果在执行FD_CLR或FD_SET的时候使用了负数,或者超过了FD_SETSIZE的话,会导致未定义行为。

巧用:
Some code calls select() with all three sets empty, nfds zero, and a non-NULL timeout as a fairly portable way to sleep with subsecond precision.
有些程序在调用select的时候传入的三个集合都NULL,nfds是0,以及一个非空的timeout,这是合法的操作,这样会实现一个精度为微秒级别的sleep操作。

On Linux, select() modifies timeout to reflect the amount of time not slept; most other implementations do not do this.
在Linux系统上,select使用timeout来反映剩余的沉睡时间,然而其他系统的实现并不会这样做。

二、正文

好了,终于把Linux中man page对于select的相关介绍翻译完了,接下来就说一说我对于select的看法:

select又叫做同步多路IO转接,这句话啥意思呢,就是,我们之前写的无论是基于多线程的还是多进程的TCP代码,都是一个线程或者一个进程对应一个连接,本质上也是一对一的,一个线程负责一个连接描述符(连接描述符以后简称cfd,对于监听描述符以后简称为lfd)这样的话,如果我们需要很多个cfd,相应的就要创建对应数目的线程。这就很麻烦,也比较乱。

然后select就产生了,我个人感觉select的意思就是批处理,相当于基数排序,每次select都有分配,select会进行一次收集,收集所有的可用的文件描述符。这些文件描述符都是统一管理的。

也就是说select是维持一个进程,在当前进程下可以建立很多个cfd,这些cfd我们可以本地保存下来,然后将他们托管给select,我们就进行select收集就行了,每次循环只处理select收集到的文件描述符。

这样就是同步多路IO转接,多路IO转接的意思,是我们将fd描述符转接给select,select收集后再转接给我们自己的代码,至于同步的意思是,select只负责给我们进行监听事件的发生,不负责处理事件,处理读取或者写入事件,如read,recvfrom,操作都是需要先阻塞在系统调用上,都是需要先将数据从内核拷贝到用户空间,而write,sendto操作,都是需要先将数据从用户空间拷贝到内核空间。

三、五种IO模型

对于一个I/O操作,如输入操作,一般都分为两个阶段:
(1)等待数据准备好
(2)从内核向进程复制数据
对于一个套接字的输入操作,第一步通常是等待数据从网络到达。当所等待的分组到达后,它被复制到内核的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

1. 阻塞式I/O【blocking I/O】

默认情况下,所有的套接字都是阻塞的。如下的UDP recvfrom函数:
在这里插入图片描述
recvfrom函数作为系统调用,会从应用进程空间运行切换到内核空间中运行,一段时间后再切换回来。

如上图,recvfrom系统调用直到数据包到达并且复制到用户空间后才返回。期间recvfrom一直都是阻塞状态。

2. 非阻塞式I/O 【nonblocking I/O】

非阻塞式I/O的实现,是基于套接字的配置的,我们在一开始将套接字设置成非阻塞状态,然后当recvfrom每次进行一次系统调用的时候,如果没有数据,则返回一个错误,而不是将recvfrom阻塞。
在这里插入图片描述
如图,当内核中没有数据包的时候,内核返回一个EWOULDBLOCK错误。像这样,对一个非阻塞的描述符循环调用recvfrom的时候,称之为轮询操作(polling)。应用进程持续轮询内核,以查看某个操作是否就绪。但是这么做会浪费大量的CPU时间。

3. I/O复用(select和poll)【I/O multiplexing】

I/O复用,就是之前我提到的批处理,I/O复用并不是非阻塞模式,它也会阻塞,只不过它阻塞在select上,而不是阻塞在I/O系统调用(recvfrom等)上。
在这里插入图片描述
如图,这种操作对于单个描述符来说,貌似并没有什么优势,甚至还有一些劣势,因为它调用了两次系统调用,这样会造成多一次的上下文切换开销。不过在进行多个描述符处理的时候,select就显得很有优势了。

4. 信号驱动式I/O(SIGIO)【signal-driven I/O】

利用信号驱动的I/O模型,让内核在描述符就绪的时候通过发送SIGIO信号通知。
在这里插入图片描述
对于信号驱动I/O,我们需要配置套接字的信号驱动式I/O功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用立即返回,之后我们的进程可以继续干自己的工作,也就是进程没有阻塞,当数据报到达,并准备好读取的时候,内核就发送一个SIGIO信号,来通知用户程序进行数据报接收,我们在信号处理函数中可以调用recvfrom函数进行接收数据。随后可以通知主循环,数据已经收到。

使用信号驱动模式,优势就是在等待数据包到达期间,主进程不阻塞,只需要等待来自信号处理函数的通知:既可以式数据包已经准备好被处理(recvfrom函数在信号处理函数中),也可以是数据包已准备好被读取(recvfrom函数在主进程中,信号处理函数仅仅只是提供一个信号给主进程)

5. 异步I/O(POSIX的aio_系列函数)【asynchronous I/O】

aio_系列函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。

异步I/O模式和信号驱动模式的区别:

  • 异步I/O模式是内核通知我们I/O操作何时完成了。
  • 信号驱动模式是内核通知我们何时可以开始进行I/O操作。
    在这里插入图片描述
    可以看到aio_系列函数,单独成一个系列,与read、recvfrom等独立。aio_系列函数以aio_开头或者lio_开头,给内核传递描述符、缓冲区指针、缓冲区大小(与read相同)和文件偏移(与lseek相同),并告知内核当整个操作完成时如何通知我们。

该系统调用立即返回,而且在等待I/O完成期间,我们的进程不会被阻塞。本例中要求内核在操作完成后产生指定的信号,该信号直到数据已经复制到应用进程缓冲区才产生,这一点区别于信号驱动式I/O。

这种异步I/O模型很少见,而且也不能确定支持异步I/O的系统是否能支持基于套接字的异步I/O。【截止到UNP的2010版】

6. I/O模型比较

POSIX把同步和异步这两个术语定义如下:

  • 同步I/O操作(synchronous I/O operation)导致请求进程阻塞,直到I/O操作完成。
  • 异步I/O操作(asynchronous I/O operation)不导致请求进程阻塞。
    在这里插入图片描述

以上五种I/O模型,前四种I/O模型属于同步I/O模型,因为其中真正的I/O操作(recvfrom)将阻塞进程,并且,他们的区别在于第一个阶段。而异步I/O属于异步I/O操作。

四、select() 函数补充

对于select函数的timeout参数,有三种形式:

  1. 无限等待,需要传入的timeout为NULL
  2. 有限等待,需要传入的timeval结构体的两个字段不同时为0.
  3. 不等待,需要传入的timeval结构体的两个字段均为0,类似于轮询操作。

上面三种状态,前两种等待,均可被捕获信号所中断,此时select函数返回,返回EINTR错误。此时我们需要做好重启select函数的准备。


对于exceptfds参数的说明,目前支持的异常条件只有两个:

  1. 某个套接字的带外数据的到达
  2. 某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息。

对于描述符集合的补充:

select使用描述符集,通常是一个整数数组,其中每个整数中的每一位对应一个描述符。如:假设使用32位整数,那么该数组的第一个元素,对应于描述符0 ~ 31,第二个元素对应于32 ~ 63,依次类推。

不过这都是与我们无关的,他们都通过fd_set函数类型以及四个宏(FD_CLR、FD_SET、FD_ZERO、FD_ISSET)实现,这些描述符集,可以通过C语言的赋值运算符进行赋值拷贝。

这些描述符集都是传入-传出参数,所以当select返回的时候,那些描述符集合内未就绪的描述符对应的位就都被清0了,为此,每次重新调用select的时候,都需要再次将所有我们关心的描述符位置1,然后传入select。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值