ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

网络 :高级IO【select,poll】

2026/8/8 11:22:35 拓冰建站 浏览量
网络 :高级IO【select,poll】

网络 :高级IO

  • 一、五种IO模型
      • **1. 阻塞 I/O (Blocking I/O)**
      • **2. 非阻塞 I/O (Non-blocking I/O)**
      • **3. I/O 多路复用 (I/O Multiplexing)**
      • **4. 信号驱动 I/O (Signal-driven I/O)**
      • **5. 异步 I/O (Asynchronous I/O, AIO)**
      • 同步和异步IO的区别
      • **五种模型对比总结**
  • 二、非堵塞I/O
    • 2.1 fcntl函数
    • 2.2 设置非堵塞示例
  • 三、 I/O多路转接之select
    • 3.1 select函数
      • 关于fd_set结构
      • 关于timeval结构
    • 3.2 socket就绪条件
    • 3.3 select的特点
    • 3.4 简易的select版本的TCP服务器
      • Log.hpp 日志
      • Socket.hpp 封装套接字接口
      • SelectServer.hpp
  • 四、 I/O多路转接之poll
    • 4.1 poll函数
    • 4.2 poll的特点
    • 4.3 简易的poll版本的TCP服务器

一、五种IO模型

任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往

往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少.
以下是五种 I/O 模型的详细对比及适用场景分析,结合系统调用流程和编程特点进行说明:


1. 阻塞 I/O (Blocking I/O)

  • 工作机制
    用户进程发起 I/O 请求后完全阻塞,直到内核完成数据准备和复制(数据从内核缓冲区→用户空间)。
    在这里插入图片描述

    伪代码示例:

    data = read(socket_fd);  // 进程在此阻塞等待
    process(data);           // 数据就绪后才执行
    
  • 特点

    • 同步阻塞,简单易用
    • CPU 利用率低(进程长时间休眠)
  • 适用场景
    单线程程序、低并发请求(如 CLI 工具)。


2. 非阻塞 I/O (Non-blocking I/O)

  • 工作机制
    用户进程轮询检查 I/O 状态,未就绪时立即返回错误(如 EWOULDBLOCK),避免阻塞。非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用。
    在这里插入图片描述

    伪代码示例:

    while (true) {data = read(socket_fd);if (data != ERROR) {  // 数据就绪process(data);break;}// 未就绪时执行其他任务(如 run_user_thread())
    }
    
  • 特点

    • 同步非阻塞,需主动轮询
    • 减少阻塞时间,但轮询消耗 CPU
  • 适用场景
    低延迟应用(如游戏服务器),需避免长时间阻塞。


3. I/O 多路复用 (I/O Multiplexing)

  • 工作机制
    通过 select/poll/epoll 监听多个 socket,当任一 socket 数据就绪时通知进程。
    在这里插入图片描述

    流程:

    while (true) {ready_fds = select(fds);  // 阻塞直到有就绪的fdfor (fd in ready_fds) {data = read(fd);      // 非阻塞读取process(data);}
    }
    
  • 特点

    • 同步阻塞在 select,但单线程管理多连接
    • 减少线程数,提升高并发性能
  • 适用场景
    高并发服务器(如 Nginx、Redis),连接数 >1000 的场景。


4. 信号驱动 I/O (Signal-driven I/O)

  • 工作机制
    内核通过信号(如 SIGIO)通知进程数据就绪,进程再读取数据。 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作
    在这里插入图片描述

  • 特点

    • 异步通知,减少轮询开销
    • 编程复杂(信号处理需谨慎)
  • 适用场景
    嵌入式系统、UDP 服务(如 DNS 服务器)。


5. 异步 I/O (Asynchronous I/O, AIO)

  • 工作机制
    用户进程发起 aio_read() 后立即返回;内核完成数据准备+复制后,通过回调函数通知进程。 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).
    在这里插入图片描述

    伪代码示例:

    aio_read(socket_fd, buffer, callback); // 立即返回
    // 进程继续执行其他任务
    void callback() {                       // 内核完成后触发process(buffer);
    }
    
  • 特点

    • 真正异步,进程全程无阻塞
    • 编程复杂(需事件循环和回调)
  • 适用场景
    高吞吐系统(如数据库 InnoDB 引擎、大文件传输)。


