select的效率一定比epoll低吗?

一般以为epoll的效率比select高,当然有人知道这个前提是网络环境较差且存在大连接(超过10k)的情况下。

网络上<<epoll内核实现>>一文中列出了下列因素:
     select/poll的缺点在于:
     1.每次调用时要重复地从用户态读入参数。

2.每次调用时要重复地扫描文件描述符。
     3.每次在调用开始时,要把当前进程放入各个文件描述符的等待队列。在调用结束后,又把进程从各个等待队列中删除。
     在实际应用中,select/poll监视的文件描述符可能会非常多,如果每次只是返回一小部分,那么,这种情况下select/poll
显得不够高效。



上面给出的原因是文件描述符非常多时,每次都要对文件描述符进行一些操作并对参数读入时内核和用户态的切换操作导致select效率比较低。但是换做另外一种场景,如果在网络最好的韩国的WAN或者天朝的某个公司内部的局域网LAN中(互联网网速咱就不跟人家棒子比了),网络环境非常好,即干扰很少,某个服务器只接受固定的计算机发来的数据(例如10台),这种情况下连接非常少, 使得epoll高效的上述因素显然不能成立了,此时可以认为epoll和select效率近似。
在上面的场景中加一个因素,10台计算机每次对服务器发送的数据都非常多(超过1M),可以参考下面的代码,则select检查到连接数目后,for循环检查确认10台机器中那几台机器来了数据,应该非常快,当收数据时,用recv或者recvfrom进行数据接收,不用在内核态和用户态各自的buf之间频繁拷贝,这种情况下效率应该能够令人满意。但是epoll呢?

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>


int main(int argc,  char** argv) {
int local_sock_fd = socket(AF_INET,SOCK_DGRAM,0);
if (local_sock_fd < 0) {
exit(0);
}


struct sockaddr_in local_addr;
local_addr.sin_family = AF_INET;
local_addr.sin_addr.s_addr = INADDR_ANY;
local_addr.sin_port = htons(6000);
int ret = bind(local_sock_fd, (struct sockaddr*)(&local_addr), sizeof(local_addr));
if (ret == -1) {
exit(1);
}
fd_set local_fd_set;
fd_set local_read_fd_set;
FD_ZERO(&local_fd_set);
FD_ZERO(&local_read_fd_set);
FD_SET(local_sock_fd, &local_fd_set);


struct timeval time_out;
time_out.tv_sec = 1;
time_out.tv_usec = 0;
char data_buf[65536] = {0};
int max_fd = local_sock_fd + 1;
while(1) {
local_read_fd_set = local_fd_set;
int fd_num = select(max_fd, &local_read_fd_set, NULL, NULL, &time_out);
if (fd_num < 0) {
perror("select");
exit(2);
}


int idx = 0;
for( idx = 0; idx < fd_num; idx++) {
struct sockaddr_in peer_addr;
int peer_addr_len = sizeof(peer_addr);
if (FD_ISSET(local_sock_fd, &local_read_fd_set)) {
int data_buf_len = recvfrom(local_sock_fd, data_buf, sizeof(data_buf) - 1, 0, (struct sockaddr*)(&peer_addr), &peer_addr_len);
if (data_buf_len < 0) {
break;
} else if (data_buf_len == 0) {
continue;
}
data_buf[data_buf_len] = '\0';
printf("recv data: %s\n", data_buf);
}
}
}//while


return 0;
}

epoll部分的代码可以参考下面这部分,epoll必须先获取一个新的连接socket描述符,然后把它加入event pool file system,并为之分配相应的inode,这期间的epoll_ctl操作很费时的,因为其本质是阻塞的,等到内核数据接收完一部分数据后(一般一次2k),它再通知相应的socket,用户态再去拷贝数据,整个过程应为有数次内核态与用户态的切换,其效率相对上面的select就有点低下了。


#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>


void set_sock_nonblock(int sock) {
int flag = fcntl(sock, F_GETFL);
flag |= O_NONBLOCK;
flag |= O_NDELAY;
fcntl(sock, F_SETFL, flag);
}


int main(int argc, char* argv[]) {
int local_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
set_sock_nonblock(local_sock_fd);
struct sockaddr_in local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_addr.s_addr = INADDR_ANY;
local_addr.sin_port = htons(6000);
int ret = bind(local_sock_fd, (struct sockaddr*)(&local_addr), sizeof(local_addr));
if (ret == -1) {
exit(1);
}
listen(local_sock_fd, 5);


int ep_fd = epoll_create(10);
struct epoll_event ep_event;
ep_event.events = EPOLLIN;
ep_event.data.fd = local_sock_fd;
epoll_ctl(ep_fd, EPOLL_CTL_ADD, local_sock_fd, &ep_event);
struct epoll_event events[10];
int time_out = 1;
int peer_sock_fd = 0;
char data_buf[1024] = {0};
int data_buf_len = 0;
while (1) {
int fd_num = epoll_wait(ep_fd, events, sizeof(events) / sizeof(struct epoll_event), time_out);
int idx = 0;
for (idx = 0; idx < fd_num; ++idx) {
if (local_sock_fd == events[idx].data.fd) {
struct sockaddr_in peer_addr;
int peer_addr_len = sizeof(struct sockaddr_in);
peer_sock_fd = accept(local_sock_fd, (struct sockaddr *)(&peer_addr), &peer_addr_len);
if (peer_sock_fd < 0){
perror("accept");
exit(1);
}
ep_event.data.fd = peer_sock_fd;
ep_event.events = EPOLLIN|EPOLLET;
epoll_ctl(ep_fd, EPOLL_CTL_ADD, peer_sock_fd, &ep_event);

if(events[idx].events & EPOLLIN) {
peer_sock_fd = events[idx].data.fd;
if (peer_sock_fd < 0) {
continue;
}
int ret = read(peer_sock_fd, data_buf, sizeof(data_buf) - 1);
if (ret < 0) {
if (errno == ECONNRESET) {
close(peer_sock_fd);
events[idx].data.fd = -1;
}
} else if (ret == 0) {
close(peer_sock_fd);
events[idx].data.fd = -1;
}
data_buf[ret] = '\0';
printf("read data: %s\n", data_buf);
}
}
}


close(ep_fd);

close(local_sock_fd);

return 0;
}
所以对二者效率的区分,也要区分使用场景。如果要搭建网络服务框架,建议使用epoll,毕竟网络情况很复杂,瓶颈不仅仅是网络数据的接收和发送,数据的处理也是很复杂的。但是在一些简单的场景下,特别是内网LAN通信,select就可以满足要求,效率不会比epoll低,而且编程简单。如果不想使用庞大的网络框架式,它显然是很好的选择。
以上是个人的一些理解,欢迎大家指正。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章