前言
本篇文章我们讲解epoll函数的使用方法,epoll相比于poll来说性能方面有所提升和改进。
一、epoll概念特点讲解
epoll 是 Linux 上一种高性能的多路复用机制,用于监视大量文件描述符并在它们就绪时通知应用程序。它是在 select 和 poll 的基础上进一步优化和改进而来的。
epoll 的主要特点包括:
1.没有文件描述符数量限制:与 select 和 poll 不同,epoll 采用了基于事件的就绪通知机制,没有预定义的文件描述符数量限制,可以支持更大规模的并发连接。
2.高效的事件通知:epoll 使用了内核和用户空间共享的事件数据结构,将文件描述符的事件注册到内核空间,当事件就绪时,内核直接将就绪的事件通知给用户空间,避免了每次调用都需要遍历整个文件描述符数组的性能开销。
3.分离的就绪事件集合:epoll 将就绪的事件从内核空间复制到用户空间,形成一个分离的就绪事件集合,用户可以直接遍历这个集合来处理就绪的事件,而不需要遍历整个文件描述符数组。
4.支持边缘触发和水平触发:epoll 提供了两种模式来处理事件,一种是边缘触发模式(EPOLLET),只在状态发生变化时通知应用程序,另一种是水平触发模式(默认),在事件就绪期间一直通知应用程序。
5.更低的内存拷贝开销:epoll 使用内存映射技术,避免了每次调用都需要将事件数据从内核复制到用户空间的开销,从而减少了系统调用的次数和内存拷贝的开销。
6.支持较高精度的超时控制:与 poll 不同,epoll 的超时参数以毫秒和纳秒为单位,提供了较高精度的超时控制。
总体来说,epoll 在性能上相比于 select 和 poll 有较大的优势,特别适用于高并发场景下的网络编程。它的高效事件就绪通知、支持大规模并发连接、较低的内存拷贝开销以及较高的超时精度,使得它成为开发高性能服务器和网络应用的首选机制。
二、epoll实现机理
epoll是使用红黑树(Red-Black Tree)实现的。epoll是Linux操作系统提供的一种高效的事件通知机制,用于处理大量的并发连接。它能够监视多个文件描述符的状态变化,当文件描述符就绪时,通过回调函数进行相应的处理。
在Linux内核中,epoll使用红黑树作为其主要的数据结构,用于维护注册的文件描述符集合。红黑树是一种自平衡的二叉搜索树,具有较快的插入、删除和搜索操作的时间复杂度。通过使用红黑树,epoll能够高效地检索和管理大量的文件描述符。
当文件描述符发生事件时,epoll通过红黑树的查找操作快速定位到相应的结点,并触发注册的回调函数进行事件处理。使用红黑树的原因是它能够保持良好的平衡性,保证搜索、插入和删除操作的最坏情况时间复杂度为O(log n),从而保证了epoll的高性能和可伸缩性。
总结来说,epoll是利用红黑树作为其底层数据结构实现的,这使得它在处理大量并发连接时能够提供高效的事件通知机制。
三、epoll相关函数讲解
1.epoll_create函数
函数原型:
int epoll_create(int size);
epoll_create 函数创建一个 epoll 实例,并返回一个文件描述符,用于标识该 epoll 实例。参数 size 是一个提示,表示 epoll 实例可以监视的文件描述符的数量上限。但在大多数情况下,该参数会被忽略,可以传递任意的值。
返回的文件描述符可以用于之后对 epoll 实例进行操作,比如注册、修改和删除文件描述符的事件。
需要注意的是,epoll_create 函数在成功时返回一个非负整数,表示 epoll 实例的文件描述符,如果出现错误,返回值为 -1,并设置 errno 错误码来指示具体的错误类型。
2.epoll_ctl函数
函数原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl 函数通过指定的 epfd 参数来操作特定的 epoll 实例,op 参数表示操作类型,可以是以下三种之一:
EPOLL_CTL_ADD:将指定的文件描述符 fd 添加到 epoll 实例中,并注册相应的事件。这样,当该文件描述符上的事件就绪时,就会通知应用程序。
EPOLL_CTL_MOD:修改已经注册在 epoll 实例中的文件描述符 fd 对应的事件。可以修改事件的类型、关注的事件、关联的用户数据等。
EPOLL_CTL_DEL:删除已经注册在 epoll 实例中的文件描述符 fd。
fd 参数是目标文件描述符,用于指定需要进行操作的文件描述符。
event 参数是一个指向 struct epoll_event 结构体的指针,用于指定事件相关的配置。该结构体包含两个成员:
uint32_t events:表示注册的事件类型,可以是 EPOLLIN(可读事件)、EPOLLOUT(可写事件)、EPOLLRDHUP(对端关闭连接)、EPOLLPRI(有紧急数据可读)、EPOLLERR(错误事件)等。可以使用位掩码进行组合。
epoll_data_t data:用于存储用户数据信息,可以是文件描述符本身的值,也可以是用户自定义的数据结构指针。
函数的返回值为 0 表示操作成功,-1 表示出现错误,具体的错误信息可以通过检查 errno 变量获得。
3.epoll_wait函数
函数原型:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epfd 参数是 epoll 实例的描述符,通过它指定要进行事件等待的 epoll 实例。
events 参数是一个用于存放事件信息的数组,每个数组元素都是 struct epoll_event 类型的结构体,用于存储就绪的文件描述符以及对应的事件信息。
maxevents 参数表示 events 数组的大小,即最多可以等待多少个事件。
timeout 参数是超时时间,单位为毫秒。它决定了 epoll_wait 函数的阻塞行为:
如果 timeout 设置为 -1,表示无限期阻塞,直到有事件发生为止。
如果 timeout 设置为 0,表示非阻塞,立即返回当前就绪的事件,如果没有事件就绪,则返回 0。
如果 timeout 设置为一个正整数,表示阻塞等待指定的时间后返回,如果在超时时间内没有等到事件就绪,则返回 0。
epoll_wait 函数在成功时返回就绪事件的文件描述符数量,如果出现错误则返回 -1,并设置 errno 错误码来指示具体的错误类型。
四、epoll实现并发服务器
当使用epoll实现并发服务器时,通常的步骤包括以下几个主要环节:
1.创建socket:使用socket函数创建一个监听套接字,用于接受客户端的连接请求。
2.绑定socket:使用bind函数将监听套接字绑定到一个特定的IP地址和端口。
3.监听连接:使用listen函数开始监听连接请求,指定服务器可接受的最大连接数。
4.创建epoll实例:使用epoll_create函数创建一个epoll实例,返回一个文件描述符。
5.将监听套接字添加到epoll实例:使用epoll_ctl函数将监听套接字添加到epoll实例中,并注册对读事件的关注。
6.进入事件循环:循环调用epoll_wait函数来等待事件的发生,该函数会阻塞当前线程直至有事件发生。一旦有事件发生,它将返回一个就绪事件的列表。
7.处理就绪事件:遍历就绪事件列表,对每个事件进行处理。根据事件类型,可以进行接受连接、读取数据、发送数据或关闭连接等操作。
8.根据需要添加或删除文件描述符:在处理完一个事件后,可以根据需要使用epoll_ctl函数动态地添加或删除文件描述符,以便继续监听其他事件。
9.重复步骤6-8:继续循环执行步骤6-8,处理新的就绪事件,直到服务器主动关闭或出现错误条件为止。
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/epoll.h> #define MAX_EVENTS 1024 //最多可以等待多少个事件 int main() { int server = 0; struct sockaddr_in saddr = {0}; int client = 0; struct sockaddr_in caddr = {0}; socklen_t asize = 0; int len = 0; char buf[32] = {0}; int maxfd; int ret = 0; int i = 0; server = socket(PF_INET, SOCK_STREAM, 0); if( server == -1 ) { printf("server socket error\n"); return -1; } saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = htonl(INADDR_ANY); saddr.sin_port = htons(8888); if( bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == -1 ) { printf("server bind error\n"); return -1; } if( listen(server, 128) == -1 ) { printf("server listen error\n"); return -1; } printf("server start success\n"); struct epoll_event event, events[MAX_EVENTS]; /*创建epoll*/ int epollInstance = epoll_create1(0); if (epollInstance == -1) { printf("Failed to create epoll instance\n"); } /*将服务器添加进入event中*/ event.events = EPOLLIN; event.data.fd = server; if (epoll_ctl(epollInstance, EPOLL_CTL_ADD, server, &event) == -1) { printf("Failed to add server socket to epoll instance"); } while( 1 ) { int numEventsReady = epoll_wait(epollInstance, events, MAX_EVENTS, -1); if (numEventsReady == -1) { printf("Failed to wait for events"); return -1; } for(i = 0; i < numEventsReady; i++) { if(events[i].data.fd == server) { /*有客户端连接上来了*/ asize = sizeof(caddr); client = accept(server, (struct sockaddr*)&caddr, &asize); printf("client is connect\n"); event.events = EPOLLIN | EPOLLET; event.data.fd = client; if (epoll_ctl(epollInstance, EPOLL_CTL_ADD, client, &event) == -1) { printf("Failed to add client socket to epoll instance"); return -1; } } else { /*处理客户端的请求*/ len = read(events[i].data.fd, buf, 1024); if(len == 0) { printf("client is disconnect\n"); close(events[i].data.fd); } else { /*对接收到的数据进行处理*/ printf("read buf : %s\n", buf); write(events[i].data.fd, buf, len); } } } } close(server); return 0; }
总结
本篇文章就讲解到这里,下篇文章继续讲解Linux网络编程的知识。