硬件看门狗:主板上的倒计时复位电路

背景 homelab 里的 r86s(R86S 小主机,Celeron N5100,跑 Proxmox VE)偶尔整机卡死。卡死后机器既不会自己恢复,也不会留下日志,前几次分别过了约 10.5 小时和 4 小时才被人发现并重启。排查过程见 R86S IRQ 16 nobody cared。 在修不了根因(BIOS 没有更新、microcode 已是最新)的情况下,第一件要做的事是让机器卡死后能自动恢复,这就是硬件看门狗的用途。这篇记录硬件看门狗是什么、Linux 和 Proxmox 怎么用它,以及它在 r86s 这次排查里起的作用。 看门狗是什么 看门狗(watchdog timer,WDT)是一个独立于 CPU 运行的倒计时器: 软件启动看门狗,设一个超时时间,比如 10 秒; 软件定期"喂狗"(kick / pet),每次喂狗把倒计时重置回 10 秒; 如果软件卡死、没人喂狗,倒计时走到 0,看门狗就直接拉复位信号,把整台机器重启。 关键在于独立:倒计时由芯片组里的电路自己走,不依赖 CPU 执行任何指令。CPU 死锁、内核卡死、关了中断,它都照样倒数,照样复位。这一点是软件手段做不到的。 这个思路最早来自嵌入式和工控领域:无人值守的设备死机了没人去按电源键,必须自己恢复。单片机几乎都内置看门狗。 硬件看门狗在哪里 它通常不是一块单独的板卡或模块,而是集成在芯片里的一小块电路。x86 机器上常见的几种: 位置 例子 Linux 驱动 Intel 芯片组(ICH/PCH)里的 TCO 定时器 从 1999 年的 ICH 一直到现在的 PCH,包括 Jasper Lake iTCO_wdt AMD 芯片组(FCH)里的 TCO 定时器 大部分 AMD 平台 sp5100_tco 主板上的 Super I/O 芯片 ITE IT87xx、Nuvoton NCT67xx、Fintek F718xx it87_wdt、w83627hf_wdt、f71808e_wdt 服务器的 BMC IPMI 看门狗 ipmi_watchdog ARM SoC 树莓派的 BCM2835 bcm2835_wdt 所以"几乎所有 x86 主板都有"是成立的,至少芯片组里会有一个 TCO 定时器。但"有"不等于"能用": ...

Created 10106-10-10 · Updated 10106-10-10 · 5 min · 1038 words · -

R86S IRQ 16 nobody cared:HD Audio 控制器引发的共享中断告警

背景 homelab 里的 r86s(R86S 小主机,Celeron N5100,Jasper Lake 平台)跑 Proxmox VE,上面是 K8s 的两台 VM:k8s-50 和 k8s-51,拓扑见 Homelab topology。这台机器偶尔会整机卡死,所以每天 04:00 有一个定时任务自动 apt full-upgrade 加重启。 排查卡死问题时,在内核日志里看到了这样一段: kernel: irq 16: nobody cared (try booting with the "irqpoll" option) kernel: CPU: 0 UID: 0 PID: 47 Comm: kworker/u16:1 Tainted: P O 7.0.14-20-pve #1 kernel: Hardware name: BROUNION R86S/R86S, BIOS 5.19 04/12/2022 kernel: Workqueue: pm pm_runtime_work ... kernel: handlers: kernel: [<...>] idma64_irq [idma64] kernel: [<...>] i2c_dw_isr kernel: [<...>] i801_isr [i2c_i801] kernel: [<...>] sdhci_irq [sdhci] threaded [<...>] sdhci_thread_irq [sdhci] kernel: [<...>] azx_interrupt [snd_hda_codec] kernel: Disabling IRQ #16 这篇记录这条告警是什么意思、根因是什么、影响什么,以及能用哪些 OS 或 BIOS 配置消除它。 ...

