I/O多路复用 epoll

epoll初识


epoll是为了处理大量的句柄而作了改进的poll;

它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法

epoll相关系统调用


epoll_create -> 创建epoll模型(即内核中的软件资源)

// 创建一个epoll句柄

int epoll_create(int size);

  • 自Linux2.6.8以后,size的参数是被忽略的,不过为了可移植性,一般还是会填128,512等值

  • 返回值是一个文件描述符,用完之后需要close掉

epoll_ctl -> epoll的事件注册函数

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

  • 它不同于select()是在监听事件时告诉内核要监听什么样的事件,而是利用这个函数先注册要监听的事件

参数:

  • epfd: 是epll_create()返回的epoll句柄

  • op: 表示动作,用一下三个宏来表示

    • EPOLL_CTL_ADD: 注册新的fd到epfd中

    • EPOLL_CTL_MOD: 修改已经注册的fd的监听事件

    • EPOLL_CTL_DEL: 从epfd中删除一个fd

  • fd: 需要监听的文件描述符

  • event: 告诉内核需要监听什么事件,结构体内容如下:

  • events可以是以下的宏的集合(这里列出部分值)

宏值

描述

EPOLLIN

0x001

表示对应文件描述符可以读(包括对端SOCKET正常关闭)

EPOLLOUT

0x004

表示对应的文件描述符可以写

EPOLLPRI

0x002

表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)

EPOLLERR

0x008

表示对应文件描述符发生错误

EPOLLHUP

0x010

表示对应的文件描述符被挂断

  • epoll_data结构体保存的是用户自定义的数据,一般我们使用的是里面的 fd 字段,保存文件描述符

epoll_wait -> 收集在epoll监控事件中已经发送的事件

int epoll_waot(int epfd, struct epoll_event* events, int maxevent, int timeout);

参数:

  • epfd: epoll_create返回的句柄

  • events: 就是上面介绍的结构体,本质上是一个结构体数组,epoll会将发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态分配内存)

  • maxevent: 是上面结构体数据的长度,maxevents的值不可以大于epoll_create()的size

  • timeout: 超时时间,和select(),poll()的意义相同

返回值:

  • 成功: 返回对应I/O上已经准备好的文件描述符个数
  • 失败: 返回值小于0
  • 超时: 返回0

epoll工作原理


构造epoll模型的3件事:

  1. 在操作系统底层构造回调机制 -> OS不需要以轮询的方式查看哪个时间已经就绪,当数据到来的时候,通过回调机制告诉操作系统,数据会立即移至就绪队列

  2. 在操作系统层构造一颗红黑树 -> 用户所关心的哪个文件描述符上的哪个时间

  3. 再构建一个就绪队列 -> 关心哪个文件描述符上的哪个时间就绪

流程如下图:

当有数据到来时:

  1. 驱动将数据上传至内核,激活红黑树中的某一个节点

  2. 根据被激活的节点的内容在就绪队列中新增一个节点(可理解为OS将红黑树当前节点拷贝至就绪队列)

  3. 将发生的事件赋值到用户态,同时将事件数量返回给用户

epoll优点

  • 接口使用方便: 不需要每次都设置关注的文件描述符,也做到了输入输出分离

  • 文件描述符无上限: 通过epoll_ctl()来注册一个文件描述符,内核中使用红黑树的数据结构来管理所要监控的文件描述符

  • 事件回调机制: 一旦被监听的某个文件描述符就绪,内核会采用类似于callback的回调机制,迅速激活这个文件描述符,这样随着文件描述符的增加也不会影响判定就绪的性能

  • 维护就绪队列: 当文件描述符就绪,就会被放到内核中一个就绪队列中,这样调用epoll_wait获取文件描述符的时候,只要取队列中的元素即可,操作的时间复杂度为O(1)

注意!!!!

很多的博客或者文章中写到 epoll具有内存映射机制,这个是根本不存在的!!!!

如果有内存映射的问题,那么消息队列中只要有文件描述符就绪,那么用户态就应该直接可以看到,那么当调用epoll_wait时的最后一个参数完全就是多此一举了

在我们平常的使用当中也并没有发现内存映射机制的存在

大家也可以看看源码,根本不存在的哦~

epoll工作方式


epoll有两种工作方式 - 水平触发(LT)和边缘触发(ET)

举一个简单的例子:

比如你买了5个快递,先到了3个,快递员张三给你派送快递,场景如下:

张三: 歪,是xxx嘛,你的快递到了,麻烦去xxx取一下

你: 好几

(然而你手头上有重要的事情,现在不能去取快递,所以就没有下去)

(一个小时过去了...)

张三: 歪,是xxx嘛,你的快递到了,麻烦取一下

你: 好几

(这个时候你忙完了,于是下去取了快递,但是快递太大了,你一次只能拿一个于是你先拿了一个回去)

(回去以后又因为有重要的事情,剩下的两个快递又被搁置了,暂时没时间拿)

(一个小时又过去了...)

张三: 歪,是xxx嘛,你的快递来了,麻烦取一下

你: 好几

.....

