select / poll / epoll 讲解

IO的过程就是数据的等待和拷贝的过程。

在计算机系统中有五种典型的IO模型:阻塞IO、非阻塞IO、信号驱动IO、异步IO、多路转接IO。

阻塞IO:

发起IO调用后,若当前不具备IO条件,则一直阻塞等待。

    优点:流程非常简单,多个IO的情况下,处理过程是串行的

    缺点:对CPU的使用率不高

非阻塞IO:

发起IO调用后,若当前不具备IO条件,则报错返回,需要循环操作,直到IO完成。

    优点:报错返回后,程序可以进行别的运算,提高CPU利用率

    缺点:需要循环操作,流程变复杂了

信号驱动IO:

自定义一个IO信号的处理方式,当IO信号到来后,操作系统通过信号通知相关的进程,在信号处理函数中处理就绪的IO。

    优点:当时用信号驱动IO的时候,由于是注册了信号处理函数,所以不需要配合循环来关注IO操作,并且内核将数据准备好,通过SIGIO来通知用户进程,相对于非阻塞IO,信号驱动IO,更加的灵活,更加的实时。 

    缺点:程序流程更加的复杂。

异步IO:

定义IO信号处理,发起异步IO调用,可以立即返回,IO的等待和数据拷贝的过程都由操作系统内核完成,完成后通过信号通知进程。

    优点:相对于信号驱动IO,直接让操作系统内核完成IO数据的等待和拷贝,自己只需要发起一个调用,IO完成后,操作系统通过信号通知进程,进程直接对数据进行处理。

    缺点:流程更加复杂。

以上四种IO方式对比:

阻塞与非阻塞的区别:发起一个调用之后,若不具备完成功能的条件,是否立即返回。

非阻塞IO 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码,也就意味着,非阻塞的调用需要搭配循环来使用,换个实际的场景,也就是意味着,当用户发起一个read操作之后,如果内核没有准备好数据,则read返回;非阻塞IO虽然需要配合循环来使用,但是并不一定CPU使用率就很好,CPU使用率很高,意味着CPU在做大量的运算(逻辑运算或者算术运算),所以不一定CPU使用率会高。

异步IO:内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)

异步阻塞/异步非阻塞:区别在于进程是否等待别人完成调用功能

同步与异步的区别:发起调用,是否由进程自身完成功能。

多路转接IO:

多路转接IO:对大量的IO(描述符)进行事件监控(IO-可读/可写/异常)判断哪些IO就绪了,若就绪了,则可以让进程仅针对就绪的IO进行操作。让进程避免对大量的未就绪的描述符进行操作,降低效率而进程仅仅针对就绪的描述符进行操作,则可以避免因为IO为就绪而导致的阻塞情况,并且仅针对就绪的描述符进行操作提高了效率。

多路转接IO的实现模型:select模型(可以跨平台)/ poll模型(处于淘汰的边缘)/ epoll模型(高性能)

select模型:

对大量的描述符进行用户关心的事件(可读事件/可写事件/异常)监控,若有描述符就绪了用户关心的事件则返回,让用户对就绪的描述符进行操作。

描述符的可读事件:接收缓冲区中的数据大小大于低水位标记(基准值,默认为1字节)

描述符可写事件:发送缓冲区中的剩余空间大小大于低水位标记(基准值,默认1字节)

实现流程:

        1、定义描述符事件集合(可读事件的描述符结合/可写事件的描述符集合/异常事件的描述符集合)

        2、用户在进程中,对哪个描述符关心哪个事件,则将这个描述符加入到指定的事件描述符集合中

        3、将这个集合拷贝到内核中,进行轮询遍历监控

        4、若某个集合中有描述符就绪了对应关心的事件,则返回,但是在返回前,会将所有集合中没有就绪的描述符从集合中删除掉 。(当select监控调用返回的时候,集合中只有就绪的描述符)

        5、用户判断哪个描述符还在集合中,哪个就是就绪的 ,就可以对这个描述符进行对应事件的操作。

具体代码操作:

    1、定义描述符事件集合 struct fd_set 这个结构体中只有一个数组成员变量, 这个数组被当成二进制位图使用;数组有多大就可以添加多少描述符,每个描述符都是一个数字,将描述符添加到集合中,就是将描述符这个位置对应的二进制置1。