同步和异步IO的区别

同步IO和异步IO是计算机系统中处理输入输出(I/O)操作的两种基本模式,它们在CPU参与方式、阻塞行为和应用场景上存在本质差异。下面我将逐步解释它们的区别、工作机制,并基于实际因素分析适用场景,确保回答清晰可靠。内容参考了权威资料。

  • 同步IO:用户进程(或线程)触发I/O操作后,必须等待或轮询操作完成,才能继续执行后续代码。CPU在操作过程中被主动占用,导致进程可能被阻塞。例如:
    • 同步阻塞IO:如调用 read() 时,如果内核缓冲区无数据,进程会完全停止,直到数据就绪。
    • 同步非阻塞IO:进程通过循环轮询检查I/O状态,避免完全阻塞,但浪费CPU资源。
  • 异步IO:用户进程触发I/O操作后立即返回,不等待结果;内核在后台完成操作后,通过回调、信号或事件通知进程。CPU在等待期间不被占用,进程可执行其他任务。
  • 核心区别:在于CPU的参与方式:
    • 同步IO中,CPU由用户进程主动控制,必须等待或轮询I/O完成。
    • 异步IO中,CPU将控制权交给内核,用户进程被动接收通知,实现非阻塞。

关键区别

  • 同步 I/O:数据就绪后仍需进程主动复制数据(阻塞/非阻塞 I/O、I/O 多路复用、信号驱动 I/O)。
  • 异步 I/O:内核完成数据准备+复制后通知进程(全程无参与)。

我们回忆在讲多进程多线程的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不想干的概念.

  • 进程/线程同步也是进程/线程之间直接的制约关系
  • 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候

五种模型对比总结

I/O 模型阻塞行为同步/异步CPU 利用率编程复杂度适用场景
阻塞 I/O全程阻塞同步简单单线程/低并发程序
非阻塞 I/O轮询时非阻塞同步中(有浪费)中等低延迟应用
I/O 多路复用阻塞在 select同步中等高并发服务器(Nginx/Redis)
信号驱动 I/O非阻塞(信号通知)异步复杂嵌入式/UDP 服务
异步 I/O (AIO)全程非阻塞异步最高复杂数据库/大文件传输

二、非堵塞I/O

2.1 fcntl函数

一个文件描述符, 默认都是阻塞IO,而我们可以通过fcntl函数设置成为非堵塞。
函数原型如下:

#include <unistd.h>
#include <fcntl.h>int fcntl(int fd, int cmd, ... /* arg */ );

传入的cmd的值不同, 后面追加的参数也不相同.

fcntl函数有5种功能:

  • 复制一个现有的描述符(cmd=F_DUPFD).
  • 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
  • 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
  • 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
  • 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).

2.2 设置非堵塞示例

