多路io复用epoll [补档-2023-07-20]

发布时间 2024-01-13 18:03:41作者: 小白同学_C

多路io- epoll

4-1简介

​ 它是linux中内核实现io多路/转接复用的一个实现。(epoll不可跨平台,只能用于Linux)io多路转接是指在同一个操作里,同时监听多个输入输出源,在其中一个或多个输入输出源可用时范慧慧这个源,然后对其进行操作。

​ epoll采用红黑树来管理待检测的集合,而poll和select都是以线性的方式进行管理。随着集合的增大,select和poll的检测速度会逐渐变慢,而epoll凭着红黑树的性质使得自己不会慢很多,所以使用epoll效率高。

​ select和poll在工作过程中会对内核/用户空间数据进行拷贝。而在epoll中,内核和用户区使用的是共享内存,省去了没有必要的拷贝。

​ epoll会直接返回那些文件描述符是已经准备就绪的,而select和poll则需要我们自己想办法判断。

​ epoll没有最大文件描述符的限制,仅受系统可以打开多少文件描述符的限制。

​ 当多路复用的文件数量庞大,io操作频繁时,可使用epoll。

4-2 epoll的相关函数

头文件:#include <sys/epoll>

epoll_create函数:

函数原型:int epoll_create(int size);

函数功能:创建一个红黑树模型实例,用于管理待检测的文件描述符集合。

函数参数

size:这个参数在Linux内核2.6.8版本以后,这个参数是被忽略的,只需要指定一个大于0的数值即可。

函数返回值

成功:返回一个有效的文件描述符,通过它就可以访问创建的epoll实例了。

失败:返回-1

epoll_ctl函数:

函数原型:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

函数功能:用于管理红黑树上的结点,可以进行添加,删除,修改等操作。

函数参数

epfd:这个是epoll_create返回的文件描述符,通过它找到对应的epoll实例。

op:这是一个枚举值,控制通过该函数执行什么操作

​ EPOLL_CTL_ADD:往epoll模型中添加新的节点。

​ EPOLL_CTL_MOD:修改epoll模型中已经存在的节点。

​ EPOLL_CTL_DEL:删除epoll模型中的指定的节点。

fd:填要添加/修改/删除的文件描述符。

event:epoll事件,用来修饰第三个参数对应的文件描述符,指定检测这个文件描述符的某些事情。

​ EPOLLIN:读事件,接收数据,如果检测到读缓冲区有动静,代表该文件描述符就绪。

​ EPOLLOUT:写事件,发送数据,如果检测到写缓冲区有动静,代表该文件描述符就绪。

​ EPOLLERR:异常事件。

data:用户数据变量,这是一个联合体类型,通常情况下使用里边的fd成员,用于存储待检测的文件描述符的值,在调用epoll_wait()函数的时候这个值会被传出。

函数返回值

成功:返回0

失败:返回-1

结构体struct epoll_event的成员

​ uin32_t events:监听的事件类型

​ epoll_data_t data它是一个epoll_data共用体,代表用户数据

共用体epoll_data:

​ void *ptr:存储一个指针

​ int fd:存储一个文件描述符

​ uint32_t u32:存储一个32位无符号整数

​ uint64_t u64:存储一个42位无符号整数

epoll_wait函数:

函数原型:int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout):

函数功能:检测创建的epoll实例中有没有就绪的文件描述符。

函数参数

epfd:填epoll_create函数的返回值,通过它找到对应的epoll实例。

events:传出参数,一个结构体数组地址,里面存储了已就绪的文件描述符信息。

maxevents:修饰函数的第二个参数,表示结构体数组的容量或者说是元素个数。

timeout:代表检测的epoll实例中没用已经就绪的文件描述符,则该函数的阻塞时长为多少,单位毫秒。

  0:函数不阻塞,无论epoll实例中有没有就绪的文件描述符,函数调用后都直接返回。

  大于0:如果epoll实例中没有已就绪的文件描述符,则阻塞对应毫秒数再返回。

   -1:函数一直阻塞,直至epoll实例中有已就绪的文件描述符之后才解除阻塞。

函数返回值

成功

​ 大于0:则代表检测到的已就绪的文件描述符总个数。

​ 等于0:则代表函数没有检测到满足条件的文件描述符,然后阻塞被解除了。

失败:返回-1

4-3 在服务端使用epoll实现io多路转接的步骤:

第一步:使用socket函数创建一个用于监听的套接字描述符lfd。

第二步:使用bind函数将用于监听的描述符lfd与服务端本地地址进行绑定。

第三步:将lfd设置为监听态。

