内核定时底层核心:时间轮算法

大家好,最近在优化我们系统的定时任务模块时,跟 Linux 时间轮算法杠上了。

说真的,很多人学 Linux 网络编程的时候,会把 epoll、Reactor、协程吹得天花乱坠。可真到了线上,最容易把服务拖死的,往往是"时间"。

是的,就是如何高效管理海量定时任务!

我第一次真正理解 Linux 时间轮算法的时候,有种"卧槽原来还能这样"的感觉。

它不像红黑树那么优雅,也不像跳表那么性感。

但它特别像一个在机房待了十年的老运维:

不讲武德,只讲效率。

今天这篇文章,我想用一个干了很多年后端、被线上事故毒打过无数次的老程序员视角,聊聊 Linux 时间轮算法到底是怎么回事。

看完你会明白:为什么Linux内核选它,为什么Netty在用它,以及------最重要的一点------什么时候千万别用它。

一、为啥要搞时间轮?

定时器的本质很简单,就是告诉系统,"xx毫秒后,帮我干个活"。

很多新人第一次写定时任务的时候,会这么写:

复制代码
while (true) {
    sleep(1);
    check_timeout_tasks();
}

甚至现在不少业务系统还这样。

功能当然能跑。

可问题是------任务一多,系统立刻开始抽风。

举个最经典的场景。

假设你有 100 万个连接。

每个连接都需要:

  • 60 秒心跳检测
  • 超时关闭
  • 延迟重试

你怎么办?

很多人第一反应是:

"那我存个时间戳呗。"

于是代码开始长这样:

复制代码
struct Connection {
    int fd;
    long expire;
};


std::vector<Connection> conns;

然后每秒扫描一次。

复制代码
for (auto &conn : conns) {
    if (conn.expire < now) {
        close(conn.fd);
    }
}

看着是不是还挺合理的?

问题来了。

100 万连接。

你每秒扫一遍。

意味着什么?

意味着 每个时钟中断去遍历百万个节点,CPU直接原地升天。

就比如你开了一个邮局,所有信件都堆在一个大篮子里。来了新信,你得从头翻一遍,找到它该排的位置插进去。来十万封信,你就得翻十万遍------这不是在解决问题,这是在培养耐心。

内核大佬们可没时间这么捣腾。大佬George Varghese和Tony Lauck在SOSP上发表了一篇论文,提出了一个现在来看仍然惊艳的思路------时间轮算法。

核心特别简单:别再用有序链表的思路去折腾了,咱们换个角度,把定时器当成钟表盘上的刻度来管理,时间复杂度是 O(1),也就是说,无论有多少个定时器,添加、删除、触发操作的效率都几乎不变。这不就是我们想要的么?

二、时间轮原理

时间轮算法的核心思想,就像钟表里的齿轮转动。Linux 内核用多级时间轮(也叫分层时间轮)来实现,它由多个层级组成,每个层级都是一个时间轮,就像多个齿轮嵌套在一起。最外层齿轮转一圈,内层齿轮就转一格,依次类推。每个时间轮上有很多槽位,每个槽位挂着一堆定时器。

很多文章一上来就讲:

  • tick
  • slot
  • round
  • cascade
  • hierarchy

看得人头皮发麻,其实核心逻辑并不复杂。

假设现在有一个钟表。

复制代码
0 1 2 3 4 5 6 7 8 9

每秒走一格。

现在有几个任务:

|--------|--------|
| 任务 | 延迟 |
| A | 2 秒 |
| B | 5 秒 |
| C | 12 秒 |

那么:

  • A 放 2
  • B 放 5
  • C 放 2

等等。

C 为什么也放 2?

因为时间轮只有 10 格。

12 % 10 = 2。

那怎么区分 A 和 C?

答案是:

圈数。

也叫 round。

A:

复制代码
2 秒后触发
round = 0

C:

复制代码
12 秒后触发
round = 1

时间轮每转一圈。

round--。

当 round == 0 时。

任务真正执行。

是不是一下就通了?

2.1 单层时间轮

理解时间轮,最好从单层开始。

