高级IO-Select

高级IO-Select_第1张图片

文章目录

  • 1. 五种IO模型
  • 2. 非阻塞IO
    • 2.1 fcntl
  • 3. I/O多路转接之select
  • 4. 基于Select的服务器
    • 4.1 处理监听套接字
    • 4.2 处理普通套接字
  • 5. 总结

1. 五种IO模型

首先,我们想一个问题,我们在调用read或者recv时,我们做了哪些事情
首先,我们要等TCP缓冲区里有数据,换句话说,就是等待IO事件就绪。这里面含有检测功能。当有数据时,从内核层拷贝到应用层。

那么IO=等+拷贝数据。

什么叫做高效的IO呢
单位事件,等的比重越小,IO效率越高。

下面有5种IO模型
阻塞IO:在内核将数据准备好之前,系统调用会一直等待。所有的套接字默认都是阻塞方式

非阻塞IO:如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询。这对CPU来说是较大的浪费,一般只有特定场景下才使用

信号驱动IO: 内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作

IO多路转接:IO多路转接能够同时等待多个文件描述符的就绪状态

异步IO:由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)

在这5种IO模型中,效率最高的其实是IO多路转接这种模式

同步通信 vs 异步通信
同步和异步关注的是消息通信机制。
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果

异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用
者,或通过回调函数处理这个调用

总而言之,同步和异步主要就看它有没有参与IO细节

另外, 我们回忆在讲多进程多线程的时候, 也提到同步和互斥,这里的同步通信和进程之间的同步是完全不想干的概念
进程/线程同步也是进程/线程之间直接的制约关系,是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待,传递信息所产生的制约关系,尤其是在访问临界资源的时候。

阻塞 vs 非阻塞
阻塞和非阻塞关注的是程序在等待事件就绪
阻塞调用是指事件就绪之前,当前线程会被挂起,调用线程只有在事件就绪后才进行拷贝。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

2. 非阻塞IO

2.1 fcntl

一个文件描述符,默认都是阻塞IO。但是我们可以用fcntl函数进行设置:
高级IO-Select_第2张图片
传入的cmd的值不同,后面追加的参数也不相同。我们主要使用的是这种方式:获得/设置文件状态标记(cmd=F_GETFL或F_SETFL),就可以将一个文件描述符设置为非阻塞
高级IO-Select_第3张图片
高级IO-Select_第4张图片
运行结果如下:
高级IO-Select_第5张图片
我们可以看到标准输入变成非阻塞了,当我们输入的时候也能获取到。

3. I/O多路转接之select

上面我们说过,IO=等+数据拷贝。等,等的是文件描述符状态的变化。文件描述符状态的变化有3种:1.可读。2.可写。3.异常

系统提供select函数来实现多路复用输入/输出模型,select系统调用是用来让我们的程序监视多个文件描述符的状态变化的,程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

select的函数原型如下: #include
在这里插入图片描述
参数nfds是需要监视的最大的文件描述符值+1

后面几个参数都是输入输出型参数,readfds作用是读取文件描述符集

输入时:用户告诉OS,你帮我关心一下我所设置的多个fd中的读事件是否就绪。输出时:OS告诉用户,你让我关心的多个fd中,有哪些已经就绪了。

那么fd_set结构是什么呢
其实这个结构就是一个 “位图”。使用位图中对应的位来表示要监视的文件描述符

比特位的位置代表fd的编号,比特位的内容代表"是否就绪"的概念

举个例子:
假设我们要传的位图结构原本是0100 1110,我们可以告诉OS位图为1的帮我关注一下,位置为0的不需要关注。当有事件就绪的时候,把就绪位置上的比特位设置成1,其余的覆盖成0。比如说,结果是0000 1000,就说明位置3的比特位上事件就绪了。

OS提供了一组操作fd_set的接口,来比较方便的操作位图
在这里插入图片描述

第二个参数就是代表写事件,第三个参数代表的是异常事件

参数timeout用来设置select()的等待时间
参数timeout取值:
NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件。
0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
特定的时间值:在指定的时间内,阻塞,如果在指定的时间段里没有事件发生,select将超时返回