#include <unistd.h>
#include <fcntl.h>
#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;void SetNonBlock(int fd)
{int status = fcntl(fd, F_GETFL);if (status < 0){perror("fcntl");return; // 出错直接退出}fcntl(fd, F_SETFL, status | O_NONBLOCK);cout << fd << " set nonblock done" << endl;
}int main()
{char buffer[1024];SetNonBlock(0); // 设置非堵塞while (true){ssize_t n = read(0, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;cout << buffer << endl;}else if (n == 0) // 说明读到文件尾EOF,或者socket的对端关闭。{cout << "read done" << endl;break;}else // n < 0{// 两种情况 1.出错   2. 没有就绪if (errno == EWOULDBLOCK){cout << "data not ready " << endl;// do_other_things()sleep(1);}else{cerr << "read error n: " << n << "errno code :"<< errno << " ,strerror:" << strerror(errno) << endl;}}}return 0;
}

三、 I/O多路转接之select

  I/O 多路复用是一种单线程高效管理多个 I/O 操作的核心技术,通过系统调用(如 select/poll/epoll)集中监控多个文件描述符的状态,当任意一个描述符就绪时通知程序处理,避免轮询开销。


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

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

3.1 select函数

函数原型如下:

#include <sys/select.h> 
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

参数解释:

  • 参数nfds是需要监视的最大的文件描述符值+1;
  • readfds,writefds,exceptfds 分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合;
  • 参数timeout为结构timeval,用来设置select()的等待时间
      参数timeout取值:
    • NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
    • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
    • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

函数返回值:

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

错误值可能为:

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

关于fd_set结构

在这里插入图片描述
其实这个结构就是一个整数数组, 更严格的说, 是一个 “位图”. 使用位图中对应的位来表示要监视的文件描述符.

提供了一组操作fd_set的接口, 来比较方便的操作位图.

void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

关于timeval结构

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
在这里插入图片描述

3.2 socket就绪条件

读就绪

  • socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
  • 监听的socket上有新的连接请求;
  • socket上有未处理的错误;

写就绪

  • socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
  • socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
  • socket使用非阻塞connect连接成功或失败之后;
  • socket上有未读取的错误;
    异常就绪
  • socket上收到带外数据. 关于带外数据, 和TCP紧急模式相关(回忆TCP协议头中, 有一个紧急指针的字段)

3.3 select的特点

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

备注: fd_set的大小可以调整,可能涉及到重新编译内核. 感兴趣的同学可以自己去收集相关资料.

select缺点

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

3.4 简易的select版本的TCP服务器

Log.hpp 日志

#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// PrintMethod
#define Screen 1
#define Onefile 2
#define Muchfile 3// leve,指的是日志等级,等级不同处理的方式也不同
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4#define LogFile "log.txt"
class Log
{
public:Log(){path = "./log/";_PrintMethod = Screen;}// 用户指定打印方式void AppontPrint(int PrintMethod){_PrintMethod = PrintMethod;}std::string levelToString(int level){switch (level){case Info:return "Info";case Debug:return "Debug";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}}void printLog(int level, const std::string &logtxt){switch (_PrintMethod){case Screen:std::cout << logtxt << std::endl;break;case Onefile:printOneFile(LogFile, logtxt);break;case Muchfile:printClassFile(level, logtxt);break;default:break;}}void printOneFile(const std::string &logname, const std::string &logtxt){std::string filename = path + logname;int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);if(fd < 0){return;}int n = write(fd,logtxt.c_str(),logtxt.size());close(fd);}void printClassFile(int level, const std::string &logtxt){std::string filename = LogFile;filename += ".";filename += levelToString(level);   // "logtxt.Info/Fatal"printOneFile(filename,logtxt);}void operator()(int level, const char *format, ...){// 自定义部分time_t t = time(nullptr);struct tm *ctime = localtime(&t);char leftbuffer[1024];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,ctime->tm_hour, ctime->tm_min, ctime->tm_sec);// 默认添加部分va_list s;va_start(s, format);char rightbuffer[1024];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);char logtxt[2024];snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);printLog(level, logtxt);}~Log(){}private:std::string path; // 将路径信息打印到某个路径文件下int _PrintMethod; // 打印的方法(打印到屏幕或文件或多个文件等)
};

Socket.hpp 封装套接字接口

#pragma Once
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>#include "Log.hpp"Log lg;enum
{SOCK_ERR = 1,BIND_ERR,LISTEN_ERR,};class Sock
{
public:void Socket(){socketfd = socket(AF_INET, SOCK_STREAM, 0);if (socketfd < 0){lg(Fatal, "socket errno : %d ,%s", errno, strerror(errno));exit(SOCK_ERR);}int opt = 1;     //实现端口复用setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));}void Bind(uint16_t &port){struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if (bind(socketfd, (const struct sockaddr *)&local, sizeof(local)) < 0){lg(Fatal, "bind errno : %d ,%s", errno, strerror(errno));exit(BIND_ERR);}}void Listen(){int n = listen(socketfd, 10);if (n < 0){lg(Fatal, "listen errno : %d ,%s", errno, strerror(errno));exit(LISTEN_ERR);}}int Accept(std::string *clientip, uint16_t *clientport){struct sockaddr_in remote;socklen_t len = sizeof(remote);int newfd = accept(socketfd, (struct sockaddr *)&remote, &len);if (newfd < 0){lg(Warning, "accept errno : %d ,%s", errno, strerror(errno));return -1;}char buffip[64];inet_ntop(AF_INET, &remote.sin_addr, buffip, sizeof(buffip));*clientip = buffip;*clientport = ntohs(remote.sin_port);return newfd;}bool Connect(std::string &serverip, uint16_t &serverport){struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);int n = connect(socketfd, (const struct sockaddr *)&server, sizeof(server));if (n < 0){lg(Warning, "connect errno : %d ,%s", errno, strerror(errno));return false;}return true;}int Getfd(){return socketfd;}void Close(){close(socketfd);}private:int socketfd;
};

SelectServer.hpp

#pragma once
#include <iostream>
#include "Socket.hpp"
#include <sys/select.h>using namespace std;
static const uint16_t defaultport = 8080;
static const int fd_num_max = (sizeof(fd_set) * 8); // 最多可以就绪的fdclass SelectServer
{
public:SelectServer(uint16_t port = defaultport): _port(port){for (int n = 0; n < fd_num_max; n++) // 将fd_array初始化为-1{fd_array[n] = -1;}}void Init(){_LisSock.Socket();_LisSock.Bind(_port);_LisSock.Listen();}void Accept(){// accpetstring clientip;uint16_t clientport;int sockfd = _LisSock.Accept(&clientip, &clientport);if (sockfd < 0)return;lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sockfd);// 将新到来的fd纳入到fd_arrayint pos = 1;for (; pos < fd_num_max; pos++) // 找到fd_array中空闲的位置{if (fd_array[pos] != -1)continue;elsebreak;}if (pos == fd_num_max){lg(Warning, "fd_array is full , close &d", sockfd);close(sockfd);}else{fd_array[pos] = sockfd;}}void Recv(int fd, int pos){char buffer[1024];int n = read(fd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;cout << "Get a message: " << buffer << endl;}else if (n == 0) // read函数返回0表示已到达文件末尾(End of File,EOF) 或对端关闭了连接(针对管道/套接字等){lg(Info, "client quit, me too, close fd is : %d", fd); // 这里我们认为是对端关闭了连接close(fd);fd_array[pos] = -1; // 这里本质是从select中移除}else{lg(Warning, "recv error: fd is : %d", fd);close(fd);fd_array[pos] = -1; // 这里本质是从select中移除}}// 处理就绪事件void Dispatcher(fd_set &rfds){// fd_set 是输入输出型参数for (int i = 0; i < fd_num_max; i++) // 处理就绪事件{if (fd_array[i] == -1)continue;if (FD_ISSET(fd_array[i], &rfds)){if (fd_array[i] == _LisSock.Getfd()){// 如果是监听fd就绪,则表示全连接队列中存在fd,因此我们要将该fd accept,并且纳入到fd_array。Accept();}else // 普通fd就绪{Recv(fd_array[i], i);}}}}void Start(){fd_array[0] = _LisSock.Getfd();while (true){fd_set rfds;FD_ZERO(&rfds);int maxfd = fd_array[0];for (int n = 0; n < fd_num_max; n++){if (fd_array[n] == -1)continue;FD_SET(fd_array[n], &rfds); // 将已经开辟好的fd 放入到 rfs当中if (maxfd < fd_array[n])    // 找出最大的maxfd{maxfd = fd_array[n];lg(Info, "max fd update, max fd is: %d", maxfd);}}// 先selectstruct timeval timeout = {2, 0};int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout);switch (n){case 0:cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;break;case -1:cerr << "select error" << endl;break;default:// 处理就绪事件cout << "get a new link!!!!!" << endl;Dispatcher(rfds);break;}}}~SelectServer(){_LisSock.Close();}private:Sock _LisSock;            // 监听uint16_t _port;           // 端口号int fd_array[fd_num_max]; // 数组,用于存储已经创立好的fd
};

SelectServer.cc

#include "SelectServer.hpp"
#include <memory>
int main()
{std::unique_ptr<SelectServer> svr(new SelectServer());svr->Init();svr->Start();return 0;
}

SelectServer.hpp中的 Dispatcher函数对于读写事件的处理是非常简易的,该服务器的重点实在理解感受,每次调用select, 都需要手动设置fd集合, 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大,同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大以及select支持的文件描述符数量太小。

我们控制fd集合是非常不便的。

四、 I/O多路转接之poll

4.1 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 */
};

返回结果

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

参数说明

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

events和revents的取值:
在这里插入图片描述

socket就绪条件
同select


4.2 poll的特点

poll的优点

  • 不同与select fdset的方式,poll使用一个pollfd的指针实现.使用三个位图来表示三个pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比select更方便.
  • poll并没有最大数量限制 (但是数量过大后性能也是会下降).

poll的缺点

  • poll中监听的文件描述符数目增多时和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符.
  • 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
  • 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.

4.3 简易的poll版本的TCP服务器

该简易服务器的都是从上面的select基础上改变而来,Log.hpp 和Socket.hpp文件都是一样的。

PollServer.hpp

#pragma once
#include <iostream>
#include "Socket.hpp"
#include<poll.h>
using namespace std;static const uint16_t defaultport = 8080;
static const int fd_num_max = 64; // 最多可以就绪的fdclass PollServer
{
public:PollServer(uint16_t port = defaultport): _port(port){for (int n = 0; n < fd_num_max; n++) // 将_event_fds初始化{_event_fds[n].fd = -1;_event_fds[n].events = 0;_event_fds[n].revents = 0;}}void Init(){_LisSock.Socket();_LisSock.Bind(_port);_LisSock.Listen();}void Accept(){// accpetstring clientip;uint16_t clientport;int sockfd = _LisSock.Accept(&clientip, &clientport);if (sockfd < 0)return;lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sockfd);// 将新到来的fd纳入到_event_fdsint pos = 1;for (; pos < fd_num_max; pos++) // 找到_event_fds中空闲的位置{if (_event_fds[pos].fd != -1)continue;elsebreak;}if (pos == fd_num_max){lg(Warning, "_event_fds is full , close &d", sockfd);close(sockfd);}else{_event_fds[pos].fd = sockfd;_event_fds[pos].events = POLLIN;_event_fds[pos].revents = 0;}}void Recv(int fd,int pos){char buffer[1024];int n = read(fd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;cout << "Get a message: " << buffer << endl;}else if (n == 0) // read函数返回0表示已到达文件末尾(End of File,EOF) 或对端关闭了连接(针对管道/套接字等){lg(Info, "client quit, me too, close fd is : %d", fd); // 这里我们认为是对端关闭了连接close(fd);_event_fds[pos].fd = -1; // 这里本质是从_event_fds中移除}else{lg(Warning, "recv error: fd is : %d", fd);close(fd);_event_fds[pos].fd = -1; // 这里本质是从_event_fds中移除}}// 处理就绪事件void Dispatcher(){// fd_set 是输入输出型参数for (int i = 0; i < fd_num_max; i++) // 处理就绪事件{if (_event_fds[i].fd == -1)continue;if (_event_fds[i].revents & POLLIN){if (_event_fds[i].fd == _LisSock.Getfd()){// 如果是监听fd就绪,则表示全连接队列中存在fd,因此我们要将该fd accept,并且纳入到_event_fds。Accept();}else // 普通fd就绪{Recv(_event_fds[i].fd,i);}}}}void Start(){_event_fds[0].fd = _LisSock.Getfd();_event_fds[0].events = POLLIN;while (true){// 先pollint n = poll(_event_fds,fd_num_max,2000);switch (n){case 0:cout << "time out " << endl;break;case -1:cerr << "select error" << endl;break;default:// 处理就绪事件cout << "get a new link!!!!!" << endl;Dispatcher();break;}}}~PollServer(){_LisSock.Close();}private:Sock _LisSock;            // 监听uint16_t _port;           // 端口号struct pollfd _event_fds[fd_num_max]; 
};

PollServer.cc

#include "PollServer.hpp"
#include <memory>
int main()
{std::unique_ptr<PollServer> svr(new PollServer());svr->Init();svr->Start();return 0;
}

通过上面的服务器代码我们可以清晰的感受到poll版本的服务器对于select版本的优势,但是同样它还是存在缺点的, poll中监听的文件描述符数目增多时和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符,每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中。


因此有了epoll的出现