select---详解

前言

传统的服务器框架都是阻塞I/O类型的,为了提高并发性将迭代型(while)改为多进程,但是多进程开销较大,然后设计多线程的模型,节约系统开销,但是考虑到传统的accept函数是阻塞函数,然后设计多个进程或是线程同时accept的方式提高并发处理能力。本质没有偏离阻塞I/O的基本很特性,其原因就是我们的API接口均是阻塞型的,没有请求就死等,没有连接就耗着。思考有没有一种非阻塞的方式可以实现同时监听多个socket?
默认方式下,accept处于阻塞状态,将套接字文件属性设置为非阻塞时,accept处于非阻塞状态(其实与该套接字相关的系统调用都是非阻塞的了)。

阻塞与非阻塞

  1. 阻塞与非阻塞是程序(线程)等待消息通知时的状态角度来说的。
  2. 阻塞模式:程序(线程)在执行I/O操作完成前会一直等待,不会把程序的控制权交给CPU。如(connect、accept、recv、recvfrom函数)默认都是阻塞的。

阻塞模式下,进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞(死等在被阻塞的地方),函数不会立即返回。

  1. 非阻塞模式:程序(线程)在执行I/O操作时,进行系统调用时在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
  • 非阻塞的方式可以明显的提高CPU的利用率,但是增加系统的线程切换增加。所以增加的CPU执行时间能不能补偿系统的切换成本需要好好评估
  • 非阻塞non-block模式下,进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以非阻塞模式效率较高。

select描述(同步IO复用)

  • select系统调用可以实现一个进程同时监听多个文件描述符(socket描述符)的状态变化,默认的当调用select()函数,程序会阻塞到这里(除非设置timeout),直到被监视的文件描述符有某一个或多个发生了状态改变,则select函数返回。

select机制中提供了一个 fd_set数据结构,仅仅包含一个整形数组,数组的每一个元素的每一位(bit)标记一个文件描述符,某个文件描述的状态改变时,设置相应位为1,表示就绪。fd_set能容纳的文件描述符的数量是由FD_SETSIZE决定,默认为1024,除非修改宏定义,并编译内核。

select函数原型

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

SYNOPSIS
       /* According to POSIX.1-2001 */
       #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); //用来清除描述词组set中相关fd 的位
       int  FD_ISSET(int fd, fd_set *set);//用来测试描述词组set中相关fd 的位是否为真
       void FD_SET(int fd, fd_set *set);//用来设置描述词组set中相关fd的位
       void FD_ZERO(fd_set *set);  // 用来清除描述词组set的全部位。

       #include <sys/select.h>

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

参数详解

  • ndfs:select监视的文件描述符数,视进程中打开的文件数而定,一般设置为监视各文件中的最大文件描述符值加1。
  • readfds:这个文件描述符集合监视文件集中的任何文件是否有数据可读,当select函数返回的时候,readfds将清除其中不可读的文件描述符,只留下可读的文件描述符。
  • writefds:这个文件描述符集合监视文件集中的任何文件是否有数据可写,当select函数返回的时候,writefds将清除其中不可写的文件描述符,只留下可写的文件描述符。
  • exceptfds:这个文件集将监视文件集中的任何文件是否发生错误。
  • timeout:本次select()的超时结束时间,使得select处于三种不同的状态:
  1. 若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
  1. 若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
  2. timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

Linux Socket编程中select的常见用处

  • accept函数的非阻塞实现{服务器端}


    select.png

如果将正在listen的socket设置到readfds中,调用select,如果有客户端connect,select将返回正值,通过宏FD_ISSET可检测到该socket可读,此时再用accept接受新的socket,并通过FD_SET将accept返回的new socket描述符添加到readfds中,若是active connection 就调用recv()读取数据。

EINPROGRESS(man connect的描述)
The socket is nonblocking and the connection cannot be completed
immediately. It is possible to select(2) or poll(2) for comple‐
tion by selecting the socket for writing. After select(2) indi‐
cates writability, use getsockopt(2) to read the SO_ERROR option
at level SOL_SOCKET to determine whether connect() completed
successfully (SO_ERROR is zero) or unsuccessfully (SO_ERROR is
one of the usual error codes listed here, explaining the reason
for the failure).
描述了connect出错时的一种值errno值:EINPROGRESS,这种错误发生在非阻塞的socket调用从connect,而连接又没有立即建立时,在这种情况下,可以调用select、poll等函数来监听这个连接的失败的socket上的可写事件,当select、poll等函数返回时,在利用getsockopt来读取错误码并清除该socket上的错误,如果错误码是0,表示连接建立成功。

主动写socket时对方突然关闭连接的处理,则可以简单地捕捉信号SIGPIPE并作出相应关闭本地socket等等的处理。SIGPIPE的解释是:写入无读者方的管道

缺点

  1. 单个进程可以监听的描述符的个数限制。
  2. 开销大(内存拷贝):包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,每次在select调用之前,需要将监听的描述符从用户态空间拷贝到内核的地址空间,在select调用都返回整个用户注册的事件集合(包括据就绪的和为就绪的),它的开销随着文件描述符数量的增加而线性增大。
  3. 效率问题:内核在帮助应用程序监听多个描述符的时候,是一种轮循检测就绪事件的方式,扫描判断哪个socket描述符的位是就绪的,这是耗时的,时间复杂度为O(n)。
  • poll的改进:只是描述fd集合的方式不同,

  • epoll的改进

  1. 没有最大并发连接的限制,上限是最大可以打开文件的数目。cat /proc/sys/fs/file-max察看。
  2. 共享内存:在epoll_ctl函数中,每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。利用mmap()文件映射内存加速与内核空间的消息传递,即epoll使用mmap减少复制开销。
  3. 效率提升:只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关。
  1. select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。
  2. select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。