所有的I/O都分为两步完成,第一步是等待事件的发生,第二步是将数据进行拷贝。
一个高效的I/O模型就是尽量减少等待在整个I/O模型中所占的比重。
阻塞I/O
在内核将数据准备好之前, 系统调⽤用会⼀一直等待. 所有的套接字, 默认都是阻塞⽅方式。
非阻塞I/O
如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回WOULDBLOCK错误码。非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较⼤大的浪费, 一般只有特定场景下才使⽤用。这种等待被称为忙等。
通过fcntl(fd, F_SETEL, flag | O_NONBLOCK)
函数将套接口设置为非阻塞。
信号驱动I/O
内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。
多路转接
这种方式看似和阻塞式IO类似,但事实上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态,这也是现在使用较多的一种IO方式。
这种I/O操作调用的函数有select、poll和epoll三种实现。
用select来管理多个IO,FD_SETSIZE个,所监听的套接口是有上限的,一旦其中一个IO或者多个IO检测到我们所感兴趣的事件。
select函数返回,返回值为检测到的事件个数。
并且返回哪些IO发生了事件,遍历这些事件,进而处理事件。
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
//ndfs表示读、写、异常集合中的文件描述符 的最大值+1
//readfds表示读事件集合,底层实现是一张位图
//writefds表示写事件的集合
//exceptfds表示异常集合
//timeout表示超时时间,NULL表示不会超时,一直等待
void FD_CLR(int fd, fd_set *set); //将文件描述符fd从集合中移除
int FD_ISSET(int fd, fd_set *set); //判断fd是否在集合当中
void FD_SET(int fd, fd_set *set); //将fd添加到集合当中
void FD_ZERO(fd_set* set); //清空集合
select无法处理多核并行的请求,处理方式,要是想处理则需要多进程/多线程使用select。
异步I/O
由内核在数据拷贝完成时,通知应用程序。
读、写、异常事件发生的条件
可读:
- 套接口缓冲区有数据可读
- 连接的读一半关闭,即收到FIN段,读操作将返回0
- 如果为监听套接口,已完成连接队列不为空
- 套接口上发生了一个错误待处理,错误可通过getsockopt指定SO_ERROR选项来获取
可写:
- 套接口发送缓冲区有空间容纳数据
- 连接的写一半关闭。即收到RST段之后,再次调用write操作(SIGPIPE)
- 套接口上发生了一个错误待处理,错误可通过getsockopt指定SO_ERROR选项
异常:
- 套接口存在带外数据
同步通信与异步通信:
首先,这里谈到的同步是指I/O中的同步与异步,而非进程或线程的同步。
同步和异步关注的是消息通信机制,上面的五种模型中,前四种皆为同步通信。
- 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回,但是一旦调用返回,就得到返回值了;也就是说调用者会一直等待调用结果。
- 异步则相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果;当一个异步过程调用发出后,调用者不会立刻得到结果;而是在调用发出后,被调用者通过状态、通知来通知调用者,或者通过回调函数处理这个调用。