一文带你吃透 IO 多路转接

本文详细介绍了阻塞与非阻塞IO、同步与异步通信,重点讲解了select、poll和epoll三种IO多路转接模型,特别是epoll的工作原理、优点、工作模式和使用场景,探讨了如何利用epoll提高高并发环境下的IO效率。
摘要由CSDN通过智能技术生成

1 五种 IO 模型的简单介绍

  • 阻塞 IO : 在内核将数据准备好之前,系统调用会一直等待。所有的套接字默认都是阻塞方式,阻塞 IO 是最常见的IO模型。
  • 非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回 EWOULDBLOCK 错误码,非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用
  • 信号驱动IO: 内核将数据准备好的时候,使用SIGIO信号通知应用程序进行 IO操作。
  • IO 多路转接:最核心的在于 IO 多路转接能够同时等待多个文件描述符的就绪状态
  • 异步IO,前面4种IO模式都是同步IO,拷贝数据的动作都是进程自己去完成的,而异步 IO由内核在数据拷贝完成时,通知应用程序(而信号驱动 IO 是告诉进程何时可以开始拷贝数据,还是要自己去拷贝)

总结

  • 任何 IO 过程中,都包含两个步骤。第一是等待,第二是拷贝。 在实际的应用场景中,等待消耗的时间往往都远高于拷贝的时间,让 IO 更高效,最有效的办法就是让等待的时间减少,这就是 IO 多路转接的核心思想所在。

2 和高效 IO 相关的一些概念

2.1 同步通信与异步通信

同步和异步关注的是消息通信机制.

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
  • 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.
  • 另外, 多进程多线程也提到同步和互斥,这里的同步通信和进程之间的同步是完全不想干的概念。
  • 进程/线程同步也是进程/线程之间直接的制约关系是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时

2.2 阻塞与非阻塞

  • 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程

3 IO 多路转接/复用

3.1 非阻塞 IO

一个文件描述符,默认都是阻塞 IO,可以调用如下的接口把描述符改为非阻塞
在这里插入图片描述

  • 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

void SNB(int fd)
{
    int fl = fcntl(fd, F_GETFL);
    if (fl < 0)
    {
        perror("fcntl error\n");
        return;
    }
    fcntl(fd,F_SETFL, fl | O_NONBLOCK);
}

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

3.2 select

3.2.1 select 介绍

系统提供select函数来实现多路复用输入/输出模型.

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

3.2.2 select 的函数原型

在这里插入图片描述
参数解释

  • 参数 nfds 是需要监视的最大的文件描述符值 + 1;文件描述符值是文件描述符表的下标,内容是结构体指针,这样可以满足左开右闭的区间,有利于遍历去检测是否有事件就绪。
  • readfs,writefds,exceptfds,分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合,异常文件描述符的集合。fd_set 的结构其实是位图,而且这3个位图既是输入型参数,又是输出型参数。
  • 参数 timeout 为结构 timeval,用来设置 select() 的等待时间

参数 timeout 取值:

  • NULL:表示 select 没有 timeout,select 将一直被阻塞,直到某个文件描述符上发生了事件
  • 0:仅检测文件描述符集合的状态,然后立即返回,并不等待事件的发生
  • 特定的时间值:如果在指定的时间段里没有事件发生,select 将超时返回。

提供了一组操作fd_set的接口, 来比较方便的操作位图
在这里插入图片描述
函数返回值

  • 执行成功则返回文件描述符状态已改变的个数
  • 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
  • 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测。

错误值可能为:

  • EBADF 文件描述词为无效的或该文件已关闭
  • EINTR 此调用被信号所中断
  • EINVAL 参数n 为负值
  • ENOMEM 核心内存不足

3.2.3 理解 select 的执行过程

  • 执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000
  • 若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
  • 若再加入fd=2,fd=1,则set变为0001,0011
  • 执行 select(6,&set,0,0,0) 阻塞等待
  • 若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的 fd = 5 将被清空

