poll与epoll
poll
poll函数接口
#include <poll.h>
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
//参数1 结构体指针(结构体数组的首地址)
//参数2 数组长度
//参数3 poll函数的超时时间(同select)
//返回值小于0,出错,等于0,poll函数等待超时
//大于0,表示poll由于监听的文件描述符就绪返回。
struct pollfd{
int fd;//文件描述符
short events;//输入参数 监听的事件集合(常用事件 POLLIN POLLOUT)
short revents;//输出参数 返回的事件集合
};
poll的优点(同select比较)
不同与select使⽤三个位图来表⽰三个fdset的⽅式,poll使⽤⼀个pollfd的指针实现
- pollfd结构包含了要监视的event和发⽣的event,不再使⽤select“参数-值”传递的⽅式. 接⼝使⽤⽐select更⽅便.
- poll并没有最⼤数量限制 (但是数量过⼤后性能也是会下降)
poll的缺点
- 和select函数⼀样,poll返回后,需要轮询pollfd来获取就绪的描述符.
- 每次调⽤poll都需要把⼤量的pollfd结构从⽤户态拷⻉到内核中.
- 同时连接的⼤量客户端在⼀时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增⻓,其效率也会线性下降
//使用epoll监控标准输入
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
int main(){
struct pollfd fds;
fds.fd = 0;//0标准输入
fds.events = POLLIN;
while(1){
int ret = poll(&fds,1,-1);//-1 永久阻塞
if(ret<0){
perror("poll");
return 1;
}
char buf[1024] = {0};
ssize_t read_size = read(0,buf,sizeof(buf)-1);
if(read_size < 0){
perror("read");
return 1;
}
if(read_size == 0){
printf("read done\n");
return 0;
}
buf[read_size] = '\0';
printf("rep = %s\n",buf);
}
return 0;
}
epoll
epoll的相关函数
创建一个epoll的句柄(验证其为文件描述符的方式:连接服务器时,第一个client为5,不是4),既然是文件描述符,用完要关闭。
int epoll_create(int size);
//size为任意值,无意义
epoll的事件注册函数
int epoll_ctl (int epfd,int op,int fd,struct epoll_event *event);
//参数1 epoll_create() 的返回值(句柄)
//参数2 表示处理事件的方式(3种)
//参数3 需要监听的文件描述符
//参数4结构体指针(操作方式)
参数2的三种方法
- EPOLL_CTL_ADD 注册新的fd到epfd;
- EPOLL_CTL_MOD 修改已经注册的fd的监听事件;
-
EPOLL_CTL_DEL 从epfd中删除一个fd;
struct epoll_event
epoll_event结构体里边有两个参数,一个events(相当于位图)其常用操作有EPOLLIN,EPOLLOUT。第二个参数为一个联合,其能更好的操作一个未知甚至更复杂的结构(含有一个void*的指针)。
events常见操作:
- EPOLLIN : 表⽰对应的⽂件描述符可以读 (包括对端SOCKET正常关闭);
- EPOLLOUT : 表⽰对应的⽂件描述符可以写;
- EPOLLPRI : 表⽰对应的⽂件描述符有紧急的数据可读 (这⾥应该表⽰有带外数据到来);
- EPOLLERR : 表⽰对应的⽂件描述符发⽣错误;
- EPOLLHUP : 表⽰对应的⽂件描述符被挂断;
- EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于⽔平触发(Level Triggered)来说的.(默认为LT模式1)
- EPOLLONESHOT:只监听⼀次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加⼊到EPOLL队列⾥
int epoll_wait(int epfd,struct epoll_event *event,int maxevents,int timeout);
//参数1 epoll句柄
//参数2 结构体数组首地址(同epoll_ctl的 *event)
//参数3 数组的大小
//参数4 超时时间
参数events是分配好的epoll_event结构体数组.
epoll将会把发⽣的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在⽤户态中分配内存).
maxevents告之内核这个events有多⼤,这个 maxevents的值不能⼤于创建epoll_create()时的size.
参数timeout是超时时间 (毫秒,0会⽴即返回,-1是永久阻塞).
如果函数调⽤成功,返回对应I/O上已准备好的⽂件描述符数目,如返回0表⽰已超时, 返回⼩于0表⽰函数失败
epoll的使用场景
对于多连接, 且多连接中只有⼀部分连接⽐较活跃时, ⽐较适合使⽤epoll
epoll的工作原理(底层红黑树存储,就绪存放于链表)
- 当某⼀进程调⽤epoll_create⽅法时,Linux内核会创建⼀个eventpoll结构体,这个结构体中有两个成员与epoll的使⽤⽅式密切相关
- 每⼀个epoll对象都有⼀个独⽴的eventpoll结构体,⽤于存放通过epoll_ctl⽅法向epoll对象中添加进来的事件.
- 这些事件都会挂载在红⿊树中,如此,重复添加的事件就可以通过红⿊树⽽⾼效的识别出来(红⿊树的插⼊时间效率是lgn,其中n为树的⾼度).
- ⽽所有添加到epoll中的事件都会与设备(网卡)驱动程序建⽴回调关系,也就是说,当响应的事件发⽣时会调⽤这个回调⽅法.
这个回调⽅法在内核中叫eppollcallback,它会将发⽣的事件添加到rdlist双链表中.
在epoll中,对于每⼀个事件,都会建⽴⼀个epitem结构体当调⽤epoll_wait检查是否有事件发⽣时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可.
- 如果rdlist不为空,则把发⽣的事件复制到⽤户态,同时将事件数量返回给⽤户. 这个操作的时间复杂度是O(1)
epoll的工作方式
- 假如有一个文件描述符就绪,携带1K的数据,epoll_wait返回,假设开始读数据,一次只能读0.5K的数据,那么缓冲区还留下0.5K,那么此时epoll_wait立刻再次返回,告诉服务器该文件描述符就绪,让其读完剩余的0.5K数据,这样的操作为水平触发。
- 边缘触发则是如上例如果1K的字节只读了0.5K,epoll_wait不会立刻返回,而是等到下次这个文件就绪后,返回,此时,服务器才有可能读取上次的缓存。故ET模式强制我们将缓冲区的数据一次性全部读取。所以只能是非阻塞读写。
水平触发(LT)
- 支持阻塞和非阻塞读写
边缘触发(ET)(效率高)
- 只支持非阻塞读写(ET(边缘触发)数据就绪只会通知⼀次,也就是说,如果要使⽤ET模式,当数据就绪时,需要⼀直read,直到出错或完成为⽌.但倘若当前fd为阻塞(默认),那么在当读完缓冲区的数据时,如果对端并没有关闭写端,那么该read函数会⼀直阻塞。)(想象僵持状态)
epoll的优点(同select比较)
- ⽂件描述符数目⽆上限: 通过epoll_ctl()来注册⼀个⽂件描述符, 内核中使⽤红⿊树的数据结构来管理所有需要监控的⽂件描述符.
- 基于事件的就绪通知⽅式: ⼀旦被监听的某个⽂件描述符就绪, 内核会采⽤类似于callback的回调机制, 迅速**这个⽂件描述符. 这样随着⽂件描述符数量的增加, 也不会影响判定就绪的性能;
- 维护就绪队列: 当⽂件描述符就绪, 就会被放到内核中的⼀个就绪队列中. 这样调⽤epoll_wait获取就绪⽂件描述符的时候, 只要取队列中的元素即可, 操作的时间复杂度是O(1);
- 从接口使用的角度上讲:不必每次都重新设置要监控的文件描述符,使接口使用方便,也能够避免用户态和内核态之间,来回的拷贝文件描述符结构。
LT
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
typedef struct epoll_event epoll_event;
void ProcessListenScok(int epoll_fd, int listen_sock){
sockaddr_in peer;
socklen_t len;
int new_sock = accept(listen_sock,(sockaddr*)&peer,&len);
if(new_sock <0 ){
perror("accept");
return;
}
//把 new_sock 加入到epoll之中
epoll_event event;
event.events = EPOLLIN;
event.data.fd = new_sock;
int ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,new_sock,&event);
if(ret < 0){
perror("epoll_ctl ADD");
return;
}
printf("[client %d] connected\n",new_sock);
return;
}
int ServerInit(const char* ip,short port){
int listen_sock = socket(AF_INET,SOCK_STREAM,0);
if(listen_sock < 0){
perror("socket");
return -1;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
int ret = bind(listen_sock,(sockaddr*)&addr,sizeof(addr));
if(ret < 0 ){
perror("bind");
return -1;
}
ret = listen(listen_sock,10);
if(ret < 0){
perror("listen");
return -1;
}
return listen_sock;
}
void ProcessNewSock(int epoll_fd,int new_sock){
char buf[1024] = {0};
ssize_t read_size = read(new_sock,buf,sizeof(buf)-1);
if(read_size < 0){
perror("read");
return;
}
//读到返回值为0,对端关闭了文件描述符。
//本端也应该关闭文件描述符,并且把文件描述符从epoll之中删除掉
if(read_size == 0){
close(new_sock);
epoll_ctl(epoll_fd,EPOLL_CTL_DEL,new_sock,NULL);
printf("[client %d] disconnected\n",new_sock);
return;
}
buf[read_size] = '\0';
printf("[client %d]say:%s\n",new_sock,buf);
write(new_sock,buf,strlen(buf));
return;
}
int main(int argc,char* argv[]){
if(argc != 3){
printf("Usage: ./server_epoll [ip] [port]\n");
return 1;
}
int listen_sock = ServerInit(argv[1],atoi(argv[2]));
if( listen_sock < 0){
perror("ServerInit");
return 1;
}
//创建并初始化epoll;
int epoll_fd = epoll_create(9);
if(epoll_fd < 0){
perror("epoll_create");
return 1;
}
//把listen_sock放置到epoll
epoll_event event;
event.events = EPOLLIN;
event.data.fd = listen_sock;
int ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_sock,&event);
if(ret < 0){
perror("epoll_ctl");
return -1;
}
printf("ServerInit OK!\n");
while(1){
epoll_event output_event[100];
int nfds = epoll_wait(epoll_fd,output_event,100,-1);
if(nfds < 0){
perror("epoll_wait");
continue;
}
//output_event[]里边保存着准备好的文件描述符
int i = 0;
for(;i < nfds;++i ){
if(listen_sock == output_event[i].data.fd){//读
ProcessListenScok(epoll_fd,listen_sock);
}
else{
ProcessNewSock(epoll_fd,output_event[i].data.fd);//增加
}
}//for
}//while
return 0;
}
ET