Linux网络编程中select、poll、epoll的区别

文章详细阐述了从阻塞IO到IO多路复用的概念,对比了select、poll和epoll在处理多路IO复用时的原理、优缺点。select存在最大连接数限制,poll无此限制但仍有遍历开销,epoll则通过红黑树和事件驱动优化了性能,适用于大规模并发连接的场景。同时提到了epoll的LT和ET两种触发模式及其应用场景。
摘要由CSDN通过智能技术生成

关于select、poll、epoll的区别,之前在另外一篇文章做了总结,本篇主要对原理上做出更详细的阐述,参考文献将在文末附上。

1. 从阻塞IO到IO多路复用

  • 阻塞IO方案,当进程使用系统调用,该系统调用涉及到IO操作,而此时数据如果没准备好,该进程就会进入等待状态(更多关于阻塞IO、非阻塞IO、同步IO、异步IO可以参考这篇文章)。这样,为了能够处理多个socket的请求,需要使用多线程来处理。缺点:线程切换开销很大

    • 假设有以下程序
      //创建socket
      int s = socket(AF_INET, SOCK_STREAM, 0);   
      //绑定
      bind(s, ...)
      //监听
      listen(s, ...)
      //接受客户端连接
      int c = accept(s, ...)
      //接收客户端数据
      recv(c, ...);
      //将数据打印出来
      printf(...)
      
    • 工作队列:A,B,C目前都处于运行状态,其中A负责执行上段程序
      在这里插入图片描述
    • 等待队列:进程A创建socket,其中包含了、发送队列,接收缓冲区,等待列表
      在这里插入图片描述
    • 当进程A执行到recv()时,没有数据,操作系统就会将A放到socket的等待列表,此时工作队列只有B、C,进程A陷入等待状态,即阻塞
      在这里插入图片描述
    • 唤醒进程:当socket接收缓冲区有数据时,将A放回工作队列,成为运行态,继续执行代码,recv()读到数据。
      在这里插入图片描述
  • 非阻塞轮询方案,非阻塞IO不会让进程等待,调用立即返回,这样就可以在一个线程里轮询多个fd是否就绪。缺点:每次检查fd都会调用系统函数,用户态与内核态间反复更换,开销很大。

//用户态
sockfd_list;
for(sfd : sockfd_list)
{
	if(readale(sfd))//非阻塞的
	{
		//处理sfd
	}
}
  • IO多路复用方案:一次系统调用,检查多个fd的状态。在其内部,使用的就是非阻塞IO。相当于把非阻塞系统调用放到内核态进行,减少了用户态与内核态的切换。
	//内核态
	sockfd_list;
	for(sfd : sockfd_list)
	{
		if(readale(sfd))//非阻塞的
		{
			count++
			FDSET(fd, &res_rset) // 将 fd 添加到就绪集合中
		}
	}
	return count;

select、poll、epoll都是同步阻塞的,有fd事件触发,返回;触发,阻塞,等待fd事件触发或超时。

2. select

int select(int nfds,
            fd_set *restrict readfds,
            fd_set *restrict writefds,
            fd_set *restrict errorfds,
            struct timeval *restrict timeout);

readfds:传入:需要监听读事件fd的集合,传出:满足读事件的fd集合
writefds:传入:需要监听写事件fd的集合,传出:满足写事件的fd集合
errorfds:传入:需要监听错误事件fd的集合,传出:满足错误事件的fd集合
timeout:超时时间

select遍历前nfds个描述符,找到触发事件的fd,放到对应的集合,返回触发事件的总数。

  • 执行流程

    • 用户线程调用select,将fd_set从用户空间拷贝到内核空间
    • 内核对fd_set遍历一遍,检查是否有就绪的fd,如果没有的话,就会进入休眠,直到有就绪的fd。
    • 内核返回select的结果给用户线程,即就绪的文件描述符数量。
    • 用户拿到就绪文件描述符数量后,再次对fd_set进行遍历,找出就绪的文件描述符。
    • 用户线程对就绪的文件描述符进行读写操作。
  • 示例

int s = socket(AF_INET, SOCK_STREAM, 0);  
bind(s, ...)
listen(s, ...)

int fds[] =  存放需要监听的socket

while(1){
    int n = select(..., fds, ...)
    for(int i=0; i < fds.count; i++){
        if(FD_ISSET(fds[i], ...)){
            //fds[i]的数据处理
        }
    }
}

select原理

  • 优点

    • 跨平台
    • 时间精度高,ns级别
  • 缺点

    • 最大限制:依赖于FD_SIZE,32位默认1024,64位默认2048
    • 时间复杂度:线性扫描fd,轮询,时间复杂度O(n)
    • 只返回就绪fd的数量,无法具体知道哪个fd是就绪的,当内核返回就绪的文件描述符数量后,还需要遍历一次找出就绪的fd,如果有大量连接,但很少的请求,这种操作就很耗时。
    • 内存拷贝:fd_set从用户态到内核态拷贝