3.2.4 socket 就绪条件都有哪些

  • 读就绪
    socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0
    socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0
    监听的socket上有新的连接请求
    socket上有未处理的错误
  • 写就绪
    socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小)大于等于低水位标记,SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0
    socket的写操作被关闭(close或者shutdown),对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号
    socket使用非阻塞connect连接成功或失败之后
    socket上有未读取的错误

3.2.5 select 的特点

  • 可监控的文件描述符个数取决与sizeof(fd_set)的值.
  • 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd
  • 一是用于再select 返回后array作为源数据和fd_set进行FD_ISSET判断
  • 二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数

3.2.6 select 的缺点

  • 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select 支持的文件描述符数量太小

3.2.7 select 的使用示例

#pragma once 
#include "Sock.hpp"

#define DFL_PORT 8080
#define NUM (sizeof(fd_set) * 8)
#define DFL_FD -1

class SelectServer
{
    private:
        int lsock;
        int port;
        int fd_array[NUM];
    public:
        SelectServer(int _p = DFL_PORT)
          :port(_p)
        {}
        void InitServer()
        {
            for (int i = 0; i < NUM; i++)
            {
                fd_array[i] = DFL_FD;
            }
            lsock = Sock::Socket();
            Sock::Setsockopt(lsock);
            Sock::Bind(lsock, port);
            Sock::Listen(lsock);

            fd_array[0] = lsock;
        }
        void AddFdToArray(int sock)
        {
            int i = 0;
            for (;i < NUM; i++)
            {
                if (fd_array[i] == DFL_FD)
                  break;
            }
            if (i >= NUM)
            {
                cerr << "fd array is full, close sock" << endl;
                close(sock);
            }
            else 
            {
                fd_array[i] = sock;
                cout << "fd: " << sock << "add to select ..." << endl;
            }
        }

        void DelFdFromArray(int index)
        {
            if (index >= 0 && index < NUM)
              fd_array[index] = DFL_FD;
        }
        void HandlerEvents(fd_set * rfds)
        {
            for (int i = 0; i < NUM; i++)
            {
                if (fd_array[i] == DFL_FD)
                {
                    continue;
                }
                if (FD_ISSET(fd_array[i],rfds))
                {
                    // 连接事件就绪
                    if (fd_array[i] == lsock)
                    {
                        // 有连接事件
                        int sock = Sock::Accept(lsock);
                        if (sock >= 0)
                        {
                            // 
                            cout << "get a new link ..." << endl;
                            AddFdToArray(sock);
                        }
                    }
                    else 
                    {
                        // 读数据事件就绪
                        char buf[1024];
                        ssize_t s = recv(fd_array[i], buf, sizeof(buf), 0);
                        if (s > 0)
                        {
                            buf[s] = 0;
                            cout << "client# " << buf << endl;
                        }
                        else if (s == 0)
                        {
                            cout << "client quit " << endl;
                            close(fd_array[i]);
                            DelFdFromArray(i);
                        }
                        else 
                        {
                            // 读出错
                        }
                    }
                }
            }
        }
        void Start()
        {
            int maxfd = DFL_FD;
            for (;;)
            {
                fd_set rfds;
                FD_ZERO(&rfds);
                cout << "fd_array : ";
                for (int i = 0; i < NUM; i++)
                {
                    if (fd_array[i] != DFL_FD)
                    {
                        cout << fd_array[i] << " ";
                        FD_SET(fd_array[i], &rfds);
                        if (maxfd < fd_array[i])
                        {
                            maxfd = fd_array[i];
                        }
                    }
                }
                cout << endl;
                cout << "begin select ..." << endl;
                //
                switch(select(maxfd + 1,&rfds, nullptr, nullptr,nullptr))
                {
                    case 0:
                      cout << "time out ..." << endl;
                      break;
                    case -1:
                      cerr << "select error!" << endl;
                      break;
                    default: 
                      HandlerEvents(&rfds);
                      break;

                }
            }
        }
        ~SelectServer()
        {
            close(lsock);
        }
};

是不是感觉用起来很麻烦,所以才有了poll,最后最优的是 epoll

3.3 poll

函数接口介绍
在这里插入图片描述
参数说明

  • fds是一个poll函数监听的结构体列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合
  • nfds表示fds数组的长度.
  • timeout表示poll函数的超时时间, 单位是毫秒(ms)

