Linux下I/O多路复用(select、poll、epoll),含函数解析及注意要点 ,干货满满!!

多路复用

1. select

简介:
select是 Linux 原生的、可跨平台的多路复用技术,主要用于网络编程中的 I/O 多路复用。

优点:
  1. 可以同时监听多个文件描述符,包括 标准输入输出、管道等,方便程序处理多种 I/O 事件。
  2. select 的使用方法简单,代码易于维护。
  3. 能够在拥有大量文件描述符时有效地避免 CPU 占用率过高
缺点:
  1. 文件描述符限制:select 的最大缺陷是能够同时监听的文件描述符数量有限,通常在1024左右。
  2. 遍历文件描述符的缺陷:每次调用 select 都需要重新遍历所有的文件描述符,效率不高,这也是 poll 和 epoll 的优势所在。
  3. 数据结构的复制:每次调用 select 都需要将文件描述符集合从用户态复制到内核态,效率较低。
在实际应用中,我们需要根据具体的情况选择 select、poll、epoll 等合适的多路复用技术。

select()

select 函数是一个阻塞函数,其主要作用是在一组文件描述符中选择出有数据可读或可写的文件描述符,从而实现 IO 多路复用,以避免阻塞,原型如下:

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • nfds:待检测的最大文件描述符加1;
  • readfds:可文件描述符集合;
  • writefds:可文件描述符集合;
  • exceptfds异常文件描述符集合;
  • timeout:超时。
  • 函数返回值
  • 0 表示超时
  • -1 则表示出现错误,可以通过 errno 变量获取错误信息

注:在使用 select 函数之前,需要调用 FD_ZERO 宏来清空可读、可写和异常文件描述符集合,并且需要调用 FD_SET 宏将需要检测的文件描述符添加到对应的集合中。

》》代码测试用例



2. poll

简介:
poll是一种 I/O 多路复用机制,在 Linux 系统中常被用于高并发网络服务器编程。

优点:
  1. 监控文件描述符的方式更加灵活:poll 可以监控任意数量的文件描述符,而不像 select 那样固定在 1024 个以内,因此在需要监控大量文件描述符的情况下,使用 poll 可能更加合适。
  2. 对文件描述符的操作不会有“文件描述符值过大”的限制:poll 可以处理任何大小的文件描述符数组,而不像 select 存在 FD_SETSIZE 限制,从而可以更加灵活地应对不同的场景。
  3. 没有“文件描述符值复制”的开销:与 select 不同,poll 没有必要复制整个文件描述符集合,因为每次调用 poll 都需要传入一个新的 pollfd 数组,这样可以减少复制的次数和开销。
缺点:
  1. 监控大量文件描述符时效率较低:poll 通过遍历整个文件描述符集合来获取 I/O 事件,当集合中文件描述符较多时,poll 的效率会降低,因为它需要遍历整个列表。
  2. 返回的事件类型缺乏细节信息:与 select 类似,poll 只是告知应用程序有文件描述符发生了 I/O 事件,但无法提供更详细的事件信息,如何具体的事件类型等。
poll 作为一种 I/O 多路复用机制,具有监控方式更加灵活、文件描述符操作没有限制等优点,但也存在着在处理大量文件描述符时效率较低、无法提供详细的事件信息等缺点。

poll()

poll 函数与 select 函数的作用相同,均可以监听多个文件描述符,一般用于 TCP 服务端程序中,
原型如下:

#include <sys/poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds:指向一个 pollfd 结构体数组的指针,每个结构体都包含了一个文件描述符和该文件描述符上需要进行监听的事件;
  • nfds:表示要监听的文件描述符数量;
  • timeout:超时时间,单位为毫秒。
  • 函数返回值
  • 返回值大于 0,则表示有就绪的文件描述符,可以通过遍历 pollfd 结构体数组,查看 revents 字段的值来确定哪些文件描述符已经就绪
  • 返回值等于 0,则表示超时
  • 返回值小于 0,则表示出错

