【Linux系统编程】(三十六)深挖信号保存机制:未决、阻塞与信号集的底层实现全解析


前言

在 Linux 进程信号的生命周期中,"信号保存" 是连接 "信号产生" 与 "信号处理" 的关键桥梁。当信号被操作系统产生后,并不会立即递达给进程执行处理动作 ------ 进程可能正在执行高优先级任务,也可能主动阻塞了该信号。此时,信号会被 "暂存" 起来,直到满足递达条件。

你是否好奇:信号被保存在哪里?进程如何记录 "有信号待处理"?阻塞信号和未决信号有何区别?信号集又是如何管理这些状态的?本文将基于 Linux 内核原理,结合实战代码,从概念解析、内核存储结构、信号集操作函数、实战验证四个维度,深度拆解信号保存的底层逻辑,带你彻底搞懂信号保存的核心机制。


一、信号保存的核心概念:未决、阻塞与递达

在深入底层实现之前,我们必须先理清三个核心概念 ------信号递达信号未决信号阻塞。这三个概念是理解信号保存的基础,也是面试高频考点。

1.1 三大核心概念的通俗解释

我们依然用 "快递" 场景类比,帮助快速理解:

  • 信号递达(Delivery):快递被你成功签收并处理(执行默认 / 忽略 / 自定义动作),是信号生命周期的终点;
  • 信号未决(Pending):快递已到达楼下(信号产生),但你暂时无法取件(如正在开会),快递处于 "等待签收" 状态;
  • 信号阻塞(Block):你提前告知快递员 "暂时不要送货"(进程主动设置阻塞),即使快递已到达,也会一直处于未决状态,直到你取消阻塞。

1.2 三大概念的官方定义与关键区别

1.2.1 信号递达

实际执行信号处理动作的过程,称为信号递达。递达的动作只有三种:

  • 执行默认动作 (如SIGINT默认终止进程);
  • 执行忽略动作(进程收到信号后不做任何操作);
  • 执行自定义动作(调用用户注册的信号处理函数)。

1.2.2 信号未决

信号从产生到递达之间的状态,称为信号未决。此时信号已被操作系统识别并记录在进程的 PCB 中,但由于某些原因(如进程阻塞该信号、进程正在执行高优先级任务),尚未执行处理动作。

1.2.3 信号阻塞

进程可以通过设置**"信号屏蔽字"(Signal Mask)**,主动阻止某个信号的递达。被阻塞的信号产生后,会一直处于未决状态,直到进程解除对该信号的阻塞,才会执行递达动作。

1.3 阻塞与忽略的核心区别(重点!)

很多初学者会混淆 "阻塞" 和 "忽略",但二者本质完全不同:

  • 阻塞是 "不让信号递达":信号处于未决状态,根本不会触发处理动作;
  • 忽略是 "信号已递达,但不处理":信号已经到达进程,只是进程选择不执行任何操作。

举个例子:

  • 阻塞SIGINT信号 :按下Ctrl+C后,信号被保存在 PCB 的未决信号集中,进程完全感知不到该信号,继续正常运行;
  • 忽略SIGINT信号 :按下Ctrl+C后,信号正常递达,但进程执行 "忽略" 动作,不终止也不反馈,继续运行。

1.4 信号保存的核心流程

结合三大概念,信号保存的完整流程可以总结为:

复制代码
信号产生 → 操作系统检查进程是否阻塞该信号 → 
  ① 未阻塞:直接递达并执行处理动作 → 流程结束;
  ② 已阻塞:将信号标记为"未决",保存到进程PCB的未决信号集中 → 
    进程解除阻塞 → 操作系统检测到未决信号 → 信号递达并执行处理动作 → 流程结束

二、内核中的信号保存:PCB 中的关键数据结构

信号的保存本质是操作系统在进程的 PCB(进程控制块)中记录信号的未决状态和阻塞状态 。Linux 内核(2.6.18 版本)中,与信号保存相关的核心数据结构主要有三个:task_structsigset_tsigpending。下面是信号在内核中的表示示意图:

2.1 进程控制块(task_struct)中的信号相关字段

task_struct是 Linux 内核描述进程的核心结构体,其中与信号保存直接相关的字段如下:

