I/O多路复用 —— fcntl()、select()的实现

前言:
recv(),send(),read()和write函数都是阻塞性函数,若资源没有准备好,则调用该函数的过程将进入阻塞状态,则有以下两种I/O多路复用的解决方案:
1)fcntl函数的实现(非阻塞模式)
2)select函数的实现

服务器端可以采用多进程模型和多线程模型解决客户端并发的场合,不管服务器端采用的是多进程还是多线程,服务器端采用子进程或者子线程和客户端进行双向通信,即调用send(),read()和write函数以及在tcp\udp协议中发送、接收数据使用的recv(),recvfrom(),sendto(),send()都是默认是阻塞性函数,若在读的时候,对方没有数据发送过来,则读阻塞,即若资源没有准备好,调用阻塞性函数就会阻塞。

io多路复用就是为了解决并发 可以监听多个文件描述符
如图:
在这里插入图片描述
若有若干个客户端和服务端进行通信,服务器端主控线程分别去启动对应的若干个子线程,每个子线程可以服务于某一个客户端,和客户端进行数据的双向通讯,若客户端一直没有数据发送过来,则服务端一直阻塞,子线程一直处于空闲状态,并且占有进程中一块栈的空间(因为多个子进程共享同一个进程的资源,包括数据段、代码段以及栈,占有进程所拥有的的某一块栈的空间,占有这部分资源),子线程不结束,不会释放。

1)fcntl函数的实现(非阻塞模式) i/o 多路复用

主控线程负责不断调用accept获得客户端的连接,获得客户端的连接后,主控线程把对应的accept返回的套接字描述符放到服务端维护的一个动态数组,子线程遍历动态数组中的所有fd,并通过这些fd和客户端进行双向通信(采用非阻塞的read/write?若采用阻塞的read/write,若服务端只有一个子线程,如果子线程对某一个客户端进行读写,客户端没有信息发送过来,则read阻塞,那子线程无法服务于下一个客户端,所以采用非阻塞,当第一个客户端没有发送信息,read没有读到数据,read不会阻塞,直接返回0),子线程不断的遍历扫描动态数组中的fd,或者扫描到fd,和对应的客户端进行通信,如果读不到数据,扫描下一个,不会阻塞。

在这里插入图片描述
服务端只启动一个子线程,子线程不断遍历扫描动态数组中的fd,通过fd与客户端进行读写操作,采用非阻塞读写,读不到,扫描下一个,不断循环扫描,直到通信所有的客户端进行通信。

主控线程一直在循环调用accept,获得客户端连接后,将返回的新的fd放到动态数组中,子线程不断扫描动态数组,然后与每一个扫描到fd对应的的客户端进行双向通讯,采用的是非阻塞模式,非阻塞的read、write,若读不到客户端的信息,扫描下一个,若读到数据,则进行处理,然后读下一个,依次不断的循环扫描。
在这里插入图片描述
fd 套接字 描述符
counter fd的总数
max_counter counter的最大值
2)select函数的实现
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* writefds,struct timeval* timeout)
参数:
nfds:最大fd+1,在三个描述符集合中找到最大的描述符+1
readfds、writefds、writefds:指向描述符集的指针。这三个描述符集合存放了用户感兴趣的可读,可写,异常事件。
用户调用select函数会阻塞,委托内核去查看用户关心的描述符集合(传进去的所有描述符集合)是否准备好,可用,返回的是( 比如,可读集合存放的都是用来读取文件、数据的描述符。)
timeout:指定愿意等待的时间
timeout的三种值:
null 永远等待,直到捕捉到信号或文件描述符已准备好为止
具体值 struct timeval类型的指针,若等待timeout时间,还没有文件描述符准备好,就立即返回
0 从不等待,测试所有指定的描述符并立即返回
返回:准备就绪的描述符的 个数,若超时则为0,若出错则为-1, 超时时间就是给的的检查时间

传向select的参数告诉内核:

  • 我们所关心的描述符
  • 对于每个描述符我们所关心的条件(是否可读一个给定的描述符,是否可写一个给定的描述符,是否关心一个描述符的异常条件)
  • 希望等待多长时间(可以永远等待,等待一个固定量时间,或完全不等待)

从select返回时内核告诉我们

  • 已准备好的描述符的数量
  • 哪一个描述符已准备好读、写或异常条件
  • 使用这种返回值,就可调用相应的io函数(一般是read或write),并且确知该函数不会阻塞(读不到数据就下一个)

Select函数根据希望希望进行的文件操作对文件分类处理,这里,对文件描述符的处理主要涉及4个宏函数

  • FD_ZERO(fd_set *set) 清除一个文件描述符集
  • FD_SET(int fd, fd_set *set) 将一个文件描述符加入文件描述符集中
  • FD_CLR(int fd,fd_set *set) 将一个文件描述符从文件描述符集中清除
  • FD_ISSET(int fd,fd_set *set) 测试该集合中的一个给定位是否有变化

select函数返回的是文件描述符的数量,而非哪些文件描述符就绪,若传入10个,只有5个准备好,则用此函数判断是10个文件描述符中的哪几个,将传给selec的文件描述符(原来的)传入到宏当中,第二个参数是现在的文件描述符集合,检测进行比对,内部采用位操作,若当前fd在set中置为1,表示这个文件描述符已经准备就绪,整个宏返回true;则可对准备好的文件描述符进行io操作。

在使用select函数之前,首先使用FD_ZERO和FD_SET来初始化文件描述符集,并使用select函数时,可循环使用FD_ISSET测试描述符集,在执行完成对相关的文件描述符后,使用FD_CLR清除描述符集。

首先要把文件描述符集清空,然后将内核检测是否准备好的描述符加到描述符集中,然后把描述符集传给select函数,让内核去检查。然后测试哪些描述符已经准备就绪,找到能使用的描述符进行io操作,最后用FD_CLR清除

在这里插入图片描述
总结:
主控线程不断循环调用accept()获得客户端的连接,根据accept返回的新的套接字描述符,将描述符放到实现准备好的动态数组中,主控线程实现启动的子线程调用select(),将需要委托内核检查的描述符集传入,先将动态数组中的描述符放到描述符集中,然后传递给子线程所调用的select函数,构造的描述符集一般三个,一般传读的,因为服务器端主要是为了读数据,读客户端发送过来的信息,如果成功调用select,返回的是准备就绪的文件符数量,再通过FD_ISSE循环检测,找到准备好的文件描述符,然后进行读写操作。(内核检测完的描述符都是非阻塞的,不用设任何非阻塞的状态标识,因为这些内核已经帮忙做了。)若read为0,则直接返回一个出错信息,但不会阻塞,接着读取下一个)

发布了93 篇原创文章 · 获赞 45 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_41431406/article/details/98471113