IO多路复用中select、poll、epoll之间的区别

 2023-09-15 阅读 23 评论 0

摘要:本文来说下IO多路复用中select、poll、epoll之间的区别 文章目录什么是IO多路复用为什么有IO多路复用机制同步阻塞(BIO)同步非阻塞(NIO)IO多路复用(现在的做法)3种多路复用方式的比较selectselect函数接口select使用示例select缺点

本文来说下IO多路复用中select、poll、epoll之间的区别

文章目录

  • 什么是IO多路复用
  • 为什么有IO多路复用机制
    • 同步阻塞(BIO)
    • 同步非阻塞(NIO)
    • IO多路复用(现在的做法)
  • 3种多路复用方式的比较
  • select
    • select函数接口
    • select使用示例
    • select缺点
  • poll
    • poll函数接口
    • poll使用示例
    • poll缺点
  • epoll
    • epoll函数接口
    • epoll使用示例
    • epoll缺点
    • epoll应用
    • epoll 水平触发(LT)与 边缘触发(ET)的区别
  • select/poll/epoll之间的区别
  • 本文小结


什么是IO多路复用

IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪时会阻塞应用程序,交出cpu。多路是指网络连接,复用指的是同一个线程

在这里插入图片描述


为什么有IO多路复用机制

poll的工作原理?没有IO多路复用机制时,有BIO、NIO两种实现方式,但有一些问题


同步阻塞(BIO)

服务端采用单线程,当accept一个请求后,在recv或send调用阻塞时,将无法accept其他请求(必须等上一个请求处recv或send完),无法处理并发

// 伪代码描述
while(1) {// accept阻塞client_fd = accept(listen_fd)fds.append(client_fd)for (fd in fds) {// recv阻塞(会影响上面的accept)if (recv(fd)) {// logic}}  
}

服务器端采用多线程,当accept一个请求后,开启线程进行recv,可以完成并发处理,但随着请求数增加需要增加系统线程,大量的线程占用很大的内存空间,并且线程切换会带来很大的开销,10000个线程真正发生读写事件的线程数不会超过20%,每次accept都开一个线程也是一种资源浪费

// 伪代码描述
while(1) {// accept阻塞client_fd = accept(listen_fd)// 开启线程read数据(fd增多导致线程数增多)new Thread func() {// recv阻塞(多线程不影响上面的accept)if (recv(fd)) {// logic}}  
}

同步非阻塞(NIO)

服务器端当accept一个请求后,加入fds集合,每次轮询一遍fds集合recv(非阻塞)数据,没有数据则立即返回错误,每次轮询所有fd(包括没有发生读写事件的fd)会很浪费cpu

setNonblocking(listen_fd)
// 伪代码描述
while(1) {// accept非阻塞(cpu一直忙轮询)client_fd = accept(listen_fd)if (client_fd != null) {// 有人连接fds.append(client_fd)} else {// 无人连接}  for (fd in fds) {// recv非阻塞setNonblocking(client_fd)// recv 为非阻塞命令if (len = recv(fd) && len > 0) {// 有读写数据// logic} else {无读写数据}}  
}

IO多路复用(现在的做法)

服务器端采用单线程通过select/epoll等系统调用获取fd列表,遍历有事件的fd进行accept/recv/send,使其能支持更多的并发连接请求

fds = [listen_fd]
// 伪代码描述
while(1) {// 通过内核获取有读写事件发生的fd,只要有一个则返回,无则阻塞// 整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,accept/recv是不会阻塞for (fd in select(fds)) {if (fd == listen_fd) {client_fd = accept(listen_fd)fds.append(client_fd)} elseif (len = recv(fd) && len != -1) { // logic}}  
}

3种多路复用方式的比较

(1)select==>时间复杂度O(n)

select、它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

(2)poll==>时间复杂度O(n)

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.

(3)epoll==>时间复杂度O(1)

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现


select

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

select poll epoll 区别,1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限

一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低

当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大


select函数接口

select函数接口

#include <sys/select.h>
#include <sys/time.h>#define FD_SETSIZE 1024
#define NFDBITS (8 * sizeof(unsigned long))
#define __FDSET_LONGS (FD_SETSIZE/NFDBITS)// 数据结构 (bitmap)
typedef struct {unsigned long fds_bits[__FDSET_LONGS];
} fd_set;// API
int select(int max_fd, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout
)                              // 返回值就绪描述符的数目FD_ZERO(int fd, fd_set* fds)   // 清空集合
FD_SET(int fd, fd_set* fds)    // 将给定的描述符加入集合
FD_ISSET(int fd, fd_set* fds)  // 判断指定描述符是否在集合中 
FD_CLR(int fd, fd_set* fds)    // 将给定的描述符从文件中删除  

select使用示例

select poll epoll,select使用示例