cpp 复制代码
struct task_struct {
    // 信号处理动作结构体(存储每个信号的处理函数)
    struct sighand_struct *sighand;
    // 阻塞信号集(信号屏蔽字):标记哪些信号被阻塞
    sigset_t blocked;
    // 未决信号集:标记哪些信号已产生但未递达
    struct sigpending pending;
    // 其他字段...
};

这三个字段的关系可以概括为:

  • blocked:"黑名单",记录进程要阻塞的信号;
  • pending:"待办清单",记录已产生但未递达的信号;
  • sighand:"处理手册",记录每个信号的处理动作(默认 / 忽略 / 自定义)。

2.2 信号集(sigset_t):阻塞与未决状态的存储载体

sigset_t是 Linux 内核定义的 "信号集" 类型,用于存储多个信号的 "有效" 或 "无效" 状态。其本质是一个位图(bitmap),每个 bit 对应一个信号(bit 位的位置对应信号编号,bit 位的值对应状态:1 为有效,0 为无效)。

2.2.1 sigset_t 的底层实现(简化版)

cpp 复制代码
// 不同系统的sigset_t长度不同,通常为32位或64位,对应支持32个或64个信号
typedef struct {
    unsigned long sig[2]; // 假设为64位,支持64个信号(编号1-64)
} sigset_t;
  • 对于阻塞信号集blocked):某个 bit 位为 1,表示该信号被阻塞;
  • 对于未决信号集pending->signal):某个 bit 位为 1,表示该信号已产生且未递达。

例如:

  • blocked的第 2 位(对应SIGINT信号)为 1,表示SIGINT被阻塞;
  • pending->signal的第 3 位(对应SIGQUIT信号)为 1,表示SIGQUIT已产生且未递达。

2.2.2 sigset_t 的核心特点

  • 信号集的操作必须通过内核提供的专用函数(如sigemptysetsigaddset),不能直接修改 bit 位(不同系统的实现可能不同,直接操作会导致兼容性问题);
  • 每个信号在信号集中只有一个 bit 位,因此常规信号(1-33)在递达前产生多次,只会被记录一次(位图无法记录产生次数);
  • 实时信号(34-64)支持排队,会通过sigpending中的链表记录产生次数,本章不讨论。

2.3 未决信号集结构体(sigpending)

sigpending结构体用于存储进程的未决信号,定义如下:

cpp 复制代码
struct sigpending {
    // 链表:用于存储实时信号(支持排队)
    struct list_head list;
    // 位图:用于存储常规信号的未决状态
    sigset_t signal;
};
  • 常规信号的未决状态通过signal字段(sigset_t 类型)记录;
  • 实时信号的未决状态通过list链表记录,支持多次产生的排队处理。

2.4 内核中信号保存的直观示例

假设进程的blockedpending->signalsighand字段如下表所示,我们来分析信号的保存与处理逻辑:

信号 信号编号 blocked(阻塞) pending(未决) 处理动作
SIGHUP 1 0(未阻塞) 0(未产生) SIG_DFL(终止)
SIGINT 2 1(已阻塞) 1(已产生) SIG_IGN(忽略)
SIGQUIT 3 1(已阻塞) 0(未产生) 自定义函数
SIGKILL 9 0(不可阻塞) 0(未产生) SIG_DFL(终止)

分析结果:

  1. **SIGINT**信号已产生,但被阻塞,因此处于未决状态,即使处理动作是 "忽略",也不会递达;
  2. **SIGHUP**信号未产生、未阻塞,若产生则直接递达并执行 "终止" 动作;
  3. **SIGQUIT**信号未产生、已阻塞,若产生则进入未决状态,直到解除阻塞才会执行自定义函数;
  4. **SIGKILL**信号不可阻塞(内核强制规定),若产生则直接递达并执行 "终止" 动作。

三、信号集操作函数:用户态控制信号的阻塞与未决

Linux 内核提供了一组专用函数,用于操作sigset_t类型的信号集(阻塞信号集和未决信号集)。这些函数是用户态程序控制信号保存的核心接口,必须熟练掌握。

3.1 信号集初始化函数:sigemptyset 与 sigfillset

这两个函数用于初始化**sigset_t**变量,任何 sigset_t 变量使用前必须先初始化,否则状态不确定。

3.1.1 sigemptyset:初始化信号集为空(所有 bit 置 0)

