Linux多路IO复用:epoll

这篇具有很好参考价值的文章主要介绍了Linux多路IO复用:epoll。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1. epoll

        epoll是为克服select、poll每次监听都需要在用户、内核空间反复拷贝,以及需要用户程序自己遍历发现有变化的文件描述符的缺点的多路IO复用技术。

epoll原理

创建内核空间的红黑树;

将需要监听的文件描述符上树;

内核监听红黑树上文件描述符的变化;

返回有变化的文件描述符。

epoll优点

        ① 无需在用户、内核空间反复拷贝数据;

        ② 内核返回发生变化的文件描述符,无需用户遍历所有文件描述符。


2. epoll API

(1)epoll_create 创建红黑树

#include<sys/epoll.h>

int epoll_create(int size);
/*
功能:
    创建内核中的epoll红黑树;
参数:
    size:监听的文件描述符上限,kernel 2.6版本后写1即可,会自动扩展。
返回值:
    成功:返回红黑树的句柄(相当于操作树的入口)。
    失败:-1,会设置errno。
*/

(2)epoll_ctl 上树、下树、修改节点的监听事件

#include<sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
/*
功能:
    上树、下树、修改节点。
参数:
    epfd:红黑树的句柄,epoll_create的返回值。
    op:对文件描述符fd的操作
        EPOLL_CTL_ADD:将文件描述符fd上树
        EPOLL_CTL_MOD:修改文件描述符fd的事件
        EPOLL_CTL_DEL:将文件描述符fd下树
    fd:op要操作的文件描述符
    event:用于对特定的文件描述符事件进行设置。
返回值:
    成功:
    失败:
*/

struct epoll_event {
    uint32_t events;    // 监听的事件
    epoll_data_t data;  // 需要监听的文件描述符(共用体中的fd)
}
/*
参数 events:
    EPOLLIN:读事件
    EPOLLOUT:写事件
*/

typedef union epoll_data {
    void* ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

(2)epoll_wait 监听

#include<sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
/*
功能:
    监听红黑树上文件描述符的变化;
参数:
    epfd:红黑树的句柄(epoll_create的返回值);
    events:接收发送变化的文件描述符的数组地址
    maxevents:数组元素的个数
    timeout:
        > 0:监听超时时间(多久监听一次);
        0:无文件描述符变化则立即返回;
        -1:阻塞监听到有文件描述符变化才返回
返回值:
    成功:0表示没有文件描述符发生变化;否则返回发生变化的文件描述符的个数
    失败:-1,调用错误,会设置errno
*/

3. epoll使用示例

(1)监听管道

        子进程每3s向管道写数据,父进程使用epoll监听管道,有数据可读则读出管道中的数据。

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/epoll.h>


int main(int argc, const char* argv[]) {

    int fd[2];

    pipe(fd);

    pid_t pid;
    pid = fork();

    if (0 == pid) { // 子进程
        close(fd[0]);
        char buf[5];
        char ch = 'a';
        while (1) {
            memset(buf, ch++, 5);
            write(fd[1], buf, 5);
            sleep(3);
        }
    } else {    // 父进程
        close(fd[1]);

        // 创建红黑树
        int epfd = epoll_create(1);

        // 上树
        struct epoll_event ev;
        ev.data.fd = fd[0];
        ev.events = EPOLLIN;
        epoll_ctl(epfd, EPOLL_CTL_ADD, fd[0], &ev); // 将fd[0]上树,同时使用ev结构体来设置监听fd[0]的读事件。

        // 监听
        struct epoll_event evs[1]; // 接收从内核返回的有变化的文件描述符的数组。
        while (1) {
            int n = epoll_wait(epfd, evs, 1, -1);
            if (1 == n) {
                char buf[64] = "";
                n = read(fd[0], buf, 64);
                if (n <= 0) {
                    printf("子进程关闭了写端");
                    close(fd[0]);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd[0], &ev); // 下树
                    break;
                } else {
                    printf("读到子进程写的内容:%s\n", buf);
                }
            }
        }
    }
    return 0;
}

运行结果:

Linux多路IO复用:epoll

(2)epoll实现简单并发服务器示例:

#include<stdio.h>
#include<sys/epoll.h>
#include"wrap.h"

