实现I/O复用的传统方法有select 函数和poll 函数。使用select函数无法得到令人满意的性能。

因此有了Linux 下的epoll 、BSD 的kqueue、Solaris 的/dev/poll和Windows的IOCP 等复用技术。

select可以参考:网编(11):服务器端的I/O复用

select复用方法其实由来巳久,因此,利用该技术后,无论如何优化程序性能也无法同时接入上百个客户端(当然,硬件性能不同, 差别也很大)。这种select方式并不适合以Web服务器端开发为主流的现代开发环境,所以要学习Linux平台下的epoll。

基于select 的I/O复用技术速度慢的原因

  • 调用select函数后,需要对所有文件描述符执行循环检查语句。
  • 每次调用select函数时都需要向该函数传递监视对象信息。

循环语句不是最大的麻烦,每次传递监视对象信息到操作系统中才是最大的麻烦。
应用程序向操作系统传递数据将对程序造成很大负担,而且无法通过优化代码解决,因此将成为性能上的致命弱点。

select函数与文件描述符有关,更准确地说,是监视套接字变化的函数。而套接字是由操作系统管理的,所以select函数绝对需要借助于操作系统才能完成功能。select函数的这一缺点可以通过如下方式弥补:

"仅向操作系统传递1 次监视对象,监视范围或内容发生变化时只通知发生变化的事项。”

这样就无需每次调用select函数时都向操作系统传递监视对象信息,但前提是操作系统支持这种处理方式(每种操作系统支持的程度和方式存在差异)。Linux的支待方式是epoll, Windows的支持方式是IOCP 。
 

select 的优点
epoll方式只在Linux下提供支持,也就是说,改进的I/O复用模型不具有兼容性。相反,大部分操作系统都支持select函数。只要满足或要求如下两个条件, 即使在Linux平台也不应拘泥于epoll 。

  • 服务器端接入者少。
  • 程序应具有兼容性。

epoll函数具有如下优点,这些优点正好与之前的select函数缺点相反。

  • 无需编写以监视状态变化为目的的针对所有文件描述符的循环语句。
  • 调用对应于select函数的epoll_wait函数时无需每次传递监视对象信息。

epoll服务器端实现中需要的3个函数,

  • epoll_ create: 创建保存epoll文件描述符的空间。
  • epoll_ctl: 向空间注册并注销文件描述符。
  • epoll_wait: 与select函数类似,等待文件描述符发生变化。


select方式

  • 用fd_set变量保存监视对象文件描述符。
  • 用FD_SET,FD_CLR添加和删除对象。
  • select函数等待文件描述符的变化。
  • 通过FD_ISSET查看状态变化的文件描述符。

epoll方式

  • 操作系统负责保存监视对象文件描述符,因此需要向操作系统请求创建保存文件描述符的空间。
  • 通过epoll_ctl 函数请求操作系统添加和删除对象。
  • epoll_wait函数等待文件描述符的变化。
  • 通过结构体epoll_event将发生变化的文件描述符单独集中到一起。


结构体:

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_even结构体数组后,传递给epoll_wait函数时.发生变化的文件描述符信息将被填入该数组。因此, 无需像select函数那样针对所有文件描述符进行循环。

epoll_ create

#include <sys/epoll. h>
int epoll_create(int size);
//成功时返回epoll 文件描述符,失败时返回-1。
#size epoll实例的大小。

Linux 2 . 6 . 8 之后的内核将完全忽略传入epoll_create 函数的size 参数,因为内核会根据情况调整epoll 例程的大小。

epoll_ create函数创建的资源与套接字相同,也由操作系统管理。因此, 该函数和创建套接字的情况相同,也会返回文件描述符。也就是说,该函数返回的文件描述符主要用与于区分epoll例程。需要终止时,与其他文件描述符相同,也要调用close函数。

epoll_ctl

#include <sys/epoll. h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event);
//成功时返回0 , 失败时返回-1 。


#epfd 用千注册监视对象的epoll例程的文件描述符。
#op 用千指定监视对象的添加、删除或更改等操作。
#fd 需要注册的监视对象文件描述符。
#event 监视对象的事件类型。

用法:

epoll_ctl(A, EPOLL_CTL_ADD, B, C);

第二个参数EPOLL_ CTL _ ADD意味着“添加“,因此上述语句具有如下含义:
"epoll例程A 中注册文件描述符B, 主要目的是监视参数C 中的事件。”

epoll_ctl(A, EPOLL_CTL_DEL, B, NULL);