假设你有一个大小为8的数组,每个位置叫一个"槽位"(bucket)。我们让一个时针每隔1秒走一格。当前指针指向槽i时,就把槽i里挂的所有定时任务统统触发掉。走完一圈8秒,再从头开始------是不是特别像一根秒针在表盘上打转?

复制代码
#define WHEEL_SIZE 256
#define WHEEL_MASK (WHEEL_SIZE - 1)


struct timer_node {
    void (*callback)(void *arg);
    void *arg;
    unsigned long expires;    // 绝对过期时间(单位:tick)
    struct list_head entry;
};


struct timer_wheel {
    struct list_head slots[WHEEL_SIZE];
    unsigned long current_slot;
};

插入任务时,计算它应该落在哪个槽位:slot_index = (expires - now) & WHEEL_MASK。如果expires - now正好等于1个tick,丢到邻位;等于WHEEL_SIZE,还丢在当前位置,但需要带一圈round计数,否则以为马上过期就乱套了。至于round计数怎么带,后面的多级轮子再展开。

插入O(1),删除O(1),这是什么概念?哈希表的定时器版本!

但天下没有免费的午餐------单层时间轮的覆盖范围极其有限。假设每个tick=10ms、256个槽,整圈合计也就2.56秒,超过这个时间范围的定时器就没法直接存了。解决办法要么硬拖多圈(带round),但这样过期检测就不是O(1)了;要么扩容------你试试把WHEEL_SIZE开到65536,直接把整个二级缓存的局部性打没了。

所以啊,单层时间轮特别适合短期、密集、高精度要求不高的场景。像网络重传超时(比如TCP的RTO)、用户态心跳检修,这些落在100ms~10s区间的任务非常香但如果你的业务动不动就要延迟一小时执行任务,用单层时间轮就是给自己找麻烦------要么数组巨大内存吃不消,要么多圈后性能直线下降。

2.2 多级时间轮

Linux这帮大牛当然不会满足于单层的限制。他们搞了一个优雅的五级结构来解决这个矛盾------TV1管近期,TV2到TV5一级级往上覆盖,越往上时间跨度越大牺牲的分辨率也越大。任务超出当前级容量就溢出到上级,上级到期的任务再级联回本级。

复制代码
#define TVR_BITS 8   // 第一级 2^8 = 256
#define TVN_BITS 6   // 第二~五级 2^6 = 64


struct timer_vec_root {
    struct list_head vec[TVR_SIZE];  // 第一级:0-255
};


struct timer_vec {
    struct list_head vec[TVN_SIZE];  // 第二到五级:各64个槽位
};


struct tvec_base {
    struct timer_vec_root tv1;  // 近期定时器
    struct timer_vec tv2;       // 中期定时器
    struct timer_vec tv3;
    struct timer_vec tv4;
    struct timer_vec tv5;       // 最远期定时器
    unsigned long timer_jiffies; // 当前时间指针
};

怎么理解这个结构?假设HZ=1000(每个tick=1ms):

  • TV1(256槽):覆盖0~255ms,精度1ms
  • TV2(64槽×256ms):覆盖256ms~16.383秒
  • TV3(64槽×16.384秒):覆盖约17分钟~18.6小时
  • TV4和TV5逐级推更长时间范围

哪个更有趣?TV2一个槽位打包了TV1整张轮(256ms),TV3一个槽位打包了整个TV2和TV1!这种嵌套设计可以把长达数日的时间跨度压缩到几个不大的数组中,同时保留了O(1)的插入和到期检测性能。

当一个定时器的expires落在256ms~16.383秒之间,它就落到TV2的一个槽里,不需要动用TV3及以上的轮子。等时钟推进到它所在的那一组时,再级联回TV1。有点类似钟表的秒分时联动------秒针走完一圈,分针推一步,拉出下一分内的秒刻度。

级联机制(cascade) 是这套设计的灵魂之处:

复制代码
static int cascade(struct tvec_base *base, struct timer_vec *tv, int index)
{
    struct list_head *list = tv->vec + index;
    struct timer_list *timer;


    // 将该槽的所有定时器重新插入时间轮
    list_for_each_entry_safe(timer, tmp, list, entry) {
        __internal_add_timer(base, timer);  // 重新计算层级
    }
    INIT_LIST_HEAD(list);
    return index;
}

