每次循环遍历所有监视对象,找出发生变化的文件描述符。
每次调用select函数时都需要向操作系统传递监视对象信息,实际上select函数是监视套接字变化的函数,而套接字由操作系统管理。
通过“仅向操作系统传递一次监视对象,监视范围或内容发生变化时只通知发生变化的事项。”的方式可以弥补select函数的缺点。
需要操作系统的支持,Linux是epoll,Windows是IOCP。
epoll从Linux2.5.44内核开始引入。
cat /proc/sys/kernel/osrelease
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;
}
#include
//size是向操作系统提供建议值
//成功返回epoll例程文件描述符,失败-1
//Linux2.6.8后size无效,系统动态调整epoll例程大小
int epoll_create(int size);
#include
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再次设置事件
#include
//成功返回发生事件的文件描述符数,失败-1
//timeout,1/1000秒,-1阻塞等待
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); //EPOLL_SIZE宏常量
event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
#include
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
printf("Usage : %s \n" , argv[0]);
exit(1);
}
int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
error_handling("socket() error!");
socklen_t addr_size = sizeof(struct sockaddr_in);
struct sockaddr_in serv_addr;
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!");
int epfd = epoll_create(EPOLL_SIZE);
struct epoll_event *ep_events = (struct epoll_event *)malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
struct epoll_event event;
event.events = EPOLLIN; // 读取事件
eventdata.fd = serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
while (1)
{
int event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if (event_cnt == -1)
{
puts("epoll_wait() error!");
break;
}
for (int i = 0; i < event_cnt; i++)
{
if (ep_events[i].data.fd == serv_sock)
{
struct sockaddr_in clnt_addr;
int 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
{
char buf[BUF_SIZE];
int 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);
}
}
}
}
// free(ep_events);
close(epfd);
close(serv_sock);
return 0;
}
// gcc 17.echo_epollserver.c -o 17.echo_epollserver && ./17.echo_epollserver 9190
条件触发(Level Trigger)和边缘触发(Edge Trigger)的区别在于发生事件的时间点。
条件触发特性,条件触发方式中,只要输入缓冲有数据就会一直通知该事件,多次注册事件。
边缘触发中输入缓冲收到数据时仅注册1次该事件(触发后失效)。
条件触发,满足条件就触发(比如输入缓冲有数据)。
边缘触发,状态变化才触发(比如无缓冲状态变为有缓冲状态)。
epoll默认以条件触发方式工作。
#include
#include
#include
#include
#include
#include
#include
#define PORT 9999
#define BUF_SIZE 4
#define EPOLL_SIZE 50
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[])
{
int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
error_handling("socket() error!");
int opt = 1;
if (setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt)) == -1)
error_handling("setsockopt() error!");
socklen_t addr_size = sizeof(struct sockaddr_in);
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, addr_size);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(PORT);
if (bind(serv_sock, (struct sockaddr *)&serv_addr, addr_size) == -1)
error_handling("bind() error!");
if (listen(serv_sock, 5) == -1)
error_handling("listen() error!");
struct epoll_event *ep_events = (struct epoll_event *)malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
int epfd = epoll_create(EPOLL_SIZE);
struct epoll_event event;
event.events = EPOLLIN; // 读取事件
event.data.fd = serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
while (1)
{
int event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if (event_cnt == -1)
{
puts("epoll_wait() error!");
break;
}
puts("return epoll_wait");
for (int i = 0; i < event_cnt; i++)
{
if (ep_events[i].data.fd == serv_sock)
{
struct sockaddr_in clnt_addr;
int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &addr_size);
event.events = EPOLLIN; // 读取事件
// event.events = EPOLLIN|EPOLLET;
event.data.fd = clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client: %d\n", clnt_sock);
}
else
{
char buf[BUF_SIZE];
int 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);
free(ep_events);
return 0;
}
// gcc 17.echo_EPLTserver.c -o 17.echo_EPLTserver && ./17.echo_EPLTserver
多次输出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以条件触发方式工作。
read函数发现缓冲中无数据可读时返回-1,同时在errno中保存EAGAIN常量。
#include
//成功返回cmd参数相关值,失败-1
int fcntl(int filedes, int cmd, ...);
//获取之前设置的属性信息
int flag = fcntl(fd, F_GETFL, 0);//获取文件描述符属性
//添加非阻塞O_NONBLOCK标志
fcntl(fd, F_SETFL, flag|O_NONBLOCK);//设置文件描述符属性
边缘触发方式中,仅注册一次事件。
一旦发生输入事件,就应该读取输入缓冲中全部数据。
read返回-1,同时在errno值为EAGAIN,说明无数据可读。
边缘触发方式中,阻塞方式工作的read&write函数可能引起服务器端的长时间停顿,边缘触发方式中一定要采用非阻塞read&write函数。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define PORT 9999
#define BUF_SIZE 4
#define EPOLL_SIZE 50
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
// 非阻塞
void setnonblockingmode(int fd)
{
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}
int main(int argc, char *argv[])
{
int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
error_handling("socket() error!");
int opt = 1;
if (setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt)) == -1)
error_handling("setsockopt() error!");
socklen_t addr_size = sizeof(struct sockaddr_in);
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, addr_size);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(PORT);
if (bind(serv_sock, (struct sockaddr *)&serv_addr, addr_size) == -1)
error_handling("bind() error!");
if (listen(serv_sock, 5) == -1)
error_handling("listen() error!");
struct epoll_event *ep_events = (struct epoll_event *)malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
int epfd = epoll_create(EPOLL_SIZE);
setnonblockingmode(serv_sock);
struct epoll_event event;
event.events = EPOLLIN; // 读取事件
event.data.fd = serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
while (1)
{
int event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if (event_cnt == -1)
{
puts("epoll_wait() error!");
break;
}
puts("return epoll_wait");
for (int i = 0; i < event_cnt; i++)
{
if (ep_events[i].data.fd == serv_sock)
{
struct sockaddr_in clnt_addr;
int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &addr_size);
setnonblockingmode(clnt_sock);
event.events = EPOLLIN | EPOLLET; // 读取事件且边缘触发
event.data.fd = clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client: %d\n", clnt_sock);
}
else
{
while (1)
{
char buf[BUF_SIZE];
int 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) // 非阻塞模式下,当没有数据可读时,read()返回-1,errno=EAGAIN
/*
{
// 再次注册边缘触发的读取事件,
event.events = EPOLLIN | EPOLLET; // 读取事件且边缘触发
event.data.fd = ep_events[i].data.fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, ep_events[i].data.fd, &event);
}
else // 读取出错
{
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
{
write(ep_events[i].data.fd, buf, str_len);
}
}
}
}
}
close(serv_sock);
close(epfd);
free(ep_events);
return 0;
}
// gcc 17.echo_EPETserver.c -o 17.echo_EPETserver && ./17.echo_EPETserver
边缘触发相对于条件触发的优点:可以分离接收数据和处理数据的时间点。
从实现模型的角度看,边缘触发更有可能带来高性能,但不能简单地认为只要使用边缘触发就一定能提高速度。