Linux 网络编程——IO Multiplexing之select, poll, epoll详解

上一章节Linux 网络编程——多进程,多线程模型已经介绍了两种服务器并发模型,但其并发量受进程/线程数量限制。这一章节,我们将介绍三种IO多路复用模型,可以实现单进程监听多个网络IO事件。

IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

SELECT模型

Common API
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
void FD_ZERO(fd_set *set);  clear the set
void FD_CLR(int fd, fd_set *set); remove a given file descriptor from the set
int  FD_ISSET(int fd, fd_set *set); tests to see if a file descriptor is part of the set
void FD_SET(int fd, fd_set *set); add a given file descriptor from the set
SELECT模型服务器demo
#include <arpa/inet.h> // 大小端转换
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h> // socket套接字
#include <unistd.h>
#define _SERVER_PORT 8080
#define BACK_LOG 128
#define IP_SIZE 16
#define BUFFER_SIZE 1600
int server_net_init(void) {
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(_SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //设置本机任意IP
// inet_pton(AF_INET, _SERVER_IP, &server_addr.sin_addr.s_addr);
// //转换并设置自定义IP
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) ==
-1) {
perror("bind Error");
exit(0);
}
listen(server_fd, BACK_LOG);
printf("TCP Server Waiting for Connect\n");
return server_fd;
}
int server_recv_response(int sockfd) {
char recv_buffer[BUFFER_SIZE];
bzero(recv_buffer, BUFFER_SIZE);
ssize_t recvlen;
if ((recvlen = recv(sockfd, recv_buffer, sizeof(recv_buffer), 0)) > 0) {
printf("%s", recv_buffer);
send(sockfd, recv_buffer, recvlen, 0);
bzero(recv_buffer, BUFFER_SIZE);
}
if (recvlen == -1) {
perror("recv error");
exit(-1);
} else if (recvlen == 0) {
printf("client exit\n");
return -1;
}
return 0;
}
void select_starting(int sockfd) {
int client_fd;
struct sockaddr_in client_addr;
socklen_t addrlen;
int readycode; // 就绪量
fd_set nset, oset; // nset传入,oset传出
int clientfd_array[1021]; // 客户端sock数组
int maxfd; // 最大的描述符
char client_ip[IP_SIZE];
bzero(client_ip, IP_SIZE);
// 初始化
maxfd = sockfd;
FD_ZERO(&nset);
FD_SET(sockfd, &nset);
for (int i = 0; i < 1021; i++) clientfd_array[i] = -1;
printf("Select Server Running ...\n");
while (1) {
oset = nset;
readycode =
select(maxfd + 1, &oset, NULL, NULL, NULL); // 阻塞监听sock读取事件
while (readycode) { // 循环处理就绪事件
// 辨别就绪
if (FD_ISSET(sockfd, &oset)) {
addrlen = sizeof(client_addr);
if ((client_fd = accept(sockfd, (struct sockaddr *)&client_addr,
&addrlen)) > 0) {
printf(
"TCP Server Accept Success:client ip[%s] client "
"prot[%d] \n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr,
client_ip, IP_SIZE),
ntohs(client_addr.sin_port));
if (client_fd > maxfd) maxfd = client_fd;
FD_SET(client_fd, &nset);
for (int i = 0; i < 1021; i++)
if (clientfd_array[i] == -1) {
clientfd_array[i] = client_fd;
break;
}
FD_CLR(sockfd, &oset);
} else {
perror("accept error");
exit(0);
}
} else { // clientfd就绪
for (int i = 0; i < 1021; i++)
if (clientfd_array[i] != -1)
if (FD_ISSET(clientfd_array[i], &oset)) {
if (server_recv_response(clientfd_array[i]) == -1) {
FD_CLR(clientfd_array[i], &nset);
clientfd_array[i] = -1;
}
FD_CLR(sockfd, &oset);
break;
}
}
--readycode;
}
}
}
int main(void) {
int sfd = server_net_init(); // 服务端网络初始化
select_starting(sfd); // 阻塞等到连接
close(sfd);
return 0;
}

SELECT模型的优点:

  • SELECT模型采用IO 复用技术,业务处理交替执行,可以仅用单进程实现一对多处理效果
  • 每个平台语言都对select有兼容和实现,便于移植
  • SELECT模型定时阻塞支持微秒级别,有较高的时间精度