下面我们来看一下参数timeout类型timeval
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0,如果有事件发生,就返回特定的时间值的剩余时间
高级IO-Select_第6张图片
第一个是秒,第二个是微秒。
函数返回值:
执行成功则返回文件描述词状态已改变的个数。
如果返回0代表在描述词状态改变前已超过timeout时间,没有返回。
当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测

错误值可能为:
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n 为负值。
ENOMEM 核心内存不足

4. 基于Select的服务器

高级IO-Select_第7张图片
我们将创建套接字封装了,这样以后再创建套接字就直接使用。
高级IO-Select_第8张图片
我们可以先测试一下这个类型多大,可以放多少个fd。
在这里插入图片描述
可以看出fds的大小是128字节,因为是位图是比特位,需要乘8,也就是1024比特位。

高级IO-Select_第9张图片
我们在这里可以直接用accept去接受监听套接字吗
如何看待listensocket:获取新连接的,本质需要先三次握手,前提给我发送syn。建立连接的本质,其实也是IO,一个建立好的连接我们称之为读事件就绪,listensocket 只(也)需要关心读事件就绪

accept: 等 + “数据拷贝”(链接拿到应用层),编写多路转接代码的时候,必须先保证条件就绪了,才能调用IO类函数。所以我们不能让accept去等,应该让select去等。

下面我们进行select设置:
高级IO-Select_第10张图片
但是这样设置有很多问题。

1. 刚启动的时候,只有一个fd,listensock

2. server 运行的时候,链接会越来越多,sock才会慢慢变多

3. select 使用位图,采用输入输出型参数的方式,来进行 内核<->用户 信息的传递, 每一次调用select,都需要对历史数据和sock进行重新设置

4. 因为在select等待成功后,其它符号会被清空listensock也就没了,但是我们需要永远把listensock设置进readfds中

5. select 就绪的时候,可能是listen 就绪,也可能是普通的IO sock就绪了

所以,我们每次要重新更新符号集,我们可以用数组来设置:
高级IO-Select_第11张图片
因为文件描述符是从0开始的整数,我们一开始都设置-1,这样当我们遍历符号集时,就可以判断fd是否合法。我们固定下标0为监听套接字。
高级IO-Select_第12张图片
这样我们每次使用的符号集就更新出来了。
高级IO-Select_第13张图片
当等待成功后,就说明有事件就绪了,我们就需要进行处理。

4.1 处理监听套接字

高级IO-Select_第14张图片
这里处理套接字有2种情况,一个是监听套接字,一个是普通套接字。
高级IO-Select_第15张图片
当我们获取链接成功后,我们就能直接进行读取了吗
不能,因为你read不知道对方什么时候给你发送数据,但是select知道!我们要想办法把新的fd托管给select
高级IO-Select_第16张图片
这样我们在数组里就找到了空余位置。
高级IO-Select_第17张图片

在调试的时候,我们也可以把有效符号集打印出来。
高级IO-Select_第18张图片

4.2 处理普通套接字

高级IO-Select_第19张图片
但是这里有一个bug,你怎么知道网络发送的报文是完整的呢
这个问题以后再解决。

5. 总结

Select的编码特征
a. select之前要进行所有参数的重置,之后要遍历所有的fd进行事件检测。
b. select需要用户自己维护第三方数组,来保存所有的合法fd,方便select进行批量处理。
c. 一旦特定的fd事件就绪,本次读取或写入不会被阻塞

Select的优缺点
优点:占用资源少,并且高效,对比之前的多线程,多进程

缺点:
a. 每一次都要进行大量的重置工作,效率比较低。
b. 每一次能够检测的fd数量是有上限的。
c. 每一次都需要内核到用户,用户到内核传递位图参数,较为大量的数据拷贝工作。
d. select编码特别不方便,需要用户自己维护数组。
e. select底层需要遍历的方式,检测所有需要检测的fd

你可能感兴趣的:(Linux,网络,服务器,linux,IO,Select)