前言:
select机制已经被很多人都讲解过,select使用起来也不是特别难,为什么还要花时间再次讲解select机制?
在回答这个问题之前,我们先问一下自己,是否有足够的信心保证在使用select编程时不出错,select机制虽然看起来简单,实际在使用时有很多坑,当我们没有从原理上真正理解select机制时,我们随时会掉入这些坑,所以这篇文章我尽量把select实现原理和细节交待清楚。
1.select简介
select机制是一种I/O多路复用技术,它可以同时监视多个文件描述符,当某个文件描述符就绪(一般是读写就绪)时,就会通知程序进行相应的操作。
select机制的优点是可以同时处理多个连接,避免了大量的线程或进程切换,提高了系统的并发性能。
在Linux系统中,select机制可以通过select函数来实现。select函数的参数包括需要监视的文件描述符集合、超时时间等。当select函数返回时,程序可以通过遍历文件描述符集合来确定哪些文件描述符已经就绪,然后进行相应的操作。
2.select实现原理
(老规矩先上原理图)
这张原理图把select主要活动轨迹都进行了描述。
-
select核心实现原理是位图,select总共有三种位图,分别为读,写,异常位图,用户程序预先将socket文件描述符注册至读,写,异常位图,然后通过select系统调用轮询位图中的socket的读,写,异常事件。
-
select底层通过轮询方式获取读,写,异常位图中注册的socket文件事件,如果检测到有socket文件处于就绪状态,则会将socket对应的事件设置到输出位图,等所有位图中的socket都被轮询完,会统一将输出位图通过copy_to_user函数复制到输入位图,并且覆盖掉输入位图注册信息(也就是用户初始化的位图被内核修改)。
-
select轮询完所有位图,如果未检测到任何socket文件处于就绪状态,根据超时时间确定是否返回或者阻塞进程。
-
socket检测到读,写,异常事件后,会通过注册到socket等待队列的回调函数poll_wake将进程唤醒,唤醒的进程将再次轮询所有位图。
-
select返回时会将剩余的超时时间通过copy_to_user覆盖原来的超时时间。
3.select位图
3.1 select位图定义
#define __FD_SETSIZE 1024
#define __NFDBITS (8 * (int) sizeof (long int))
typedef struct
{
long int fds_bits[__FD_SETSIZE / __NFDBITS];
} fd_set;
-
select位图为1024比特位图,通过整型数组模拟而成。
-
select位图每个比特对应一个文件描述符数值。
-
select位图数组长度为16,每个数组元素为8字节,一个字节为8比特,位图大小=16 * 8 * 8 = 1024比特。
3.2 常用位图操作函数:
void FD_CLR(int fd, fd_set *set); //设置fd对应位图位置为0。
int FD_ISSET(int fd, fd_set *set); //判断fd对应位图位置是否为1。
void FD_SET(int fd, fd_set *set); //设置fd对应位图位置为1。
void FD_ZERO(fd_set *set);//整个位图清零。
4.select编程
4.1 select函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:select函数是Linux系统中的一种I/O多路复用机制,它可以同时监视多个文件描述符。
参数:
nfds:最大文件描述符+1。
readfds:读文件描述符集合,可设置为NULL。
writefds:写文件描述符集合,可设置为NULL。
exceptfds:异常文件描述符集合,可设置为NULL。
timeout:超时时间,设置为NULL为阻塞模式,
struct timeval {
__kernel_time_t tv_sec; //秒,超时多少秒。
__kernel_suseconds_t tv_usec; //微妙,超时多少微秒。
};
返回值:
成功:返回检测到的文件描述符数量。
失败:返回-1,设置errno。
超时:返回0。
4.2 select使用示例:
fd_set rfds;
FD_ZERO(&rfds_storage); //清空位图
FD_SET(sock_fd, &rfds_storage); //设置位图
struct timeval tv = {.tv_sec = 5, .tv_usec = 0}; //设置超时时间
ret = select(max_fd + 1, &rfds, NULL, NULL, &tv);
4.3 select编程模型
5.select常见问题?
问题1:select函数最大文件描述(maxfd)有什么作用?
select使用1024比特位图监测最多1024个文件描述符,然而实际的情况很少会到达1024文件描述符上限,使用maxfd可以避免每次都轮询1024个文件描述符,从而提高轮询效率。
maxfd通常设置为已打开最大文件描述符+1,目的是为了保证位图中每个文件描述符都被轮询到。
问题2:select优缺点有哪些?
优点:
-
select支持多种文件描述符类型,包括socket、标准输入输出、管道、FIFO等。
-
select是跨平台的,可以在多种操作系统上使用。
缺点:
-
select的效率不高,每次调用select都需要将所有的文件描述符从用户态复制到内核态,这个过程比较耗时。
-
select返回后需要遍历所有的文件描述符,找到就绪的文件描述符,这个过程也比较耗时。
-
select支持的文件描述符数量有限,通常是1024个。
问题3:select为什么会有1024文件描述符限制?
-
进程默认打开最大文件描述符为1024(次要原因)。
-
select采用轮询的方式获取读,写,异常事件,如果需要轮询的文件描述符比较多的话,select执行效率会非常低(个人观点)。
-
select是通过一个整型数组来模拟位图,整型数组长度和元素大小已经固定,只能模拟出1024比特位图,使用select如果超过1024文件描述符的限制,可能会导致内存越界和其他未知问题(真正原因)。
问题4:select有哪些设计缺陷?
-
select最大文件描述符为1024,所以无法满足高并发应用场景,高并发场景请使用epoll机制。
-
select采用轮询的方式获取读,写,异常事件,轮询的方式效率低,不管文件事件是否就绪,都需要去做检测。
-
select位图设置和获取采用覆盖方式,用户输入的读,写,异常位图会被内核修改,编程非常容易出错,可参考select实现原理图分析,所以每次调用select之前都要重新设置位图。
-
select执行完后会返回剩余超时时间,剩余超时时间会覆盖原来的超时时间,导致超时机制异常。可参考select实现原理图分析,所以每次调用select之前都要重新设置超时时间。