epoll 底层如何做到 O(1)(通俗理解)

新页面

  1. epoll

    - 通俗的理解:

         有个小区,一开始小区里的人寄快递都是叫快递员上门取件,或者是去快递店里寄,前者有点费快递员的人力,后者可以有浪费顾客的时间,后来快递公司想到了一个方法, 在小区里建立了一个无人快递收件柜子(蜂巢),顾客把想寄的东西都放在那个柜子了,快递人员到一定的时间带个大袋子,把要寄送的东西带走,这样就省去快递员挨家挨户的收快递,提高了效率

         小区里的人就是 那些文件io,柜子里的装的东西就是 有事件响应的io,这样就不用挨个去遍历了

    - 底层如何实现的呢?

        面试有问过,我一般都是说红黑树加双向链表实现的,然后就说不下去了,现在做一个epoll的大总结

        epoll 使用就不在多说,现在说个话题 为什么 epoll 时间复杂度可以做到O(1) 呢?

             我们一般都会说,底层把有事件发生的io放在一个队列里,这个对个队列是一个双向链表,插入的时间复杂为O(1),

            没错,在用户态 我们遍历队列的时候,可以保证遍历的每个io都是有事件发生,效率肯定是上去了,那么在内核态,底层是如何把有事件发生的io放到队列呢? 怎么放我们已经知道了,现在问题是在那么多io里,怎么马上挑出有事件的io呢?难道也是在内核态全部遍历一次?我一开始也百思不得其解,查阅很多资料知道,原来在更底层有那么一个机制,刚一个io有时间发生会产生一个软终端,然后我想到了 信号,信号的类型有个是 SIGIO(这里的软中断我也不是很确定是不是io中断)

  sigaction(SIGIO, &sigio_action, NULL);

        当io有什么事件发生时候,会马上调用这个对应的回调函数,那这样,我并不需要每个io的去问有没有事件发生,我就每个io都设置一个回调函数,那么io有事件了,马上调用回调函数,那么就准备把这个io装在队列里面里,而红黑树的作用就是管理这些io并设置其回调函数,就好像这个api

**epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);** 

             把新的io加入到红黑树里面,然后注册一个对应的回调函数,我们都知道红黑树的特性是 增删改查时间复杂度稳定 为

            logn,有io退出,取消对应的回调函数,然后删除这个io

            以上就是当前对epoll底层实现 的初步了解,后续看了源码在来更新

            具体流程如下如下所示 (在知乎看到的)

            

在这里插入图片描述

最后放一个epoll 使用,这里用到了reactor模式(大家可以搜下这个),加了比较通俗一点的注释,方便理解


#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <pthread.h>

#include <errno.h>
#include <sys/epoll.h>
#define Buffersize 1024

enum WS_STATUS
{
    WS_INIt,
    WS_HANDSHAKE,
    WS_DATAFORM,
    WS_DATAEND,
};
int handshake(struct sockitem *si, struct reactor *mainloop)
{
}
struct sockitem
{ //
    int sockfd;
    int (*callback)(int fd, int events, void *arg);
    char recvbuffer[Buffersize]; //
    char sendbuffer[Buffersize];
    int rlength;
    int stauts;
};

// mainloop / eventloop --> epoll -->
struct reactor
{
    int epfd;
    struct epoll_event events[512];
};

struct reactor *eventloop = NULL;

int recv_cb(int fd, int events, void *arg);

int send_cb(int fd, int events, void *arg)
{

    struct sockitem *si = (struct sockitem *)arg;
    memcpy(si->sendbuffer, si->recvbuffer, si->rlength);
    send(fd, si->sendbuffer, si->rlength + 1, 0); //
    send(fd, "\n", 1, 0);
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;
    //ev.data.fd = clientfd;
    si->sockfd = fd;
    si->callback = recv_cb;
    ev.data.ptr = si;

    epoll_ctl(eventloop->epfd, EPOLL_CTL_MOD, fd, &ev);
}

//  ./epoll 8080

int recv_cb(int fd, int events, void *arg)
{

    //这里就是普通 用户的处理方案了
    //int clientfd = events[i].data.fd;
    struct sockitem *si = (struct sockitem *)arg;
    struct epoll_event ev;

    char buffer[1024] = {0};
    int ret = recv(fd, buffer, 1024, 0);
    if (ret < 0)
    {

        if (errno == EAGAIN || errno == EWOULDBLOCK)
        { //
            return -1;
        }
        else
        {
        }

        ev.events = EPOLLIN;
        //ev.data.fd = fd;
        epoll_ctl(eventloop->epfd, EPOLL_CTL_DEL, fd, &ev);

        close(fd);

        free(si);
    }
    else if (ret == 0)
    { //

        //
        printf("disconnect %d\n", fd);

        ev.events = EPOLLIN;
        //ev.data.fd = fd;
        epoll_ctl(eventloop->epfd, EPOLL_CTL_DEL, fd, &ev);

        close(fd);

        free(si);
    }
    else
    {

        printf(" Yeah! Recv: %s, %d Bytes\n", buffer, ret);
        memcpy(si->recvbuffer, buffer, ret);
        //不出意外地话,最后把事情都做完了
        //更新自己在 epoll  大家族的信息
        struct epoll_event ev;
        ev.events = EPOLLOUT | EPOLLET;
        //ev.data.fd = clientfd;
        si->rlength = ret;
        si->sockfd = fd;
        si->callback = send_cb;
        ev.data.ptr = si;
        //和 大管家报告,说自己要修改个人信息
        epoll_ctl(eventloop->epfd, EPOLL_CTL_MOD, fd, &ev);
    }
}

