调用send/write、read/recv
这些IO接口进行网络通信时,需要等待IO条件满足(IO事件就绪)才能正常拷贝数据。比如调用send/write
需要等待TCP的发送缓冲区有剩余空间才能将数据拷贝到TCP发送缓冲区中,调用read/recv
需要等待TCP的接收缓冲区有数据才能将数据拷贝到应用层,即IO=等+数据拷贝,而这些等待是由用户来完成的,高级IO就是降低用户等的时间以提高IO效率。常见的五种IO模型有:阻塞IO、非阻塞IO、信号驱动IO、多路复用/多路转接、异步IO(让操作系统进行IO)。前四种模型是同步IO,最后一种模型是异步IO。同步还是异步取决于进程有没有参与等+数据拷贝的过程。
一.阻塞IO
二.非阻塞IO
我们如果调用read函数从0号文件描述符中读数据时,如果不输入数据,那么程序就会进入阻塞状态。如果想让这个接口进行非阻塞IO,我们可以调用fcntl()
系统调用接口将该文件描述符设置为非阻塞状态。
- int fd:想要设置的那个文件描述符
- int cmd:
- 如果传入
F_GETFL
:返回一个位图,获取当前文件属性 - 如果传入
F_SETFL
: 可以设置文件的属性,可以设置O_NONBLOCK
将该文件描述符的属性设置为非阻塞
- 如果传入
- 一旦将文件描述符设置为非阻塞状态,那么它的返回状态有4种情况
- 成功,返回值大于0
- 读到文件末尾,返回值等于0
- 读失败,IO事件没有就绪,错误码被设置为11(EAGAIN or EWOULDBLOCK)或者(EINTER)代表这次IO被信号中断,需要重新读取
- 读失败,真正读失败,错误码不等于EAGAIN or EWOULDBLOCK or EINTER
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>int main()
{char buff[128] = {0};int sl = fcntl(0, F_GETFL);if (sl < 0) {perror("fcntl");abort();}fcntl(0, F_SETFL, O_NONBLOCK|sl);while (true){printf("please enter# ");fflush(stdout);ssize_t n = read(0, buff, sizeof(buff)-1);if (n > 0) {buff[n-1] = 0;printf("echo# %s\n", buff); } else if (n == 0){std::cout << "读到文件末尾!" << std::endl;break;}else{if (errno == EAGAIN || errno == EWOULDBLOCK){sleep(1);std::cout << "数据没有准备好 " << std::endl;continue;}else if (errno == EINTR){std::cout << "这次IO被信号中断,重新读取 " << std::endl;continue;}else {std::cout << "读取失败 " << std::endl;break;}}}return 0;
}
三.多路复用/多路转接
read/write系统调用,一次只能等待一个文件描述符,而接下来的select,poll、epoll可以一次等待多个文件描述符,这不同于read和write,因为select、poll、epoll只会进行等待,拷贝数据还是io系统调用完成。
3.1 select
如果要使用select进行等待多个文件描述符,我们就必须先得知道哪些文件描述符要被等待,所以我们使用select接口时,必须先设置文件描述符集。
#include <sys/select.h>
void FD_CLR(int fd, fd_set* set); 将set中的fd设置为0
void FD_ISSET(int fd, fd_set* set); 判断set中是否存在fd
void FD_SET(int fd, fd_set* set); 将set中的fd设置为1
void FD_ZERO(fd_set* set); set全部置0int select (int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
- 返回值:
- n > 0 ,有n个fd就绪
- n==0 ,timeout
- n < 0 等待失败
- int nfds:等待的多个fd中,最大的fd+1
下面四个参数是输入输出型参数,你可以设置值给系统,操作系统也可以设置值返回给用户。
- fd_set* readfds:
- 用户要告诉内核,哪些fd的读事件需要被等待
- 内核要告诉用户,哪些fd的读事件已经就绪
- fd_set* writefds:
- fd_set* exceptfds:
struct timeval
{
time_t tv_sec;
suseconds_t tv_usec:
}
- struct timeval* timeout:
- 如果设置为nullptr,则阻塞等待,如果等待的文件描述符中有一个没有就绪,那么select就阻塞。
- tv_sec:tv_usec = 0:0 ,非阻塞等待
- tv_sec:tv_usec = n:0 , n秒以内阻塞等待,否则timeout一次
- 输出:表示剩余时间
缺点:
- 每次调用select时,都需要手动设置fd集合,接口使用不方便
- 每次调用select时,都需要将fd集合从用户态拷贝到内核态
- 每次调用select时,都需要在内核中遍历传递进来的fd集合
- select支持的文件描述符数量太少
第二种多路转接方案poll一定程序上解决了select的缺点,将输入输出参数分离,于是就不需要手动设置fd集合,接口使用方便许多,同时也解决了select文件描述符数量太少的缺点。
3.2 poll
- 返回值:
- 大于0,几个fd就绪
- =0,timeout
- <0 ,等待出错
- struct pollfd* fds :等待的fd集合,可以设置检测文件描述符的哪些事件,events可以设置关心哪些事件,revents表示哪些事件触发。
- POLLIN:读就绪
- POLLOUT:写就绪
- nfds_t nfds:fds集合的长度
- int timeout:单位毫秒
- -1:阻塞等待
- 0:非阻塞等待
-
0:阻塞等待n秒,然后timeout一次
第三种多路复用技术epoll,解决了上述select和poll的缺点。
3.3 epoll
- epoll接口
创建epoll模型:
- int size:设置一个大于0的值就行,这个参数被忽略
- 返回值:返回一个epoll文件描述符
控制epoll模型:
功能:用户告诉内核哪个fd的什么事件需要被关心
- int epfd:epoll_create的返回值
- int op:对epoll模型的操作
- EPOLL_CTL_ADD
- EPOLL_CTL_DEL
- EPOLL_CTL_MOD
- int fd:操作的目标fd
- event:关心的事件
等待事件就绪:
- int epfd:对某个epoll模型操作
- struct epoll_event* events:输出型参数,告诉程序员哪些事件就绪
- int maxevents:events的长度
- int timeout:效果同timeout
events:填写下面
- epoll原理
在我们调用epoll_create时,操作系统会为我们创建一个epoll模型,这个模型中有包含三个机制:红黑树、就绪队列、回调机制。所谓epoll模型就是一个结构体,当你调用epoll_create时,os会创建一个结构体,然后创建一个struct file
将这个epoll结构体放入这个文件结构体中,给用户返回一个文件描述符,用户就可以通过文件描述符来找到这个epoll模型,进而对这个epoll模型进行操作。这三个机制解决了select和poll的缺点。这颗红黑树就相当于在select和poll中用户定义的第三方fd表,程序员调用epoll_ctl可以操作这棵树;就绪队列保存了已经就绪的文件描述符。当底层有文件就绪时,文件结构体内的回调函数就会被调用,将红黑树中的节点链入就绪队列中,这不像select和poll还需要每次遍历文件描述符集才能确定哪个文件就绪,时间复杂度从n变为了1。当程序员调用epoll_wait接口时,其底层只需要判断就绪队列是否为空即可,也不需要遍历检测。
让我们回想下select的缺点,1.用户需要每次手动设置关心的fd 2.需要将第三方fd数组拷贝到内核 3.内核在底层要遍历fd集 4.fd数量太少 。其中1和4这两个缺点poll解决了,但是2和3这两个缺点是select和poll都有的,而epoll在底层实现了红黑树解决了缺点2,回调机制解决了缺点3。所以epoll的效率极高。
- epoll工作模式
在我们使用select、poll、epoll时,如果有事件就绪,但是没有取走这个数据,那么底层就会一直通知事件就绪。 这种方式是epoll默认的工作模式:LT模式(水平触发),另外epoll还有一种工作模式:ET(边缘触发)
- ET模式:有效通知只有一次,数据变化时才会通知一次。这种通知方式的效率显然要比LT模式高,因为LT模式在单位时间内,一直在做重复的通知。因为ET会倒逼上层,尽快取走数据,即循环调用recv接口进行非阻塞读取,直到读到的数据小于期望值,那么说明底层数据已经读取完毕。
LT模式,既可以在阻塞模式下工作也可以在非阻塞模式下工作,ET模式就只能在非阻塞模式下工作。对于LT模式和ET模式的IO效率谁高谁低,是要看LT的工作模式是非阻塞还是阻塞。
只要缓冲区中的剩余空间变多,那么TCP报文给对方通知的窗口大小也会变化,对方的滑动窗口也有可能变大,对方发送的数据就会变多,IO效率就会变高
- ET的应用场景是高IO,LT是要求响应及时的场景