Select、Poll、Epoll的使用和区别,多种IO的区别

目录

一、四种IO分类

二、I/O多路复用select

三、I/O多路复用Poll

四、I/O多路复用Epoll

五、三种多路复用的区别总结

1、支持一个进程所能打开的最大连接数

2、fd剧增后带来的IO效率问题

3、 消息传递方式

4、索引就绪事件的效率上。


一、四种IO分类

  1. 阻塞IO:程序发起 I/O 操作时,如果数据还没有准备好,I/O 函数会一直阻塞当前线程,直到数据准备好为止
  2. 非阻塞IO:当程序发起 I/O 操作时,数据还没有准备好,立即返回给用户一个状态值,告诉程序数据还没有准备好,程序可以继续执行其他任务。(但需要用户自己循环调用I/O函数)
  3. 同步IO:(也称为阻塞 I/O)是指程序发起 I/O 操作后,必须等待 I/O 操作完成才能继续往下执行。在同步 I/O 中,I/O 函数会阻塞当前线程直到操作完成。
    • 优点:简单易用。
    • 缺点:会阻塞程序的执行,影响程序的性能。
    • 场景:在调用read/write时,必须等待函数返回(不关心返回值)之后才可以执行后面的代码。
  4. 异步IO:异步 I/O(也称为非阻塞 I/O)是指程序发起 I/O 操作后,不需要等待 I/O 操作完成就可以继续往下执行。在异步 I/O 中,I/O 函数会立即返回,操作完成I/O请求后会通过回调函数通知程序。
    1. 优点:可以提高程序的性能。
    2. 缺点:是实现比较复杂,需要使用回调函数等技术。

二、I/O多路复用select

使用步骤:

  1. 调用 select 函数:首先需要定义一个 fd_set 类型的集合,将要监听的文件描述符加入集合中,然后调用 select 函数,指定需要监听的文件描述符集合以及超时时间。

  2. 检查返回值:如果 select 函数返回正数n,表示有n个文件描述符就绪,可以进行读写操作。如果返回 0,表示超时。如果返回 -1,表示出现错误。

  3. 处理就绪文件描述符:通过遍历之前设置的文件描述符集合,检查其中的文件描述符是否就绪,如果就绪,则可以进行读写操作。

  4. 返回到步骤1:如果没有出现错误且没有超时,可以再次调用 select 函数继续监听文件描述符集合。

函数原型:

int select(int nfds,

          fd_set *readfds,

          fd_set *writefds,

          fd_set *exceptfds,

          struct timeval *timeout) 

其中:

  • nfds 参数指定需要监听的最大文件描述符值加一。
  • readfdswritefdsexceptfds 参数分别是指向文件描述符集合的指针,用于指定需要监听的文件描述符集合。
  • timeout 参数指定超时时间,可以是 NULL(表示永久阻塞),也可以是指向 timeval 结构体的指针。

在 select 函数返回之后,可以通过 FD_ISSET(fd, &fdset) 宏来判断某个文件描述符是否就绪,其中 fd 表示要判断的文件描述符,fdset 表示文件描述符集合。以下是与select搭配使用的宏。

FD_ZERO(fd_set* fds);
# 清空文件描述符集合 fds,也就是把所有比特位都置为 0

FD_SET(int fd, fd_set* fds);
# 把文件描述符 fd 加入集合 fds,也就是把对应的比特位置为 1

FD_ISSET(int fd, fd_set* fds); 
# 判断文件描述符 fd 是否在集合 fds 中,也就是判断对应的比特位是否为 1

FD_CLR(int fd, fd_set* fds);
# 把文件描述符 fd 从集合 fds 中删除,也就是把对应比特位置为 0

优点:

  • select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。

缺点和限制:

  • 文件描述符数量受限:select 函数可以监听的最大文件描述符数量是有限制的,一般默认是 1024。
  • 数据传输效率较低:对socket进行扫描时是线性扫描,即采用轮询的方法,而且当需要监听的文件描述符比较多时,频繁调用 select 函数会影响程序的效率,因为每次调用都需要进行一次遍历。
  • select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理,因此需要维护一个用来存放大量fd的数据结构(fd_set)。fd_set简单地理解为一个长度是1024的比特位,每个比特位表示一个需要处理的FD,如果是1,那么表示这个FD有需要处理的I/O事件,否则没有,其是连续存储的。每次select查询都要遍历整个事件列表。这样会使得用户空间和内核空间在传递该结构时复制开销大。
  • 每次select之后,事件集合都会被内核修改,再次调用select之前需要用户自己重新设置事件集合。

