【Linux】高级IO——五种IO方式,select,poll,epoll


代码仓库:
https://gitee.com/deng-zitao-bit/linux

一、简单了解什么是IO及五种IO模式

什么是IO?
IO = 等 + 拷贝。

五种IO

以钓鱼为例:

  • 1.阻塞式IO ——我就死死盯着那根鱼竿,鱼竿动了我才拉杆
  • 2.非阻塞式IO,非阻塞轮询——我把鱼竿放那,我就去刷抖音,刷一会抖音,我又去看看鱼竿动没动
  • 3.信号驱动IO——我自己不钓鱼,我在鱼竿上挂个铃铛,铃铛一响,我马上拉杆。
  • 4.多路复用,多路转接。——我是一个首富,我去钓鱼拉一车鱼竿,100条杆,那条鱼竿动了我就拉杆。
  • 5.异步IO——我是公司CEO,我不钓鱼,但我会告诉老墨,我想吃鱼了,所以老墨拿着桶,鱼竿,电话,去钓鱼。鱼钓上后,打电话通知我。 (我相当于客户端,我向操作系统发起请求,操作系统就进行监听,只要有数据就绪就通知我。)

同步IO和异步IO区别

在这里插入图片描述

总结:任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间减少.

1.阻塞IO(张三钓鱼方式)

以下是张三阻塞式钓鱼的方式:
一直阻塞盯着鱼竿不离开。

//实现阻塞式等待读
int main()
{
    char buffer[1024];
    while(1)
    {
        printf("please Enter# ");
        // //刷新一下缓冲区,让提示信息先显示出来
        fflush(stdout);
        ssize_t n = read(0,buffer,sizeof(buffer)-1);  // -1是因为,读取1024不太合理,需要给最后一个位置留'\0',最多只读1023
        if(n < 0) //出错了
        {
            cerr << "read error , n = " << n << " errno = " << errno << "err string:" << strerror(errno) << endl;
            sleep(1);
        }
        else if (n == 0) //直接按回车不行,回车也会被读取
        {
            cout << "read done" << endl;
        }
        else
        {
            buffer[n-1] = '\0'; //设置终止符
            cout << "echo : " << buffer << endl;
        }   
    }

    return 0;
}

2.非阻塞IO+非阻塞轮询(李四钓鱼方式)

使用fcntl函数实现SetNonBlock非阻塞

在这里插入图片描述

我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.

以下是李四的非阻塞钓鱼方式。
李四把鱼竿放那里,然后刷抖音,时不时看一眼鱼竿。

解析:

使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.

更具体解析在代码的注释中:

//设置非阻塞的方式有很多,比如在open的时候,就设置非阻塞,
//但是下面的方法是最通用的
void SetNonBlock(int fd)
{
    //先获取fd文件描述符的状态标记,这是一个位图
    int fl = fcntl(fd,F_GETFL);
    if(fl < 0) //获取位图失败
    {
        perror("fcntl");
        return ;
    }
    //再将位图设置回去,同时加上一个非阻塞标记位
    fcntl(fd,F_SETFL,fl | O_NONBLOCK);
}

//实现阻塞式等待读
int main()
{
    char buffer[1024];
    SetNonBlock(0);//设置非阻塞了

    while(1)
    {
        // printf("please Enter# ");
        // //刷新一下缓冲区,让提示信息先显示出来
        // fflush(stdout);
        ssize_t n = read(0,buffer,sizeof(buffer)-1);  // sizeof(buffer)-1是因为,读取1024不太合理,需要给最后一个位置留'\0',最多只读1023
        if(n < 0) //出错了
        {
            //在非阻塞的前提下,不一定是真的出错了,两种情况:
            //1.底层数据没有就绪, recv/read/write/send等函数会以出错的形式返回。(因为我们在键盘输入数据实在是太慢了,所以非阻塞检测的大部分时间都在询问)
            //2.真的出错了
            //数据未就绪
            if(errno == EWOULDBLOCK)  // 数据未就绪
            {
                cout << "read data didn't ready" << endl;
                //do_other_things();
            }
            else
            {
                cerr << "read error , n = " << n << " errno = " << errno << "err string:" << strerror(errno) << endl;
            }
            sleep(1);
        }
        else if (n == 0) //直接按回车不行,回车也会被读取
        {
            cout << "read done" << endl;
        }
        else
        {
            buffer[n-1] = '\0'; //设置终止符
            cout << "echo : " << buffer << endl;
        }   
    }

    return 0;
}