第二个参数EPOLL_CTL_DEL指“删除“,因此该语句具有如下含义:
“从epoll例程A 中删除文件描述符B 。”

epoll_ctl第二个参数传递的常量及含义。

  • EPOLL_CTL_ADD: 将文件描述符注册到epoll例程。
  • EPOLL_CTL_DEL: 从epoll例程中删除文件描述符。
  • EPOLL CTL MOD: 更改注册的文件描述符的关注事件发生情况。


在epoll例程中注册文件描述符时,用于注册关注的事件。

struct epoll_event event;
event.events=EPOLLIN; //发生需要读取数据的情况(事件) 时
event.data.fd=sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
  • EPOLLIN: 需要读取数据的情况。
  • EPOLLOUT: 输出缓冲为空,可以立即发送数据的情况。
  • EPOLLPRI: 收到OOB数据的情况。
  • EPOLLRDHUP: 断开连接或半关闭的情况,这在边缘触发方式下非常有用。
  • EPOLLERR: 发生错误的情况。
  • EPOLLET: 以边缘触发的方式得到事件通知。
  • EPOLLONESHOT: 发生一次事件后,相应文件描述符不再收到事件通知。因此需要向epoll_ctl 函数的第二个参数传递EPOLL_CTL_MOD , 再次设置事件。

epoll_wait

#include <sys/epoll. h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//成功时返回发生事件的文件描述符数,失败时返回-1。

#epfd 表示事件发生监视范围的epoll例程的文件描述符。
#events 保存发生事件的文件描述符集合的结构体地址值。
#maxevents 第二个参数中可以保存的最大事件数。
#timeout 以1/1000秒为单位的等待时间,传递-1 时, 一直等待直到发生事件。

第二个参数所指缓冲需要动态分配。

int event_cnt;
struct epoll_event * ep_events;
ep_events = malloc(sizeof(struct epoll_event}*EPOLL_SIZE); //EPOLL_SIZE 是宏常量
event cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);

服务器:

    struct epoll_event *ep_events;  //用来装载发生的事件
    struct epoll_event event;  //用来装载要添加或者删除的事件

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *buf);

int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t adr_sz;
int str_len, i;
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");

epfd=epoll_create(EPOLL_SIZE);//第一步:创建epoll文件描述符
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);//第二步:创建事件的内存空间

event.events=EPOLLIN;//第三步:设置事件的类型
event.data.fd=serv_sock;//第四步:添加要监控的文件描述符
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);//第五步:添加到epoll文件描述符种

while(1)
{
//第六步:返回事件发生的文件描述符
event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if(event_cnt==-1)
{
puts("epoll_wait() error");
break;
}

for(i=0; i<event_cnt; i++ )
{
if(ep_events[i].data.fd==serv_sock)
{
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中
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client : %d \n", clnt_sock);
}
else
{
str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
if (str_len==0)//close request!
{
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);

//从epoll中删除连接到服务器的文件描述符
close(ep_events[i].data.fd);
printf("closed client: %d \n", ep_events[i].data.fd);
}
else{
buf[str_len] = '\0';
write(ep_events[i].data.fd, buf, str_len); // echo!
memset(buf, 0, sizeof(buf));
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}

void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

客户端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>


#define BUF_SIZE 1024

void error_handling(char *message);

int main(int argc, char* argv[])
{
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;

if(argc!=3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}

sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error");

memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));

if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
error_handling("connect() error I");

while(1)
{
fputs("Input message(Q to quit) : ", stdout);
fgets(message, BUF_SIZE, stdin);

if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;

write(sock, message, strlen(message));
memset(message, 0, sizeof(message));

str_len=read(sock, message, BUF_SIZE-1);
message[str_len]='\0';
printf("Message from server: %s", message);

memset(message, 0, sizeof(message));
}

close(sock);
return 0;
}

void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

epoll条件触发和边缘触发

条件触发:

只要输入缓冲有数据就会一直触发事件,直到缓冲区没有数据。

1. 对于读操作

只要缓冲内容不为空,LT模式返回读就绪。

2. 对于写操作

只要缓冲区还不满,LT模式会返回写就绪。

边缘触发

1. 对于读操作

(1)当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候。

(2)当有新数据到达时,即缓冲区中的待读数据变多的时候。

(3)当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时。

2. 对于写操作

(1)当缓冲区由不可写变为可写时。

(2)当有旧数据被发送走,即缓冲区中的内容变少的时候。

(3)当缓冲区有空间可写,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLOUT事件时。

可以参考:

网编(17):epoll条件(水平)触发和边缘触发