什么是IO?
I/O是input/output的缩写,即输入输出端口。每个设备都会有一个专用的I/O地址,用户来处理自己的输入输出信息。在Linux中,一切皆文件,文件就是一串二进制流而已,不管是socket,还是FIFO,管道,终端都是文件,都是流,在信息交换的的过程中,我们都是对这些流进行数据的收发操作,简称I/O操作
I/O操作具体分为俩部分
- 等待数据就绪,也就是文件描述符上由事件就绪我们才可以对其进行IO操作
- 数据搬迁,说白了就是将一个文件中的数据搬到另一个文件中
五种I/O模型
- 阻塞I/O:在内核将数据准备好之前,系统调用会一直等待(所有的套接字,默认都是阻塞方式)
- 非阻塞式I/O:如果内核还未将数据准备好,系统调用仍会直接返回,并且返回EWOULDBLOCK错误码
非阻塞IO往往需要程序员以循环的方式反复尝试独写文件描述符,这个过程叫轮询,这对CPU来说是较大的浪费,一般只有特定场景下才使用
- 信号驱动I/O:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作
优点:无需等待;系统通知后去处理;代码简单;无需轮询
缺点:处理读数据时处于阻塞状态;
- I/O复用(I/O多路转接):核心在于I/O多路转接可以同时等待多个文件描述符的就绪状态
(用一个线程等待多个文件描述符)
- 异步I/O:由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序合适可以开始拷贝数据)
注:收到通知后,不一定会去处理
任何I/O过程中都包含俩个步骤:一是等待数据,二是将数据从内核拷贝到用户空间,而且在实际应用中等待消耗的时间往往都远远高于拷贝让I/O高效,最核心的方法就是让等待时间尽量少
同步通信于异步通信
- 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。一旦调用返回,就得到返回值了;换句话说,就是由调用者主动等待这个调用结果(调用者主动,有返回值)
- 异步:调用在发出之后,这个调用就直接返回了,没有返回结果,换句话说,当一个异步过程调用发送后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用(被调用者通知调用者,没有返回结果)
这里的同步,与同步和互斥中的同步不一样
线程/进程间的同步互斥:
- 线程/进程同步:线程/进程间直接的制约关系,基本场景:两个或两个以上的进程/线程在运行过程中协调步调,按预定的先后顺序运行。比如B任务的运行依赖于A任务产生的数据
- 互斥:一个公共资源同一时刻只能被一个线程/进程使用,多个线程/进程不能同时使用公共资源
- 同步是一种复杂的互斥,互斥是一种特殊的同步
I/O多路转接之select:
系统提供select函数来实现多路复用输入输出模型
- select系统调用是用来让我们的程序监视多个文件描述符的变化
- 程序会停在select这里等待,知道被监视的文件描述符由一个或多个发送了状态改变
select函数原型:
#include<sys/select.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
参数解释:
- nfds是需要监视的最大的文件描述符值+1
- rdset,wrset,exset分别对应需要检测的可读文件描述符的集合,可以写文件描述符的集合,以及异常文件描述符的集合
- 参数timeout为结构timeval,用来设置select()的等待时间, 如果需要监视的文件描述符在这段时间内没有事件发生,则返回值为0
参数timeout的取值:
- NULL:表示select()没有timeout,select一直被阻塞,直到某个文件描述符上发生了事件
- 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生
- 特定的时间值:如果在指定的时间段里没有时间发生,select将超时返回
fd_set结构:
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中的全部位
这个结构其实就是一个整数数组,更严格的说,是一个“位图”,使用位图中对应的位来表示要监视的文件描述符
函数返回值:
- 如果执行成功,返回门将描述符的状态以改变的个数
- 如果返回0,代表在描述词状态改变之前,已经超过timeout时间,没有返回
- 当有错误发生时则返回-1,错误原因存于error,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测。
理解select执行过程:
理解select模型的关键在于理解fd_set,为了说明方便,去fd_set长度为1字节,fd_set中的每一位代表一个文件描述符,则一个字节长的fd_set最大可以代表8个fd
- 执行fd_set set;FD_ZERO(&set),则对应的fd_set 用位表示为00000000
- 若fd=5,执行FD_SET(fd,&set)后,set变为0001 0000(第五位为1)
- 若在加入fd=2,fd=1,则set变成 0001 0011
- 执行select(6,&set,0,0,0)阻塞等待*
- 若fd=1,fd=2上都发生可读事件,则select返回,此时select返回,此时set变成了 0000 0011(没有事件发生的fd=5 被清空)
select读就绪:
- socket内核中,接受缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以无阻塞的读该文件描述符,并且返回值大于0
- socket TCP通信中,对端关闭连接,此时对该socket读,则返回0
- 监听的socket上有新的连接请求
- socket上有未处理的错误
select的优点:
- select资源占用比较少
- 用户量较多的时候它的性能和效率比较好
select的缺点:
- 它总共监视的文件描述符是有限的,最多1024个
- 因为参数为输入型参数,在操作时,一直要遍历,查找使用
- select所监视的文件描述符,select的调用频繁,然后他会反复遍历,可能会达到性能瓶颈
- 当select频繁调用的过程中,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时很大
- select的参数为输入输出型