I/O多路复用之epoll模型简析

1 epoll函数

1.1 epoll_create()

#include <sys/epoll.h>
int epoll_create(int size);
  1. size:无意义,必须大于0
  2. 返回值:失败返回-1;成功返回操作epoll实例的文件描述符

当某一进程调用epoll_create函数时,Linux内核会创建一个eventpoll结构体:

struct eventpoll{
    /*红黑树的根节点,存储着所有添加到epoll中的需要监控的事件*/
    struct rb_root  rbr;
    /*双链表中存放着将要通过epoll_wait返回给用户的满足条件的事件*/
    struct list_head rdlist;
};

1.2 epoll_ctl()

int epoll_ctl(epfd, int op,int fd,struct epoll_event *event);
int ret = epoll_wait(epfd, ...);
  1. epfd:epoll实例对应的文件描述符

  2. op:要进行的操作

    EPOLL_CTL_ADD:添加
    EPOLL_CTL_MOD:修改
    EPOLL_CTL_DEL:删除

  3. fd:要监控的文件描述符

  4. event:监控文件描述符发生的事件

struct epoll_event {
	uint32_t events;      //Epoll events
	epoll_data_t data;    //User data variable 
};
/*
常见的epoll检测事件:
	EPOLLIN
	EPOLLOUT
	EPOLLERR
*/
typedef union epoll_data {
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
} epoll_data_t;

1.3 epoll_wait()

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  1. epfd:epoll实例对应的文件描述符
  2. events:传出参数,保存了发生变化的文件描述符的信息
  3. maxevents:第二个参数结构体数组的大小
  4. timeout:超时时间(毫秒)
  5. 返回值:成功返回就绪文件描述符个数,失败返回-1

2 epoll工作模式

2.1 LT模式(水平触发)

LT是默认的工作方式,并且同时支持block和no-block socket。在这种模式下,只要这个文件描述符还有数据可读,每次epoll_wait都会返回它的事件,提醒用户程序去操作。

2.1.1 程序示例

//水平触发
#include <sys/epoll.h>
#include <iostream>
#include <stdlib.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

#define MAX_CON 1024
#define SERVER_PORT 9998

using namespace std;

