欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

I/O多路转接-----epoll服务器

程序员文章站 2022-06-14 14:40:26
...

在前面的两篇博客中,我们介绍了最早期的select和改进版的poll;但是,他两都没有改进的就是,想要快速知道事件就绪并没有得到改进,两个全部是遍历数组,我们都知道它的时间复杂度就是O(N);效率不是很高,时间复杂度达到O(1)才是高效的;

epoll介绍

epoll是Linux特有的I/O复用函数,它在实现和使用上与select、poll有很大差异,首先,epoll使用一组函数来完成任务,而不是单个函数。其次,epoll把用户关心的文件描述符上的事件放在内核里面的一个事件表中,从而无需向select和poll那样每次都要重复的传入文件描述符集或事件集。
其实,简单的来理解epoll服务器,它的底层就是由一颗红黑树和一个队列组成的,当调用epoll_create函数时就是创建了一个空的红黑树和空的队列,当调用epoll_ctl函数的时候,就是在红黑树上面进行节点操作,当调用epoll_wait函数时就是把红黑树中的指定节点放到队列中去,这样就可以利用队列先进先出的特点迅速的找到就绪节点,相比较二巷比数组遍历一次要节省不少时间;

epoll函数调用

1、epoll_create()

#include<sys/epoll.h>
int epoll_create(int size);
//size参数现在并不起什么作用,只是给内核一个提示,告诉它事件表需要多大,该函数返回的文件描述符将作用其他所有epoll系统调用的第一个参数,已指定要访问的内核事件表

2、epoll_ctl()

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

fd:是要操作的文件描述符;
op:指定操作类型

  • EPOLL_CTL_ADD:往事件表中注册fd上的事件(往红黑树中添加结点)

  • EPOLL_CTL_MOD:修改fd上的注册事件

  • EPOLL_CTL_DEL:删除fd上的注册事件
    event:参数指定事件,是一个结构体

struct epoll_event
{
    _uint32_t events;  //epoll事件
    epoll_data_t data; //用户数据
}

data成员用于存储用户数据,其类型:

typedef union epoll_data
{
    void* ptr;
    inf fd;
    uint32_t u32;
    uint64_t u64;
}epoll_data_t;

3、epoll_wait()

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

从后往前讨论参数:
timeout:超时设置;
maxevents:最多监听多少个事件,必须大于0;(类似于数组的最大值)
epoll_wait函数如果检测到了事件,就将所有就绪事件ongoing内核事件表中赋值到它的第二个参数events指向的数组中。这个数组直营与出输出epoll_wait检测到的就绪事件。

LT和ET模式

epoll对文件描述符的操作有两种方式LT(水平触发)和ET(边缘触发),LT是默认的工作模式,这种模式epoll相当于一个高效的poll,当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符,ET模式是epoll的高效工作模式。
对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不利己处理该事件,这样,就当程序次啊一次调用epoll_wait时,epoll_wait还会再次向应用程序通知此事件,知道该事件被处理。而对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用不在向应用程序通知这一事件。可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此次奥绿要比LT模式高

ET模式的epoll服务器

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <assert.h>
#include <pthread.h>
#define MAX_READY_EVENTS 64

//将文件描述符设置为非阻塞的
int setnonblocking(int fd)
{
    int old_option = fcntl(fd, F_GETFL);
    int new_option = old_option|O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
}
//将文件描述符fd上的EPOLLIN注册到epollfd指示的epoll内核事件表中,参数enable_et指定是否对fd启用ET模式
void addfd(int epollfd, int fd, int enable_et)
{
    struct epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN;
    if(enable_et)
    {
        event.events |= EPOLLET;
    }
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    setnonblocking(fd);
}

static void usage(const char* proc)
{
    printf("Usage %s: [local_ip] [local_port]\n", proc);
}

int startup(const char* _ip, int _port)
{
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        perror("sock");
        return 2;
    }

    int opt = 1;
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(_port);
    local.sin_addr.s_addr = inet_addr(_ip);
    if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    {
        perror("bind");
        return 3;
    }

    if(listen(sock, 10) < 0)
    {
        perror("listen");
        return 4;
    }
    return sock;
}


int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        return 1;
    }

    int listen_sock = startup(argv[1], atoi(argv[2]));

    int epfd = epoll_create(256);
    if(epfd < 0)
    {
        perror("epoll_create");
        return 5;
    }

    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = listen_sock;
    if(epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev) < 0)
    {
        perror("epoll_ctl");
        return 6;
    }
    int timeout = 1000;

    int nums = -1;
    struct epoll_event revs[MAX_READY_EVENTS];
    while(1)
    {
        switch(nums = epoll_wait(epfd, revs, MAX_READY_EVENTS, /*timeout*/-1))
        {
            case 0:
                {
                    printf("timeout...\n");
                }
                break;
            case -1:
                {
                    perror("epolly_wait");
                    return 7;
                }
                break;
            default:
                {
                    int i = 0;
                    for(; i < nums; i++)
                    {
                        int fd = revs[i].data.fd;
                        if((fd == listen_sock) && (revs[i].events == EPOLLIN))
                        {
                            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 8;
                            }

                            printf("get new client: [%s:%d]\n",inet_ntoa(client.sin_addr), ntohs(client.sin_port));

                            struct epoll_event ev1;
                            ev1.events = EPOLLIN;
                            ev1.data.fd = new_sock;
                            if(epoll_ctl(epfd, EPOLL_CTL_ADD, new_sock, &ev1) < 0)
                            {
                                perror("epoll_ctl");
                                return 9;
                            }
                            addfd(epfd, new_sock, 1);//开启ET模式
                        }// listen_sock can read
                        else if((fd != listen_sock) && (revs[i].events & EPOLLIN))
                        {
                            //ET模式下下面代码不会重复触发,所以我们循环读取数据
                            while(1)
                            {
                                char *buf[1024];
                                int s = read(fd, buf, sizeof(buf)-1);
                                if(s < 0)
                                {
                                    if((errno == EAGAIN) || (errno == EWOULDBLOCK))
                                    {
                                        break;
                                    }
                                    close(fd);
                                    break;
                                }
                                else if(s == 0)
                                {
                                    printf("client quit...\n");
                                    close(fd);
                                }
                                else
                                {
                                    buf[s] = 0;
                                    printf("client ####:%s", buf);
                                }
                            }
                        }//other events can read
                        else if((fd != listen_sock) && (revs[i].events & EPOLLOUT))
                        {
                            char* msg = "";
                            write(fd, msg, strlen(msg));
                            close(fd);
                            epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                        }//events can write
                    }//for
                }//default
                break;
        }

    }

    return 0;
}

epoll的优缺点

epoll在调用epoll_create时操作系统会创建一颗红黑树存放socket和一个队列存放就绪事件

  • 支持一个进程打开大数目的socket描述符(FD)
  • IO效率不随FD数目增加而线性下降
  • 使用mmap加速内核与用户空间的消息传递。

三种模式服务器的比较
I/O多路转接-----epoll服务器

mmap

转载一篇写的很好的博客学习学习:mmap是什么,为什么,怎么用

相关标签: 服务器 epoll