这个数组中有多少二进制位,取决于宏‘FD_SETSIZE’默认是1024,表示默认的情况下select最多监控1024个描述符。 

    void FD_ZERO(fd_set *set); //初始化清空set集合

    2、将描述符添加到所关心的事件集合中:void FD_SET(int fd,fd_set *set)//向set集合中添加描述符

    3、将集合拷贝到内核中,进行轮询遍历,判断是否就绪;

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

        nfds:集合中的最大描述符+1(为了防止空遍历,提高遍历效率);select是轮询遍历监控,集合默认大小是1024;通过指定最大描述符+1决定集合遍历大小,提高遍历效率     

        readfds:可读事件集合/writefds:可写事件集合/exceptfds:异常事件集合

        timeout:设置select是否阻塞/限制阻塞时间;

             NULL-若没有就绪就一直监控下去

             timeout内部数据为0-非阻塞 ,遍历一次若没有就绪则直接返回

             timeout设置阻塞时长:tv_sec/tv_usec 若在指定的时间内都没有就绪,则返回。

                    struct timeval tv; tv.tv_sec=3(秒); tv.tv_usec=0(微妙)

        返回值:>0返回值为就绪的描述符个数,==0返回值表示等待超时 ,没有就绪,<0返回值表示监控出错。

        每次遍历完毕:判断是否有就绪,若有就绪,将集合中的未就绪的描述符移除然后调用返回,若没有就绪,则判断是否监控等待超时,若超时则从集合中移除所有的描述符,然后调用返回,若没有超时,则休眠一会,重新遍历判断。

        在select返回的时候,所传入的集合中只保留已经就绪的描述符信息(在返回之前将集合中所有没有就绪的描述符从集合中移除)

        因为select每次都会改变传入的集合中的数据,因此每次重新监控之前都要重新向集合中添加描述符信息

     4、select返回给用户就绪的描述符集合,用户在进程中通过遍历判断哪个描述符在集合中,得知描述符是就绪的,针对就绪的描述符进行操作

        int FD_ISSET(int fd,fd_set *set)   //判断描述符是否在集合中,返回值(以返回值真假表示是否在集合中,)

    5、若某个描述符关闭了连接,则需要从集合中移除(不再监控它了)

        void FD_CLR(int fd ,fd_set *set) //从set集合中移除fd描述符

使用C++对select进行封装,使用封装后的代码,实现一个TCP并发服务器。

并发:数据进行轮询处理(一个个判断谁就绪了,然后一个个去处理,依赖CPU的分时机制)

并行:数据同时处理(若在单核心CPU的计算机上多线程/多进程其实也是并发,操作系统层面的均衡并发),

若有一万个描述符就绪了,我们自己使用的多路转接模型实现的并发,一个一个往下处理,采用多进程/多线程,操作系统以CPU分时机制 实现每个进程的轮询调度。

select的优缺点:

    1、select所能监控的描述符是由最大上限的,取决于宏_FD_SETSIZE,默认为1024个

    2、select的监控原理,是在内核中进行轮询遍历判断, 这种轮询遍历会随着描述符的增多而性能下降

    3、select返回的时候会移除所有未就绪描述符,给用户返回就绪的描述符集合,但是没有直接告诉用户哪个描述符就绪,需要程序员自己遍历哪个描述符在集合中,才能获知哪个描述符就绪,操作流程比较麻烦。

    4、每次监控都需要重新将集合拷贝到内核中才能监控

优点:

select遵循POSIX标准,跨平台移植性比较好,监控的超时等待时间,可以精确到微妙。

代码示例

封装一个select类  select.hpp

#include <iostream>
#include <vector>
#include <sys/select.h>
#include <time.h>
#include "tcpsocket.hpp"