每次你没有拿走快递或者没有那完快递张三都会好脾气的通知你快递到了,希望你取一下,按照张三这个性格,不管过多久,你总可以取完你的快递

派送员李四就不像张三那么好脾气了,假如你的五个快递先到了3个

李四: 歪,是xxx吗,你的快递到了,麻烦下来取一下

(然而你实在是太忙了,没有时间下去取)

(然而李四也不催你,也不通知你)

不知道过了多久...你的剩下两个快递也到了,这个时候李四才打来了第二个电话

李四: 歪,是xxx吗....

.....

也就是说,如果在后面两个快递到来之前,李四都不会再次通知你,在这段时间里,你可能忘记了或者李四已经走了,就会造成你可能永远拿不到你的快递了

上面张三就是一个水平触发的例子,李四就是一个典型的边缘触发的例子

水平触发Level Triggered工作模式

  • 当epoll检测到socket上事件就绪的时候,可以不立即处理,或者可以只处理一部分

  • 如果你只处理了一般或者没有处理,第二次调用epoll_wait的时候仍然会立即返回并通知socket就绪

  • 直到数据吹完成,epoll_wait才会立刻返回

  • 支持阻塞的读写和非阻塞的读写

边缘处罚 Edge Triggered工作模式

  • 当epoll检测到socket上事件就绪时,不许立刻处理

  • 如果没有及时处理/只处理了一些,那么wpoll_wait就不会再返回了

  • 也就是说,在ET模式下,文件描述符就绪以后只有一次处理的机会

  • ET的性能比LT的性能高,因为epoll_wait返回的次数比较少一些

  • 只支持非阻塞式的读写

理解ET模式下非阻塞读写的原因:

因为只读一次,事件一旦就绪以后,我们就会循环的读,而循环的读就有可能造成阻塞

倘若当前fd为阻塞(默认),那么当读完缓冲区数据的时候,如果对端没有关闭写端,那么read函数就会一直阻塞,影响其他fd以及后续的逻辑

所以讲fd设置为非阻塞,当没有数据到来的时候,read虽然读不到任何的内容,但是肯定不会被hang住,那么此刻,说明缓冲区数据已经读取完毕,需要继续处理后续逻辑(读取其他的fd或者进入wait状态)

epoll使用场景


 epoll的高性能是由一定的应用场景的,如果场景选择的不适宜,可能会适得其反

  • 使用场景: 适用于多连接,且多连接中只有一部分连接比较活跃的场景

例如: 一个需要处理上万客户端的服务器

     各种互联网APP的入口服务器

  • 不适场景: 如果只是系统内部,服务器和服务器之间进行通信,只有少数的几个连接,这种情况就不合适

epoll代码示例 - LT


//epoll_server.cc

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int startup(short port)
{
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
        perror("socket");
        exit(3);
    }
    int opt = 1;
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_ANY);
    local.sin_port = htons(port);
    if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
    {
        perror("bind");
        exit(4);
    }
    if(listen(sock,5) < 0)
    {
        perror("listen");
        exit(5);
    }
    return sock;
}

void handler_event(int epfd,struct epoll_event revs[],int num,int listen_sock)
{
    struct epoll_event ev;
    int i = 0;
    for(; i < num; ++i)
    {
        int fd = revs[i].data.fd;
        if(fd == listen_sock && (revs[i].events & EPOLLIN))
        {// listen_sock OK
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int new_sock = accept(fd,(struct sockaddr*)&client,&len);
            if(new_sock < 0)
            {
                perror("accept");
                continue;
            }
            printf("get new client...\n");
            // 链接建立好以后,不可以直接read,可能会造成阻塞
            ev.events = EPOLLIN;
            ev.data.fd = new_sock;
            epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);
            continue;
        }
        // 不是listen_sock, 有可能是读时间就绪,也可能是写时间就绪,也可能是同时就绪
        // 不过我们这里只先关心读事件,再关心写时间
        // 因为HTTP是基于请求应答的,必须先关心读事件
        if(revs[i].events & EPOLLIN)
        {
            char buf[1024*10] = {0};
            ssize_t s = read(fd,buf,sizeof(buf)-1);
            if(s > 0)
            {
                buf[s] = '\0';
                printf("%s\n",buf);
                // 将时间读完成以后,应该讲时事件改为关心写时间
                // 先关心读再关心写是为了控制读写顺序
                ev.events = EPOLLOUT;
                ev.data.fd = fd;
                epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);
            }
            else if(s == 0)
            {
                printf("client quit...\n");
                close(fd);
                epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
            }
            else
            {
                perror("read");
                close(fd);
                epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
            }
            continue;
        }
        if(revs[i].events & EPOLLOUT)
        {
            // 写时间
            const char* echo = "HTTP/1.1 200 OK\r\n\r\n<html>hello epoll server</html>\r\n";
            write(fd,echo,strlen(echo));
            close(fd);
            epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
        }
    }
}