二、IO多路转接——select(赵六钓鱼方式)

赵六有多条鱼竿,所以可以一次等待多条鱼竿,就像是select函数。

关于select函数的详细解释如下图:

int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);

在这里插入图片描述
在关心fd是否就绪时:
用户可以同时关心fd的读,写事件,只需要在readfds,writefds两个参数的位图中设置为1即可。

用户也可以先关心读事件,再关心写事件。
用户只需在读时间获取完毕后,再将writefds的位图设置为1即可。

其他的以此类推。

下面的函数,就是专门用来对select的几个位图参数准备的。
这些函数就能修改位图的值了。
在这里插入图片描述

注意:

  • 1.在timeout设置的等待时间期间,一旦有新链接到来,select会立刻返回!
  • 2.如果上层不处理,select会一直通知你!

情况3:在处理select时,假如有一个事件就绪,上层拿来处理,并建立了新链接,但是,不能直接使用read函数等待该客户端给我发数据,有可能客户端只请求和我建立链接,但它一直不发数据,我一旦使用了read函数,就会一直阻塞在那里等待数据。
要知道,我服务器可是一个单进程!
这要是阻塞了,这还玩什么。

所以,谁规定建立了新链接我必须要读取数据呢?
我应该把这个客户端的套接字再次交给select,让它继续监听。

那么如何把客户端的sock交给select的位图呢?

设置一个,辅助数组,具体请看下面代码的详细注释:

void HandlerEvent(fd_set& rfds)
{
    //获取读事件
    if(FD_ISSET(_listensock.Fd(),&rfds)) // 确定读时间已经就绪
    {
        std::string clientip;
        uint16_t clientport;
        int sock = _listensock.Accept(&clientip,&clientport);
        if(sock < 0)
        {
            std::cerr << "get accept fail" << std::endl;
            return;
        }
        lg(Info,"accept success, %s : %d , sock fd : %d",clientip.c_str(),clientport,sock);
        
        //这里不能直接使用read读取,因为就算现在建立连接成功了,但如果客户端一直不发数据呢?
        //服务器现在是单进程的,如果阻塞在这里,那还玩啥
        //所以就需要把这个就绪的fd,想办法交给fd_set* readfds位图,让它等待多一些!
        //方法:建立辅助数组,将这个sock添加到fd_array[]中
        int pos = 1;
        for(;pos < fd_num_max;pos++)
        {
            if(fd_array[pos] != defaultfd) continue; //该位置被其他fd占用了
            else break;
        }
        if(pos == fd_num_max) //说明所有能用的位置都被用了, 服务器满载
        {
            lg(Warning,"Server is Full, close fd: ",sock);
            return;
        }
        else
        {
            fd_array[pos] = sock;
        }

    }
}

void Start()
{
    int listensock = _listensock.Fd();

    while (1)
    {
        // 不能直接Accept
        fd_set readfds;
        FD_ZERO(&readfds);
        fd_array[0] = listensock;
        int maxfd = fd_array[0];

        //每次都重新设置位图
        for(int i =1; i< fd_num_max;i++)
        {
            if(fd_array[i] == defaultfd) continue; 
            else 
            {
                if(maxfd < fd_array[i])
                {
                    maxfd = fd_array[i];
                    lg(Info,"maxfd update ,maxfd is %d",maxfd);
                }
                FD_SET(fd_array[i], &readfds);    // 我要关心listensock,就将其设置进读事件位图交给OS来关心。
            }
        }


        //放在循环内,肯定每次都要重新设置,因为一旦超时,readfds位图,就会被清零
        struct timeval timeout = {1, 0}; // 必须每次都要设置,否则剩余时间为0时,就变成了非阻塞等待,这也是个输入输出型参数
        int n = select(maxfd + 1, &readfds, nullptr, nullptr, &timeout);
        //在等待期间,一旦有新链接到来,select会立刻返回!
        //并且如果上层不处理,select会一直通知你!
        switch (n)
        {
        case 0: // 超时,没有错误,也没有fd就绪
            std::cout << "sec: " << timeout.tv_sec << " usec: " << timeout.tv_usec << std::endl;
            break; 
        case -1:
            std::cout << "select error" << std::endl;
            break;
        default:
            // 有事件准备就绪
            // TO_DO;
            HandlerEvent(readfds);
            std::cout <<"get a new link!" << std::endl;
            break; 
        }
    }
}