注:在使用 poll 函数时,需要先定义一个 pollfd 结构体类型的数组,然后将待监听的文件描述符和需要监听的事件绑定到每个 pollfd 结构体上,并将这些结构体存放到数组中。当调用 poll 函数时,将该数组作为第一个参数传入即可。



3. epoll

简介:
epoll是一种优于poll的 I/O 多路复用机制,在 Linux 系统中常被用于高并发网络服务器编程;epoll 通过 epoll_create、epoll_ctl 和 epoll_wait 这三个系统调用来实现事件多路转接。

优点:
  1. 监控文件描述符的方式更加灵活:epoll 可以监控任意数量的文件描述符,而不像 select 那样固定在 1024 个以内,因此在需要监控大量文件描述符的情况下,使用 epoll 可能更加合适。
  2. 更加高效:与 select 和 poll 不同,epoll 采用基于回调的方式,只有在文件描述符上有 I/O 事件发生时才会通知应用程序。这避免了轮询过程中的资源浪费,提高了性能。
  3. 没有“文件描述符值复制”的开销:与 select 不同,epoll 没有必要复制整个文件描述符集合,因为每次调用 epoll_wait 都需要传入一个新的 epoll_event 数组,这样可以减少复制的次数和开销。
  4. 采用水平触发模式更加灵活:epoll 采用水平触发模式(LT, Level-Triggered),当文件描述符的状态变化时就会反复通知应用程序,这使得应用程序可以重复检查文件描述符是否可读或可写。相比之下,select 和 poll 采用边沿触发模式(ET, Edge-Triggered),在读写事件完成之前只会通知应用程序一次,这可能会使应用程序错过某些事件。
缺点:
  1. epoll 对文件描述符的操作有“文件描述符值过大”的限制:epoll 依赖于内核对文件描述符进行管理,如果文件描述符的值过大,可能会导致 epoll 失效。
  2. 在添加或删除文件描述符时效率较低:与 select 和 poll 不同,epoll 的 epoll_ctl 调用需要将每个文件描述符都插入到内核维护的红黑树中,这个过程需要频繁地分配和释放内存,可能会导致一定的开销。
三者之中epoll最高效,epoll 使用 epoll_event 来管理文件描述符集合,不仅可以处理任意数量的文件描述符,而且在监听过程中只会通知已经就绪的文件描述符,不需要像轮询那样遍历所有文件描述符。

》》代码测试用例


  • epoll_event
  • epoll_create()
  • epoll_ctl()
  • epoll_wait()

1. epoll_event

用于描述 epoll 实例上文件描述符事件的结构体,其定义如下:

struct epoll_event {
    uint32_t events;    // 表示关注的事件类型
    epoll_data_t data;  // 表示用户数据
};
  • events 表示关注的事件类型,是一个32位整型变量,可以包含以下几个值之一(可以通过位运算符组合多个事件类型):
    • EPOLLIN:表示对应的文件描述符可读
    • EPOLLOUT:表示对应的文件描述符可写
    • EPOLLERR:表示对应的文件描述符发生错误
    • EPOLLRDHUP:表示对应的文件描述符被对端关闭或者半关闭
    • EPOLLPRI:表示对应的文件描述符有紧急数据可读
    • EPOLLHUP:表示对应的文件描述符被挂起
    • EPOLLET:表示采用边缘触发模式
    • EPOLLONESHOT:表示只会发生一次事件
    • EPOLLWAKEUP:表示唤醒被阻塞的 epoll_wait 调用
  • data 表示用户数据,是一个联合体类型 epoll_data_t,可以存放指针、整型数据和文件描述符三种类型的数据。在使用时,需要根据 events 成员的值来确定使用哪种类型的数据

注:如果在调用 epoll_wait 函数等待事件时,events 成员的值设置为 EPOLLONESHOT,则表示只会发生一次事件,即在事件被处理完毕后需要重新注册该文件描述符才能继续监视该文件描述符。

2. epoll_create

用于创建 epoll 实例的系统调用函数,其原型如下:

#include <sys/epoll.h>

int epoll_create(int size);
  • size 是创建的 epoll 实例所能够管理的最大文件描述符数量
  • 函数返回一个整型的文件描述符(epfd),它是这个 epoll 实例的句柄,之后所有针对这个 epoll 实例的操作都需要使用这个句柄

