Linux下的I/O多路复用
在 I/O 多路复用中,epoll、poll 和 select 是常用的三种机制,它们都可以用于实现事件驱动的网络编程。
select
select 是 Unix 系统最早引入的 I/O 多路复用函数,它允许一个进程监视多个文件描述符,当其中任何一个文件描述符准备好进行 I/O 操作时,select 函数就会返回。
- 优点:跨平台支持好,几乎所有的操作系统都支持 select。
- 缺点:效率较低,因为在调用 select 函数后,内核需要遍历所有的文件描述符来检查状态变化,同时 select 函数有最大文件描述符数量的限制。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
int main() {
int server_socket, client_socket, max_sd, activity;
struct sockaddr_in address;
fd_set readfds;
char buffer[1025];
// 创建 TCP 套接字
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 绑定地址和端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8888);
if (bind(server_socket, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听
if (listen(server_socket, 5) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
// 接受连接
client_socket = accept(server_socket, (struct sockaddr *)NULL, NULL);
while (1) {
FD_ZERO(&readfds);
FD_SET(client_socket, &readfds);
max_sd = client_socket;
// 监视文件描述符变化
activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
if ((activity < 0) && (errno != EINTR)) {
printf("select error");
}
if (FD_ISSET(client_socket, &readfds)) {
// 读取数据
int valread = read(client_socket, buffer, 1024);
if (valread == 0) {
printf("客户端关闭连接\n");
break;
} else {
buffer[valread] = '\0';
printf("收到数据: %s\n", buffer);
}
}
}
close(server_socket);
return 0;
}
poll
poll 是对 select 的改进,它也能够监视多个文件描述符,并在其中任何一个准备好进行 I/O 操作时返回。
- 优点:没有最大文件描述符数量的限制,相比 select 更加灵活。
- 缺点:效率仍然有限,因为调用 poll 函数后,内核必须遍历所有的文件描述符来检查状态变化。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <poll.h>
int main() {
int server_socket, client_socket, i, activity;
struct sockaddr_in address;
struct pollfd fds[1];
char buffer[1025];
// 创建 TCP 套接字
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 绑定地址和端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8888);
if (bind(server_socket, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听
if (listen(server_socket, 5) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
// 接受连接
client_socket = accept(server_socket, (struct sockaddr *)NULL, NULL);
fds[0].fd = client_socket;
fds[0].events = POLLIN;
while (1) {
activity = poll(fds, 1, -1);
if (activity < 0) {
perror("poll failed");
break;
}
if (fds[0].revents & POLLIN) {
int valread = read(client_socket, buffer, 1024);
if (valread == 0) {
printf("客户端关闭连接\n");
break;
} else {
buffer[valread] = '\0';
printf("收到数据: %s\n", buffer);
}
}
}
close(server_socket);
return 0;
}
epoll
epoll 是 Linux 特有的高性能 I/O 多路复用机制,可以显著提升大规模并发连接的性能。
- 优点:使用红黑树结构存储文件描述符,只有活跃的文件描述符才会被放入红黑树中,因此效率很高。同时,epoll 支持水平触发和边缘触发两种模式,可以更精确地控制事件通知。
- 缺点:只能在 Linux 系统上使用,不具有跨平台特性。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
int main() {
int server_socket, client_socket, i, activity;
struct sockaddr_in address;
struct epoll_event event, events[1];
char buffer[1025];
// 创建 TCP 套接字
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 绑定地址和端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8888);
if (bind(server_socket, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听
if (listen(server_socket, 5) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
// 创建 epoll 实例
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1 failed");
exit(EXIT_FAILURE);
}
event.events = EPOLLIN;
event.data.fd = server_socket;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_socket, &event) == -1) {
perror("epoll_ctl failed");
exit(EXIT_FAILURE);
}
while (1) {
activity = epoll_wait(epoll_fd, events, 1, -1);
if (activity < 0) {
perror("epoll_wait failed");
break;
}
for (i = 0; i < activity; i++) {
if (events[i].data.fd == server_socket) {
client_socket = accept(server_socket, (struct sockaddr *)NULL, NULL);
event.events = EPOLLIN;
event.data.fd = client_socket;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_socket, &event) == -1) {
perror("epoll_ctl failed");
exit(EXIT_FAILURE);
}
} else {
int valread = read(events[i].data.fd, buffer, 1024);
if (valread == 0) {
printf("客户端关闭连接\n");
close(events[i].data.fd);
} else {
buffer[valread] = '\0';
printf("收到数据: %s\n", buffer);
}
}
}
}
close(server_socket);
close(epoll_fd);
return 0;
}
总的来说,select 和 poll 在处理大量并发连接时效率较低,而 epoll 则在 Linux 系统上表现更好,因此在高性能网络编程中更常用。选择合适的 I/O 多路复用机制取决于具体的应用场景和目标平台。