但是这里又出现了新的问题:
我把一堆的fd放进位图中,该怎么区分是读事件fd继续还是其他事件呢?

  • 第一:
if (FD_ISSET(fd, &rfds)) // 确定读事件已经就绪

先确定读事件已经就绪(这里只关心读事件)
在这里插入图片描述

  • 第二:
if (fd == _listensock.Fd())

确定是不是监听套接字,如果是监听套接字,那就获取新链接。
如果不是监听套接字,那肯定就是读事件,(因为这里只关心读事件,所以只有读事件就绪)
在这里插入图片描述

关于不理解的点(现在理解了):
在创建listen套接字并设置进Fd_set位图集中,一旦有新链接到啦,listensock就是那个在门口外面的服务员,而新的读事件的fd,就是一对一针对某个客户端的accept获取新链接返回的sockfd的那个服务员。
两者是不一样的,所以在第一次链接到服务器时,会出现两个fd在位图集中,第一个是listenfd,第二个是accept返回的sockfd。这个两个的区别就相当于在门口吆喝的服务员和针对某个客户进店之后的一对一服务员。

select 的缺点

  • 1.每次都要对位图进行设置,不方便。
  • 2.select支持的文件描述符数量有限。
  • 3.用户层,每次调用select,要维护一个辅助数组,并把辅助数组的fd拷贝到内核层的fd_set.
  • 4.内核层,每次调用select,都要遍历所有传递进来的fd。

IO多路转接——poll

poll函数结构如下:

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

// pollfd结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};

解析:

  • 1.第一个参数fds, 是一个结构体指针,该结构体的第一个成员就是fd,要关心的文件描述符,第二个成员是events,表示用户要关心的事件是什么,第三个成员是revents,表示内核最终的关心的事件的结果,设置在这个revents事件上。
  • 2.第二个参数nfds,表明要关心多少个文件fd,第三个参数是等待时间,单位是ms。

关于events和revents的取值如图:

在这里插入图片描述
所以,poll提供了一个指针,用户我想要关心多少个文件fd,我就传参传多少。
完全由使用者决定。

poll解决了select的两个问题:

  • 1.解决了文件描述符数量有限的问题。
  • 2.解决了每次用户层都要拷贝的问题。

具体的代码使用:请看https://gitee.com/deng-zitao-bit/linux/tree/master/2024_6_1_IO/3.PollServer

IO多路转接——epoll(重点)

Epoll的原理:

在这里插入图片描述

epoll模型最后也有一个地址,也会被管理起来。
因为一个进程可能调用10次epoll_create,就会创建10个epoll模型。

对epoll模型的管理,无非就是先描述,再组织。
在这里插入图片描述

这就是为什么,最后epoll_create函数会返回int ,返回fd的原因。
只要有了fd,就可以在OS维护的fd数组中找到该文件描述符所有的struct files对象,不同的对象就有不同的管理,顺着指针就能找到epoll模型,访问epoll模型中的所有事件了。

epoll_create()

功能:创建上面的epoll模型,并在下层注册回调函数。
(当然,一开始的红黑树和就绪队列肯定是为空的。)

int epoll_create(int size); 
//size表示监听的fd有多大,现代的内核中,该参数已经被忽略,但仍然要传一个参数。

