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

大家好,最近在优化我们系统的定时任务模块时,跟 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)才解决。

相关推荐
风曦Kisaki10 小时前
#Linux数据库管理Day06:主从同步与MaxScale读写分离
linux·运维·数据库
小楼昨夜又东风12610 小时前
使用python快速拉包
linux
Tipriest_11 小时前
ubuntu创建和更换当前swap大小
linux·运维·ubuntu
WI8LbH78813 小时前
Ubuntu 部署Harbor
linux·运维·ubuntu
researcher-Jiang13 小时前
高性能计算之MPI:第一次MPI并行程序设计练习
linux·运维·服务器
Wireless_wifi613 小时前
Why Choose IPQ9574 for Your WiFi 7 Solution
linux·人工智能·5g
MYMOTOE614 小时前
国内对标腾讯 WorkBuddy 的桌面 AI 智能体软件大全
linux
小c君tt14 小时前
linux学习笔记1
linux·笔记·学习
RisunJan14 小时前
Linux命令-read(Bash 内建读取输入)
linux
CCPC不拿奖不改名16 小时前
Redis 工程化部署深度解析
linux·服务器·数据库·redis·深度学习·缓存·rag