class Select
{
    public:
        Select():_maxfd(-1){
            FD_ZERO(&_rfds);//初始化清空集合
        }
        bool Add(TcpSocket &sock) {
            //将描述符添加到监控集合
            int fd = sock.GetFd();
            FD_SET(fd, &_rfds);// 将描述符添加到集合中
            _maxfd = _maxfd > fd ? _maxfd : fd;//每次添加监控重新判断最大描述符
            return true;
        }
        bool Del(TcpSocket &sock) {
            //从监控集合中移除描述符
            int fd = sock.GetFd();
            FD_CLR(fd, &_rfds);// 从集合中移除描述符 // 0 3 5 8
            if (fd != _maxfd) {//判断删除的描述符是否是最大的描述符
                return true;
            }
            for (int i = fd; i >= 0; i--) {//重新判断最大的描述符是多少
                if (FD_ISSET(i, &_rfds)) {
                    _maxfd = i;
                    break;
                }
            }
            return true;
        }
        bool Wait(std::vector<TcpSocket> *list, int mtimeout = 3000) {
            //开始监控,并返回就绪的socket信息
            struct timeval tv;
            tv.tv_usec = (mtimeout % 1000) * 1000;
            tv.tv_sec = mtimeout / 1000;//mtimeout 单位为毫秒
            //因为select会修改描述符集合,返回时将未就绪的描述符全部移除
            //因此不能直接使用_rfds,而是使用临时集合,避免对_rfds做出修改
            fd_set tmp_rfds = _rfds;
            int nfds = select(_maxfd + 1, &tmp_rfds, NULL, NULL, &tv);
            if (nfds < 0) {//监控出错
                perror("select error");
                return false;
            }else if (nfds == 0) {//监控等待超时
                printf("wait timeout\n");
                return true;
            }
            for (int i = 0; i <= _maxfd; i++) {//这是一种笨办法,从0到_maxfd逐个判断谁在集合中
                if (FD_ISSET(i, &tmp_rfds)) {     //谁在集合,谁就是就绪的
                    //就绪的描述符
                    TcpSocket sock;
                    sock.SetFd(i);
                    list->push_back(sock);
                }
            }
            return true;
        }
    private:
        int _maxfd;  //最大的描述符
        fd_set _rfds;//可读事件的描述符集合
};

select服务器类:select_srv.cpp

#include <cstdio>
#include "select.hpp"

int main(int argc, char *argv[])
{
    if (argc != 3) {
        printf("usage: ./select_srv ip port\n");
        return -1;
    }
    std::string srv_ip = argv[1];
    uint16_t srv_port = std::stoi(argv[2]);


    TcpSocket listen_sock;
    CHECK_RET(listen_sock.Socket());//创建套接字
    CHECK_RET(listen_sock.Bind(srv_ip, srv_port));//为套接字绑定地址信息
    CHECK_RET(listen_sock.Listen());//开始监听

    Select s;
    s.Add(listen_sock);
    while(1) {
        std::vector<TcpSocket> list;
        bool ret = s.Wait(&list);
        if (ret == false) {
            return -1;//监控出错
        }
        for (auto sock : list) {
            if (sock.GetFd() == listen_sock.GetFd()) {
                //这个就绪的套接字是监听套接字--表示由新连接到来
                TcpSocket client_sock;
                bool ret = listen_sock.Accept(&client_sock);//获取新建连接
                if (ret == false) {
                    continue;
                }
                s.Add(client_sock); //将通信套接字也添加监控
            }else {
                //这个就绪的套接字是一个普通的通信套接字
                std::string buf;
                ret = sock.Recv(&buf);//使用新建连接接收客户端数据
                if (ret == false) {
                    s.Del(sock);//描述符出错则移除监控
                    sock.Close();
                    continue;
                }
                std::cout << "client say: " << buf << std::endl;
                buf.clear();
                std::cout << "server say: ";
                std::cin >> buf;
                ret = sock.Send(buf);//通过新建连接向客户端发送数据
                if (ret == false) {
                    s.Del(sock);//描述符出错则移除监控
                    sock.Close();
                    continue;
                }
            }
        }
    }
    listen_sock.Close();
    return 0;
}

poll模型:

相较于select有一个好处,select将IO事件分了多个集合,而poll则采用了一种事件结构,关心什么事件,添加标志位就可以了,不需要定义多个集合,稍微简化了流程。

事件结构:

struct pollfd

{

    int fd; 用户关心的描述符

    short events;  针对fd描述符用户所关心的事件-POLLIN-可读 POLLOUT-可写 POLLIN | POLLOUT

    short revents;  这个成员用于保存当poll返回的时候,当前fd描述符就绪的事件

}