3. poll

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};

  • 执行流程

    • 用户调用poll,将fd数组从用户态拷贝到内核态,然后转换为fd链表
    • 内核对fd链表遍历一遍,检查是否有就绪的fd,如果没有的话,就会进入休眠,直到有就绪的fd。
    • 返回给用户线程就绪的文件描述符数量
    • 用户线程再遍历一次fd数组,找出就绪的文件描述符。
    • 用户线程对就绪的fd进程读写操作。
  • 与select的区别

    • select是fd数组,有长度限制;poll是fd链表,无长度限制
  • 示例

   struct pollfd fds[POLL_LEN];
   unsigned int nfds=0;
   fds[0].fd=server_sockfd;
   fds[0].events=POLLIN|POLLPRI;
   nfds++;
   while {
       res=poll(fds,nfds,-1);
       if(fds[0].revents&(POLLIN|POLLPRI)) {
           //执行accept并加入fds中,nfds++
           if(--res<=0) continue
       }
       //循环之后的fds
       if(fds[i].revents&(POLLIN|POLLERR )) {
           //读操作或处理异常等
           if(--res<=0) continue
       }
   }
  • 优点
    • 没有最大连接数限制
  • 缺点
    • 时间复杂度:内核采用线性扫描,时间复杂度O(n)。
    • 只返回就绪fd的数量,无法具体知道哪个fd是就绪的,当内核返回就绪的文件描述符数量后,还需要遍历一次找出就绪的fd,如果有大量连接,但很少的请求,这种操作就很耗时。
    • 内存拷贝:fd数组从用户态到内核态拷贝。
    • LT触发:报告fd后没被处理,下次poll还会再次报告该fd。

4. epoll

#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;
    ...
};

// 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_ctl负责维护等待队列,epoll_wait负责阻塞监听
    • 使用红黑树存储fd集合。
    • 使用双链表存储就绪的fd,只需要判断该链表有无元素就可以知道有无fd就绪。
    • 每个fd只需在添加时传入一次,通过事件 callback 更改fd状态。
  • epoll_create

    int epoll_create(int size);
    
    • 创建epoll实例,返回一个fd引用该实例
    • epoll实例包含监听列表(红黑树)、就绪列表(双向链表)
  • epoll_ctl

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    
    • 将fd添加到监听列表,同时为fd设置一个回调函数,并监听事件event。当fd触发事件时,调用回调函数,将fd添加到就绪队列。
  • epoll_wait

    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    
    • 查看就绪队列有无fd,有就返回就绪fd的个数;无就等待timeout毫秒。
    • events会保存所有就绪fd。
  • 示例

    int s = socket(AF_INET, SOCK_STREAM, 0);   
    bind(s, ...)
    listen(s, ...)
    
    int epfd = epoll_create(...);
    epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中
    
    while(1){
        int n = epoll_wait(...)
        for(接收到数据的socket){
            //处理
        }
    }
    

epoll原理

  • 优点

    • 没有最大连接数限制
    • 时间复杂度低:ET触发+事件驱动,监听回调,O(1)。就绪的fd调用回调函数,加入就绪队列。
    • 内存拷贝:利用mmap()文件映射内存加速与内核空间的消息传递,减少拷贝开销。
      在这里插入图片描述
      在这里插入图片描述
  • 缺点

    • 只适用于Linux
  • 应用场景

    • 适合用epoll的应用场景:

      • 对于连接特别多,活跃的连接特别少
      • 典型的应用场景为一个需要处理上万的连接服务器,例如各种app的入口服务器,例如qq。
    • 不适合epoll的场景:

      • 连接比较少,数据量比较大,例如ssh
      • epoll 的惊群问题:因为epoll 多用于多个连接,只有少数活跃的场景,但是万一某一时刻,epoll 等的上千个文件描述符都就绪了,这时候epoll 要进行大量的I/O,此时压力太大。
  • epoll的两种模式

    • LT水平触发:缓冲区有数据就触发,所以如果当前事件没有被处理,epoll_wait还会再触发。
      在这里插入图片描述

    • ET边缘触发:缓冲区有新数据才触发,所以当事件没被处理,epoll_wait不会再次触发。

      • ET模式下,应当将fd的数据读完(非阻塞轮询),直到errno返回EAGAIN,否则下次epoll_wait会丢掉事件,不会返回剩余的数据。
      • ET模式很大程度减少了epoll事件被重复触发的次数,效率比LT模式高。
      • epoll工作在ET模式的时候,必须使用非阻塞,以避免由于一个fd的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
        在这里插入图片描述

5. 总结

在这里插入图片描述

参考连接:
https://www.cnblogs.com/Hijack-you/p/13057792.html
https://zhuanlan.zhihu.com/p/554348972
https://www.cnblogs.com/yungyu16/p/13066744.html
https://zhuanlan.zhihu.com/p/367591714

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值