epoll
select速度慢的原因
- select函数遍历所有文件描述符
- 每次都需要向select函数传递监视对象信息
每次循环遍历所有监视对象,找出发生变化的文件描述符。
每次调用select函数时都需要向操作系统传递监视对象信息,实际上select函数是监视套接字变化的函数,而套接字由操作系统管理。
通过“仅向操作系统传递一次监视对象,监视范围或内容发生变化时只通知发生变化的事项。”的方式可以弥补select函数的缺点。
需要操作系统的支持,Linux是epoll,Windows是IOCP。
select优点
- 服务器端接入者少
- 程序具有兼容性(多数操作系统都支持select函数)
epoll相关函数和结构体
epoll从Linux2.5.44内核开始引入。
cat /proc/sys/kernel/osrelease
epoll函数优点:
- 无需循环以监视所有文件描述符的状态变化
- 调用对应select函数的epoll_wait函数时无需每次传递监视对象信息
- epoll_event
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_create
epoll方式下,操作系统负责保存监视对象文件描述符,因此需要使用epoll_create向操作系统请求创建保存epoll文件描述符的空间。
#include <sys/epoll.h>
int epoll_create(int size);
- epoll_ctl
通过epoll_ctl请求操作系统添加和删除监视对象文件描述符
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll(A, EPOLL_CTL_ADD, B, C); epoll例程A注册文件描述符B,监视C中事件。
epoll(A, EPOLL_CTL_ADD, B, NULL); epoll例程A删除文件描述符B。
-
epfd,epoll例程文件描述符。 -
op用于指定监视对象的添加、删除、更改等操作。 EPOLL_CTL_ADD:注册文件描述符 EPOLL_CTL_DEL:删除文件描述符(次数事件为NULL) EPOLL_CTL_MOD:更改文件描述符关注事件 -
event,关注的事件
struct epoll_event event;
event.events=EPOLLIN;
event.data.fd=sockfd;
epoll(epfd, EPOLL_CTL_ADD, sockfd, &event);
EPOLLIN:需要读取数据 EPOLLOUT:输出缓冲为空,可以立即发送数据 EPOLLPRI:收到OOB数据 EPOLLRDHUP:断开连接或半关闭,边缘触发有用 EPOLLERR:发生错误 EPOLLET:边缘触发得到事件通知 EPOLLONESHOT:发生一次事件后,相应文件描述符不在收到事件通知,需要EPOLL_CTL_MOD再次设置事件
- epoll_wait
类似select,等待文件描述符发生变化
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int event_cnt;
struct epoll_event *events;
events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
epoll回声服务器端
echo_epollserver.c
#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 *message);
int main(int argc, char *argv[]) {
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t addr_size;
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);
if(serv_sock==-1)
error_handling("socket() error");
addr_size = sizeof(struct sockaddr_in);
memset(&serv_addr, 0, serv_addr_size);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_addr, serv_addr_size)==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
epfd=epoll_create(EPOLL_SIZE);
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
event.events=EPOLLIN;
eventdata.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(i=0; i<event_cnt; i++) {
if(ep_events[i].data.fd==serv_sock) {
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &addr_size);
event.events=EPOLLIN;
eventdata.fd=clnt_sock;
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) {
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
close(ep_events[i].data.fd);
printf("closed 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(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
gcc echo_epollserver.c -o epollserver
./epollserver 9190
条件触发和边缘触发
条件触发(Level Trigger)和边缘触发(Edge Trigger)的区别在于发生事件的时间点。
条件触发特性,条件触发方式中,只要输入缓冲有数据就会一直通知该事件,多次注册事件。
边缘触发中输入缓冲收到数据时仅注册1次该事件。
条件触发,满足条件就触发(比如输入缓冲有数据)。 边缘触发,状态变化才触发(比如无缓冲状态变为有缓冲状态)。
条件触发的事件特性
epoll默认以条件触发方式工作。
echo_EPLTserver.c
#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 4
#define EPOLL_SIZE 50
void error_handling(char *message);
int main(int argc, char *argv[]) {
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t addr_size;
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);
if(serv_sock==-1)
error_handling("socket() error");
addr_size = sizeof(struct sockaddr_in);
memset(&serv_addr, 0, serv_addr_size);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_addr, serv_addr_size)==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
epfd=epoll_create(EPOLL_SIZE);
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
event.events=EPOLLIN;
eventdata.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;
}
puts("return epoll_wait")
for(i=0; i<event_cnt; i++) {
if(ep_events[i].data.fd==serv_sock) {
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &addr_size);
event.events=EPOLLIN;
eventdata.fd=clnt_sock;
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) {
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
close(ep_events[i].data.fd);
printf("closed 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(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
gcc echo_EPLTserver.c -o EPLTserver
./EPLTserver 9190
多次输出return epoll_wait
将 clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &addr_size); 后面 event.events=EPOLLIN; 修改为 event.events=EPOLLIN|EPOLLET; 仅输出一次return epoll_wait
select以条件触发方式工作。
边缘触发服务器必知两点
- errno变量验证错误原因
- 套接字特性设为非阻塞(Non-blocking)I/O
read函数发现缓冲中无数据可读时返回-1,同时在errno中保存EAGAIN常量。
#include <fcntl.h>
int fcntl(int filedes, int cmd, ...);
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
边缘触发服务器
边缘触发方式中,仅注册一次事件。 一旦发生输入事件,就应该读取输入缓冲中全部数据。
read返回-1,同时在errno值为EAGAIN,说明无数据可读。
边缘触发方式中,阻塞方式工作的read&write函数可能引起服务器端的长时间停顿,边缘触发方式中一定要采用非阻塞read&write函数。
echo_EPETserver.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#define BUF_SIZE 4
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);
void error_handling(char *message);
int main(int argc, char *argv[]) {
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t addr_size;
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);
if(serv_sock==-1)
error_handling("socket() error");
addr_size = sizeof(struct sockaddr_in);
memset(&serv_addr, 0, serv_addr_size);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_addr, serv_addr_size)==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
epfd=epoll_create(EPOLL_SIZE);
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
setnonblockingmode(serv_sock);
event.events=EPOLLIN;
eventdata.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;
}
puts("return epoll_wait")
for(i=0; i<event_cnt; i++) {
if(ep_events[i].data.fd==serv_sock) {
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &addr_size);
setnonblockingmode(clnt_sock);
event.events=EPOLLIN|EPOLLET;
eventdata.fd=clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client: %d\n", clnt_sock);
} else {
while(1) {
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("closed client: %d\n", ep_events[i].data.fd);
break;
} else if(str_len<0) {
if(errno==EAGAIN)
break;
}else {
write(ep_events[i].data.fd, buf, str_len);
}
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
void setnonblockingmode(int fd) {
int flag=fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}
void error_handling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
gcc echo_EPETserver.c -o EPETserver
./EPETserver 9190
条件触发与边缘触发优劣
边缘触发相对于条件触发的优点:可以分离接收数据和处理数据的时间点。
从实现模型的角度看,边缘触发更有可能带来高性能,但不能简单地认为只要使用边缘触发就一定能提高速度。
|