文章目录
- IO模型的分类
- 多路复用型IO的分类
- select
- select系统调用
- 缺点
- poll
- poll系统调用
- 缺点
- epoll
- epoll系统调用
- epoll模型
- 优点
- LT模式与ET模式
IO模型的分类
大家都知道,一个完整的IO操作所花费的时间在计算机中是非常多的(速度非常慢),那么这些时间都花费在哪里呢?
IO = 等待数据就绪 + 数据拷贝
而等待数据就绪所花费的时间占了整个IO时间的99%,数据拷贝所花费的时间仅占1%。
IO模型分为同步IO和异步IO。
- 同步IO:用户发起IO,用户阻塞或轮训的查看数据是否就绪,用户进行内核态到用户态的数据拷贝。
- 异步IO:用户发起IO,数据是否就绪以及数据拷贝的过程全交给内核,当IO完成时内核通知用户。
同步IO分为阻塞IO、非阻塞IO、信号驱动型IO、多路复用型IO。
- 阻塞IO:用户发起IO后,若数据还未就绪,该线程就一直阻塞等待着数据的就绪。
- 非阻塞IO:用户发起IO后,若数据还未就绪,该线程不会阻塞,而是继续向后执行代码,所以需要配合循环来一直检查数据是否就绪,而这也叫做轮询。
- 信号驱动型IO:用户发起IO后,若数据还未就绪,该线程不会阻塞,而是继续向后执行代码,由内核来检查数据是否就绪,当数据就绪时,内核通知用户。
- 多路复用型IO:由一个线程来监视(等待数据就绪)多个fd,当某个fd上的数据就绪时,就通知用户。
多路复用型IO和前三者的区别就是,多路复用型IO能同时监视多个fd,从而大大提高了效率。
本篇讲述多路复用型IO。
多路复用型IO的分类
select
select系统调用
函数原型:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数解析:
-
nfds:需要监视的文件描述符的最大值+1(告诉操作系统要查找的文件描述符的范围);
-
readfds、writefds、exceptfds:分别都是一个位图,表示要监视的某些fd的读事件、写事件、异常事件。它们都是输入输出型参数,输入时,将需要监视的文件描述符和事件添加到位图中,输出时,就是已经就绪的文件描述符和事件。
-
timeout:
- 取值为NULL时,表示select阻塞式调用;
- 取值为0时,表示select非阻塞式调用;
- 取值>0时,表示在timeout时间内阻塞式调用。
-
返回值:
- >0时,表示已就绪的文件描述符的数量;
- ==0时,表示timeout时间到期返回了;
- <0时,出错返回。
-
fd_set:
typedef long int __fd_mask; typedef struct { __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS]; } fd_set;
-
struct timeval:
struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ }; struct timespec { long tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ };
还有一批系统调用用来控制fd_set类型的参数。
void FD_CLR(int fd, fd_set *set);// 将fd从set中移除
int FD_ISSET(int fd, fd_set *set);// 判断fd是否在set中
void FD_SET(int fd, fd_set *set);// 将fd设置进set中
void FD_ZERO(fd_set *set);// 将set设置为0
缺点
- 由于中间三个参数是输入输出型参数,所以函数每次返回时,原来设置的内容就都已经被覆盖掉了,所以需要重新设置。这就导致编码难度上升;
- 每次调用select都会把fd_set从内核态拷贝到用户态,效率较低;
- 由于采用位图结构,所以底层选用轮询的方法来检测文件描述符是否就绪,效率较低;
- fd_set是一个固定的类型,那么单个线程能够监视的文件描述符的数量就是固定的(centos7.6上是1024个)。
poll
poll系统调用
函数原型:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数解析:
-
fds:是一个指针(也可以说是一个数组),它里面存放着需要监视的fd以及事件(是一个输入输出型参数);
-
nfds:表示监视的fd的数量,也就是fds的长度;
-
timeout与select一样;
-
返回值与select一样;
-
struct pollfd:
struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ };
- fd:需要监视的文件描述符;
- events:该fd上需要监视的事件(输入型参数);
- revents:该fd上已经就绪的事件(输出型参数)。
这两个事件也分别都是位图结构,通过宏来设置对应事件:
事件 描述 是否可作为输入 是否可作为输出 POLLIN 数据(包括普通数据和优先数据)可读 ✔ ✔ POLLRDNORM 普通数据可读 ✔ ✔ POLLRDBAND 优先级带数据可读(Linux不支持) ✔ ✔ POLLPRI 高优先级数据可读,比如TCP带外数据 ✔ ✔ POLLOUT 数据(包括普通数据和优先数据)可写 ✔ ✔ POLLWRNORM 普通数据可写 ✔ ✔ POLLWRBAND 优先级带数据可写 ✔ ✔ POLLRDHUP TCP连接被对方关闭,或者对方关闭了写操作,它由GNU引入 ✔ ✔ POLLERR 错误 ✘ ✔ POLLHUP 挂起。比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件 ✘ ✔ POLLNVAL 文件描述符没有打开 ✘ ✔
缺点
- 每次调用都会将fds从内核态拷贝到用户态(因为填写了revents);
- 底层也是采用轮询的方式来检测文件描述符是否就绪,效率较低。
epoll
epoll系统调用
epoll有最基础的三个系统调用。
函数原型:
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
-
epoll_create用来创建一个epoll模型,返回创建好的epoll模型的句柄(是一个文件描述符)。参数size在旧版本中悲剧,在新版本(2.6.8及之后的版本)被用作一个提示,提示内核为epoll实例预留size个文件描述符,建议设置成大于0的数字。
-
epoll_ctl用来控制(增删改)epoll模型中文件描述符及其事件
-
epfd:epoll模型的句柄;
-
op:控制类型
op 效果 EPOLL_CTL_ADD 将文件描述符及其事件添加到epoll模型中 EPOLL_CTL_MOD 改变文件描述符对应的事件 EPOLL_CTL_DEL 将文件描述符及其事件从epoll模型中移除 -
struct epoll_event:
typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
-
返回值:调用成功返回0,调用失败返回-1,同时errno被设置;
-
-
epoll_wait:等待IO事件就绪
- epfd:同上;
- events:一个指针(一个数组),里面都是已经就绪的fd和事件;
- maxevents:指定本次返回的events的最大数量,通常设置为epoll_create的参数。
- timeout:同上;
- 返回值:跟select、poll一样。
epoll模型
当调用epoll_create时,内核会创建一个eventpoll结构体。
struct eventpoll {
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
};
也就是创建一颗红黑树,而后每次通过epoll_ctl添加进来的fd和事件都会组织成一个结构体挂载到这颗红黑树上。
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
epoll_ctl中的ADD就是往红黑树中添加结点、DEL就是删除红黑树中的结点、MOD就是修改红黑树中结点内部的event。
每个添加进来的事件都会与设备驱动建立回调关系,当事件触发时就会调用回调函数将其添加到就绪队列中
每个结点是可能同时在多个数据结构中的!
红黑树中就绪的结点同时会被组织成一个就绪队列,epoll_wait返回的就是该队列。
优点
- 跟poll一样,解决了select中fd有上限的问题;
- 解决了select、poll中的轮询遍历的问题;
- 减少了用户态向内核态的拷贝(每次调用select、poll都会拷贝,而epoll仅在合适的时候用epoll_ctl添加结点)
LT模式与ET模式
- LT(Level Triggered)水平触发模式:只要当fd上有数据时,每次调用epoll_wait都会返回该fd;
- ET(Edge Triggered)边缘触发模式:只有当fd上的数据发生变化时(从零到有,从少到多),调用epoll_wait才会返回该fd。
select、poll、epoll都是默认处于LT模式,而epoll可以选择ET模式。
假设有一种场景:第一次从fd上读数据,没有读全,需要读第二次,如果处于LT模式下,第二次调用epoll_wait是能够继续从该fd上读数据的,而如果处于ET模式下,第二次调用epoll_wait就不能够从该fd上继续读数据了。这时就会出现问题,所以需要程序员能够一次性读全数据,无论处于LT还是ET,程序员都能够选择一次性读全数据,但是处于ET模式下,程序员就必须要一次性读全数据。
一次性读全数据的优势:能够更快速的将数据从内核拷贝到用户,从而能够拥有更大的TCP窗口,提高底层的数据发送效率,提高吞吐量。