1、定义pollfd描述符事件数组(要监控多少描述符,数组就定义多大),将描述符的信息填充进去

     struct pollfd arr[1]  arr[0].fd = 0 arr[0].events = POLLIN 对标准输入监控可读事件

2、将数组拷贝到内核中进行轮询遍历监控

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

        fds:定义好的事件结构体数组,nfds:数组中有效的事件个数  timeout:监控超时等待时间,以毫秒为单位

        poll在返回时,会将各个描述符就绪的事件放到各自对应的事件结构节点的revents成员中

3、调用返回后,只需要根据事件结构数组中各个节点的revents成员就能判断就绪了什么事件,进而进行相应的操作。

poll优缺点分析:

1、poll也需要每次将事件信息拷贝到内核中进行监控

2、poll监控的原理也是在内核进行轮询遍历判断,性能会随着描述符的增多而下降

3、仅支持linux,跨平台移植性比较差

4、poll也没有直接告诉用户哪个描述符就绪了哪个事件,需要用户遍历,通过节点的revents成员获知

优点:

1、采用事件结构方式对描述符进行事件监控,相较于select简化了很多流程

2、对监控的描述符数量没有上限(监控多少描述符就创建多大的结构体)

3、相较于select每次都重新添加描述符,他不需要去重新初始化事件节点。

epoll模型:linux下性能最高的事件通知工具(多路转接模型)

操作步骤:

1、创建并初始化内核中的eventpoll结构;

    int epoll_create(int size)

size:决定能够监控 多少个描述符,但是linux2.6.8后被忽略,但是必须大于0

        这个函数返回epoll的操作句柄(文件描述符),通过这个文件描述符,可以找到内核中的eventpoll结构体

2、若用户需要监控什么描述符的什么事件,则为描述符构建一个事件结构 struct epoll_event,添加到内核的eventpoll中。

struct epoll_event{

    uint32_t events ; 用户针对描述符所关心的事件 EPOLLIN- 可读 EPOLLOUT-可写

    typedef union epoll_data_t{

        int fd ; 用户需要监控的描述符

        void * ptr

    } data;     

}; 

            struct epoll_event ev; ev.events = EPOLLIN ev.data.fd = 0;对标准输入监控可读事件

int epoll_ctl(int epfd ,int how ,int fd ,struct epoll_event * event);--通过这个结构将事件添加到内核中

epfd:epoll_create返回的epoll的操作句柄

how:EPOLL_CTL_ADD(添加事件) / EPOLL_CTL_DEL(移除事件) / EPOLL_CTL_MOD(修改事件),对内核中描述符的事件信息进行的操作

fd 要监控的描述符,

event:fd描述符对应的事件结构体信息,当这个描述符就绪了就会给进程返回这个事件结构体

3、epoll_ctl(epfd ,EPOLL_CTL_ADD, 0, &ev) 这一步将事件结构以及监控的描述符添加到内核中进行监控

                如果描述符0就绪了ev中定义的事件,就会给用户返回这个ev结构。用户就可以直接针对                                    ev.data.fd这个描述符进行操作。

epoll监控原理,是一种异步阻塞/非阻塞操作,将节点添加到内核中的eventpoll的红黑树中,一旦开始监控,若哪个描述符就绪,将这个描述符对应的epoll_event结构添加到双向链表当中(并且这个过程操作系统采用事件回调方式完成,为每个描述符的就绪事件定义回调函数,让这个回调函数把描述符事件结构添加到双向链表中)。而我么的进程只需要判断双向链表中是否有节点就可以判断是否有序,并且可以直接获取到就绪描述符对应的事件结构信息epoll_event

4、开始监控

    内核中操作系统开始描述符的事件监控,进程自身只需要判断双向链表中是否有数据,就可以判断是否有就续事件。

int epoll_wait(int epfd struct epoll_event * evs int maxevent ,int timeout) 

epfd :epoll操作句柄,通过这个句柄能够找到内核中的eventpoll结构

evs:struct epoll_event 结构体数组,用于获取双向链表中就绪的描述符对应的事件结构信息,添加数组内容的过程是由epoll_wait函数自己完成的。

maxevent:evs数组的节点个数,也是本次想要获取的就绪事件的最大个数。

timeout:超时事等待时间,毫秒为单位。