epoll_ctl()

该函数是对epoll模型中的红黑树进行增删改的,(没有查,查询是回调函数做的)
成功返回0,失败返回-1.

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

epoll的事件注册函数.
它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
第一个参数是epoll_create()的返回值。
第二个参数表示动作,用三个宏来表示.
第三个参数是需要监听的fd.
第四个参数是告诉内核需要监听什么事

第二个参数的取值:
EPOLL_CTL_ADD :注册新的fd到epfd中;
EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
EPOLL_CTL_DEL :从epfd中删除一个fd

epoll_event结构体如下:
在这里插入图片描述

epoll_wait()

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

epoll_wait函数是对epoll模型中的就绪队列进行处理的。
将就绪队列中的节点,插入到epoll_wait的第二个参数:event数组中,所以这是一个输出型参数。

返回值就表示插入了的个数。n==0时,表示就绪队列为空。
n<0时表示失败。

根据第二第三个参数可知,epoll_wait所能管理的就绪节点,完全由用户决定,这也同样解决了select所能等待的有限fd的问题。(但是maxevents不能超过epoll_create函数传入的参数size)

参数events是分配好的epoll_event结构体数组.
epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存).
maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞).
如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败.

总结:
epoll_create 创建epoll模型。
epoll_ctl 对epoll模型的红黑树进行增删改。
epoll_wait 将epoll模型的就绪节点插入到自己的events数组中。

epoll中的ET模式和LT模式

ET模式和LT模式的解析如下:

在这里插入图片描述
简单举例:LT模式下,假如A同学有5个快递,今天快递员张三打电话给A同学,说你有5个快递,快点下来取。A同学正在打团战呢,A同学马上说好的等会下去拿。张三看到A同学还没下来拿,过一会,又打电话给A同学,A同学还不下来,过一会又打电话,直到张三下来拿快递为止。

ET模式下:A同学有5个快递,快递员李四打了一次电话给A同学,“同学,你有5个快递赶紧下来拿,你不拿我就走了,不会再打电话给你了。”这时A同学还不来拿快递,李四就直接走了。
过一会,A同学的另外3个快递到了,李四手上此时有了8个快递,这时李四再次打电话给A同学,说,现在你有8个快递了!又新增了3个快递,你快点下来拿!你爱要不要不要我就走了!

默认情况下, 是LT模式。

epoll——LT模式

要谈细节,就得看代码,具体代码在这里:
https://gitee.com/deng-zitao-bit/linux/tree/master/2024_6_1_IO/4.epollServer

  • 细节1:不管怎么等,前提是肯定要有listensock套接字,也就是先将listensock设置进epoll模型中才能走后续的工作!
    就像是,得先有个服务员站在门口吆喝,说我这里是饭店,你饿了就进来吃饭。(listensock)
    这样才有顾客进来,才有在里面的服务员给该客户提供一对一服务! (其他sock)
    所以在epoll_wait()去就绪队列捞继续事件之前,必须调用epoll_ctl()先往红黑树中ADD一个listensock套接字!

  • 细节2:epoll_wait函数中的events数组最多能捞取就绪事件的大小由用户决定,如果就绪队列的事件太多,而epoll_wait一次性读取不完,该怎么办呢?那就等下次调用epoll_wait的时候再重新捞取就绪事件!!!
    这个就绪队列的就绪事件过多其实是因为,OS太忙了,忙到没事件通知上层要把就绪事件读走,导致就绪队列的事件越积越多,此时在底层,就会强制调用callback函数,通知OS,让OS告诉应用层,快点读走!这个过程就像是上下联动,OS代表一个工厂组长,组长有很多事情要忙,底层就像是工人,工人为大量来自客户端的事件做好了准备,就会取通知组长,让组长通知上层处理车间赶紧处理这批货一样。
    在这里插入图片描述

  • 细节3:处理Recvor部分,当客户端不再发消息时,是先将fd从rb_tree中移除,还是先close(fd)呢?
    实际上,在调用epoll_ctl将fd从rb_tree中移除时,需要保证该fd是合法的,如果先close,这个fd就非法了,epoll_ctl就会报错。

