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

简介: 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网络编程的知识。


相关文章
用MASM32按Time Protocol(RFC868)协议编写网络对时程序中的一些有用的函数代码
用MASM32按Time Protocol(RFC868)协议编写网络对时程序中的一些有用的函数代码
|
16天前
|
存储 JSON Java
细谈 Linux 中的多路复用epoll
大家好,我是 V 哥。`epoll` 是 Linux 中的一种高效多路复用机制,用于处理大量文件描述符(FD)事件。相比 `select` 和 `poll`,`epoll` 具有更高的性能和可扩展性,特别适用于高并发服务器。`epoll` 通过红黑树管理和就绪队列分离事件,实现高效的事件处理。本文介绍了 `epoll` 的核心数据结构、操作接口、触发模式以及优缺点,并通过 Java NIO 的 `Selector` 类展示了如何在高并发场景中使用多路复用。希望对大家有所帮助,欢迎关注威哥爱编程,一起学习进步。
|
1月前
|
机器学习/深度学习 编解码
深度学习笔记(三):神经网络之九种激活函数Sigmoid、tanh、ReLU、ReLU6、Leaky Relu、ELU、Swish、Mish、Softmax详解
本文介绍了九种常用的神经网络激活函数:Sigmoid、tanh、ReLU、ReLU6、Leaky ReLU、ELU、Swish、Mish和Softmax,包括它们的定义、图像、优缺点以及在深度学习中的应用和代码实现。
116 0
深度学习笔记(三):神经网络之九种激活函数Sigmoid、tanh、ReLU、ReLU6、Leaky Relu、ELU、Swish、Mish、Softmax详解
|
1月前
|
Linux C++
Linux C/C++之IO多路复用(poll,epoll)
这篇文章详细介绍了Linux下C/C++编程中IO多路复用的两种机制:poll和epoll,包括它们的比较、编程模型、函数原型以及如何使用这些机制实现服务器端和客户端之间的多个连接。
23 0
Linux C/C++之IO多路复用(poll,epoll)
|
1月前
|
机器学习/深度学习 数据可视化 算法
激活函数与神经网络------带你迅速了解sigmoid,tanh,ReLU等激活函数!!!
激活函数与神经网络------带你迅速了解sigmoid,tanh,ReLU等激活函数!!!
|
3月前
|
机器学习/深度学习 算法
神经网络中激活函数的重要性
【8月更文挑战第23天】
27 0
|
3月前
|
监控
【网络编程】poll函数
【网络编程】poll函数
28 0
|
3月前
|
监控
【网络编程】select函数
【网络编程】select函数
61 0
|
3月前
|
机器学习/深度学习 Shell 计算机视觉
一文搞懂 卷积神经网络 卷积算子应用举例 池化 激活函数
这篇文章通过案例详细解释了卷积神经网络中的卷积算子应用、池化操作和激活函数,包括如何使用卷积算子进行边缘检测和图像模糊,以及ReLU激活函数如何解决梯度消失问题。
|
3月前
|
Linux
Linux0.11 文件打开open函数(五)
Linux0.11 文件打开open函数(五)
45 0