返回值:返回值大于0,表示就绪事件个数;返回值等于0,表示等待超时,返回值小于0,表述出错。

 

操作流程

1、在内核中创建epoll句柄

2、向内核中添加监控的描述符以及对应事件节点struct epoll_event

3、开始监控,给用户直接返回就绪的事件节点(异步阻塞操作:监控由系统完成,进程只判断是否有就绪)

4、用户直接针对就绪的事件进行操作

 

性能高的原因:

    epoll的监控只需要判断双向链表是否为空就可以完成判断,不需要遍历,因此性能不会随着描述符的增多而下降

    epoll的事件节点只需要向内核中添加一次,不需要重复添加

    直接给用户返回就绪的节点,用户不需要进行空遍历判断谁就绪

    epoll向用户返回就绪节点采用内存映射,避免了数据的拷贝过程

实例:epoll.hpp、关于tcpsocket.hpp会在下一篇博客中写到。

#include <cstdio>
#include <iostream>
#include <vector>
#include <sys/epoll.h>
#include "tcpsocket.hpp"

class Epoll
{
    public:
        Epoll(): _epfd(-1){
            //epoll_create(int maxevent) // maxevent已经忽略了,非0就可以
            _epfd = epoll_create(1);
            if (_epfd < 0) {
                perror("epoll_create error");
                exit(-1);//因为构造函数没有返回值,不知道构造成功与否,因此失败直接退出
            }
        }
        bool Add(TcpSocket &sock){
            int fd = sock.GetFd();
            //epoll_ctl(epoll句柄,操作, 描述符, 对应的事件结构)
            struct epoll_event ev;
            ev.events = EPOLLIN | EPOLLET;//EPOLLIN-可读事件
            ev.data.fd = fd;
            int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
            if (ret < 0) {
                perror("epoll_ctl add error");
                return false;
            }
            return true;
        }
        bool Del(TcpSocket &sock) { 
            int fd = sock.GetFd();
            int ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, NULL);
            if (ret < 0) {
                perror("epoll_ctl del error");
                return false;
            }
            return true;
        }
        bool Wait(std::vector<TcpSocket> *list, int timeout = 3000){
            //epoll_wait(操作句柄,事件数组首地址,数组节点数量,超时时间)
            struct epoll_event evs[10];
            int ret = epoll_wait(_epfd, evs, 10, timeout);
            if (ret < 0) {
                perror("epoll wait error");
                return false;
            }else if (ret == 0) {
                printf("timeout\n");
                return true;
            }
            //就绪的事件都放在evs中,就绪的个数就是返回值
            for (int i = 0; i < ret; i++) {
                if (evs[i].events & EPOLLIN) {//判断就绪的事件是否是可读事件
                    //这里其实可以不用判断,因为我们的添加函数就只监控了可读事件
                    TcpSocket sock;
                    sock.SetFd(evs[i].data.fd);
                    list->push_back(sock);
                }
            }
            return true;
        }
    private:
        int _epfd;//epoll的操作句柄
};

epoll_srv.cpp

#include <iostream>
#include <string>
#include <vector>
#include "epoll.hpp"

int main(int argc, char *argv[]) 
{
    if (argc != 3) {
        printf("usage: ./epoll_srv ip port\n");
        return -1;
    }
    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);

    TcpSocket lst_sock;
    CHECK_RET(lst_sock.Socket());
    //将描述符设置为非阻塞
    lst_sock.SetNonBlock();
    CHECK_RET(lst_sock.Bind(ip, port));
    CHECK_RET(lst_sock.Listen());

    Epoll epoll;
    CHECK_RET(epoll.Add(lst_sock));

    while(1) {
        std::vector<TcpSocket> list;
        bool ret = epoll.Wait(&list);
        if (ret == false) {
            return -1;
        }
        for (int i = 0; i < list.size(); i++) {
            if (list[i].GetFd() == lst_sock.GetFd()) {
                TcpSocket cli_sock;
                bool ret = lst_sock.Accept(&cli_sock);
                if (ret == false) {
                    continue;
                }
                cli_sock.SetNonBlock();
                epoll.Add(cli_sock);
            }
			else {
                std::string buf;
                ret = list[i].Recv(&buf);
                if (ret == false) {
                    epoll.Del(list[i]);
                    list[i].Close();
                    continue;
                }
                std::cout << "client say: " << buf << std::endl;

                std::cout << "server say: ";
                buf.clear();
                std::cin >> buf;
                ret = list[i].Send(buf);
                if (ret == false) {
                    epoll.Del(list[i]);
                    list[i].Close();
                }
            }
        }
    }
    lst_sock.Close();
    return 0;
}

