select、poll和epoll形象化理解与对比
一、引入问题
现在有一个服务器程序和一个客户端程序,客户端程序连接上服务器程序后向服务器发送“Hello server,I am client!”,服务器收到客户端的连接后给客户端发送“Hello client,I am server!”。如果是迭代服务器,那么它运行时一旦与一个客户端程序建立连接,只有处理完这个客户端的请求(上述程序中是给客户端发完“Hello client,I am server!”)后才能与下一个客户端建立连接。也就是说,迭代服务器一次只能处理一个客户端的请求。
与迭代服务器不同的是,并发服务器一次能处理多个客户端请求。在上述程序中是服务器可以在非常短的时间内读取到多个客户端给它发送的“Hello server,I am client!”,并给多个客户端发送“Hello client,I am server!”。
我们可以用多进程或多线程的方式来实现并发服务器。父进程或父线程负责接听客户端的连接,然后交给子进程或子线程来处理该客户端的请求,而父进程继续接听客户端连接。但是在非常多的客户端请求需要服务器处理的情况下,多进程和多线程并发服务器非常消耗资源,于是又引入了多路复用来解决这个问题。
二、形象化解释select、poll、epoll
多路复用有三种方式select、poll、epoll,下面用一个生活中的例子来形象化解释这三种多路复用的原理。
假如你是便利店老板,需要看顾客是否进店了,要给顾客结账,还要防止偷窃。这时你只能一直看着门等待顾客进店,然后一直监视顾客的行为,直到顾客结账出店,你又开始等待下一个顾客。
select相当于一个监控,会替你一直监视进来的每一个顾客(select可以替你监视1024个人),顾客进店、发生偷窃行为或者顾客要结账时它都会通知你有顾客发生了事件,但是它不会通知你是哪个顾客发生了哪种事件,在没接到select的通知期间,你就可以摸鱼做其他的事,接到select的通知后,你需要一个个地问select是不是XXX人发生了XXX事件,问到以后再处理事件。
poll也是一个监控,它的功能和select相同,但是性能更好,它替你监控的人数没有限制,但是当你接到poll的通知时仍然需要一个个地问poll是不是XXX人发生了XXX事件,问到以后再处理事件。
epoll也是一个监控,它的功能更强,它不仅监视的人数没有限制,而且一旦有人进店、结账或偷窃它就会告诉你是哪个人发生了哪种事件,你就可以直接去处理事件。
三、select、poll、epoll“监控”步骤
无论你是想要select、poll还是epoll帮你监视顾客,让你可以及时知道店里现在发生了什么事然后去处理,你都需要做三步:
- 一旦有顾客进店,你就要告诉它们要监视这个顾客,要不然它们可不会主动帮你监视
- 你要准备好接收发生事件的通知
- 问它们具体发生了什么事件
接下来就是你去处理对应的事件了,注意:select和poll只会通知你有事件发生,想知道是谁发生了什么事件你还得一个个地问,epoll会主动告诉你是谁发生了什么事件。 换句话说,select和poll会给你一份所有监视顾客的名单,你得对着这个名单问select或poll是不是XXX人发生了XXX事件,然后去处理对应事件;epoll也会给你一份顾客名单,不过这份顾客名单上全部是发生事件的顾客,你拿到名单以后直接按着名单去处理相应事件。
四、图解select、poll、epoll多路复用实现并发服务器
select、poll、epoll用它们的”监控“功能可以实现多路复用(只一个服务器程序就可以在极短时间内处理多个客户端请求)优化服务器性能,实现服务器高并发。那它们又是如何应用到服务器上的呢?我们用并发服务器的流程图和select、poll、epoll实现的迭代服务器流程图对比来说明这个问题。
五、select、poll、epoll使用的头文件和函数
为了方便理解,这里我们仍用“便利店老板看店”的例子来解释select、poll、epoll相关函数的功能
1)select
#include <sys/select.h>
#include <sys/time.h>
FD_ZERO(fd_set* fds)
FD_SET(int fd, fd_set* fds)
FD_ISSET(int fd, fd_set* fds)
FD_CLR(int fd, fd_set* fds)
int select(int max_fd, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout);
2)poll
#include <poll.h>
struct pollfd
{
int fd;
short events;
short revents;
} ;
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
3)epoll
#include <sys/epoll.h>
struct epoll_event
{
uint32_t events;
epoll_data_t data;
};
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
六、用epoll写一个简单的并发服务器程序
现在我们用epoll多路复用来实现我们最开始提到的服务器程序:客户端程序连接上服务器程序后向服务器发送“Hello server,I am client!”,服务器收到客户端的消息后给客户端发送“Hello client,I am server!”
服务器端:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/epoll.h>
#define SERVER_PORT 9999
#define MAX_EVENTS 100
int main(char argc, char *argv[])
{
int serv_fd;
int clie_fd;
struct sockaddr_in serv_addr;
struct sockaddr_in clie_addr;
socklen_t cliaddr_len = sizeof(struct sockaddr_in);
int rv = -1;
char buf[1024];
int on = 1;
int epollfd;
struct epoll_event event;
struct epoll_event event_array[MAX_EVENTS];
int events;
int i;
serv_fd = socket(AF_INET, SOCK_STREAM, 0);
if(serv_fd < 0)
{
printf("accept socket failure: %s\n", strerror(errno));
rv = -2;
goto cleanup;
}
printf("create socket[%d] successfully!\n",serv_fd);
setsockopt(serv_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVER_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(serv_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("bind socket[%d] failure: %s\n", serv_fd, strerror(errno));
rv = -3;
goto cleanup;
}
listen(serv_fd, 13);
epollfd = epoll_create(MAX_EVENTS);
if(epollfd < 0)
{
printf("epoll_create() failure: %s\n", strerror(errno));
rv = -4;
goto cleanup;
}
event.data.fd = serv_fd;
event.events = EPOLLIN;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, serv_fd, &event) < 0)
{
printf("epoll add listen socket failure: %s\n", strerror(errno));
rv = -5;
goto cleanup;
}
while(1)
{
printf("\nStart waiting epoll inform...\n");
events = epoll_wait(epollfd, event_array, MAX_EVENTS, -1);
if(events < 0)
{
printf("epoll failure: %s\n", strerror(errno));
break;
}
else if(events == 0)
{
printf("epoll get timeout\n");
continue;
}
for(i=0; i<events; i++)
{
if((event_array[i].events&EPOLLERR) || (event_array[i].events&EPOLLHUP))
{
printf("epoll_wait get error on fd[%d]: %s\n", event_array[i].data.fd, strerror(errno));
epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd, NULL);
close(event_array[i].data.fd);
}
if(event_array[i].data.fd == serv_fd)
{
clie_fd = accept(serv_fd, (struct sockaddr*)NULL, NULL);
if(clie_fd < 0)
{
printf("accept new client failure: %s\n", strerror(errno));
continue;
}
event.data.fd = clie_fd;
event.events = EPOLLIN;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, clie_fd, &event) < 0)
{
printf("epoll add client socket failure: %s\n", strerror(errno));
close(event_array[i].data.fd);
continue;
}
}
else
{
rv = read(event_array[i].data.fd, buf, sizeof(buf));
if(rv <= 0)
{
printf("socket[%d] read failure or get disconncet and will be removed.\n",event_array[i].data.fd);
epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd, NULL);
close(event_array[i].data.fd);
continue;
}
else
{
printf("socket[%d] read get %d bytes data:%s\n", event_array[i].data.fd, rv, buf);
if(write(event_array[i].data.fd, "Hello client, I am cloud server!", sizeof("Hello client, I am cloud server!")) < 0)
{
printf("socket[%d] write failure: %s\n", event_array[i].data.fd, strerror(errno));
epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd, NULL);
close(event_array[i].data.fd);
}
}
}
}
}
cleanup:
if(serv_fd > 0)
close(serv_fd);
if(clie_fd > 0)
close(clie_fd);
return rv;
}
客户端:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <getopt.h>
#define SERVER_PORT 9999
#define SERVER_IP "127.0.0.1"
#define MSG_STR "Hello server, I am client!"
int main(char argc, char *argv[])
{
int conn_fd = -1;
int rv = 0;
char buf[1024];
struct sockaddr_in serv_addr;
conn_fd = socket(AF_INET, SOCK_STREAM, 0);
if (conn_fd < 0)
{
printf("create socket failure: %s\n", strerror(errno));
rv = -2;
goto cleanup;
}
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVER_PORT);
inet_aton( SERVER_IP, &serv_addr.sin_addr );
if( connect(conn_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("connect to server[%s:%d] failure: %s\n", serv_ip, port, strerror(errno));
rv = -3;
goto cleanup;
}
rv = write(conn_fd, MSG_STR, sizeof(MSG_STR));
if(rv < 0)
{
printf("Write data to server [%s:%d] failure: %s\n", serv_ip, port, strerror(errno));
rv = -4;
goto cleanup;
}
memset(buf, 0, sizeof(buf));
rv = read(conn_fd, buf, sizeof(buf));
if(rv < 0)
{
printf("Read data to server [%s:%d] failure: %s\n", serv_ip, port, strerror(errno));
rv = -5;
goto cleanup;
}
printf("Read %d bytes data from Server: %s\n", rv, buf);
cleanup:
if(conn_fd > 0)
close(conn_fd);
return rv;
}
七、对比总结select、poll、epoll
首先说明,select和poll的用处越来越有限,epoll已经成为了目前实现高性能网络服务器的必备技术。
select的缺点:
- 最多只能监视1024个文件描述符,虽然可以更改,但select采用轮询的方式扫描文件描述符,文件描述符数量越多,select性能越差。
- 内核/用户空间内存拷贝问题,会产生巨大的开销。
- 需要遍历整个文件描述符数组才能发现哪些文件描述符发生了那些事件。
- 如果程序没有完成对一个就绪文件描述符事件的处理,那么每次调用select都还是会将那些正在处理的文件描述符通知给进程。
而poll除了没有最大文件描述符限制,以上的缺点都存在。
epoll与select、poll的实现机制完全不同,它没有以上的缺点,可以直接返回就绪文件描述符数组,能显著提高程序在大量并发连接中只有少量活跃情况下的系统CPU利用率。
|