cpp 复制代码
#include <signal.h>
// 功能:将set指向的信号集初始化为空,所有信号的对应bit置0
int sigemptyset(sigset_t *set);
  • 返回值:成功返回 0,失败返回 - 1;
  • 用途:初始化一个空的信号集,后续通过sigaddset添加需要操作的信号。

3.1.2 sigfillset:初始化信号集为满(所有 bit 置 1)

cpp 复制代码
#include <signal.h>
// 功能:将set指向的信号集初始化为满,所有信号的对应bit置1
int sigfillset(sigset_t *set);
  • 返回值:成功返回 0,失败返回 - 1;
  • 用途:初始化一个包含所有信号的信号集,后续通过sigdelset删除不需要操作的信号。

实战代码:信号集初始化

cpp 复制代码
#include <iostream>
#include <signal.h>
using namespace std;

int main()
{
    sigset_t set;
    // 初始化信号集为空
    int ret = sigemptyset(&set);
    if (ret == -1)
    {
        perror("sigemptyset failed");
        return 1;
    }
    cout << "信号集初始化为空成功" << endl;

    // 重新初始化为满
    ret = sigfillset(&set);
    if (ret == -1)
    {
        perror("sigfillset failed");
        return 1;
    }
    cout << "信号集初始化为满成功" << endl;

    return 0;
}

编译运行:

bash 复制代码
g++ sigset_init.cpp -o sigset_init
./sigset_init

输出:

复制代码
信号集初始化为空成功
信号集初始化为满成功

3.2 信号集添加 / 删除函数:sigaddset 与 sigdelset

这两个函数用于向信号集中添加或删除某个特定信号。

3.2.1 sigaddset:向信号集中添加一个信号(bit 置 1)

cpp 复制代码
#include <signal.h>
// 功能:将signo对应的信号添加到set指向的信号集中(该信号的bit置1)
int sigaddset(sigset_t *set, int signo);
  • 参数:
    • set:指向要操作的信号集;
    • signo:要添加的信号编号(如 2 对应 SIGINT);
  • 返回值:成功返回 0,失败返回 - 1。

3.2.2 sigdelset:从信号集中删除一个信号(bit 置 0)

cpp 复制代码
#include <signal.h>
// 功能:将signo对应的信号从set指向的信号集中删除(该信号的bit置0)
int sigdelset(sigset_t *set, int signo);
  • 参数与返回值同sigaddset
  • 若信号集原本不包含该信号,删除操作仍视为成功。

实战代码:信号集添加与删除

cpp 复制代码
#include <iostream>
#include <signal.h>
using namespace std;

// 打印信号是否在信号集中
void print_sig_member(sigset_t &set, int signo, const char *sig_name)
{
    int ret = sigismember(&set, signo);
    if (ret == 1)
    {
        cout << sig_name << "(" << signo << "号)在信号集中" << endl;
    }
    else if (ret == 0)
    {
        cout << sig_name << "(" << signo << "号)不在信号集中" << endl;
    }
    else
    {
        perror("sigismember failed");
    }
}

int main()
{
    sigset_t set;
    // 初始化信号集为空
    sigemptyset(&set);
    cout << "初始化后:" << endl;
    print_sig_member(set, SIGINT, "SIGINT");
    print_sig_member(set, SIGQUIT, "SIGQUIT");

    // 向信号集中添加SIGINT(2号)
    int ret = sigaddset(&set, SIGINT);
    if (ret == -1)
    {
        perror("sigaddset SIGINT failed");
        return 1;
    }
    cout << "\n添加SIGINT后:" << endl;
    print_sig_member(set, SIGINT, "SIGINT");
    print_sig_member(set, SIGQUIT, "SIGQUIT");

    // 向信号集中添加SIGQUIT(3号)
    ret = sigaddset(&set, SIGQUIT);
    if (ret == -1)
    {
        perror("sigaddset SIGQUIT failed");
        return 1;
    }
    cout << "\n添加SIGQUIT后:" << endl;
    print_sig_member(set, SIGINT, "SIGINT");
    print_sig_member(set, SIGQUIT, "SIGQUIT");

    // 从信号集中删除SIGINT(2号)
    ret = sigdelset(&set, SIGINT);
    if (ret == -1)
    {
        perror("sigdelset SIGINT failed");
        return 1;
    }
    cout << "\n删除SIGINT后:" << endl;
    print_sig_member(set, SIGINT, "SIGINT");
    print_sig_member(set, SIGQUIT, "SIGQUIT");

    return 0;
}