注:在使用完 epoll 实例后,应该使用 close 函数来关闭文件描述符并释放资源。因为每个 epoll 实例都需要占用一个文件描述符,如果不及时关闭,就可能导致文件描述符被耗尽,从而引起一系列问题。

3. epoll_ctl

用于向 epoll 实例中注册、修改或删除文件描述符上的事件的系统调用函数,其原型如下:

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epfd 是 epoll 实例的句柄(文件描述符 )
  • op 是控制操作(EPOLL_CTL_ADD、EPOLL_CTL_MOD 或 EPOLL_CTL_DEL)
  • EPOLL_CTL_ADD epoll_ctl :将 fd 上的 event 指定的事件添加到 epoll 实例中
  • EPOLL_CTL_MOD epoll_ctl :修改已经注册在 epoll 实例中的 fd 的事件状态为 event 指定的状态
  • EPOLL_CTL_DEL :epoll_ctl 从 epoll 实例中删除文件描述符 fd
  • fd 是需要控制的文件描述符
  • event 是需要控制的事件,是一个 epoll_event 结构体指针
  • 函数返回值
  • 返回值为 0,则表示操作执行成功
  • 返回值为 -1,则表示函数执行失败,错误码存储在 errno 中

注:使用 epoll 实例进行多路复用后,每个文件描述符只能有一个事件类型在 epoll 实例中注册。如果需要修改这个文件描述符的监视事件,需要先使用 EPOLL_CTL_DEL 将其从 epoll 实例中删除,再使用 EPOLL_CTL_ADD 添加新的事件类型。

4. epoll_wait

用于等待 epoll 实例上有事件发生的系统调用函数,其原型如下:

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • epfd 是 epoll 实例的句柄(文件描述符)
  • events 是指向 epoll_event 结构体数组的指针,用于存放发生事件的文件描述符和对应的事件信息
  • maxevents 是 events 数组的最大长度
  • timeout 是等待事件的超时时间(单位为毫秒),可以设置为一个正整数表示等待的毫秒数,也可以设置为 -1 表示一直阻塞,直到有事件发生
  • 返回值为发生事件的文件描述符数量
  • 返回一个正整数 n:表示在超时时间内(如果指定了超时时间)或者有事件发生时,有 n 个文件描述符发生了事件;
  • 返回:表示在超时时间内(如果指定了超时时间),没有文件描述符发生事件;
  • 返回 -1,并设置 errno:表示出现错误。
  • 当 epoll_wait 返回时,它会将已经发生事件的文件描述符和对应的事件信息写入到 events 缓冲区中
  • 如果 events[i].events & EPOLLIN !=0,则表示对应文件描述符可读,可以调用 read 函数来读取数据;
  • 如果 events[i].events & EPOLLOUT !=0,则表示对应文件描述符可写,可以调用 write 函数来写入数据;
  • 如果 events[i].events & EPOLLERR !=0,则表示对应文件描述符发生错误,可以关闭该文件描述符;
  • 如果 events[i].events & EPOLLRDHUP !=0,则表示对应文件描述符被对端关闭或者半关闭,可以关闭该文件描述符;
  • 如果 events[i].events & EPOLLPRI !=0,则表示对应文件描述符有紧急数据可读,可以调用 recv 函数来读取数据。
  • 详见 epoll_event 结构中 envent 的作用

注:epoll_wait 会一直阻塞等待事件发生,直到超时或者有事件发生。因此,在使用 epoll_wait 函数前需要确保至少有一个文件描述符已经注册到 epoll 实例中,否则会一直阻塞等待。

》》代码测试用例


测试代码用例

1.select

#include <iostream>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080
#define MAXLINE 1024
#define LISTENQ 5

int main() {
   
    int sockfd, nready, maxfdp1;
    fd_set rset;
    char buf[MAXLINE];
    socklen_t clilen;
    struct sockaddr_in cliaddr, servaddr;

    // 创建服务器端socket
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) 
  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值