每次时钟中断,timer_jiffies递增,TV1的当前槽被执行并清空。当timer_jiffies增长到TV1边界时,TV2相应槽被触发cascade,把里面的定时器按剩余时间重新塞回TV1对应的槽------这种懒处理的妙处在于,TV2整层都不需要高频扫描,只用时指针到达时一次性迁移。长时任务在绝大多数时间只待在高层数组里"睡大觉",连链表遍历的压力都没有。

我在调网络网关时曾经用单机hotplug压到10万个tcp timer,内核的timer轮几乎感觉不到压力,CPU在0.3%附近波动。要是改用有序链表的话,你说要挂多少。这,就是多级时间轮的威力。

但------敲黑板,这里必须提一嘴------低精度定时器的时间轮精度受HZ限制。HZ=100时,定时器精度只能到10ms,对毫秒级甚至微秒级精度需求完全无能为力。所以后面高精度定时器(hrtimer)子系统引入时,就把时间轮换成了红黑树(timerqueue),因为高精度定时器的到期时间是稀疏且随机分布的,时间轮在面对纳秒级精度需求时,要么需要一个巨大且大部分为空的时间轮(效率极低),要么精度崩坏。两种定时器场景不同,各司其职。

2.3 用户态的时间轮

说完了内核的多级轮子,咱们把目光拉回用户空间。

Java圈有Netty的HashedWheelTimer,C++这边呢?

很多网络库(比如muduo、evpp)自己也手撸过类似的东西。你有没有好奇过"Hashed"这个前缀啥意思?把定时器的deadline当key,用哈希法定位槽位------和内核那个线性推进jiffies的思路不太一样。说白了就是用空间换时间,加个hash的壳,让插入和删除都变成O(1)。

复制代码
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <thread>
#include <vector>
#include <list>
#include <memory>


using TimerTask = std::function<void()>;


struct TimeoutEntry {
    uint64_t deadline;          // 绝对到期时间(ms)
    TimerTask callback;
    // 还可以加cancel标志,为了简单先不写
};


class HashedWheelTimer {
public:
    explicit HashedWheelTimer(int ticksPerWheel = 512,
                              int tickDurationMs = 100)
        : wheel_(ticksPerWheel),
          tickDurationMs_(tickDurationMs),
          currentTick_(0),
          running_(true) {
        worker_ = std::thread(&HashedWheelTimer::workerLoop, this);
    }


    ~HashedWheelTimer() {
        running_ = false;
        cv_.notify_one();
        if (worker_.joinable()) worker_.join();
    }


    // 添加一个延迟任务,delayMs单位毫秒
    void addTimer(uint64_t delayMs, TimerTask task) {
        uint64_t deadline = getCurrentTimeMs() + delayMs;
        // 计算槽位:基于延迟的绝对时间
        int slot = (deadline / tickDurationMs_) % wheel_.size();
        std::lock_guard<std::mutex> lock(mutex_);
        wheel_[slot].push_back({deadline, task});
    }


private:
    std::vector<std::list<TimeoutEntry>> wheel_;
    const int tickDurationMs_;          // 每个tick的毫秒数
    std::atomic<uint64_t> currentTick_; // 当前已经走过的tick数
    std::atomic<bool> running_;
    std::thread worker_;
    std::mutex mutex_;
    std::condition_variable cv_;


    uint64_t getCurrentTimeMs() {
        auto now = std::chrono::steady_clock::now().time_since_epoch();
        return std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
    }


    void workerLoop() {
        auto nextTickTime = std::chrono::steady_clock::now();
        while (running_) {
            // 1. 等待到下一个tick
            {
                std::unique_lock<std::mutex> lock(mutex_);
                cv_.wait_until(lock, nextTickTime, [this] { return !running_; });
            }
            if (!running_) break;


            // 2. 更新当前tick,并处理该槽位的所有任务
            uint64_t tick = currentTick_.fetch_add(1);
            int slot = tick % wheel_.size();


            std::list<TimeoutEntry> bucket;
            {
                std::lock_guard<std::mutex> lock(mutex_);
                bucket.swap(wheel_[slot]);
            }


            uint64_t now = getCurrentTimeMs();
            for (auto& entry : bucket) {
                // 严谨起见,这里可以判断 deadline <= now
                if (entry.deadline <= now + tickDurationMs_) {
                    entry.callback();
                } else {
                    // 理论上不应该发生,多圈任务我们没做级联,先忽略
                    // 后面会讲到多圈怎么办
                }
            }


            // 3. 计算下一个tick的绝对时间
            nextTickTime += std::chrono::milliseconds(tickDurationMs_);
        }
    }
};