int main() {
    int lfd, cfd;
    int epfd;
    char recvBuf[5];
    sockaddr_in lskt, cskt;
    sockaddr_in Clientaddr[MAX_CON];
    epoll_event epev, epevs[MAX_CON];

    //创建服务端套接字
    if((lfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(-1);
    }

    //初始化服务端地址结构信息
    lskt.sin_family = AF_INET;
    lskt.sin_port = htons(SERVER_PORT);
    lskt.sin_addr.s_addr = htonl(INADDR_ANY);

    //设置端口复用
    socklen_t optval = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));

    //绑定服务端地址信息
    if(bind(lfd, (sockaddr *)&lskt, sizeof(lskt)) == -1) {
        perror("bind");
        exit(-1);
    }

    //监听socket上的连接
    if(listen(lfd, MAX_CON) == -1) {
        perror("listen");
        exit(-1);
    }

    //提示服务端初始化完毕
    char ServerIP[16];
    inet_ntop(AF_INET, &lskt.sin_addr.s_addr, ServerIP, 16);
    cout << "init success" << endl;
    cout << "host ip: " << ServerIP << ", port: " << ntohs(lskt.sin_port) << endl;

    //调用epoll_create()创建一个epoll实例
    epfd = epoll_create(100);
    //将监听的文件描述符相关的检测信息加入到epoll实例中
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    while(1) {
        int rfd = 0;
        if((rfd = epoll_wait(epfd, epevs, sizeof(epevs), -1)) == -1) {
            perror("epoll_wait");
            exit(-1);
        }
        cout << "epoll_wait call" <<endl;

        for(int i = 0; i < rfd ; i++) {
            int curfd = epevs[i].data.fd;
            if(curfd == lfd) { //说明有新连接请求
                //客户端连接
                socklen_t len = sizeof(cskt);
                if((cfd = accept(lfd, (sockaddr *)&cskt, &len)) == -1) {
                    perror("accept");
                    exit(-1);
                }

                //输出连接进来的客户端信息
                char ClientIP[16];
                inet_ntop(AF_INET, &cskt.sin_addr.s_addr, ClientIP, 16);
                cout << "client ip : " << ClientIP << " , port : " << ntohs(cskt.sin_port) << endl;

                Clientaddr[cfd] = cskt;

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            }
            else {//对于除监听描述符之外的数据,若有返回则说明有数据读取
                //通信
                if(epevs[i].events & EPOLLOUT) {  //若监听很多事件,则针对不同事件需要进行不同的处理
                    continue;
                }
                int buflen = read(curfd, recvBuf, sizeof(recvBuf));
                if(buflen == -1) {
                    perror("read");
                    exit(-1);
                }
                else if(buflen == 0) {
                    cout << "(SERVER) client closed..." << "(client port : " << ntohs(Clientaddr[curfd].sin_port) << ")"<< endl;
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                }
                else if(buflen > 0) {
                    cout << "(SERVER) recv client data : ";
                    for(int j = 0; j < buflen; j++) cout << recvBuf[j];
                    cout << "  (from port : " << ntohs(Clientaddr[curfd].sin_port) << ")"<< endl;
                    int j = 0;
                    for(; j < buflen; j++) {
                        recvBuf[j] = toupper(recvBuf[j]);
                    }
                    recvBuf[j] = '\0';
                    write(curfd, recvBuf, strlen(recvBuf));
                }
            }
        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

2.1.2 运行结果

在这里插入图片描述

2.2 ET模式(边沿触发)

ET(edge - triggered)是高速工作模式,只支持 no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll通知用户程序。然后它会假设用户程序已知文件描述符已就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个 fd 作 I/O 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。
ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作使处理多个文件描述符的任务停滞

ET模式(边沿触发):只有数据到来才触发,不管缓存区中是否还有数据,缓冲区剩余未读尽的数据不会导致epoll_wait返回;
LT 模式(水平触发,默认):只要有数据都会触发,缓冲区剩余未读尽的数据会导致epoll_wait返回。

2.2.1 程序示例

//边沿触发
#include <sys/epoll.h>
#include <iostream>
#include <stdlib.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#define MAX_CON 1024
#define SERVER_PORT 9998

using namespace std;

int main() {
    int lfd, cfd;
    sockaddr_in lskt, cskt;
    sockaddr_in Clientaddr[MAX_CON];
    char recvBuf[5];
    epoll_event epev;
    epoll_event epevs[MAX_CON];

    //创建服务端套接字
    if((lfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(-1);
    }

    //初始化服务端地址结构信息
    lskt.sin_family = AF_INET;
    lskt.sin_port = htons(SERVER_PORT);
    lskt.sin_addr.s_addr = htonl(INADDR_ANY);

    //设置端口复用
    socklen_t optval = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));

    //绑定服务端地址信息
    if(bind(lfd, (sockaddr *)&lskt, sizeof(lskt)) == -1) {
        perror("bind");
        exit(-1);
    }

    //监听socket上的连接
    if(listen(lfd, MAX_CON) == -1) {
        perror("listen");
        exit(-1);
    }

    //提示服务端初始化完毕
    char ServerIP[16];
    inet_ntop(AF_INET, &lskt.sin_addr.s_addr, ServerIP, 16);
    cout << "init success" << endl;
    cout << "host ip: " << ServerIP << ", port: " << ntohs(lskt.sin_port) << endl;

    //调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    //将监听的文件描述符相关的检测信息加入到epoll实例中
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    while(1) {
        int ret = 0;
        if((ret = epoll_wait(epfd, epevs, sizeof(epevs), -1)) == -1) {
            perror("epoll_wait");
            exit(-1);
        }
        cout << "epoll_wait call" <<endl;

        for(int i = 0; i < ret ; i++) {

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
                //客户端连接
                socklen_t len = sizeof(cskt);
                if((cfd = accept(lfd, (sockaddr *)&cskt, &len)) == -1) {
                    perror("accept");
                    exit(-1);
                }

                //设置cfd属性非阻塞
                int flag = fcntl(cfd, F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(cfd, F_SETFL);

                Clientaddr[cfd] = cskt;

                //输出连接进来的客户端信息
                char ClientIP[16];
                inet_ntop(AF_INET, &cskt.sin_addr.s_addr, ClientIP, 16);
                cout << "client ip : " << ClientIP << " , port : " << ntohs(cskt.sin_port) << endl;

                epev.events = EPOLLIN | EPOLLET; //设置边沿触发
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            }
            else {
                //通信
                if(epevs[i].events & EPOLLOUT) {  //若监听很多事件,则针对不同事件需要进行不同的处理
                    continue;
                }

                //循环读出缓冲区所有数据
                // memset(recvBuf, 0, sizeof(recvBuf));
                int buflen = 0;
                while((buflen = read(curfd, recvBuf, sizeof(recvBuf))) > 0) {
                    cout << "(SERVER) recv client data : ";
                    for(int j = 0; j < buflen; j++) cout << recvBuf[j];
                    cout << "  (from port : " << ntohs(Clientaddr[curfd].sin_port) << ")"<< endl;
                    int j = 0;
                    for(; j < buflen; j++) {
                        recvBuf[j] = toupper(recvBuf[j]);
                    }
                    recvBuf[j] = '\0';
                    write(curfd, recvBuf, strlen(recvBuf));
                }
                if(buflen == 0) {
                    cout << "(SERVER) client closed..." << "(client port : " << ntohs(Clientaddr[curfd].sin_port) << ")"<< endl;
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                }
                if(buflen == -1) {
                    if(errno == EAGAIN) {
                        cout << "client data over..." << endl;                        
                    }
                    else {
                        perror("read");
                        exit(-1);   
                    }                   
                }
            }
        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

2.2.2 运行结果

在这里插入图片描述


版权声明:本文为weixin_42323999原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。