编译运行:

bash 复制代码
g++ sigset_add_del.cpp -o sigset_add_del
./sigset_add_del

输出:

复制代码
初始化后:
SIGINT(2号)不在信号集中
SIGQUIT(3号)不在信号集中

添加SIGINT后:
SIGINT(2号)在信号集中
SIGQUIT(3号)不在信号集中

添加SIGQUIT后:
SIGINT(2号)在信号集中
SIGQUIT(3号)在信号集中

删除SIGINT后:
SIGINT(2号)不在信号集中
SIGQUIT(3号)在信号集中

3.3 信号集查询函数:sigismember

sigismember函数用于查询某个信号是否在信号集中,是信号集操作中最常用的查询函数。

cpp 复制代码
#include <signal.h>
// 功能:查询signo对应的信号是否在set指向的信号集中
int sigismember(const sigset_t *set, int signo);
  • 返回值:
    • 1:信号在信号集中;
    • 0:信号不在信号集中;
    • -1:调用失败(如信号编号无效)。

3.4 阻塞信号集操作函数:sigprocmask(核心!)

sigprocmask函数是用户态程序修改进程阻塞信号集(信号屏蔽字) 的核心接口,支持 "添加阻塞""解除阻塞""设置阻塞集" 三种操作。

3.4.1 函数原型

cpp 复制代码
#include <signal.h>
// 功能:读取或修改进程的阻塞信号集(信号屏蔽字)
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

3.4.2 参数详解

(1)how:指定修改阻塞信号集的方式,有三种取值:

(2)set:指向要操作的信号集:

  • set为非空指针:根据how参数修改阻塞信号集;
  • set为空指针:不修改阻塞信号集,仅通过oset读取当前阻塞信号集。

(3)oset:用于存储修改前的阻塞信号集:

  • oset为非空指针:将修改前的阻塞信号集保存到oset中,便于后续恢复;
  • oset为空指针:不保存修改前的阻塞信号集。

3.4.3 返回值

  • 成功返回 0;
  • 失败返回 - 1,并设置errno(如EINVAL表示how参数无效)。

3.4.4 关键特性

  • 若调用sigprocmask解除了对某个未决信号的阻塞,则该信号会立即递达 (在sigprocmask返回前);
  • SIGKILL(9 号)和SIGSTOP(19 号)信号不可阻塞,即使通过sigprocmask添加到阻塞信号集,也不会生效。

实战代码 1:设置信号阻塞与解除阻塞

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

// 自定义信号处理函数
void sig_handler(int signo)
{
    cout << "\n捕获到信号:" << signo << "(" << (signo == SIGINT ? "SIGINT" : "SIGQUIT") << "),信号递达成功!" << endl;
}

int main()
{
    cout << "进程PID:" << getpid() << endl;

    // 注册SIGINT和SIGQUIT的处理函数
    signal(SIGINT, sig_handler);
    signal(SIGQUIT, sig_handler);

    sigset_t block_set, old_set;
    // 初始化阻塞信号集为空
    sigemptyset(&block_set);
    sigemptyset(&old_set);

    // 向阻塞信号集中添加SIGINT(2号)和SIGQUIT(3号)
    sigaddset(&block_set, SIGINT);
    sigaddset(&block_set, SIGQUIT);

    // 设置阻塞信号集(SIG_BLOCK:新增阻塞)
    int ret = sigprocmask(SIG_BLOCK, &block_set, &old_set);
    if (ret == -1)
    {
        perror("sigprocmask SIG_BLOCK failed");
        return 1;
    }
    cout << "已阻塞SIGINT和SIGQUIT信号,持续10秒..." << endl;
    cout << "期间按下Ctrl+C(SIGINT)或Ctrl+\\(SIGQUIT),信号会被保存为未决状态" << endl;

    // 睡眠10秒,期间可以按下Ctrl+C或Ctrl+\测试
    sleep(10);

    // 解除阻塞(SIG_SETMASK:恢复为原来的阻塞信号集)
    ret = sigprocmask(SIG_SETMASK, &old_set, NULL);
    if (ret == -1)
    {
        perror("sigprocmask SIG_SETMASK failed");
        return 1;
    }
    cout << "\n已解除阻塞,未决信号会立即递达" << endl;

    // 继续运行,观察信号递达情况
    while (true)
    {
        sleep(1);
        cout << "进程正常运行中..." << endl;
    }

    return 0;
}