默认tickDurationMs_ = 100ms意味着所有定时器的精度最多误差±100ms。绝大多数网络场景其实无所谓------TCP超时重传差个几十毫秒,人根本感觉不到。但如果你拿这玩意儿去做量化交易里的价格推送延迟控制,我劝你善良。

worker是单线程驱动的。假设你某个槽位挂了上千个任务,一个tick里就要把这些回调全部执行完。如果某个回调里不小心写了同步DB查询或者RPC调用------恭喜你,整个时间轮卡死在这一槽。后续所有tick全部延迟,积压的任务越来越多,最后雪崩式崩溃。这不是bug,是算法边界,千万记住。

那有人说了:我把每个回调扔到线程池里异步执行行不行?

行,但注意定时器回调里如果访问共享数据,锁竞争又成了新的瓶颈。

而且异步执行会打乱任务完成的时序------本来希望A任务在B之前完成,线程池一调度全乱了。所以HashedWheelTimer这类设计天生就只适合轻量、短平快的回调,比如设置一个标志位、发一个消息到队列。凡是涉及I/O或复杂计算的,统统交给专门的工作线程去做,时间轮只负责"闹钟响铃"这个动作。

官方文档也好,各路大牛的博客也好,都反复强调这种timer是approximated(近似)的,别指望准时。你要是非要毫秒级精度,把tickDurationMs设成1ms,那新问题又来了:tick频率太高,worker线程大部分时间在空转遍历大量空槽位,CPU先吃不消了。典型的精度和吞吐量不可兼得。

C++社区有没有更优雅的实现?

有。比如asio::steady_timer配合io_context,每个定时器独立管理,但那是基于最小堆的,不适合超大规模并发。还有基于timerfd+epoll驱动的轮子------这个我们后面会专门讲。

说了这么多,核心就一句话:HashedWheelTimer适合高吞吐、低精度的批量超时管理,别拿它当高精度定时器用。

2.4 timerfd:用户态时间轮的终极方案

如果你在Linux用户空间手撸时间轮,最大的痛点是------谁来驱动这个轮子转?

以前很多人写Demo就这么写:

复制代码
while (true) {
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    // 推进时间轮...
}

看起来很自然对不对?

但如果服务跑在1000qps以上的生产环境里,sleep的精度和调度抖动会搞得你怀疑人生。我在排查第一次线上事故时才发现,std::this_thread::sleep_for底层依赖clock_nanosleep,它的实际唤醒时间受进程调度器的延迟影响,高负载下误差经常飙到几毫秒甚至几十毫秒。更致命的是,sleep线程哪怕什么都不干也在持续占用CPU做系统调用。这简直是"活着就已经给对方添麻烦了"的代码典范。

后来翻了大量资料,找到了最正统、最Linux式的方式:timerfd_create + epoll_wait。

timerfd机制把定时器到期变成了文件描述符上的可读事件,直接融入epoll事件循环,完全避免轮询。

核心用法:

复制代码
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
struct itimerspec new_value;
new_value.it_value.tv_sec = interval_sec;
new_value.it_value.tv_nsec = interval_nsec;
timerfd_settime(tfd, 0, &new_value, NULL);


int epfd = epoll_create1(0);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = tfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, tfd, &ev);


while (true) {
    int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
    // tfd可读------定时器已到期
    uint64_t expirations;
    read(tfd, &expirations, sizeof(uint64_t));  // 必须读!清空到期次数
    tick();  // 驱动时间轮推进
}

定时器到期时,内核在定时器fd的内核缓冲区写入一个8字节的整数值,表示到期的次数。epoll检测到EPOLLIN时,必须调用read读走这8字节,否则内核会认为你不想清空,下次不会重新触发。

