epoll
I/O 多路复用
I/O 多路复用主要是为了解决同时要等待多个文件描述符而导致的错误,它一般的使用场景
- 处理多个文件描述符(同时处理 一个 socket 或者 标准输入/输出)
- 处理多个socket (Server)
- 一个Server同时处理tcp 和 udp模块(一个端口号即绑定tcp 又绑定 udp)
- 处理多种服务或者协议(inetd)
它一般用于不帮我们直接 I/O,它只是帮我们通知事件就绪,对于tcp 和 udp的就绪条件如下
- udp一个完整的数据报到达
- tcp 模块收到的数据已经到达了低水位线
epoll的三个系统调用
int epoll_create(int size);
它会创建一个epoll模型在内存中,创建成功后返回该模型的文件描述符。size表最多能注册多少个文件描述符,在Linux 2.6.8 版本之后不在关心。
int epoll_ctl(int epfd,int op,int fd,struct epoll_event*event)
epfd :epoll模型的文件描述符。
op : 指将那个文件描述符,在红黑树中进行增、删、改的那个操作。
fd:指要操作的特定的文件描述符。
event:指特定文件描述符所关心的事件。
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
epfd : epoll 模型的文件描述符。
events: epoll_event结构体数组的地址。
maxevents:数组大小
timeout:等待超时的时间设置,单位为毫秒。当设为-1代表阻塞,0代表非阻塞,大于0代表轮询。
op参数
EPOLL_CTL_ADD
EPOLL_CTL_MOD
EPOLL_CTL_DEL
struct event_event结构体
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable ,大小为8字节是为了64位下指针为8字节*/
};
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
具体的模型:
写epoll服务器的步骤
step 1 :
创建出来一个listen_sock
具体为:
socket调用
setsockopt调用(设置后该socket的端口号,可被该主机的其他ip地址绑定,设置后服务器主动断开不再time_wait等待)
fcntl调用(可选项 LT模式可不调用,ET模式必须把该套接字调用设为非阻塞)
bind调用
listen调用step 2:
创建一个epoll模型,epoll_create调用
将listen_sock调用epoll_ctl,把监听套接字加入到红黑树中,并关心listen_sock的读事件。
调用epoll_wait,等待时间就绪
设置 switch case 分支语句step 3:
switch case {
-1:
表调用失败
0:
表超时
default:
表等待成功
for 循环遍历整个传入的event数组,遍历每个fd就绪事件,进行处理
}step 4:
遍历整个数组的逻辑:
if(fd是监听套接字){
accept 调用 (LT这里为if 确保不会被阻塞服务器进程,ET这里为while需遍历完所有数据以防被下次到来的数据覆盖)
将accept 的读事件先注册进红黑树(调用epoll_ctl)
}
else if(其他fd读事件就绪){
进行数据读取,并修改该套接字,将该套接字的写时间也注册进红黑树,一般只要该进程在跑,write事件立刻就绪。
}
else{
这里就是所有文件描述符的写事件了,
这里可以写我们自己想回复的数据
}
LT 模式
LT 模式
只要底层有数据,它会不断通知调用者去取走数据。只要数据没有被取走它会一直把相应事件标志为事件就绪。epoll默认为LT模式,我们可在epoll_ctl中设置为ET模式。
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <stdlib.h>
#define SIZE 10240
void usege(const char * arg)
{
printf("%s ip port\n",arg);
}
typedef struct epoll_buff{
int fd; //这个结构体很重要如果没有的话,数据就混乱了读的时候数据全读到buff中了,write的时候这个
char buf [SIZE];//buff 不知道到底是对应的是那个文件描述符
} epoll_buf,*epoll_buf_p;
epoll_buf_p alloc(int fd)
{
size_t len=sizeof(struct epoll_buff);
// printf("size:%d\n",len);
epoll_buf_p ret =(epoll_buf_p) malloc(len);
if (ret ==NULL) {
perror("malloc");
exit(10);
}
ret->fd=fd;
return ret;
}
int startup(const char * ip,const char * port)
{
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd<0) {
perror("socket");
exit(1);
}
int opt = 1;
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in sock_addr;
sock_addr.sin_family=AF_INET;
sock_addr.sin_port = htons(atoi(port));
sock_addr.sin_addr.s_addr=inet_addr(ip);
socklen_t len=sizeof(sock_addr);
if((bind(fd,(struct sockaddr*)&sock_addr,len))<0)
{
perror("bind");
exit(2);
}
if(listen(fd,128)<0)
{
perror("listen");
exit(3);
}
return fd;
}
int main(int argv,const char * args[])
{
if(argv != 3)
{
usege(args[0]);
exit(4);
}
int listen_sock=startup(args[1],args[2]);
int epfd=epoll_create(256);
// printf("lsock%d , epfd%d\n",listen_sock,epfd);
if(epfd<0)
{ perror("epoll_create");}
struct epoll_event ev;
struct epoll_event env[32];
ev.events=EPOLLIN;
ev.data.ptr=alloc(listen_sock);
if(epoll_ctl(epfd, EPOLL_CTL_ADD,listen_sock,&ev)<0)
{
perror("epoll_ctl");
exit(5);
}
while(1)
{
int timeout = -1;
int reve_n = epoll_wait(epfd,env,sizeof(env)/sizeof(env[0]),timeout);
// printf("reve_n :%d\n",reve_n);
switch (reve_n)
{
case 0:
printf("time out \n");
break;
case -1:
perror("epoll_wait");
exit(6);
default:
{ int idx=0;
for(idx;idx<reve_n;idx++)
{
epoll_buf_p p=(epoll_buf_p)env[idx].data.ptr;
// printf("p->fd %d\n",p->fd);
if(p->fd == listen_sock&&env[idx].events==EPOLLIN)
{ struct sockaddr_in cilent ;
socklen_t len=sizeof(cilent);
int sock=0;
if((sock=accept(listen_sock,(struct sockaddr*)&cilent,&len))>0) // while ET
{
ev.events=EPOLLIN;
ev.data.ptr=alloc(sock);
if(epoll_ctl(epfd, EPOLL_CTL_ADD,sock,&ev)<0)
{
perror("epoll_ctl");
exit(7);
}
const char * cilent_ip=inet_ntoa(cilent.sin_addr);
int cilent_port=ntohs(cilent.sin_port);
printf("cilent connect ip:%s port:%u \n",cilent_ip,cilent_port);
}
if(sock<0) {
perror("accept");
}
continue;
}
else if(env[idx].events==EPOLLIN)
{
int res=read(p->fd,p->buf,SIZE);
if(res<0){
perror("read");
exit(8);
}
else if(res==0)
{
printf("cilent quit!!\n");
close(p->fd);
free(p);
epoll_ctl(epfd,EPOLL_CTL_MOD,p->fd,NULL);
}
else {
// if((p->buf)[res-2]=='\r')
// (p->buf)[res-2]=0;
// else {
// (p->buf)[res-1]=0;
// }
(p->buf)[res]=0;
printf("####cilent : %s",p->buf);
fflush(stdout);
}
ev.events=(env[idx]).events|EPOLLOUT;
if(epoll_ctl(epfd,EPOLL_CTL_MOD,p->fd,&ev)<0)
{
perror("epoll_ctl");
exit(9);
}
} else{ // end read
const char * temp ="HTTP/1.1 200 OK\r\n Content-Length :%s \r\n\r\n wo xi huan yutian !!! ";
int ret= sprintf(p->buf,"%s",temp);
write(p->fd,p->buf,ret);
epoll_ctl(epfd,EPOLL_CTL_DEL,p->fd,NULL);
close(p->fd);
// ev.events=EPOLLIN;
// if(epoll_ctl(epfd,EPOLL_CTL_MOD,p->fd,&ev)<0)
// {
// perror("epoll_ctl");
// exit(9);
// }
}
}
}
break;
}// end switch
} // end while(1)
close(epfd);
return 0;
}
ET 模式
ET模式是epoll的高效模式,可以在每次进行epoll_ctl时进行设置为EPOLLET时,该文件描述符以ET模式工作。ET模式只在数据从无到有时通知一次,并且ET模式下工作的套接字应为非阻塞套接字。所以要注意当时间就绪时,要一次性把数据都读取完。
在ET模式下设置为非阻塞套接字十分重要,因为我们不知道下次是否还有数据没有,如果没有我们阻塞住了那么整个Server就阻塞住了。
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#define SIZE 4096
void usege(const char * arg)
{
printf("%s ip port\n",arg);
}
typedef struct epoll_buff{
int fd;
char buf [SIZE];
} epoll_buf,*epoll_buf_p;
epoll_buf_p alloc(int fd)
{
size_t len=sizeof(struct epoll_buff);
// printf("size:%d\n",len);
epoll_buf_p ret =(epoll_buf_p) malloc(len);
if (ret ==NULL) {
perror("malloc");
exit(10);
}
ret->fd=fd;
return ret;
}
static void set_noblock(int fd)
{
int fd_flag = fcntl(fd,F_GETFL);
if(fcntl(fd,F_SETFL,fd_flag|O_NONBLOCK)<0)
{
perror("fcntl");
exit(11);
}
}
void Read(epoll_buf_p p,int epfd, struct epoll_event * ev_arr)
{
while((res=read(p->fd,p->buf,SIZE))>0)
{
(p->buf)[res]=0;
printf("#### cilent : %s",p->buf);
fflush(stdout);
}
if(res==0)
{
printf("cilent quit");
fflush(stdout);
if(epoll_ctl(epfd,EPOLL_CTL_DEL,p->fd,NULL)<0)
{
perror("epoll_ctl");
exit(12);
}
close(p->fd);
}
if(res<0&&errno!=EAGAIN)
{
perror("read");
exit(13);
}
struct epoll_event ev;
ev.events = ev_arr->events|EPOLLOUT; //这步是为了将关心的事件改为即关心读又关心写
ev.data.ptr=p;
if(epoll_ctl(epfd,EPOLL_CTL_MOD,p->fd,&ev)<0)
{
perror("epoll_ctl");
}
}
void Write(epoll_buf_p p,int epfd)
{
const char * temp ="HTTP/1.1 200 OK\r\n Content-Length :%s \r\n\r\n wo xihuan yutian !!! ";
int ret= sprintf(p->buf,"%s",temp);
write(p->fd,p->buf,ret);
epoll_ctl(epfd,EPOLL_CTL_DEL,p->fd,NULL);
close(p->fd);
}
int startup(const char * ip,const char * port)
{
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd<0) {
perror("socket");
exit(1);
}
set_noblock(fd);
int opt = 1;
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in sock_addr;
sock_addr.sin_family=AF_INET;
sock_addr.sin_port = htons(atoi(port));
sock_addr.sin_addr.s_addr=inet_addr(ip);
socklen_t len=sizeof(sock_addr);
if((bind(fd,(struct sockaddr*)&sock_addr,len))<0)
{
perror("bind");
exit(2);
}
if(listen(fd,128)<0)
{
perror("listen");
exit(3);
}
return fd;
}
int main(int argv,const char * args[])
{
if(argv != 3)
{
usege(args[0]);
exit(4);
}
int listen_sock=startup(args[1],args[2]);
int epfd=epoll_create(256);
// printf("lsock%d , epfd%d\n",listen_sock,epfd);
if(epfd<0)
{ perror("epoll_create");}
struct epoll_event ev;
struct epoll_event env[32];
ev.events=EPOLLIN|EPOLLET;
ev.data.ptr=alloc(listen_sock);
if(epoll_ctl(epfd, EPOLL_CTL_ADD,listen_sock,&ev)<0)
{
perror("epoll_ctl");
exit(5);
}
while(1)
{
int timeout = -1;
int reve_n = epoll_wait(epfd,env,sizeof(env)/sizeof(env[0]),timeout);
// printf("reve_n :%d\n",reve_n);
switch (reve_n)
{
case 0:
printf("time out \n");
break;
case -1:
perror("epoll_wait");
exit(6);
default:
{ int idx=0;
for(idx;idx<reve_n;idx++)
{
epoll_buf_p p=(epoll_buf_p)env[idx].data.ptr;
if(p->fd == listen_sock&&env[idx].events&EPOLLIN)
{ struct sockaddr_in cilent ;
socklen_t len=sizeof(cilent);
int sock=0;
while((sock=accept(listen_sock,(struct sockaddr*)&cilent,&len))>0) // while ET
{
set_noblock(sock);
ev.events=EPOLLIN|EPOLLET;
ev.data.ptr=alloc(sock);
if(epoll_ctl(epfd, EPOLL_CTL_ADD,sock,&ev)<0)
{
perror("epoll_ctl");
exit(7);
}
const char * cilent_ip=inet_ntoa(cilent.sin_addr);
int cilent_port=ntohs(cilent.sin_port);
printf("cilent connect ip:%s port:%u \n",cilent_ip,cilent_port);
}
if(sock<0) {
perror("accept");
}
continue;
}
else if(env[idx].events&EPOLLIN)
{
Read(p,epfd, env[idx]);
}
else{
Write(p,epfd);
}
}
}
break;
}// end switch
} // end while(1)
return 0;
}
epoll 服务器优缺点
优点
1 它其中所等待的文件描述符没有上限,具体极限跟硬件设备内存有关。
2 以高效的回调方式通知事件就绪,所以通知事件就绪的时间复杂度为O(1)
3 epoll模型,底层是以红黑树构成,增删查改很高效
4 以回调方式**就绪节点,很高效。
5 epoll_wait的返回值为就绪事件的个数,所以上层每一次遍历都为有效遍历。
6 使用内存映射技术,将就绪队列映射到event数组上,不需再次进行拷贝数据给上层event数组
缺点
1 代码书写复杂,难以调试。