目录
[3.1. select:最早的多路复用接口](#3.1. select:最早的多路复用接口)
[3.2. poll:select的改进版](#3.2. poll:select的改进版)
[3.3. epoll:Linux的高性能解决方案](#3.3. epoll:Linux的高性能解决方案)
[4.1.1.select API详解](#4.1.1.select API详解)
[4.2.poll API详解(- select的改进版)](#4.2.poll API详解(- select的改进版))
[4.3.1. epoll API详解](#4.3.1. epoll API详解)
在构建高性能网络服务器时,一个核心挑战是如何高效地处理大量并发连接 。传统的"一线程/进程一连接"模型在连接数激增时会迅速耗尽系统资源。此时,IO多路复用技术便成为解决这一难题的核心技术之一。它允许单个线程/进程同时监控多个IO描述符(如socket),极大地提升了资源利用率和程序性能。
1.并发处理的必要性
在深入技术细节前,先明确几个基本概念:
-
并发:指服务器能够同时处理多个客户端请求的能力。就像一家餐厅需要同时服务多桌客人一样,服务器也需要同时应对多个连接。
-
协议差异:
-
UDP:无连接协议,每个数据报独立处理,天然无连接状态,无需专门的并发机制
-
TCP:面向连接协议,每个连接都是长期状态,必须实现并发处理才能服务多个客户端
-
-
传统并发方案的局限:
早期常用多进程/多线程模型(每个连接创建一个线程/进程),虽实现简单但存在明显缺点:
-
创建/销毁开销大(比如创建一个线程要8M栈区,最多可创建三四百个线程)
-
上下文切换成本高
-
内存消耗随连接数线性增长
-
受操作系统线程数限制
-
-
多线程实现TCP并发服务器测试示例如下:
cpp
#服务端
#include "head.h"
int CreateListenSocket(const char *pip, int port)
{
int ret = 0;
int sockfd = 0;
struct sockaddr_in seraddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("fail to socket");
return -1;
}
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(port);
seraddr.sin_addr.s_addr = inet_addr(pip);
ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (-1 == ret)
{
perror("fail to bind");
return -1;
}
ret = listen(sockfd, 10);
if (-1 == ret)
{
perror("fail to listen");
return -1;
}
return sockfd;
}
void *threadfun(void *arg)
{
char tmpbuff[4096] = {0};
ssize_t nret = 0;
int confd = (int)arg;
while (1)
{
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = recv(confd, tmpbuff, sizeof(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to recv");
return NULL;
}
else if (0 == nret)
{
printf("关闭连接\n");
break;
}
printf("RECV:%s\n", tmpbuff);
sprintf(tmpbuff, "%s --- echo", tmpbuff);
nret = send(confd, tmpbuff, strlen(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to send");
return NULL;
}
}
close(confd);
return NULL;
}
int main(void)
{
int sockfd = 0;
int confd = 0;
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
sockfd = CreateListenSocket("192.168.0.118", 50000);
while (1)
{
confd = accept(sockfd, NULL, NULL);
if (-1 == confd)
{
perror("fail to accept");
return -1;
}
pthread_create(&tid, &attr, threadfun, (void *)confd);
}
close(sockfd);
pthread_attr_destroy(&attr);
return 0;
}
cpp
#客户端代码
#include "head.h"
int main(void)
{
int sockfd = 0;
int ret = 0;
struct sockaddr_in seraddr;
char tmpbuff[4096] = {0};
int cnt = 0;
ssize_t nret = 0;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("fail to socket");
return -1;
}
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.0.118");
ret = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (-1 == ret)
{
perror("fail to connect");
return -1;
}
while (1)
{
sprintf(tmpbuff, "hello world ---------- %d", cnt);
cnt++;
nret = send(sockfd, tmpbuff, strlen(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to send");
return -1;
}
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = recv(sockfd, tmpbuff, sizeof(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to recv");
return -1;
}
else if (0 == nret)
{
printf("关闭连接\n");
break;
}
printf("RECV:%s\n", tmpbuff);
}
close(sockfd);
return 0;
}
2.Linux系统的四种IO模型
2.1四种基础IO模型
在讨论多路复用前,先了解Linux提供的四种基础IO模型:
-
阻塞IO(block):进程发起IO请求后一直等待,直到数据就绪
-
非阻塞IO(nonblock):应用层向内核层读数据,不管有没有数据都返回相应的结果,不会阻塞等待,所以得通过轮询检查状态
-
异步信号驱动IO(async):内核在数据就绪时发送信号通知进程(类似中断)
-
多路复用IO:内核完成整个IO操作后通知进程(由一个函数接口监听多个连接,类似于ADC,服务器都用它)
前三种都只能处理一对一,只有多路复用IO可以处理一对多,它属于前两种模型的增强,它让单个进程能同时等待多个文件描述符的就绪事件。
2.2示例
阻塞IO
cpp
//客户端
#include "head.h"
int main(void)
{
int fd = 0;
char tmpbuff[4096] = {0};
mkfifo("/tmp/myfifo", 0664);
fd = open("/tmp/myfifo", O_WRONLY);
if (-1 == fd)
{
perror("fail to open");
return -1;
}
while (1)
{
memset(tmpbuff, 0, sizeof(tmpbuff));
gets(tmpbuff);
write(fd, tmpbuff, strlen(tmpbuff));
}
close(fd);
return 0;
cpp
//服务端
#include "head.h"
int main(void)
{
int fd = 0;
char tmpbuff[4096] = {0};
mkfifo("/tmp/myfifo", 0664);
fd = open("/tmp/myfifo", O_RDONLY);
if (-1 == fd)
{
perror("fail to open");
return -1;
}
while (1)
{
memset(tmpbuff, 0, sizeof(tmpbuff));
read(fd, tmpbuff, sizeof(tmpbuff)); //阻塞等待
printf("FIFO:%s\n", tmpbuff);
memset(tmpbuff, 0, sizeof(tmpbuff));
gets(tmpbuff); //阻塞等待
printf("STDIN:%s\n", tmpbuff);
}
close(fd);
return 0;
}
非阻塞IO
cpp
//服务端
#include "head.h"
int main(void)
{
int fd = 0;
int flags = 0;
char tmpbuff[4096] = {0};
ssize_t nret = 0;
char *pret = NULL;
mkfifo("/tmp/myfifo", 0664);
fd = open("/tmp/myfifo", O_RDONLY);
if (-1 == fd)
{
perror("fail to open");
return -1;
}
flags = fcntl(fd, F_GETFL);
flags |= O_NONBLOCK;
fcntl(fd, F_SETFL, flags);
flags = fcntl(0, F_GETFL);
flags |= O_NONBLOCK;
fcntl(0, F_SETFL, flags);
while (1)
{
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = read(fd, tmpbuff, sizeof(tmpbuff));
if (nret > 0)
{
printf("FIFO:%s\n", tmpbuff);
}
memset(tmpbuff, 0, sizeof(tmpbuff));
pret = gets(tmpbuff);
if (pret != NULL)
{
printf("STDIN:%s\n", tmpbuff);
}
}
close(fd);
return 0;
}
异步信号驱动IO
cpp
//服务端
#include "head.h"
int fd = 0;
void handler(int signo)
{
char tmpbuff[4096] = {0};
memset(tmpbuff, 0, sizeof(tmpbuff));
read(fd, tmpbuff, sizeof(tmpbuff));
printf("FIFO:%s\n", tmpbuff);
return;
}
int main(void)
{
int flags = 0;
char tmpbuff[4096] = {0};
ssize_t nret = 0;
char *pret = NULL;
signal(SIGIO, handler);
mkfifo("/tmp/myfifo", 0664);
fd = open("/tmp/myfifo", O_RDONLY);
if (-1 == fd)
{
perror("fail to open");
return -1;
}
flags = fcntl(fd, F_GETFL);
flags |= O_ASYNC;
//设置fd为异步IO(当有IO事件发生时,内核会发送SIGIO异步信号通知应用层)
fcntl(fd, F_SETFL, flags);
//设置fd对应的异步IO信号由当前进程接收
fcntl(fd, F_SETOWN, getpid());
while (1)
{
memset(tmpbuff, 0, sizeof(tmpbuff));
gets(tmpbuff);
printf("STDIN:%s\n", tmpbuff);
}
close(fd);
return 0;
}
3.IO多路复用核心机制
IO多路复用的核心思想是:通过一个函数接口同时监听多个文件描述符的IO事件,当任一描述符就绪时,函数则不再阻塞,程序即可处理对应事件。------类似中断处理
Linux提供了三种主要实现,各有特点:
select
poll
apoll
3.1. select:最早的多路复用接口
工作原理:创建文件描述符集合,把所有需要监听的文件描述符放入,用一个函数来监听,只要有一个事件发生,就不阻塞,把产生事件的文件描述符留在集合中,没有产生的则踢出去(补充:文件描述符的规律--新文件描述符总选择最小的可用的非负整数。系统中预定义的标准文件描述符:0为stdin,1为stdout,2为stderr)
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
select通过三个位图集合(读、写、异常)管理文件描述符,监控其状态变化。
四大局限性:
-
数量限制 :监听的描述符数有上限(通常1024),由FD_SETSIZE宏定义
-
性能开销 :select监听的文件描述符集合在应用层,当事件发生需要产生一次内核层向应用层数据的拷贝,会产生资源开销
-
触发模式单一 :仅支持水平触发模式(Level Triggered)(低速模式),即只要可读/写就不断通知

-
效率问题 :每次调用后需遍历整个集合找出就绪的描述符,时间复杂度O(n)
3.2. poll:select的改进版
工作原理:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
poll使用pollfd结构体数组替代select的位图,消除了描述符数量限制。
相对于select的改进:
-
✅ 描述符数量无硬性上限
-
❌ 仍需每次调用时传递整个描述符数组
-
❌ 仍需遍历所有描述符查找就绪事件
-
❌ 同样只支持水平触发模式
3.3. epoll:Linux的高性能解决方案
设计优势:
-
无数量限制:仅受系统最大文件描述符数限制
-
高效内核数据结构:在内核维护事件表,避免用户-内核空间的数据拷贝
-
双触发模式:支持水平触发(默认)和边沿触发(通过EPOLLET标志设置)
-
精准事件返回 :只返回就绪的描述符,无需遍历整个集合
4.相关函数接口
4.1.1.select API详解
fcntl - 文件描述符控制
fcntl函数用于对已打开的文件描述符进行各种控制操作,在多路复用中常用于设置非阻塞模式。
#include <unistd.h> #include <fcntl.h> int fcntl(int fd, int cmd, ... /* arg */ );功能
向文件描述符发送cmd控制命令,可获取或设置文件描述符的属性。
参数说明
fd:要操作的文件描述符
cmd:控制命令,常用值包括:
F_GETFL:获取文件描述符的状态标志
F_SETFL:设置文件描述符的状态标志arg:可变参数,根据cmd的不同而不同
常用操作示例
// 获取文件描述符当前标志 int flags = fcntl(fd, F_GETFL, 0); // 设置为非阻塞模式 fcntl(fd, F_SETFL, flags | O_NONBLOCK); // 取消非阻塞模式 fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);返回值
成功:根据cmd不同返回不同值
F_GETFL:返回文件状态标志
F_SETFL:返回0失败:返回-1,并设置errno
select - 传统多路复用接口
select是最早的IO多路复用实现,可同时监控多个文件描述符的读、写和异常事件。
#include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);功能
监听指定文件描述符集合中是否有事件发生(可读、可写或异常)。
参数说明
nfds :最大文件描述符值加1,即
max(fd1, fd2, ...) + 1readfds:读文件描述符集合(传入要监听的可读fd,返回可读的fd)
writefds:写文件描述符集合(传入要监听的可写fd,返回可写的fd)
exceptfds:异常文件描述符集合(传入要监听的异常fd,返回异常的fd)
timeout:超时时间
NULL:一直阻塞,直到有事件发生
0:立即返回,不阻塞具体值:等待指定时间
返回值
成功返回产生事件的文件描述符个数
失败返回-1
如果超时仍然没有事件发生返回0
辅助函数
void FD_ZERO(fd_set *set); // 清空文件描述符集合 void FD_SET(int fd, fd_set *set); // 将fd加入集合 void FD_CLR(int fd, fd_set *set); // 从集合中删除fd int FD_ISSET(int fd, fd_set *set); // 判断fd是否在集合中基本使用流程
fd_set readfds; struct timeval timeout; int max_fd = 0; // 1. 清空集合 FD_ZERO(&readfds); // 2. 添加要监控的文件描述符 FD_SET(sock_fd1, &readfds); FD_SET(sock_fd2, &readfds); max_fd = (sock_fd1 > sock_fd2) ? sock_fd1 : sock_fd2; // 3. 设置超时(可选) timeout.tv_sec = 5; // 5秒 timeout.tv_usec = 0; // 4. 调用select int ret = select(max_fd + 1, &readfds, NULL, NULL, &timeout); // 5. 检查哪些描述符就绪 if (FD_ISSET(sock_fd1, &readfds)) { // sock_fd1可读 }返回值
成功:返回就绪的文件描述符总数
超时:返回0
错误:返回-1,并设置errno
注意事项
select返回后,未就绪的描述符会被从集合中清除
每次调用前需要重新设置描述符集合
描述符集合大小受
FD_SETSIZE限制(通常1024)
4.1.2示例
cpp
//客户端
#include "head.h"
int main(void)
{
int sockfd = 0;
int ret = 0;
struct sockaddr_in seraddr;
char tmpbuff[4096] = {0};
int cnt = 0;
ssize_t nret = 0;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("fail to socket");
return -1;
}
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.0.118");
ret = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (-1 == ret)
{
perror("fail to connect");
return -1;
}
while (1)
{
sprintf(tmpbuff, "hello world ---------- %d", cnt);
cnt++;
nret = send(sockfd, tmpbuff, strlen(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to send");
return -1;
}
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = recv(sockfd, tmpbuff, sizeof(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to recv");
return -1;
}
else if (0 == nret)
{
printf("关闭连接\n");
break;
}
printf("RECV:%s\n", tmpbuff);
}
close(sockfd);
return 0;
}
cpp
//服务端
#include "head.h"
int CreateListenSocket(const char *pip, int port)
{
int ret = 0;
int sockfd = 0;
struct sockaddr_in seraddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("fail to socket");
return -1;
}
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(port);
seraddr.sin_addr.s_addr = inet_addr(pip);
ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (-1 == ret)
{
perror("fail to bind");
return -1;
}
ret = listen(sockfd, 10);
if (-1 == ret)
{
perror("fail to listen");
return -1;
}
return sockfd;
}
int main(void)
{
int sockfd = 0;
int confd = 0;
ssize_t nret = 0;
char tmpbuff[4096] = {0};
fd_set rdfds;
fd_set tmpfds;
int maxfd = 0;
int ret = 0;
int i = 0;
sockfd = CreateListenSocket("192.168.0.118", 50000);
FD_ZERO(&rdfds);
FD_SET(sockfd, &rdfds);
maxfd = sockfd;
while (1)
{
tmpfds = rdfds;
ret = select(maxfd+1, &tmpfds, NULL, NULL, NULL);
if (-1 == ret)
{
perror("fail to select");
return -1;
}
if (FD_ISSET(sockfd, &tmpfds))
{
confd = accept(sockfd, NULL, NULL);
if (-1 == confd)
{
perror("fail to accept");
FD_CLR(sockfd, &rdfds);
close(sockfd);
continue;
}
FD_SET(confd, &rdfds);
maxfd = maxfd > confd ? maxfd : confd;
}
for (i = 4; i <= maxfd; i++)
{
if (FD_ISSET(i, &tmpfds))
{
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = recv(i, tmpbuff, sizeof(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to recv");
FD_CLR(i, &rdfds);
close(i);
continue;
}
else if (0 == nret)
{
printf("关闭连接\n");
FD_CLR(i, &rdfds);
close(i);
break;
}
printf("RECV:%s\n", tmpbuff);
sprintf(tmpbuff, "%s --- echo", tmpbuff);
nret = send(i, tmpbuff, strlen(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to send");
FD_CLR(i, &rdfds);
close(i);
continue;
}
}
}
}
close(sockfd);
return 0;
}
4.2.poll API详解(- select的改进版)
poll解决了select的文件描述符数量限制问题,使用结构体数组替代位图。
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout);功能
监听事件集合中的事件是否发生。
参数说明
fds:pollfd结构体数组的首地址
nfds:数组元素个数
timeout:超时时间(毫秒)
-1:一直阻塞,直到事件发生
0:立即返回
>0:等待指定毫秒数返回值:
失败返回-1
成功返回产生事件的个数
时间达到没有事件发生返回0
pollfd结构体
struct pollfd { int fd; /* 文件描述符 */ short events; /* 监听的事件 */ short revents; /* 实际发生的事件(由内核填充) */ };常用事件标志
POLLIN:有数据可读
POLLPRI:有紧急数据可读
POLLOUT:可写
POLLERR:错误
POLLHUP:挂起
POLLNVAL:描述符未打开
使用示例
struct pollfd fds[2]; // 设置要监听的描述符 fds[0].fd = sock_fd1; fds[0].events = POLLIN; fds[1].fd = sock_fd2; fds[1].events = POLLIN | POLLOUT; // 调用poll int ret = poll(fds, 2, 5000); // 等待5秒 // 检查结果 if (fds[0].revents & POLLIN) { // sock_fd1可读 }返回值
成功:返回就绪的描述符个数
超时:返回0
错误:返回-1,并设置errno
4.2.2示例
cpp
//服务端
#include "head.h"
int CreateListenSocket(const char *pip, int port)
{
int ret = 0;
int sockfd = 0;
struct sockaddr_in seraddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("fail to socket");
return -1;
}
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(port);
seraddr.sin_addr.s_addr = inet_addr(pip);
ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (-1 == ret)
{
perror("fail to bind");
return -1;
}
ret = listen(sockfd, 10);
if (-1 == ret)
{
perror("fail to listen");
return -1;
}
return sockfd;
}
void InitPollFds(struct pollfd *pfds, int maxlen)
{
int i = 0;
for (i = 0; i < maxlen; i++)
{
pfds[i].fd = -1;
}
return;
}
int AddEvent(struct pollfd *pfds, int maxlen, int fd, short event)
{
int i = 0;
for (i = 0; i < maxlen; i++)
{
if (-1 == pfds[i].fd)
{
pfds[i].fd = fd;
pfds[i].events = event;
break;
}
}
return 0;
}
int DelEvent(struct pollfd *pfds, int maxlen, int fd)
{
int i = 0;
for (i = 0; i < maxlen; i++)
{
if (fd == pfds[i].fd)
{
pfds[i].fd = -1;
break;
}
}
return 0;
}
int main(void)
{
int sockfd = 0;
int confd = 0;
int nready = 0;
int i = 0;
ssize_t nret = 0;
char tmpbuff[4096] = {0};
struct pollfd fds[1024];
sockfd = CreateListenSocket("192.168.0.165", 50000);
InitPollFds(fds, 1024);
AddEvent(fds, 1024, sockfd, POLLIN);
while (1)
{
nready = poll(fds, 1024, -1);
if (-1 == nready)
{
perror("fail to poll");
return -1;
}
if (fds[0].revents & POLLIN)
{
confd = accept(sockfd, NULL, NULL);
if (-1 == confd)
{
perror("fail to accept");
DelEvent(fds, 1024, sockfd);
close(sockfd);
continue;
}
AddEvent(fds, 1024, confd, POLLIN);
}
for (i = 1; i < 1024; i++)
{
if (-1 == fds[i].fd)
{
continue;
}
if (fds[i].revents & POLLIN)
{
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = recv(fds[i].fd, tmpbuff, sizeof(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to recv");
DelEvent(fds, 1024, fds[i].fd);
close(fds[i].fd);
continue;
}
else if (0 == nret)
{
printf("关闭连接\n");
DelEvent(fds, 1024, fds[i].fd);
close(fds[i].fd);
continue;
}
printf("RECV:%s\n", tmpbuff);
sprintf(tmpbuff, "%s --- echo", tmpbuff);
nret = send(fds[i].fd, tmpbuff, strlen(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to send");
DelEvent(fds, 1024, fds[i].fd);
close(fds[i].fd);
continue;
}
}
}
}
close(sockfd);
return 0;
}
cpp
//客户端
#include "head.h"
int main(void)
{
int sockfd = 0;
int ret = 0;
struct sockaddr_in seraddr;
char tmpbuff[4096] = {0};
int cnt = 0;
ssize_t nret = 0;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("fail to socket");
return -1;
}
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.0.165");
ret = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (-1 == ret)
{
perror("fail to connect");
return -1;
}
while (1)
{
sprintf(tmpbuff, "hello world ---------- %d", cnt);
cnt++;
nret = send(sockfd, tmpbuff, strlen(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to send");
return -1;
}
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = recv(sockfd, tmpbuff, sizeof(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to recv");
return -1;
}
else if (0 == nret)
{
printf("关闭连接\n");
break;
}
printf("RECV:%s\n", tmpbuff);
}
close(sockfd);
return 0;
}
4.3.1. epoll API详解
epoll通过三个核心系统调用提供完整功能:
epoll_create - 创建事件表(epoll 集合)(内核维护的二叉树结构)
int epoll_create(int size);功能:创建一张内核监听的事件表(本质是红黑树+就绪链表)
参数:
size:建议的事件表初始大小(Linux 2.6.8后忽略,但需大于0)返回值:
成功:epoll文件描述符(epfd)
失败:-1
epoll_ctl - 管理监控事件(添加、修改、删除事件)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);功能:向事件表添加、修改或删除监控事件
参数:
epfd:epoll_create返回的描述符
op:操作类型
EPOLL_CTL_ADD:添加事件
EPOLL_CTL_MOD:修改事件
EPOLL_CTL_DEL:删除事件
fd:要操作的目标文件描述符
event:事件配置结构体指针返回值:
成功:0
失败:-1
epoll_wait - 等待事件就绪
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);功能:阻塞等待监听事件表中的事件发生
参数:
epfd:epoll描述符
events:输出参数,存放就绪事件数组
maxevents:events数组的最大容量
timeout:超时时间(毫秒),-1表示无限等待返回值:
成功:就绪事件数量(产生事件的文件描述符个数)
超时:0(时间达到没有事件产生)
错误:-1
4.3.2示例
cpp
//服务端
#include "head.h"
int CreateListenSocket(const char *pip, int port)
{
int ret = 0;
int sockfd = 0;
struct sockaddr_in seraddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("fail to socket");
return -1;
}
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(port);
seraddr.sin_addr.s_addr = inet_addr(pip);
ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (-1 == ret)
{
perror("fail to bind");
return -1;
}
ret = listen(sockfd, 10);
if (-1 == ret)
{
perror("fail to listen");
return -1;
}
return sockfd;
}
int main(void)
{
int sockfd = 0;
int confd = 0;
int epfd = 0;
int ret = 0;
int nready = 0;
int i = 0;
ssize_t nret = 0;
char tmpbuff[4096] = {0};
struct epoll_event env;
struct epoll_event retenv[1024];
epfd = epoll_create(1024);
if (-1 == epfd)
{
perror("fail to epoll_create");
return -1;
}
sockfd = CreateListenSocket("192.168.0.177", 50000);
env.events = EPOLLIN;
env.data.fd = sockfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &env);
if (-1 == ret)
{
perror("fail to epoll_ctl");
return -1;
}
while (1)
{
nready = epoll_wait(epfd, retenv, 1024, -1);
if (-1 == nready)
{
perror("fail to epoll_wait");
return -1;
}
for (i = 0; i < nready; i++)
{
if (retenv[i].data.fd == sockfd)
{
confd = accept(sockfd, NULL, NULL);
if (-1 == confd)
{
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
close(sockfd);
continue;
}
env.events = EPOLLIN;
env.data.fd = confd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, confd, &env);
if (-1 == ret)
{
perror("fail to epoll_ctl");
return -1;
}
}
else
{
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = recv(retenv[i].data.fd, tmpbuff, sizeof(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to recv");
epoll_ctl(epfd, EPOLL_CTL_DEL, retenv[i].data.fd, NULL);
close(retenv[i].data.fd);
continue;
}
else if (0 == nret)
{
printf("关闭连接\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, retenv[i].data.fd, NULL);
close(retenv[i].data.fd);
continue;
}
printf("RECV:%s\n", tmpbuff);
sprintf(tmpbuff, "%s --- echo", tmpbuff);
nret = send(retenv[i].data.fd, tmpbuff, strlen(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to send");
epoll_ctl(epfd, EPOLL_CTL_DEL, retenv[i].data.fd, NULL);
close(retenv[i].data.fd);
continue;
}
}
}
}
close(sockfd);
return 0;
}
cpp
//客户端
#include "head.h"
int main(void)
{
int sockfd = 0;
int ret = 0;
struct sockaddr_in seraddr;
char tmpbuff[4096] = {0};
int cnt = 0;
ssize_t nret = 0;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("fail to socket");
return -1;
}
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.0.177");
ret = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (-1 == ret)
{
perror("fail to connect");
return -1;
}
while (1)
{
sprintf(tmpbuff, "hello world ---------- %d", cnt);
cnt++;
nret = send(sockfd, tmpbuff, strlen(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to send");
return -1;
}
memset(tmpbuff, 0, sizeof(tmpbuff));
nret = recv(sockfd, tmpbuff, sizeof(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to recv");
return -1;
}
else if (0 == nret)
{
printf("关闭连接\n");
break;
}
printf("RECV:%s\n", tmpbuff);
}
close(sockfd);
return 0;
}
5、三种机制对比总结
| 特性 | select | poll | epoll |
|---|---|---|---|
| 最大连接数 | 有限制(1024) | 无限制 | 无限制 |
| 数据结构 | 位图数组 | 结构体数组 | 内核红黑树 |
| 内存拷贝 | 每次调用都拷贝 | 每次调用都拷贝 | 仅初始化时注册 |
| 触发模式 | 仅水平触发 | 仅水平触发 | 水平/边沿触发 |
| 时间复杂度 | O(n) | O(n) | O(1)(就绪事件数) |
| 可移植性 | 跨平台 | 多数Unix | Linux特有 |
6.总结
并发服务器实现图示:
选择建议:
-
低并发、跨平台需求:select/poll
-
高并发、Linux专属:epoll
-
极致性能、最新内核:考虑io_uring(Linux 5.1+)
IO多路复用技术是现代高性能网络服务器的基石,理解其原理和实现差异,是每个Linux后台开发者的必修课。随着连接数的增长,从select/poll迁移到epoll往往是性能提升的关键一步。