int accept_cb(int fd, int events, void *arg)
{
    //这里是 第一个进 epoll大家庭的用户, 他比较职责比较特殊,算是大管家的小助手,职责是接待新用户
    struct sockaddr_in client_addr;
    memset(&client_addr, 0, sizeof(struct sockaddr_in));
    socklen_t client_len = sizeof(client_addr);
    //以上是给新用户腾出空间来,登记好信息;并且分给他一个 id好,这个是在这个地方识别新用户的唯一标志
    int clientfd = accept(fd, (struct sockaddr *)&client_addr, &client_len);
    if (clientfd <= 0)
        return -1;
    //

    char str[INET_ADDRSTRLEN] = {0};
    printf("recv from %s at port %d\n", inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),
           ntohs(client_addr.sin_port));

    //然后准备加入 epoll 大家庭, 流程和的第一个加入的一样
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;
    //ev.data.fd = clientfd;

    struct sockitem *si = (struct sockitem *)malloc(sizeof(struct sockitem));
    si->sockfd = clientfd;
    //这里不同的是, 事件发生时,其对应的解决方案不同(就是其回调函数不同)
    si->callback = recv_cb;
    si->stauts = WS_HANDSHAKE;
    ev.data.ptr = si;
    //信息准备好好,最后跟大管家说,有新成员要加入了
    epoll_ctl(eventloop->epfd, EPOLL_CTL_ADD, clientfd, &ev);

    return clientfd;
}

int main(int argc, char *argv[])
{

    if (argc < 2)
    {
        return -1;
    }

    int port = atoi(argv[1]);

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        return -1;
    }

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(struct sockaddr_in));

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = INADDR_ANY;
    //给socket 绑定一个身份
    if (bind(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) < 0)
    {
        return -2;
    }
    //最大连接数
    if (listen(sockfd, 5) < 0)
    {
        return -3;
    }
    // struct reactor
    // {
    //     int epfd;
    //     struct epoll_event events[512];
    // };
    //  reactor 分配空间,  eventloop 是最大的管家
    eventloop = (struct reactor *)malloc(sizeof(struct reactor));
    // epoll opera
    // 给epoll->epfd 分配一个权限,可以管理 io
    eventloop->epfd = epoll_create(1);

    //每个io(文件描述符都应该都身份 ,才能进入 epoll 里,这样 管家  eventloop->epfd 才好方便管理)
    //结构体 epoll_event 包含了io的身份
    struct epoll_event ev;
    // 事件类型
    ev.events = EPOLLIN;
    //ev.data.fd = sockfd; //int idx = 2000;
    //这里为了方便,在引用一个结构体, 详细记录每个io 信息, ev 结构体里有个空指针, 这也是在引用一个结构体的原因
    struct sockitem *si = (struct sockitem *)malloc(sizeof(struct sockitem));
    //结构体 sockitem 记录该io 状态,是什么类型的io 以及发生了什么事件调用哪个函数
    si->sockfd = sockfd;
    si->stauts = WS_INIt;
    si->callback = accept_cb;
    // ev里面的空指针指向 si空间,这一个ev 就可以包含很多信息了,用的时候只需要 强转一下就行了
    ev.data.ptr = si;
    // 将新的 io 带给 管家  eventloop->epfd ,告诉管家怎么出来 这里是 EPOLL_CTL_ADD  意为加入epoll 大家庭
    // 告诉自己是id,以及 其身份信息 ev
    epoll_ctl(eventloop->epfd, EPOLL_CTL_ADD, sockfd, &ev);
    //最先加入的是 监听是否有 新用户接入socket,其职责是 有用户来了,告诉大管家,然后把用户拉进 epoll 大家庭
    //pthread_cond_waittime();

    while (1)
    {
        //这里是大管家 的职责,在 epoll 大家庭了谁有事了,就把他拉倒一个小房间里,统计有多少个人有事,然后再来统一处理 nready 为有事io的数量
        int nready = epoll_wait(eventloop->epfd, eventloop->events, 512, -1);
        if (nready < -1)
        {
            break;
        }

        int i = 0;
        //拉倒一个房间后再逐个处理
        for (i = 0; i < nready; i++)
        {
            //事件驱动
            //根据每个io的事件的类型来处理,提高效率   (这里的事件类型 为可读可写)
            if (eventloop->events[i].events & EPOLLIN)
            {
                //printf("sockitem\n");
                //每个用户进来之前都登记好了个人信息,以及我出事了,该怎么做(对应的处理函数),所以管家也不必着急
                //先抽出 用户的信息,在来看 用户他自己写好的处理方案(这里是函数回调)
                struct sockitem *si = (struct sockitem *)eventloop->events[i].data.ptr;
                si->callback(si->sockfd, eventloop->events[i].events, si);
            }
            //同上,这里只是不同的事件类型
            if (eventloop->events[i].events & EPOLLOUT)
            {

                struct sockitem *si = (struct sockitem *)eventloop->events[i].data.ptr;
                si->callback(si->sockfd, eventloop->events[i].events, si);
            }
        }
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值