Created 10106-10-10 · Updated 10106-10-10 · 6 min · 1182 words · -

CFS, Completely Fair Scheduler 完全公平调度器

CFS(Completely Fair Scheduler,完全公平调度器)是 Linux 普通进程(SCHED_NORMAL/SCHED_BATCH/SCHED_IDLE)曾经使用的调度算法,自 2007 年发布的 Linux 2.6.23 版本起取代了此前的 O(1) 调度器,用红黑树替代了原来的按优先级分桶的数组结构(调度器体系的整体介绍见 Linux 进程调度)。 需要先说明的是:CFS 已经不是当前内核的默认实现。从 2023 年 10 月发布的 Linux 6.6 起,普通进程的默认调度算法换成了 EEVDF,CFS 退居历史。本文仍以 CFS 为主,因为它的 vruntime 和红黑树是理解现在这套调度器的基础。 EEVDF:6.6 之后的默认调度器 EEVDF(Earliest Eligible Virtual Deadline First,最早合格虚拟截止时间优先)由 Peter Zijlstra 实现并在 Linux 6.6 合入,取代 CFS 成为 SCHED_NORMAL 等普通调度策略的默认算法。算法本身并不新,出自 Ion Stoica 和 Hussein Abdel-Wahab 1995 年的一篇论文。 它要解决的是 CFS 的一个短板:CFS 只保证长期的 CPU 时间份额公平,但对"什么时候该轮到我"没有约束,交互式任务的调度延迟不好控制。CFS 时期只能靠 sched_latency_ns 之类的旋钮和一些启发式补偿去调,效果有限。 EEVDF 引入两个概念: lag:进程"应得的 CPU 时间"与"实际获得的 CPU 时间"之差。lag 大于等于 0 说明它还欠着账,才有资格(eligible)被调度;lag 为负说明它已经超额使用,暂时靠边。 virtual deadline:在合格的进程中,按各自请求的时间片长度算出一个虚拟截止时间,调度器每次挑选虚拟截止时间最早的那个。 这样一来,份额公平由 lag 保证,响应延迟由 deadline 保证,需要低延迟的任务可以通过请求更短的时间片来更频繁地被调度,而不必牺牲它的总 CPU 份额。 ...

Created 14146-07-14 · Updated 19196-07-19 · 2 min · 350 words · -

Linux Process Scheduling, 进程调度