第四步:创建一个struct epoll_event 变量ev,用来每次为epoll实例送入要检测的描述符。创建一个相同类型的结构体数组evs,用于存储epoll_wait函数每次调用后获得的结果。

第五步:创建一个epoll实例,并且将用于监听的文件描述符lfd通过epoll_ctl函数加入到epoll实例中,然后让epoll检测lfd的读缓冲区。

第六步:进入无限循环,在循环中使用epoll_wait函数不断检测epoll实例中有没有描述符就绪,如果有文件描述符就绪了,那么epoll_wait函数则会返回就绪的文件描述符个数,同时会将就绪的文件描述符都存放到刚才我们创建的evs数组当中去,然后我们就可以对这个数组进行遍历,来挨个处理里面已经准备就绪的文件描述符。因为一开始我们将监听描述符lfd放入了epoll实例中了,所以epoll_wait修改的evs数组中,可能既监听到请求的lfd也包含其他与客户端建立连接的文件描述符。所以现在分两种情况:

n 如果是lfd也就是监听描述符就绪了,那么代表监听到了新的客户端连接请求。当我们在在遍历evs数组时,如果发现某个就绪的文件描述符是用于监听的lfd,则使用lfd通过accept函数与客户端建立连接,然后将accept返回的已经建立连接的文件描述符通过epoll_ctl函数添加到epoll实例中去,同时我们可以设置让epoll监听它那些缓冲区。

n 如果是普通的与客户端进行连接的文件描述符就绪了,那么代表有客户的向服务端发送数据了,那么我们可以使用这个文件描述符与客户端进行通信了,记得在通常的过程中要判断客户端是否已经关闭链接或者其他的一些什么行为,如果客户端断开了链接,那么我们可以通过epoll_ctl函数将这个文件描述符从epoll实例中删除掉。

第七步:重复第六步,直至我们服务端不需要接收请求了,可以使用close函数关闭监听文件描述符lfd以及epoll创建的实例,就此结束。

4-4 使用epoll实现io多路转接的服务端代码实例(epoll反应堆):

点击查看代码
#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <string.h>

#include <arpa/inet.h>

#include <sys/socket.h>

#include <sys/epoll.h>

#define MAX_NUM 128 //待检测的描述符的最大数量

 

int main()

{

  struct sockaddr_in serv;//用于记录服务端本地地址

  struct epoll_event ev; //用于每次往epoll实例中送入数据

  struct epoll_event evs[MAX_NUM]; //用于记录每次epoll_wait函数检测就绪的描述符

  int epfd; //用于记录epoll实例

 

  //创建套接字

  int lfd = socket(AF_INET, SOCK_STREAM, 0);

  if (lfd == -1) {

​     printf("创建套接字失败!\n");

​     exit(0);

  }

 

  //绑定

  serv.sin_family = AF_INET;

  serv.sin_port = htons(10066);

  serv.sin_addr.s_addr = htonl(INADDR_ANY);

  int rent = bind(lfd,(struct sockaddr *)&serv,sizeof(serv));

  if (rent == -1) {

​     printf("绑定套接字失败!\n");

​     exit(0);

  }

 

  //监听

  rent = listen(lfd,MAX_NUM);

  if (rent == -1) {

​     printf("监听失败!\n");

​     exit(0);

  }

 

  //创建epoll实例

  epfd = epoll_create(100);

  if (epfd == -1) {

​     printf("创建epfd实例失败!\n");

​     exit(0);

  }

 

  //将监听文件描述符添加到实例epfd中

  ev.events = EPOLLIN;//设置检测读缓存

  ev.data.fd = lfd;

  rent = epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);

  if (rent == -1) {

​     printf("监听文件描述符加入epoll实例失败!\n");

​     exit(0);

  }

 

  //持续检测

  int size = sizeof(evs) / sizeof(struct epoll_event);

 

  while (1)

  {

​     //调用该函数检测epfd实例中是否有文件描述符已经就绪,没有则一直阻塞(可改)

​     int num = epoll_wait(epfd,evs,size,-1);

​     

​     //循环遍历evs数组,并且处理就绪的文件描述符

​     for (int i = 0; i < num; i++) {

​       //取出文件描述符

​       int log_fd = evs[i].data.fd;

​       //判断它是不是监听文件描述符,如果是则代表有客户端连接请求

​       if (log_fd == lfd) {

​         //建立连接

​         int cfd = accept(lfd, NULL, NULL);

​         //将新得到的与客户端建立连接的文件描述符添加到epoll实例中

​         ev.events = EPOLLIN;//这里检测读缓存,可以根据实际情况更改

​         ev.data.fd = cfd;

​         rent = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);

​         if (rent == -1) {

​           printf("监听文件描述符加入epoll实例失败!\n");

​           exit(0);

​         }

​       }

​       else {

​         //如果不是监听描述符那就是与客户端通信的描述符

​         //你可以在这里与客户端进行通信,并且处理来自各项事物

​         //这里以简单的收到信息然后回复客户端为例子

​         char data[100];//用于存储来自客户端的信息

​         char serv_data[100] = "服务端已经收到你的数据!";

​         memset(data, 0, sizeof(data));

​         int len = recv(log_fd, data, sizeof(data),0);

​         if (len == 0) {

​           printf("客户端已经关闭连接!\n");

​           //将该文件描述符从epoll实例中去除

​           epoll_ctl(epfd,EPOLL_CTL_DEL,log_fd,NULL);

​           //关闭该描述符

​           close(log_fd);

​         }

​         else if (len > 0) {

​           //代表接收到了数据

​           printf("来自客户端:%s\n",data);

​           if (send(log_fd,serv_data,sizeof(serv_data),0) == -1) {

​             printf("服务器回应客户端失败!\n");

​           }

​         }

​         else {

​           perror("recv");

​           exit(0);

​         }

​       }

​     }

  }

 

  close(epfd);//关闭epoll实例

  close(lfd); //关闭监听文件描述符

  return 0;

}