在这里插入图片描述

epoll——ET模式

关于ET模式:代码在下面:
https://gitee.com/deng-zitao-bit/linux/tree/master/2024_6_1_IO/5.Reactor

提示:观看下面细节时,请结合代码观看。

  • 细节1:
    将接待客户端的accept_sockfd包装成一个connection对象,该对象含有许多成员:
    在这里插入图片描述
    有发送缓冲区和接收缓冲区。
    有三个回调函数,绑定的是读事件,写事件,异常事件的处理方法。
    有客户端的ip和port
    有服务器指针,可以回指向服务器对象。

  • 细节2
    服务器对象的成员如下:
    在这里插入图片描述
    epoll模型自成一个对象,listensockfd也被包装成一个对象在其他文件中,
    由于一个accept_sockfd被包装成一个connection描述起来了,所以服务器对象就有一个unordered_map哈希表来将accept_sockfd和connection对象映射起来,进行管理。
    _onMessage是一个服务器的应用层的处理方法,包含将一个报文序列化和反序列化等。

具体的流程:

首先服务器起来,进行初始化,在初始化的过程,调用AddConnection函数,也就是给listen套接字包装设置一个connection对象。
在这里插入图片描述
AddConnection会做几件事情:
1.将listensock套接字添加到epoll模型中(listen套接字只关心读事件,所以参数设置成EVENTIN)
(EVENTIN = EPOLLIN | EPOLLET) ,也就是将epoll模型的读写事件设置成ET模式了。
2.绑定的函数是Acceptor。
在这里插入图片描述
这个Acceptor函数就是在当有客户端到来时,获取链接,前面讲过多次,一个客户端就会有一个accpet_sockfd为之服务,这个accept_sockfd在这里也会被包装成connection对象。在最开始时,所有的accept_sockfd都只关心读事件(等待客户端发数据)。
那一开始,connections里面,就有了第一个connection对象,是listensock的,一旦新连接到来,就有了第二个,第三个connection对象,是accept_sockfd的。他们绑定的回调方法不同。
但相同点是,它们都会被设置进epoll模型。

所以后续的工作也就了然于胸了。

  • 细节3:
    关于为什么要将accept_sockfd设置成非阻塞?
    非阻塞模式,这是为了解决一个文件fd由于EPOLL在ET模式下,会出现这样的情况:
    假如客户端向服务器发起10k的请求,如果一个fd是阻塞的,那么这个read读完(可能只能读到1k),就会返回就出现了剩余的9k一直留在这个sockfd的接收缓冲区中,那么就不会向客户端发起响应。而ET模式下,epoll_wait只有一次读取数据的机会,只返回一次,需要等到下次epoll_wait返回才能再读剩下的9k数据。而epoll_wait下次返回又是客户端发起下一个请求的时候才会返回。
    客户端什么时候发起下一个请求呢?等服务器把我上一个10k的请求响应之后,我才会发起下一个请求。这时候就阻塞住了…
    为了解决这个问题,就需要将sockfd设置成非阻塞,这样就能通过非阻塞,+ 循环的方式,把缓冲区的数据都读完。(非阻塞轮询)
    如果不设置成非阻塞,默认是阻塞方式,就变成了,阻塞轮询,如果我读完了缓冲区的数据呢?
    同样也会一直阻塞在那里等待数据到来,这也不能解决问题。
    所以就需要设置成非阻塞轮询。
    在这里插入图片描述

下面这个函数,就是一个事件派发器:
在这里插入图片描述

服务器启动时,就在运行该事件派发器,进行任务派发。
最开始就是将监听套接字设置进epoll模型中,前面已经讲过。
由监听套接字去监听事件(epoll_wait),看是否有事件就绪,一旦有事件就绪,就返回就绪事件数量。并判断就绪的事件类型,派发不同的处理方法。在派发之前,将所有的异常事件,按照读写方式进行处理。是读事件,就调用读事件的回调方法。(在将accept_sockfd创建时,已经创建了一个与之相映射的connection对象,对象中已经绑定了recv,send,except等处理方法),回调就是调这个recv。是写事件,就调用send。