进程调度概述 操作系统要实现多进程,进程调度必不可少。进程调度是对 TASK_RUNNING 状态的进程进行调度(进程状态详见 Linux Process State, 进程状态)。如果进程不可执行(正在睡眠或其他),那么它跟进程调度没多大关系。 所以,如果系统负载非常低,盼星星盼月亮才出现一个可执行状态的进程,那么进程调度也就不会太重要,哪个进程可执行就让它执行去,没有什么需要多考虑的。 反之,如果系统负载非常高,时时刻刻都有 N 多个进程处于可执行状态,等待被调度运行,那么进程调度程序为了协调这 N 个进程的执行,必定得做很多工作。协调得不好,系统的性能就会大打折扣,这个时候,进程调度就是非常重要的。 尽管我们平常接触的很多计算机(如桌面系统、网络服务器等)负载都比较低,但是 Linux 作为一个通用操作系统,不能假设系统负载低,必须为应付高负载下的进程调度做精心的设计。 当然,这些设计对于低负载(且没有什么实时性要求)的环境没多大用。极端情况下,如果 CPU 的负载始终保持 0 或 1(永远都只有一个进程或没有进程需要在 CPU 上运行),那么这些设计基本上都是徒劳的。 优先级 现在的操作系统为了协调多个进程的"同时"运行,最基本的手段就是给进程定义优先级。定义了进程的优先级,如果有多个进程同时处于可执行状态,那么谁优先级高谁就去执行,没有什么好纠结的了。 进程的优先级该如何确定呢?有两种方式:由用户程序指定、由内核的调度程序动态调整。Linux 内核将进程分成两个级别:普通进程和实时进程。实时进程的优先级都高于普通进程,除此之外,它们的调度策略也有所不同。 实时进程的调度 实时,原本的涵义是"给定的操作一定要在确定的时间内完成"。重点并不在于操作一定要处理得多快,而是时间要可控(在最坏情况下也不能突破给定的时间)。 这样的"实时"称为"硬实时",多用于很精密的系统之中(比如火箭、导弹之类的)。一般来说,硬实时的系统是相对比较专用的。 像 Linux 这样的通用操作系统显然没法满足这样的要求,中断处理、虚拟内存等机制的存在给处理时间带来了很大的不确定性,硬件的 cache、磁盘寻道、总线争用也会带来不确定性。 比如考虑 i++; 这么一句 C 代码,绝大多数情况下它执行得很快,但是极端情况下还是有这样的可能: i 的内存空间未分配,CPU 触发缺页异常,而 Linux 在缺页异常的处理代码中试图分配内存时,又可能由于系统内存紧缺而分配失败,导致进程进入睡眠; 代码执行过程中硬件产生中断,Linux 进入中断处理程序而搁置当前进程,而中断处理程序的处理过程中又可能发生新的硬件中断,中断永远嵌套不止…… 而像 Linux 这样号称实现了"实时"的通用操作系统,其实只是实现了"软实时",即尽可能地满足进程的实时需求。 如果一个进程有实时需求(它是一个实时进程),则只要它是可执行状态的,内核就一直让它执行,以尽可能地满足它对 CPU 的需要,直到它完成所需要做的事情,然后睡眠或退出(变为非可执行状态)。 而如果有多个实时进程都处于可执行状态,则内核会先满足优先级最高的实时进程对 CPU 的需要,直到它变为非可执行状态。于是,只要高优先级的实时进程一直处于可执行状态,低优先级的实时进程就一直不能得到 CPU;只要一直有实时进程处于可执行状态,普通进程就一直不能得到 CPU。 后来,内核添加了 /proc/sys/kernel/sched_rt_runtime_us 和 /proc/sys/kernel/sched_rt_period_us 两个参数,限定了在以 sched_rt_period_us 为周期的时间内,实时进程最多只能运行 sched_rt_runtime_us 这么多时间,这样就在一直有实时进程处于可执行状态的情况下,给普通进程留了一点点能够得到执行的机会。 那么,如果多个相同优先级的实时进程都处于可执行状态呢?这时就有两种调度策略可供选择: SCHED_FIFO:先进先出,直到先被执行的进程变为非可执行状态,后来的进程才被调度执行。在这种策略下,先来的进程可以调用 sched_yield 系统调用,自愿放弃 CPU,以让权给后来的进程; SCHED_RR:轮转调度,内核为实时进程分配时间片,在时间片用完时,让下一个进程使用 CPU; 强调一下,这两种调度策略仅仅针对于相同优先级的多个实时进程同时处于可执行状态的情况。 ...

Created 10106-02-10 · Updated 14146-07-14 · 3 min · 584 words · -

Linux Process State, 进程状态

