I/O编程关注的问题
epoll我们经常使用在网络编程I/O模型中,在此模型中我们主要关注的问题点是连接如何建立,连接何时断开,消息如何到达,消息是否发送完毕。 用户空间监测内核的消息得到这个结果。 在上图的蓝色框中,我们主要的关注的是读写是否被阻塞,数据是否达到,到达了怎么通知给用户空间。
推荐免费的直播课程: https://ke.qq.com/course/417774?flowToken=1040690
阻塞io模型和?阻塞io模型
fcntl(c->fd, F_SETFL, O_NONBLOCK);
io函数在数据未到达时是否?刻返回
阻塞模型和非阻塞模型主要区别在数据准备阶段是否立刻返回,如果是在阻塞模型,数据准备阶段和数据拷贝阶段都会被阻塞,处理时间会比较长;在非阻塞模型中,如果在数据准备阶段,调用read/recv会立刻给一个结果(为准备好返回-1)。
I/O多路复用
I/O多路复用主要有slect, poll 和 epoll 三个主要的函数。这里我们主要介绍epoll函数。实现的机制主要是使用一个线程来检测多个io事件,把相应的事件fd添加到epoll中,使用epoll来管理。如果读写事件准备好时,epoll会触发相应的世间来通知到用户。
epoll的API
epoll的核心是3个API,核心数据结构是:1个红黑树和1个链表组成。
struct eventpoll {
struct rb_root rbr;
struct list_head rdllist;
};
struct epitem {
struct rb_node rbn;
struct list_head rdllist;
struct epoll_filefd ffd;
struct eventpoll *ep;
struct epoll_event event;
};
struct epoll_event {
__uint32_t events;
epoll_data_t data;
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
主要函数
int epoll_create(int size);
size参数告诉内核这个epoll对象会处理的事件?致数量,?不是能够处理的事件的最?数(同时,size不要传0,会报invalid argument错误)。 在现在linux版本中,这个size参数已经没有意义了; 返回: epoll对象句柄;之后针对该epoll的操作需要通过该句柄来标识该epoll对象;
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* 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_data_t data;
};
epoll_ctl向epoll对象添加、修改或删除事件; 返回: 0表示成功, -1表示错误,根据errno错误码判断错误类型。 op类型:
EPOLL_CTL_ADD 添加新的事件到epoll中
EPOLL_CTL_MOD 修改epoll中的事件
EPOLL_CTL_DEL 删除epoll中的事件
event.events 取值:
EPOLLIN 表示该连接上有数据可读(tcp连接远端主动关闭连接,也是可读事件,因为需要处理发送来的FIN包; FIN包就是read 返回 0)
EPOLLOUT 表示该连接上可写发送(主动向上游服务器发起?阻塞tcp连接,连接建?成功事件相当于可写事件)
EPOLLRDHUP 表示tcp连接的远端关闭或半关闭连接
EPOLLPRI 表示连接上有紧急数据需要读
EPOLLERR 表示连接发?错误
EPOLLHUP 表示连接被挂起
EPOLLET 将触发?式设置为边缘触发,系统默认为?平触发
EPOLLONESHOT 表示该事件只处理?次,下次需要处理时需重新加?epoll
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
收集 epoll 监控的事件中已经发?的事件,如果 epoll 中没有任何?个事件发?,则最多等待timeout 毫秒后返回。 返回:表示当前发?的事件个数 返回0表示本次没有事件发?; 返回-1表示出现错误,需要检查errno错误码判断错误类型。
注意: events 这个数组必须在?户态分配内存,内核负责把就绪事件复制到该数组中; maxevents 表示本次可以返回的最?事件数?,?般设置为 events 数组的?度; timeout表示在没有检测到事件发?时最多等待的时间;如果设置为0,检测到rdllist为空?刻返回;如果设置为-1,?直等待; 所有添加到epoll中的事件都会与?卡驱动程序建?回调关系,相应的事件发?时会调?这?的回调?法(ep_poll_callback) ,它会把这样的事件放在rdllist双向链表中。
epoll的两种触发方式
epoll监控多个文件描述符的I/O事件。epoll支持边缘触发(edge trigger,ET)或水平触发(level trigger,LT),通过epoll_wait等待I/O事件,如果当前没有可用的事件则阻塞调用线程。ET模式可以理解为状态的改变(无数据->有数据, 有数据->无数据), 而LT可理解为一直持续的某种状态(数据不为空或者不满)。 select和poll只支持LT工作模式,epoll的默认的工作模式是LT模式。
1. 水平触发的时机
- 对于读操作,只要缓冲内容不为空,LT模式返回读就绪。
- 对于写操作,只要缓冲区还不满,LT模式会返回写就绪。
当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你。如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率。 LT模式适合一次性读大数据。
2. 边缘触发的时机
- 对于读操作
当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候。 当有新数据到达时,即缓冲区中的待读数据变多的时候。 当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时。
当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。 ET模式适合读少数据。
epoll与select、poll的对比
1. 用户态将文件描述符传入内核的方式
- select:创建3个文件描述符集并拷贝到内核中,分别监听读、写、异常动作。这里受到单个进程可以打开的fd数量限制,默认是1024。
- poll:将传入的struct pollfd结构体数组拷贝到内核中进行监听。
- epoll:执行epoll_create会在内核的高速cache区中建立一颗红黑树以及就绪链表(该链表存储已经就绪的文件描述符)。接着用户执行的epoll_ctl函数添加文件描述符会在红黑树上增加相应的结点。
2. 内核态检测文件描述符读写状态的方式
-
select:采用轮询方式,遍历所有fd,最后返回一个描述符读写操作是否就绪的mask掩码,根据这个掩码给fd_set赋值。 -
poll:同样采用轮询方式,查询每个fd的状态,如果就绪则在等待队列中加入一项并继续遍历。 -
epoll:采用回调机制。在执行epoll_ctl的add操作时,不仅将文件描述符放到红黑树上,而且也注册了回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数,该回调函数将文件描述符放在就绪链表中。
3. 找到就绪的文件描述符并传递给用户态的方式
-
select:将之前传入的fd_set拷贝传出到用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。 -
poll:将之前传入的fd数组拷贝传出用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。 -
epoll:epoll_wait只用观察就绪链表中有无数据即可,最后将链表的数据返回给数组并返回就绪的数量。内核将就绪的文件描述符放在传入的数组中,所以只用遍历依次处理即可。这里返回的文件描述符是通过mmap让内核和用户空间共享同一块内存实现传递的,减少了不必要的拷贝。
4. 重复监听的处理方式
- select:将新的监听文件描述符集合拷贝传入内核中,继续以上步骤。
- poll:将新的struct pollfd结构体数组拷贝传入内核中,继续以上步骤。
- epoll:无需重新构建红黑树,直接沿用已存在的即可。
epoll更高效的原因
- select和poll的动作基本一致,只是poll采用链表来进行文件描述符的存储,而select采用fd标注位来存放,所以select会受到最大连接数的限制,而poll不会。
- select、poll、epoll虽然都会返回就绪的文件描述符数量。但是select和poll并不会明确指出是哪些文件描述符就绪,而epoll会。造成的区别就是,系统调用返回后,调用select和poll的程序需要遍历监听的整个文件描述符找到是谁处于就绪,而epoll则直接处理即可。
- select、poll都需要将有关文件描述符的数据结构拷贝进内核,最后再拷贝出来。而epoll创建的有关文件描述符的数据结构本身就存于内核态中,系统调用返回时利用mmap()文件映射内存加速与内核空间的消息传递:即epoll使用mmap减少复制开销。
- select、poll采用轮询的方式来检查文件描述符是否处于就绪态,而epoll采用回调机制。造成的结果就是,随着fd的增加,select和poll的效率会线性降低,而epoll不会受到太大影响,除非活跃的socket很多。
- epoll的边缘触发模式效率高,系统不会充斥大量不关心的就绪文件描述符
虽然epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
Reference: https://www.jianshu.com/p/31cdfd6f5a48
|