SELECT模型的缺点:

  • 监听最大数为1024,不能满足高并发需求。
  • SELECT监听采用轮询模式,随着轮询的数量增多,IO处理能力呈线性下降。
  • SELECT对于sockfd的事件监听是批处理的,如果你希望不同的sockfd监听不同的网络事件,SELECT无法实现。
  • 在退出时,SELECT对每个文件描述符集进行适当的修改,以指示哪些文件描述符实际上已更改状态。因此,如果在循环中使用select(),则必须在此之前重新初始化这些集。
  • 单进程限制,阻塞问题导致任务无法继续,单进程模型不允许业务处理时间过长。
  • SELECT模型在使用的时候会出现大量的无意义的重复开销,浪费系统资源(每次往set监听集合增加监听sockfd描述符时,select()都会将整个监听集合拷贝到内核层,并且将集合的监听项挂载到监听设备,无论以前是否挂载过)

POLL模型

#include <poll.h>

用户自定义长度结构体数组作为POLL监听集合

struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};

API:

int poll(struct pollfd * fds , nfds_t nfds , int timeout ); //timeout(工作模式):-1 阻塞监听 >0 定时阻塞 0非阻塞

返回值:成功时,poll () 返回一个非负值,该值是pollfds中其revents字段已设置为非零值(指示事件或错误)的元素数。返回值为0表示系统调用在任何文件描述符被读取之前超时。出错时返回 -1,并设置 errno以指示错误。

POLL模型服务器demo
#include <arpa/inet.h> // 大小端转换
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h> // socket套接字
#include <unistd.h>
#define _SERVER_PORT 8080
#define BACK_LOG 128
#define IP_SIZE 16
#define BUFFER_SIZE 1600
int server_net_init(void) {
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(_SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //设置本机任意IP
// inet_pton(AF_INET, _SERVER_IP, &server_addr.sin_addr.s_addr);
// //转换并设置自定义IP
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) ==
-1) {
perror("bind Error");
exit(0);
}
listen(server_fd, BACK_LOG);
printf("TCP Server Waiting for Connect\n");
return server_fd;
}
int server_recv_response(int sockfd) {
char recv_buffer[BUFFER_SIZE];
bzero(recv_buffer, BUFFER_SIZE);
ssize_t recvlen;
if ((recvlen = recv(sockfd, recv_buffer, sizeof(recv_buffer), 0)) > 0) {
printf("%s", recv_buffer);
send(sockfd, recv_buffer, recvlen, 0);
bzero(recv_buffer, BUFFER_SIZE);
}
if (recvlen == -1) {
perror("recv error");
exit(-1);
} else if (recvlen == 0) {
printf("client exit\n");
return -1;
}
return 0;
}
// poll模型
void poll_starting(int sockfd) {
int readycode; // 就绪量
struct pollfd listen_array[1024]; // POLL监听集合
int client_fd;
struct sockaddr_in client_addr;
socklen_t addrlen;
char client_ip[IP_SIZE];
bzero(client_ip, IP_SIZE);
// init
listen_array[0].fd = sockfd;
listen_array[0].events = POLLIN;
for (int i = 1; i < 1024; i++) {
listen_array[i].fd = -1;
listen_array[i].events = POLLIN;
}
printf("POLL Server Running ...\n");
while (1) {
readycode = poll(listen_array, 1024, -1); // 阻塞监听sockfd读取事件
while (readycode) { // 循环处理就绪事件
if (listen_array[0].revents == POLLIN) { // server_fd就绪
addrlen = sizeof(client_addr);
if ((client_fd = accept(sockfd, (struct sockaddr *)&client_addr,
&addrlen)) > 0) {
printf(
"TCP Server Accept Success:client ip[%s] client "
"prot[%d] \n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr,
client_ip, IP_SIZE),
ntohs(client_addr.sin_port));
for (int i = 1; i < 1024; i++) {
if (listen_array[i].fd == -1) {
listen_array[i].fd = client_fd;
break;
}
}
listen_array[0].revents = 0;
} else {
perror("accept error");
exit(0);
}
} else { // client_fd就绪
for (int i = 1; i < 1024; i++) {
if (listen_array[i].fd != -1) {
if (listen_array[i].revents == POLLIN) {
if (server_recv_response(listen_array[i].fd) ==
-1) {
close(listen_array[i].fd);
listen_array[i].fd = -1;
}
listen_array[i].revents = 0;
break;
}
}
}
}
--readycode;
}
}
}
int main(void) {
int sfd = server_net_init(); // 服务端网络初始化
poll_starting(sfd); // 阻塞等到连接
close(sfd);
return 0;
}

