Linux内核select/poll,epoll实现与区别
在Linux内核中,select、poll和epoll是三种常用的网络I/O多路复用机制。其中select和poll是早期的实现方式,epoll是较新的实现方式,相比于前两者具有更好的性能。本文将从多个方面进行介绍,以帮助读者更好地了解它们的实现和区别。
select
select是Unix中最古老的网络I/O多路复用技术,也是目前应用最广泛的多路复用技术。它的实现方式是通过对一组文件描述符进行轮询,当其中某个或某些文件描述符可以进行 I/O 操作时就进行通知。
select的函数接口如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
其中,nfds是待监视的最大文件描述符加一;readfds、writefds、exceptfds用来判断哪些文件描述符可读、可写、出现异常;timeout表示超时时间。
select的缺点是每次调用时都要传递待监视的全部文件描述符集合,系统调用开销较大。而且,文件描述符数量较大时,会耗费大量的内存和 CPU 资源。
poll
poll是一种更加高效的网络I/O多路复用技术,它是在select的基础上进一步优化而来的。
poll的函数接口如下所示:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
其中,fds代表一组文件描述符和等待的事件,nfds为文件描述符数量,timeout为超时时间。
poll的优点在于可以避免select的缺点,因为poll时只监视需要监视的文件描述符集合,需要轮询的次数也会更少。
epoll
epoll是Linux内核2.5.44以后才开始引入的,是一种新的高效的I/O事件通知机制。和select、poll相比,epoll的性能更优,很好地解决了select和poll在处理大量文件描述符时的效率问题。
epoll的函数接口如下所示:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
其中,epoll_create用来创建一个epoll监听器,size表示监听器支持的最大文件描述符数量。epoll_ctl则用来对文件描述符进行注册、修改或删除操作。epoll_wait则等待I/O事件的发生。
epoll的优点在于它支持"边缘触发"(edge-triggered)和"水平触发"(level-triggered),可以更加灵活地应对事件。当某个文件可读时,只发出一次通知,而不是像select、poll那样会一直通知,有助于减少轮询次数和CPU负载。此外,epoll还可以支持大量的文件描述符,可获得更高的并发性。
下面提供一个示例来说明select、poll和epoll的实现和区别:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#define MAXLINE 1024
#define OPEN_MAX 100
void use_select() {
int i, listenfd, connfd, sockfd;
int nready, efd;
ssize_t n;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
struct timeval tv;
int maxi = 0;
int client[FD_SETSIZE];
fd_set rset, allset;
/* set default port and ip familiy */
int port = 8888;
int family = AF_INET;
listenfd = socket(family, SOCK_STREAM, 0);
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = family;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(port);
bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
listen(listenfd, 20);
maxfd = listenfd;
maxi = -1;
for(i = 0; i < FD_SETSIZE; ++i) {
client[i] = -1;
}
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
printf("start select loop\n");
for( ; ; ) {
rset = allset;
tv.tv_sec = 5;
tv.tv_usec = 0;
nready = select(maxfd+1, &rset, NULL, NULL, &tv);
if(nready == -1) {
perror("select error");
break;
}
if(nready == 0) {
printf("timeout..\n");
continue;
}
if(FD_ISSET(listenfd, &rset)) {
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);
for(i = 0; i < FD_SETSIZE; ++i) {
if(client[i] < 0) {
client[i] = connfd;
break;
}
}
if(i == FD_SETSIZE) {
printf("too many clients\n");
}
FD_SET(connfd, &allset);
if(connfd > maxfd) {
maxfd = connfd;
}
if(i > maxi) {
maxi = i;
}
if(--nready <= 0) {
continue;
}
}
for(i = 0; i <= maxi; ++i) {
if((sockfd = client[i]) < 0) {
continue;
}
if(FD_ISSET(sockfd, &rset)) {
if((n = read(sockfd, buf, MAXLINE)) == 0) {
close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
} else {
write(sockfd, buf, n);
}
if(--nready <= 0) {
break;
}
}
}
}
}
void use_poll() {
int i, maxi, listenfd, connfd, sockfd;
int nready;
ssize_t n;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
struct pollfd client[OPEN_MAX];
int maxfd;
/* set default port and ip familiy */
int port = 8888;
int family = AF_INET;
listenfd = socket(family, SOCK_STREAM, 0);
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = family;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(port);
bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
listen(listenfd, 20);
maxfd = 0;
for(i = 0; i < OPEN_MAX; ++i) {
client[i].fd = -1;
}
client[0].fd = listenfd;
client[0].events = POLLRDNORM;
maxi = 0;
printf("start poll loop\n");
for( ; ; ) {
nready = poll(client, maxi+1, 5000);
if(nready < 0) {
perror("poll error");
break;
}
if(nready == 0) {
printf("timeout..\n");
continue;
}
if(client[0].revents & POLLRDNORM) {
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);
for(i = 1; i < OPEN_MAX; ++i) {
if(client[i].fd < 0) {
client[i].fd = connfd;
break;
}
}
if(i == OPEN_MAX) {
printf("too many clients\n");
}
client[i].events = POLLRDNORM;
if(i > maxi) {
maxi = i;
}
if(--nready <= 0) {
continue;
}
}
for(i = 1; i <= maxi; ++i) {
if((sockfd = client[i].fd) < 0) {
continue;
}
if(client[i].revents & (POLLRDNORM|POLLERR)) {
if((n = read(sockfd, buf, MAXLINE)) < 0) {
if(errno == ECONNRESET) {
close(sockfd);
client[i].fd = -1;
} else {
perror("read error");
}
} else if(n == 0) {
close(sockfd);
client[i].fd = -1;
} else {
write(sockfd, buf, n);
}
if(--nready <= 0) {
break;
}
}
}
}
}
void use_epoll() {
int i, maxi, listenfd, connfd, sockfd;
ssize_t n;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
struct epoll_event ev, events[20];
int epfd;
/* set default port and ip familiy */
int port = 8888;
int family = AF_INET;
listenfd = socket(family, SOCK_STREAM, 0);
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = family;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(port);
bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
listen(listenfd, 20);
epfd = epoll_create(256);
ev.data.fd = listenfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
printf("start epoll loop\n");
for( ; ; ) {
int nready = epoll_wait(epfd, events, 20, 5000);
if(nready < 0) {
perror("epoll_wait");
break;
}
if(nready == 0) {
printf("timeout\n");
continue;
}
for(i = 0; i < nready; ++i) {
if(events[i].data.fd == listenfd) {
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);
ev.data.fd = connfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
} else if(events[i].events & EPOLLIN) {
if((sockfd = events[i].data.fd) < 0) {
continue;
}
if((n = read(sockfd, buf, MAXLINE)) < 0) {
if(errno == ECONNRESET) {
close(sockfd);
events[i].data.fd = -1;
} else {
perror("read error");
}
} else if(n == 0) {
close(sockfd);
events[i].data.fd = -1;
} else {
write(sockfd, buf, n);
}
}
}
}
}
int main(int argc, char **argv) {
use_select();
//use_poll();
//use_epoll();
return 0;
}
上述代码中,提供了一个TCP服务器的实现,并分别使用了select、poll和epoll三种多路复用技术。在测试过程中,可以更好地了解select、poll和epoll的实现和不同之处。
结论
在实现TCP服务器时,使用select、poll和epoll进行多路复用时均可以将 CPU 资源分配给更多的连接,并使程序的处理方式更加灵活。但是,在文件描述符数量较多时,select和poll的效率会有比较大的下降,而epoll则能很好地避免这个问题,并且支持边沿触发和水平触发等更高级的事件模型,使得程序的性能有了大幅提升。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:linux内核select/poll,epoll实现与区别 - Python技术站