Linux(本文以 4.8.4 内核为例)下,进程状态大致有 7 种: 进程状态 说明 TASK_RUNNING 可运行状态。未必正在使用 CPU,也许是在等待调度 TASK_INTERRUPTIBLE 可中断的睡眠状态。正在等待某个条件满足 TASK_UNINTERRUPTIBLE 不可中断的睡眠状态。不会被信号中断 __TASK_STOPPED 暂停状态。收到某种信号,运行被停止 __TASK_TRACED 被跟踪状态。进程停止,被另一个进程跟踪 EXIT_ZOMBIE 僵尸状态。进程已经退出,但尚未被父进程或者 init 进程收尸 EXIT_DEAD 真正的死亡状态 在 include/linux/sched.h 中,进程状态的定义并没有那么少: /* * Task state bitmask. NOTE! These bits are also * encoded in fs/proc/array.c: get_task_state(). * * We have two separate sets of flags: task->state * is about runnability, while task->exit_state are * about the task exiting. Confusing, but this way * modifying one set can't modify the other one by * mistake. */ #define TASK_RUNNING 0 #define TASK_INTERRUPTIBLE 1 #define TASK_UNINTERRUPTIBLE 2 #define __TASK_STOPPED 4 #define __TASK_TRACED 8 /* in tsk->exit_state */ #define EXIT_DEAD 16 #define EXIT_ZOMBIE 32 #define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD) /* in tsk->state again */ #define TASK_DEAD 64 #define TASK_WAKEKILL 128 #define TASK_WAKING 256 #define TASK_PARKED 512 #define TASK_NOLOAD 1024 #define TASK_NEW 2048 #define TASK_STATE_MAX 4096 #define TASK_STATE_TO_CHAR_STR "RSDTtXZxKWPNn" extern char ___assert_task_state[1 - 2*!!( sizeof(TASK_STATE_TO_CHAR_STR)-1 != ilog2(TASK_STATE_MAX)+1)]; /* Convenience macros for the sake of set_task_state */ #define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE) #define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED) #define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED) #define TASK_IDLE (TASK_UNINTERRUPTIBLE | TASK_NOLOAD) /* Convenience macros for the sake of wake_up */ #define TASK_NORMAL (TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE) #define TASK_ALL (TASK_NORMAL | __TASK_STOPPED | __TASK_TRACED) /* get_task_state() */ #define TASK_REPORT (TASK_RUNNING | TASK_INTERRUPTIBLE | \ TASK_UNINTERRUPTIBLE | __TASK_STOPPED | \ __TASK_TRACED | EXIT_ZOMBIE | EXIT_DEAD) TASK_RUNNING TASK_RUNNING 是教科书中两种状态的结合:一种是正在占用 CPU 的 RUNNING 状态,一种是 RUNNING 状态的进程时间片耗尽、或者主动让出 CPU、或者被更高优先级进程抢占后,进入的 READY 状态。处于 TASK_RUNNING 状态的进程要么正在 CPU 上运行,要么随时都可以投入运行,只不过 CPU 资源有限,调度器暂时没有选中它们。 ...

Created 14146-06-14 · Updated 14146-07-14 · 7 min · 1324 words · -

Linux OOM Killer

Linux OOM Killer OOM Killer 会触发业务退出逻辑吗 通常不会。 Linux OOM Killer 选中目标进程后,现代内核通常会直接发送 SIGKILL(9)终止进程。SIGKILL 不能被捕获、阻塞或忽略,因此大多数“优雅退出”逻辑都不会执行,例如: 注册的退出回调(如 atexit) Java shutdown hook 依赖信号捕获的清理代码 所以 OOM 场景应按“强制中断”来设计:关键状态及时落盘、写操作幂等、依赖外部重启拉起,而不是依赖进程退出时做收尾。 业务代码中的退出逻辑由什么触发 通常由“可捕获的退出路径”触发,而不是 OOM Killer 这类强制终止。 常见触发源: 收到可处理信号(如 SIGTERM、SIGINT),应用捕获后进入优雅停机流程 应用主动正常退出(如主流程结束、显式调用 exit) 进程编排系统发起优雅终止(如 systemd、Kubernetes 先发 SIGTERM,宽限期后再决定是否 SIGKILL) 不会触发退出逻辑的典型场景: SIGKILL(包括 OOM Killer) 内核崩溃、断电等非正常中断 因此,退出回调适合做“尽力而为”的清理,不应承载必须完成的关键一致性动作。 一个协议解析服务在生产环境被 OOM Killer 杀进程并引发滚动离线的案例,可参考 物联网平台协议解析服务生产环境 OOM:MySQL 迁移 InfluxDB。 https://blog.csdn.net/GugeMichael/article/details/24017515 Linux - 内存控制之oom killer机制及代码分析 2014年04月18日 15:04:29 阅读数: 28048 最近,线上一些内存占用比较敏感的应用,在访问峰值的时候,偶尔会被kill掉,导致服务重启。发现是Linux的out-of-memory kiiler的机制触发的。 http://linux-mm.org/OOM_Killer oom kiiler会在内存紧张的时候,会依次kill内存占用较高的进程,发送Sig15(SIGTERM)或Sig9(SIGKILL),取决于内核版本(可见uname -a,>= 2.6.32只会发送sigkill https://elixir.free-electrons.com/linux/v2.6.18/source/mm/oom_kill.c),。并在/var/log/message中进行记录。里面会记录一些如pid,process,。并在/var/log/message中进行记录。里面会记录一些如pid,process) name,cpu mask,trace等信息,通过监控可以发现类似问题。今天特意分析了一下oom killer相关的选择机制,挖了一下代码,感觉该机制简单粗暴,不过效果还是挺明显的,给大家分享出来。 oom killer初探 ...

