系列文章目录

第一章 高性能服务器技术栈 (select)
第二章 高性能服务器技术栈 (epool/poll)



前言

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


一、epoll 接口

epoll 的实现共有三个接口。它们分别如下所示:

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

int epoll_create(int size)

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

参数:
- 参数1 epfd: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对象进行操作:

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

// 收集 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(等待时间)。断开与服务器没有交互的客户端

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 原理

高性能服务器-I/O多路复用(epoll)-LMLPHP
注:调用 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); 在处理非大量数据时候性能相差不大,但是代码逻辑处理有区别。

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

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

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

// 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等技术内容,点击立即学习:

06-14 06:55