多路复用I/O-epoll

系列文章目录

第一章 多路复用I/O-select
第二章 多路复用I/O-epoll


文章目录

  • 系列文章目录
  • 前言
  • 一、epoll 接口
    • 1.1 struct epoll_event{ } 结构体
    • 1.2 接口 epoll_create()
    • 1.3 接口 epoll_ctl()
    • 1.4 接口 epoll_wait()
    • 1.5 接口 fcntl()
  • 二、epoll 原理
  • 三、epoll 触发方式
  • 四、设置阻塞方式
  • 代码 实例
  • 总结
  • 参考


前言

在网络中实现IO多路复用的技术,最常用的就是(select, poll,epoll)三种模型,但是select 受限于底层的实现,随着管理fd数量的增多,造成轮询效率下降。进而出现了epoll模型,epoll 模型底层实现是采用红黑树,不会受限于检测句柄的数量。


一、epoll 接口

epoll 的实现共有三个接口。它们分别如下所示:epoll_create()、 epoll_ctl()、epoll_wati();
头文件:

#include

1.1 struct epoll_event{ } 结构体

struct epoll_event{ }是一个结构体,用于描述一个文件描述符上的事件。其定义如下:

struct epoll_event { 
	uint32_t events; // 表示监听的事件类型
 	epoll_data_t data; // 用户数据,可以是指针或者整数值
 };

其中events字段表示需要监听的事件类型,常用事件类型如下:

EPOLLIN:表示该文件描述符上有可读数据。
EPOLLOUT:表示该文件描述符上可以写数据。
EPOLLRDHUP:表示对端关闭连接或者半关闭连接,即收到FIN包。
EPOLLHUP:表示该文件描述符被挂起,可能是对端进程崩溃或者其他错误情况导致的。
EPOLLERR:表示出错。

