大家都知道,一个完整的IO操作所花费的时间在计算机中是非常多的(速度非常慢),那么这些时间都花费在哪里呢?
IO = 等待数据就绪 + 数据拷贝
而等待数据就绪所花费的时间占了整个IO时间的99%,数据拷贝所花费的时间仅占1%。
IO模型分为同步IO和异步IO。
同步IO分为阻塞IO、非阻塞IO、信号驱动型IO、多路复用型IO。
多路复用型IO和前三者的区别就是,多路复用型IO能同时监视多个fd,从而大大提高了效率。
本篇讲述多路复用型IO。
函数原型:
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数解析:
nfds:需要监视的文件描述符的最大值+1(告诉操作系统要查找的文件描述符的范围);
readfds、writefds、exceptfds:分别都是一个位图,表示要监视的某些fd的读事件、写事件、异常事件。它们都是输入输出型参数,输入时,将需要监视的文件描述符和事件添加到位图中,输出时,就是已经就绪的文件描述符和事件。
timeout:
返回值:
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
函数原型:
#include
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 */
};
这两个事件也分别都是位图结构,通过宏来设置对应事件:
事件 | 描述 | 是否可作为输入 | 是否可作为输出 |
---|---|---|---|
POLLIN | 数据(包括普通数据和优先数据)可读 | ✔ | ✔ |
POLLRDNORM | 普通数据可读 | ✔ | ✔ |
POLLRDBAND | 优先级带数据可读(Linux不支持) | ✔ | ✔ |
POLLPRI | 高优先级数据可读,比如TCP带外数据 | ✔ | ✔ |
POLLOUT | 数据(包括普通数据和优先数据)可写 | ✔ | ✔ |
POLLWRNORM | 普通数据可写 | ✔ | ✔ |
POLLWRBAND | 优先级带数据可写 | ✔ | ✔ |
POLLRDHUP | TCP连接被对方关闭,或者对方关闭了写操作,它由GNU引入 | ✔ | ✔ |
POLLERR | 错误 | ✘ | ✔ |
POLLHUP | 挂起。比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件 | ✘ | ✔ |
POLLNVAL | 文件描述符没有打开 | ✘ | ✔ |
epoll有最基础的三个系统调用。
函数原型:
#include
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事件就绪
当调用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返回的就是该队列。
select、poll、epoll都是默认处于LT模式,而epoll可以选择ET模式。
假设有一种场景:第一次从fd上读数据,没有读全,需要读第二次,如果处于LT模式下,第二次调用epoll_wait是能够继续从该fd上读数据的,而如果处于ET模式下,第二次调用epoll_wait就不能够从该fd上继续读数据了。这时就会出现问题,所以需要程序员能够一次性读全数据,无论处于LT还是ET,程序员都能够选择一次性读全数据,但是处于ET模式下,程序员就必须要一次性读全数据。
一次性读全数据的优势:能够更快速的将数据从内核拷贝到用户,从而能够拥有更大的TCP窗口,提高底层的数据发送效率,提高吞吐量。