编译运行:

bash 复制代码
g++ sigprocmask_block.cpp -o sigprocmask_block
./sigprocmask_block

测试步骤:

  1. 运行程序后,10 秒内按下Ctrl+CCtrl+\
  2. 观察终端:程序不会立即执行信号处理函数,因为信号被阻塞并保存为未决状态;
  3. 10 秒后解除阻塞,未决信号会立即递达,终端打印信号捕获信息。

输出示例:

复制代码
进程PID:12345
已阻塞SIGINT和SIGQUIT信号,持续10秒...
期间按下Ctrl+C(SIGINT)或Ctrl+\(SIGQUIT),信号会被保存为未决状态
^C^\  # 按下Ctrl+C和Ctrl+\,无反应
已解除阻塞,未决信号会立即递达

捕获到信号:2(SIGINT),信号递达成功!

捕获到信号:3(SIGQUIT),信号递达成功!
进程正常运行中...
进程正常运行中...

实战代码 2:验证 SIGKILL 不可阻塞

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

int main()
{
    cout << "进程PID:" << getpid() << endl;

    sigset_t block_set;
    sigemptyset(&block_set);
    // 尝试阻塞SIGKILL(9号)
    sigaddset(&block_set, SIGKILL);

    // 设置阻塞信号集
    int ret = sigprocmask(SIG_BLOCK, &block_set, NULL);
    if (ret == -1)
    {
        perror("sigprocmask failed");
        return 1;
    }
    cout << "尝试阻塞SIGKILL信号(实际无效),持续20秒..." << endl;
    cout << "可以通过另一个终端执行:kill -9 " << getpid() << " 测试" << endl;

    // 睡眠20秒
    sleep(20);

    cout << "进程正常结束(若被kill -9终止,则不会打印此行)" << endl;
    return 0;
}

编译运行:

bash 复制代码
g++ sigprocmask_kill.cpp -o sigprocmask_kill
./sigprocmask_kill

测试步骤:

  1. 运行程序后,打开另一个终端,执行kill -9 进程PID
  2. 观察原终端:程序会立即终止,证明SIGKILL信号不可阻塞。

3.5 未决信号集读取函数:sigpending

sigpending函数用于读取当前进程的未决信号集,让用户态程序可以查询哪些信号已产生但未递达。

3.5.1 函数原型

cpp 复制代码
#include <signal.h>
// 功能:读取当前进程的未决信号集,保存到set指向的变量中
int sigpending(sigset_t *set);
  • 参数:set:指向存储未决信号集的变量;
  • 返回值:成功返回 0,失败返回 - 1。

实战代码:读取并打印未决信号集

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

// 打印未决信号集(遍历1-31号常规信号)
void print_pending(sigset_t &pending)
{
    cout << "当前未决信号集(1-31号):";
    for (int signo = 1; signo <= 31; signo++)
    {
        if (sigismember(&pending, signo))
        {
            cout << signo << " ";
        }
    }
    cout << endl;
}

int main()
{
    cout << "进程PID:" << getpid() << endl;

    sigset_t block_set, pending;
    // 初始化信号集
    sigemptyset(&block_set);
    sigemptyset(&pending);

    // 阻塞SIGINT(2号)和SIGQUIT(3号)
    sigaddset(&block_set, SIGINT);
    sigaddset(&block_set, SIGQUIT);
    sigprocmask(SIG_BLOCK, &block_set, NULL);
    cout << "已阻塞SIGINT(2号)和SIGQUIT(3号),持续15秒..." << endl;
    cout << "期间按下Ctrl+C(2号)或Ctrl+\\(3号),观察未决信号集变化" << endl;

    // 每隔2秒读取并打印一次未决信号集
    for (int i = 0; i < 7; i++)
    {
        // 读取未决信号集
        sigpending(&pending);
        print_pending(pending);
        sleep(2);
    }

    return 0;
}

编译运行:

bash 复制代码
g++ sigpending_print.cpp -o sigpending_print
./sigpending_print

测试步骤:

  1. 运行程序后,在 15 秒内按下Ctrl+C(2 号)和Ctrl+\(3 号);
  2. 观察终端输出:未决信号集会新增 2 和 3 号信号,证明信号被成功保存。