Created 4046-06-04 · Updated 3036-06-03 · 4 min · 801 words · -

kernel thread, 内核线程, KTL

kernel thread, 内核线程, KTL 内核线程, ktl 为什么需要内核线程 Linux 内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求). 内核需要多个执行流并行,为了防止可能的阻塞,支持多线程是必要的. 内核线程就是内核的分身,一个分身可以处理一件特定事情。内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。 这与用户线程是不一样的。因为内核线程只运行在内核态 因此,它只能使用大于P AGE_OFFSET (传统的x86_32上是3G) 的地址空间。 内核线程概述 内核线程是直接由内核本身启动的进程。内核线程实际上是将内核函数委托给独立的进程,它与内核中的其他进程"并行"执行。内核线程经常被称之为内核守护进程。 他们执行下列任务 周期性地将修改的内存页与页来源块设备同步 如果内存页很少使用,则写入交换区 管理延时任务 (Deferred work),如:中断的下半部 实现文件系统的事务日志 内核线程主要有两种类型 线程启动后一直等待,直至内核请求线程执行某一特定操作。 线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制时采取行动。 它们在CPU的管态执行,而不是用户态。 它们只可以访问虚拟地址空间的内核部分 (高于TASK_SIZE的所有地址) ,但不能访问用户空间 内核线程的进程描述符 task_struct task_struct 进程描述符中包含两个跟 进程地址空间 相关的字段 mm, active_mm struct task_struct { // ... struct mm_struct *mm; struct mm_struct *avtive_mm; //... }; 大多数计算机上系统的全部虚拟地址空间分为两个部分: 供用户态程序访问的虚拟地址空间和供内核访问的内核空间。每当内核执行上下文切换时, 虚拟地址空间的用户层部分都会切换, 以便当前运行的进程匹配, 而内核空间不会放生切换。 mm 对于普通用户进程来说,mm 指向虚拟地址空间的用户空间部分,而对于内核线程,mm 为NULL。这为优化提供了一些余地, 可遵循所谓的惰性 TLB 处理(lazy TLB handing)。 active_mm active_mm 主要用于优化,由于内核线程不与任何特定的用户层进程相关,内核并不需要倒换虚拟地址空间的用户层部分,保留旧设置即可。由于内核线程之前可能是任何用户层进程在执行,故用户空间部分的内容本质上是随机的,内核线程决不能修改其内容,故将mm设置为NULL,同时如果切换出去的是用户进程,内核将原来进程的 mm 存放在新内核线程的 active_mm 中,因为某些时候内核必须知道用户空间当前包含了什么。 惰性 TLB 进程 为什么没有 mm 指针的进程称为惰性 TLB 进程? ...

Created 5056-05-05 · Updated 5056-05-05 · 3 min · 596 words · -