一.epoll概述
epoll 是 Linux 特有的 I/O 复用函数。它在实现和使用上与 select、poll 有很大差异。首
先,epoll 使用一组函数来完成任务,而不是单个函数。其次,epoll 把用户关心的文件描述
符上的事件放在内核里的一个事件表中。从而无需像 select 和 poll 那样每次调用都要重复传
入文件描述符或事件集。但 epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这
个事件表。epoll 相关的函数如下:
◼ epoll_create()用于创建内核事件表
◼ epoll_ctl()用于操作内核事件表,增加,删除,修改内核事件表
◼ epoll_wait()用于在一段超时时间内等待一组文件描述符上的事件;监听事件描述符,获取就绪的事件描述符,执行相应处理
1.epoll接口
#include <sys/epoll.h>
int epoll_create(int size);
/*
epoll_create()成功返回内核事件表的文件描述符,失败返回-1
size 参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大。
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
epoll_ctl()成功返回 0,失败返回-1
epfd 参数指定要操作的内核事件表的文件描述符
fd 参数指定要操作的文件描述符
op 参数指定操作类型:
EPOLL_CTL_ADD 往内核事件表中注册 fd 上的事件
EPOLL_CTL_MOD 修改 fd 上的注册事件
EPOLL_CTL_DEL 删除 fd 上的注册事件
event 参数指定事件,它是 epoll_event 结构指针类型,epoll_event 的定义如下:
struct epoll_event
{
_uint32_t events; // epoll 事件
epoll_data_t data; // 用户数据
};
其中,events 成员描述事件类型,epoll 支持的事件类型与 poll 基本相同,表示
epoll 事件的宏是在 poll 对应的宏前加上‘E’,比如 epoll 的数据可读事件是
EPOLLIN。但是 epoll 有两个额外的事件类型--EPOLLET 和 EPOLLONESHOT。
data 成员用于存储用户数据,是一个联合体,其定义如下:
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
其中 fd 成员使用的最多,它指定事件所从属的目标文件描述符。
*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
/*
epoll_wait()成功返回就绪的文件描述符的个数,失败返回-1,超时返回 0
epfd 参数指定要操作的内核事件表的文件描述符
events 参数是一个用户数组,这个数组仅仅在 epoll_wait 返回时保存内核检测到
的所有就绪事件,而不像 select 和 poll 的数组参数那样既用于传入用户注册的事
件,又用于输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件
描述符的效率。
maxevents 参数指定用户数组的大小,即指定最多监听多少个事件,它必须大于0
timeout 参数指定超时时间,单位为毫秒,如果 timeout 为 0,则 epoll_wait 会立即
返回,如果 timeout 为-1,则 epoll_wait 会一直阻塞,直到有事件就绪。
*/
2.epoll的两种模式
epoll 对文件描述符有两种操作模式:LT(Level Trigger,电平触发)模式和 ET(Edge
Trigger,边沿触发)模式。LT 模式是默认的工作模式。当往 epoll 内核事件表中注册一个文
件描述符上的 EPOLLET 事件时,epoll 将以高效的 ET 模式来操作该文件描述符。
2.1 LT模式
当服务器端不能完全接受客户端发送的数据时,如果服务器端无法一次性处理完,服务器回持续处理提醒;
对于 LT 模式操作的文件描述符,当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用 epoll_wait 时,还会再次向应用程序通告此事件,直到该事件被处理。
2.2 ET模式
事件就绪后,只提醒一次,指导下次事件就绪再次提醒,指导内存缓冲区的数据被完全处理
对于 ET 模式操作的文件描述符,当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的 epoll_wait 调用将不再向应用程序通知这一事件。所以 ET 模式在很大程度上降低了同一个 epoll 事件被重复触发的次数,因此效率比 LT 模式高。
二.epoll代码
1.epoll ——LT代码
epoll.c
#include<stdio.h> // 标准输入输出库
#include<stdlib.h> // 标准库,提供一些通用函数
#include<string.h> // 字符串操作库
#include<unistd.h> // UNIX 标准函数库
#include<sys/socket.h> // 套接字相关函数
#include<netinet/in.h> // 网络接口的头文件,提供一些网络编程需要的宏和数据结构
#include<arpa/inet.h> // 提供inet_addr等函数,用于将点分十进制IP地址转换为网络字节顺序
#include<sys/epoll.h> // 用于epoll相关的函数声明
#define MAXFD 10 // 定义最大文件描述符数量
// 初始化socket并绑定到端口
int socket_init()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
if(sockfd == -1) // 如果创建失败
{
return -1;
}
struct sockaddr_in saddr; // 定义地址结构
memset(&saddr, 0, sizeof(saddr)); // 初始化地址结构
saddr.sin_family = AF_INET; // 地址族,使用IPv4
saddr.sin_port = htons(6000); // 端口号,使用网络字节顺序
saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // IP地址
int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr)); // 绑定地址
if(res == -1) // 如果绑定失败
{
printf("bind err\n");
return -1;
}
if(listen(sockfd, 5) == -1) // 开始监听,设置队列长度为5
{
return -1;
}
return sockfd; // 返回套接字描述符
}
// 向epoll事件表中添加描述符
void epoll_add(int epfd, int fd)
{
struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN; // 只关心读事件
if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1) // 添加描述符
{
printf("epoll ctl add err\n");
}
}
// 从epoll事件表中删除描述符
void epoll_del(int epfd, int fd)
{
if(epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) == -1) // 删除描述符
{
printf("epoll ctl del err\n");
}
}
// 接受客户端连接请求
void accept_cli(int sockfd, int epfd)
{
int c = accept(sockfd, NULL, NULL); // 接受连接
if(c < 0)
{
return;
}
printf("accept c=%d\n", c);
epoll_add(epfd, c); // 将新的连接添加到epoll事件表
}
// 接收客户端发送的数据
void recv_data(int c, int epfd)
{
char buff[128] = {0}; // 定义接收缓冲区
int n = recv(c, buff, 127, 0); // 接收数据
if(n <= 0) // 如果接收到的数据小于等于0
{
printf("cli close=%d\n", c);
epoll_del(epfd, c); // 从epoll事件表中删除该描述符
close(c); // 关闭连接
return;
}
printf("buff(%d)=%s\n", c, buff); // 打印接收到的数据
send(c, "ok", 2, 0); // 向客户端发送确认消息
}
int main()
{
int sockfd = socket_init(); // 初始化socket
if(sockfd == -1)
{
exit(1); // 如果初始化失败,退出程序
}
int epfd = epoll_create(MAXFD); // 创建epoll实例
if(epfd == -1)
{
exit(1); // 如果创建失败,退出程序
}
epoll_add(epfd, sockfd); // 将服务器监听的套接字添加到epoll事件表
struct epoll_event evs[MAXFD]; // 定义事件数组
while(1) // 无限循环,等待事件
{
int n = epoll_wait(epfd, evs, MAXFD, 5000); // 等待事件,超时时间5000ms
if(n == -1) // 如果等待失败
{
printf("epoll wait err\n");
}
else if(n == 0) // 如果超时没有事件发生
{
printf("time out\n");
}
else // 如果有事件发生
{
for(int i = 0; i < n; i++) // 遍历所有事件
{
if(evs[i].events & EPOLLIN) // 如果事件是读事件
{
if(evs[i].data.fd == sockfd) // 如果是监听套接字
{
accept_cli(sockfd, epfd); // 接受新的连接
}
else // 如果是已连接的客户端
{
recv_data(evs[i].data.fd, epfd); // 接收数据
}
}
}
}
}
}
2.epoll_ET代码
epoll_et.c
#include <stdio.h> // 标准输入输出库
#include <stdlib.h> // 标准库,提供动态内存分配等
#include <string.h> // 字符串操作库
#include <unistd.h> // UNIX标准函数库,提供close函数等
#include <sys/socket.h>// 套接字库
#include <netinet/in.h> // 网络头文件,提供IPv4地址格式
#include <arpa/inet.h> // 网络地址转换库
#include <sys/epoll.h> // epoll库
#include <errno.h> // 错误号库
#include <fcntl.h> // 文件控制库
#define MAXFD 10 // 定义最大的文件描述符数量
// 设置非阻塞函数
void setnonblock(int fd) {
int oldfl = fcntl(fd, F_GETFL); // 获取文件描述符的原有属性
int newfl = oldfl | O_NONBLOCK; // 设置非阻塞属性
if (fcntl(fd, F_SETFL, newfl) == -1) {
printf("fcntl err\n"); // 打印错误信息
}
}
// 初始化socket函数
int socket_init() {
// ... 省略的代码 ...
}
// 向epoll内核事件表添加文件描述符
void epoll_add(int epfd, int fd) {
struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN | EPOLLET; // 设置为ET模式,并关注读事件
setnonblock(fd); // 设置文件描述符为非阻塞
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1) {
printf("epoll ctl add err\n"); // 打印错误信息
}
}
// 从epoll内核事件表删除文件描述符
void epoll_del(int epfd, int fd) {
if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) == -1) {
printf("epoll ctl del err\n"); // 打印错误信息
}
}
// 接受客户端连接并添加到epoll事件表
void accept_cli(int sockfd, int epfd) {
// ... 省略的代码 ...
}
// 接收客户端数据
void recv_data(int c, int epfd) {
while (1) {
char buff[128] = {0};
int n = recv(c, buff, sizeof(buff), 0); // 接收数据
if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
send(c, "ok", 2, 0); // 发送确认消息
} else {
printf("recv err\n"); // 打印错误信息
}
break;
} else if (n == 0) {
printf("close(%d)\n", c); // 打印关闭连接信息
epoll_del(epfd, c); // 从epoll事件表删除
break;
} else {
printf("buff(%d)=%s\n", c, buff); // 打印接收到的数据
}
}
}
// 主函数
int main() {
int sockfd = socket_init(); // 初始化socket
if (sockfd == -1) {
exit(1); // 如果初始化失败,退出程序
}
int epfd = epoll_create(MAXFD); // 创建epoll实例
if (epfd == -1) {
exit(1); // 如果创建失败,退出程序
}
epoll_add(epfd, sockfd); // 将监听的socket添加到epoll事件表
struct epoll_event evs[MAXFD]; // 定义epoll事件数组
while (1) { // 无限循环等待事件
int n = epoll_wait(epfd, evs, MAXFD, 5000); // 等待事件,超时时间5000ms
if (n == -1) {
printf("epoll wait err\n"); // 打印错误信息
} else if (n == 0) {
printf("time out\n"); // 打印超时信息
} else {
for (int i = 0; i < n; i++) { // 遍历所有触发的事件
if (evs[i].events & EPOLLIN) { // 如果事件类型为读事件
if (evs[i].data.fd == sockfd) {
accept_cli(sockfd, epfd); // 接受新的客户端连接
} else {
recv_data(evs[i].data.fd, epfd); // 接收数据
}
}
}
}
}
}
三.select、poll 、epoll三者之间的区别
1.select、poll、epoll 区别
1.1. 支持一个进程所能打开的最大连接数
select:单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。
poll:poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的
epoll:虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接
1.5.3. 消息传递方式
select:内核需要将消息传递到用户空间,都需要内核拷贝动作
poll:同上
epoll:epoll通过内核和用户空间共享一块内存来实现的。