POLL模型优点:

  • 针对sockfd的监听设置更为灵活,可以针对不同的sockfd设置不同的监听项,相比select可以监听的事件更为丰富,选择更多
  • poll监听大小没有select模型的1024的数量限制,但是poll采用的依然是轮询监听机制
  • poll采用events和revents两个参数将传入传出分离

POLL模型的缺点和select一致

 EPOLL模型

EPOLL模型服务器demo
#include <arpa/inet.h> // 大小端转换
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h> // socket套接字
#include <unistd.h>
#define _SERVER_PORT 8080
#define BACK_LOG 128
#define IP_SIZE 16
#define BUFFER_SIZE 1600
#define EPOLLSIZE 1e5
int server_net_init(void) {
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(_SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //设置本机任意IP
// inet_pton(AF_INET, _SERVER_IP, &server_addr.sin_addr.s_addr);
// //转换并设置自定义IP
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) ==
-1) {
perror("bind Error");
exit(0);
}
listen(server_fd, BACK_LOG);
printf("TCP Server Waiting for Connect\n");
return server_fd;
}
int server_recv_response(int sockfd) {
char recv_buffer[BUFFER_SIZE];
bzero(recv_buffer, BUFFER_SIZE);
ssize_t recvlen;
if ((recvlen = recv(sockfd, recv_buffer, sizeof(recv_buffer), 0)) > 0) {
printf("%s", recv_buffer);
// 回传
send(sockfd, recv_buffer, recvlen, 0);
bzero(recv_buffer, BUFFER_SIZE);
}
if (recvlen == -1) {
perror("recv error");
exit(-1);
} else if (recvlen == 0) {
printf("client exit ...\n");
return -1;
}
return 0;
}
// epoll模型
void epoll_starting(
int sockfd) { // 文件描述符, 默认情况下进程打开的最大文件描述符数量为1024
int client_fd;
struct sockaddr_in client_addr;
socklen_t addrlen;
char client_ip[IP_SIZE];
bzero(client_ip, IP_SIZE);
int readycode; // 就绪量
struct epoll_event ready_array[(int)EPOLLSIZE]; // 就绪等待队列
// init server_fd
struct epoll_event node;
node.data.fd = sockfd;
node.events = EPOLLIN;
//创建监听树
int epfd = epoll_create(EPOLLSIZE);
int i;
// 第一次添加节点, 添加server_fd
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &node);
printf("EPOLL Server Running ...\n");
while (1) {
readycode = epoll_wait(epfd, ready_array, EPOLLSIZE,
-1); // 阻塞监听sock读取事件
while (readycode) { // 循环处理就绪事件
i = 0;
// 辨别就绪
if (ready_array[i].data.fd == sockfd) {
addrlen = sizeof(client_addr);
if ((client_fd = accept(sockfd, (struct sockaddr *)&client_addr,
&addrlen)) > 0) {
printf(
"TCP Server Accept Success:client ip[%s] client "
"prot[%d] \n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr,
client_ip, IP_SIZE),
ntohs(client_addr.sin_port));
node.data.fd = client_fd;
node.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &node);
} else {
perror("accept error");
exit(0);
}
} else { // clientfd就绪
if ((server_recv_response(ready_array[i].data.fd)) == -1) {
close(ready_array[i].data.fd); // 关闭连接
epoll_ctl(epfd, EPOLL_CTL_DEL, ready_array[i].data.fd,
NULL); // 删除监听节点
}
}
--readycode;
++i;
}
}
}
int main(void) {
int sfd = server_net_init(); // 服务端网络初始化
epoll_starting(sfd); // 阻塞等到连接
close(sfd);
return 0;
}

EPOLL模型相比其他模型的优势:

  1. epoll模型没有轮询限制,理论上epoll可以监听系统上最大的描述符数量,也不会有多余的开销
  2. epollm没有多余的拷贝开销和挂载开销,保证每个监听节点只拷贝一次挂载一次
  3. epoll不仅会返回监听数量还会返回就绪节点,用户直接处理即可
  4. epoll设置监听比较灵活,可以对不同的sock设置不同的事件监听,可监听的事件也比较丰富

EPOLL监听能力较为出色,但是处理能力而言与select,poll没有区别

posted on   SocialistYouth  阅读(124)  评论(0编辑  收藏  举报

编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人

统计

点击右上角即可分享
微信分享提示