int main(int argc, const char* argv[]) {

    // 1.创建socket,绑定
    int lfd = tcp4bind(8888, NULL);

    // 2.监听
    Listen(lfd, 128);

    // 3.创建树
    int epfd = epoll_create(1);

    // 5.将lfd上树
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    // 4.while epoll_wait监听
    struct epoll_event evs[1024];
    while (1) {
        int n = epoll_wait(epfd, evs, 1024, -1); // 阻塞监听到有文件描述符变化才返回
        if (n < 0) {  // 调用出错
            perror("epoll_wait");
            break;
        } else if (0 == n) {
            continue;
        } else {  // 有文件描述符变化
            for (int i = 0;i < n;i++) {
                // 若lfd有读事件
                if (evs[i].data.fd == lfd && evs[i].events & EPOLLIN) {
                    struct sockaddr_in cliaddr;
                    char ip[16] = "";
                    socklen_t len = sizeof(cliaddr);
                    int cfd = Accept(lfd, (struct sockaddr*)&cliaddr, &len); // 提取
                    printf("新连接到来:IP = %s, port = %d\n",
                        inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16),
                        ntohs(cliaddr.sin_port));

                    ev.data.fd = cfd;
                    ev.events = EPOLLIN;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); // cfd上树;监听cfd的读事件

                } else if (evs[i].events & EPOLLIN) { // 若cfd有读事件
                    char buf[1024] = "";
                    int n = read(evs[i].data.fd, buf, 1024);
                    if (n < 0) { // 出错
                        perror("read");
                        close(evs[i].data.fd);
                        epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下树
                    } else if (0 == n) {  // 客户端关闭
                        printf("客户端关闭.\n");
                        close(evs[i].data.fd);
                        epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下树
                    } else {
                        write(STDOUT_FILENO, buf, 1024); / printf依赖于字符串终止符'\0',而write输出指定长度的字符串。
                    }
                }
            }
        }
    }
    return 0;
}

运行结果:

Linux多路IO复用:epoll


4.  epoll的两种工作方式

epoll有两种工作方式:水平触发(LT)、边缘触发(ET)。

(1)水平触发

        如监听文件描述符的读缓冲区时,只要读缓冲区有数据就会触发epoll_wait。例如读缓冲区有数据,只要没读干净,就会触发epoll_wait。

        如监听文件描述符的写缓冲区时,只要可写就会触发epoll_wait,因此监听写缓冲区时推荐使用边缘触发。

(2)边缘触发

        如监听文件描述符的读缓冲区时,读缓冲区有数据到来才会触发epoll_wait;与水平触发不一样,缓冲区数据没读干净且无数据到来,则下次不会再触发epoll_wait,因此要求一次性将读缓冲区数据读干净

        如监听文件描述符的写缓冲区时,写缓冲区数据从有到无才会触发epoll_wait。

epoll默认工作方式为水平触发,但推荐使用边缘触发,以减少epoll_wait系统调用次数


5. epoll的边缘触发使用示例

使用边缘触发,主要两点:1. 监听的事件加上边缘触发的属性;2. 只要触发就一次性将事情处理完。

1. 监听的事件加上边缘触发的属性

无需将监听的文件描述符设置为边缘触发,而是将与客户端通信的文件描述符设置为边缘触发,需将上面的 “epoll实现简单并发服务器示例” 代码第44行:

ev.events = EPOLLIN;

 改为如下,即加上边缘触发的属性。

ev.events = EPOLLIN | EPOLLET;

2. 只要触发就一次性将事情处理完

        以读事件为例,将上面的 “epoll实现简单并发服务器示例” 一次性读取字节数由1024B变为4B,则大多数情况下无法一次read调用就读完缓冲区中所有数据因此循环读取缓冲区,直至读完

        由于是循环读取,直至读完,因此文件描述符cfd需要设置为非阻塞,否则循环到最后一次无数据可读时,read函数将阻塞,无法返回继续监听。

        而水平触发时,结合上面代码,read是阻塞的,但通常不会阻塞住。因为是只要有数据可读就会触发epoll_wait,无数据就不会触发,因此不会阻塞住。

上述 "epoll实现简单并发服务器示例" 改为边缘触发:

#include<stdio.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include"wrap.h"

int main(int argc, const char* argv[]) {

    // 1.创建socket,绑定
    int lfd = tcp4bind(8888, NULL);

    // 2.监听
    Listen(lfd, 128);

    // 3.创建树
    int epfd = epoll_create(1);

    // 5.将lfd上树
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    // 4.while epoll_wait监听
    struct epoll_event evs[1024];
    while (1) {
        int n = epoll_wait(epfd, evs, 1024, -1); // 阻塞监听到有文件描述符变化才返回
        if (n < 0) {  // 调用出错
            perror("epoll_wait");
            break;
        } else if (0 == n) {
            continue;
        } else {  // 有文件描述符变化
            for (int i = 0;i < n;i++) {
                // 若lfd有读事件
                if (evs[i].data.fd == lfd && evs[i].events & EPOLLIN) {
                    struct sockaddr_in cliaddr;
                    char ip[16] = "";
                    socklen_t len = sizeof(cliaddr);
                    int cfd = Accept(lfd, (struct sockaddr*)&cliaddr, &len); // 提取

                    /* 设置cfd非阻塞 */
                    int flag = fcntl(cfd, F_GETFL);
                    flag |= O_NONBLOCK;
                    fcntl(cfd, F_SETFL, flag);

                    printf("新连接到来:IP = %s, port = %d\n",
                        inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16),
                        ntohs(cliaddr.sin_port));

                    ev.data.fd = cfd;
                    ev.events = EPOLLIN | EPOLLET;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); // cfd上树;监听cfd的读事件

                } else if (evs[i].events & EPOLLIN) { // 若cfd有读事件
                    while (1) { // 循环读取
                        char buf[4] = "";
                        /*缓冲区无数据时,以阻塞的方式读取,则会阻塞等待;若以非阻塞的方式读取,则返回-1,并且设置errno为EAGAIN*/
                        int n = read(evs[i].data.fd, buf, 4);
                        if (n < 0) { // 出错
                            if (EAGAIN == errno) { // 缓冲区被读干净,则继续下一次监听
                                break;
                            }
                            perror("read");
                            close(evs[i].data.fd);
                            epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下树
                            break;
                        } else if (0 == n) {  // 客户端关闭
                            printf("客户端关闭.\n");
                            close(evs[i].data.fd);
                            epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下树
                            break;
                        } else {
                            write(STDOUT_FILENO, buf, 4); // printf依赖于字符串终止符'\0',而write输出指定长度的字符串。
                        }
                    }
                }
            }
        }
    }
    return 0;
}