4.5 epoll的水平触发模式LT

​ 当文件描述符上有事件发送时,epoll_wait函数会立刻返回,并且返回所以处于就绪状态的文件描述符。但如果我们对已就绪的文件描述符进行了数据处理后仍有一些文件描述符上剩下了没有处理完的数据,那么下次调用epoll_wait函数时任然会返回该文件描述符。

​ 水平模式适用于阻塞式io或阻塞式io,并且epoll默认的是水平触发模式。

4.6 epoll的边沿触发模式ET

​ 在边沿触发模式下,如果文件描述符就绪了则epoll_wait函数会返回就绪的文件描述符,但是如果我们在对文件描述符处理时没有处理完,文件描述符上依然剩余着某些数据或者缓冲区未满,那么下一次epoll_wait检测时遇到这个没有处理完的描述符,将不再通知。直到有新的数据到来。

​ 综上所述:epoll在边沿模式下epoll_wait检测到文件描述符有新事件时才会通知,如果不是新事件,则不通知,当通知的次数减少后,效率才能提高。

4.6.1水平模式切换为边沿模式的方法

第一步:在为epoll实例添加文件描述符时,我们会用到一个结构体struct epoll_event,其中中有一个events 成员,将它按照你的实际需求设置为以下内容即可:

​ EPOLLIN | EPOLLET:设置边沿触发模式,并且监听可读事件

​ EPOLLOUT | EPOLLET:设置边沿触发模式,并且监听可写事件

第二步:对于读缓冲区,因为边沿模式只会提醒一次,如果我们操作完某个文件描述符后发现文件描述符 的读缓冲区还有剩余数据,但是接下来epoll_wait就不会提醒整个文件描述符了。所以为了避免数据的丢失和 处理不完整,我们就需要一次性读完整个缓冲区的内容,假设读缓冲区中的内容特别多,我们一次性读不完我 们该怎么办?(写缓冲区不需要担心)

第一种方法:准备一块特大的内存。假设读缓冲区有1MB的数据,那么我们准备2MB来接收这个数据, 像这种通过提前准备一块大的内存的方法来接收数据的方法存在一些弊端:1.系统能够分配的内存是有上限的。 2.内存的大小没有一个统一的设定标准,太多了浪费,太小了不够使用。

第二种方法:通过循环的方式来接收数据,假设1024kb的数据,我们只有100kb的内存用于接收数据, 那么我们可以循环11次来接收这些数据。但是这存在一个大大的弊端,因为套接字是默认阻塞的。当调用read 和recv函数时,如果缓冲区的数据被读完后,当前进程和线程就阻塞了,这样我们就无法进行其他操作了。在 边沿模式下,我们必须将套接字设置为非阻塞。以下是设置非阻塞的方法:

第一步:int flags = fcntl(cfd,F_GETFL); cfd是用于通信的套接字,也是要设置为非阻塞的套接字。

第二步:flags |= O_NONBLOCK; 我们通过按位或操作符将 O_NONBLOCK 标志位设置到 flags 变量中,表示将套接字设置为非阻塞模式。

第三步:fcntl(cfd, F_SETGL, flags); 我们使用 F_SETFL 参数将新的标志设置回套接字。

注意:调用fcntl函数需要头文件#include <fcntl.h>。当缓冲区数据被读完了,调用的read()/recv()函数还会继续从缓冲区中读数据,此时函数调用就失败了,返回-1