目录
select的缺陷
epoll函数
epoll_create
epoll_ctl
epoll_wait
基于epoll的回声服务器实现
select的缺陷
在之前,我们使用了select函数完成了对回声服务器端I/O的复用,但是从代码上依然存有缺陷,主要集中在:
- 每次调用select函数需向函数传递监视对象信息
- 调用select后,要在fd_set中设计循环语句以监视所有对象变量
这两项操作对于性能损耗较大。并且需要注意的是,套接字是由操作系统管理,在需要频繁调用select函数的场景下,select函数要求必须向其传递监视对象(套接字),那么这样时间、空间都会产生巨大耗费。
不过,这个问题可以通过合适的策略来进行优化——仅向操作系统传递1次监视对象文件描述符,当监视对象有变化时只通知关注的事件。
在Linux中,epoll可以支持这项操作。
epoll函数
* 2.5.44版本之前的Linux内核无法使用epoll函数
在实现epoll时,需要用到其他一些关联函数和结构体:
- epoll_create: 创建保存epoll文件描述符的空间。
- epoll_ctl: 向空间注册并注销文件描述符。
- epoll_wait: 与select函数类似,等待文件描述符发生变化。
相较于select方式中使用fd_set变量来存储监视对象文件描述符,epoll使用epoll_create函数来向操作系统请求保存对象文件描述符。
select方法中需要使用到 FD_SET 和 FD_CLR 宏函数,而在epoll方法中只需要通过 epoll_ctl 函数请求操作系统完成即可。
在监视文件描述符的变化方面,epoll调用epoll_wait函数来实现对文件描述符变化的监视,同时用结构体类型epoll_event将发生变化的文件描述符集中于一起。
epoll_event结构体类型定义如下:
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
uint32_t events; /* 欲监视的 Epoll 事件 */
epoll_data_t data; /* 用户数据变量 */
}
声明epoll_event结构体后,将其传递给epoll_wait函数,发生变化的文件描述符将被填入该结构体中,而无需再采用循环的方式对范围内所有文件描述符进行扫描监视。
epoll_create
epoll_create函数用以创建epoll实例,其引用头文件和函数结构如下:
#include <sys/epoll.h>
int epoll_create(int size);
/* 成功时返回 epoll 文件描述符,失败时返回-1。
参数size 用来传递epoll实例的大小 */
需要注意的是,函数中参数size的值并不能控制epoll实例的实际大小,该值只负责告诉操作系统实例大小的建议值,供操作系统参考。
epoll_create函数创建的实例本质上是套接字,因此也会返回文件描述符,同时在结束对套接字的操作时,要调用close函数来进行关闭。
拓展:
Linux2.6.8之后的内核版本会忽略epoll_create中的size参数意义,即size参数不再具有实际意义,操作系统将会自行根据情况调整epoll实例的大小。
epoll_ctl
创建epoll实例后,需要通过epoll_ctl函数来对监视对象文件描述符进行注册。
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: 监视对象的时间类型
*/
其中参数op有设计好的宏,用来表示增加、删除或修改操作:
- EPOLL_CTL_ADD: 将文件描述符注册到epoll实例
- EPOLL_CTL_DEL: 删除epoll实例中的文件描述符
- EPOLL_CTL_MOD: 更改已注册的文件描述符中所关注的事件发生情况
使用EPOLL_CTL_DEL时,应向第四个参数(event)中传递NULL。
示例:
//从实例 A 中删除文件描述符SOCK_1
epoll_ctl(A , EPOLL_CTL_DEL , SOCK_1, NULL);
//向实例 A 中注册文件描述符SOCK_1,并监视 EVENT中 EPOLLIN 事件的发生与否
struct epoll_event EVENT;
EVENT.events=EPOLLIN;
EVENT.data.fd=sockfd;
epoll_ctl(A , EPOLL_CTL_ADD , SOCK_1, &EVENT);
示例中EPOLLIN是一种事件类型,用以表示“需要读取数据的情况”,还有其他事件类型,具体如下:
- EPOLLIN: 需要读取数据的情况 。
- EPOLLOUT: 输出缓冲为空,可立即发送数据的情况 。
- EPOLLPRI: 收到OOB数据的情况 。
- EPOLLRDHUP: 断开连接或半关闭的情况(常用在边缘触发方式下) 。
- EPOLLERR: 发生错误的情况 。
- EPOLLET: 以边缘触发的方式得到事件通知 。
- EPOLLONESHOT: 发生一次事件后,相应文件描述符不再收到事件通知的情况。(需要向epoll_ctL函数的第二个参数传递EPOLL_CTL_MOD,再次设置事件)
epoll_wait
epoll_wait用于完成最后对对象文件描述符的监视。
epoll_wait函数的引用头文件和函数结构如下:
#include <sys/epoll.h>
int epoll_wait(int epfd , struct epoll_event * events , ïnt maxevents , int
timeout);
// 成功时返回发生事件的文件描述符数,失败时返回 -1。
/* 参数含义 */
/*
epfd: 表示事件发生监视范围的 epoll 实例 的文件描述符
events: 保存发生事件的文件描述符集合的结构体地址值
maxevents: 可以保存的最大事件数 (对应第二个参数events)
timeout: 以 1/ 1000秒为单位的等待时间,传递 -1 时代表一直等待到事件的发生。
*/
需要注意的是,epoll_wait函数中的events变量(第二个参数)需要用malloc来开辟空间。
比如:
struct epoll_event * EVENTS;
//EPOLL_SIZE是宏常量,其值代表欲开辟的实例数。注意最后要对类型进行强转,因为malloc返回的是void*
EVENTS = (struct epoll_event *) malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
epoll_wait(SOCK, EVENTS, EPOLL_SIZE, -1)
接下来让我们尝试用epoll来实现之前的回声服务器
基于epoll的回声服务器实现
代码示例:
#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 1024
#define EPOLL_SIZE 5
void Sender_message(char *message)
{
puts(message);
exit(1);
}
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;
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
Sender_message((char *)"sock creation error");
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)
Sender_message((char *)"bind error");
if (listen(serv_sock, 5) == -1)
Sender_message((char *)"listen error");
epfd = epoll_create(EPOLL_SIZE);
ep_events = (struct epoll_event *)malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
event.events = EPOLLIN; //声明欲监视的事件为需要读取数据的情况
event.data.fd = serv_sock; //将服务器端套接字保存至epoll_event结构体中
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((char *)"supervise 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_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf((char *)"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((char *)"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;
}
运行结果:
得到验证