输出示例:

复制代码
进程PID:12346
已阻塞SIGINT(2号)和SIGQUIT(3号),持续15秒...
期间按下Ctrl+C(2号)或Ctrl+\(3号),观察未决信号集变化
当前未决信号集(1-31号):
当前未决信号集(1-31号):
^C  # 按下Ctrl+C
当前未决信号集(1-31号):2 
当前未决信号集(1-31号):2 
^\  # 按下Ctrl+\
当前未决信号集(1-31号):2 3 
当前未决信号集(1-31号):2 3 
当前未决信号集(1-31号):2 3 

四、常见问题与注意事项

4.1 常规信号的未决状态不支持排队

常规信号(1-33 号)在递达前产生多次,只会被记录一次(位图的 bit 位只能是 0 或 1)。例如:

  • 阻塞SIGINT后,连续按下 3 次Ctrl+C,未决信号集中SIGINT的 bit 位仍为 1;
  • 解除阻塞后,信号仅递达一次,处理函数仅执行一次。

验证代码

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

void sigint_handler(int signo)
{
    cout << "捕获到SIGINT信号(" << signo << "号),处理函数执行一次" << endl;
}

int main()
{
    signal(SIGINT, sigint_handler);

    sigset_t block_set;
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGINT);
    sigprocmask(SIG_BLOCK, &block_set, NULL);

    cout << "已阻塞SIGINT,连续按下3次Ctrl+C,然后等待5秒..." << endl;
    sleep(5);

    sigprocmask(SIG_UNBLOCK, &block_set, NULL);
    cout << "已解除阻塞" << endl;

    sleep(3);
    return 0;
}

运行后连续按下 3 次Ctrl+C,解除阻塞后处理函数仅执行一次,证明常规信号不支持排队。

4.2 信号集操作前必须初始化

sigset_t变量未初始化时,内部 bit 位状态不确定,直接调用sigaddsetsigdelset等函数会导致不可预期的结果。因此,任何 sigset_t 变量使用前,必须通过 sigemptyset 或 sigfillset 初始化

4.3 不可阻塞的信号

Linux 中有两个信号无法通过**sigprocmask**阻塞:

  • SIGKILL(9 号):强制终止进程;
  • SIGSTOP(19 号):强制暂停进程。

这是内核的强制规定,目的是保证操作系统能绝对控制进程,避免进程通过阻塞信号变成 "无法管理的僵尸进程"。

4.4 信号处理函数中避免使用不可重入函数

信号处理函数可能在任意时刻被调用,若处理函数中调用了不可重入函数(如mallocprintf、标准 I/O 库函数),可能导致数据错乱或程序崩溃。因此,信号处理函数应尽量简洁,仅执行必要的操作(如设置全局变量、发送通知)。


总结

信号保存是 Linux 信号机制的核心环节,理解其底层原理需要结合内核数据结构和实战验证。本文的所有代码都经过 Ubuntu 20.04 环境验证,建议大家亲手编译运行,通过修改代码(如阻塞不同信号、多次产生信号)加深理解。如果在学习过程中遇到问题,欢迎在评论区留言讨论!

相关推荐
catoop1 小时前
Nginx 解决 upstream sent too big header 错误
运维·nginx
IvanCodes2 小时前
六、Linux核心服务与包管理
linux
ayaya_mana2 小时前
Linux一键部署Docker与镜像加速配置
linux·运维·docker
七夜zippoe2 小时前
模拟与存根实战:unittest.mock深度使用指南
linux·服务器·数据库·python·模拟·高级摸您
市安2 小时前
基于 Alpine 构建轻量 Nginx 错误页面 Docker 镜像
运维·nginx·docker·alpine
bitbot2 小时前
Linux是什麼與如何學習
linux·运维·服务器
哈哈浩丶2 小时前
ATF (ARM Trusted Firmware) -2:完整启动流程(冷启动)
android·linux·arm开发·驱动开发
哈哈浩丶2 小时前
ATF (ARM Trusted Firmware) -3:完整启动流程(热启动)
android·linux·arm开发
杨云龙UP2 小时前
Oracle RMAN 归档日志清理标准流程:CROSSCHECK / EXPIRED / SYSDATE-N
运维·服务器·数据库