select
select函数允许进程指示内核等待多个事件中的任何一个发生,只有在一个或多个事件(描述符状态变化)发生或者阻塞时间超时会返回.
select的接口如下:
#include<sys/select.h>
#include<sys/time.h>
int select(int maxfdp1,fd_set* readset,fd_set* writeset,fd_set* exceptset,const struct timeval* timeout);
其中第一个参数表示所有关心的描述符总数,即后面三个集合中描述符的数量和,后面三个参数分别表示测试读写和异常条件的描述符.
最后一个参数表示最大阻塞时间,即超过时间没有描述符就绪则返回.
struct timeval{
long tv_sec;// 秒数
long tv_usec;// 微妙数
};
fd_set这个结构的初始化和赋值使用已定义的宏:
void FD_ZERO(fd_set \*fdset); //初始化清零
void FD_SET(int fd,FD_SET \*fdset); //设置某一位描述符
void FD_CLR(int fd,FD_SET \*fdset); //清除某一位描述符
int FD_ISSET(int fd,FD_SET \*fdset); //测试是否有指定位描述符
对于select返回某个套接字就绪的条件,总结如下:
条件 | 可读吗? | 可写吗? | 异常吗? |
---|---|---|---|
有数据可读 | Y | ||
关闭连接的读一半 | Y | ||
给监听套接口准备好新连接 | Y | ||
有可用于写的空间 | Y | ||
关闭连接的写一半 | Y | ||
待处理错误 | Y | Y | |
TCP带外数据 | Y |
说明:关闭套接字的读一半后,执行读操作会立即返回EOF,所以也算作可读状态,关闭写一半,执行写操作会立即返回SIGPIPE信号,算作可写状态.
poll
poll功能和select相似,在处理流设备时,能提供额外信息.
poll函数接口原型:
#include<poll.h>
int poll(struct pollfd *fdarray,unsigned long nfds,int timeout);
结构体 pollfd的定义:
struct pollfd{
int fd; //要测试的描述符
short events; //感兴趣的事件(读写异常)
short revents; //发生的事件
};
POSIX定义了events标志的一些常值
常值 | 作为events输入吗 | 作为revents输出吗 | 说明 |
---|---|---|---|
POLLIN | Y | Y | 普通或优先级带数据可读 |
POLLRDNORM | Y | Y | 普通数据可读 |
POLLOUT | Y | Y | 普通数据可写 |
POLLERR | Y | 发生错误 |
参数nfds表示套接字的数量
epoll
select的缺点有两个,第一,针对每个描述符的循环语句.第二,每次调用都要向操作系统传递套接字信息,造成无法优化的负担.
为了解决第二个问题,epoll仅向操作系统传递一次监视对象,监视范围或者内容发生变化式只通知发生变化的事项(注意,Linux才支持epoll)
epoll实现需要3个函数接口:
- epoll_create:创建保存epoll文件描述符的空间
- epoll_ctl:向空间注册并注销文件描述符
- epoll_wait:与select函数类似,等待文件描述符的变化.12345678910111213141516int epoll_create(int size);//成功返回文件描述符,创建的fd保存空间在内核中,size为建议的空间大小,用完需要调用close关闭.int epoll_ctl(int epfd,int op,int fd,struct epoll_event \*event);//用来添加需要监视的fd。int epoll_wait(int epfd,struct epoll_event \*events,int maxevents,int timeout);//成功返回就绪fd数量struct epoll_event{__uint32_t events;epoll_data_t data;}typedef union epoll_data{void* ptr;int fd;__uint32_t u32;__uint64_t u64;}epoll_data_t;
epoll_ctl中op选项:
- EPOLL_CTL_ADD
- EPOLL_CTL_DEL
- EPOLL_CTL_MOD:更改出册的文件描述符的关注事件发生情况
epoll_events中的events常用值:
- EPOLLIN:需要读取数据的情况
- EPOLLOUT:输出缓冲为空,可以立即发送数据的情况
- EPOLLRDHUP:断开连接或者半关闭的情况
- EPOLLERR:错误情况
- EPOLLLET:以边缘触发的方式
- EPOLLONESHOT:发生一次事件后,相应描述符不在收到事件通知(所以需要EPOLL_CTL_MOD选项.再次设置)
epoll_wait中events保存就绪的描述符,所以需要给它动态分配缓存区,maxevents用来指示第二个参数最大数量.
#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
const int BUF_SIZE=100;
const int EPOLL_SIZE=50;
void error_handling(const char* message);
int main(int argc,char** argv)
{
int serv_sock,clnt_sock;
struct sockaddr_in serv_adr,clnt_adr;
char buf[BUF_SIZE];
struct epoll_event* ep_events;
struct epoll_event event;
int epfd,event_cnt;
if(argc!=2){
printf("Usage: %s<port>\n",argv[0]);
exit(1);
}
serv_sock=socket(PF_INET,SOCK_STREAM,0);
memset(&serv_adr,0,sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock,5)==-1)
error_handling("listen() error");
//创建epoll
epfd=epoll_create(EPOLL_SIZE);
ep_events=(struct epoll_event*)malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
event.events=EPOLLIN;
event.data.fd=serv_sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,serv_sock,&event);
//监听
while(1)
{
event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
if(event_cnt==-1){
puts("epoll_wait() error");
break;
}
for(int i=0;i<event_cnt;i++){
if(ep_events[i].data.fd==serv_sock)
{
socklen_t adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr,&adr_sz);
event.events=EPOLLIN;
event.data.fd=clnt_sock;
epoll_ctl(epfd,EPOLL_CTL_ADD,clnt_sock,&event);
printf("connected client: %d \n",clnt_sock);
}else{
int str_len=read(ep_events[i].data.fd,buf,BUF_SIZE);
if(str_len==0)
{
epoll_ctl(epfd,EPOLL_CTL_DEL,ep_events[i].data.fd,NULL);
close(ep_events[i].data.fd);
printf("Disconnected client: %d \n",ep_events[i].data.fd);
}else{
write(ep_events[i].data.fd,buf,str_len);
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
void error_handling(const char* message){
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
条件触发和边缘出发
条件触发:只要满足条件,就触发一个事件(例如缓冲区的数据没有读完,内核会一直通知)
边缘触发:每当状态变化时,触发一个事件.
“举个读socket的例子,假定经过长时间的沉默后,现在来了100个字节,这时无论边缘触发和条件触发都会产生一个read ready notification通知应用程序可读。应用程序读了50个字节,然后重新调用api等待io事件。这时水平触发的api会因为还有50个字节可读从 而立即返回用户一个read ready notification。而边缘触发的api会因为可读这个状态没有发生变化而陷入长期等待。 因此在使用边缘触发的api时,要注意每次都要读到socket返回EWOULDBLOCK为止,否则这个socket就算废了。而使用条件触发的api 时,如果应用程序不需要写就不要关注socket可写的事件,否则就会无限次的立即返回一个write ready notification。大家常用的select就是属于水平触发这一类,长期关注socket写事件会出现CPU 100%的毛病。
引用自网友博客的总结:
epoll的优点:
1.支持一个进程打开大数目的socket描述符(FD)
select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显 然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完 美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
2.IO效率不随FD数目增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是”活跃”的, 但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对”活跃”的socket进行 操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有”活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个”伪”AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的—比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
3.使用mmap加速内核与用户空间的消息传递。
这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就 很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记手工 mmap这一步的。
4.内核微调
这一点其实不算epoll的优点了,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。 比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么可以在运行时期动态调整这个内存pool(skb_head_pool)的大小 — 通过echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手 的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网 卡驱动架构。