我手改那个网关方案时换了timerfd+epoll驱动后,定时器的调度精度从±20ms提升到±2ms以内,GC毛刺也大幅减少。关键是不再有一个线程在那里傻乎乎地sleep了------定时事件完全被epoll复用,线程数量原地降级。

但这个方案有个细节大家容易翻车:CLOCK_MONOTONIC。你如果用了CLOCK_REALTIME,NTP校准时系统时间往后或往前突变,绝对时间模式(TFD_TIMER_ABSTIME) 下任务可能永久丢失或瞬间全部触发。别问我怎么知道的╮(╯▽╰)╭。

往期文章推荐

图解 TCP/IP协议,看图秒懂

图解 C/C++ 多线程,看图秒懂

爆肝整理!嵌入式开发必知10种调试手段

【图解】SSH(安全外壳协议)工作原理

👉 【就业避坑】C++ 就业前景全解析:为什么劝退声不断,大厂核心岗仍刚需 C++?

👉 【大厂标准】Linux C/C++ 后端开发系统学习路线

👉 【音视频】音视频流媒体高级开发核心学习路径

👉 【Qt进阶】C++ Qt 桌面 & 嵌入式开发一条龙学习攻略

👉 【内核底层】Linux 内核硬核修炼指南

👉 【面试冲刺】C/C++ 高频八股面试题 1000 题(三)
👉 【项目实战】手撕线程池:C++ 程序员的能力试金石

三、手写一个简单的时间轮

懂了原理,咱们自己动手写一个简化版的时间轮。

假设:

  • 60 个槽
  • 每秒走一次
  • 支持秒级定时任务

3.1 数据结构

复制代码
#include <iostream>
#include <vector>
#include <list>
#include <functional>


using namespace std;


const int SLOT_NUM = 60;


struct TimerTask {
    int rotation;
    int slot;
    function<void()> callback;
};


class TimeWheel {
private:
    vector<list<TimerTask>> slots;
    int current_slot;


public:
    TimeWheel() {
        slots.resize(SLOT_NUM);
        current_slot = 0;
    }
};

其实核心就两个东西:

  • 环形槽位
  • 当前指针

简单得有点不像高性能算法。

3.2 添加任务

复制代码
void add_timer(int timeout, function<void()> cb) {
    int ticks = timeout;


    int rotation = ticks / SLOT_NUM;


    int ts = (current_slot + (ticks % SLOT_NUM)) % SLOT_NUM;


    TimerTask task;
    task.rotation = rotation;
    task.slot = ts;
    task.callback = cb;


    slots[ts].push_back(task);
}

重点看:

复制代码
rotation = ticks / SLOT_NUM

和:

复制代码
slot = (current + offset) % SLOT_NUM

整个时间轮精髓就在这儿。

说白了,就是:

  • 算落哪个格子
  • 算还要转几圈

3.3 时间推进

复制代码
void tick() {
    list<TimerTask> &lst = slots[current_slot];


    for (auto it = lst.begin(); it != lst.end();) {
        if (it->rotation > 0) {
            it->rotation--;
            ++it;
        } else {
            it->callback();
            it = lst.erase(it);
        }
    }


    current_slot = (current_slot + 1) % SLOT_NUM;
}

这个 tick,就是整个时间轮发动机。

每秒调用一次。

然后:

  • 当前槽位任务 round--
  • round 为 0 的执行
  • 指针继续旋转

完事。

没有排序。

没有平衡树。

没有堆调整。

所以它非常快。

四、时间轮面试题

几道常考不衰的时间轮面试题:

第一题:为什么时间轮里每个槽都要用双向链表?

有的人愣半天,说"因为方便遍历"。双向链表的本事在于------它能以O(1)的代价从中间任意位置删除或取消节点。假设定时任务支持cancel()操作,没有双向指针的链表只能从头找到尾才能删除。单层桶里几百个任务一挂,删除一次就是O(n),这谁受得了。在多级时间轮里,跨级迁移和删除尤其需要双向链表的随机删除能力。

第二题:多级时间轮的级联机制如果某槽内有百万任务,会有什么问题?