events和revents的取值:

  • POLLIN:数据可读
  • POLLOUT:数据可写

返回值

  • 返回值小于0, 表示出错;
  • 返回值等于0, 表示poll函数等待超时
  • 返回值大于0, 表示poll由于监听的文件描述符就绪而返回

poll 的优点和缺点
优点
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比select更方便
poll并没有最大数量限制 (但是数量过大后性能也是会下降)
缺点
和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符
每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中
同时连接的大量客户端在一时刻可能只有很少的处于就绪状态 因此随着监视的描述符数量的增长, 其效率也会线性下降

poll 使用示例

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <poll.h>

using namespace std;

int main()
{
    struct pollfd rfds[1];
    rfds[0].fd = 1;
    rfds[0].events = POLLOUT;
    rfds[0].revents = 0;

    char buf[1024] = {0};

    cout << "poll begin..." << endl;
    while(true){
        switch(poll(rfds, 1, 1000)){
            case 0:
                cout << "time out ..." << endl;
                break;
            case -1:
                cout << "poll error" << endl;
                break;
            default:
                cout << "events happen!" << endl;
                //HandlerEvents(rfds);
                if(rfds[0].fd == 1 && (rfds[0].revents & POLLOUT)){
                    printf("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
                    //cout << "hello world";
                   // ssize_t s = read(0, buf, sizeof(buf));
                   // buf[s] = 0;
                   // cout <<"echo# "<< buf <<endl;
                }
                break;
        }
    }
    return 0;
}

3.4 epoll(重点)

3.4.1 epoll 初识

设想一个场景:有100万用户同时与一个进程保持着TCP连接,而每一时刻只有几十个或几百个TCP连接是活跃的(接收TCP包),也就是说在每一时刻进程只需要处理这100万连接中的一小部分连接。那么,如何才能高效的处理这种场景呢?进程是否在每次询问操作系统收集有事件发生的TCP连接时,把这100万个连接告诉操作系统,然后由操作系统找出其中有事件发生的几百个连接呢?实际上,在Linux2.4版本以前,那时的select或者poll事件驱动方式是这样做的。
这里有个非常明显的问题,即在某一时刻,进程收集有事件的连接时,其实这100万连接中的大部分都是没有事件发生的。因此如果每次收集事件时,都把100万连接的套接字传给操作系统(这首先是用户态内存到内核态内存的大量复制),而由操作系统内核寻找这些连接上有没有未处理的事件,将会是巨大的资源浪费,然后select和poll就是这样做的,因此它们最多只能处理几千个并发连接。而epoll不这样做,它在Linux内核中申请了一个简易的文件系统,把原先的一个select或poll调用分成了3部分:

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

3.4.2 epoll 模型的3个系统调用接口

在这里插入图片描述
创建一个epoll的句柄(可以理解为一种智能指针)
注意

  • 自从linux2.6.8之后,size参数是被忽略的
  • 用完之后, 必须调用close()关闭

调用完epoll_create OS在底层会创建epoll模型,返回文件描述符

在这里插入图片描述
epoll_ctl 的作用是:向epoll模型中添加文件描述符和及其对应的事件,即:用户告诉内核=>你帮我留意这些文件描述符(套接字sock)上的事件哦

epoll 的参数说明

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

第二个参数的取值

  • EPOLL_CTL_ADD :注册新的fd到epfd中;
  • EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
  • EPOLL_CTL_DEL :从epfd中删除一个fd;

struct epoll_event结构分析

typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;

struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

events可以是以下几个宏的集合(常用的读写):

  • EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
  • EPOLLOUT : 表示对应的文件描述符可以写

在这里插入图片描述
函数功能
收集在epoll监控的事件中已经发送的事件

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

第二个参数和第三个参数联合起来是一个输出型参数,即:内核(OS)告诉用户:这些文件描述符上的一些事件已经就绪了,需要定义一个缓存区或者数据区把这些数据填入

3.4.3 epoll 的工作原理

  • 当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关
struct eventpoll {
  ...
  /*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,
  也就是这个epoll监控的事件*/
  struct rb_root rbr;
  /*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/
  struct list_head rdllist;
  ...
};
  • 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
  • 这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)
  • 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法
  • 这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中.
  • 在epoll中,对于每一个事件,都会建立一个epitem结构体.
struct epitem {
  ...
  //红黑树节点
  struct rb_node rbn;
  //双向链表节点
  struct list_head rdllink;
  //事件句柄等信息
  struct epoll_filefd ffd;
  //指向其所属的eventepoll对象
  struct eventpoll *ep;
  //期待的事件类型
  struct epoll_event event;
  ...
}; // 这里包含每一个事件对应着的信息。
  • 当调用epoll_wait检查是否有发生事件的连接时,只是检查eventpoll对象中的rdllist双向链表是否有epitem元素而已,如果rdllist链表不为空,则这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户。因此epoll_waitx效率非常高。epoll_ctl在向epoll对象中添加、修改、删除事件时,从rbr红黑树中查找事件也非常快,也就是说epoll是非常高效的,它可以轻易地处理百万级别的并发连接

3.4.4 epoll 的优点

  • 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
  • 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
  • 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响
  • 没有数量限制: 文件描述符数量无上限

3.4.5 epoll 的工作模式

epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。
水平触发(LT)
epoll默认状态下就是LT工作模式.

  • 当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分
  • LT(水平触发)模式下,只要这个文件描述符还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作;

边缘触发(ET)

  • ET(边缘触发)模式下,在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,让 errno 返回 EAGAIN 为止,否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。如果ET模式不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。
  • 还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

3.4.6 epoll 模型使用示例

#include "Sock.hpp"

#define SIZE 64
// 每一个sock都应该有一个bucket,包括缓冲区 buffer,读取的位置记录pos,其文件描述符
class bucket
{
    public:
        char buffer[20];
        int pos;
        int fd;
        bucket(int sock)
          :fd(sock),pos(0)
        {
            memset(buffer, 0, sizeof(buffer));
        }
        ~bucket()
        {

        }
};
// select poll 只支持水平触发 LT 
// epoll 支持水平触发 和 边缘触发(ET)
class EpollServer{
    private:
        int lsock; // 监听套接字
        int port;  // 端口号
        int epfd;  // epoll 模型的文件描述符,指向一个epoll结构体,结构体里有红黑树和双向链表,这些机制使得epoll模型非常高效
    public:
        EpollServer(int _p = 8081):port(_p)
        {}
        void InitServer()
        {
            lsock = Sock::Socket();
            Sock::Setsockopt(lsock);
            Sock::Bind(lsock, port);
            Sock::Listen(lsock);

            epfd = epoll_create(256);// 创建函数模型
            if (epfd < 0)
            {
                cerr << "epoll_create error " << endl;
                exit(5);
            }
            cout << "listen sock : " << lsock << endl;
            cout << "epoll    fd : " << epfd  << endl;
        }
        // 将套接字和事件加入到 epoll 模型中
        void AddEventToEpoll(int sock, uint32_t event)
        {
            struct epoll_event ev;
            ev.events = event;
            if (sock == lsock)
            {
                ev.data.ptr = nullptr;
            }
            else 
            {
                ev.data.ptr = new bucket(sock);
            }
            // 用epoll 模型的第二个借口 epoll_ctl 将事件和套接字加入到epoll模型中
            epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);
        }
        void DelEventFromEpoll(int sock)
        {
            close(sock);
            epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr);
        }

        void  HandlerEvents(struct epoll_event revs[],int num)
        {
            for (int i = 0; i < num; i++)
            {
                uint32_t ev = revs[i].events;
                if (ev & EPOLLIN) // 读事件就绪
                {
                    if (revs[i].data.ptr != nullptr) // 通信套接字
                    {
                         bucket *bp = (bucket*)revs[i].data.ptr;
                         // 把这个套接字的 bucket 拿出来进行读取
                         ssize_t s = recv(bp->fd, bp->buffer + bp->pos, sizeof(bp->buffer) - bp->pos, 0);
                         if (s > 0)
                         {
                            bp->pos += s;
                            cout << "client# " << bp->buffer << endl;
                            if (bp->pos >= sizeof(bp->buffer))
                            {
                                struct epoll_event temp;
                                temp.events = EPOLLOUT;
                                temp.data.ptr = bp;
                                epoll_ctl(epfd, EPOLL_CTL_MOD, bp->fd, &temp);
                            }
                         }
                         else if (s == 0)
                         {
                            // 数据读完,短连接
                            DelEventFromEpoll(bp->fd);
                            delete bp;
                         }
                         else 
                         {
                            // 读出错,抛异常什么的
                         }
                    }
                    else // 监听套接字 
                    {
                        int sock = Sock::Accept(lsock);
                        if (sock > 0)
                        {
                            AddEventToEpoll(sock, EPOLLIN); // 将新获得的通信套接字放到 epoll 模型里
                        }
                    }
                }
                else if (ev & EPOLLOUT) // 写事件就绪
                {

                }
                else 
                {
                  // 其他事件就绪
                }
            }
        }
        void Start()
        {
            // 首先将监听套接字加到epoll模型里
            // 用户告诉内核你要帮我关心这个监听套接字上是否其他连接来的事件就绪,这个事件也是读事件
            AddEventToEpoll(lsock, EPOLLIN);
            int timeout = -1; // 阻塞等
            struct epoll_event revs[SIZE];// 等待就绪事件放到这个数组中
            for (;;)
            {
                int num = epoll_wait(epfd, revs, SIZE, timeout);
                switch(num)
                {
                    case 0:
                      cout << "time out ..." << endl;
                      break;
                    case -1:
                      cerr << "epoll_wait error" << endl;
                      break;
                    default:
                      HandlerEvents(revs,num);
                      break;
                }
            }
        }
        ~EpollServer()
        {
            close(lsock);
            close(epfd);
        }
};

完整代码请参考:https://github.com/CZH214926/C_repo/commit/2a93605e0b8d5c7843340ead3100ad460856c273

3.4.7 epoll 的使用场景

epoll的高性能, 是有一定的特定场景的, 如果场景选择的不适宜, epoll的性能可能适得其反
对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll

  • 例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联网APP的入口服务器, 这样的服务器就很适合epoll.如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接, 这种情况下用epoll就并不合适. 具体要根据需求和场景特点来决定使用哪种IO模型。

参考资料:https://www.cnblogs.com/yungyu16/p/13066954.html

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
操作系统是计算机系统中的核心组成部分,负责管理和协调计算机硬件和软件资源,提供程序运行环境。在CSDN上有很多关于操作系统的专题文章,以下将从操作系统的基本概念、功能和常见类型等方面简要介绍一下。 首先是操作系统的基本概念。操作系统是一种系统软件,它是计算机硬件和应用软件之间的桥梁,提供给应用程序一系列的服务和资源,同时负责调度和管理系统资源。它为用户屏蔽了底层的硬件差异,提供了一个统一的、易于使用的界面。 操作系统主要有四个基本功能。首先是处理器管理,负责将处理器分配给系统中的各个进程,并进行进程切换,实现多道程序并发执行。其次是内存管理,管理计算机的内存资源,包括分配、回收和保护等操作。再次是文件管理,负责管理文件的存储、命名和保护等操作,提供了文件操作的接口。最后是设备管理,负责管理计算机的各种设备,包括输入输出设备和存储设备等。 常见的操作系统有多种类型。最主流的是Windows、Linux和Mac OS等桌面操作系统。此外还有服务器操作系统,如Windows Server和Linux等,用于管理和部署服务器。还有嵌入式操作系统,如Android和iOS等,用于移动设备和物联网设备。操作系统也有实时操作系统,用于需要实时控制和响应的系统,如工控系统和航空航天系统等。 总之,操作系统是计算机系统中不可或缺的重要组成部分,通过CSDN上的相关文章,我们可以更深入了解操作系统的基本概念、功能和不同类型。这些知识对于理解计算机系统的工作原理和提升编程能力都有着重要意义。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值