好的!这里给你整理一篇 Linux 网络编程系列(十一):select 基本使用及其与 epoll 的区别,帮你快速理解两者的核心差异和使用场景。
Linux 网络编程系列(十一)
select 基本使用以及它和 epoll 的区别
一、select 的基本使用
1. select 作用
- 监听多个文件描述符(socket、管道等),等待其中一个或多个变为“可读”、“可写”或“异常”状态。
- 适合单线程处理中多路 I/O 事件。
2. select 函数原型
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
nfds
:监听文件描述符的范围是 [0, nfds-1],通常是最大文件描述符+1。readfds
:监控可读事件的文件描述符集合。writefds
:监控可写事件的文件描述符集合。exceptfds
:监控异常事件的文件描述符集合。timeout
:超时时间。
3. fd_set 操作宏
FD_ZERO(&fdset)
: 清空集合。FD_SET(fd, &fdset)
: 加入文件描述符。FD_CLR(fd, &fdset)
: 从集合移除文件描述符。FD_ISSET(fd, &fdset)
: 检查文件描述符是否在集合中。
4. select 使用示例
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
struct timeval timeout = {5, 0};
int ret = select(sockfd + 1, &readfds, NULL, NULL, &timeout);
if (ret > 0 && FD_ISSET(sockfd, &readfds)) {
// sockfd 可读,处理数据
}
二、select 存在的问题
- 文件描述符数量限制
select
监控的最大文件描述符数量默认一般是 1024(可以编译内核或调整宏常量)。 - 性能瓶颈
- 每次调用
select
都需要传入完整的 fd 集合,内核每次都要线性遍历检查。 - 大量 fd 时,效率低下。
- 每次调用
- 修改 fd_set 问题
select
调用后会修改 fd_set,需要每次重置。 - 不支持边缘触发(Edge-triggered)
三、epoll 的介绍
- Linux 特有的 I/O 复用机制,替代了
select
和poll
。 - 主要特点:
- 支持大量文件描述符(理论上无限制)。
- 内核维护就绪事件列表,避免每次重复遍历 fd。
- 支持边缘触发(Edge-triggered)和水平触发(Level-triggered)。
- 效率高,适合高并发场景。
四、epoll 使用流程简述
- 调用
epoll_create
创建 epoll 实例。 - 调用
epoll_ctl
注册、修改、删除感兴趣的文件描述符及事件。 - 调用
epoll_wait
等待事件发生,返回就绪的文件描述符列表。
五、select 与 epoll 的对比总结
特性 | select | epoll |
---|---|---|
最大监控 fd 数量 | 受限(通常1024) | 几乎无限 |
内核遍历 fd 集合 | 每次都线性遍历 fd_set | 内核维护就绪事件列表,避免遍历 |
触发模式 | 仅支持水平触发(Level Triggered) | 支持水平和边缘触发(Level & Edge) |
fd_set 传递开销 | 每次传入全部 fd_set | 只传递就绪事件 |
代码复杂度 | 简单 | 较复杂,需要额外管理 |
性能 | 低效,适合少量 fd | 高效,适合大量 fd |
六、总结
- select 适合文件描述符数量少、并发量不高的场景,使用简单。
- epoll 更适合高并发、大量连接的网络服务,性能优势明显,但编程复杂度较高。
- Linux 下生产环境推荐使用 epoll。
好的!下面给你一个简洁的 select 和 epoll 的对比代码示例,帮助你直观理解两者的使用差异和代码风格。
select vs epoll 简单代码对比示例
1. select 示例(监听单个 socket)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#define PORT 12345
#define BUFFER_SIZE 1024
int main() {
int listen_fd, conn_fd;
struct sockaddr_in addr;
fd_set readfds;
char buffer[BUFFER_SIZE];
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, 5);
printf("Listening on port %d\n", PORT);
while (1) {
FD_ZERO(&readfds);
FD_SET(listen_fd, &readfds);
int max_fd = listen_fd;
int ret = select(max_fd + 1, &readfds, NULL, NULL, NULL);
if (ret < 0) {
perror("select");
break;
}
if (FD_ISSET(listen_fd, &readfds)) {
conn_fd = accept(listen_fd, NULL, NULL);
if (conn_fd >= 0) {
int n = read(conn_fd, buffer, BUFFER_SIZE - 1);
if (n > 0) {
buffer[n] = '\0';
printf("Received: %s\n", buffer);
write(conn_fd, buffer, n); // 回显
}
close(conn_fd);
}
}
}
close(listen_fd);
return 0;
}
2. epoll 示例(监听单个 socket)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#define PORT 12345
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024
int main() {
int listen_fd, conn_fd, epoll_fd;
struct sockaddr_in addr;
struct epoll_event ev, events[MAX_EVENTS];
char buffer[BUFFER_SIZE];
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, 5);
epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);
printf("Listening on port %d\n", PORT);
while (1) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
break;
}
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == listen_fd) {
conn_fd = accept(listen_fd, NULL, NULL);
if (conn_fd >= 0) {
ev.events = EPOLLIN;
ev.data.fd = conn_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev);
}
} else {
int n = read(events[i].data.fd, buffer, BUFFER_SIZE - 1);
if (n <= 0) {
close(events[i].data.fd);
} else {
buffer[n] = '\0';
printf("Received: %s\n", buffer);
write(events[i].data.fd, buffer, n);
}
}
}
}
close(listen_fd);
close(epoll_fd);
return 0;
}
代码差异总结
方面 | select | epoll |
---|---|---|
代码复杂度 | 低,简单直接 | 稍高,需要管理事件注册和回调 |
监听 fd 数量 | 受限(通常1024) | 可支持大量 fd |
性能 | 线性扫描 fd 集合 | 内核维护就绪列表,效率高 |
事件触发 | 只支持水平触发 | 支持水平和边缘触发 |
发表回复