I/O多路复用在英文中其实叫 I/O multiplexing。就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO事件驱动IO。就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程,每次new一个线程),这样可以大大节省系统资源。所以,I/O 多路复用的特点是 通过一种机制一个进程能同时等待多个文件描述符而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。详见IO系列2-深入理解五种IO模型。I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态(对应空管塔里面的Fight progress strip槽)来同时管理多个I/O流. 发明它的原因,是尽量多的提高服务器的吞吐能力。
是不是听起来好拗口,看个图就懂了
大家都用过Nginx, nginx使用epoll接收请求,ngnix会有很多链接进来, epoll会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。Redis类似同理。
1.2 文件描述符文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、linux这样的操作系统。如下:
/*
* @author Pavani Diwanji
* @see java.io.FileInputStream
* @see java.io.FileOutputStream
* @since JDK1.0
*/
public final class FileDescriptor {
private int fd;
private Closeable parent;
private List<Closeable> otherParents;
private boolean closed;
}
复制代码
所谓 I/O 多路复用机制指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要 select 、 poll 、 epoll 来配合。
select是第一个实现IO多路复用 (1983 左右在BSD里面实现)。select是三者当中最底层的,它的事件的轮训机制是基于比特位的。每次查询都要遍历整个事件列表。
2.1.1 数据结构和参数定义理解select,首先要理解select要处理的fd_set数据结构,每个select都要处理一个fd_set结构。fd_set数据结构如下:
typedef long int __fd_mask;
/* fd_set for select and pselect. */
typedef struct
{
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
复制代码
fd_set简单地理解为一个长度是1024的比特位,每个比特位表示一个需要处理的FD(File descriptor),如果是1,那么表示这个FD有需要处理的I/O事件,否则没有。Linux为了简化位操作,定义了一组宏函数来处理这个比特位数组。
void FD_CLR(int fd, fd_set *set); // 清空fd在fd_set上的映射,说明select不在处理该fd
int FD_ISSET(int fd, fd_set *set); // 查询fd指示的fd_set是否是有事件请求
void FD_SET(int fd, fd_set *set); // 把fd指示的fd_set置1
void FD_ZERO(fd_set *set); // 清空整个fd_set,一般用于初始化
复制代码
从上述可以看出,select能处理fd最大的数量是1024,这是由fd_set的容量决定的。
再看select的调用方式:select函数详细参数文档
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
复制代码
select 函数有三个类型的返回值:
select 函数调用过程如下:
流程:
优点:
缺点:
小结:select方式,既做到了一个线程处理多个客户端连接(文件描述符),又减少了系统调用的开销(多个文件描述符只有一次 select 的系统调用 N次就绪状态的文件描述符的 read 系统调用
2.2 poll函数1993年实现了poll函数。可以认为poll是一个增强版本的select,因为select的比特位操作决定了一次性最多处理的读或者写事件只有1024个,而poll使用一个新的方式优化了这个模型。
2.2.1 数据结构和参数定义poll底层操作的数据结构pollfd,使用链表存储:
struct pollfd {
int fd; // 需要监视的文件描述符
short events; // 需要内核监视的事件,比如读事件、写事件
short revents; // 实际发生的事件,如果该文件描述符有事件发生置为1
};
复制代码
在使用该结构的时候,不用进行比特位的操作,而是对事件本身进行操作就行。同时还可以自定义事件的类型。具体可以参考手册。
同样的,事件默认初始化全部都是0,通过bzero或者memset统一初始化即可,之后在events上注册感兴趣的事件,监听的时候在revents上监听即可。注册事件使用|操作,查询事件使用&操作。比如想要注册POLLIN数据到来的事件,需要pfd.events |= POLLIN,注册多个事件进行多次|操作即可。取消事件进行~操作,比如pfd.events ~= POLLIN。查询事件:pfd.revents & POLLIN。
使用poll函数进行操作:
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
复制代码
参数说明:
基本与select函数执行过程类似:
优点:
缺点: poll 解决了select缺点中的前两条,其本质原理还是select的方法,还存在select中原来的问题
2002年被大神 Davide Libenzi (戴维德·利本兹)发明出epoll函数。epoll是一个更加高级的操作,上述的select或者poll操作都需要轮询所有的候选队列逐一判断是否有事件,而且事件队列是直接暴露给调用者的,比如上面select的write_fd和poll的fds,这样复杂度高,而且容易误操作。epoll给出了一个新的模式,直接申请一个epollfd的文件,对这些进行统一的管理,初步具有了面向对象的思维模式。
2.3.1 数据结构和参数定义我们先了解底层的数据结构:
#include <sys/epoll.h>
// 数据结构
// 每一个epoll对象都有一个独立的eventpoll结构体
// 红黑树用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
// epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
struct eventpoll {
...
/*红黑树的根节点,这颗树存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表存储所有就绪的文件描述符*/
struct list_head rdlist;
...
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events;
epoll_data_t data;
};
// API
int epoll_create(int size); // 内核中间加一个 eventpoll 对象,把所有需要监听的 socket 都放到 eventpoll 对象中
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 负责把 socket 增加、删除到内核红黑树
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 检测双链表中是否有就绪的文件描述符,如果有,则返回
复制代码
注意到,epoll_data是一个union类型。fd很容易理解,是文件描述符;而文件描述符本质上是一个索引内核中资源地址的一个下标描述,因此也可以用ptr指针代替;同样的这些数据可以用整数代替。参数定义 再来看epoll_event,有一个data用于表示fd,之后又有一个events表示注册的事件。
epoll通过下面三个函数进行。
从下图可以得知epoll相关接口作用:
事件回调通知机制:
LT模式也就是水平触发模式,是epoll的默认触发模式(select和poll只有这种模式) 触发条件:
所以简单点说,水平触发模式就是只要缓冲区中还有数据,就会一直触发事件。当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分。 例如:由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait 仍然会立刻返回并通知socket读事件就绪.直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回。支持阻塞读写和非阻塞读写
2.3.4.2 边缘触发模式(ET模式)ET模式也就是边缘触发模式,如果我们将socket添加到epoll_event描述符的时候使用了EPOLLET标志, epoll就会进入ET工作模式。 触发条件
简单点说,ET模式下只有在新数据到来的情况下才会触发事件。这也就要求我们在新数据到来的时候最好能够一次性将所有数据取出,否则不会触发第二次事件,只有等到下次再有新数据到来才会触发。而我们也不知道具体有多少数据,所以就需要循环处理,直到缓冲区为空,但是recv是一个阻塞读取,如果没有数据时就会阻塞等待,这时候就需要将描述符的属性设置为非阻塞,才能解决这个问题
void SetNoBlock(int fd)
{
int flag = fcntl(fd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, flag);
}
复制代码
当epoll检测到socket上事件就绪时, 必须立刻处理。 如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候, epoll_wait 不会再返回了。也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会。
ET的性能比LT性能更高( epoll_wait 返回的次数少了很多)。只支持非阻塞的读写
2.3.4.3 区别优点:
缺点:
select |
poll |
epoll | |
如何从fd数据中获取就绪的fd |
遍历 |
遍历 |
回调 |
底层数据结构 |
bitmap存储文件描述符 |
链表存储文件描述符 |
红黑树存储监控的文件描述符,双链表存储就绪的文件描述符 |
时间复杂度 |
获得就绪的文件描述符需要遍历fd数组,On) |
获得就绪的文件描述符需要遍历fd链表,O(n) |
当有就绪事件时,系统注册的回调函数就会被调用,将就绪的fd放入到就绪链表中。O(1) |
最大支持文件描述符 |
一般有最大值限制 |
65535 |
65535 |
最大连接数 |
1024(x86)或2048(x64) |
无限制 |
无限制 |
FD数据拷贝 |
每次调用select,需要将fd数据从用户空间拷贝到内核空间 |
每次调用poll,需要将fd数据从用户空间拷贝到内核空间 |
使用内存映射(mmap),不需要从用户空间频繁拷贝fd数据到内核空间 |
Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是 由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现
所谓 I/O 多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要 select 、 poll 、 epoll 来配合。多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符)。Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:
Redis对IO多路复用函数的选择如下图:
作者:hsfxuebao链接:https://juejin.cn/post/7050773195745411085
,版权声明:xxxxxxxxx;
工作时间:8:00-18:00
客服电话
电子邮件
扫码二维码
获取最新动态