网络 :高级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:如调用
- 异步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的出现