int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s [port]\n",argv[0]);
        return 1;
    }
    int epfd = epoll_create(256);
    if(epfd < 0)
    {
        perror("epoll_create");
        return 2;
    }
    int listen_sock = startup(atoi(argv[1]));
    // 1.将listen_sock加入到epoll中
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = listen_sock;
    epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);

    // 事件循环
    struct epoll_event revs[128];
    int n = sizeof(revs)/sizeof(revs[0]);
    int timeout = 12345;
    int num = 0;
    while(1)
    {
        switch((num = epoll_wait(epfd,revs,n,timeout)))
        {
        case -1:
            perror("epoll_wait");
            break;
        case 0:
            printf("timeout...\n");
            break;
        default:
            handler_event(epfd,revs,num,listen_sock);
            break;
        }
    }

    close(epfd);
    close(listen_sock);
    return 0;
}

epoll代码示例 - ET


 //epoll_server.cc

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>

int startup(short port)
{
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
        perror("socket");
        exit(3);
    }
    int opt = 1;
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_ANY);
    local.sin_port = htons(port);
    if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
    {
        perror("bind");
        exit(4);
    }
    if(listen(sock,5) < 0)
    {
        perror("listen");
        exit(5);
    }
    return sock;
}

void SetNoBlock(int fd)
{
    int f1 = fcntl(fd,F_GETFL);
    if(f1 < 0)
    {
        perror("fcntl F_GETFL");
        return;
    }
    fcntl(fd,F_SETFL,f1 | O_NONBLOCK);
}

void ProcessConnect(int listen_sock, int epfd)
{
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
    if(new_sock < 0)
    {
        perror("accept");
        return;
    }
    printf("[client %d] connected\n",new_sock);

    SetNoBlock(new_sock);
    struct epoll_event event;
    event.data.fd = new_sock;
    event.events = EPOLLIN | EPOLLET;
    int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&event);
    if(ret < 0)
    {
        perror("epoll_ctl ADD");
        return;
    }
    return;
}

ssize_t NoBlockRead(int fd,char* buf,int maxsize)
{
    ssize_t total_size = 0;
    while(1)
    {
        ssize_t cur_size = read(fd,buf+total_size,maxsize);
        total_size += cur_size;
        if(cur_size < maxsize || errno == EAGAIN)
        {// 如果 cur_size<1024, 证明已经读完了,否则的话一定会将换城区装满的
         // 如果errno为EAGAIN,证明是非阻塞的输入输出,但是当前并没有数据
            break;
        }
    }
    buf[total_size] = '\0';
    return total_size;
}

void ProcessRequest(int new_sock,int epfd)
{
    char buf[1024] = {0};
    ssize_t read_size = NoBlockRead(new_sock,buf,sizeof(buf)-1);
    if(read_size < 0)
    {
        perror("read");
        return;
    }
    else if(read_size == 0)
    {
        // 表示对端已经退出
        int ret = epoll_ctl(epfd,EPOLL_CTL_DEL,new_sock,NULL);
        if(ret < 0)
        {
            perror("epoll_ctl DEL");
            return;
        }
        close(new_sock);
        printf("client disconnected...\n");
    }
    printf("[client %d]> %s\n",new_sock,buf);
    write(new_sock,buf,strlen(buf));
    return;
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s [port]\n",argv[0]);
    }
    // 1.初始化服务器
    int listen_sock = startup(atoi(argv[1]));
    if(listen_sock < 0)
    {
        printf("Server startup faild\n");
        return 1;
    }
    int epfd = epoll_create(10);
    if(epfd < 0)
    {
        perror("epoll_create");
        return 1;
    }
    // 2.将listen_sock设置为非阻塞,ET模式下要求非阻塞
    SetNoBlock(listen_sock);
    // 3.构建epoll结构体,将listen_sock加入到epfd当中,注意,listen_sock只关心写事件
    struct epoll_event event;
    event.events = EPOLLIN | EPOLLET; //加上EPOLLET宏才是边缘触发
    event.data.fd = listen_sock;
    int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&event);
    if(ret < 0)
    {
        perror("epoll_ctr ADD");
        return 1;
    }
    // 4.进行事件循环
    while(1)
    {
        // a) 表示就绪队列
        struct epoll_event events[10];
        int size = epoll_wait(epfd,events,sizeof(events)/sizeof(events[0]),-1);
        if(size < 0)
        {
            perror("epoll_wait");
            return 1;
        }
        if(size == 0)
        {
            printf("timeout...");
            continue;
        }
        // b) 对已就绪的文件描述符进行相应的操作
        int i = 0;
        for(i = 0; i < size; ++i)
        {
            if(!(events[i].events & EPOLLIN))
            {// 当前只关心读事件,如果不是读事件,直接退出
                continue;
            }
            if(events[i].data.fd == listen_sock)
            {// listen_sock 进行accept
                ProcessConnect(listen_sock,epfd);
            }
            else
            {// new_sock 进行读写
                ProcessRequest(events[i].data.fd,epfd);
            }
        }
    }
    return 0;
}

客户端逻辑和之前的一模一样,可以参考select中最下方的客户端代码

I/O多路复用 select

猜你喜欢

转载自blog.csdn.net/j4ya_/article/details/81166956