1. select机制在高并发时性能下降的原因
- 文件描述符集合大小限制:在许多系统中,
select
所处理的文件描述符集合大小存在限制(如FD_SETSIZE
,通常为1024),当高并发场景下需要处理的文件描述符数量超过此限制时,无法满足需求。
- 线性扫描效率低:
select
每次调用时,内核需要线性扫描整个文件描述符集合,检查哪些文件描述符已就绪。随着文件描述符数量增多,扫描时间会线性增长,导致性能下降。
- 数据拷贝开销大:每次调用
select
时,需要将用户空间的文件描述符集合拷贝到内核空间,返回时又要将就绪的文件描述符集合从内核空间拷贝回用户空间,在高并发时这种数据拷贝的开销不容忽视。
2. 优化方案及代码示例
方案一:使用epoll
- 原理:
epoll
是Linux下高性能I/O事件通知机制,通过一个文件描述符管理多个文件描述符,内核使用红黑树管理监控的文件描述符,用链表管理就绪的文件描述符,因此在添加、删除文件描述符和获取就绪文件描述符时效率较高。
- 代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#define MAX_EVENTS 10
int main() {
int listen_fd, conn_fd;
struct sockaddr_in servaddr;
socklen_t clilen;
struct epoll_event ev, events[MAX_EVENTS];
int epoll_fd;
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket");
exit(1);
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8080);
if (bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind");
close(listen_fd);
exit(1);
}
if (listen(listen_fd, 5) < 0) {
perror("listen");
close(listen_fd);
exit(1);
}
epoll_fd = epoll_create1(0);
if (epoll_fd < 0) {
perror("epoll_create1");
close(listen_fd);
exit(1);
}
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) < 0) {
perror("epoll_ctl: listen_fd");
close(listen_fd);
close(epoll_fd);
exit(1);
}
for (;;) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds < 0) {
perror("epoll_wait");
break;
}
for (int n = 0; n < nfds; ++n) {
if (events[n].data.fd == listen_fd) {
clilen = sizeof(struct sockaddr_in);
conn_fd = accept(listen_fd, (struct sockaddr *)&servaddr, &clilen);
if (conn_fd < 0) {
perror("accept");
continue;
}
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev) < 0) {
perror("epoll_ctl: conn_fd");
close(conn_fd);
}
} else {
conn_fd = events[n].data.fd;
char buf[1024];
int nread = read(conn_fd, buf, sizeof(buf));
if (nread < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
continue;
} else {
perror("read");
close(conn_fd);
}
} else if (nread == 0) {
close(conn_fd);
} else {
buf[nread] = '\0';
printf("Received: %s\n", buf);
}
}
}
}
close(listen_fd);
close(epoll_fd);
return 0;
}
方案二:使用kqueue(FreeBSD、macOS等系统)
- 原理:
kqueue
是FreeBSD和macOS等系统提供的高效事件通知机制,它通过一个内核对象管理多个事件,同样避免了线性扫描的开销,并且在事件管理和通知方面有较好的性能。
- 代码示例:
#include <sys/types.h>
#include <sys/event.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define MAX_EVENTS 10
int main() {
int listen_fd, conn_fd;
struct sockaddr_in servaddr;
socklen_t clilen;
int kq;
struct kevent chlist[MAX_EVENTS], evlist[MAX_EVENTS];
int nev;
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket");
exit(1);
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8080);
if (bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind");
close(listen_fd);
exit(1);
}
if (listen(listen_fd, 5) < 0) {
perror("listen");
close(listen_fd);
exit(1);
}
kq = kqueue();
if (kq < 0) {
perror("kqueue");
close(listen_fd);
exit(1);
}
EV_SET(&chlist[0], listen_fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
for (;;) {
nev = kevent(kq, chlist, 1, evlist, MAX_EVENTS, NULL);
if (nev < 0) {
perror("kevent");
break;
}
for (int i = 0; i < nev; i++) {
if (evlist[i].ident == listen_fd) {
clilen = sizeof(struct sockaddr_in);
conn_fd = accept(listen_fd, (struct sockaddr *)&servaddr, &clilen);
if (conn_fd < 0) {
perror("accept");
continue;
}
EV_SET(&chlist[0], conn_fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
} else {
conn_fd = evlist[i].ident;
char buf[1024];
int nread = read(conn_fd, buf, sizeof(buf));
if (nread < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
continue;
} else {
perror("read");
close(conn_fd);
}
} else if (nread == 0) {
close(conn_fd);
} else {
buf[nread] = '\0';
printf("Received: %s\n", buf);
}
}
}
}
close(listen_fd);
close(kq);
return 0;
}