tips:

事件的就绪是什么意思

    接收缓冲区中的数据大小大于低水位标记,则数据是可读的

    发送缓冲区中剩余的空间大小大于低水位标记,则数据是可写的。

epoll对描述符就绪事件的触发方式,

    水平触发:EPOLLT 默认的select和poll支持

        可读事件: 只要接收缓冲区中的数据大小大于低水位标记,就会触发可读事件就绪

        可写事件:只要发送缓冲区中的数据大小大于低水位标记,就会触发可写事件就绪

    边缘触发:EPOLLET   只有epoll支持

        可读事件:只有新数据到来的时候才会触发可读就绪事件(不管上一次数据有没有读完,缓冲区中有没有数据遗留)

        可写事件:只有发送缓冲区剩余空间从不可写变为可写的时候才会触发一次可写就绪事件。

主要是为了避免就绪事件的判断方式导致程序不断的对就绪事件进行大量的遍历操作

但是边缘触发,也就是要求我们一次性处理完所有的数据。否则剩余的数据不会触发就绪,要等到下一次就绪的到来才会处理。

设置描述符为非阻塞的操作:

int fcntl(int fd ,int cmd , /* arg*/);

fd:描述符;cmd :F_GETFL--获取已有的属性,通过返回值返回/F_SETFT--设置属性,通过arg参数设置

非阻塞属性:O_NONBLOCK 一个描述符若被设置为非阻塞,而接受数据的时候缓冲器没有数据则会报错 EAGAIN/EWOULDBLOCK,这个错是可以原谅的,return true就可以了,因为没有数据了。

优点:

1、监控的描述符数量没有上限

2、监控采用异步阻塞操作,性能不会随着描述符的增多而下降

3、直接给进程提供就绪的事件以及描述符操作,不需要进程进行空遍历

4、描述符的事件信息,只需要向内核拷贝一次

5、给进程返回就绪的事件信息,通过内存映射完成,节省了数据的拷贝过程

缺点:

无法跨平台

epoll的惊群问题: 

同事唤醒了多个进程处理一件事情,导致了不必要的CPU空转,为什么会唤醒多个进程,因为发生事件的文件描述符在多个进程之间是共享的,引起惊群问题的主要是epoll的LT模式,LT模式、可读事件就绪,接收缓冲区的数据大小大于一个字节,在epoll_wait有就绪事件到来后,操作系统一直唤醒等待线程,直到就绪事件被处理解决方案:改用ET模式,操作系统只通知进程一次,只有一个进程处理就绪事件。 想详细了解的老铁看:https://blog.csdn.net/dog250/article/details/80837278?locationNum=7&fps=1

多路复用的需求让select,poll,epoll等事件模型更为受到欢迎,所谓的事件模型即 阻塞在事件上而不是阻塞在事务上 。内核仅仅通知 发生了某件事 ,具体 发生了什么事 ,则有处理进程或者线程自己来poll。如此一来,这个事件模型( 无论其实现是select,poll,还是epoll )便可以 一次搜集多个事件 ,从而满足多路复用的需求
 
ET事件发生仅通知一次的原因是只被添加到rdlist中一次,而LT可以有多次添加的机会,由于ET模式只通知一次的机制,所以在使用ET模式的收,需要搭配成非阻塞来使用
epoll理论上可以监控无限多的文件描述符,虽然每个进程有打开文件描述符的上线限制

多路转接模型的应用场景

tcp服务器的并发处理
多进程/多线程/多路转接模型
多进程-资源消耗高
多线程-资源消耗较低,并且所有的描述符的处理通过操作系统层面的CPU分时技术实现均衡处理
多路转接模型: 用户在进程中对所有的描述符轮询处理 --最后一个描述符等待的时间比较长(需要用户考虑均衡处理问题),适用于有大量描述符需要监控,但是同一时间有少量描述符活跃的场景。
 
EOF

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值