Linux网络编程(epoll函数的使用)


前言

本篇文章我们讲解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网络编程的知识。

猜你喜欢

转载自blog.csdn.net/m0_49476241/article/details/132376808