Linux下epoll的简单解释

这一篇博客是简单解释linux下的epoll

epoll是linux提供的一种I/O事件通知机制,能够高效的处理文件描述符[1]的I/O事件,其基于事件驱动

结构体

1
2
3
4
5
6
7
8
9
10
11
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;

events:事件

data:事件携带的数据,是一个联合体

ptr:一个C风格的any

fd:文件描述符

u32:一个无符号32位整型

u64:一个无符号64位整型

常用函数

1
int epoll_create(int size)

epoll_create函数用来创建epoll的,需要传入一个int类型的变量用来指定最多监听几个描述符,如果没有出现错误便会返回一个文件描述符也就是int,如果出现错误会返回一个-1

1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

epoll_ctl函数可以用来操作epoll文件描述符

epfd:epoll的文件描述符

op:对epoll文件描述符进行操作

fd:需要监听的文件描述符

event:要监听fd的哪些事件

op的有效值有 EPOLL_CTL_ADD EPOLL_CTL_MOD EPOLL_CTL_DEL 分别代表添加、修改、删除

EPOLL_CTL_ADD:注册新的fd到epfd中;

EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

EPOLL_CTL_DEL:从epfd中删除一个fd;

event是一个集合,值可以为

EPOLLIN:对应的文件描述符可以进行读取操作时获得通知

EPOLLOUT:对应的文件描述符可以进行写入操作时获得通知

EPOLLRDHUP:进行通信时对端关闭了连接或者写的操作时获得通知

EPOLLPRI:对应文件描述符有紧急数据可以进行读取时获得通知,一般是带外数据(Out-of-Band data)

EPOLLERR:对应文件描述符发生错误时获得通知

EPOLLHUP:发生挂起事件时,通常是对端关闭连接时获得通知

EPOLLET:会开启边缘触发模式,在文件描述符状态变化时获得通知

EPOLLONESHOT:表示启用一次事件通知,文件描述符会被设置成一次性模式,需要重新注册才会重新获取到通知

EPOLLEXCLUSIVE:表示该文件描述符为独占,不与1其他的文件描述符共享通知

如果函数发生错误将返回-1

1
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

epoll_wait执行后会阻塞线程,等待事件

epfd:epoll的文件描述符

events:用来存放被触发事件的数组

maxevents:数组的长度或最大接受事件的数量

timeout:超时时长,以毫秒为单位,超过该时间如果没有事件也会结束阻塞,如果为-1即代表阻塞到有事件被触发

返回值为int类型,表示的是触发了几个事件,如果出现错误便返回-1

示例解释

接下来会给出一个基于epoll的socket编程示例,并逐步解释来让你知道epoll该如何使用(会过关于套接字的部分),因为只是示例就不添加错误检查了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <sys/epoll.h> // 想使用epoll必须加上这个头文件
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string>
int main() {
int epollfd = epoll_create(10);
epoll_event epoll_events[10] = {0};
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
sockaddr_in addr = {0};
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_family = AF_INET;
addr.sin_port = htons(9888);
bind(sockfd, (sockaddr *) &addr, sizeof addr);

epoll_event event;
event.data.fd = sockfd;
event.events = (EPOLLIN);
epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &event);

while (true) {
int event_nums = epoll_wait(epollfd, epoll_events, 10, 100);
for (int i = 0; i < event_nums; i++) {
if (epoll_events[i].events & EPOLLIN) {
sockaddr_in sock = {0};
std::string *buffer = new std::string(1024, '\0');
socklen_t socklen = sizeof sock;
recvfrom(sockfd, buffer->data(), buffer->length(), 0, (sockaddr *) &sock, &socklen);
printf("%s发送数据:%s", std::string(inet_ntoa(sock.sin_addr)).c_str(), buffer->c_str());
delete buffer;
}
}
}
close(sockfd);
}

先声明所有需要的变量

epollfd:epoll的文件描述符,使用epoll_create创建

epoll_events:前文提到过epoll是基于事件驱动的,所以这里面存放的是事件,初始化为0

1
2
int epollfd = epoll_create(10);
epoll_event epoll_events[10] = {0};

设置需要监听哪些事件,这里因为示例只监听了可读事件

1
2
3
4
epoll_event event;
event.data.fd = fockfd;
event.events = (EPOLLIN);
epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &event);

开始循环,等待事件,用event_nums来接收有多少个事件

1
2
while (true) {
int event_nums = epoll_wait(epollfd, epoll_events, 10, 100);

遍历数组中的事件

1
for (int i = 0; i < event_nums; i++)

判断其是否为写入事件

1
if (epoll_events[i].events & EPOLLIN) 

对数据进行处理

1
2
3
4
5
6
sockaddr_in sock = {0};
std::string *buffer = new std::string(1024, '\0');
socklen_t socklen = sizeof sock;
recvfrom(sockfd, buffer->data(), buffer->length(), 0, (sockaddr *) &sock, &socklen);
printf("%s发送数据:%s", std::string(inet_ntoa(sock.sin_addr)).c_str(), buffer->c_str());
delete buffer;

这样便实现了一个简单的Udp服务器,当然epoll的用法不只是网络编程,还有更多的用法等待着你去探索,讲解就到这里了,如果有不清楚的可以在下面提问,我会尽力解答的

[1] 文件描述符:linux下一切皆是文件,文件描述符就是内核为了高效管理这些已经被打开的文件所创建的索引,想了解的可以看一下这篇文章

提示:epoll实例不需要显式关闭,但是要及时关闭其他不需要的文件描述符