全部事件类型

     EPOLLIN
              The associated file is available for read(2) operations.

       EPOLLOUT
              The associated file is available for write(2) operations.

       EPOLLRDHUP (since Linux 2.6.17)
              Stream socket peer closed connection, or shut down writing half of connection.  (This flag is especially  useful  for
       EPOLLOUT
              The associated file is available for write(2) operations.

       EPOLLRDHUP (since Linux 2.6.17)
              Stream socket peer closed connection, or shut down writing half of connection.  (This flag is especially  useful  for
              writing simple code to detect peer shutdown when using Edge Triggered monitoring.)

       EPOLLPRI
              There is urgent data available for read(2) operations.

       EPOLLERR
              Error condition happened on the associated file descriptor.  epoll_wait(2) will always wait for this event; it is not
              necessary to set it in events.

       EPOLLHUP
              Hang up happened on the associated file descriptor.  epoll_wait(2) will always wait for this event; it is not  neces‐
              sary to set it in events.  Note that when reading from a channel such as a pipe or a stream socket, this event merely
              indicates that the peer closed its end of the channel.  Subsequent reads from the channel will return 0 (end of file)
              only after all outstanding data in the channel has been consumed.

       EPOLLET
              Sets  the  Edge Triggered behavior for the associated file descriptor.  The default behavior for epoll is Level Trig‐
              gered.  See epoll(7) for more detailed information about Edge and Level Triggered event distribution architectures.

       EPOLLONESHOT (since Linux 2.6.2)
              Sets the one-shot behavior for the associated file descriptor.  This means that after an event  is  pulled  out  with
              epoll_wait(2) the associated file descriptor is internally disabled and no other events will be reported by the epoll
              interface.  The user must call epoll_ctl() with EPOLL_CTL_MOD to rearm the file descriptor with a new event mask.

       EPOLLWAKEUP (since Linux 3.5)
              If EPOLLONESHOT and EPOLLET are clear and the process has the CAP_BLOCK_SUSPEND capability, ensure  that  the  system
              does  not  enter "suspend" or "hibernate" while this event is pending or being processed.  The event is considered as
              being "processed" from the time when it is returned by a call to epoll_wait(2) until the next call  to  epoll_wait(2)
              on  the  same epoll(7) file descriptor, the closure of that file descriptor, the removal of the event file descriptor
              with EPOLL_CTL_DEL, or the clearing of EPOLLWAKEUP for the event file descriptor with EPOLL_CTL_MOD.  See also BUGS.

1.2 接口 epoll_create()

epoll_create()主要用来创建epoll 实例并对其进行初始化,同时返回一个指向epoll实例的文件描述符。

int epoll_create(int size)

参数 int size: 现无意义,必须大于0,一般填1,告知内核事件表有多大。
返回值:成功返回对应的 epfd ( event poll 结构体) ,失败返回 -1

1.3 接口 epoll_ctl()

将一个 fd 添加到 epoll 的红黑树中,并设置 ep_poll_callback,callback 触发时,把对应 fd 加入到rdlist 就绪列表中。 对fd 实现增删改的操作。
epoll_ctl()函数是Linux内核提供的用于控制epoll实例的系统调用函数之一,它可以用来添加、修改或删除需要监听的文件描述符以及相应事件

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

返回值:成功返回0,失败返回-1。

参数:
- 参数1 epfd:epoll对象/epoll实例标识符

- 参数2 op:指定操作类型。
  EPOLL_CTL_ADD 增加 将指定文件描述符加入到监听队列中;
  EPOLL_CTL_MOD 修改 修改已经加入监听队列中的文件描述符对应事件信息;
  EPOLL_CTL_DEL 删除 将已经加入监听队列中的文件描述符移除。
  
- 参数3 fd:要监听的句柄(fd)

- 参数4 event: 指向一个结构体变量,用来设置需要监听的事件类型以及相关属性 要监听的事件类型,红黑树键值对kv:fd-event
  event.events常用类型:
  EPOLLIN		可读
  EPOLLOUT		可写
  EPOLLET		边缘触发,默认⽔平触发LT


具体详解:
epoll_ctl主要是对epitem对象进行操作:把fd 和struct epoll_event{}绑定

EPOLL_CTL_ADD:先查看含fd的epitem对象是否在红黑树中,如果在则退出,不在则将fd和event加到epitem对象中并将该对象插入红黑树中。
EPOLL_CTL_DEL:先查看含fd的epitem对象是否在红黑树中,如果不在则退出,在则将该epitem对象从红黑树中删除。
EPOLL_CTL_MOD:先查看含fd的epitem对象是否在红黑树中,如果不在则退出,在则更新事件。

1.4 接口 epoll_wait()

检测 红黑树列表(rdlist 列表)是否为空,不为空时候则主要是把就绪链表中的事件信息拷贝到events数组中 返回就绪的 fd 的数量;epoll_wait函数是Linux内核提供的用于异步IO操作的系统调用函数之一,它可以用于等待一个或多个文件描述符上的事件发生,并在事件发生时通知用户进程。

// 收集 epoll 监控的事件中已经发生的事件,如果 epoll 中没有任何⼀个事件发⽣,则最多等待 timeout 毫秒后返回。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
    
返回值:成功返回实际就绪的 fd 数量,失败返回-1

参数:
- 参数1 epfd:epoll 对象。

- 参数2 events:指向存储返回事件的结构体数组。 用户态创建的evnet数组,内核拷贝就绪的 fd 到该数组中。
    events[i].events:
    EPOLLIN 		触发读事件
    EPOLLOUT 		触发写事件
    EPOLLERR 		连接发生错误
    EPOLLRDHUP 		连接读端关闭
    EPOLLHUP 		连接双端关闭

- 参数3 maxevents:可以返回的最⼤事件数目,一般设置为event数组的⻓度

- 参数4 timeout:超时时间 ms。-1(一直等待)0(不等待)>0(等待时间)。断开与服务器没有交互的客户端

当有事件到达时,epoll_wait会将所有就绪的事件存放在传入的events数组中,并返回就绪事件数量。对于每个就绪的文件描述符,需要通过判断events[i].events字段中位设置情况来确定具体是哪种类型的事件(例如可读、可写等)。同时,在处理完就绪事件后,需要将相应文件描述符重新加入到epoll监听队列中。

1.5 接口 fcntl()

fcntl 函数可以将 fd 设置为非阻塞。
//修改(获取)文件描述符属性
int fcntl(int fd, int cmd, ... /* arg */ );

返回值:失败返回-1
参数
- 参数1:需要修改的文件描述符,
- 参数2:修改(获取)文件描述符的操作

例子:

//1、获取原有套接字状态的信息
	int status = fcntl(fd, F_GETFL);
//2、将非阻塞的标志与原有的标志信息做或操作
	status |= O_NONBLOCK;
//3、将标志位信息写回到socket中
	fcntl(fd, F_SETFL, status);

二、epoll 原理

epoll 场景图解
多路复用I/O-epoll_第1张图片

多路复用I/O-epoll_第2张图片
注:调用 epoll_create 会创建一个 epoll 对象。调用 epoll_ctl 添加到 epoll 中的事件都会与网卡驱动程序建立回调关系,相应事件触发时会调用回调函数(ep_poll_callback),将触发的事件拷贝到 rdlist 双向链表中。调用epoll_wait 将会把 rdlist 中就绪事件拷贝到用户态中;

调用 epoll_create 函数:返回一个epfd,同时内核会创建 eventpoll 结构体,该结构体的成员由红黑树和双向链表组成。红黑树:保存需要监听的描述符。双向链表:保存就绪的文件描述符。

调用 epoll_ctl 函数: 对红黑树进行增添,修改、删除。
添加 fd 到红黑树上,从用户空间拷贝到内核空间。一次注册 fd,永久生效。内核会给每一个节点注册一个回调函数,当该 fd 就绪时,会主动调用自己的回调函数,将其加入到双向链表当中。

调用 epoll_wait 函数: 内核监控双向链表,如果双向链表里面有数据,从内核空间拷贝数据到用户空间;如果没有数据,就阻塞。


三、epoll 触发方式

当事件就绪的时候,调用 epoll_wait(select、poll)可以得到通知。事件通知的模式有两种:水平触发(LT)和边缘触发(ET); 在处理非大量数据时候性能相差不大,但是代码逻辑处理有区别。

水平触发模式
在水平触发模式下,如果文件描述符上的事件没有被处理完毕,epoll 会持续通知应用程序该文件描述符上仍有事件待处理。在这种情况下,如果应用程序不及时响应并读取数据,则 epoll 会一直通知应用程序该文件描述符上有数据可读取。
边沿触发模式
在边沿触发模式下,只要文件描述符上出现新的事件(例如数据可读或连接建立),epoll 就会通知应用程序。但是,在通知之后,如果应用程序没有立即响应并读取所有数据,则 epoll 不会再次通知该文件描述符上有新的数据可读。

水平触发 LT: 当内核读缓冲区非空,写缓冲区不满,则一直触发,直至数据处理完成。
边缘触发 ET: 当 IO 状态发生改变,触发一次。每次触发需要一次性把事件处理完。

LT 和 ET 的特性,决定了对于数据的读操作不同

LT + 一次性读,阻塞
ET + 循环读, 非阻塞 循环recv

总体来说,边沿触发模式相比于水平触发模式更为高效,并且可以避免由于重复监听导致 CPU 占用率过高的问题,一般用于数据量很大,需要分批次接收的时候。但是,在使用边沿触发模式时需要注意及时读取所有数据,并确保每个事件都得到了正确处理。
需要注意的是,EPOLLET模式下,并不会丢失数据。即如果数据未全部接收,此时又发送新的数据,接收的时候将先接收上一次的数据。并且,epoll默认是EPOLLLT。

代码实现

// lt + 一次性读,小数据
ret = read(fd, buf, sizeof(buf));

// et + 循环读,大数据
while(true) {
    ret = read(fd, buf, sizeof(buf);
    // 此时,说明读缓冲区已经空了
    if (ret == EAGAIN || ret == EWOULDBLOCK) break;
}

ET特点:

  1. ET 模式避免了 LT 模式可能出现的惊群现象(如:一个 listenfd 被多个 epoll 监听,一个调用accept接受连接,其他 accept 阻塞);
  2. ET 模式减少了 EPOLL 事件被重复触发的次数,效率高。

ET 的使用

ET 的使用: ET+ 非阻塞 IO + 循环读

循环读:若数据不能一次性处理完,只能等到下次数据到达网卡后才触发读事件。

四、设置阻塞方式

将 recv 函数设置为非阻塞的两种方式:
1.recv 函数的属性设置为MSG_DONWAIT;

ret = recv(newFd, buf, sizeof(buf)-1, MSG_DONTWAIT);
  1. fcntl 函数将文件描述符设置为非阻塞性的。

代码 实例

int conn_fd = 0;

// fd --> epoll  在epoll 底层创建的时候也是通过fd 操作的。
int epfd = epoll_create(1);//参数无意义只要大于0 即可;老版本参数是用来确定一次行就绪数量

struct epoll_event ev, events[EVENTS_LENGTH];
ev.events = EPOLLIN|EPOLLET;
ev.data.fd = listen_fd

epoll_ctl(epfd,EPOLL_CTL_ADD,listen_fd,&ev)  // 先把listen_fd 注册到事件中; 非阻塞,不可能在这个地方存在挂起

printf("fd : %d\n",epfd);

while(1){  //7*24   所有的服务器都有这个循环,做成永真的循环,while(!finshed) / for(;;)
    int nready = epoll_wait(epfd,events, EVENTS_LENGTH,1000); // 是阻塞
    printf("nready :%d",nready);
    int i = 0;
    for (i = 0; i < nready;i++){
        int clientfd = events[i].data.fd ;
        if (listen_fd == clientfd){  // accept处理

            while( 1) {
                struct sockaddr_in client_addr;
                socklen_t length = sizeof(client_addr);

                fcntl(listen_fd, F_SETFL, fcntl(listen_fd, F_GETFL, 0)|O_NONBLOCK);
                conn_fd = accept(listen_fd,(struct sockaddr *)&client_addr, &length);
                if (0 > conn_fd ) {
                    printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                    return 0;
                }

                if (conn_fd == -1){
                    break;
                }

                printf("accept  : %d\n",conn_fd);
                ev.events = EPOLLIN;
                ev.data.fd = conn_fd;
                epoll_ctl(epfd,EPOLL_CTL_ADD, conn_fd, &ev);
            }
        }else if(events[i].events & EPOLLIN)  {   // clientfd

           unsigned char buff[BUFF_LENGTH] = {0};

            int n = recv(clientfd, rbuff, BUFF_LENGTH,0);
            if (n > 0) {
                //rbuffer[n] = '\0';
            
                printf("recv: %s, n: %d\n", rbuffer, n);
            
                memcpy(wbuffer, rbuffer, BUFFER_LENGTH);
            
                ev.events = EPOLLOUT;
                ev.data.fd = clientfd;
            
                epoll_ctl(epfd, EPOLL_CTL_MOD, clientfd, &ev);
                
            } 

        }else if (events[i].events & EPOLLOUT) {
        	int sent = send(clientfd, wbuffer, BUFFER_LENGTH, 0); //
			printf("sent: %d\n", sent);

			ev.events = EPOLLIN;
			ev.data.fd = clientfd;

			epoll_ctl(epfd, EPOLL_CTL_MOD, clientfd, &ev);

        }
    }
}

总结

参考

IO多路复用
epoll原理学习笔记

荐一个零声学院免费教程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:

你可能感兴趣的:(网络组件,服务器)