Linux —— epoll(1)

目录

[1. epoll 是什么?核心定位?解决什么问题?](#1. epoll 是什么?核心定位?解决什么问题?)

[2. epoll的接口](#2. epoll的接口)

[2.1 epoll_create()](#2.1 epoll_create())

[2.2 epoll_ctl()](#2.2 epoll_ctl())

[2.3 epoll_wait()](#2.3 epoll_wait())

[3. epoll的原理](#3. epoll的原理)

[3.1 自己的理解](#3.1 自己的理解)

[3.2 理性 ----- 源代码 + 重谈 socket 对象](#3.2 理性 ----- 源代码 + 重谈 socket 对象)

[4. 代码](#4. 代码)

[5. epoll 工作模式](#5. epoll 工作模式)

[5.1 LT:水平触发工作模式(Level Triggered)](#5.1 LT:水平触发工作模式(Level Triggered))

[5.2 ET:边缘触发工作模式(Edge Triggered)](#5.2 ET:边缘触发工作模式(Edge Triggered))

[5.3 LT 和 ET 的设计??](#5.3 LT 和 ET 的设计??)

[5.4 LT vs ET](#5.4 LT vs ET)


1. epoll 是什么?核心定位?解决什么问题?

  • epoll 是一个多路复用,多路转接的一种具体方案
  • epoll 核心定位:IO = 等 + 拷贝,一次等待多个fd -> 任意一个或者多个fd就绪,通知用户 -> 通过等待多个fd(手段),通知用户哪些fd就绪了(目的);select / poll / epoll 的核心定位就是:就绪事件通知机制!!!
  • epoll **是为处理大批量句柄(文件描述符)而作了改进的poll。**它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44),它几乎具备了之前所说的⼀切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法

2. epoll的接口

2.1 epoll_create()

int epoll_create(int size);

  • 参数:size:被忽略的,设置上大于 0 的值就行了,对应的就是底层红黑树节点的个数的上限,现在是不需要这个上限的,所以忽略
  • 返回值:成功,返回文件描述符;失败,返回-1;返回值通常代表的是 epoll 模型

2.2 epoll_ctl()

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

epoll_ctl:向指定的文件描述符**(** int epfd ) ,增加、删除 or 修改(int op ),对指定文件描述符上(int fd ),指定事件(struct epoll_event *event )的关心,是一个 **输入型数组!!**是用户告诉内核!!

  • int op 常见有3种:
  • struct epoll_event 的结果如下

events 可以是以下几个宏的集合:

  • EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
  • EPOLLOUT : 表示对应的文件描述符可以写;
  • EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这⾥应该表⽰有带外数据到来);
  • EPOLLERR : 表示对应的文件描述符发⽣错误;
  • EPOLLHUP : 表示对应的文件描述符被挂断;
  • EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
  • EPOLLONESHOT:只监听⼀次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加⼊到EPOLL红⿊树⾥.返回值:

2.3 epoll_wait()

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

参数:

1. int epfd:就是 epoll_create 创建出来的返回值

2. int timeout:同poll一样

  • timeout = 1000,表示1000以内,阻塞等待,没有就绪,直到超时,超时之后,自动进行timeout
  • timeout = -1,代表的是阻塞等待,除非有文件描述符就绪,否则不返回
  • timeout = 0,代表的是非阻塞式等待

3. struct epoll_event *events, int maxevents:是一个输出型数组,是内核告诉用户

返回值:和select、poll一样

  • > 0:有多少个 fd 就绪
  • == 0:超时,没有就绪,也没有报错
  • < 0:select 自己调用出错

3. epoll的原理

3.1 自己的理解

3.2 理性 ----- 源代码 + 重谈 socket 对象

创建 epoll 模型,实际上是创建一个 eventpoll 的数据结构对象。

为什么 epoll_create 的返回值是一个文件描述符???

返回文件描述符也意味着要创建 struct file 对象!!!

所以调用 epoll_ctl 拿着文件描述符 -> 找到文件对象 -> 找到 private_data -> 找到eventpoll 就可以读取红黑树、就绪队列,所以这就是为什么在epoll_ctl()函数中的第一个参数是 fd 的根本原因。

回调机制在哪里???什么时候会被触发???

实际上在内核中创建套接字时,创建 struct file ,创建 struct socket ,创建 tcp_socket 或是 udp_socket ,所以获取连接的时候,accept 内核中得到文件描述符,其实创建tcp套接字,创建 socket,创建struct file,分配文件描述符,返回出去。在底层的任意一层,收到了sock结构体,就有 (*sk_data_ready)(struct sock *sk,int bytes);的回调,就将节点激活了。 struct sock是在最内层的,不管是创建udp还是tcp,sock直接就会有,所以接受和发送缓冲区对于tcp、udp都是存在的。

回调机制是在 struct sock 结构体中,有函数指针,创建对象时,直接初始化成 epoll 模型里面的一个方法,节点一激活,放到就绪队列中。

4. 代码

events 可以是以下几个宏的集合:

  • EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
  • EPOLLOUT : 表示对应的文件描述符可以写;
  • EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这⾥应该表示有带外数据到来);
  • EPOLLERR : 表示对应的文件描述符发⽣错误;
  • EPOLLHUP : 表示对应的文件描述符被挂断;
  • EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
  • EPOLLONESHOT:只监听⼀次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL红黑树里.
cpp 复制代码
// EpollEchoServer.hpp
#pragma once

#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include "Logger.hpp"
#include "Socket.hpp"

const static int gsize = 64;

class EpollServer
{
public:
    EpollServer(uint16_t port)
    :_listensock(std::make_unique<TcpSocket>()),_epfd(-1)
    {
        _listensock->BuildListenSocketMethod(port);  // 3
        _epfd = epoll_create(123);  // 4
        if(_epfd < 0)
        {
            LOG(LogLevel::FATAL) << "epoll_create failed";
            return;
        }
        LOG(LogLevel::INFO) << "Listen sockfd is: " << _listensock->SockFd()
        << " epoll fd is: " << _epfd;
        // 首先要做的是把唯一的一个listensock添加到epoll模型中!!
        // 用户告诉内核,你要帮我关心哪一个fd上面的哪些事件
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = _listensock->SockFd();  // ?
        int n = epoll_ctl(_epfd,EPOLL_CTL_ADD,_listensock->SockFd(),&ev);
        (void)n;

    }
    void Start()
    {
        int timeout = 1000; //同poll
        while(true)
        {
            // 1. 可以直接accept吗?不可以!!accept只需要拷贝,不需要等待
            // 内核告诉用户,哪些fd上面的哪些事件就绪了
            int n = epoll_wait(_epfd,revs,gsize,timeout);
            switch(n)
            {
                case 0:
                    LOG(LogLevel::DEBUG) << "time out......";
                    break;
                case -1:
                    LOG(LogLevel::FATAL) << "epoll_wait filed";
                    break;
                default:
                    // Dispatcher();  // 存在就绪事件,就得派发
                    break;
            }
        }
    }
    ~EpollServer()
    { 
        
    }
private:
    std::unique_ptr<Socket> _listensock;
    int _epfd;
    // 就绪事件放在外面
    struct epoll_event revs[gsize]; //revs:return events; 
};

非阻塞轮询:

cpp 复制代码
int timeout = 0; 

阻塞等待:

cpp 复制代码
int timeout = -1; 
cpp 复制代码
#pragma once

#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include "Logger.hpp"
#include "Socket.hpp"
#include "InetAddr.hpp"

const static int gsize = 64;

class EpollServer
{
public:
    EpollServer(uint16_t port)
        : _listensock(std::make_unique<TcpSocket>()), _epfd(-1)
    {
        _listensock->BuildListenSocketMethod(port); // 3
        _epfd = epoll_create(123);                  // 4
        if (_epfd < 0)
        {
            LOG(LogLevel::FATAL) << "epoll_create failed";
            return;
        }
        LOG(LogLevel::INFO) << "Listen sockfd is: " << _listensock->SockFd()
                            << " epoll fd is: " << _epfd;
        // 首先要做的是把唯一的一个listensock添加到epoll模型中!!
        // 用户告诉内核,你要帮我关心哪一个fd上面的哪些事件
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = _listensock->SockFd(); // ?
        int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->SockFd(), &ev);
        (void)n;
    }

    void Dispatcher()
    {
        LOG(LogLevel::INFO) << "有事件就绪了......";
        // 获取新连接
        InetAddr clientaddr;
        int sockfd = _listensock->Accept(&clientaddr);
        if (sockfd > 0)
        {
            LOG(LogLevel::INFO) << "获取一个新连接, fd: " << sockfd
                                << " 客户端地址是:" << clientaddr.ToString();
            // sockfd ,可以对这个新的连接进行读取吗??不能!!连接建立是三次握手完成,三次握手完成并没有说要跟你建立通信
            // 当前套接字上的数据是否就绪,根本就不知道
            // 应该做什么??将新的sockfd添加到 epoll模型中,本质是红黑树中
            struct epoll_event ev;
            ev.events = EPOLLIN;
            ev.data.fd = sockfd;
            int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
            (void)n;
            LOG(LogLevel::INFO) << "添加新连接到 epoll 中: " << sockfd;
        }
    }

    void Start()
    {
        int timeout = -1; // 同poll
        while (true)
        {
            // 1. 可以直接accept吗?不可以!!accept只需要拷贝,不需要等待
            // 内核告诉用户,哪些fd上面的哪些事件就绪了
            int n = epoll_wait(_epfd, revs, gsize, timeout);
            switch (n)
            {
            case 0:
                LOG(LogLevel::DEBUG) << "time out......";
                break;
            case -1:
                LOG(LogLevel::FATAL) << "epoll_wait filed";
                break;
            default:
                Dispatcher(); // 存在就绪事件,就得派发
                break;
            }
        }
    }
    ~EpollServer()
    {
    }

private:
    std::unique_ptr<Socket> _listensock;
    int _epfd;
    // 就绪事件放在外面
    struct epoll_event revs[gsize]; // revs:return events;
};

上层如果没有处理的话,先是阻塞式的等待,只有有一个连接到了,就会一直处于死循环

cpp 复制代码
    void Dispatcher()
    {
        LOG(LogLevel::INFO) << "有事件就绪了......";
        // 获取新连接
        // InetAddr clientaddr;
        // int sockfd = _listensock->Accept(&clientaddr);
        // if (sockfd > 0)
        // {
        //     LOG(LogLevel::INFO) << "获取一个新连接, fd: " << sockfd
        //                         << " 客户端地址是:" << clientaddr.ToString();
        //     // sockfd ,可以对这个新的连接进行读取吗??不能!!连接建立是三次握手完成,三次握手完成并没有说要跟你建立通信
        //     // 当前套接字上的数据是否就绪,根本就不知道
        //     // 应该做什么??将新的sockfd添加到 epoll模型中,本质是红黑树中
        //     struct epoll_event ev;
        //     ev.events = EPOLLIN;
        //     ev.data.fd = sockfd;
        //     int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
        //     (void)n;
        //     LOG(LogLevel::INFO) << "添加新连接到 epoll 中: " << sockfd;
        // }
    }

为什么会出现这样的情况?

epoll_wait 底层只要有就绪事件,如果没有将就绪事件没有取走 or 即便是取走了,但是没有真正的处理了,底层就会一直触发,进而一直看到的就是死循环。

触发的死循环:

cpp 复制代码
    void Start()
    {
        int timeout = -1; // 同poll
        while (true)
        {
            // 1. 可以直接accept吗?不可以!!accept只需要拷贝,不需要等待
            // 内核告诉用户,哪些fd上面的哪些事件就绪了
            int n = epoll_wait(_epfd, revs, gsize, timeout);
            switch (n)
            {
            case 0:
                LOG(LogLevel::DEBUG) << "time out......";
                break;
            case -1:
                LOG(LogLevel::FATAL) << "epoll_wait filed";
                break;
            default:
                Dispatcher(); // 存在就绪事件,就得派发
                break;
            }
        }
    }
cpp 复制代码
#pragma once

#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include "Logger.hpp"
#include "Socket.hpp"
#include "InetAddr.hpp"

const static int gsize = 64;

class EpollServer
{
public:
    EpollServer(uint16_t port)
        : _listensock(std::make_unique<TcpSocket>()), _epfd(-1)
    {
        _listensock->BuildListenSocketMethod(port); // 3
        _epfd = epoll_create(123);                  // 4
        if (_epfd < 0)
        {
            LOG(LogLevel::FATAL) << "epoll_create failed";
            return;
        }
        LOG(LogLevel::INFO) << "Listen sockfd is: " << _listensock->SockFd()
                            << " epoll fd is: " << _epfd;
        // 首先要做的是把唯一的一个listensock添加到epoll模型中!!
        // 用户告诉内核,你要帮我关心哪一个fd上面的哪些事件
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = _listensock->SockFd(); // 方便后续处理!!
        int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->SockFd(), &ev);
        (void)n;
    }

    void Accepter()
    {
        // 获取新连接
        InetAddr clientaddr;
        int sockfd = _listensock->Accept(&clientaddr);
        if (sockfd > 0)
        {
            LOG(LogLevel::INFO) << "获取一个新连接, fd: " << sockfd
                                << " 客户端地址是:" << clientaddr.ToString();
            // sockfd ,可以对这个新的连接进行读取吗??不能!!连接建立是三次握手完成,三次握手完成并没有说要跟你建立通信
            // 当前套接字上的数据是否就绪,根本就不知道
            // 应该做什么??将新的sockfd添加到 epoll模型中,本质是红黑树中
            struct epoll_event ev;
            ev.events = EPOLLIN;
            ev.data.fd = sockfd;
            int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
            (void)n;
            LOG(LogLevel::INFO) << "添加新连接到 epoll 中: " << sockfd;
        }
    }

    void Recver(int sockfd)
    {
        char buffer[1024]; // bug ! --- 读取的时候,不能保证能将报文读取完,也不能保证读取的就是一个完整的报文
                           // 必须结合协议,序列化、反序列化
        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            // 成功
            buffer[n] = 0;
            std::cout << "Client Say@ " << buffer;
            std::string echo_string = "echo server# ";
            echo_string += buffer;

            // 写?没错,但是不完善
            send(sockfd, echo_string.c_str(), echo_string.size(), 0);
        }
        else if (n == 0)
        {
            // 对端将连接关掉
            LOG(LogLevel::INFO) << "client quit, me too, fd is : " << sockfd;
            // 坑:epoll_ctl EPOLL_CTL_DEL:不能对非法fd进行操作
            int n = epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
            (void)n;
            close(sockfd);
        }
        else
        {
            // 失败
            LOG(LogLevel::INFO) << "recv error, fd is : " << sockfd;
            int n = epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
            (void)n;
            close(sockfd);
        }
    }

    void Dispatcher(int num)
    {
        LOG(LogLevel::INFO) << "有事件就绪了......";
        for (int i = 0; i < num; i++)
        {
            int fd = _revs[i].data.fd;
            uint32_t events = _revs[i].events;

            if (events & EPOLLIN)
            {
                // 1. listensockfd
                if (fd == _listensock->SockFd())
                {
                    Accepter();
                }
                else
                {
                    // 2.普通的sockfd
                    Recver(fd);
                }
            }
            // if(events & EPOLLOUT)
            // {
            //     // TODO
            // }
        }
    }

    void Start()
    {
        int timeout = -1; // 同poll
        while (true)
        {
            // 1. 可以直接accept吗?不可以!!accept只需要拷贝,不需要等待
            // 内核告诉用户,哪些fd上面的哪些事件就绪了
            // 2. epoll_wait 就绪的fd,就绪的事件可是多种多样的!
            int n = epoll_wait(_epfd, _revs, gsize, timeout);
            switch (n)
            {
            case 0:
                LOG(LogLevel::DEBUG) << "time out......";
                break;
            case -1:
                LOG(LogLevel::FATAL) << "epoll_wait filed";
                break;
            default:
                Dispatcher(n); // 存在就绪事件,就得派发
                break;
            }
        }
    }
    ~EpollServer()
    {
    }

private:
    std::unique_ptr<Socket> _listensock;
    int _epfd;
    // 就绪事件放在外面
    struct epoll_event _revs[gsize]; // revs:return events;
};

5. epoll 工作模式

LT 和 ET 是就绪事件的通知机制。

5.1 LT:水平触发工作模式(Level Triggered)

epoll默认状态下就是LT工作模式

  • 当epoll检测到socket上事件就绪的时候, 可以不⽴刻进⾏处理. 或者只处理⼀部分
  • 如上⾯的例⼦, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时,epoll_wait 仍然会⽴刻返回并通知socket读事件就绪.
  • 直到缓冲区上所有的数据都被处理完, epoll_wait 才不会⽴刻返回.
  • 支持阻塞读写和非阻塞读写

5.2 ET:边缘触发工作模式(Edge Triggered)

如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进⼊ET⼯作模式

  • 当epoll检测到socket上事件就绪时, 必须⽴刻处理.
  • 如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第⼆次调⽤ epoll_wait 的时候, epoll_wait 不会再返回了.
  • 也就是说, ET模式下, ⽂件描述符上的事件就绪后, 只有⼀次处理机会
  • ET的性能比LT性能更⾼( epoll_wait 返回的次数少了很多). Nginx默认采⽤ET模式使⽤epoll.
  • 只支持非阻塞的读写

例子,区分上面的两种工作模式:

小帅,在某购物平台上买了5件商品,到了菜鸟驿站,有两个快递员,张三,李四,这天下午,张三将其中3个快递,其余的快递没看到,张三送到小帅的宿舍楼下,小帅在宿舍里面打游戏,张三打电话让小帅下来取快递,但是小帅过来好久都没下去,张三又打电话,让他下来,就这样一直打电话,小帅就下去了,但是其中有一个是大件,先将其余两个取了上去,小帅忘了下面还有了一个快递,就呆在宿舍里面了。此时李四来送小帅剩余的2个快递了,看见张三还在小帅的宿舍楼下等,就将剩余的两个快递给张三,让张三一起给小帅,张三就又给小帅打电话说,你又有两个快递了,快下来取,小帅才去取快递。张三车里只要一直都有小帅的快递,就会一直给小帅打电话。这种就被称为 LT策略,水平触发工作模式

第二天,又有小帅的5个快递了,但是今天的快递员是李四,李四将所有人的快递包括小帅的3个快递拉到小帅的宿舍楼下,打电话给小帅说,小帅快下来取快递,我只打一次电话,你不下来我就走了,小帅听了立马就下去取快递了,看见有一个大件的,就先将大的拿了上去,给李四说,我等下下来取剩下的两个,李四鸟都不鸟他,上去之后,又被舍友拉着打游戏去了,李四将所有人的快递派发完了,此时张三将小帅的剩下的两个快递给李四了,李四再一次打电话,说,小帅你的快递又有两个快下来取,不来的话我就要走了,小帅就下去了,将剩下的所有快递取完了。李四的策略是:通知你,你就要下来,只通知你一次 --- ET模式。从无到有,从有到多,才会通知你一次!!

其实,在通知的过程就是 epoll_wait 的过程,在通知上层哪些事件就绪了!!!

问题1:谁派发快递的效率高一些???李四,为什么??

通知效率高,本质是无效通知少!!!

问题2:你更喜欢谁派发快递的方式???---- 张三

在示波器中,一直处于峰值的横线被称为LT模式,从无到有 或 从右到无变化的称为 ET模式(了解)

5.3 LT 和 ET 的设计??

LT :一个文件描述符节点被激活,通知上层第一次,上层一直再取但是一直没有取完,所以不能将这个文件描述符从就绪队列中移走。

ET :第一次底层有数据,5号文件描述符被激活了,epoll_wait 读取就绪事件,5号节点一旦被epoll_wait 节点拿走,5号节点就会在就绪队列中立即被移除掉,就只在红黑树中存在,从激活状态恢复到非激活状态,当数据新增发生变化,再次被激活。

LT 和 ET 本质就是衡量的是就绪事件,就绪节点在就绪队列中的存活时长的问题。

读写并不会影响红黑树节点的任何状态,只有 epoll_ctl () 对应的add、del、mod 才会修改红黑树,读写只会影响就绪队列。

5.4 LT vs ET

两种模式相比,谁的效率更高??? ---- ET的通知效率高!!

IO方面呢?(通知只是IO方面的一个环节) --- 面试(为什么在ET模式下,将文件描述符设置为非阻塞模式)

ET -> 从无到有,从有到多 -> 才会通知我一次 -> 要求程序员必须一次将本轮数据全部读取完毕 -> 如何保证你把TCP缓冲区本次的数据全部读取完毕!!---- 循环读 - > 最后一次读取完毕,我们往往还要读 -> 因为fd阻塞,recv就会被卡住!!!此时服务器就会被挂起!!!

所以ET模式下,必须把对应的文件描述符设置为非阻塞!!!是为了读取完本轮数据!!!

为什么ET模式下IO会更高呢??

ET 本质是倒逼程序员,按照它的要求把数据读取完毕!上层就会将数据尽快取走,此时ACK应答,就会给发送方通知一个更大的win窗口!!!影响对方的滑动窗口大小!!变得更大,IO效率就更高了!

历史问题:PSH,要求对方内核把数据尽快交给上层!!!交给上层是由用户recv才会把数据读上去,内核能做的就是让对应的fd事件就绪!!!不是说一有数据就调用系统调用,OS会在TCP缓冲区中设置一个高低水位线的概念,低于低水位线,TCP不通知上层,epoll也不通知,超过了才会通知,这样让上层调用一次系统调用拷贝更多的数据,从而减少系统调用次数。而PSH的本质就是:无视高低水位线,只要有数据,立马通知上层。

ET 做的工作 LT 也能做,循环读,将fd设置为非阻塞,但是 LT 本质: 不能倒逼程序员!!!

所以之前的select、poll,获取了一个连接,到来了一个新的事件,但不进行处理,为什么回答while死循环:

因为 epoll 的工作模式默认是 LT !底层事件没有做处理,底层就一直通知,一直返回。select 和 poll 没有工作模式,就变相的是 LT 的工作模式。

相关推荐
2035去旅行6 小时前
bootcmd 详细讲解
linux·嵌入式·arm·soc
陈希1237 小时前
Linux 防火墙配置实战:firewalld、iptables 与云安全组
linux·运维·服务器
女神下凡8 小时前
芯参谋(95):嵌入式文件系统_开发指南
linux·服务器·arm开发·驱动开发·设计规范
且从容.11 小时前
Ubuntu安装flume
linux·ubuntu·flume
承渊政道11 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
且从容.14 小时前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落16 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨16 小时前
WSL2的安装与使用
linux·wsl
微小的鱼16 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng16 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器