上一节(https://www.cnblogs.com/yuanwebpage/p/12362876.html)记录了多路IO复用的第一种方式select函数,以及其相应的缺点。本节记录多路IO复用的第二种方式epoll(在windows系统下叫IOCP)。
1. epoll相关函数
epoll函数克服了select函数的相关缺点,其优点如下:
(1) 只需向OS注册一次文件描述符集合,不用每次循环传递;
(2) epoll函数会将发生变化的文件描述符单独集中起来,这样每次遍历时只需要遍历发生变化的文件描述符。
(3) 相对于select同时监听的数量有限制,epoll监听数量一般远大于select,这对于多连接的服务器至关重要。
epoll用来集中通知变化的文件描述符结构体如下:
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;
//可以看到,常用的为events, fd两个
epoll相关的函数总共有3个:
#include <sys/epoll.h>
int epoll_create(int size); //向OS申请创建管理所有文件描述符的epoll例程,返回该例程的文件描述符
size:可能注册的最大监视事件,仅供OS参考
int epoll_ctl(int epfd, int op, struct epoll_event* event); //成功返回0,失败-1,用于田间/删除/修改某个文件描述符
epfd:epoll_create返回的该epoll例程的描述符
op:具体的操作,如添加/删除,常用以下三种模式
EPOLL_CTL_ADD:将fd的描述符注册到epfd,等价于FD_SET
EPOLL_CTL_DEL:将fd的描述符从epfd移出,等价于FD_CLR
EPOLL_CTL_MOD:修改fd所指描述符的监听类型
event:struct epoll_event结构体,内有一个event变量,指明需要监听的具体类型
EPOLLiN:输入事件
EPOLLOUT:输出事件
EPOLLET:以边缘触发方式接收事件通知(稍后详述)
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout); //类似于select,调用后监听发生变化的描述符,成功时返回发生事件的个数
epfd:epoll例程描述符
events:动态申请的结构体数组,用于保存,通知发生变化的文件描述符
maxevents:监视的最大事件数目,即events数组的大小
timeout:设置超时,单位为ms。-设置-1为无限等待
使用完了epoll例程后记得调用close()关闭。
有了以上epoll相关函数就很容易将之前两节的回声服务器服务器端改写为epoll形式的IO复用,完整代码如下:

1 #include <stdlib.h>
2 #include <stdio.h>
3 #include <string.h>
4 #include <sys/socket.h>
5 #include <arpa/inet.h>
6 #include <unistd.h>
7 #include <sys/epoll.h>
8 #define EPOLL_SIZE 30 //定义监视事件的数组数量
9
10 void error_handle(const char* msg)
11 {
12 fputs(msg,stderr);
13 fputc('\n',stderr);
14 exit(1);
15 }
16
17 int main(int argc,char* argv[])
18 {
19 //服务器建立连接
20 int servsock,clntsock;
21 struct sockaddr_in servaddr,clntaddr;
22 char message[50];
23 socklen_t clntlen;
24
25 if(argc!=2)
26 error_handle("Please input port number");
27
28 servsock=socket(PF_INET,SOCK_STREAM,0); //1.建立套接字
29
30 memset(&servaddr,0,sizeof(servaddr));
31 servaddr.sin_family=AF_INET;
32 servaddr.sin_addr.s_addr=htonl(INADDR_ANY); //默认本机IP地址
33 servaddr.sin_port=htons(atoi(argv[1]));
34
35 if(bind(servsock,(struct sockaddr*)&servaddr,sizeof(servaddr))==-1)
36 error_handle("bind error"); //2.建立连接
37
38 if(listen(servsock,10)==-1) //3.监听建立
39 error_handle("listen() error");
40
41 //到此的代码为socket创建过程,与前两节相同
42
43 //关于epoll的相关函数
44 epoll_event event;
45 event.events=EPOLLIN; //输入监听
46 event.data.fd=servsock;
47 int epfd=epoll_create(20); //创建epoll例程
48 epoll_ctl(epfd,EPOLL_CTL_ADD,servsock,&event); //向epfd添加fd的输入监听事件
49 struct epoll_event* events;
50 events=(epoll_event*)malloc(sizeof(struct epoll_event)*EPOLL_SIZE); //申请存放发生事件的数组
51 while(1)
52 {
53 int event_cnt=epoll_wait(epfd,events,EPOLL_SIZE,-1); //设置无限等待
54 if(event_cnt==-1)
55 printf("epoll_wait() error");
56 for(int i=0;i<event_cnt;i++)
57 {
58 if(events[i].data.fd==servsock) //说明是新的客户端请求
59 {
60 clntlen=sizeof(clntaddr);
61 clntsock=accept(servsock,(struct sockaddr*)&clntaddr,&clntlen);
62 if(clntsock==-1) //accept错误
63 {
64 close(clntsock);
65 continue;
66 }
67 event.events=EPOLLIN;
68 event.data.fd=clntsock;
69 epoll_ctl(epfd,EPOLL_CTL_ADD,clntsock,&event); //将新申请的连接加入epfd
70 printf("connecting\n");
71 }
72 else //客户端发来消息
73 {
74 int strlen=read(events[i].data.fd,message,sizeof(message));
75 if(strlen==0){ //关闭连接请求
76 epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL); //从epfd中删除
77 close(events[i].data.fd);
78 }
79 else
80 write(events[i].data.fd,message,strlen);
81 }
82 }
83 }
84 close(epfd); //关闭epoll例程
85 close(servsock);
86 return 0;
87 }
注,代码中用malloc申请的events数组,也可以直接申请struct epoll_event events[EPOLL_SIZE];
2. 条件触发和边缘触发
上面在提到的在epoll_ctl加入新的描述符时,有一个选项:EPOLLET,即以边缘触发方式响应,1中代码的方式是条件触发,那么条件触发和边缘触发有什么区别呢?条件触发只要输入缓冲中有数据,epoll_wait就能检测到并将其注册到通知描述符的events数组中;边缘触发只在接收缓冲进入数据时在events数组中注册一次,此后缓冲区的数据即时没读完,仍不再注册。举个例子,假设输入缓冲中来了20个字节的数据,每次读取4个字节,那么条件触发方式在循环时每次调用epoll_wait()都向events数组注册输入事件,边缘触发只在数据进入时触发一次,此后不再触发。如果按照这种方式工作,边缘触发将使输入缓冲的数据不断累积,造成溢出(PS:select函数其实可以算作条件触发)。
那么怎么检测输入缓冲中是否有数据呢?Linux在<error.h>中声明了全局变量errno,read函数发现输入缓冲中没有数据时返回-1,同时在errno中保存EAGAIN常量。那么怎么设置为边缘触发模式呢?这里要用到fcntl函数:
#include <fcntl.h>
void setnonblock(int fd){ int flag=fcntl(fd, F_GETFL, 0); //获取描述符fd的属性 fcntl(fd, F_SETFL, flag|O_NONBLOCK); //添加非阻塞属性}
为了将条件触发改成边缘触发,在1中完整的代码上进行以下改动

头文件中添加
#include <fcntl.h>
#include <error.h>
45行:
event.events=EPOLLIN|EPOLLET;
在46行之后添加:
setnonblock(servsock);
67行:
event.events=EPOLLIN|EPOLLET;
68行之后添加:
setnonblock(clntsock);
此后在接收客户端消息的代码如下:
else //接收客户端消息
{
while(1) //循环读写直到输入缓冲为空
{
int strlen=read(events[i].data.fd,message,sizeof(message)); //read函数此时不再阻塞
if(strlen==0){
epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL); //从epfd中删除
close(events[i].data.fd);
}
else if(strlen<0)
{
if(errno==EAGAIN) break;
}
else
write(events[i].data.fd,message,strlen);
}
}
关于边缘触发和条件触发的优缺点和应用场景,目前还没有发现比较好的资料。
来源:https://www.cnblogs.com/yuanwebpage/p/12365123.html