int main() {/** 这里进行一些初始化的设置,* 包括socket建立,地址的设置等,*/fd_set read_fs, write_fs;struct timeval timeout;int max = 0;  // 用于记录最大的fd,在轮询中时刻更新即可// 初始化比特位FD_ZERO(&read_fs);FD_ZERO(&write_fs);int nfds = 0; // 记录就绪的事件,可以减少遍历的次数while (1) {// 阻塞获取// 每次需要把fd从用户态拷贝到内核态nfds = select(max + 1, &read_fd, &write_fd, NULL, &timeout);// 每次需要遍历所有fd,判断有无读写事件发生for (int i = 0; i <= max && nfds; ++i) {if (i == listenfd) {--nfds;// 这里处理accept事件FD_SET(i, &read_fd);//将客户端socket加入到集合中}if (FD_ISSET(i, &read_fd)) {--nfds;// 这里处理read事件}if (FD_ISSET(i, &write_fd)) {--nfds;// 这里处理write事件}}}

select缺点

select缺点

  • 单个进程所打开的FD是有限制的,通过FD_SETSIZE设置,默认1024
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 对socket扫描时是线性扫描,采用轮询的方法,效率较低(高并发时)

poll

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点

  • 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
  • poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

poll函数接口

poll与select相比,只是没有fd的限制,其它基本一样

#include <poll.h>
// 数据结构
struct pollfd {int fd;                         // 需要监视的文件描述符short events;                   // 需要内核监视的事件short revents;                  // 实际发生的事件
};// API
int poll(struct pollfd fds[], nfds_t nfds, int timeout);

poll使用示例

poll使用示例

// 先宏定义长度
#define MAX_POLLFD_LEN 4096  int main() {/** 在这里进行一些初始化的操作,* 比如初始化数据和socket等。*/int nfds = 0;pollfd fds[MAX_POLLFD_LEN];memset(fds, 0, sizeof(fds));fds[0].fd = listenfd;fds[0].events = POLLRDNORM;int max  = 0;  // 队列的实际长度,是一个随时更新的,也可以自定义其他的int timeout = 0;int current_size = max;while (1) {// 阻塞获取// 每次需要把fd从用户态拷贝到内核态nfds = poll(fds, max+1, timeout);if (fds[0].revents & POLLRDNORM) {// 这里处理accept事件connfd = accept(listenfd);//将新的描述符添加到读描述符集合中}// 每次需要遍历所有fd,判断有无读写事件发生for (int i = 1; i < max; ++i) {     if (fds[i].revents & POLLRDNORM) { sockfd = fds[i].fdif ((n = read(sockfd, buf, MAXLINE)) <= 0) {// 这里处理read事件if (n == 0) {close(sockfd);fds[i].fd = -1;}} else {// 这里处理write事件     }if (--nfds <= 0) {break;       }   }}}

poll缺点

epoll详解。poll缺点

  • 每次调用poll,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 对socket扫描时是线性扫描,采用轮询的方法,效率较低(高并发时)

epoll

epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无 论fd中是否还有数据可读。

所以在ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值,或者 遇到EAGAIN错误。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

epoll为什么要有EPOLLET触发模式

如果采用EPOLLLT模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。而采用EPOLLET这种边沿触发模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。

如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符

epoll wait、epoll的优点:

  • 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
  • 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
  • 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

epoll函数接口

epoll函数接口

#include <sys/epoll.h>// 数据结构
// 每一个epoll对象都有一个独立的eventpoll结构体
// 用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
// epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
struct eventpoll {/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/struct rb_root  rbr;/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/struct list_head rdlist;
};// APIint epoll_create(int size); // 内核中间加一个 ep 对象,把所有需要监听的 socket 都放到 ep 对象中
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 负责检测可读队列,没有可读 socket 则阻塞进程

epoll使用示例

epoll使用示例

int main(int argc, char* argv[])
{/** 在这里进行一些初始化的操作,* 比如初始化数据和socket等。*/// 内核中创建ep对象epfd=epoll_create(256);// 需要监听的socket放到ep中epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);while(1) {// 阻塞获取nfds = epoll_wait(epfd,events,20,0);for(i=0;i<nfds;++i) {if(events[i].data.fd==listenfd) {// 这里处理accept事件connfd = accept(listenfd);// 接收新连接写到内核对象中epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);} else if (events[i].events&EPOLLIN) {// 这里处理read事件read(sockfd, BUF, MAXLINE);//读完后准备写epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);} else if(events[i].events&EPOLLOUT) {// 这里处理write事件write(sockfd, BUF, n);//写完后准备读epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);}}}return 0;
}

epoll缺点

epoll缺点

  • epoll只能工作在linux下

epoll应用

epoll应用

  • redis
  • nginx

epoll 水平触发(LT)与 边缘触发(ET)的区别

epoll LT 与 ET模式的区别

  • epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。
  • LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作。
  • ET模式下,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读完,或者遇到EAGAIN错误

select/poll/epoll之间的区别

同步epoll、select/poll/epoll之间的区别

selectpollepoll
数据结构bitmap数组红黑树
最大连接数1024无上限无上限
fd拷贝每次调用select拷贝每次调用poll拷贝fd首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝
工作效率轮询:O(n)轮询:O(n)回调:O(1)

本文小结

本文详细介绍了IO多路复用中select、poll、epoll之间的区别。

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/2/62286.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息