三、I/O多路复用Poll

使用步骤:

  1. 定义一个事件集合(实际上是数组,其中每个元素都是 struct pollfd结构体)。
  2. 事件绑定:把关注的文件描述符和在这个文件描述符上关注的事件绑定到一个 struct pollfd结构体中。
  3. 检查返回值:调用poll函数遍历事件集合,检查返回值返回正数n,表示有n个文件描述符就绪,可以进行读写操作。如果返回 0,表示超时。如果返回 -1,表示出现错误。
  4. 处理就绪的文件描述符:在已经就绪的事件中,根据事件和文件描述符的种类进行处理。
  5. 重复调用3~5。

函数原型:

#include <poll.h>

int poll(struct pollfd *fds,  /* 把文件描述符和事件绑定到一起 */
         nfds_t nfds,         /* pollfd的数量 */
         int timeout);        /* 超时时间 */

// poll事件结构体
struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* 需要监听的事件,由用户决定 */
               short revents;    /* 已经就绪的事件,由内核决定 */
           };

优点:

  • 把文件描述符和事件绑定到一起,使接口更简单。
  • 它没有最大连接数的限制,原因是它是基于链表来存储的。

缺点和限制:

  • poll本质上和select没有区别,每次调用时,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态。

四、I/O多路复用Epoll

步骤:

  1. 创建一个 epoll 实例,通过 epoll_create()epoll_create1() 系统调用实现。

  2. 向 epoll 实例中添加需要监听的文件描述符,可以通过 epoll_ctl() 系统调用实现,指定添加操作为 EPOLL_CTL_ADD

    int epoll_ctl(int epfd, /*epoll*/
                  int op,
                  int fd,
                  struct epoll_event *event);
    EPOLL_CTL_ADD  //往事件表上注册fd的事件
    EPOLL_CTL_MOD  //修改fd上的注册事件
    EPOLL_CTL_DEL  //删除fd上的注册事件
    

  3. 调用 epoll_wait() 等待文件描述符上的事件发生,如果有事件发生,则 epoll_wait() 函数会返回一个事件数组。

  4. 遍历事件数组,处理每个事件,根据事件类型进行相应的操作。

常见事件类型:

  • EPOLLIN:文件描述符可读;
  • EPOLLOUT:文件描述符可写;
  • EPOLLET:边缘触发模式;
  • EPOLLERR:文件描述符发生错误;
  • EPOLLRDHUP:对端关闭连接,或者半关闭连接;
  • EPOLLHUP:文件描述符被挂起;
  • EPOLLONESHOT:表示该事件只会被触发一次(同时只能被一个线程所处理)。

优点:

  • 在获取已就绪事件的时候,不会将整个文件描述符集全部遍历,只需要把那些被系统API异步唤醒后放入Ready队列的文件描述符返回给用户就好。
  • 独有ET边缘触发模式,可以提高I/O复用效率。
  • 不同于前两种哪种I/O机制,无法避免fd在操作过程中拷贝的问题。而epoll 使用了 mmap内存映射 (是指文件/对象的内存映射,被映射到多个内存页上),可以避免这个问题。

缺点:

  • 在编写轻量型的服务器时,和select相比,提升效果不大。

五、三种多路复用的区别总结

1、支持一个进程所能打开的最大连接数

  • select:单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。
  • poll:poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。
  • epoll:虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。

2、fd剧增后带来的IO效率问题

  • select:因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
  • poll:同上
  • epoll:因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

3、 消息传递方式

  • select:每次调用时,事件集合都需要在用户空间与内核空间之间传递,需要内核拷贝。
  • poll:同上
  • epoll:epoll通过内核和用户空间mmap共享内存来实现的,可以避免内核拷贝。

4、索引就绪事件的效率上。

  • select:O(N),每次调用,内核都需要遍历事件集合;且调用后,用户并不知道哪个事件就绪,还需要使用FD_ISSET来判断。
  • poll:O(N),同上,需要判断每个struct pollfd的revents是否就绪。
  • epoll:O(1),调用epoll_wait时,内核直接把ready队列里的事件集合返回给用户,且这些事件都是已就绪的。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李吱恩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值