这里通常没人想过。其实当某层的某个槽里有海量定时器时,触发cascade的那一次tick会一次性转移全部百万个任务到下一级,导致该次tick的CPU耗时飙高,后续所有调度都跟着延迟。这不是算法崩溃,而是延迟尖刺。解决方案:如果业务特征里确实可能聚集到百万个任务同时落在同一槽,可以考虑在高层级槽使用延迟队列加分批迁移的策略,而不是一次性全搬。

第三题:epoll_wait的timeout参数可以替代时间轮吗?

这是老有人偷懒的"骚操作"------epoll_wait(epfd, events, MAX_EVENTS, 100)直接当定时器用。但epoll的超时精度最差能到1ms级,且它的设计目标是等待I/O事件,不是管理大量并发定时器。你怎么给上万个连接设各自不同的超时?定时器链表里还得额外维护到期时间,对epoll_timeout来说是做不到的。所以时间轮的核心价值在于:管理大量独立超时源的O(1)能力,epoll_timeout做不到这一点。

第四题:Redis 为什么没直接用时间轮?

很多面试官会问,Redis有大量过期key,为什么不用时间轮?因为Redis场景不同。Redis key过期时间跨度太随机,而且数量巨大、时间分布离散、精度要求不高。于是Redis采用惰性删除加定期随机扫描。本质上,它在空间和CPU之间找了平衡。所以技术方案永远没有银弹,一定是场景驱动。别看到一个新技术就想everywhere。

第五题:为什么很多公司最后还是不用时间轮?

因为很多文章会让你误以为"时间轮天下无敌",其实并不是。时间轮有几个硬伤:精度有限 ------tick越大误差越大,tick越小CPU开销越大,这是天然的tradeoff。不适合超高精度场景 ,比如高频交易、实时控制、音视频同步,这些更偏向hrtimer。超长延迟管理复杂 ------虽然多层时间轮能解决,但实现复杂度会上升,而且cascade本身也有成本。调试困难,这点特别真实。线上timer出问题时,你会发现根本不知道它在哪层、哪一圈、什么时候触发,日志都不好打。

五、什么时候千万别用时间轮

时间轮,优雅,高效,但一定要锚定场景。

你拿它做双11秒杀的延迟库存释放集群没问题。拿它做证券交易中价位和波动率依赖的精准调度,一个100ms的偏差就能让策略模型直接失效。精度不够就是精度不够,算法再美也得选对场景。

Linux内核为什么时间轮和高精度定时器红黑树同时存在?因为它们服务的精度域不同------时间轮面向高吞吐、批量粗粒度的定时任务;hrtimer面向低延迟、纳秒级精度要求。你需要知道自己面对的是哪类问题。

这几年代码从多级轮子改到timerqueue,又改到基于timerfd的多级离散版本,我发现一个规律------写代码总是在"整洁的抽象"和"残酷的现实"之间反复摩擦。10ms的精度差别也许在代码里只是一行的差异,在业务上可能是一个重度游戏玩家的暴怒退游。所以,永远不要被算法的美迷惑了双眼,扎实用好tickDuration、慎用绝对时间模式,并且------务必把监控配起来。监控tick间隔偏离、pending timeout的数量、cascade耗时这些指标,别等到线上崩了再去补。

六、避坑点

坑 1:时间精度问题

系统时钟滴答的精度影响时间轮精度。如果滴答是 10 毫秒,那你设置的 5 毫秒定时器肯定不准。我们之前有个实时监控任务,要求 1 毫秒精度,结果发现系统滴答是 10 毫秒,只能手动修改内核配置,把 HZ 从 100 调到 1000,这才搞定。

坑 2:定时器过期处理

如果任务执行时间超过到期时间,比如定时器 A 应该在 10:00:00 触发,但执行到 10:00:05 才完成,那后面的定时器就全乱套了!我们当时的解决办法是:任务执行时,先检查当前时间是否已经过了下一个到期点,如果过了就重新入队,确保不会积压。

坑 3:高并发下的锁竞争

在多线程环境下,添加和删除定时器需要加锁。但锁太粗会导致性能暴跌。我们后来用了细粒度锁,给每个时间轮槽位加独立的锁,把性能提升了 5 倍!

坑 4:内存管理陷阱

大量创建和删除定时器会导致内存碎片。我们曾经遇到过系统运行几天后,内存碎片率飙升到 90%,最后改用内存池(比如 tcmalloc)才解决。

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