select
优点:
- 高性能
- select一次等待多个文件描述符
- select的cpu压力低
- 等待时间变短,提升了性能
缺点:
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小了,默认是1024
//select.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/select.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<stdlib.h>
static int start_up(const char* _ip,int _port)//套接字初始化
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
exit(1);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))< 0)
{
perror("bind");
exit(2);
}
if(listen(sock,10)<0)
{
perror("listen");
exit(3);
}
return sock;
}
void usage(const char* proc)
{
printf("usage: %s [local_ip] [local_port]\n",proc);
}
int read_array[sizeof(fd_set)*8];
int main(int argc,char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
return 1;
}
int listen_sock = start_up(argv[1],atoi(argv[2]));
int i = 0;
int arrlen = sizeof(read_array)/sizeof(read_array[0]);
for(;i<arrlen;++i)//将read数组初始化为-1
{
read_array[i] = -1;
}
read_array[0] = listen_sock;//让监听套接字始终在数组0号位置
while(1)
{
fd_set read_set;
FD_ZERO(&read_set);//全部初始化为0
int max_fd = -1;//
for(i=0;i<arrlen;++i)//设置有效的连接
{
if(read_array[i] == -1)
continue;
FD_SET(read_array[i],&read_set);
if(max_fd<read_array[i])
max_fd = read_array[i];//改变最大值
}
struct timeval time = {1,0};
int st = select(max_fd+1,&read_set,NULL,NULL,NULL);///??
switch(st)
{
case 0://timeout
printf("timeout...\n");
break;
case -1://error
printf("select error\n");
break;
default://success
{
for(i=0;i<arrlen;++i)
{
if(read_array[i] < 0)
continue;
if(i==0 && FD_ISSET(listen_sock,&read_set))
{
//accept
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_sock<0)
{
perror("accept");
return 2;
}
printf("get a client [%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
int j = 1;
for(;j<arrlen;++j)
{
if(read_array[j]<0)
break;
}
if(j == arrlen)
{
printf("read_set full\n");
close(new_sock);
}else
{
read_array[j] = new_sock;
}
}
else if(i !=0 &&FD_ISSET(read_array[i],&read_set))
{
char buf[1024];
ssize_t s = read(read_array[i],buf,sizeof(buf));
if(s>0)
{
buf[s] = 0;
printf("client#: %s\n",buf);
}
else if(s == 0)
{
printf("client quit..\n");
close(read_array[i]);
read_array[i] = -1;
}
else
{
printf("client error..\n");
close(read_array[i]);
read_array[i] = -1;
}
}
}
}
break;
}
}
return 0;
}
//client.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
void usage(const char* proc)
{
printf("usage:%s [local_ip] [local_port]\n");
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
return 1;
}
// int fd = open("./log",O_WRONLY|O_CREAT,0664);
// if(fd < 0)
// {
//
// return 1;
// }
int sk = socket(AF_INET,SOCK_STREAM,0);
if(sk <0 )
{
perror("socket");
return 1;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sk,(struct sockaddr*)&server,sizeof(server))<0)
{
perror("connect");
exit(1);
}
close(1);
// int dp = dup2(sk,1);//重定向
int dp = dup(sk);
if(dp<0)
{
perror("dup");
exit(2);
}
char buf[1024];
while(1)
{
// printf("Please Enter#:");
// fflush(stdout);
ssize_t s = read(0,buf,sizeof(buf)-1);
if(s > 0)
{
buf[s-1] = 0;
printf("%s\n",buf);
fflush(stdout);
// write(sk,buf,strlen(buf));
// s = read(sk,buf,sizeof(buf)-1);
// if(s > 0)
// {
// buf[s] = 0;
// printf("server echo# %s\n",buf);
// }
}
}
return 0;
}
epoll
epoll是Linux下多路复用IO接口select/poll的增强版本。
它能显著减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它不会复用文件描述符集合来传递结果而迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合。
另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
epoll除了提供select/poll 那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
- 底层实现
- epoll在底层实现了自己的高速缓存区,并且建立了一个红黑树用于存放socket,另外维护了一个链表用来存放准备就绪的事件。
- 工作过程
执行epoll_ create时,创建了红黑树和就绪链表,执行epoll_ ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。 - epoll优点
- 支持一个进程打开大数目的socket描述符(FD)
- IO效率不随FD数目增加而线性下降
- 使用mmap加速内核与用户空间的消息传递。
//epoll.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<sys/epoll.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#define _SIZE_ 256
#define _MAXEVENTS_ 1024
static void usage(const char* proc)
{
printf("usage:%s [local_ip][local_port]\n",proc);
}
static int start_up(const char* _ip,int _port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
return 2;
}
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
socklen_t len = sizeof(local);
if(bind(sock,(struct sockaddr*)&local,len)<0)
{
perror("bind");
return 3;
}
if(listen(sock,10)<0)
{
perror("listen");
return 4;
}
return sock;
}
int main(int argc,char*argv[])
{
if(argc != 3)
{
usage(argv[0]);
return 1;
}
int listen_sock = start_up(argv[1],atoi(argv[2]));
int epfd = epoll_create(_SIZE_);
if(epfd<0)
{
perror("epoll_create");
return 5;
}
struct epoll_event ev;
struct epoll_event event[_MAXEVENTS_];
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
int ec = epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);
if(ec<0)
{
perror("epoll_ctl");
return 6;
}
int timeout = -1;
int nums = -1;
while(1)
{
int nums = epoll_wait(epfd,event,_MAXEVENTS_,timeout);
switch(nums)
{
case 0:
printf("timeout...\n");
break;
case -1:
perror("epoll_ctl error");
break;
default:
{
//至少有一个事件就绪
int i = 0;
for(i=0;i<nums;i++)
{
int sock = event[i].data.fd;
if(sock == listen_sock && (event[i].events & EPOLLIN))//listen_sock
{
//accept
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_sock<0)
{
perror("accept");
continue;
}
printf("get a client [%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
ev.events = EPOLLIN;
ev.data.fd = new_sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);
}
else if(sock != listen_sock)
{
if(event[i].events & EPOLLIN)
{
//read
char buf[10240];
ssize_t s = read(sock,buf,sizeof(buf));
if(s>0)
{
printf("client:%s\n",buf);
ev.events = EPOLLOUT;
ev.data.fd = sock;
epoll_ctl(epfd,EPOLL_CTL_MOD,sock,&ev);
}else if(s ==0)
{
printf("client quit...\n");
close(sock);
epoll_ctl(epfd,EPOLL_CTL_DEL,sock,&ev);
}else
{
perror("read error..\n");
close(sock);
epoll_ctl(epfd,EPOLL_CTL_DEL,sock,&ev);
}
}
else if(event[i].events & EPOLLOUT)
{
//write
const char* msg = "HTTP/1.1 OK 200\r\n\r\n<html><h1>hello world</h1></html>";
write(sock,msg,strlen(msg));
close(sock);
epoll_ctl(epfd,EPOLL_CTL_DEL,sock,&ev);
}
else
{}
}
}
}
break;
}
}
return 0;
}
总结
- select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。
- 而epoll其实也需要调用 epoll_ wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在 epoll_wait中进入睡眠的进程。
- 虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的 时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间,这就是回调机制带来的性能提升。
- select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内 部定义的等待队列),这也能节省不少的开销