读事件处理方法如下:
在这里插入图片描述
调用recv函数,将数据从fd配套的接收缓冲区读取出来,然后放入到我自己服务器的接收缓冲区中。读取完成后,再将服务器的接收缓冲区的数据交给服务器应用层处理(onMessage),因为我底层并不知道哪些数据是一个报文,所以我只负责读数据,关于数据的处理交给上层,不归我服务器底层管。
这个过程是非阻塞轮询的,直到将fd的接收缓冲区的数据读取完为止。

写事件的处理方法如下:

在这里插入图片描述
整个过程下来,还是看图吧。
在这里插入图片描述
请结合代码来看才能容易懂。

  • 细节4
    在服务器的send处理方法中,有一个开启对写事件的关心。
    这是因为服务器向客户端发送数据时,有可能客户端的接收缓冲区满了,就不能再发,但此时服务器的发送缓冲区还有数据未发送,所以此时就需要开启对写事件的关心,只要对方接收缓冲区有剩余空间了,我就立刻把数据发送过去。
    读事件设置为持续关心,写事件按需关心。
  • 细节5
    shared_ptr和weak_ptr问题

总结select, poll, epoll之间的优点和缺点(重要, 面试中常见)

int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);

select的优点:
1.select具有可监控的最大文件描述符(一个位图,位图大小有限)
2.select每次返回,都会将没有就绪的文件描述符清空,只保留就绪的文件描述符。
所以下次调用select前,还需要重新设置要关心的文件描述符。(内核层)
3.怎么保存我要关心的文件描述符呢?那就用到一个辅助数组了,在链接就绪时,将该accept_fd保存到辅助数组中。(应用层)
select的缺点:
select的优点也同时是缺点。
要在应用层和内核层同时遍历,设置要关心的文件描述符。O(n)了。

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

// pollfd结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};

poll的优点:
将要关心的文件描述符给用户设置,所以不限制能关心的文件描述符的数量。
由于要关心的fd变成了pollfd对象,对象中有要关心的读/写/异常事件可以设置。
就绪的事件放在了另一个位图,所以就不需要在每次调用前,再循环将要关心的事件设置进位图了。
poll的缺点:
仍然要用一个struct pollfd array[]数组将这些pollfd对象管理起来,在应用层每次监听套接字就绪时,都需要为fd创建pollfd对象,并放进数组中,设置要关心的事件。
每次调用poll,都要将pollfd array[]数组中的pollfd对象拷贝到内核中进行关心。所以当pollfd很多时,也会降低效率。

int epoll_create(int size); 
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll的优点:
先讲原理:epoll创建了一个epoll模型,有一棵红黑树和就绪队列对多个事件进行管理。
当有新事件到来,将要关心的事件插入到红黑树中,并为之设置回调方法。
一旦要关心的事件就绪,就会将该节点链入到继续队列中,这样,一个节点就会同时存在于红黑树和就绪队列两个数据结构中,上层处理数据时,只需要从就绪队列中拿取节点一个个处理即可,处理完成后,就将该节点从rb_tree中删除即可!
至于对就绪事件的处理,绑定一个回调函数,处理事件时直接回调即可。
1.解决了关心的fd数量限制问题。
2.接口使用方便,虽然拆成了三个函数,但是不用再考虑应用层内核层将要关心的事件再保留拷贝问题。
3.只在关心某个事件的时候,将fd包装成的结构体拷贝到红黑树中,后续不需要再拷贝,也就是只拷贝一次。不像select,epoll,每次循环调用都要拷贝。
4.事件就绪时,不需要像select,poll那样,遍历一个数组,一个个判断谁就绪,epoll直接从就绪队列中一次捞取一大批节点进行处理即可,所以根本不影响处理效率。

epoll没什么缺点。
要说有,那就是ET模式和LT模式的区别。在不同场景,用不同的模式。

  • 35
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邓富民

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值