运行结果:

Linux多路IO复用:epoll文章来源地址https://www.toymoban.com/news/detail-440083.html

到了这里,关于Linux多路IO复用:epoll的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Linux】高级IO --- 多路转接,select,poll,epoll

    所有通过捷径所获取的快乐,无论是金钱、性还是名望,最终都会给自己带来痛苦 1. 后端服务器最常用的网络IO设计模式其实就是Reactor,也称为反应堆模式,Reactor是单进程,单线程的,但他能够处理多客户端向服务器发起的网络IO请求,正因为他是单执行流,所以他的成本就

    2024年02月09日
    浏览(65)
  • Linux多路IO复用技术——epoll详解与一对多服务器实现

    本文详细介绍了Linux中epoll模型的优化原理和使用方法,以及如何利用epoll模型实现简易的一对多服务器。通过对epoll模型的优化和相关接口的解释,帮助读者理解epoll模型的工作原理和优缺点,同时附带代码实现和图解说明。

    2024年02月05日
    浏览(44)
  • 驱动开发,IO多路复用实现过程,epoll方式

    被称为当前时代最好用的io多路复用方式; 核心操作:一棵树(红黑树)、一张表(内核链表)以及三个接口;  思想:(fd代表文件描述符)         epoll要把检测的事件fd挂载到内核空间红黑树上,遍历红黑树,调用每个fd对应的操作方法,找到发生事件的fd,如果没有发

    2024年02月07日
    浏览(54)
  • 【Linux网络编程】TCP并发服务器的实现(IO多路复用select)

    服务器模型主要分为两种, 循环服务器 和 并发服务器 。 循环服务器 : 在同一时间只能处理一个客户端的请求。 并发服务器 : 在同一时间内能同时处理多个客户端的请求。 TCP的服务器默认的就是一个循环服务器,原因是有两个阻塞 accept函数 和recv函数 之间会相互影响。

    2024年02月03日
    浏览(82)
  • 网络编程 IO多路复用 [epoll版] (TCP网络聊天室)

    //head.h            头文件 //TcpGrpSer.c     服务器端 //TcpGrpUsr.c     客户端 通过IO多路复用实现服务器在单进程单线程下可以与多个客户端交互  API epoll函数  head.h TcpGrpSer.c TcpGrpUsr.c  

    2024年02月11日
    浏览(57)
  • 【TCP服务器的演变过程】使用IO多路复用器epoll实现TCP服务器

    手把手教你从0开始编写TCP服务器程序,体验开局一块砖,大厦全靠垒。 为了避免篇幅过长使读者感到乏味,对【TCP服务器的开发】进行分阶段实现,一步步进行优化升级。 本节,在上一章节的基础上,将IO多路复用机制select改为更高效的IO多路复用机制epoll,使用epoll管理每

    2024年01月17日
    浏览(70)
  • 多路转接高性能IO服务器|select|poll|epoll|模型详细实现

    那么这里博主先安利一下一些干货满满的专栏啦! Linux专栏 https://blog.csdn.net/yu_cblog/category_11786077.html?spm=1001.2014.3001.5482 操作系统专栏 https://blog.csdn.net/yu_cblog/category_12165502.html?spm=1001.2014.3001.5482 手撕数据结构 https://blog.csdn.net/yu_cblog/category_11490888.html?spm=1001.2014.3001.5482 去仓库获

    2024年02月15日
    浏览(62)
  • 网络编程 IO多路复用 [select版] (TCP网络聊天室)

    //head.h                 头文件 //TcpGrpSer.c        服务器端 //TcpGrpUsr.c        客户端 select函数  功能:阻塞函数,让内核去监测集合中的文件描述符是否准备就绪,若准备就绪则解除阻塞。 原型: head.h TcpGrpSer.c TcpGrpUsr.c    

    2024年02月14日
    浏览(52)
  • IO多路复用中select的TCP服务器模型和poll服务模型

    服务器端 客户端 poll客户端

    2024年02月12日
    浏览(51)
  • 使用IO多路复用select完成TCP循环服务器接收客户端消息并打印

    服务器       客户端     结果    

    2024年02月12日
    浏览(53)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包